Coverage for tests / unit / tools / core / test_runtime_discovery.py: 100%

99 statements  

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

1"""Tests for lintro.tools.core.runtime_discovery module.""" 

2 

3from __future__ import annotations 

4 

5from unittest.mock import MagicMock, patch 

6 

7import pytest 

8from assertpy import assert_that 

9 

10from lintro.tools.core.runtime_discovery import ( 

11 DiscoveredTool, 

12 _extract_version, 

13 clear_discovery_cache, 

14 discover_all_tools, 

15 discover_tool, 

16 format_tool_status_table, 

17 get_available_tools, 

18 get_tool_path, 

19 get_unavailable_tools, 

20 is_tool_available, 

21) 

22 

23 

24@pytest.fixture(autouse=True) 

25def _clear_cache_before_each_test() -> None: 

26 """Clear discovery cache before each test.""" 

27 clear_discovery_cache() 

28 

29 

30def test_extract_version_semantic() -> None: 

31 """Extract semantic version from output.""" 

32 assert_that(_extract_version("ruff 0.1.0")).is_equal_to("0.1.0") 

33 

34 

35def test_extract_version_with_prefix() -> None: 

36 """Extract version with v prefix.""" 

37 assert_that(_extract_version("tool v1.2.3")).is_equal_to("1.2.3") 

38 

39 

40def test_extract_version_keyword() -> None: 

41 """Extract version after 'version' keyword.""" 

42 assert_that(_extract_version("black, version 23.0.0")).is_equal_to("23.0.0") 

43 

44 

45def test_extract_version_multiline() -> None: 

46 """Extract version from multiline output.""" 

47 output = "mypy 1.0.0 (compiled: yes)\nPython 3.11" 

48 assert_that(_extract_version(output)).is_equal_to("1.0.0") 

49 

50 

51def test_extract_version_returns_none_for_no_version() -> None: 

52 """Return None when no version found.""" 

53 assert_that(_extract_version("no version here")).is_none() 

54 

55 

56def test_discover_tool_available() -> None: 

57 """Discover tool that exists in PATH.""" 

58 with ( 

59 patch("shutil.which", return_value="/usr/bin/ruff"), 

60 patch("subprocess.run") as mock_run, 

61 ): 

62 mock_run.return_value = MagicMock( 

63 returncode=0, 

64 stdout="ruff 0.1.0", 

65 stderr="", 

66 ) 

67 result = discover_tool("ruff", use_cache=False) 

68 

69 assert_that(result.name).is_equal_to("ruff") 

70 assert_that(result.path).is_equal_to("/usr/bin/ruff") 

71 assert_that(result.version).is_equal_to("0.1.0") 

72 assert_that(result.available).is_true() 

73 

74 

75def test_discover_tool_unavailable() -> None: 

76 """Handle tool not found in PATH.""" 

77 with patch("shutil.which", return_value=None): 

78 result = discover_tool("nonexistent", use_cache=False) 

79 

80 assert_that(result.name).is_equal_to("nonexistent") 

81 assert_that(result.available).is_false() 

82 assert_that(result.error_message).contains("not found in PATH") 

83 

84 

85def test_discover_tool_handles_timeout() -> None: 

86 """Tool is unavailable when version probe times out.""" 

87 import subprocess 

88 

89 with ( 

90 patch("shutil.which", return_value="/usr/bin/slow_tool"), 

91 patch( 

92 "subprocess.run", 

93 side_effect=subprocess.TimeoutExpired(cmd=["tool"], timeout=5), 

94 ), 

95 ): 

96 result = discover_tool("slow_tool", use_cache=False) 

97 

98 assert_that(result.available).is_false() 

99 assert_that(result.version).is_none() 

100 

101 

102def test_discover_tool_uses_cache() -> None: 

103 """Use cache by default on second call.""" 

104 with ( 

105 patch("shutil.which", return_value="/usr/bin/ruff") as mock_which, 

106 patch("subprocess.run") as mock_run, 

107 ): 

108 mock_run.return_value = MagicMock(returncode=0, stdout="ruff 0.1.0", stderr="") 

109 

110 discover_tool("ruff") 

111 discover_tool("ruff") 

112 

113 assert_that(mock_which.call_count).is_equal_to(1) 

114 

115 

116def test_discover_all_tools() -> None: 

117 """Discover all tools in TOOL_VERSION_COMMANDS.""" 

118 with ( 

119 patch("shutil.which", return_value="/usr/bin/tool"), 

120 patch("subprocess.run") as mock_run, 

121 ): 

122 mock_run.return_value = MagicMock(returncode=0, stdout="1.0.0", stderr="") 

123 

124 tools = discover_all_tools(use_cache=False) 

125 

126 assert_that(len(tools)).is_greater_than(5) 

127 assert_that(tools).contains_key("ruff", "black", "mypy") 

128 

129 

130def test_is_tool_available_returns_true() -> None: 

131 """is_tool_available returns True for available tool.""" 

132 with ( 

133 patch("shutil.which", return_value="/usr/bin/ruff"), 

134 patch("subprocess.run") as mock_run, 

135 ): 

136 mock_run.return_value = MagicMock(returncode=0, stdout="ruff 0.1.0", stderr="") 

137 assert_that(is_tool_available("ruff")).is_true() 

138 

139 

140def test_is_tool_available_returns_false() -> None: 

141 """is_tool_available returns False for unavailable tool.""" 

142 with patch("shutil.which", return_value=None): 

143 assert_that(is_tool_available("nonexistent")).is_false() 

144 

145 

146def test_get_tool_path_returns_path() -> None: 

147 """get_tool_path returns path for available tool.""" 

148 with ( 

149 patch("shutil.which", return_value="/usr/bin/ruff"), 

150 patch("subprocess.run") as mock_run, 

151 ): 

152 mock_run.return_value = MagicMock(returncode=0, stdout="ruff 0.1.0", stderr="") 

153 assert_that(get_tool_path("ruff")).is_equal_to("/usr/bin/ruff") 

154 

155 

156def test_get_tool_path_returns_none() -> None: 

157 """get_tool_path returns None for unavailable tool.""" 

158 with patch("shutil.which", return_value=None): 

159 assert_that(get_tool_path("nonexistent")).is_none() 

160 

161 

162def test_get_unavailable_tools() -> None: 

163 """get_unavailable_tools returns list of missing tools.""" 

164 

165 def mock_which(name: str) -> str | None: 

166 return "/usr/bin/ruff" if name == "ruff" else None 

167 

168 with ( 

169 patch("shutil.which", side_effect=mock_which), 

170 patch("subprocess.run") as mock_run, 

171 ): 

172 mock_run.return_value = MagicMock(returncode=0, stdout="1.0.0", stderr="") 

173 unavailable = get_unavailable_tools() 

174 assert_that(unavailable).does_not_contain("ruff") 

175 assert_that(len(unavailable)).is_greater_than(0) 

176 

177 

178def test_get_available_tools() -> None: 

179 """get_available_tools returns list of available tools.""" 

180 

181 def mock_which(name: str) -> str | None: 

182 return "/usr/bin/ruff" if name == "ruff" else None 

183 

184 with ( 

185 patch("shutil.which", side_effect=mock_which), 

186 patch("subprocess.run") as mock_run, 

187 ): 

188 mock_run.return_value = MagicMock(returncode=0, stdout="1.0.0", stderr="") 

189 available = get_available_tools() 

190 assert_that(available).contains("ruff") 

191 

192 

193def test_format_tool_status_table() -> None: 

194 """Format status table with discovered tools.""" 

195 with ( 

196 patch("shutil.which", return_value="/usr/bin/tool"), 

197 patch("subprocess.run") as mock_run, 

198 ): 

199 mock_run.return_value = MagicMock(returncode=0, stdout="1.0.0", stderr="") 

200 table = format_tool_status_table() 

201 assert_that(table).contains("Tool Discovery Status") 

202 

203 

204def test_clear_discovery_cache() -> None: 

205 """Clear cache makes next call rediscover tools.""" 

206 with ( 

207 patch("shutil.which", return_value="/usr/bin/ruff") as mock_which, 

208 patch("subprocess.run") as mock_run, 

209 ): 

210 mock_run.return_value = MagicMock(returncode=0, stdout="ruff 0.1.0", stderr="") 

211 

212 discover_tool("ruff") 

213 initial_count = mock_which.call_count 

214 

215 clear_discovery_cache() 

216 discover_tool("ruff") 

217 

218 assert_that(mock_which.call_count).is_equal_to(initial_count + 1) 

219 

220 

221def test_discovered_tool_default_values() -> None: 

222 """DiscoveredTool has correct defaults.""" 

223 tool = DiscoveredTool(name="test") 

224 

225 assert_that(tool.name).is_equal_to("test") 

226 assert_that(tool.path).is_equal_to("") 

227 assert_that(tool.version).is_none() 

228 assert_that(tool.available).is_false()