Files
Pier_v1/Code/교각모델링앱/tools/test_batch_notice.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

155 lines
7.7 KiB
Python

"""Test of improvement #34-5 (2026-10-20): a batch run that did not finish must not look like a finished one. No Rhino. Own data root
검증/batch_notice_test_data (recreated): a copy of Ungok P4 whose run folder is built from the json files of its real runs (copied) with chosen file times.
Acceptance: the gates page says WHY the run stopped (error / FAIL / blocked / killed), marks the results that are older than the run, shows the partial
results computed before it stopped (and only then), labels an approval of an earlier input as void, and shows nothing of this for a finished run."""
import json
import os
import shutil
import sys
import time
from pathlib import Path
APP = Path(__file__).resolve().parents[1]
D = APP / "data"
TD = APP / "검증" / "batch_notice_test_data"
if TD.exists():
shutil.rmtree(TD)
sys.path.insert(0, str(APP / "tools"))
from testlib import verified_pier # noqa: E402
src = verified_pier("운곡교P4")
if src is None or not (src / "runs" / "current" / "G1_gate.json").exists():
print("SKIP no real run of 운곡교P4")
print("RESULT PASS")
sys.exit(0)
shutil.copytree(src, TD / "운곡교P4", ignore=shutil.ignore_patterns("runs", "backup", "dwg", "dxf", "confirm.json"))
shutil.copytree(D / "_템플릿", TD / "_템플릿", ignore=shutil.ignore_patterns("_이전"))
os.environ["PIERAPP_DATA"] = str(TD)
sys.path.insert(0, str(APP / "web"))
sys.path.insert(0, str(APP))
import app as webapp # noqa: E402
import store # noqa: E402
assert Path(str(store.DATA)) == TD
ok = True
def check(name, cond, detail=""):
global ok
ok &= bool(cond)
print("PASS" if cond else "FAIL", name, str(detail)[:300], flush=True)
p = store.Pier("운곡교P4")
run = p.run_dir
run.mkdir(parents=True, exist_ok=True)
def fresh():
for f in run.glob("*.json"):
f.unlink()
for f in (src / "runs" / "current").glob("*.json"):
if f.name.startswith("G") and not f.name.endswith(("_negH.json", "_negV.json")) or f.name == "approvals.json":
shutil.copy2(f, run / f.name)
def at(path, ago_s):
t = time.time() - ago_s
os.utime(path, (t, t))
def batch(state, **kw):
b = {"state": state, "by": "시험자", "pid": os.getpid(), "started": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 600)),
"at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60)), "log": ["G1~G5 단일 세션 실행", "[batch] one Rhino session"]}
b.update(kw)
store.wjson(run / "batch.json", b)
def page():
return webapp.app.test_client().get("/pier/운곡교P4/gates").get_data(as_text=True)
# ---- A. error: old recorded results, newer checker outputs
fresh()
for g in ("G1", "G2", "G3", "G4", "G5"):
at(run / f"{g}_gate.json", 20000) # recorded long ago (an earlier run)
at(run / f"{g}.json", 300) # checker output of the aborted run (newer)
batch("중단", reason="error", error="RuntimeError: display: hidden_ids not found ['P1-CP']")
h = page()
check("error: the page says the run did not finish and why (error text shown)", 'id="batch-aborted"' in h and "오류" in h and "hidden_ids not found" in h)
check("error: results older than the run are marked 이전 실행 결과", h.count("이전 실행 결과") >= 5)
check("error: partial results of the aborted run are shown per gate and labelled as not used for approval", 'id="partial-G1"' in h and "임시 결과" in h and "승인·보고서에는 쓰이지 않습니다" in h)
check("error: the partial block reports the checker's own pass / total", f"{json.loads((run / 'G1.json').read_text(encoding='utf-8'))['summary']['passed']} / " in h)
check("partial_result() returns the counts and the failed items", p.partial_result("G3") and p.partial_result("G3")["total"] and isinstance(p.partial_result("G3")["fails"], list))
# ---- B. the recorded result is NEWER than the checker output (a normal finished gate): no partial block, no 'older' mark
at(run / "G1_gate.json", 100)
at(run / "G1.json", 300)
check("a recorded result newer than the checker output is not a partial result", p.partial_result("G1") is None)
# ---- C. FAIL stop
batch("중단", reason="fail", gate="G3")
check("fail: names the gate that stopped the run", "G3</b>가 FAIL" in page())
# ---- D. blocked / killed
batch("중단", reason="blocked")
check("blocked: says the run could not start", "시작하지 못했습니다" in page())
batch("실행 중", pid=os.getpid() + 1) # a run of another (dead) server process
h = page()
check("killed: a 실행 중 record of another process is shown as 중단됨 with its reason", "서버가 멈추거나" in h and "중단됨" in h)
# ---- E. finished run: nothing of the abort notice
batch("완료")
h = page()
check("finished run: no abort banner, no partial block, no 'older' mark", 'id="batch-aborted"' not in h and 'id="partial-' not in h and "이전 실행 결과 —" not in h)
# ---- F. an approval of an earlier input is labelled void
ap = store.rjson(run / "approvals.json", {})
for g in ap:
ap[g]["inputs_sha"] = "0000000000000000"
store.wjson(run / "approvals.json", ap)
h = page()
st = p.gate_status("G1")
check("approval of an earlier input: approval_stale and the label 승인 기록은 이전 입력 것(무효)", st["approval_stale"] and not st["approved"] and "승인 기록은 이전 입력 것(무효)" in h)
check("the void approval is not shown as a plain 승인", "· 승인</h2>" not in h and 'class="ok">승인</span>' not in h)
# ---- G. the real worker (app._batch) with a fake Rhino session: it records started + a reason on every stop path
fresh()
need = ["G1", "G2", "G3", "G4", "G5"]
for f in run.glob("*_gate.json"):
f.unlink() # no current PASS result -> the worker has to start a session
for f in run.glob("approvals.json"):
f.unlink()
webapp.gate_blockers = lambda pp, g: []
def boom(*a, **k):
raise RuntimeError("fake Rhino failure")
webapp.run_batch_session = boom
webapp._batch(p, "시험자", need)
b = store.rjson(run / "batch.json")
check("worker: a failure inside the Rhino session -> 중단 with reason error, the last traceback line, and a start time",
b["state"] == "중단" and b["reason"] == "error" and "fake Rhino failure" in b["error"] and b.get("started"), {k: b.get(k) for k in ("state", "reason", "error", "started")})
def fake_session(gates, *a, **k):
rep = {"verdict": "PASS", "summary": {"passed": 3, "total": 3}, "planned_fails": [], "unplanned_fails": [], "negative": {}, "mode": "batch"}
return {g: dict(rep, verdict="FAIL" if g == "G2" else "PASS") for g in gates}
webapp.run_batch_session = fake_session
webapp._batch(p, "시험자", need)
b = store.rjson(run / "batch.json")
check("worker: a FAIL result -> 중단 with reason fail and the failing gate", b["state"] == "중단" and b.get("reason") == "fail" and b.get("gate") == "G2", {k: b.get(k) for k in ("state", "reason", "gate")})
g1 = store.rjson(run / "G1_gate.json")
check("worker: every recorded gate result carries its own time (at)", g1.get("at") and store.rjson(run / "G2_gate.json").get("at"))
check("worker: G1 (PASS before the failing gate) was approved, G2 was not", p.approvals().get("G1", {}).get("decision") == "승인" and "G2" not in p.approvals())
webapp.gate_blockers = lambda pp, g: ["G2: 입력 묶음 미확정"] if g == "G2" else []
webapp._batch(p, "시험자", need)
b = store.rjson(run / "batch.json")
check("worker: a blocked run -> 중단 with reason blocked", b["state"] == "중단" and b.get("reason") == "blocked", {k: b.get(k) for k in ("state", "reason")})
print("RESULT", "PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)