Coverage for tests / unit / parsers / test_base_parser.py: 99%

175 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Tests for lintro.parsers.base_parser module.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6 

7import pytest 

8from assertpy import assert_that 

9 

10from lintro.parsers.base_issue import BaseIssue 

11from lintro.parsers.base_parser import ( 

12 collect_continuation_lines, 

13 extract_dict_field, 

14 extract_int_field, 

15 extract_str_field, 

16 safe_parse_items, 

17 strip_ansi_codes, 

18 validate_int_field, 

19 validate_str_field, 

20) 

21 

22 

23def test_extract_int_field_first_candidate() -> None: 

24 """extract_int_field returns value from first matching candidate.""" 

25 data: dict[str, object] = {"line": 10, "row": 20} 

26 result = extract_int_field(data, ["line", "row"]) 

27 assert_that(result).is_equal_to(10) 

28 

29 

30def test_extract_int_field_second_candidate() -> None: 

31 """extract_int_field falls back to second candidate.""" 

32 data: dict[str, object] = {"row": 20} 

33 result = extract_int_field(data, ["line", "row"]) 

34 assert_that(result).is_equal_to(20) 

35 

36 

37def test_extract_int_field_default() -> None: 

38 """extract_int_field returns default when no match.""" 

39 data: dict[str, object] = {"other": 5} 

40 result = extract_int_field(data, ["line", "row"], default=0) 

41 assert_that(result).is_equal_to(0) 

42 

43 

44def test_extract_int_field_none_default() -> None: 

45 """extract_int_field returns None default.""" 

46 data: dict[str, object] = {} 

47 result = extract_int_field(data, ["line"]) 

48 assert_that(result).is_none() 

49 

50 

51def test_extract_int_field_excludes_bool() -> None: 

52 """extract_int_field excludes boolean values.""" 

53 data: dict[str, object] = {"line": True} 

54 result = extract_int_field(data, ["line"], default=0) 

55 assert_that(result).is_equal_to(0) 

56 

57 

58def test_extract_str_field_first_candidate() -> None: 

59 """extract_str_field returns value from first matching candidate.""" 

60 data: dict[str, object] = {"filename": "test.py", "file": "other.py"} 

61 result = extract_str_field(data, ["filename", "file"]) 

62 assert_that(result).is_equal_to("test.py") 

63 

64 

65def test_extract_str_field_second_candidate() -> None: 

66 """extract_str_field falls back to second candidate.""" 

67 data: dict[str, object] = {"file": "test.py"} 

68 result = extract_str_field(data, ["filename", "file"]) 

69 assert_that(result).is_equal_to("test.py") 

70 

71 

72def test_extract_str_field_default() -> None: 

73 """extract_str_field returns default when no match.""" 

74 data: dict[str, object] = {"other": "value"} 

75 result = extract_str_field(data, ["filename", "file"], default="unknown") 

76 assert_that(result).is_equal_to("unknown") 

77 

78 

79def test_extract_str_field_empty_default() -> None: 

80 """extract_str_field returns empty string default.""" 

81 data: dict[str, object] = {} 

82 result = extract_str_field(data, ["filename"]) 

83 assert_that(result).is_equal_to("") 

84 

85 

86def test_extract_dict_field_first_candidate() -> None: 

87 """extract_dict_field returns value from first matching candidate.""" 

88 data: dict[str, object] = {"location": {"line": 1}, "start": {"row": 2}} 

89 result = extract_dict_field(data, ["location", "start"]) 

90 assert_that(result).is_equal_to({"line": 1}) 

91 

92 

93def test_extract_dict_field_second_candidate() -> None: 

94 """extract_dict_field falls back to second candidate.""" 

95 data: dict[str, object] = {"start": {"row": 2}} 

96 result = extract_dict_field(data, ["location", "start"]) 

97 assert_that(result).is_equal_to({"row": 2}) 

98 

99 

100def test_extract_dict_field_default() -> None: 

101 """extract_dict_field returns default when no match.""" 

102 data: dict[str, object] = {"other": "value"} 

103 result = extract_dict_field(data, ["location"], default={"line": 0}) 

104 assert_that(result).is_equal_to({"line": 0}) 

105 

106 

107def test_extract_dict_field_empty_default() -> None: 

108 """extract_dict_field returns empty dict default.""" 

109 data: dict[str, object] = {} 

110 result = extract_dict_field(data, ["location"]) 

111 assert_that(result).is_equal_to({}) 

112 

113 

114def test_strip_ansi_codes_removes_color() -> None: 

115 """strip_ansi_codes removes color codes.""" 

116 text = "\x1b[31mError\x1b[0m: message" 

117 result = strip_ansi_codes(text) 

118 assert_that(result).is_equal_to("Error: message") 

119 

120 

121def test_strip_ansi_codes_plain_text() -> None: 

122 """strip_ansi_codes returns plain text unchanged.""" 

123 text = "plain text without codes" 

124 result = strip_ansi_codes(text) 

125 assert_that(result).is_equal_to("plain text without codes") 

126 

127 

128def test_strip_ansi_codes_multiple_codes() -> None: 

129 """strip_ansi_codes handles multiple ANSI codes.""" 

130 text = "\x1b[1m\x1b[32mSuccess\x1b[0m: \x1b[34minfo\x1b[0m" 

131 result = strip_ansi_codes(text) 

132 assert_that(result).is_equal_to("Success: info") 

133 

134 

135def test_strip_ansi_codes_empty_string() -> None: 

136 """strip_ansi_codes handles empty string.""" 

137 result = strip_ansi_codes("") 

138 assert_that(result).is_equal_to("") 

139 

140 

141def test_validate_str_field_valid_string() -> None: 

142 """validate_str_field returns string value.""" 

143 result = validate_str_field("test", "field") 

144 assert_that(result).is_equal_to("test") 

145 

146 

147def test_validate_str_field_non_string_returns_default() -> None: 

148 """validate_str_field returns default for non-string.""" 

149 result = validate_str_field(123, "field", default="default") 

150 assert_that(result).is_equal_to("default") 

151 

152 

153def test_validate_str_field_none_returns_default() -> None: 

154 """validate_str_field returns default for None.""" 

155 result = validate_str_field(None, "field", default="default") 

156 assert_that(result).is_equal_to("default") 

157 

158 

159def test_validate_int_field_valid_int() -> None: 

160 """validate_int_field returns integer value.""" 

161 result = validate_int_field(42, "field") 

162 assert_that(result).is_equal_to(42) 

163 

164 

165def test_validate_int_field_non_int_returns_default() -> None: 

166 """validate_int_field returns default for non-integer.""" 

167 result = validate_int_field("not_int", "field", default=0) 

168 assert_that(result).is_equal_to(0) 

169 

170 

171def test_validate_int_field_bool_returns_default() -> None: 

172 """validate_int_field returns default for boolean.""" 

173 result = validate_int_field(True, "field", default=0) 

174 assert_that(result).is_equal_to(0) 

175 

176 

177def test_validate_int_field_none_returns_default() -> None: 

178 """validate_int_field returns default for None.""" 

179 result = validate_int_field(None, "field", default=0) 

180 assert_that(result).is_equal_to(0) 

181 

182 

183def test_collect_continuation_lines_basic() -> None: 

184 """collect_continuation_lines collects indented lines.""" 

185 lines = ["main message", " continued", " more", "next item"] 

186 result, next_idx = collect_continuation_lines( 

187 lines, 

188 1, 

189 lambda line: line.startswith(" "), 

190 ) 

191 assert_that(result).is_equal_to("continued more") 

192 assert_that(next_idx).is_equal_to(3) 

193 

194 

195def test_collect_continuation_lines_no_continuation() -> None: 

196 """collect_continuation_lines handles no continuation.""" 

197 lines = ["main message", "next item"] 

198 result, next_idx = collect_continuation_lines( 

199 lines, 

200 1, 

201 lambda line: line.startswith(" "), 

202 ) 

203 assert_that(result).is_equal_to("") 

204 assert_that(next_idx).is_equal_to(1) 

205 

206 

207def test_collect_continuation_lines_end_of_list() -> None: 

208 """collect_continuation_lines handles end of list.""" 

209 lines = ["main message", " continued"] 

210 result, next_idx = collect_continuation_lines( 

211 lines, 

212 1, 

213 lambda line: line.startswith(" "), 

214 ) 

215 assert_that(result).is_equal_to("continued") 

216 assert_that(next_idx).is_equal_to(2) 

217 

218 

219def test_collect_continuation_lines_strips_prefix() -> None: 

220 """collect_continuation_lines strips colon prefix.""" 

221 lines = ["message", ": continued part"] 

222 result, next_idx = collect_continuation_lines( 

223 lines, 

224 1, 

225 lambda _: True, 

226 ) 

227 assert_that(result).is_equal_to("continued part") 

228 

229 

230def test_safe_parse_items_valid_items() -> None: 

231 """safe_parse_items parses valid dictionaries.""" 

232 

233 @dataclass 

234 class TestIssue(BaseIssue): 

235 pass 

236 

237 def parse_func(item: dict[str, object]) -> TestIssue | None: 

238 file = item.get("file", "") 

239 return TestIssue(file=str(file) if file else "") 

240 

241 items: list[object] = [{"file": "a.py"}, {"file": "b.py"}] 

242 result = safe_parse_items(items, parse_func, "test") 

243 assert_that(len(result)).is_equal_to(2) 

244 assert_that(result[0].file).is_equal_to("a.py") 

245 assert_that(result[1].file).is_equal_to("b.py") 

246 

247 

248def test_safe_parse_items_skips_non_dict() -> None: 

249 """safe_parse_items skips non-dictionary items.""" 

250 

251 @dataclass 

252 class TestIssue(BaseIssue): 

253 pass 

254 

255 def parse_func(item: dict[str, object]) -> TestIssue | None: 

256 file = item.get("file", "") 

257 return TestIssue(file=str(file) if file else "") 

258 

259 items: list[object] = [{"file": "a.py"}, "invalid", 123] 

260 result = safe_parse_items(items, parse_func, "test") 

261 assert_that(len(result)).is_equal_to(1) 

262 

263 

264def test_safe_parse_items_handles_parse_failure() -> None: 

265 """safe_parse_items handles parse function exceptions.""" 

266 

267 @dataclass 

268 class TestIssue(BaseIssue): 

269 pass 

270 

271 def parse_func(item: dict[str, object]) -> TestIssue | None: 

272 if "error" in item: 

273 raise ValueError("Parse error") 

274 file = item.get("file", "") 

275 return TestIssue(file=str(file) if file else "") 

276 

277 items: list[object] = [{"file": "a.py"}, {"error": True}, {"file": "b.py"}] 

278 result = safe_parse_items(items, parse_func, "test") 

279 assert_that(len(result)).is_equal_to(2) 

280 

281 

282def test_safe_parse_items_handles_none_return() -> None: 

283 """safe_parse_items filters out None returns.""" 

284 

285 @dataclass 

286 class TestIssue(BaseIssue): 

287 pass 

288 

289 def parse_func(item: dict[str, object]) -> TestIssue | None: 

290 if item.get("skip"): 

291 return None 

292 file = item.get("file", "") 

293 return TestIssue(file=str(file) if file else "") 

294 

295 items: list[object] = [{"file": "a.py"}, {"skip": True}, {"file": "b.py"}] 

296 result = safe_parse_items(items, parse_func, "test") 

297 assert_that(len(result)).is_equal_to(2) 

298 

299 

300def test_safe_parse_items_empty_list() -> None: 

301 """safe_parse_items handles empty list.""" 

302 

303 @dataclass 

304 class TestIssue(BaseIssue): 

305 pass 

306 

307 def parse_func(item: dict[str, object]) -> TestIssue | None: 

308 return TestIssue() 

309 

310 result = safe_parse_items([], parse_func, "test") 

311 assert_that(result).is_empty() 

312 

313 

314@pytest.mark.parametrize( 

315 ("data", "candidates", "expected"), 

316 [ 

317 ({"a": 1, "b": 2}, ["a"], 1), 

318 ({"a": 1, "b": 2}, ["c", "b"], 2), 

319 ({"a": 1, "b": 2}, ["c", "d"], None), 

320 ], 

321) 

322def test_extract_int_field_parametrized( 

323 data: dict[str, object], 

324 candidates: list[str], 

325 expected: int | None, 

326) -> None: 

327 """extract_int_field handles various scenarios. 

328 

329 Args: 

330 data: Dictionary to extract from. 

331 candidates: List of candidate keys. 

332 expected: Expected result value. 

333 """ 

334 result = extract_int_field(data, candidates) 

335 assert_that(result).is_equal_to(expected) 

336 

337 

338@pytest.mark.parametrize( 

339 ("data", "candidates", "expected"), 

340 [ 

341 ({"a": "x", "b": "y"}, ["a"], "x"), 

342 ({"a": "x", "b": "y"}, ["c", "b"], "y"), 

343 ({"a": "x", "b": "y"}, ["c", "d"], ""), 

344 ], 

345) 

346def test_extract_str_field_parametrized( 

347 data: dict[str, object], 

348 candidates: list[str], 

349 expected: str, 

350) -> None: 

351 """extract_str_field handles various scenarios. 

352 

353 Args: 

354 data: Dictionary to extract from. 

355 candidates: List of candidate keys. 

356 expected: Expected result value. 

357 """ 

358 result = extract_str_field(data, candidates) 

359 assert_that(result).is_equal_to(expected)