Coverage for tests / unit / ai / test_rerun.py: 100%

64 statements  

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

1"""Tests for lintro.ai.rerun module.""" 

2 

3from __future__ import annotations 

4 

5import os 

6from pathlib import Path 

7from unittest.mock import MagicMock, patch 

8 

9from assertpy import assert_that 

10 

11from lintro.ai.rerun import ( 

12 _tool_cwd, 

13 apply_rerun_results, 

14 paths_for_context, 

15 rerun_tools, 

16) 

17from lintro.models.core.tool_result import ToolResult 

18from lintro.parsers.base_issue import BaseIssue 

19 

20from .conftest import MockIssue 

21 

22_ByTool = dict[str, tuple[ToolResult, list[BaseIssue]]] 

23 

24 

25# -- TestToolCwd: Tests for _tool_cwd context manager. ----------------------- 

26 

27 

28def test_tool_cwd_restores_original_cwd(tmp_path: Path) -> None: 

29 """_tool_cwd changes cwd and restores it after exit.""" 

30 original = os.getcwd() 

31 target = str(tmp_path) 

32 

33 with _tool_cwd(target): 

34 assert_that(os.getcwd()).is_equal_to(target) 

35 

36 assert_that(os.getcwd()).is_equal_to(original) 

37 

38 

39def test_tool_cwd_skips_when_none() -> None: 

40 """When cwd is None, the context manager yields without changing directory.""" 

41 original = os.getcwd() 

42 

43 with _tool_cwd(None): 

44 assert_that(os.getcwd()).is_equal_to(original) 

45 

46 assert_that(os.getcwd()).is_equal_to(original) 

47 

48 

49# -- TestPathsForContext: Tests for paths_for_context. ----------------------- 

50 

51 

52def test_paths_for_context_relativizes_paths(tmp_path: Path) -> None: 

53 """Given absolute paths and a cwd, returns relative paths for children.""" 

54 cwd = str(tmp_path) 

55 child = str(tmp_path / "src" / "main.py") 

56 outside = "/some/other/path/file.py" 

57 

58 result = paths_for_context(file_paths=[child, outside], cwd=cwd) 

59 

60 assert_that(result).is_length(2) 

61 assert_that(result[0]).is_equal_to(str(Path("src") / "main.py")) 

62 # Outside path stays absolute (resolved) 

63 assert_that(result[1]).is_equal_to(str(Path(outside).resolve())) 

64 

65 

66def test_paths_for_context_returns_originals_when_no_cwd() -> None: 

67 """When cwd is None, returns original paths unchanged.""" 

68 paths = ["/a/b/c.py", "/d/e/f.py"] 

69 

70 result = paths_for_context(file_paths=paths, cwd=None) 

71 

72 assert_that(result).is_equal_to(paths) 

73 

74 

75# -- TestRerunTools: Tests for rerun_tools. ---------------------------------- 

76 

77 

78def test_rerun_tools_with_missing_tools() -> None: 

79 """When tool_manager.get_tool() raises KeyError, the tool is skipped.""" 

80 issue = MockIssue( 

81 file="src/main.py", 

82 line=1, 

83 column=1, 

84 message="test", 

85 code="T001", 

86 severity="low", 

87 ) 

88 result = ToolResult(name="missing_tool", success=False, issues_count=1) 

89 by_tool: _ByTool = {"missing_tool": (result, [issue])} 

90 

91 mock_tool_manager = MagicMock() 

92 mock_tool_manager.get_tool.side_effect = KeyError("missing_tool") 

93 

94 with patch("lintro.tools.tool_manager", mock_tool_manager): 

95 results = rerun_tools(by_tool) 

96 

97 assert_that(results).is_not_none() 

98 assert_that(results).is_empty() 

99 

100 

101# -- TestApplyRerunResults: Tests for apply_rerun_results. ------------------- 

102 

103 

104def test_apply_rerun_results_preserves_native_counters() -> None: 

105 """Native initial/fixed counts are preserved after rerun.""" 

106 original_result = ToolResult( 

107 name="ruff", 

108 success=False, 

109 issues_count=10, 

110 initial_issues_count=10, 

111 fixed_issues_count=7, 

112 remaining_issues_count=3, 

113 ) 

114 issue = MockIssue( 

115 file="src/main.py", 

116 line=1, 

117 column=1, 

118 message="remaining issue", 

119 code="E501", 

120 severity="warning", 

121 ) 

122 by_tool: _ByTool = {"ruff": (original_result, [issue])} 

123 

124 rerun_result = ToolResult( 

125 name="ruff", 

126 success=True, 

127 issues_count=2, 

128 issues=[ 

129 MockIssue( 

130 file="src/main.py", 

131 line=1, 

132 column=1, 

133 message="issue 1", 

134 code="E501", 

135 severity="warning", 

136 ), 

137 MockIssue( 

138 file="src/main.py", 

139 line=5, 

140 column=1, 

141 message="issue 2", 

142 code="E502", 

143 severity="warning", 

144 ), 

145 ], 

146 ) 

147 

148 apply_rerun_results(by_tool=by_tool, rerun_results=[rerun_result]) 

149 

150 assert_that(original_result.initial_issues_count).is_equal_to(10) 

151 assert_that(original_result.fixed_issues_count).is_equal_to(7) 

152 

153 

154def test_apply_rerun_results_updates_remaining_issues() -> None: 

155 """Remaining issues are updated from rerun results.""" 

156 original_result = ToolResult( 

157 name="ruff", 

158 success=False, 

159 issues_count=5, 

160 initial_issues_count=5, 

161 fixed_issues_count=3, 

162 remaining_issues_count=2, 

163 ) 

164 issue = MockIssue( 

165 file="src/main.py", 

166 line=1, 

167 column=1, 

168 message="old issue", 

169 code="E501", 

170 severity="warning", 

171 ) 

172 by_tool: _ByTool = {"ruff": (original_result, [issue])} 

173 

174 refreshed_issue = MockIssue( 

175 file="src/main.py", 

176 line=10, 

177 column=1, 

178 message="remaining issue", 

179 code="E501", 

180 severity="warning", 

181 ) 

182 rerun_result = ToolResult( 

183 name="ruff", 

184 success=True, 

185 issues_count=1, 

186 issues=[refreshed_issue], 

187 ) 

188 

189 apply_rerun_results(by_tool=by_tool, rerun_results=[rerun_result]) 

190 

191 assert_that(original_result.remaining_issues_count).is_equal_to(1) 

192 assert_that(original_result.issues_count).is_equal_to(1) 

193 assert_that(original_result.issues).is_length(1) 

194 assert_that(original_result.issues).is_not_none() 

195 assert_that(original_result.issues[0].message).is_equal_to("remaining issue") # type: ignore[index] # assertpy is_not_none narrows this 

196 assert_that(original_result.success).is_true()