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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for lintro.parsers.base_parser module."""
3from __future__ import annotations
5from dataclasses import dataclass
7import pytest
8from assertpy import assert_that
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)
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)
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)
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)
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()
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)
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")
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")
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")
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("")
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})
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})
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})
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({})
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")
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")
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")
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("")
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")
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")
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")
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)
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)
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)
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)
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)
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)
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)
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")
230def test_safe_parse_items_valid_items() -> None:
231 """safe_parse_items parses valid dictionaries."""
233 @dataclass
234 class TestIssue(BaseIssue):
235 pass
237 def parse_func(item: dict[str, object]) -> TestIssue | None:
238 file = item.get("file", "")
239 return TestIssue(file=str(file) if file else "")
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")
248def test_safe_parse_items_skips_non_dict() -> None:
249 """safe_parse_items skips non-dictionary items."""
251 @dataclass
252 class TestIssue(BaseIssue):
253 pass
255 def parse_func(item: dict[str, object]) -> TestIssue | None:
256 file = item.get("file", "")
257 return TestIssue(file=str(file) if file else "")
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)
264def test_safe_parse_items_handles_parse_failure() -> None:
265 """safe_parse_items handles parse function exceptions."""
267 @dataclass
268 class TestIssue(BaseIssue):
269 pass
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 "")
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)
282def test_safe_parse_items_handles_none_return() -> None:
283 """safe_parse_items filters out None returns."""
285 @dataclass
286 class TestIssue(BaseIssue):
287 pass
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 "")
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)
300def test_safe_parse_items_empty_list() -> None:
301 """safe_parse_items handles empty list."""
303 @dataclass
304 class TestIssue(BaseIssue):
305 pass
307 def parse_func(item: dict[str, object]) -> TestIssue | None:
308 return TestIssue()
310 result = safe_parse_items([], parse_func, "test")
311 assert_that(result).is_empty()
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.
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)
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.
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)