zcbot/windows-node/adapters/cad.drawing.author@v1/acceptance.py

166 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Fixed offline acceptance harness for the 2D CAD adapter.
This validates the production worker and deterministic outputs. It does not claim
AutoCAD, ZWCAD, or GstarCAD interoperability; those remain manual target-machine gates.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import shutil
import subprocess
import sys
from pathlib import Path
from uuid import uuid4
WORKER_PATH = Path(__file__).with_name("worker.py")
SPEC = importlib.util.spec_from_file_location("zcbot_cad2d_worker", WORKER_PATH)
assert SPEC and SPEC.loader
worker = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(worker)
def _base(title: str, sheet: str = "A3", drawing_type: str = "floor_plan") -> dict:
walls = [
{"id": "south", "start": [0, 0], "end": [12000, 0], "thickness": 240, "kind": "exterior"},
{"id": "east", "start": [12000, 0], "end": [12000, 9000], "thickness": 240, "kind": "exterior"},
{"id": "north", "start": [12000, 9000], "end": [0, 9000], "thickness": 240, "kind": "exterior"},
{"id": "west", "start": [0, 9000], "end": [0, 0], "thickness": 240, "kind": "exterior"},
{"id": "partition", "start": [5000, 0], "end": [5000, 9000], "thickness": 120, "kind": "interior"},
]
return {
"schema_version": 1, "inputs": [], "outputs": [],
"operation": {"drawing": {
"type": drawing_type, "recipe_version": 1, "title": title, "unit": "mm",
"sheet": {"size": sheet, "orientation": "landscape", "scale": 50},
"walls": walls,
"openings": [
{"id": "entry", "wall_id": "south", "offset": 900, "width": 1000, "type": "single_door", "hinge": "start", "swing": "in"},
{"id": "window1", "wall_id": "north", "offset": 1200, "width": 1800, "type": "window"},
],
"areas": [{"id": "room", "boundary": [[0,0],[12000,0],[12000,9000],[0,9000],[0,0]], "name": "房间", "label_position": [6000,4500], "show_area": True}],
"objects": [{"id": "desk1", "type": "desk", "position": [2500,2500], "rotation": 0, "label": "试验台"}],
"dimensions": [{"id": "overall1", "type": "overall", "points": [[0,0],[5000,0],[12000,0]], "offset": -700}],
"notes": [{"id": "note1", "position": [8000,8000], "text": "尺寸单位mm"}],
}},
}
def _cases() -> dict[str, tuple[dict, str | None]]:
small = _base("中文小三居", "A3")
small["operation"]["drawing"]["walls"] += [
{"id":"left_split","start":[0,4500],"end":[5000,4500],"thickness":120,"kind":"interior"},
{"id":"right_split","start":[5000,5200],"end":[12000,5200],"thickness":120,"kind":"interior"},
]
small["operation"]["drawing"]["areas"] = [
{"id":"bed1","boundary":[[0,0],[5000,0],[5000,4500],[0,4500],[0,0]],"name":"卧室一","label_position":[2500,3200],"show_area":True},
{"id":"bed2","boundary":[[0,4500],[5000,4500],[5000,9000],[0,9000],[0,4500]],"name":"卧室二","label_position":[2500,6800],"show_area":True},
{"id":"bed3","boundary":[[5000,5200],[12000,5200],[12000,9000],[5000,9000],[5000,5200]],"name":"卧室三","label_position":[8500,7200],"show_area":True},
{"id":"living","boundary":[[5000,0],[12000,0],[12000,5200],[5000,5200],[5000,0]],"name":"客餐厅","label_position":[8500,3000],"show_area":True},
]
two_room = _base("两室一厅", "A4")
two_room["operation"]["drawing"]["sheet"]["scale"] = 100
lab = _base("实验室设备布置", "A2", "equipment_layout")
lab["operation"]["drawing"]["objects"] += [
{"id": "hood", "type": "fume_hood", "position": [8000,2500], "label": "通风柜"},
{"id": "exit", "type": "safety_exit", "position": [10500,7000], "label": "安全出口"},
]
workshop = _base("车间布置", "A2", "equipment_layout")
workshop["operation"]["drawing"]["objects"] = [
{"id": "machine1", "type": "machine", "position": [3000,3000], "size": [2200,1600], "label": "成型机"},
{"id": "tank1", "type": "tank", "position": [9000,5000], "size": [1800,1800], "label": "储罐"},
]
dense = _base("密集门窗尺寸", "A2")
dense["operation"]["drawing"]["dimensions"] = [{"id":"chain1","type":"chain","points":[[x,0] for x in range(0,12001,1000)],"offset":-900}]
outside = _base("门窗越界")
outside["operation"]["drawing"]["openings"][0]["offset"] = 11900
overlap = _base("洞口重叠")
overlap["operation"]["drawing"]["openings"].append({"id":"entry2","wall_id":"south","offset":1500,"width":1200,"type":"opening"})
unclosed = _base("区域不闭合")
unclosed["operation"]["drawing"]["areas"][0]["boundary"][-1] = [1, 0]
collision = _base("对象碰撞警告")
collision["operation"]["drawing"]["objects"][0]["position"] = [1000, 500]
return {
"small_three_bedroom": (small, None), "two_bedroom_living": (two_room, None),
"laboratory_layout": (lab, None), "workshop_layout": (workshop, None),
"dense_openings_dimensions": (dense, None),
"opening_outside_wall": (outside, "CAD2D_OPENING_OUTSIDE_WALL"),
"overlapping_openings": (overlap, "CAD2D_OPENINGS_OVERLAP"),
"unclosed_area": (unclosed, "CAD2D_AREA_NOT_CLOSED"),
"object_collision_warning": (collision, None),
}
def _digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _execute(root: Path, name: str, request: dict, expected_error: str | None, repeat: int) -> dict:
digests: list[dict[str, str]] = []
result: dict = {"name": name, "passed": False, "expected_error": expected_error}
for index in range(repeat):
job = root / f"验证 输出 {name}-{index + 1}"
(job / "request").mkdir(parents=True)
record = {"job_id": str(uuid4()), "lease_id": str(uuid4()), "request_digest": "a" * 64, "request": request}
(job / "request" / "request.json").write_text(json.dumps(record, ensure_ascii=False), encoding="utf-8")
try:
artifacts = worker.run(job)
if expected_error:
result["detail"] = "expected failure but worker succeeded"
return result
current = {item["filename"]: _digest(job / "output" / (".meta/provenance.json" if item["filename"] == "provenance.json" else item["filename"])) for item in artifacts if item["filename"] not in {"provenance.json", "drawing.pdf"}}
digests.append(current)
report = json.loads((job / "output" / "validation-report.json").read_text(encoding="utf-8"))
if not report["passed"] or report["checks"]["version"] != "R2013":
result["detail"] = "mechanical validation failed"
return result
except worker.DrawingError as exc:
if exc.code != expected_error:
result["detail"] = f"unexpected error: {exc.code}"
return result
result.update({"passed": True, "observed_error": exc.code})
return result
result["passed"] = all(item == digests[0] for item in digests[1:])
result["deterministic_repeats"] = len(digests)
result["warnings"] = report["warnings"]
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--work-root", type=Path, required=True)
parser.add_argument("--repeat", type=int, default=3, choices=range(1, 6))
parser.add_argument("--case", action="append", choices=sorted(_cases()))
args = parser.parse_args()
if args.work_root.exists():
print("[ERR] --work-root must not already exist", file=sys.stderr)
return 2
args.work_root.mkdir(parents=True)
selected = args.case or list(_cases())
results = [_execute(args.work_root, name, *_cases()[name], args.repeat) for name in selected]
# Cancellation/cleanup gate: terminate a child before it can touch a job directory.
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"])
child.terminate()
try:
child.wait(timeout=5)
cancellation = {"passed": True, "returncode": child.returncode}
except subprocess.TimeoutExpired:
child.kill(); child.wait(); cancellation = {"passed": False, "returncode": child.returncode}
report = {
"passed": all(item["passed"] for item in results) and cancellation["passed"],
"adapter_version": worker.ADAPTER_VERSION, "cases": results,
"cancellation_and_cleanup": cancellation,
"path_coverage": "work root supports spaces and Chinese characters",
"manual_compatibility_pending": ["AutoCAD", "ZWCAD", "GstarCAD"],
}
(args.work_root / "acceptance-report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("[OK] 2D CAD acceptance passed." if report["passed"] else "[ERR] 2D CAD acceptance failed.")
return 0 if report["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())