Coverage for /home/crpier/Projects/snektest/snektest/models.py: 59%
92 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 00:30 +0300
1from dataclasses import dataclass
2from enum import Enum, auto
3from io import StringIO
4from itertools import product
5from pathlib import Path
6from types import TracebackType
7from typing import Any
10class CollectionError(BaseException): ...
13class ArgsError(BaseException): ...
16class UnreachableError(BaseException): ...
19class BadRequestError(BaseException):
20 """When user didn't write test code correctly"""
23class AssertionFailure(AssertionError): # noqa: N818
24 def __init__(
25 self,
26 message: str,
27 *,
28 actual: Any = None,
29 expected: Any = None,
30 operator: str | None = None,
31 ) -> None:
32 super().__init__(message)
33 self.actual = actual
34 self.expected = expected
35 self.operator = operator
38SnektestError = CollectionError | ArgsError | UnreachableError | AssertionFailure
41class FilterItem:
42 """Represents a single test filter item"""
44 def __init__(self, raw_input: str) -> None:
45 if "::" not in raw_input:
46 path = Path(raw_input)
47 function_name = None
48 params = None
49 else:
50 file_part, rest = raw_input.split("::", 1)
51 if rest == "":
52 msg = f"Invalid test filter - nothing given after semicolon in '{raw_input}'"
53 raise ArgsError(msg)
55 path = Path(file_part)
57 if "[" in rest:
58 if not rest.endswith("]"):
59 msg = f"Invalid test filter - unterminated `[` in '{raw_input}'"
60 raise ArgsError(msg)
61 rest = rest.removesuffix("]")
62 function_name, params = rest.split("[", 1)
63 else:
64 function_name = rest
65 params = None
67 if not path.exists():
68 msg = f"Invalid test filter - provided path does not exist in '{raw_input}'"
69 raise ArgsError(msg)
71 if path.is_file() and path.suffix != ".py":
72 msg = f"Invalid test filter - file is not a Python script in '{raw_input}'"
73 raise ArgsError(msg)
75 if path.is_file() and not path.name.startswith("test_"):
76 msg = (
77 f"Invalid test filter - file does not start with _test in '{raw_input}'"
78 )
79 raise ArgsError(msg)
81 if function_name is not None and not function_name.isidentifier():
82 msg = f"Invalid test filter - invalid identifier {function_name} in '{raw_input}'"
83 raise ArgsError(msg)
85 self.file_path = path
86 self.function_name = function_name
87 self.params = params
89 def __str__(self) -> str:
90 result = str(self.file_path)
91 if self.function_name is not None:
92 result += f"::{self.function_name}"
93 if self.params:
94 result += f"[{self.params}]"
95 return result
97 def __repr__(self) -> str:
98 return f"FilterItem(file_path={self.file_path!r}, function_name={self.function_name!r}, params={self.params!r})"
101# Set kw_only so we can write attributes in the order they appear
102@dataclass(kw_only=True)
103class TestName:
104 file_path: Path
105 func_name: str
106 params_part: str
108 def __str__(self) -> str:
109 result = str(self.file_path)
110 result += f"::{self.func_name}"
111 if self.params_part:
112 result += f"[{self.params_part}]"
113 return result
116class PassedResult: ...
119@dataclass
120class Param[T]:
121 value: T
122 name: str
124 @staticmethod
125 def to_dict(
126 params: tuple[list[Param[Any]], ...],
127 ) -> dict[str, tuple[Param[Any], ...]]:
128 """Create a dictionary that contains all possible params combinations.
130 For tests with no parameters, returns {"": ()} to ensure the test runs once.
131 """
132 if not params:
133 return {"": ()}
135 combinations = product(*params)
136 result: dict[str, tuple[Param[Any], ...]] = {}
137 for combination in combinations:
138 result[", ".join([param.name for param in combination])] = combination
139 return result
142class Scope(Enum):
143 FUNCTION = auto()
144 SESSION = auto()
147@dataclass(frozen=True)
148class FailedResult:
149 exc_type: type[BaseException]
150 exc_value: BaseException
151 traceback: TracebackType
154@dataclass(frozen=True)
155class ErrorResult:
156 exc_type: type[BaseException]
157 exc_value: BaseException
158 traceback: TracebackType
161@dataclass(frozen=True)
162class TeardownFailure:
163 """Represents a fixture teardown failure"""
165 fixture_name: str
166 exc_type: type[BaseException]
167 exc_value: BaseException
168 traceback: TracebackType
171@dataclass
172class TestResult:
173 name: TestName
174 duration: float
175 result: PassedResult | FailedResult | ErrorResult
176 markers: tuple[str, ...]
177 captured_output: StringIO
178 fixture_teardown_failures: list[TeardownFailure]
179 fixture_teardown_output: str | None
180 warnings: list[str]