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
« 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."""
3from __future__ import annotations
5from unittest.mock import MagicMock, patch
7import pytest
8from assertpy import assert_that
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)
24@pytest.fixture(autouse=True)
25def _clear_cache_before_each_test() -> None:
26 """Clear discovery cache before each test."""
27 clear_discovery_cache()
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")
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")
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")
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")
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()
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)
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()
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)
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")
85def test_discover_tool_handles_timeout() -> None:
86 """Tool is unavailable when version probe times out."""
87 import subprocess
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)
98 assert_that(result.available).is_false()
99 assert_that(result.version).is_none()
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="")
110 discover_tool("ruff")
111 discover_tool("ruff")
113 assert_that(mock_which.call_count).is_equal_to(1)
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="")
124 tools = discover_all_tools(use_cache=False)
126 assert_that(len(tools)).is_greater_than(5)
127 assert_that(tools).contains_key("ruff", "black", "mypy")
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()
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()
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")
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()
162def test_get_unavailable_tools() -> None:
163 """get_unavailable_tools returns list of missing tools."""
165 def mock_which(name: str) -> str | None:
166 return "/usr/bin/ruff" if name == "ruff" else None
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)
178def test_get_available_tools() -> None:
179 """get_available_tools returns list of available tools."""
181 def mock_which(name: str) -> str | None:
182 return "/usr/bin/ruff" if name == "ruff" else None
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")
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")
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="")
212 discover_tool("ruff")
213 initial_count = mock_which.call_count
215 clear_discovery_cache()
216 discover_tool("ruff")
218 assert_that(mock_which.call_count).is_equal_to(initial_count + 1)
221def test_discovered_tool_default_values() -> None:
222 """DiscoveredTool has correct defaults."""
223 tool = DiscoveredTool(name="test")
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()