82 lines
4.0 KiB
Python
82 lines
4.0 KiB
Python
import json
|
|
from pathlib import Path, PureWindowsPath
|
|
from typing import Any
|
|
|
|
from .discovery import DiscoveryError, validate_csproj
|
|
from .models import DotnetConfig, HarnessConfig, TDDConfig
|
|
|
|
DEFAULT_TEST_PATTERNS = ("{stem}Tests.cs", "{stem}Test.cs")
|
|
|
|
|
|
class ConfigError(ValueError):
|
|
pass
|
|
|
|
|
|
def _mapping(value: Any, allowed: set[str], label: str) -> dict:
|
|
if not isinstance(value, dict):
|
|
raise ConfigError(f"{label} must be an object")
|
|
unknown = sorted(set(value) - allowed)
|
|
if unknown:
|
|
raise ConfigError(f"{label} contains unexpected field: {unknown[0]}; use version 2 .NET configuration")
|
|
return value
|
|
|
|
|
|
def _string(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise ConfigError(f"{label} must be a non-empty string")
|
|
return value
|
|
|
|
|
|
def _array(value: Any, label: str, *, allow_empty=False) -> tuple[str, ...]:
|
|
if not isinstance(value, list) or (not value and not allow_empty):
|
|
raise ConfigError(f"{label} must be a {'non-empty ' if not allow_empty else ''}string array")
|
|
return tuple(_string(item, label) for item in value)
|
|
|
|
|
|
def _repo_path(root: Path, raw: str, label: str) -> Path:
|
|
raw = _string(raw, label)
|
|
windows = PureWindowsPath(raw)
|
|
if windows.drive or windows.root or Path(raw).is_absolute():
|
|
raise ConfigError(f"{label} must be repository-relative")
|
|
resolved = (root / raw.replace("\\", "/")).resolve()
|
|
if not resolved.is_relative_to(root):
|
|
raise ConfigError(f"{label} resolves outside the repository")
|
|
return resolved
|
|
|
|
|
|
def load_config(root: Path) -> HarnessConfig:
|
|
root = root.resolve()
|
|
path = root / ".harness/config.json"
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {"version": 2}
|
|
except (OSError, ValueError) as exc:
|
|
raise ConfigError(f"cannot read configuration {path}: {exc}") from exc
|
|
data = _mapping(data, {"version", "projectType", "dotnet", "tdd"}, "config")
|
|
if type(data.get("version")) is not int or data["version"] != 2:
|
|
raise ConfigError("version must be 2; rewrite legacy configuration using the .NET config example")
|
|
project_type = data.get("projectType", "auto")
|
|
if project_type not in ("auto", "dotnet"):
|
|
raise ConfigError("projectType must be auto or dotnet")
|
|
dotnet = _mapping(data.get("dotnet", {}), {"target", "configuration", "testProjects"}, "dotnet")
|
|
target = _repo_path(root, dotnet["target"], "dotnet.target") if "target" in dotnet else None
|
|
if target is not None and (target.suffix.lower() not in {".sln", ".csproj"} or not target.is_file()):
|
|
raise ConfigError("dotnet.target must be an existing .sln or SDK-style .csproj")
|
|
configuration = _string(dotnet.get("configuration", "Debug"), "dotnet.configuration")
|
|
tests = tuple(_repo_path(root, item, "dotnet.testProjects") for item in _array(dotnet["testProjects"], "dotnet.testProjects")) if "testProjects" in dotnet else ()
|
|
for test in tests:
|
|
try:
|
|
validate_csproj(test)
|
|
except DiscoveryError as exc:
|
|
raise ConfigError(f"dotnet.testProjects: {exc}") from exc
|
|
tdd = _mapping(data.get("tdd", {}), {"testRoots", "testPatterns", "exclude"}, "tdd")
|
|
roots = _array(tdd["testRoots"], "tdd.testRoots") if "testRoots" in tdd else ("tests",)
|
|
test_roots = tuple(_repo_path(root, item, "tdd.testRoots") for item in roots)
|
|
patterns = _array(tdd["testPatterns"], "tdd.testPatterns") if "testPatterns" in tdd else DEFAULT_TEST_PATTERNS
|
|
for pattern in patterns:
|
|
if "{stem}" not in pattern or not pattern.endswith(".cs") or any(char in pattern.replace("{stem}", "") for char in "{}/\\"):
|
|
raise ConfigError("tdd.testPatterns must be C# filenames containing only the {stem} placeholder")
|
|
exclude = _array(tdd["exclude"], "tdd.exclude", allow_empty=True) if "exclude" in tdd else ()
|
|
for item in exclude:
|
|
_repo_path(root, item, "tdd.exclude")
|
|
return HarnessConfig(2, project_type, DotnetConfig(target, configuration, tests), TDDConfig(test_roots, patterns, exclude))
|