#!/usr/bin/env python3 """Run C#/.NET Harness validation commands.""" from __future__ import annotations import os import subprocess import sys from pathlib import Path class ValidationConfigurationError(Exception): """Raised when automatic target discovery is ambiguous.""" def load_env_commands() -> list[str]: raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "") return [line for line in raw.splitlines() if line.strip()] def _root_files_with_suffix(root: Path, suffix: str) -> list[Path]: return sorted( ( path.resolve() for path in root.iterdir() if path.is_file() and path.suffix.lower() == suffix ), key=lambda path: path.name.casefold(), ) def _select_single_target(candidates: list[Path], candidate_kind: str) -> Path | None: if len(candidates) > 1: names = ", ".join(path.name for path in candidates) raise ValidationConfigurationError( f"Multiple {candidate_kind} targets found at repository root: {names}. " "Keep exactly one target or set HARNESS_VALIDATION_COMMANDS " "to define validation explicitly." ) return candidates[0] if candidates else None def discover_target(root: Path) -> Path | None: solutions = _root_files_with_suffix(root, ".sln") if solutions: return _select_single_target(solutions, "solution (.sln)") projects = _root_files_with_suffix(root, ".csproj") return _select_single_target(projects, "project (.csproj)") def load_dotnet_commands(root: Path) -> list[str]: target = discover_target(root) if target is None: return [] quoted_target = f'"{target}"' return [ f"dotnet restore {quoted_target}", f"dotnet build {quoted_target} --configuration Debug --no-restore", f"dotnet test {quoted_target} --configuration Debug --no-build --no-restore", ] def discover_commands(root: Path) -> list[str]: env_commands = load_env_commands() if env_commands: return env_commands return load_dotnet_commands(root) def run_command(command: str, root: Path) -> subprocess.CompletedProcess: return subprocess.run( command, cwd=root, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", ) def emit_stream(prefix: str, content: str, *, stream) -> None: text = (content or "").strip() if not text: return print(prefix, file=stream) print(text, file=stream) def main() -> int: root = Path(__file__).resolve().parent.parent try: commands = discover_commands(root) except ValidationConfigurationError as error: print(f"Validation configuration error: {error}", file=sys.stderr) return 2 if not commands: print("No C#/.NET validation target found.") print("Add a root .sln or .csproj, or set HARNESS_VALIDATION_COMMANDS.") return 0 for command in commands: print(f"$ {command}") result = run_command(command, root) emit_stream("[stdout]", result.stdout, stream=sys.stdout) emit_stream("[stderr]", result.stderr, stream=sys.stderr) if result.returncode != 0: print(f"Validation failed: {command}", file=sys.stderr) return result.returncode print("Validation succeeded.") return 0 if __name__ == "__main__": raise SystemExit(main())