Coverage for tests / unit / parsers / test_mypy_parser.py: 100%
58 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"""Unit tests for mypy parser."""
3from __future__ import annotations
5from assertpy import assert_that
7from lintro.parsers.mypy.mypy_parser import parse_mypy_output
10def test_parse_mypy_json_array() -> None:
11 """Parse standard mypy JSON array output."""
12 output = (
13 '[{"path":"app.py","line":3,"column":5,"endLine":3,"endColumn":10,'
14 '"message":"Incompatible return value type","code":"return-value"}]'
15 )
16 issues = parse_mypy_output(output)
18 assert_that(issues).is_length(1)
19 issue = issues[0]
20 assert_that(issue.file).ends_with("app.py")
21 assert_that(issue.line).is_equal_to(3)
22 assert_that(issue.column).is_equal_to(5)
23 assert_that(issue.end_line).is_equal_to(3)
24 assert_that(issue.end_column).is_equal_to(10)
25 assert_that(issue.code).is_equal_to("return-value")
28def test_parse_mypy_errors_object() -> None:
29 """Parse mypy output wrapped in an errors object."""
30 output = (
31 "{"
32 '"errors": ['
33 '{"filename": "service.py", "line": 1, "column": 1, '
34 '"message": "Name \\"x\\" is not defined", '
35 '"code": {"code": "name-defined"}, "severity": "error"}'
36 "]"
37 "}"
38 )
40 issues = parse_mypy_output(output)
41 assert_that(issues).is_length(1)
42 issue = issues[0]
43 assert_that(issue.file).ends_with("service.py")
44 assert_that(issue.code).is_equal_to("name-defined")
45 assert_that(issue.severity).is_equal_to("error")
48def test_parse_mypy_invalid_output_returns_empty() -> None:
49 """Return empty list when mypy output is not JSON parseable."""
50 output = "mypy: command failed"
51 issues = parse_mypy_output(output)
52 assert_that(issues).is_equal_to([])
55def test_parse_mypy_empty_and_whitespace_output() -> None:
56 """Return empty list when output is empty or whitespace."""
57 assert_that(parse_mypy_output("")).is_empty()
58 assert_that(parse_mypy_output(" \n\t")).is_empty()
61def test_parse_mypy_json_lines_multiple_issues() -> None:
62 """Handle JSON lines payload with multiple issues."""
63 output = "\n".join(
64 [
65 (
66 '{"path": "pkg/a.py", "line": 1, "column": 2, '
67 '"message": "err A", "code": "A1"}'
68 ),
69 (
70 '{"path": "pkg/b.py", "line": 3, "column": 4, '
71 '"message": "err B", "code": "B2"}'
72 ),
73 ],
74 )
75 issues = parse_mypy_output(output)
77 assert_that(issues).is_length(2)
78 assert_that(issues[0].file).ends_with("pkg/a.py")
79 assert_that(issues[1].file).ends_with("pkg/b.py")
82def test_parse_mypy_multiple_array_entries() -> None:
83 """Parse multiple issues from a JSON array."""
84 output = (
85 "["
86 '{"file":"one.py","line":10,"column":1,"message":"first","code":"A"},'
87 '{"file":"two.py","line":20,"column":2,"message":"second","code":"B"}'
88 "]"
89 )
90 issues = parse_mypy_output(output)
92 assert_that(issues).is_length(2)
93 assert_that(issues[0].code).is_equal_to("A")
94 assert_that(issues[1].code).is_equal_to("B")
97def test_parse_mypy_nested_code_object_variants() -> None:
98 """Support nested code objects with alternate keys."""
99 output = (
100 "{"
101 '"errors": ['
102 '{"path": "nested.py", "line": 1, "column": 1, '
103 '"message": "msg", "code": {"text": "X123"}},'
104 '{"path": "nested2.py", "line": 2, "column": 2, '
105 '"message": "msg2", "code": {"id": "Y234"}}'
106 "]"
107 "}"
108 )
109 issues = parse_mypy_output(output)
111 codes = {issue.code for issue in issues}
112 assert_that(codes).is_equal_to({"X123", "Y234"})
115def test_parse_mypy_field_name_variations() -> None:
116 """Handle path/filename/file keys uniformly."""
117 output = "\n".join(
118 [
119 '{"path":"path_variant.py","line":1,"column":1,"message":"one","code":"P"}',
120 '{"filename":"filename_variant.py","line":2,"column":2,"message":"two","code":"F"}',
121 '{"file":"file_variant.py","line":3,"column":3,"message":"three","code":"L"}',
122 ],
123 )
124 issues = parse_mypy_output(output)
126 assert_that(issues).is_length(3)
127 assert_that(
128 any(issue.file.endswith("path_variant.py") for issue in issues),
129 ).is_true()
130 assert_that(
131 any(issue.file.endswith("filename_variant.py") for issue in issues),
132 ).is_true()
133 assert_that(
134 any(issue.file.endswith("file_variant.py") for issue in issues),
135 ).is_true()
138def test_parse_mypy_skips_entries_without_file() -> None:
139 """Skip entries that lack a file path."""
140 output = (
141 "{"
142 '"errors": ['
143 '{"message":"missing file","line":1,"column":1,"code":"X"},'
144 '{"path":"valid.py","line":2,"column":2,"message":"ok","code":"OK"}'
145 "]"
146 "}"
147 )
148 issues = parse_mypy_output(output)
150 assert_that(issues).is_length(1)
151 assert_that(issues[0].file).ends_with("valid.py")