Files

91 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""run_all.py — 단일 테스트 러너 (finding #18). CI·로컬 공통 진입점.
.claude/tests/test_*.py 를 모두 발견해 순차 실행하고, 스위트별 결과와 총계를 출력한다.
하나라도 실패하면 exit 1(CI가 빨간불). 실행 무결성(doctor·lint_refs)도 함께 게이트한다.
Usage:
CLAUDE_PROJECT_DIR="$PWD" ORGOS_WORKSPACE=_sandbox python3 .claude/tests/run_all.py
python3 .claude/tests/run_all.py --no-preflight # doctor/lint_refs 생략(테스트만)
각 test_*.py 는 exit 0=통과 / 비0=실패 규약을 따른다(기존 스위트와 동일).
"""
import glob
import os
import signal
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
PY = sys.executable
# CI/로컬에서 workspace 미지정 시 조용한 실패 대신 명확히 _sandbox 로 고정(운영 아님·테스트용).
ENV = dict(os.environ)
ENV.setdefault("CLAUDE_PROJECT_DIR", ROOT)
ENV.setdefault("ORGOS_WORKSPACE", "_sandbox")
# 스위트별 타임아웃(초). 한 테스트가 멈춰도 CI 전체가 정지하지 않게 한다(재리뷰 지적).
SUITE_TIMEOUT = int(os.environ.get("ORGOS_TEST_TIMEOUT", "300"))
def run(cmd, label):
print(f"\n{'='*60}\n{label}\n{'='*60}")
process = None
try:
process = subprocess.Popen(cmd, env=ENV, start_new_session=True)
return process.wait(timeout=SUITE_TIMEOUT) == 0
except subprocess.TimeoutExpired:
print(f"⏱️ TIMEOUT after {SUITE_TIMEOUT}s — {label} (fail)", file=sys.stderr)
if process is not None:
try:
os.killpg(process.pid, signal.SIGTERM)
process.wait(timeout=3)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait()
except ProcessLookupError:
pass
return False
def main():
no_preflight = "--no-preflight" in sys.argv[1:]
results = []
if not no_preflight:
results.append(("artifact-registry", run(
[PY, os.path.join(ROOT, ".claude/hooks/compile_artifact_registry.py"), "--check"],
"preflight: compiled artifact registry")))
results.append(("orgos-registry", run(
[PY, os.path.join(ROOT, ".claude/hooks/compile_orgos_registry.py"), "--check"],
"preflight: compiled Org OS registries + architecture views")))
results.append(("design-system", run(
[PY, os.path.join(ROOT, ".claude/hooks/compile_design_system.py"), "--check"],
"preflight: compiled organization design system")))
results.append(("doctor.py", run(
[PY, os.path.join(ROOT, ".claude/hooks/doctor.py")], "preflight: doctor.py")))
results.append(("lint_refs.py", run(
[PY, os.path.join(ROOT, ".claude/hooks/lint_refs.py")], "preflight: lint_refs.py")))
suites = sorted(glob.glob(os.path.join(HERE, "test_*.py")))
if not suites:
print("no test_*.py found", file=sys.stderr)
return 1
for s in suites:
name = os.path.basename(s)
results.append((name, run([PY, s], name)))
print(f"\n{'='*60}\nSUMMARY\n{'='*60}")
passed = [n for n, ok in results if ok]
failed = [n for n, ok in results if not ok]
for n, ok in results:
print(f" {'✅' if ok else '❌'} {n}")
print(f"\n{len(passed)}/{len(results)} green · failed: {failed or '없음'}")
return 0 if not failed else 1
if __name__ == "__main__":
sys.exit(main())