diff --git a/run_local_smoke_tests.py b/run_local_smoke_tests.py new file mode 100644 index 0000000..4c40754 --- /dev/null +++ b/run_local_smoke_tests.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import argparse +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Sequence + + +DEFAULT_TESTS = ( + "tests/test_release_candidate_check.py", + "tests/test_runtime_readiness.py", + "tests/test_ui_unified_persona_pipeline.py", + "tests/test_stockfish_service_recovery.py", + "tests/test_main_ui_elite_recommendation.py", +) + + +@dataclass(frozen=True) +class SmokeStep: + name: str + command: tuple[str, ...] + required: bool = True + + +def run_command( + step: SmokeStep, + *, + project_root: Path, +) -> int: + print() + print("=" * 72) + print(step.name) + print("=" * 72) + print(" ".join(step.command)) + + completed = subprocess.run( + step.command, + cwd=project_root, + text=True, + encoding="utf-8", + errors="replace", + ) + + if completed.returncode == 0: + print(f"[OK] {step.name}") + else: + print(f"[FAILED] {step.name} exit code {completed.returncode}") + + return int(completed.returncode) + + +def build_steps( + *, + python_executable: str, + engine_path: str, + strict_release_check: bool, + tests: Sequence[str], +) -> tuple[SmokeStep, ...]: + release_command = [ + python_executable, + "check_release_candidate.py", + "--engine-path", + engine_path, + ] + + if not strict_release_check: + release_command.append("--allow-dirty-code") + + pytest_command = [ + python_executable, + "-m", + "pytest", + *tests, + "-q", + ] + + return ( + SmokeStep( + name="Release candidate safety check", + command=tuple(release_command), + required=True, + ), + SmokeStep( + name="Focused smoke test suite", + command=tuple(pytest_command), + required=True, + ), + ) + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Run the minimal local smoke checks for ChessPersona before a " + "merge, release candidate, or stable tag." + ) + ) + parser.add_argument( + "--project-root", + default=".", + ) + parser.add_argument( + "--engine-path", + default="engines/stockfish/stockfish.exe", + ) + parser.add_argument( + "--strict-release-check", + action="store_true", + help=( + "Do not allow dirty source files in the release candidate check. " + "Use this after everything is committed." + ), + ) + parser.add_argument( + "--test", + action="append", + dest="tests", + help=( + "Override focused pytest target. Can be passed more than once. " + "Default uses the stable MVP smoke targets." + ), + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_arg_parser() + args = parser.parse_args(argv) + + project_root = Path(args.project_root).resolve() + tests = tuple(args.tests or DEFAULT_TESTS) + + steps = build_steps( + python_executable=sys.executable, + engine_path=str(args.engine_path), + strict_release_check=bool(args.strict_release_check), + tests=tests, + ) + + failed: list[SmokeStep] = [] + + print("ChessPersona Local Smoke Tests") + print("=" * 72) + print(f"Project root : {project_root}") + print(f"Python : {sys.executable}") + print(f"Engine path : {args.engine_path}") + print(f"Strict check : {bool(args.strict_release_check)}") + + for step in steps: + code = run_command( + step, + project_root=project_root, + ) + if code != 0 and step.required: + failed.append(step) + + print() + print("=" * 72) + if failed: + print("SMOKE TEST RESULT: FAILED") + for step in failed: + print(f"- {step.name}") + print() + print("Fix the failed step above before merging or tagging.") + return 2 + + print("SMOKE TEST RESULT: PASSED") + print("Local MVP checks are green.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_local_smoke_tests.py b/tests/test_local_smoke_tests.py new file mode 100644 index 0000000..2582ea3 --- /dev/null +++ b/tests/test_local_smoke_tests.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from run_local_smoke_tests import ( + DEFAULT_TESTS, + build_steps, +) + + +def test_build_steps_uses_allow_dirty_by_default() -> None: + steps = build_steps( + python_executable="python", + engine_path="engines/stockfish/stockfish.exe", + strict_release_check=False, + tests=("tests/test_runtime_readiness.py",), + ) + + release_step = steps[0] + + assert release_step.name == "Release candidate safety check" + assert "--allow-dirty-code" in release_step.command + + +def test_build_steps_can_be_strict_after_commit() -> None: + steps = build_steps( + python_executable="python", + engine_path="engines/stockfish/stockfish.exe", + strict_release_check=True, + tests=("tests/test_runtime_readiness.py",), + ) + + release_step = steps[0] + + assert "--allow-dirty-code" not in release_step.command + + +def test_build_steps_runs_focused_pytest_targets() -> None: + steps = build_steps( + python_executable="python", + engine_path="engine.exe", + strict_release_check=False, + tests=DEFAULT_TESTS, + ) + + pytest_step = steps[1] + + assert pytest_step.name == "Focused smoke test suite" + assert pytest_step.command[:3] == ("python", "-m", "pytest") + assert "tests/test_runtime_readiness.py" in pytest_step.command + assert "tests/test_ui_unified_persona_pipeline.py" in pytest_step.command + assert pytest_step.command[-1] == "-q"