Skip to content

Commit

Permalink
#12: Add tests for violation codes and docs (#711)
Browse files Browse the repository at this point in the history
  • Loading branch information
nifadyev authored Oct 12, 2024
1 parent 0fee69a commit cb170aa
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 12 deletions.
2 changes: 1 addition & 1 deletion dotenv_linter/violations/assigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from typing import final
from typing_extensions import final

from dotenv_linter.violations.base import BaseFSTViolation

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/violations/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import final
from typing_extensions import final

from dotenv_linter.grammar.fst import Node

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/violations/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from typing import final
from typing_extensions import final

from dotenv_linter.violations.base import BaseFSTViolation

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/violations/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""

from typing import final
from typing_extensions import final

from dotenv_linter.violations.base import BaseFSTViolation

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/violations/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from typing import final
from typing_extensions import final

from dotenv_linter.violations.base import BaseFileViolation

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/violations/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"""

from typing import final
from typing_extensions import final

from dotenv_linter.violations.base import BaseFSTViolation

Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/visitors/fst/assigns.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import final
from typing_extensions import final

from dotenv_linter.grammar.fst import Assign
from dotenv_linter.violations.assigns import SpacedAssignViolation
Expand Down
2 changes: 1 addition & 1 deletion dotenv_linter/visitors/fst/comments.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import final
from typing_extensions import final

from dotenv_linter.grammar.fst import Comment
from dotenv_linter.violations.comments import SpacedCommentViolation
Expand Down
4 changes: 3 additions & 1 deletion dotenv_linter/visitors/fst/values.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Final, final
from typing import Final

from typing_extensions import final

from dotenv_linter.grammar.fst import Value
from dotenv_linter.violations.values import (
Expand Down
32 changes: 29 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import inspect
from collections.abc import Callable
from operator import itemgetter
from pathlib import PurePath
from types import ModuleType
from typing import Dict, List

import pytest
from typing_extensions import TypeAlias

from dotenv_linter import violations
from dotenv_linter.violations.base import (
Expand All @@ -11,6 +15,8 @@
BaseViolation,
)

AllViolationsType: TypeAlias = Dict[ModuleType, List[BaseViolation]]


def _is_violation_class(cls) -> bool:
base_classes = {
Expand All @@ -24,7 +30,7 @@ def _is_violation_class(cls) -> bool:
return issubclass(cls, BaseViolation) and cls not in base_classes


def _load_all_violation_classes():
def _load_all_violation_classes() -> AllViolationsType:
modules = [
violations.assigns,
violations.comments,
Expand All @@ -42,7 +48,7 @@ def _load_all_violation_classes():


@pytest.fixture(scope='session')
def all_violations():
def all_violations() -> List[BaseViolation]:
"""Loads all violations from the package."""
classes = _load_all_violation_classes()
all_errors_container = []
Expand All @@ -51,9 +57,29 @@ def all_violations():
return all_errors_container


@pytest.fixture(scope='session')
def all_module_violations() -> AllViolationsType:
"""Loads all violations from the package."""
return _load_all_violation_classes()


@pytest.fixture()
def fixture_path():
def fixture_path() -> Callable[[str], str]:
"""Returns path to the fixture."""
def factory(path: str) -> str:
return str(PurePath(__file__).parent.joinpath('fixtures', path))
return factory


@pytest.fixture(scope='session')
def all_violation_codes(
all_module_violations: AllViolationsType, # noqa: WPS442
) -> Dict[ModuleType, Dict[int, BaseViolation]]:
"""Loads all codes and their violation classes from the package."""
return {
module: {
violation.code: violation
for violation in all_module_violations[module]
}
for module in all_module_violations.keys()
}
41 changes: 41 additions & 0 deletions tests/test_violations/test_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from collections import Counter
from typing import Dict, List

from dotenv_linter.violations.base import BaseViolation


def test_all_unique_violation_codes(
all_violations: List[BaseViolation],
) -> None:
"""Ensures that all violations have unique violation codes."""
codes = [int(violation.code) for violation in all_violations]

assert len(set(codes)) == len(all_violations)


def test_all_violations_are_final(all_violations: List[BaseViolation]) -> None:
"""Ensures that all violations are final."""
for violation_type in all_violations:
assert getattr(violation_type, '__final__', False), violation_type


def test_all_unique_violation_messages(
all_violations: List[BaseViolation],
) -> None:
"""Ensures that all violations have unique violation messages."""
messages = Counter([
violation.error_template for violation in all_violations
])
for message, count in messages.items():
assert count == 1, message


def test_no_holes(all_violation_codes: Dict) -> None:
"""Ensures that there are no off-by-one errors."""
for module_codes in all_violation_codes.values():
previous_code = None
for code in sorted(module_codes.keys()):
if previous_code is not None:
diff = code - previous_code
assert diff == 1 or diff > 2, module_codes[code].__qualname__
previous_code = code
41 changes: 41 additions & 0 deletions tests/test_violations/test_definition_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import inspect
import re
from types import ModuleType
from typing import Dict, List, Tuple

from dotenv_linter.violations.base import BaseViolation


def _get_sorted_classes(
classes: List[BaseViolation],
) -> Tuple[List[BaseViolation], List[BaseViolation]]:
sorted_by_code = sorted(classes, key=lambda cl: cl.code)
sorted_by_source = sorted(
classes,
key=lambda cl: inspect.findsource(cl)[1],
)

return sorted_by_code, sorted_by_source


def test_violation_source_order(
all_module_violations: Dict[ModuleType, List[BaseViolation]],
) -> None:
"""Used to force violations order inside the source code."""
for _, classes in all_module_violations.items():
sorted_by_code, sorted_by_source = _get_sorted_classes(classes)

assert sorted_by_code == sorted_by_source


def test_violation_autoclass_order(
all_module_violations: Dict[ModuleType, List[BaseViolation]],
) -> None:
"""Used to force violations order inside the `autoclass` directives."""
for module, classes in all_module_violations.items():
sorted_by_code, _ = _get_sorted_classes(classes)
pattern = re.compile(r'\.\.\sautoclass::\s(\w+)')
sorted_by_autoclass = pattern.findall(module.__doc__)
sorted_by_code = [cl.__qualname__ for cl in sorted_by_code]

assert sorted_by_code == sorted_by_autoclass
47 changes: 47 additions & 0 deletions tests/test_violations/test_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from types import ModuleType
from typing import Dict, List

from dotenv_linter.violations.base import BaseViolation


def test_all_violations_are_documented(
all_module_violations: Dict[ModuleType, List[BaseViolation]],
) -> None:
"""Ensures that all violations are documented."""
for module, classes in all_module_violations.items():
for violation_class in classes:
# Once per `autoclass`
assert module.__doc__.count(violation_class.__qualname__) == 1


def test_all_violations_have_versionadded(
all_violations: List[BaseViolation],
) -> None:
"""Ensures that all violations have `versionadded` tag."""
for violation in all_violations:
assert '.. versionadded:: ' in violation.__doc__


def test_violation_name(all_violations: List[BaseViolation]) -> None:
"""Ensures that all violations have `Violation` suffix."""
for violation in all_violations:
class_name = violation.__qualname__
assert class_name.endswith('Violation'), class_name


def test_violation_template_ending(all_violations: List[BaseViolation]) -> None:
"""Ensures that all violation templates do not end with a dot."""
for violation in all_violations:
assert not violation.error_template.endswith('.'), violation


def test_previous_codes_versionchanged(
all_violations: List[BaseViolation],
) -> None:
"""Tests that we put both in case violation changes."""
for violation in all_violations:
previous_codes = getattr(violation, 'previous_codes', None)
if previous_codes is not None:
assert violation.__doc__.count(
'.. versionchanged::',
) >= len(violation.previous_codes)

0 comments on commit cb170aa

Please sign in to comment.