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>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,138 @@
|
||||
"""Test of the batch features (2026-10-12): bulk confirmation and G1..G5 run with auto-approval.
|
||||
|
||||
Own data root (검증/web_batch_test_data, recreated) with the Ungok P1 params/expected as a pier; real Rhino runs.
|
||||
Checks: name required; bulk confirm confirms exactly the required unconfirmed sections; single run refused while
|
||||
a batch runs; a wrong input stops the batch at G1 without approval; a correct input approves G1..G5 under the
|
||||
given name with the auto note.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
APP = Path(__file__).resolve().parents[1]
|
||||
TD = APP / "검증" / "web_batch_test_data"
|
||||
if TD.exists():
|
||||
shutil.rmtree(TD)
|
||||
(TD / "배치P1").mkdir(parents=True)
|
||||
for f in ("params.json", "expected.json"):
|
||||
shutil.copy2(APP / "data" / "운곡교P1" / f, TD / "배치P1" / f)
|
||||
os.environ["PIERAPP_DATA"] = str(TD)
|
||||
sys.path.insert(0, str(APP / "web"))
|
||||
import app as webapp # noqa: E402
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from testlib import confirm_required # noqa: E402
|
||||
import store # noqa: E402
|
||||
|
||||
P, BY = "배치P1", "시험자"
|
||||
results = []
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
results.append({"check": name, "result": "PASS" if ok else "FAIL", "detail": str(detail)[:300]})
|
||||
print("PASS" if ok else "FAIL", name, str(detail)[:200], flush=True)
|
||||
|
||||
|
||||
def html(r):
|
||||
return r.get_data(as_text=True)
|
||||
|
||||
|
||||
def wait_batch(limit=1500):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < limit:
|
||||
b = webapp.batch_state(store.Pier(P))
|
||||
if b and b["state"] != "실행 중":
|
||||
return b, round(time.time() - t0)
|
||||
time.sleep(5)
|
||||
return webapp.batch_state(store.Pier(P)), round(time.time() - t0)
|
||||
|
||||
|
||||
def main():
|
||||
c = webapp.app.test_client()
|
||||
pier = store.Pier(P)
|
||||
need0 = webapp.unconfirmed(pier)
|
||||
r = c.post(f"/pier/{P}/section/{need0[0][0]}/{need0[0][1]}/confirm", data={"by": "", "why": "시험"}, follow_redirects=True)
|
||||
check("confirm needs a name", "확정자 이름" in html(r) and webapp.unconfirmed(store.Pier(P)) == need0)
|
||||
confirm_required(c, webapp, store, P, BY)
|
||||
p = store.Pier(P)
|
||||
conf = p.confirmations()
|
||||
check("confirm: all required sections confirmed", not webapp.unconfirmed(p)
|
||||
and len(need0) == 23 and all(conf[f"{f}:{k}"]["by"] == BY for f, k in need0), f"{len(need0)} sections")
|
||||
check("confirm: sections not required stay unconfirmed", "params:_meta" not in conf)
|
||||
r = c.get(f"/pier/{P}")
|
||||
check("pier page shows all confirmed", "모두 확정되어 있습니다" in html(r))
|
||||
r = c.post(f"/pier/{P}/gates/run_all", data={"by": ""}, follow_redirects=True)
|
||||
check("batch run needs a name", "판정자 이름" in html(r))
|
||||
|
||||
# wrong input: pile length +5 mm -> G1 FAIL, batch stops, nothing approved
|
||||
pr = p.load("params")
|
||||
pile = dict(pr["piles"], length=pr["piles"]["length"] + 5)
|
||||
c.post(f"/pier/{P}/section/params/piles", data={"value": json.dumps(pile)})
|
||||
confirm_required(c, webapp, store, P, BY)
|
||||
r = c.post(f"/pier/{P}/gates/run_all", data={"by": BY}, follow_redirects=True)
|
||||
r2 = c.post(f"/pier/{P}/gate/G2/run", follow_redirects=True)
|
||||
check("single run refused while the batch runs", "일괄 실행 중에는" in html(r2))
|
||||
b, secs = wait_batch()
|
||||
p = store.Pier(P)
|
||||
check("wrong input: batch stops at G1 FAIL", b["state"] == "중단" and any("G1 FAIL" in m for m in b["log"]), b["log"])
|
||||
check("wrong input: nothing approved", not p.approvals(), p.approvals())
|
||||
|
||||
# correct input: G1..G5 approved under the given name with the auto note
|
||||
pile["length"] -= 5
|
||||
c.post(f"/pier/{P}/section/params/piles", data={"value": json.dumps(pile)})
|
||||
confirm_required(c, webapp, store, P, BY)
|
||||
c.post(f"/pier/{P}/gates/run_all", data={"by": BY})
|
||||
b, secs = wait_batch()
|
||||
p = store.Pier(P)
|
||||
ap = p.approvals()
|
||||
check("batch completes", b["state"] == "완료", f"{b['log']} {secs}s")
|
||||
check("G1..G5 approved by the given name with the auto note",
|
||||
all(p.gate_status(g)["approved"] and ap[g]["by"] == BY and ap[g]["note"] == webapp.AUTO_NOTE
|
||||
for g in ("G1", "G2", "G3", "G4", "G5")), ap)
|
||||
reps = {g: p.gate_status(g)["report"] for g in ("G1", "G2", "G3", "G4", "G5")}
|
||||
check("batch: one session, gate checks only (mode batch, reload/unchanged skipped)",
|
||||
all(r["mode"] == "batch" and r["reload"]["ok"] is None and r["unchanged"]["ok"] is None for r in reps.values())
|
||||
and len({r["job_id"] for r in reps.values()}) == 1, {g: r["mode"] for g, r in reps.items()})
|
||||
run = TD / P / "runs" / "current"
|
||||
check("batch: only G5 model saved", (run / "G5.3dm").exists() and not any((run / f"G{i}.3dm").exists() for i in range(1, 5)))
|
||||
check("batch: G1 download refused, G5 download served",
|
||||
c.get(f"/pier/{P}/download/G1").status_code == 404 and c.get(f"/pier/{P}/download/G5").status_code == 200)
|
||||
check("batch: session time", True, f"{reps['G5']['session_secs']}s for G1..G5 (individual runs ~330s)")
|
||||
# the single-session model equals the model of individual gate runs (same FAST fingerprint mode)
|
||||
sys.path.insert(0, str(APP)); sys.path.insert(0, str(APP / "tools"))
|
||||
from runner import run_job
|
||||
from compare_fingerprints import compare
|
||||
fp = run / "G5_session_fp.json"
|
||||
run_job([{"name": "fp", "script": APP / "engine" / "rhino" / "fingerprint.py",
|
||||
"globals": {"MODEL_PATH": str(run / "G5.3dm"), "OUT_PATH": str(fp), "FAST": True}}])
|
||||
ref = APP / "출력" / "P1회귀_엔진수정" / "G5_fp.json"
|
||||
cmp = compare(json.loads(ref.read_text(encoding="utf-8")), json.loads(fp.read_text(encoding="utf-8")))
|
||||
check("batch G5 model == individual-run G5 model (3190 objects, FAST fingerprints)", cmp["identical"],
|
||||
{k: cmp[k] for k in ("count", "objects_with_diffs", "diff_fields")})
|
||||
check("planned FAIL gates still listed", len(p.gate_status("G3")["report"]["planned_fails"]) == 1
|
||||
and len(p.gate_status("G4")["report"]["planned_fails"]) == 5)
|
||||
r = c.post(f"/pier/{P}/gates/run_all", data={"by": BY}, follow_redirects=True)
|
||||
check("nothing left to run", "이미 승인되어 있습니다" in html(r))
|
||||
r = c.get(f"/pier/{P}/gates")
|
||||
check("gates page renders batch log", r.status_code == 200 and "일괄 승인 (필요하면 실행)" in html(r) and "G5 PASS" in html(r))
|
||||
# an existing current PASS result is approved without running the gate again
|
||||
jid5 = p.gate_status("G5")["job"]["id"]
|
||||
c.post(f"/pier/{P}/gate/G5/decide", data={"by": BY, "decision": "반려", "note": "시험: 승인 취소"})
|
||||
r = c.get(f"/pier/{P}")
|
||||
check("pier page offers 'G5 일괄 승인' for one unapproved gate", ">G5 일괄 승인</button>" in html(r))
|
||||
c.post(f"/pier/{P}/gates/run_all", data={"by": BY})
|
||||
b2, secs2 = wait_batch(120)
|
||||
p = store.Pier(P)
|
||||
check("existing PASS approved without re-run", b2["state"] == "완료" and p.gate_status("G5")["approved"]
|
||||
and p.gate_status("G5")["job"]["id"] == jid5 and any("재실행 없음" in m for m in b2["log"]),
|
||||
f"{b2['log']} {secs2}s")
|
||||
out = APP / "검증" / "batch_web_test.json"
|
||||
out.write_text(json.dumps({"results": results, "passed": sum(x["result"] == "PASS" for x in results),
|
||||
"total": len(results)}, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
print("TOTAL", sum(x["result"] == "PASS" for x in results), "/", len(results))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user