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

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 

8 

9 

10class CollectionError(BaseException): ... 

11 

12 

13class ArgsError(BaseException): ... 

14 

15 

16class UnreachableError(BaseException): ... 

17 

18 

19class BadRequestError(BaseException): 

20 """When user didn't write test code correctly""" 

21 

22 

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 

36 

37 

38SnektestError = CollectionError | ArgsError | UnreachableError | AssertionFailure 

39 

40 

41class FilterItem: 

42 """Represents a single test filter item""" 

43 

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) 

54 

55 path = Path(file_part) 

56 

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 

66 

67 if not path.exists(): 

68 msg = f"Invalid test filter - provided path does not exist in '{raw_input}'" 

69 raise ArgsError(msg) 

70 

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) 

74 

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) 

80 

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) 

84 

85 self.file_path = path 

86 self.function_name = function_name 

87 self.params = params 

88 

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 

96 

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})" 

99 

100 

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 

107 

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 

114 

115 

116class PassedResult: ... 

117 

118 

119@dataclass 

120class Param[T]: 

121 value: T 

122 name: str 

123 

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. 

129 

130 For tests with no parameters, returns {"": ()} to ensure the test runs once. 

131 """ 

132 if not params: 

133 return {"": ()} 

134 

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 

140 

141 

142class Scope(Enum): 

143 FUNCTION = auto() 

144 SESSION = auto() 

145 

146 

147@dataclass(frozen=True) 

148class FailedResult: 

149 exc_type: type[BaseException] 

150 exc_value: BaseException 

151 traceback: TracebackType 

152 

153 

154@dataclass(frozen=True) 

155class ErrorResult: 

156 exc_type: type[BaseException] 

157 exc_value: BaseException 

158 traceback: TracebackType 

159 

160 

161@dataclass(frozen=True) 

162class TeardownFailure: 

163 """Represents a fixture teardown failure""" 

164 

165 fixture_name: str 

166 exc_type: type[BaseException] 

167 exc_value: BaseException 

168 traceback: TracebackType 

169 

170 

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]