Files
claude-scripts/tests/hooks/test_comprehensive_scenarios.py
Travis Vasceannie 4ac9b1c5e1 Refactor: move hooks to quality package
- Move Claude Code hooks under src/quality/hooks (rename modules)
- Add a project-local installer for Claude Code hooks
- Introduce internal_duplicate_detector and code_quality_guard
- Update tests to reference new module paths and guard API
- Bump package version to 0.1.1 and adjust packaging
2025-10-26 22:15:04 +00:00

593 lines
19 KiB
Python

"""Comprehensive test suite covering all hook interaction scenarios."""
# ruff: noqa: SLF001
# pyright: reportPrivateUsage=false, reportPrivateImportUsage=false, reportPrivateLocalImportUsage=false, reportUnusedCallResult=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownLambdaType=false, reportUnknownMemberType=false
from __future__ import annotations
import json
import os
import subprocess
from collections.abc import Mapping
from pathlib import Path
from tempfile import gettempdir
import pytest
from quality.hooks import code_quality_guard as guard
class TestProjectStructureVariations:
"""Test different project structure layouts."""
def test_flat_layout_no_src(self) -> None:
"""Project without src/ directory."""
root = Path.home() / f"test_flat_{os.getpid()}"
try:
root.mkdir()
(root / ".venv/bin").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "main.py"
test_file.write_text("# test")
# Should find project root
found_root = guard._find_project_root(str(test_file))
assert found_root == root
# Should create .tmp in root
tmp_dir = guard._get_project_tmp_dir(str(test_file))
assert tmp_dir == root / ".tmp"
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_src_layout(self) -> None:
"""Project with src/ directory."""
root = Path.home() / f"test_src_{os.getpid()}"
try:
(root / "src/package").mkdir(parents=True)
(root / ".venv/bin").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "src/package/module.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
venv_bin = guard._get_project_venv_bin(str(test_file))
assert venv_bin == root / ".venv/bin"
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_nested_projects_uses_closest(self) -> None:
"""Nested projects should use closest .venv."""
outer = Path.home() / f"test_outer_{os.getpid()}"
try:
# Outer project
(outer / ".venv/bin").mkdir(parents=True)
(outer / ".git").mkdir()
# Inner project
inner = outer / "subproject"
(inner / ".venv/bin").mkdir(parents=True)
(inner / "pyproject.toml").touch()
test_file = inner / "main.py"
test_file.write_text("# test")
# Should find inner project root
found_root = guard._find_project_root(str(test_file))
assert found_root == inner
# Should use inner venv
venv_bin = guard._get_project_venv_bin(str(test_file))
assert venv_bin == inner / ".venv/bin"
finally:
import shutil
if outer.exists():
shutil.rmtree(outer)
def test_no_project_markers_uses_parent(self) -> None:
"""File with no project markers searches up to filesystem root."""
root = Path.home() / f"test_nomarkers_{os.getpid()}"
try:
(root / "subdir").mkdir(parents=True)
test_file = root / "subdir/file.py"
test_file.write_text("# test")
# With no markers, searches all the way up
# (may find .git in home directory or elsewhere)
found_root = guard._find_project_root(str(test_file))
# Should at least not crash
assert isinstance(found_root, Path)
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_deeply_nested_file(self) -> None:
"""File deeply nested finds root correctly."""
root = Path.home() / f"test_deep_{os.getpid()}"
try:
deep = root / "a/b/c/d/e/f"
deep.mkdir(parents=True)
(root / ".git").mkdir()
test_file = deep / "module.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestConfigurationInheritance:
"""Test configuration file inheritance."""
def test_pyrightconfig_in_root(self) -> None:
"""pyrightconfig.json at project root is found."""
root = Path.home() / f"test_pyright_{os.getpid()}"
try:
(root / "src").mkdir(parents=True)
(root / ".venv/bin").mkdir(parents=True)
config = {"reportUnknownMemberType": False}
(root / "pyrightconfig.json").write_text(json.dumps(config))
test_file = root / "src/mod.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
assert (found_root / "pyrightconfig.json").exists()
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_pyproject_toml_as_marker(self) -> None:
"""pyproject.toml serves as project marker."""
root = Path.home() / f"test_pyproj_{os.getpid()}"
try:
root.mkdir()
(root / "pyproject.toml").write_text("[tool.mypy]\n")
test_file = root / "main.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_gitignore_updated_for_tmp(self) -> None:
""".tmp/ is added to .gitignore if not present."""
root = Path.home() / f"test_gitignore_{os.getpid()}"
try:
root.mkdir()
(root / "pyproject.toml").touch()
(root / ".gitignore").write_text("*.pyc\n__pycache__/\n")
test_file = root / "main.py"
test_file.write_text("# test")
tmp_dir = guard._get_project_tmp_dir(str(test_file))
assert tmp_dir.exists()
gitignore_content = (root / ".gitignore").read_text()
assert ".tmp/" in gitignore_content
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_gitignore_not_modified_if_tmp_present(self) -> None:
""".gitignore not modified if .tmp already present."""
root = Path.home() / f"test_gitignore2_{os.getpid()}"
try:
root.mkdir()
(root / "pyproject.toml").touch()
original = "*.pyc\n.tmp/\n"
(root / ".gitignore").write_text(original)
test_file = root / "main.py"
test_file.write_text("# test")
_ = guard._get_project_tmp_dir(str(test_file))
# Should not have been modified
assert (root / ".gitignore").read_text() == original
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestVirtualEnvironmentEdgeCases:
"""Test virtual environment edge cases."""
def test_venv_missing_fallback_to_claude_scripts(self) -> None:
"""No .venv in project falls back."""
root = Path.home() / f"test_novenv_{os.getpid()}"
try:
root.mkdir()
(root / "pyproject.toml").touch()
test_file = root / "main.py"
test_file.write_text("# test")
venv_bin = guard._get_project_venv_bin(str(test_file))
# Should not be in the test project
assert str(root) not in str(venv_bin)
# Should be a valid path
assert venv_bin.name == "bin"
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_venv_exists_but_no_bin(self) -> None:
""".venv exists but bin/ directory missing."""
root = Path.home() / f"test_nobin_{os.getpid()}"
try:
(root / ".venv").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "main.py"
test_file.write_text("# test")
venv_bin = guard._get_project_venv_bin(str(test_file))
# Should fallback since bin/ doesn't exist in project
assert str(root) not in str(venv_bin)
assert venv_bin.name == "bin"
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_pythonpath_not_set_without_src(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""PYTHONPATH not set when src/ doesn't exist."""
root = Path.home() / f"test_nosrc_{os.getpid()}"
try:
(root / ".venv/bin").mkdir(parents=True)
(root / "pyproject.toml").touch()
tool = root / ".venv/bin/basedpyright"
tool.write_text("#!/bin/bash\necho fake")
tool.chmod(0o755)
test_file = root / "main.py"
test_file.write_text("# test")
captured_env: dict[str, str] = {}
def capture_run(
cmd: list[str],
**kw: object,
) -> subprocess.CompletedProcess[str]:
env_obj = kw.get("env")
if isinstance(env_obj, Mapping):
captured_env.update({str(k): str(v) for k, v in env_obj.items()})
return subprocess.CompletedProcess(list(cmd), 0, stdout="", stderr="")
monkeypatch.setattr(guard.subprocess, "run", capture_run)
guard._run_type_checker(
"basedpyright",
str(test_file),
guard.QualityConfig(),
original_file_path=str(test_file),
)
# PYTHONPATH should not be set (or not include src)
if "PYTHONPATH" in captured_env:
assert "src" not in captured_env["PYTHONPATH"]
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestTypeCheckerIntegration:
"""Test type checker tool integration."""
def test_all_tools_disabled(self) -> None:
"""All type checkers disabled returns no issues."""
config = guard.QualityConfig(
basedpyright_enabled=False,
pyrefly_enabled=False,
sourcery_enabled=False,
)
issues = guard.run_type_checks("test.py", config)
assert issues == []
def test_tool_not_found_returns_warning(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Missing tool returns warning, doesn't crash."""
monkeypatch.setattr(guard.Path, "exists", lambda _: False, raising=False)
monkeypatch.setattr(guard, "_ensure_tool_installed", lambda _: False)
success, message = guard._run_type_checker(
"basedpyright",
"test.py",
guard.QualityConfig(),
)
assert success is True
assert "not available" in message
def test_tool_timeout_handled(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Tool timeout is handled gracefully."""
monkeypatch.setattr(guard.Path, "exists", lambda _: True, raising=False)
def timeout_run(*_args: object, **_kw: object) -> None:
raise subprocess.TimeoutExpired(cmd=["tool"], timeout=30)
monkeypatch.setattr(guard.subprocess, "run", timeout_run)
success, message = guard._run_type_checker(
"basedpyright",
"test.py",
guard.QualityConfig(),
)
assert success is True
assert "timeout" in message.lower()
def test_tool_os_error_handled(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""OS errors from tools are handled."""
monkeypatch.setattr(guard.Path, "exists", lambda _: True, raising=False)
def error_run(*_args: object, **_kw: object) -> None:
message = "Permission denied"
raise OSError(message)
monkeypatch.setattr(guard.subprocess, "run", error_run)
success, message = guard._run_type_checker(
"basedpyright",
"test.py",
guard.QualityConfig(),
)
assert success is True
assert "execution error" in message.lower()
def test_unknown_tool_returns_warning(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Unknown tool name returns warning."""
# Mock tool not existing
monkeypatch.setattr(guard.Path, "exists", lambda _: False, raising=False)
monkeypatch.setattr(guard, "_ensure_tool_installed", lambda _: False)
success, message = guard._run_type_checker(
"unknown_tool",
"test.py",
guard.QualityConfig(),
)
assert success is True
assert "not available" in message.lower()
class TestWorkingDirectoryScenarios:
"""Test different working directory scenarios."""
def test_cwd_set_to_project_root(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Type checker runs with cwd=project_root."""
root = Path.home() / f"test_cwd2_{os.getpid()}"
try:
(root / "src").mkdir(parents=True)
(root / ".venv/bin").mkdir(parents=True)
(root / "pyrightconfig.json").touch()
tool = root / ".venv/bin/basedpyright"
tool.write_text("#!/bin/bash\npwd")
tool.chmod(0o755)
test_file = root / "src/mod.py"
test_file.write_text("# test")
captured_cwd: list[Path] = []
def capture_run(
cmd: list[str],
**kw: object,
) -> subprocess.CompletedProcess[str]:
cwd_obj = kw.get("cwd")
if cwd_obj is not None:
captured_cwd.append(Path(str(cwd_obj)))
return subprocess.CompletedProcess(list(cmd), 0, stdout="", stderr="")
monkeypatch.setattr(guard.subprocess, "run", capture_run)
guard._run_type_checker(
"basedpyright",
str(test_file),
guard.QualityConfig(),
original_file_path=str(test_file),
)
assert captured_cwd
assert captured_cwd[0] == root
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestErrorConditions:
"""Test error handling scenarios."""
def test_invalid_syntax_in_content(self) -> None:
"""Invalid Python syntax is detected."""
issues = guard._detect_any_usage("def broken(:\n pass")
# Should still check for Any even with syntax error
assert isinstance(issues, list)
def test_tmp_dir_creation_permission_error(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Permission error creating .tmp is handled."""
def raise_permission(*_args: object, **_kw: object) -> None:
message = "Cannot create directory"
raise PermissionError(message)
monkeypatch.setattr(Path, "mkdir", raise_permission)
# Should raise and be caught by caller
with pytest.raises(PermissionError):
guard._get_project_tmp_dir("/some/file.py")
def test_empty_file_content(self) -> None:
"""Empty file content is handled."""
root = Path.home() / f"test_empty_{os.getpid()}"
try:
(root / ".venv/bin").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "empty.py"
test_file.write_text("")
# Should not crash
tmp_dir = guard._get_project_tmp_dir(str(test_file))
assert tmp_dir.exists()
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestFileLocationVariations:
"""Test files in various locations."""
def test_file_in_tests_directory(self) -> None:
"""Test files are handled correctly."""
root = Path.home() / f"test_tests_{os.getpid()}"
try:
(root / "tests").mkdir(parents=True)
(root / ".git").mkdir()
test_file = root / "tests/test_module.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
# Test file detection
assert guard.is_test_file(str(test_file))
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_file_in_project_root(self) -> None:
"""File directly in project root."""
root = Path.home() / f"test_rootfile_{os.getpid()}"
try:
root.mkdir()
(root / ".git").mkdir()
test_file = root / "main.py"
test_file.write_text("# test")
found_root = guard._find_project_root(str(test_file))
assert found_root == root
finally:
import shutil
if root.exists():
shutil.rmtree(root)
class TestTempFileManagement:
"""Test temporary file handling."""
def test_temp_files_cleaned_up(self) -> None:
"""Temp files are deleted after analysis."""
root = Path.home() / f"test_cleanup_{os.getpid()}"
try:
(root / "src").mkdir(parents=True)
(root / ".venv/bin").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "src/mod.py"
test_file.write_text("def foo(): pass")
tmp_dir = root / ".tmp"
# Analyze code (should create and delete temp file)
config = guard.QualityConfig(
duplicate_enabled=False,
complexity_enabled=False,
modernization_enabled=False,
basedpyright_enabled=False,
pyrefly_enabled=False,
sourcery_enabled=False,
)
guard.analyze_code_quality(
"def foo(): pass",
str(test_file),
config,
enable_type_checks=False,
)
# .tmp directory should exist but temp file should be gone
if tmp_dir.exists():
temp_files = list(tmp_dir.glob("hook_validation_*"))
assert not temp_files
finally:
import shutil
if root.exists():
shutil.rmtree(root)
def test_temp_file_in_correct_location(self) -> None:
"""Temp files created in project .tmp/ not /tmp."""
root = Path.home() / f"test_tmploc_{os.getpid()}"
try:
(root / "src").mkdir(parents=True)
(root / "pyproject.toml").touch()
test_file = root / "src/mod.py"
test_file.write_text("# test")
tmp_dir = guard._get_project_tmp_dir(str(test_file))
# Should be in project, not /tmp
assert str(tmp_dir).startswith(str(root))
assert not str(tmp_dir).startswith(gettempdir())
finally:
import shutil
if root.exists():
shutil.rmtree(root)
if __name__ == "__main__":
pytest.main([__file__, "-v"])