#!/usr/bin/env python3 """Block "shadow tests": test modules that never import the SUT. A shadow test imports nothing from ``local_deep_research`` and therefore exercises no production code. Such files test inline reimplementations, pure stdlib behaviour, or locally-built dicts — inflating the test count and coverage metrics while catching zero real regressions. PRs #4239, #4242, #4243 removed ~80 such files; this hook stops them creeping back. Detection (AST-based): a file whose basename matches the test-module pattern (``test_*.py`` / ``*_test.py``) is flagged when none of its ``import`` / ``from ... import`` statements reference ``local_deep_research`` (or ``src.local_deep_research``). Imports under ``if TYPE_CHECKING:`` don't run, so they don't count as exercising the SUT. Opt-out: a small number of files legitimately have no SUT import — e.g. guardian tests that assert on repository structure or CI workflow files. Add a marker line to exempt one:: # allow: no-sut-import — The reason text is required so the exemption is self-documenting. """ import ast import re import sys from pathlib import Path # Marker that exempts a file, mirroring the `# allow: unmarked-sleep` # convention used by check-unmarked-sleep.py. A trailing reason is required. ALLOW_RE = re.compile(r"#\s*allow:\s*no-sut-import\b\s*[-—:]\s*\S+") SUT_ROOTS = ("local_deep_research", "src.local_deep_research") # Only emit ANSI colour when stdout is a TTY (matches recommend-release-notes.py); # CI log viewers and Windows consoles then get clean plain text. _USE_COLOR = sys.stdout.isatty() _RED = "\033[31m" if _USE_COLOR else "" _RESET = "\033[0m" if _USE_COLOR else "" def is_test_module(path: str) -> bool: """True for pytest test modules (not conftest/__init__/helpers).""" # Path().name handles both / and \ so the hook behaves the same # if it's ever invoked outside pre-commit (which normalizes to POSIX). name = Path(path).name if not name.endswith(".py"): return False return name.startswith("test_") or name.endswith("_test.py") def _references_sut(name: str | None) -> bool: if not name: return False return any( name == root or name.startswith(root + ".") for root in SUT_ROOTS ) def _is_type_checking_guard(node: ast.If) -> bool: """True for ``if TYPE_CHECKING:`` / ``if typing.TYPE_CHECKING:``.""" test = node.test if isinstance(test, ast.Name): return test.id == "TYPE_CHECKING" if isinstance(test, ast.Attribute): return test.attr == "TYPE_CHECKING" return False def imports_sut(tree: ast.AST) -> bool: """True if any *runtime* import statement pulls from the SUT package. Imports under ``if TYPE_CHECKING:`` don't execute, so a test whose only SUT reference is a type-hint import still exercises no production code — those guard bodies are skipped (but the runtime ``else`` branch is not). """ stack: list[ast.AST] = [tree] while stack: node = stack.pop() if isinstance(node, ast.Import): if any(_references_sut(alias.name) for alias in node.names): return True continue if isinstance(node, ast.ImportFrom): # Absolute import from the package (level 0); relative imports # inside tests/ never reach the installed package, so ignore. if node.level == 0 and _references_sut(node.module): return True continue if isinstance(node, ast.If) and _is_type_checking_guard(node): # Skip the type-only body; the else branch runs at runtime. stack.extend(node.orelse) continue stack.extend(ast.iter_child_nodes(node)) return False def check_file(path: str) -> bool: """Return True if `path` is an unmarked shadow test.""" if not is_test_module(path): return False try: with open(path, encoding="utf-8") as fh: source = fh.read() except (OSError, UnicodeDecodeError): return False if ALLOW_RE.search(source): return False try: tree = ast.parse(source, filename=path) except SyntaxError: # Let ruff / other hooks report syntax errors. return False return not imports_sut(tree) def main(argv: list[str]) -> int: shadow = [p for p in argv if check_file(p)] if not shadow: return 0 print(f"{_RED}Shadow tests detected{_RESET}") print("=" * 60) print( "These test modules import nothing from `local_deep_research`, so\n" "they exercise no production code (see CONTRIBUTING.md → Testing):\n" ) for path in shadow: print(f" - {path}") print() print("Fix one of these ways:") print(" 1. Import and exercise the real code under test, or") print(" 2. If the file legitimately has no SUT import (e.g. a guardian") print(" test), add a marker line stating why:") print(" # allow: no-sut-import — ") print() return 1 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))