import os from fnmatch import fnmatch from pathlib import Path from typing import Iterator CPP_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx"}) DEFAULT_EXCLUDES = ( ".harness/build/**", "build/**", "out/**", "cmake-build-*/**", "third_party/**", "external/**", "vendor/**", "generated/**", ) def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> bool: try: relative = path.resolve().relative_to(root.resolve()).as_posix() except ValueError: return False return any(fnmatch(relative, pattern) for pattern in DEFAULT_EXCLUDES + extra) def _fully_excluded_directory( path: Path, root: Path, patterns: tuple[str, ...], ) -> bool: try: relative = path.resolve().relative_to(root).as_posix() except ValueError: return False for pattern in patterns: for suffix in ("/**", "\\**"): if pattern.endswith(suffix) and fnmatch(relative, pattern[:-3]): return True return False def _iter_files( root: Path, pruned_patterns: tuple[str, ...] = (), ) -> Iterator[Path]: for current, directories, files in os.walk(root, followlinks=False): current_path = Path(current) if pruned_patterns: directories[:] = [ name for name in directories if not _fully_excluded_directory( current_path / name, root, pruned_patterns, ) ] for name in files: path = current_path / name if path.is_file(): yield path def _iter_cpp_files( root: Path, extra_excludes: tuple[str, ...] = (), ) -> Iterator[Path]: root = root.resolve() excludes = DEFAULT_EXCLUDES + extra_excludes for path in _iter_files(root, excludes): if ( path.suffix.lower() in CPP_SUFFIXES and not is_excluded_path(path, root, extra_excludes) ): yield path def find_cpp_files( root: Path, extra_excludes: tuple[str, ...] = (), ) -> tuple[Path, ...]: return tuple(sorted(_iter_cpp_files(root, extra_excludes))) def has_cpp_files( root: Path, extra_excludes: tuple[str, ...] = (), ) -> bool: return next(_iter_cpp_files(root, extra_excludes), None) is not None def is_test_file(path: Path, root: Path, config) -> bool: resolved = path.resolve() if any(root == resolved or root in resolved.parents for root in config.test_roots): return True relative = resolved.relative_to(root.resolve()) if {"test", "tests"} & {part.lower() for part in relative.parts}: return True stem = resolved.stem.lower() return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test")) def _is_simple_filename_pattern(pattern: str) -> bool: pattern_path = Path(pattern) return ( not pattern_path.is_absolute() and len(pattern_path.parts) == 1 and pattern == pattern_path.name and "**" not in pattern and not pattern.endswith(("/", "\\")) ) class _TestFileIndex: def __init__(self) -> None: self._roots: dict[Path, Path] = {} self._files: dict[Path, list[Path]] = {} self._iterators: dict[Path, Iterator[Path]] = {} self._exhausted: set[Path] = set() self._path_matches: dict[tuple[Path, str], bool] = {} def _path_pattern_match( self, key: Path, root: Path, pattern: str, ) -> bool: cache_key = (key, pattern) if cache_key not in self._path_matches: self._path_matches[cache_key] = any( candidate.is_file() for candidate in root.rglob(pattern) ) return self._path_matches[cache_key] def matching_file_exists(self, root: Path, pattern: str) -> bool: key = root.resolve() traversal_root = self._roots.setdefault(key, root.absolute()) if not _is_simple_filename_pattern(pattern): return self._path_pattern_match(key, traversal_root, pattern) files = self._files.setdefault(key, []) if any(fnmatch(path.name, pattern) for path in files): return True if key in self._exhausted: return False iterator = self._iterators.setdefault(key, _iter_files(traversal_root)) for path in iterator: files.append(path) if fnmatch(path.name, pattern): return True self._exhausted.add(key) return False def _matching_test_exists(path: Path, config, index: _TestFileIndex) -> bool: names = tuple(pattern.format(stem=path.stem) for pattern in config.test_patterns) roots = ( *config.test_roots, path.parent / "tests", path.parent / "test", ) for root in roots: if not root.is_dir(): continue for name in names: if index.matching_file_exists(root, name): return True return False def matching_test_exists(path: Path, config) -> bool: return _matching_test_exists(path, config, _TestFileIndex()) def evaluate_paths(paths, root: Path, config) -> str | None: root = root.resolve() test_files = _TestFileIndex() for raw in paths: path = raw.resolve() try: path.relative_to(root) except ValueError: return f"TDD GUARD: '{path}' is outside the repository" if path.suffix.lower() not in CPP_SUFFIXES: continue if ( path.name.lower() == "main.cpp" or is_excluded_path(path, root, config.exclude) or is_test_file(path, root, config) ): continue if not _matching_test_exists(path, config, test_files): expected = config.test_patterns[0].format(stem=path.stem) return ( f"TDD GUARD: '{path.name}' requires an existing test such as " f"'{expected}'. Add the test in a configured test root first." ) return None