Skip to content

Commit da7f04e

Browse files
test: increase test coverage from 87% to 99% (#168)
* test: increase test coverage from 87% to 98% Add 30 new test cases covering previously untested code paths: test_util.py additions: - test_version_non_numeric: Version() with non-numeric input - test_version_full_semver: full semantic version parsing - test_version_major_only: major-only version parsing - test_version_major_minor: major.minor version parsing - test_verify_sha512_valid/invalid/with_filename: sha512 verification - test_download_file_http_error: HTTP error handling - test_download_file_value_error: invalid URL handling - test_download_file_bad_status: non-200 status handling - test_download_file_with_progress_bar: progress bar code path - test_check_install_os_unsupported: unsupported OS check test_install.py additions: - test_is_installed_found: tool detection success path - test_is_installed_wrong_major_version: version mismatch - test_is_installed_not_found_in_path: shutil.which returns None - test_install_tool_sha512_mismatch: sha512 invalid triggers re-download - test_move_and_chmod_bin: direct test of binary move+chmod - test_move_and_chmod_bin_create_dir: auto-creates install dir - test_create_symlink_with_target: explicit target parameter - test_uninstall_tool_direct/nonexistent: direct uninstall tests - test_uninstall_tool_with_dead_symlink: dead symlink cleanup - test_install_dir_name_default: default directory logic - test_install_clang_tools_path_not_in_env: PATH warning test_main.py additions: - test_main_uninstall: main() with --uninstall flag - test_main_install: main() with --install flag - test_main_install_invalid_version: non-semver version handling - test_main_no_args: no arguments shows help message test_wheel.py additions: - test_get_parser: parser configuration test - test_get_parser_requires_tool: --tool is required Coverage: 87% → 98% (212 statements, 4 remaining uncovered lines) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: resolve CI failures in test_install.py - Remove unused variable (ruff F841) - Fix to handle Linux default (~/.local/bin/) vs macOS/Windows default (sys.executable dir) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b1717da commit da7f04e

4 files changed

Lines changed: 406 additions & 2 deletions

File tree

tests/test_install.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pathlib import PurePath, Path
44
import os
5+
import subprocess
6+
from unittest.mock import Mock
57
import pytest
68
from clang_tools import install_arch, install_os, suffix
79
from clang_tools.install import (
@@ -11,6 +13,8 @@
1113
install_tool,
1214
install_clang_tools,
1315
is_installed,
16+
move_and_chmod_bin,
17+
uninstall_tool,
1418
uninstall_clang_tools,
1519
)
1620
from clang_tools.util import Version
@@ -126,3 +130,205 @@ def test_install_clang_tools_download_error(
126130
monkeypatch.setattr("clang_tools.install.binary_repo", "not-a-valid-url")
127131
with pytest.raises(OSError, match="Failed to download"):
128132
install_clang_tools(Version("12"), "clang-format", str(tmp_path), False, True)
133+
134+
135+
def test_is_installed_found(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
136+
"""Test is_installed when the tool exists and has a matching major version."""
137+
tool_name = "clang-format"
138+
version = Version("12")
139+
exe_name = f"{tool_name}-{version.info[0]}{suffix}"
140+
fake_bin = tmp_path / exe_name
141+
fake_bin.write_bytes(b"fake binary")
142+
fake_bin.chmod(0o755)
143+
144+
# Mock subprocess to return matching version output
145+
mock_result = Mock()
146+
mock_result.stdout = b"LLVM version 12.0.1\n"
147+
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: mock_result)
148+
monkeypatch.setattr("shutil.which", lambda x: str(fake_bin))
149+
150+
result = is_installed(tool_name, version)
151+
assert result == fake_bin.resolve()
152+
153+
154+
def test_is_installed_wrong_major_version(
155+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
156+
):
157+
"""Test is_installed when the tool exists but has a different major version."""
158+
tool_name = "clang-format"
159+
version = Version("12")
160+
exe_name = f"{tool_name}-{version.info[0]}{suffix}"
161+
fake_bin = tmp_path / exe_name
162+
fake_bin.write_bytes(b"fake binary")
163+
fake_bin.chmod(0o755)
164+
165+
# subprocess succeeds but returns a different major version
166+
mock_result = Mock()
167+
mock_result.stdout = b"LLVM version 14.0.0\n"
168+
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: mock_result)
169+
monkeypatch.setattr("shutil.which", lambda x: str(fake_bin))
170+
171+
result = is_installed(tool_name, version)
172+
assert result is None
173+
174+
175+
def test_is_installed_not_found_in_path(monkeypatch: pytest.MonkeyPatch):
176+
"""Test is_installed when the tool runs but shutil.which can't find it."""
177+
tool_name = "clang-format"
178+
version = Version("15")
179+
180+
mock_result = Mock()
181+
mock_result.stdout = b"LLVM version 15.0.0\n"
182+
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: mock_result)
183+
monkeypatch.setattr("shutil.which", lambda x: None)
184+
185+
result = is_installed(tool_name, version)
186+
assert result is None
187+
188+
189+
def test_install_tool_sha512_mismatch(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
190+
"""Test install_tool when existing binary has an invalid sha512 checksum.
191+
192+
This forces a re-download."""
193+
monkeypatch.chdir(tmp_path)
194+
tool_name = "clang-format"
195+
version = "12"
196+
bin_name = f"{tool_name}-{version}{suffix}"
197+
198+
# Pre-create the binary with invalid content
199+
existing_bin = tmp_path / bin_name
200+
existing_bin.write_bytes(b"corrupted binary data")
201+
202+
# install_tool should detect invalid sha, uninstall, and re-download
203+
assert install_tool(tool_name, version, str(tmp_path), False)
204+
assert existing_bin.exists()
205+
206+
207+
def test_move_and_chmod_bin(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
208+
"""Test move_and_chmod_bin directly."""
209+
monkeypatch.chdir(tmp_path)
210+
old_name = "downloaded-file"
211+
new_name = "clang-format-12"
212+
src_file = tmp_path / old_name
213+
src_file.write_bytes(b"binary content")
214+
215+
install_dir = tmp_path / "bin"
216+
move_and_chmod_bin(old_name, new_name, str(install_dir))
217+
218+
target = install_dir / new_name
219+
assert target.exists()
220+
assert not src_file.exists() # moved, not copied
221+
assert os.access(target, os.X_OK)
222+
223+
224+
def test_move_and_chmod_bin_create_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
225+
"""Test move_and_chmod_bin creates install directory if it doesn't exist."""
226+
monkeypatch.chdir(tmp_path)
227+
old_name = "downloaded-file"
228+
new_name = "clang-format-12"
229+
src_file = tmp_path / old_name
230+
src_file.write_bytes(b"binary content")
231+
232+
install_dir = tmp_path / "nested" / "bin"
233+
# directory does not exist yet
234+
assert not install_dir.exists()
235+
move_and_chmod_bin(old_name, new_name, str(install_dir))
236+
237+
target = install_dir / new_name
238+
assert target.exists()
239+
assert os.access(target, os.X_OK)
240+
241+
242+
def test_create_symlink_with_target(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
243+
"""Test create_sym_link with an explicit target parameter."""
244+
monkeypatch.chdir(str(tmp_path))
245+
tool_name = "clang-tool"
246+
version = "1"
247+
248+
# Create the target in a different location
249+
target_dir = tmp_path / "targets"
250+
target_dir.mkdir()
251+
target = target_dir / f"{tool_name}-{version}{suffix}"
252+
target.write_bytes(b"some binary data")
253+
254+
# Create symlink pointing to the explicit target
255+
assert create_sym_link(tool_name, version, str(tmp_path), False, target=target)
256+
link = tmp_path / f"{tool_name}{suffix}"
257+
assert link.is_symlink()
258+
assert link.resolve() == target
259+
260+
261+
def test_uninstall_tool_direct(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
262+
"""Test uninstall_tool directly."""
263+
monkeypatch.chdir(str(tmp_path))
264+
tool_name = "clang-format"
265+
version = "12"
266+
bin_name = f"{tool_name}-{version}{suffix}"
267+
tool_path = tmp_path / bin_name
268+
tool_path.write_bytes(b"binary")
269+
270+
assert tool_path.exists()
271+
uninstall_tool(tool_name, version, str(tmp_path))
272+
assert not tool_path.exists()
273+
274+
275+
def test_uninstall_tool_with_dead_symlink(
276+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
277+
):
278+
"""Test uninstall_tool cleans up a dead symlink."""
279+
monkeypatch.chdir(str(tmp_path))
280+
tool_name = "clang-format"
281+
version = "12"
282+
bin_name = f"{tool_name}-{version}{suffix}"
283+
284+
# Create the actual tool binary
285+
tool_path = tmp_path / bin_name
286+
tool_path.write_bytes(b"binary")
287+
288+
# Create a symlink
289+
link = tmp_path / f"{tool_name}{suffix}"
290+
link.symlink_to(tool_path)
291+
292+
# Remove the actual binary to make the symlink dead
293+
tool_path.unlink()
294+
295+
# Now the symlink exists but points to nothing
296+
assert link.is_symlink()
297+
assert not link.exists()
298+
299+
# uninstall_tool should clean up the dead symlink
300+
uninstall_tool(tool_name, version, str(tmp_path))
301+
assert not link.exists()
302+
303+
304+
def test_uninstall_tool_nonexistent(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
305+
"""Test uninstall_tool with a tool that doesn't exist (no-op)."""
306+
monkeypatch.chdir(str(tmp_path))
307+
# Should not raise any error
308+
uninstall_tool("clang-format", "99", str(tmp_path))
309+
310+
311+
def test_install_dir_name_default():
312+
"""Test install_dir_name returns proper default when no dir given."""
313+
result = install_dir_name("")
314+
if install_os == "linux":
315+
assert result == os.path.expanduser("~/.local/bin/")
316+
else:
317+
import sys
318+
319+
assert result == os.path.dirname(sys.executable)
320+
321+
322+
def test_install_clang_tools_path_not_in_env(
323+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture
324+
):
325+
"""Test install_clang_tools warns when install dir is not in PATH."""
326+
monkeypatch.chdir(tmp_path)
327+
monkeypatch.setattr("clang_tools.install.binary_repo", "not-a-valid-url")
328+
monkeypatch.setenv("PATH", "/usr/bin:/bin")
329+
330+
with pytest.raises(OSError):
331+
install_clang_tools(Version("12"), "clang-format", str(tmp_path), False, True)
332+
333+
result = capsys.readouterr()
334+
assert "directory is not in your environment variable PATH" in result.out

tests/test_main.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from typing import Optional, List
44
from argparse import ArgumentParser
5+
import sys
56
import pytest
6-
from clang_tools.main import get_parser
7+
from clang_tools import suffix
8+
from clang_tools.main import get_parser, main
79

810

911
class Args:
@@ -44,3 +46,80 @@ def test_default_args(parser: ArgumentParser):
4446
args = parser.parse_args([])
4547
for name, value in args.__dict__.items():
4648
assert getattr(Args, name) == value
49+
50+
51+
def test_main_uninstall(monkeypatch: pytest.MonkeyPatch, tmp_path, capsys):
52+
"""Test main() with --uninstall flag."""
53+
# Create a dummy bin to uninstall
54+
tool_name = "clang-format"
55+
version = "12"
56+
install_dir = str(tmp_path)
57+
dummy_bin = tmp_path / f"{tool_name}-{version}{suffix}"
58+
dummy_bin.write_bytes(b"dummy")
59+
60+
monkeypatch.setattr(
61+
sys,
62+
"argv",
63+
[
64+
"clang-tools",
65+
"--uninstall",
66+
version,
67+
"--tool",
68+
tool_name,
69+
"--directory",
70+
install_dir,
71+
],
72+
)
73+
main()
74+
# Verifies uninstall path was entered (printed the uninstall message)
75+
result = capsys.readouterr()
76+
assert "Uninstalling" in result.out
77+
78+
79+
def test_main_install(monkeypatch: pytest.MonkeyPatch, tmp_path):
80+
"""Test main() with --install flag."""
81+
monkeypatch.chdir(tmp_path)
82+
monkeypatch.setattr(
83+
sys,
84+
"argv",
85+
[
86+
"clang-tools",
87+
"--install",
88+
"12",
89+
"--tool",
90+
"clang-format",
91+
"--directory",
92+
str(tmp_path),
93+
"--no-progress-bar",
94+
],
95+
)
96+
main()
97+
# Binary should be installed
98+
bin_path = tmp_path / f"clang-format-12{suffix}"
99+
assert bin_path.exists()
100+
101+
102+
def test_main_install_invalid_version(monkeypatch: pytest.MonkeyPatch, capsys):
103+
"""Test main() with --install using a non-semver version."""
104+
monkeypatch.setattr(
105+
sys,
106+
"argv",
107+
[
108+
"clang-tools",
109+
"--install",
110+
"not-a-version",
111+
"--tool",
112+
"clang-format",
113+
],
114+
)
115+
main()
116+
result = capsys.readouterr()
117+
assert "not a semantic" in result.out
118+
119+
120+
def test_main_no_args(monkeypatch: pytest.MonkeyPatch, capsys):
121+
"""Test main() with no arguments shows help."""
122+
monkeypatch.setattr(sys, "argv", ["clang-tools"])
123+
main()
124+
result = capsys.readouterr()
125+
assert "Nothing to do" in result.out

0 commit comments

Comments
 (0)