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

86 statements  

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

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

2 

3from __future__ import annotations 

4 

5import subprocess 

6from pathlib import Path 

7from unittest.mock import patch 

8 

9from assertpy import assert_that 

10 

11from lintro.parsers.rustfmt.rustfmt_parser import parse_rustfmt_output 

12from lintro.tools.definitions.rustfmt import RustfmtPlugin 

13 

14# ============================================================================= 

15# Tests for timeout handling 

16# ============================================================================= 

17 

18 

19def test_check_with_timeout( 

20 rustfmt_plugin: RustfmtPlugin, 

21 tmp_path: Path, 

22) -> None: 

23 """Check handles timeout correctly. 

24 

25 Args: 

26 rustfmt_plugin: The RustfmtPlugin instance to test. 

27 tmp_path: Temporary directory path for test files. 

28 """ 

29 cargo_toml = tmp_path / "Cargo.toml" 

30 cargo_toml.write_text('[package]\nname = "test"\nversion = "0.1.0"') 

31 

32 test_file = tmp_path / "src" / "main.rs" 

33 test_file.parent.mkdir(parents=True, exist_ok=True) 

34 test_file.write_text("fn main() {}") 

35 

36 with patch( 

37 "lintro.plugins.execution_preparation.verify_tool_version", 

38 return_value=None, 

39 ): 

40 with patch.object( 

41 rustfmt_plugin, 

42 "_run_subprocess", 

43 side_effect=subprocess.TimeoutExpired(cmd=["cargo", "fmt"], timeout=60), 

44 ): 

45 result = rustfmt_plugin.check([str(test_file)], {}) 

46 

47 assert_that(result.success).is_false() 

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

49 assert_that(result.issues_count).is_equal_to(1) 

50 

51 

52def test_fix_with_timeout_on_initial_check( 

53 rustfmt_plugin: RustfmtPlugin, 

54 tmp_path: Path, 

55) -> None: 

56 """Fix handles timeout on initial check correctly. 

57 

58 Args: 

59 rustfmt_plugin: The RustfmtPlugin instance to test. 

60 tmp_path: Temporary directory path for test files. 

61 """ 

62 cargo_toml = tmp_path / "Cargo.toml" 

63 cargo_toml.write_text('[package]\nname = "test"\nversion = "0.1.0"') 

64 

65 test_file = tmp_path / "src" / "main.rs" 

66 test_file.parent.mkdir(parents=True, exist_ok=True) 

67 test_file.write_text("fn main() {}") 

68 

69 with patch( 

70 "lintro.plugins.execution_preparation.verify_tool_version", 

71 return_value=None, 

72 ): 

73 with patch.object( 

74 rustfmt_plugin, 

75 "_run_subprocess", 

76 side_effect=subprocess.TimeoutExpired(cmd=["cargo", "fmt"], timeout=60), 

77 ): 

78 result = rustfmt_plugin.fix([str(test_file)], {}) 

79 

80 assert_that(result.success).is_false() 

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

82 # Timeout is counted as an execution failure (consistent with clippy) 

83 assert_that(result.issues_count).is_equal_to(1) 

84 

85 

86def test_fix_with_timeout_on_fix_command( 

87 rustfmt_plugin: RustfmtPlugin, 

88 tmp_path: Path, 

89) -> None: 

90 """Fix handles timeout on fix command correctly. 

91 

92 Args: 

93 rustfmt_plugin: The RustfmtPlugin instance to test. 

94 tmp_path: Temporary directory path for test files. 

95 """ 

96 cargo_toml = tmp_path / "Cargo.toml" 

97 cargo_toml.write_text('[package]\nname = "test"\nversion = "0.1.0"') 

98 

99 test_file = tmp_path / "src" / "main.rs" 

100 test_file.parent.mkdir(parents=True, exist_ok=True) 

101 test_file.write_text("fn main(){}") 

102 

103 call_count = 0 

104 

105 def mock_run( 

106 cmd: list[str], 

107 timeout: int, 

108 cwd: str | None = None, 

109 ) -> tuple[bool, str]: 

110 """Mock subprocess that times out on fix command. 

111 

112 Args: 

113 cmd: Command list. 

114 timeout: Timeout in seconds. 

115 cwd: Working directory. 

116 

117 Returns: 

118 tuple[bool, str]: Tuple of (success, output). 

119 

120 Raises: 

121 subprocess.TimeoutExpired: On second call (fix command). 

122 """ 

123 nonlocal call_count 

124 call_count += 1 

125 if call_count == 1: 

126 # First check - issues found 

127 return (False, "Diff in src/main.rs:1:") 

128 # Fix command times out 

129 raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout) 

130 

131 with patch( 

132 "lintro.plugins.execution_preparation.verify_tool_version", 

133 return_value=None, 

134 ): 

135 with patch.object(rustfmt_plugin, "_run_subprocess", side_effect=mock_run): 

136 result = rustfmt_plugin.fix([str(test_file)], {}) 

137 

138 assert_that(result.success).is_false() 

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

140 assert_that(result.initial_issues_count).is_equal_to(1) 

141 assert_that(result.fixed_issues_count).is_equal_to(0) 

142 

143 

144def test_fix_with_timeout_on_verification( 

145 rustfmt_plugin: RustfmtPlugin, 

146 tmp_path: Path, 

147) -> None: 

148 """Fix handles timeout on verification check correctly. 

149 

150 Args: 

151 rustfmt_plugin: The RustfmtPlugin instance to test. 

152 tmp_path: Temporary directory path for test files. 

153 """ 

154 cargo_toml = tmp_path / "Cargo.toml" 

155 cargo_toml.write_text('[package]\nname = "test"\nversion = "0.1.0"') 

156 

157 test_file = tmp_path / "src" / "main.rs" 

158 test_file.parent.mkdir(parents=True, exist_ok=True) 

159 test_file.write_text("fn main(){}") 

160 

161 call_count = 0 

162 

163 def mock_run( 

164 cmd: list[str], 

165 timeout: int, 

166 cwd: str | None = None, 

167 ) -> tuple[bool, str]: 

168 """Mock subprocess that times out on verification. 

169 

170 Args: 

171 cmd: Command list. 

172 timeout: Timeout in seconds. 

173 cwd: Working directory. 

174 

175 Returns: 

176 tuple[bool, str]: Tuple of (success, output). 

177 

178 Raises: 

179 subprocess.TimeoutExpired: On third call (verification). 

180 """ 

181 nonlocal call_count 

182 call_count += 1 

183 if call_count == 1: 

184 # First check - issues found 

185 return (False, "Diff in src/main.rs:1:") 

186 elif call_count == 2: 

187 # Fix command succeeds 

188 return (True, "") 

189 # Verification times out 

190 raise subprocess.TimeoutExpired(cmd=cmd, timeout=timeout) 

191 

192 with patch( 

193 "lintro.plugins.execution_preparation.verify_tool_version", 

194 return_value=None, 

195 ): 

196 with patch.object(rustfmt_plugin, "_run_subprocess", side_effect=mock_run): 

197 result = rustfmt_plugin.fix([str(test_file)], {}) 

198 

199 assert_that(result.success).is_false() 

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

201 assert_that(result.initial_issues_count).is_equal_to(1) 

202 assert_that(result.fixed_issues_count).is_equal_to(0) 

203 

204 

205# ============================================================================= 

206# Tests for output parsing 

207# ============================================================================= 

208 

209 

210def test_parse_rustfmt_output_with_diff() -> None: 

211 """Parse diff output from rustfmt.""" 

212 output = """Diff in src/main.rs:1: 

213-fn main(){let x=1;} 

214+fn main() { 

215+ let x = 1; 

216+}""" 

217 issues = parse_rustfmt_output(output) 

218 

219 assert_that(issues).is_length(1) 

220 assert_that(issues[0].file).contains("main.rs") 

221 

222 

223def test_parse_rustfmt_output_multiple_files() -> None: 

224 """Parse output with multiple file diffs.""" 

225 output = """Diff in src/main.rs:1: 

226-fn main(){} 

227+fn main() {} 

228Diff in src/lib.rs:5: 

229-fn foo(){} 

230+fn foo() {}""" 

231 issues = parse_rustfmt_output(output) 

232 

233 assert_that(issues).is_length(2) 

234 

235 

236def test_parse_rustfmt_output_empty() -> None: 

237 """Parse empty output returns empty list.""" 

238 issues = parse_rustfmt_output("") 

239 

240 assert_that(issues).is_empty() 

241 

242 

243def test_parse_rustfmt_output_none() -> None: 

244 """Parse None output returns empty list.""" 

245 issues = parse_rustfmt_output(None) 

246 

247 assert_that(issues).is_empty()