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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Unit tests for plugins/registry module."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any
8import pytest
9from assertpy import assert_that
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
17# =============================================================================
18# Tests for ToolRegistry.register
19# =============================================================================
22def test_register_tool_adds_to_registry(clean_registry: None) -> None:
23 """Verify that registering a tool class adds it to the registry.
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)
31 assert_that(ToolRegistry.is_registered("test-register-tool")).is_true()
34def test_register_tool_stores_instance(clean_registry: None) -> None:
35 """Verify that registering a tool creates and stores an instance.
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)
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")
48def test_register_overwrite_replaces_tool(clean_registry: None) -> None:
49 """Verify that registering a tool with the same name replaces the previous one.
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")
57 ToolRegistry.register(plugin_class1)
58 ToolRegistry.register(plugin_class2)
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")
66def test_register_returns_class_unchanged(clean_registry: None) -> None:
67 """Verify that register returns the class unchanged for decorator use.
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)
75 assert_that(result).is_equal_to(plugin_class)
78# =============================================================================
79# Tests for ToolRegistry.get
80# =============================================================================
83def test_get_registered_tool_returns_instance() -> None:
84 """Verify that getting a registered tool returns its instance."""
85 tool = ToolRegistry.get("ruff")
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")
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")
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.
111 Args:
112 tool_name_variant: Different case variations of tool names.
113 """
114 tool = ToolRegistry.get(tool_name_variant)
116 assert_that(tool).is_not_none()
117 assert_that(tool.definition.name.lower()).is_equal_to("ruff")
120# =============================================================================
121# Tests for ToolRegistry.get_all
122# =============================================================================
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()
129 assert_that(all_tools).is_not_empty()
130 assert_that("ruff" in all_tools).is_true()
133def test_get_all_returns_instances() -> None:
134 """Verify that get_all returns tool instances, not classes."""
135 all_tools = ToolRegistry.get_all()
137 for _name, tool in all_tools.items():
138 assert_that(tool).is_instance_of(BaseToolPlugin)
141def test_get_all_tools_have_valid_names() -> None:
142 """Verify that all returned tools have matching names."""
143 all_tools = ToolRegistry.get_all()
145 for name, tool in all_tools.items():
146 assert_that(tool.definition.name.lower()).is_equal_to(name.lower())
149# =============================================================================
150# Tests for ToolRegistry.get_definitions
151# =============================================================================
154def test_get_definitions_returns_non_empty_dict() -> None:
155 """Verify that get_definitions returns definitions for all tools."""
156 definitions = ToolRegistry.get_definitions()
158 assert_that(definitions).is_not_empty()
161def test_get_definitions_returns_tool_definitions() -> None:
162 """Verify that get_definitions returns ToolDefinition instances."""
163 definitions = ToolRegistry.get_definitions()
165 for _name, defn in definitions.items():
166 assert_that(defn).is_instance_of(ToolDefinition)
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.
177 Args:
178 required_field: A field that should be present in tool definitions.
179 """
180 definitions = ToolRegistry.get_definitions()
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()
189# =============================================================================
190# Tests for ToolRegistry.get_names
191# =============================================================================
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()
198 assert_that(names).is_equal_to(sorted(names))
201def test_get_names_includes_builtin_tools() -> None:
202 """Verify that get_names includes builtin tools like ruff."""
203 names = ToolRegistry.get_names()
205 assert_that("ruff" in names).is_true()
208def test_get_names_returns_list_type() -> None:
209 """Verify that get_names returns a list."""
210 names = ToolRegistry.get_names()
212 assert_that(names).is_instance_of(list)
215# =============================================================================
216# Tests for ToolRegistry.is_registered
217# =============================================================================
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.
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)
244 assert_that(result).is_equal_to(expected)
247# =============================================================================
248# Tests for ToolRegistry.clear
249# =============================================================================
252def test_clear_removes_all_tools(clean_registry: None) -> None:
253 """Verify that clear removes all registered tools.
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()
263 # Clear and verify
264 ToolRegistry.clear()
266 assert_that(ToolRegistry._tools).is_empty()
267 assert_that(ToolRegistry._instances).is_empty()
270def test_clear_results_in_empty_registry(empty_registry: None) -> None:
271 """Verify that an empty registry has no tools.
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()
280# =============================================================================
281# Tests for ToolRegistry.get_check_tools
282# =============================================================================
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()
290 assert_that(check_tools).is_length(len(all_tools))
293def test_get_check_tools_returns_instances() -> None:
294 """Verify that get_check_tools returns tool instances."""
295 check_tools = ToolRegistry.get_check_tools()
297 for _name, tool in check_tools.items():
298 assert_that(tool).is_instance_of(BaseToolPlugin)
301# =============================================================================
302# Tests for ToolRegistry.get_fix_tools
303# =============================================================================
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()
310 for _name, tool in fix_tools.items():
311 assert_that(tool.definition.can_fix).is_true()
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()
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()
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()
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()
336# =============================================================================
337# Tests for register_tool decorator
338# =============================================================================
341def test_register_tool_decorator_registers_tool(clean_registry: None) -> None:
342 """Verify that the register_tool decorator registers a tool class.
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)
350 assert_that(ToolRegistry.is_registered("decorator-test-tool")).is_true()
353def test_register_tool_decorator_returns_class(clean_registry: None) -> None:
354 """Verify that the register_tool decorator returns the class unchanged.
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)
362 assert_that(result).is_equal_to(plugin_class)
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.
370 Args:
371 clean_registry: Fixture that provides a clean registry.
372 """
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")
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 )
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)
394# =============================================================================
395# Tests for edge cases and robustness
396# =============================================================================
399def test_registry_handles_multiple_registrations(clean_registry: None) -> None:
400 """Verify that registering multiple different tools works correctly.
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")
409 ToolRegistry.register(plugin1)
410 ToolRegistry.register(plugin2)
411 ToolRegistry.register(plugin3)
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()
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")
423 assert_that(tool1).is_same_as(tool2)