Files
Agentic-AI-Template/Codex/Coding/harness framework cpp/scripts/msvc_harness/tdd_policy.py
T
2026-09-08 10:30:07 +09:00

201 lines
6.0 KiB
Python

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