Coverage for tests / unit / plugins / test_registry.py: 99%

153 statements  

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

1"""Unit tests for plugins/registry module.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from typing import Any 

7 

8import pytest 

9from assertpy import assert_that 

10 

11from lintro.models.core.tool_result import ToolResult 

12from lintro.plugins.base import BaseToolPlugin 

13from lintro.plugins.protocol import ToolDefinition 

14from lintro.plugins.registry import ToolRegistry, register_tool 

15from tests.unit.plugins.conftest import create_fake_plugin 

16 

17# ============================================================================= 

18# Tests for ToolRegistry.register 

19# ============================================================================= 

20 

21 

22def test_register_tool_adds_to_registry(clean_registry: None) -> None: 

23 """Verify that registering a tool class adds it to the registry. 

24 

25 Args: 

26 clean_registry: Fixture to ensure clean registry state. 

27 """ 

28 plugin_class = create_fake_plugin(name="test-register-tool") 

29 ToolRegistry.register(plugin_class) 

30 

31 assert_that(ToolRegistry.is_registered("test-register-tool")).is_true() 

32 

33 

34def test_register_tool_stores_instance(clean_registry: None) -> None: 

35 """Verify that registering a tool creates and stores an instance. 

36 

37 Args: 

38 clean_registry: Fixture to ensure clean registry state. 

39 """ 

40 plugin_class = create_fake_plugin(name="test-instance-tool") 

41 ToolRegistry.register(plugin_class) 

42 

43 tool = ToolRegistry.get("test-instance-tool") 

44 assert_that(tool).is_instance_of(BaseToolPlugin) 

45 assert_that(tool.definition.name).is_equal_to("test-instance-tool") 

46 

47 

48def test_register_overwrite_replaces_tool(clean_registry: None) -> None: 

49 """Verify that registering a tool with the same name replaces the previous one. 

50 

51 Args: 

52 clean_registry: Fixture to ensure clean registry state. 

53 """ 

54 plugin_class1 = create_fake_plugin(name="overwrite-test", description="First") 

55 plugin_class2 = create_fake_plugin(name="overwrite-test", description="Second") 

56 

57 ToolRegistry.register(plugin_class1) 

58 ToolRegistry.register(plugin_class2) 

59 

60 assert_that(ToolRegistry.is_registered("overwrite-test")).is_true() 

61 # The second registration should have replaced the first 

62 tool = ToolRegistry.get("overwrite-test") 

63 assert_that(tool.definition.description).is_equal_to("Second") 

64 

65 

66def test_register_returns_class_unchanged(clean_registry: None) -> None: 

67 """Verify that register returns the class unchanged for decorator use. 

68 

69 Args: 

70 clean_registry: Fixture to ensure clean registry state. 

71 """ 

72 plugin_class = create_fake_plugin(name="return-test-tool") 

73 result = ToolRegistry.register(plugin_class) 

74 

75 assert_that(result).is_equal_to(plugin_class) 

76 

77 

78# ============================================================================= 

79# Tests for ToolRegistry.get 

80# ============================================================================= 

81 

82 

83def test_get_registered_tool_returns_instance() -> None: 

84 """Verify that getting a registered tool returns its instance.""" 

85 tool = ToolRegistry.get("ruff") 

86 

87 assert_that(tool).is_not_none() 

88 assert_that(tool).is_instance_of(BaseToolPlugin) 

89 assert_that(tool.definition.name.lower()).is_equal_to("ruff") 

90 

91 

92def test_get_unknown_tool_raises_value_error() -> None: 

93 """Verify that getting an unknown tool raises ValueError with helpful message.""" 

94 with pytest.raises(ValueError, match="Unknown tool"): 

95 ToolRegistry.get("nonexistent-tool-xyz") 

96 

97 

98@pytest.mark.parametrize( 

99 "tool_name_variant", 

100 [ 

101 "ruff", 

102 "RUFF", 

103 "Ruff", 

104 "RuFf", 

105 ], 

106 ids=["lowercase", "uppercase", "capitalized", "mixed_case"], 

107) 

108def test_get_is_case_insensitive(tool_name_variant: str) -> None: 

109 """Verify that tool lookup is case-insensitive. 

110 

111 Args: 

112 tool_name_variant: Different case variations of tool names. 

113 """ 

114 tool = ToolRegistry.get(tool_name_variant) 

115 

116 assert_that(tool).is_not_none() 

117 assert_that(tool.definition.name.lower()).is_equal_to("ruff") 

118 

119 

120# ============================================================================= 

121# Tests for ToolRegistry.get_all 

122# ============================================================================= 

123 

124 

125def test_get_all_returns_non_empty_dict() -> None: 

126 """Verify that get_all returns a dictionary with registered tools.""" 

127 all_tools = ToolRegistry.get_all() 

128 

129 assert_that(all_tools).is_not_empty() 

130 assert_that("ruff" in all_tools).is_true() 

131 

132 

133def test_get_all_returns_instances() -> None: 

134 """Verify that get_all returns tool instances, not classes.""" 

135 all_tools = ToolRegistry.get_all() 

136 

137 for _name, tool in all_tools.items(): 

138 assert_that(tool).is_instance_of(BaseToolPlugin) 

139 

140 

141def test_get_all_tools_have_valid_names() -> None: 

142 """Verify that all returned tools have matching names.""" 

143 all_tools = ToolRegistry.get_all() 

144 

145 for name, tool in all_tools.items(): 

146 assert_that(tool.definition.name.lower()).is_equal_to(name.lower()) 

147 

148 

149# ============================================================================= 

150# Tests for ToolRegistry.get_definitions 

151# ============================================================================= 

152 

153 

154def test_get_definitions_returns_non_empty_dict() -> None: 

155 """Verify that get_definitions returns definitions for all tools.""" 

156 definitions = ToolRegistry.get_definitions() 

157 

158 assert_that(definitions).is_not_empty() 

159 

160 

161def test_get_definitions_returns_tool_definitions() -> None: 

162 """Verify that get_definitions returns ToolDefinition instances.""" 

163 definitions = ToolRegistry.get_definitions() 

164 

165 for _name, defn in definitions.items(): 

166 assert_that(defn).is_instance_of(ToolDefinition) 

167 

168 

169@pytest.mark.parametrize( 

170 "required_field", 

171 ["name", "description"], 

172 ids=["has_name", "has_description"], 

173) 

174def test_get_definitions_have_required_fields(required_field: str) -> None: 

175 """Verify that each definition has the required field populated. 

176 

177 Args: 

178 required_field: A field that should be present in tool definitions. 

179 """ 

180 definitions = ToolRegistry.get_definitions() 

181 

182 for _name, defn in definitions.items(): 

183 value = getattr(defn, required_field) 

184 assert_that(value).is_not_none() 

185 if isinstance(value, str): 

186 assert_that(value).is_not_empty() 

187 

188 

189# ============================================================================= 

190# Tests for ToolRegistry.get_names 

191# ============================================================================= 

192 

193 

194def test_get_names_returns_sorted_list() -> None: 

195 """Verify that get_names returns a sorted list of tool names.""" 

196 names = ToolRegistry.get_names() 

197 

198 assert_that(names).is_equal_to(sorted(names)) 

199 

200 

201def test_get_names_includes_builtin_tools() -> None: 

202 """Verify that get_names includes builtin tools like ruff.""" 

203 names = ToolRegistry.get_names() 

204 

205 assert_that("ruff" in names).is_true() 

206 

207 

208def test_get_names_returns_list_type() -> None: 

209 """Verify that get_names returns a list.""" 

210 names = ToolRegistry.get_names() 

211 

212 assert_that(names).is_instance_of(list) 

213 

214 

215# ============================================================================= 

216# Tests for ToolRegistry.is_registered 

217# ============================================================================= 

218 

219 

220@pytest.mark.parametrize( 

221 ("tool_name", "expected"), 

222 [ 

223 ("ruff", True), 

224 ("RUFF", True), 

225 ("nonexistent-xyz", False), 

226 ("", False), 

227 ], 

228 ids=[ 

229 "registered_lowercase", 

230 "registered_uppercase", 

231 "not_registered", 

232 "empty_name", 

233 ], 

234) 

235def test_is_registered_returns_correct_boolean(tool_name: str, expected: bool) -> None: 

236 """Verify that is_registered returns the correct boolean for various inputs. 

237 

238 Args: 

239 tool_name: The name of the tool to check. 

240 expected: The expected result of the check. 

241 """ 

242 result = ToolRegistry.is_registered(tool_name) 

243 

244 assert_that(result).is_equal_to(expected) 

245 

246 

247# ============================================================================= 

248# Tests for ToolRegistry.clear 

249# ============================================================================= 

250 

251 

252def test_clear_removes_all_tools(clean_registry: None) -> None: 

253 """Verify that clear removes all registered tools. 

254 

255 Args: 

256 clean_registry: Fixture to ensure clean registry state. 

257 """ 

258 # First register some tools 

259 plugin_class = create_fake_plugin(name="clear-test-tool") 

260 ToolRegistry.register(plugin_class) 

261 assert_that(ToolRegistry.is_registered("clear-test-tool")).is_true() 

262 

263 # Clear and verify 

264 ToolRegistry.clear() 

265 

266 assert_that(ToolRegistry._tools).is_empty() 

267 assert_that(ToolRegistry._instances).is_empty() 

268 

269 

270def test_clear_results_in_empty_registry(empty_registry: None) -> None: 

271 """Verify that an empty registry has no tools. 

272 

273 Args: 

274 empty_registry: Fixture that provides an empty registry. 

275 """ 

276 assert_that(ToolRegistry._tools).is_empty() 

277 assert_that(ToolRegistry._instances).is_empty() 

278 

279 

280# ============================================================================= 

281# Tests for ToolRegistry.get_check_tools 

282# ============================================================================= 

283 

284 

285def test_get_check_tools_returns_all_tools() -> None: 

286 """Verify that get_check_tools returns all tools (all support check).""" 

287 check_tools = ToolRegistry.get_check_tools() 

288 all_tools = ToolRegistry.get_all() 

289 

290 assert_that(check_tools).is_length(len(all_tools)) 

291 

292 

293def test_get_check_tools_returns_instances() -> None: 

294 """Verify that get_check_tools returns tool instances.""" 

295 check_tools = ToolRegistry.get_check_tools() 

296 

297 for _name, tool in check_tools.items(): 

298 assert_that(tool).is_instance_of(BaseToolPlugin) 

299 

300 

301# ============================================================================= 

302# Tests for ToolRegistry.get_fix_tools 

303# ============================================================================= 

304 

305 

306def test_get_fix_tools_returns_only_fix_capable() -> None: 

307 """Verify that get_fix_tools returns only tools that can fix.""" 

308 fix_tools = ToolRegistry.get_fix_tools() 

309 

310 for _name, tool in fix_tools.items(): 

311 assert_that(tool.definition.can_fix).is_true() 

312 

313 

314def test_get_fix_tools_excludes_non_fix_tools() -> None: 

315 """Verify that get_fix_tools excludes tools that cannot fix.""" 

316 fix_tools = ToolRegistry.get_fix_tools() 

317 all_tools = ToolRegistry.get_all() 

318 

319 non_fix_tools = [ 

320 name for name, tool in all_tools.items() if not tool.definition.can_fix 

321 ] 

322 for name in non_fix_tools: 

323 assert_that(name in fix_tools).is_false() 

324 

325 

326def test_get_fix_tools_is_subset_of_all_tools() -> None: 

327 """Verify that fix tools is a subset of all tools.""" 

328 fix_tools = ToolRegistry.get_fix_tools() 

329 all_tools = ToolRegistry.get_all() 

330 

331 assert_that(len(fix_tools)).is_less_than_or_equal_to(len(all_tools)) 

332 for name in fix_tools: 

333 assert_that(name in all_tools).is_true() 

334 

335 

336# ============================================================================= 

337# Tests for register_tool decorator 

338# ============================================================================= 

339 

340 

341def test_register_tool_decorator_registers_tool(clean_registry: None) -> None: 

342 """Verify that the register_tool decorator registers a tool class. 

343 

344 Args: 

345 clean_registry: Fixture that provides a clean registry. 

346 """ 

347 plugin_class = create_fake_plugin(name="decorator-test-tool") 

348 register_tool(plugin_class) 

349 

350 assert_that(ToolRegistry.is_registered("decorator-test-tool")).is_true() 

351 

352 

353def test_register_tool_decorator_returns_class(clean_registry: None) -> None: 

354 """Verify that the register_tool decorator returns the class unchanged. 

355 

356 Args: 

357 clean_registry: Fixture to ensure clean registry state. 

358 """ 

359 plugin_class = create_fake_plugin(name="decorator-return-test") 

360 result = register_tool(plugin_class) 

361 

362 assert_that(result).is_equal_to(plugin_class) 

363 

364 

365def test_register_tool_decorator_can_be_used_as_decorator( 

366 clean_registry: None, 

367) -> None: 

368 """Verify that register_tool works when used as a decorator. 

369 

370 Args: 

371 clean_registry: Fixture that provides a clean registry. 

372 """ 

373 

374 @dataclass 

375 @register_tool 

376 class DecoratorSyntaxPlugin(BaseToolPlugin): 

377 @property 

378 def definition(self) -> ToolDefinition: 

379 return ToolDefinition(name="decorator-syntax-test", description="Test") 

380 

381 def check(self, paths: list[str], options: dict[str, Any]) -> ToolResult: 

382 return ToolResult( 

383 name="decorator-syntax-test", 

384 success=True, 

385 output="", 

386 issues_count=0, 

387 ) 

388 

389 assert_that(ToolRegistry.is_registered("decorator-syntax-test")).is_true() 

390 tool = ToolRegistry.get("decorator-syntax-test") 

391 assert_that(tool).is_instance_of(DecoratorSyntaxPlugin) 

392 

393 

394# ============================================================================= 

395# Tests for edge cases and robustness 

396# ============================================================================= 

397 

398 

399def test_registry_handles_multiple_registrations(clean_registry: None) -> None: 

400 """Verify that registering multiple different tools works correctly. 

401 

402 Args: 

403 clean_registry: Fixture that clears the registry before the test. 

404 """ 

405 plugin1 = create_fake_plugin(name="multi-test-1") 

406 plugin2 = create_fake_plugin(name="multi-test-2") 

407 plugin3 = create_fake_plugin(name="multi-test-3") 

408 

409 ToolRegistry.register(plugin1) 

410 ToolRegistry.register(plugin2) 

411 ToolRegistry.register(plugin3) 

412 

413 assert_that(ToolRegistry.is_registered("multi-test-1")).is_true() 

414 assert_that(ToolRegistry.is_registered("multi-test-2")).is_true() 

415 assert_that(ToolRegistry.is_registered("multi-test-3")).is_true() 

416 

417 

418def test_get_returns_same_instance_on_multiple_calls() -> None: 

419 """Verify that get returns the same instance on multiple calls.""" 

420 tool1 = ToolRegistry.get("ruff") 

421 tool2 = ToolRegistry.get("ruff") 

422 

423 assert_that(tool1).is_same_as(tool2)