91 lines
3.5 KiB
Python
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())
|