from fnmatch import fnmatch from pathlib import Path 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 find_cpp_files( root: Path, extra_excludes: tuple[str, ...] = (), ) -> tuple[Path, ...]: found = ( path for path in root.resolve().rglob("*") if path.is_file() and path.suffix.lower() in CPP_SUFFIXES and not is_excluded_path(path, root, extra_excludes) ) return tuple(sorted(found)) 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 matching_test_exists(path: Path, config) -> 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 any(candidate.is_file() for candidate in root.rglob(name)): return True return False def evaluate_paths(paths, root: Path, config) -> str | None: root = root.resolve() 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): 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