modify harness framework
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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 = (
|
||||
@@ -22,18 +24,70 @@ def is_excluded_path(path: Path, root: Path, extra: tuple[str, ...] = ()) -> boo
|
||||
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, ...]:
|
||||
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))
|
||||
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:
|
||||
@@ -47,7 +101,60 @@ def is_test_file(path: Path, root: Path, config) -> bool:
|
||||
return stem.startswith("test_") or stem.endswith(("_test", "_tests", ".test"))
|
||||
|
||||
|
||||
def matching_test_exists(path: Path, config) -> bool:
|
||||
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,
|
||||
@@ -58,13 +165,18 @@ def matching_test_exists(path: Path, config) -> bool:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for name in names:
|
||||
if any(candidate.is_file() for candidate in root.rglob(name)):
|
||||
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:
|
||||
@@ -79,7 +191,7 @@ def evaluate_paths(paths, root: Path, config) -> str | None:
|
||||
or is_test_file(path, root, config)
|
||||
):
|
||||
continue
|
||||
if not matching_test_exists(path, config):
|
||||
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 "
|
||||
|
||||
Reference in New Issue
Block a user