chore: merge dev into eigen execution prep
This commit is contained in:
@@ -1,16 +1,9 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from string import Formatter
|
||||
from typing import Any
|
||||
|
||||
from .models import CMakeConfig, HarnessConfig, MSBuildConfig, TDDConfig
|
||||
|
||||
DEFAULT_TEST_PATTERNS = (
|
||||
"{stem}_test.cpp",
|
||||
"{stem}_tests.cpp",
|
||||
"test_{stem}.cpp",
|
||||
"{stem}.test.cpp",
|
||||
)
|
||||
from .models import CMakeConfig, HarnessConfig, MSBuildConfig
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
@@ -60,38 +53,6 @@ def _string_array(value: Any, label: str) -> tuple[str, ...]:
|
||||
)
|
||||
|
||||
|
||||
def _validate_test_pattern(pattern: str) -> None:
|
||||
candidate = Path(pattern)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or bool(candidate.drive)
|
||||
or "/" in pattern
|
||||
or "\\" in pattern
|
||||
or candidate.name != pattern
|
||||
):
|
||||
raise ConfigError(
|
||||
"every tdd.testPatterns entry must be a safe basename pattern"
|
||||
)
|
||||
try:
|
||||
fields = [
|
||||
(field_name, format_spec, conversion)
|
||||
for _, field_name, format_spec, conversion in Formatter().parse(pattern)
|
||||
if field_name is not None
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise ConfigError(f"invalid tdd.testPatterns entry: {exc}") from exc
|
||||
if (
|
||||
not fields
|
||||
or any(
|
||||
field_name != "stem" or format_spec or conversion
|
||||
for field_name, format_spec, conversion in fields
|
||||
)
|
||||
):
|
||||
raise ConfigError(
|
||||
"every tdd.testPatterns entry may contain only the {stem} field"
|
||||
)
|
||||
|
||||
|
||||
def load_config(root: Path) -> HarnessConfig:
|
||||
root = root.resolve()
|
||||
config_path = root / ".harness" / "config.json"
|
||||
@@ -166,21 +127,12 @@ def load_config(root: Path) -> HarnessConfig:
|
||||
else None
|
||||
)
|
||||
|
||||
tdd_data = _mapping(data.get("tdd", {}), "tdd")
|
||||
_reject_unknown(tdd_data, {"testRoots", "testPatterns", "exclude"}, "tdd")
|
||||
raw_test_roots = _string_array(tdd_data["testRoots"], "tdd.testRoots") if "testRoots" in tdd_data else ("tests", "test")
|
||||
test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in raw_test_roots)
|
||||
test_patterns = _string_array(tdd_data["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd_data else DEFAULT_TEST_PATTERNS
|
||||
for pattern in test_patterns:
|
||||
_validate_test_pattern(pattern)
|
||||
exclude = _string_array(tdd_data["exclude"], "tdd.exclude") if "exclude" in tdd_data else ()
|
||||
for item in exclude:
|
||||
_repo_path(root, item, "tdd.exclude")
|
||||
if "tdd" in data:
|
||||
print("Deprecated config field 'tdd' is ignored; it may be removed.", file=sys.stderr)
|
||||
|
||||
return HarnessConfig(
|
||||
version=1,
|
||||
project_type=project_type,
|
||||
cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset),
|
||||
msbuild=MSBuildConfig(solution, configuration, platform, test_command),
|
||||
tdd=TDDConfig(test_roots, test_patterns, exclude),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from pathlib import Path
|
||||
|
||||
from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection
|
||||
from .tdd_policy import find_cpp_files
|
||||
|
||||
|
||||
class DiscoveryError(RuntimeError):
|
||||
@@ -68,10 +67,4 @@ def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult:
|
||||
ProjectSelection(ProjectKind.MSBUILD, projects[0])
|
||||
)
|
||||
|
||||
cpp_files = find_cpp_files(root)
|
||||
if cpp_files:
|
||||
raise DiscoveryError(
|
||||
"C/C++ files exist but no CMakeLists.txt, preset, .sln, or .vcxproj "
|
||||
"was found; configure projectType and project path"
|
||||
)
|
||||
return DiscoveryResult(None, ())
|
||||
return DiscoveryResult(None)
|
||||
|
||||
@@ -30,20 +30,12 @@ class MSBuildConfig:
|
||||
test_command: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TDDConfig:
|
||||
test_roots: tuple[Path, ...]
|
||||
test_patterns: tuple[str, ...]
|
||||
exclude: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HarnessConfig:
|
||||
version: int
|
||||
project_type: str
|
||||
cmake: CMakeConfig
|
||||
msbuild: MSBuildConfig
|
||||
tdd: TDDConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -55,7 +47,6 @@ class ProjectSelection:
|
||||
@dataclass(frozen=True)
|
||||
class DiscoveryResult:
|
||||
selection: ProjectSelection | None
|
||||
cpp_files: tuple[Path, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
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
|
||||
Reference in New Issue
Block a user