Skip to content

Commit b6baba3

Browse files
committed
increase test coverage
1 parent 02670a3 commit b6baba3

File tree

1 file changed

+346
-2
lines changed

1 file changed

+346
-2
lines changed

tests/test_util.py

Lines changed: 346 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import logging
22
import pytest
33
from itertools import product
4-
from unittest.mock import patch
4+
from unittest.mock import patch, mock_open, MagicMock
5+
from pathlib import Path
6+
import subprocess
7+
import sys
58

6-
from cpp_linter_hooks.util import ensure_installed, is_installed
9+
from cpp_linter_hooks.util import (
10+
ensure_installed,
11+
is_installed,
12+
get_version_from_dependency,
13+
_resolve_version,
14+
_get_runtime_version,
15+
_install_tool,
16+
_resolve_install,
17+
CLANG_FORMAT_VERSIONS,
18+
CLANG_TIDY_VERSIONS,
19+
DEFAULT_CLANG_FORMAT_VERSION,
20+
DEFAULT_CLANG_TIDY_VERSION,
21+
)
722

823

924
VERSIONS = [None, "20"]
@@ -79,3 +94,332 @@ def test_ensure_installed_tool_not_found(caplog):
7994
"not found and could not be installed" in record.message
8095
for record in caplog.records
8196
)
97+
98+
99+
# Tests for get_version_from_dependency
100+
@pytest.mark.benchmark
101+
def test_get_version_from_dependency_success():
102+
"""Test get_version_from_dependency with valid pyproject.toml."""
103+
mock_toml_content = {
104+
"project": {
105+
"dependencies": [
106+
"clang-format==20.1.7",
107+
"clang-tidy==20.1.0",
108+
"other-package==1.0.0",
109+
]
110+
}
111+
}
112+
113+
with (
114+
patch("pathlib.Path.exists", return_value=True),
115+
patch("toml.load", return_value=mock_toml_content),
116+
):
117+
result = get_version_from_dependency("clang-format")
118+
assert result == "20.1.7"
119+
120+
result = get_version_from_dependency("clang-tidy")
121+
assert result == "20.1.0"
122+
123+
124+
@pytest.mark.benchmark
125+
def test_get_version_from_dependency_missing_file():
126+
"""Test get_version_from_dependency when pyproject.toml doesn't exist."""
127+
with patch("pathlib.Path.exists", return_value=False):
128+
result = get_version_from_dependency("clang-format")
129+
assert result is None
130+
131+
132+
@pytest.mark.benchmark
133+
def test_get_version_from_dependency_missing_dependency():
134+
"""Test get_version_from_dependency with missing dependency."""
135+
mock_toml_content = {"project": {"dependencies": ["other-package==1.0.0"]}}
136+
137+
with (
138+
patch("pathlib.Path.exists", return_value=True),
139+
patch("toml.load", return_value=mock_toml_content),
140+
):
141+
result = get_version_from_dependency("clang-format")
142+
assert result is None
143+
144+
145+
@pytest.mark.benchmark
146+
def test_get_version_from_dependency_malformed_toml():
147+
"""Test get_version_from_dependency with malformed toml."""
148+
mock_toml_content = {}
149+
150+
with (
151+
patch("pathlib.Path.exists", return_value=True),
152+
patch("toml.load", return_value=mock_toml_content),
153+
):
154+
result = get_version_from_dependency("clang-format")
155+
assert result is None
156+
157+
158+
# Tests for _resolve_version
159+
@pytest.mark.benchmark
160+
@pytest.mark.parametrize(
161+
"user_input,expected",
162+
[
163+
(None, None),
164+
("20", "20.1.7"), # Should find latest 20.x
165+
("20.1", "20.1.7"), # Should find latest 20.1.x
166+
("20.1.7", "20.1.7"), # Exact match
167+
("18", "18.1.8"), # Should find latest 18.x
168+
("18.1", "18.1.8"), # Should find latest 18.1.x
169+
("99", None), # Non-existent major version
170+
("20.99", None), # Non-existent minor version
171+
("invalid", None), # Invalid version string
172+
],
173+
)
174+
def test_resolve_version_clang_format(user_input, expected):
175+
"""Test _resolve_version with various inputs for clang-format."""
176+
result = _resolve_version(CLANG_FORMAT_VERSIONS, user_input)
177+
assert result == expected
178+
179+
180+
@pytest.mark.benchmark
181+
@pytest.mark.parametrize(
182+
"user_input,expected",
183+
[
184+
(None, None),
185+
("20", "20.1.0"), # Should find latest 20.x
186+
("18", "18.1.8"), # Should find latest 18.x
187+
("19", "19.1.0.1"), # Should find latest 19.x
188+
("99", None), # Non-existent major version
189+
],
190+
)
191+
def test_resolve_version_clang_tidy(user_input, expected):
192+
"""Test _resolve_version with various inputs for clang-tidy."""
193+
result = _resolve_version(CLANG_TIDY_VERSIONS, user_input)
194+
assert result == expected
195+
196+
197+
# Tests for _get_runtime_version
198+
@pytest.mark.benchmark
199+
def test_get_runtime_version_clang_format():
200+
"""Test _get_runtime_version for clang-format."""
201+
mock_output = "Ubuntu clang-format version 20.1.7-1ubuntu1\n"
202+
203+
with patch("subprocess.check_output", return_value=mock_output):
204+
result = _get_runtime_version("clang-format")
205+
assert result == "20.1.7-1ubuntu1"
206+
207+
208+
@pytest.mark.benchmark
209+
def test_get_runtime_version_clang_tidy():
210+
"""Test _get_runtime_version for clang-tidy."""
211+
mock_output = "LLVM (http://llvm.org/):\n LLVM version 20.1.0\n"
212+
213+
with patch("subprocess.check_output", return_value=mock_output):
214+
result = _get_runtime_version("clang-tidy")
215+
assert result == "20.1.0"
216+
217+
218+
@pytest.mark.benchmark
219+
def test_get_runtime_version_exception():
220+
"""Test _get_runtime_version when subprocess fails."""
221+
with patch(
222+
"subprocess.check_output",
223+
side_effect=subprocess.CalledProcessError(1, ["clang-format"]),
224+
):
225+
result = _get_runtime_version("clang-format")
226+
assert result is None
227+
228+
229+
@pytest.mark.benchmark
230+
def test_get_runtime_version_clang_tidy_single_line():
231+
"""Test _get_runtime_version for clang-tidy with single line output."""
232+
mock_output = "LLVM version 20.1.0\n"
233+
234+
with patch("subprocess.check_output", return_value=mock_output):
235+
result = _get_runtime_version("clang-tidy")
236+
assert result is None # Should return None for single line
237+
238+
239+
# Tests for _install_tool
240+
@pytest.mark.benchmark
241+
def test_install_tool_success():
242+
"""Test _install_tool successful installation."""
243+
mock_path = "/usr/bin/clang-format"
244+
245+
with (
246+
patch("subprocess.check_call") as mock_check_call,
247+
patch("shutil.which", return_value=mock_path),
248+
):
249+
result = _install_tool("clang-format", "20.1.7")
250+
assert result == mock_path
251+
252+
mock_check_call.assert_called_once_with(
253+
[sys.executable, "-m", "pip", "install", "clang-format==20.1.7"]
254+
)
255+
256+
257+
@pytest.mark.benchmark
258+
def test_install_tool_failure():
259+
"""Test _install_tool when pip install fails."""
260+
with (
261+
patch(
262+
"subprocess.check_call",
263+
side_effect=subprocess.CalledProcessError(1, ["pip"]),
264+
),
265+
patch("cpp_linter_hooks.util.LOG") as mock_log,
266+
):
267+
result = _install_tool("clang-format", "20.1.7")
268+
assert result is None
269+
270+
mock_log.error.assert_called_once_with(
271+
"Failed to install %s==%s", "clang-format", "20.1.7"
272+
)
273+
274+
275+
@pytest.mark.benchmark
276+
def test_install_tool_success_but_not_found():
277+
"""Test _install_tool when install succeeds but tool not found in PATH."""
278+
with patch("subprocess.check_call"), patch("shutil.which", return_value=None):
279+
result = _install_tool("clang-format", "20.1.7")
280+
assert result is None
281+
282+
283+
# Tests for _resolve_install
284+
@pytest.mark.benchmark
285+
def test_resolve_install_tool_already_installed_correct_version():
286+
"""Test _resolve_install when tool is already installed with correct version."""
287+
mock_path = "/usr/bin/clang-format"
288+
289+
with (
290+
patch("shutil.which", return_value=mock_path),
291+
patch("cpp_linter_hooks.util._get_runtime_version", return_value="20.1.7"),
292+
):
293+
result = _resolve_install("clang-format", "20.1.7")
294+
assert result == Path(mock_path)
295+
296+
297+
@pytest.mark.benchmark
298+
def test_resolve_install_tool_version_mismatch():
299+
"""Test _resolve_install when tool has wrong version."""
300+
mock_path = "/usr/bin/clang-format"
301+
302+
with (
303+
patch("shutil.which", return_value=mock_path),
304+
patch("cpp_linter_hooks.util._get_runtime_version", return_value="18.1.8"),
305+
patch(
306+
"cpp_linter_hooks.util._install_tool", return_value=Path(mock_path)
307+
) as mock_install,
308+
patch("cpp_linter_hooks.util.LOG") as mock_log,
309+
):
310+
result = _resolve_install("clang-format", "20.1.7")
311+
assert result == Path(mock_path)
312+
313+
mock_install.assert_called_once_with("clang-format", "20.1.7")
314+
mock_log.info.assert_called_once_with(
315+
"%s version mismatch (%s != %s), reinstalling...",
316+
"clang-format",
317+
"18.1.8",
318+
"20.1.7",
319+
)
320+
321+
322+
@pytest.mark.benchmark
323+
def test_resolve_install_tool_not_installed():
324+
"""Test _resolve_install when tool is not installed."""
325+
with (
326+
patch("shutil.which", return_value=None),
327+
patch(
328+
"cpp_linter_hooks.util._install_tool",
329+
return_value=Path("/usr/bin/clang-format"),
330+
) as mock_install,
331+
):
332+
result = _resolve_install("clang-format", "20.1.7")
333+
assert result == Path("/usr/bin/clang-format")
334+
335+
mock_install.assert_called_once_with("clang-format", "20.1.7")
336+
337+
338+
@pytest.mark.benchmark
339+
def test_resolve_install_no_version_specified():
340+
"""Test _resolve_install when no version is specified."""
341+
with (
342+
patch("shutil.which", return_value=None),
343+
patch(
344+
"cpp_linter_hooks.util._install_tool",
345+
return_value=Path("/usr/bin/clang-format"),
346+
) as mock_install,
347+
):
348+
result = _resolve_install("clang-format", None)
349+
assert result == Path("/usr/bin/clang-format")
350+
351+
mock_install.assert_called_once_with(
352+
"clang-format", DEFAULT_CLANG_FORMAT_VERSION
353+
)
354+
355+
356+
@pytest.mark.benchmark
357+
def test_resolve_install_invalid_version():
358+
"""Test _resolve_install with invalid version."""
359+
with (
360+
patch("shutil.which", return_value=None),
361+
patch(
362+
"cpp_linter_hooks.util._install_tool",
363+
return_value=Path("/usr/bin/clang-format"),
364+
) as mock_install,
365+
):
366+
result = _resolve_install("clang-format", "invalid.version")
367+
assert result == Path("/usr/bin/clang-format")
368+
369+
# Should fallback to default version
370+
mock_install.assert_called_once_with(
371+
"clang-format", DEFAULT_CLANG_FORMAT_VERSION
372+
)
373+
374+
375+
# Tests for ensure_installed edge cases
376+
@pytest.mark.benchmark
377+
def test_ensure_installed_version_mismatch(caplog):
378+
"""Test ensure_installed with version mismatch scenario."""
379+
mock_path = "/usr/bin/clang-format"
380+
381+
with (
382+
patch("shutil.which", return_value=mock_path),
383+
patch("cpp_linter_hooks.util._get_runtime_version", return_value="18.1.8"),
384+
patch("cpp_linter_hooks.util._install_tool", return_value=Path(mock_path)),
385+
):
386+
caplog.clear()
387+
caplog.set_level(logging.INFO, logger="cpp_linter_hooks.util")
388+
389+
result = ensure_installed("clang-format", "20.1.7")
390+
assert result == "clang-format"
391+
392+
# Should log version mismatch
393+
assert any("version mismatch" in record.message for record in caplog.records)
394+
395+
396+
@pytest.mark.benchmark
397+
def test_ensure_installed_no_runtime_version():
398+
"""Test ensure_installed when runtime version cannot be determined."""
399+
mock_path = "/usr/bin/clang-format"
400+
401+
with (
402+
patch("shutil.which", return_value=mock_path),
403+
patch("cpp_linter_hooks.util._get_runtime_version", return_value=None),
404+
):
405+
result = ensure_installed("clang-format", "20.1.7")
406+
assert result == "clang-format"
407+
408+
409+
# Tests for constants and defaults
410+
@pytest.mark.benchmark
411+
def test_default_versions():
412+
"""Test that default versions are set correctly."""
413+
assert DEFAULT_CLANG_FORMAT_VERSION is not None
414+
assert DEFAULT_CLANG_TIDY_VERSION is not None
415+
assert isinstance(DEFAULT_CLANG_FORMAT_VERSION, str)
416+
assert isinstance(DEFAULT_CLANG_TIDY_VERSION, str)
417+
418+
419+
@pytest.mark.benchmark
420+
def test_version_lists_not_empty():
421+
"""Test that version lists are not empty."""
422+
assert len(CLANG_FORMAT_VERSIONS) > 0
423+
assert len(CLANG_TIDY_VERSIONS) > 0
424+
assert all(isinstance(v, str) for v in CLANG_FORMAT_VERSIONS)
425+
assert all(isinstance(v, str) for v in CLANG_TIDY_VERSIONS)

0 commit comments

Comments
 (0)