Coverage for tests / unit / tools / semgrep / test_error_handling.py: 100%

63 statements  

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

1"""Unit tests for Semgrep plugin error handling.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import subprocess 

7from pathlib import Path 

8from typing import TYPE_CHECKING 

9from unittest.mock import patch 

10 

11from assertpy import assert_that 

12 

13from lintro.parsers.semgrep.semgrep_parser import parse_semgrep_output 

14from lintro.tools.definitions.semgrep import SemgrepPlugin 

15 

16if TYPE_CHECKING: 

17 pass 

18 

19 

20# ============================================================================= 

21# Tests for timeout handling 

22# ============================================================================= 

23 

24 

25def test_check_with_timeout( 

26 semgrep_plugin: SemgrepPlugin, 

27 tmp_path: Path, 

28) -> None: 

29 """Check handles timeout correctly. 

30 

31 Args: 

32 semgrep_plugin: The SemgrepPlugin instance to test. 

33 tmp_path: Temporary directory path for test files. 

34 """ 

35 test_file = tmp_path / "large_file.py" 

36 test_file.write_text('"""Large file that takes too long."""\n') 

37 

38 with patch( 

39 "lintro.plugins.execution_preparation.verify_tool_version", 

40 return_value=None, 

41 ): 

42 with patch.object( 

43 semgrep_plugin, 

44 "_run_subprocess", 

45 side_effect=subprocess.TimeoutExpired(cmd=["semgrep"], timeout=120), 

46 ): 

47 result = semgrep_plugin.check([str(test_file)], {}) 

48 

49 assert_that(result.success).is_false() 

50 assert_that(result.output).contains("timed out") 

51 

52 

53def test_check_with_json_parse_error( 

54 semgrep_plugin: SemgrepPlugin, 

55 tmp_path: Path, 

56) -> None: 

57 """Check handles JSON parse errors gracefully. 

58 

59 Args: 

60 semgrep_plugin: The SemgrepPlugin instance to test. 

61 tmp_path: Temporary directory path for test files. 

62 """ 

63 test_file = tmp_path / "test_module.py" 

64 test_file.write_text('"""Test module."""\n') 

65 

66 # Invalid JSON output 

67 invalid_output = "Error: Something went wrong\n{invalid json" 

68 

69 with patch( 

70 "lintro.plugins.execution_preparation.verify_tool_version", 

71 return_value=None, 

72 ): 

73 with patch.object( 

74 semgrep_plugin, 

75 "_run_subprocess", 

76 return_value=(False, invalid_output), 

77 ): 

78 result = semgrep_plugin.check([str(test_file)], {}) 

79 

80 assert_that(result.success).is_false() 

81 assert_that(result.issues_count).is_equal_to(0) 

82 

83 

84def test_check_with_semgrep_errors( 

85 semgrep_plugin: SemgrepPlugin, 

86 tmp_path: Path, 

87) -> None: 

88 """Check handles Semgrep errors in response. 

89 

90 Args: 

91 semgrep_plugin: The SemgrepPlugin instance to test. 

92 tmp_path: Temporary directory path for test files. 

93 """ 

94 test_file = tmp_path / "test_module.py" 

95 test_file.write_text('"""Test module."""\n') 

96 

97 # Semgrep JSON output with errors 

98 semgrep_output = json.dumps( 

99 { 

100 "results": [], 

101 "errors": [ 

102 {"message": "Failed to fetch rules from registry"}, 

103 ], 

104 }, 

105 ) 

106 

107 with patch( 

108 "lintro.plugins.execution_preparation.verify_tool_version", 

109 return_value=None, 

110 ): 

111 with patch.object( 

112 semgrep_plugin, 

113 "_run_subprocess", 

114 return_value=(False, semgrep_output), 

115 ): 

116 result = semgrep_plugin.check([str(test_file)], {}) 

117 

118 assert_that(result.success).is_false() 

119 

120 

121# ============================================================================= 

122# Tests for output parsing 

123# ============================================================================= 

124 

125 

126def test_parse_semgrep_output_single_issue() -> None: 

127 """Parse single issue from Semgrep output.""" 

128 output = json.dumps( 

129 { 

130 "results": [ 

131 { 

132 "check_id": "python.lang.security.audit.eval-usage", 

133 "path": "test.py", 

134 "start": {"line": 10, "col": 1}, 

135 "end": {"line": 10, "col": 15}, 

136 "extra": { 

137 "message": "Detected use of eval()", 

138 "severity": "WARNING", 

139 "metadata": {"category": "security"}, 

140 }, 

141 }, 

142 ], 

143 }, 

144 ) 

145 issues = parse_semgrep_output(output) 

146 

147 assert_that(issues).is_length(1) 

148 assert_that(issues[0].file).is_equal_to("test.py") 

149 assert_that(issues[0].line).is_equal_to(10) 

150 assert_that(issues[0].check_id).is_equal_to("python.lang.security.audit.eval-usage") 

151 assert_that(issues[0].message).contains("eval()") 

152 

153 

154def test_parse_semgrep_output_multiple_issues() -> None: 

155 """Parse multiple issues from Semgrep output.""" 

156 output = json.dumps( 

157 { 

158 "results": [ 

159 { 

160 "check_id": "rule1", 

161 "path": "file1.py", 

162 "start": {"line": 5, "col": 1}, 

163 "end": {"line": 5, "col": 10}, 

164 "extra": { 

165 "message": "Issue 1", 

166 "severity": "ERROR", 

167 "metadata": {}, 

168 }, 

169 }, 

170 { 

171 "check_id": "rule2", 

172 "path": "file2.py", 

173 "start": {"line": 15, "col": 1}, 

174 "end": {"line": 15, "col": 20}, 

175 "extra": { 

176 "message": "Issue 2", 

177 "severity": "WARNING", 

178 "metadata": {}, 

179 }, 

180 }, 

181 ], 

182 }, 

183 ) 

184 issues = parse_semgrep_output(output) 

185 

186 assert_that(issues).is_length(2) 

187 assert_that(issues[0].check_id).is_equal_to("rule1") 

188 assert_that(issues[1].check_id).is_equal_to("rule2") 

189 

190 

191def test_parse_semgrep_output_empty() -> None: 

192 """Parse empty output returns empty list.""" 

193 issues = parse_semgrep_output("") 

194 

195 assert_that(issues).is_empty() 

196 

197 

198def test_parse_semgrep_output_empty_results() -> None: 

199 """Parse output with no results returns empty list.""" 

200 output = json.dumps({"results": []}) 

201 issues = parse_semgrep_output(output) 

202 

203 assert_that(issues).is_empty() 

204 

205 

206def test_parse_semgrep_output_with_cwe() -> None: 

207 """Parse output with CWE information.""" 

208 output = json.dumps( 

209 { 

210 "results": [ 

211 { 

212 "check_id": "security-rule", 

213 "path": "app.py", 

214 "start": {"line": 20, "col": 1}, 

215 "end": {"line": 20, "col": 30}, 

216 "extra": { 

217 "message": "SQL injection vulnerability", 

218 "severity": "ERROR", 

219 "metadata": { 

220 "category": "security", 

221 "cwe": ["CWE-89"], 

222 }, 

223 }, 

224 }, 

225 ], 

226 }, 

227 ) 

228 issues = parse_semgrep_output(output) 

229 

230 assert_that(issues).is_length(1) 

231 assert_that(issues[0].cwe).contains("CWE-89") 

232 

233 

234def test_parse_semgrep_output_none_input() -> None: 

235 """Parse None input returns empty list.""" 

236 issues = parse_semgrep_output(None) 

237 

238 assert_that(issues).is_empty()