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

1"""Unit tests for mypy parser.""" 

2 

3from __future__ import annotations 

4 

5from assertpy import assert_that 

6 

7from lintro.parsers.mypy.mypy_parser import parse_mypy_output 

8 

9 

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) 

17 

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

26 

27 

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 ) 

39 

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

46 

47 

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([]) 

53 

54 

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() 

59 

60 

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) 

76 

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

80 

81 

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) 

91 

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

95 

96 

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) 

110 

111 codes = {issue.code for issue in issues} 

112 assert_that(codes).is_equal_to({"X123", "Y234"}) 

113 

114 

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) 

125 

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() 

136 

137 

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) 

149 

150 assert_that(issues).is_length(1) 

151 assert_that(issues[0].file).ends_with("valid.py")