zcbot/tests/test_cad2d_adapter.py

112 lines
6.4 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.

from __future__ import annotations
import importlib.util
import json
import math
import tempfile
import unittest
from pathlib import Path
from uuid import uuid4
from core.software_contracts import SoftwareContractError, get_contract
ROOT = Path(__file__).resolve().parents[1]
WORKER_PATH = ROOT / "windows-node" / "adapters" / "cad.drawing.author@v1" / "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 request() -> dict:
return {
"schema_version": 1, "inputs": [], "outputs": [],
"operation": {"drawing": {
"type": "floor_plan", "recipe_version": 1, "title": "中文平面图", "unit": "mm",
"sheet": {"size": "A3", "orientation": "landscape", "scale": 50},
"walls": [
{"id":"wall1","start":[0,0],"end":[10000,0],"thickness":240,"kind":"exterior"},
{"id":"wall2","start":[10000,0],"end":[10000,7000],"thickness":240,"kind":"exterior"},
{"id":"wall3","start":[10000,7000],"end":[0,7000],"thickness":240,"kind":"exterior"},
{"id":"wall4","start":[0,7000],"end":[0,0],"thickness":240,"kind":"exterior"},
],
"openings": [{"id":"door1","wall_id":"wall1","offset":800,"width":900,"type":"single_door","hinge":"start","swing":"in"}],
"areas": [{"id":"area1","boundary":[[0,0],[10000,0],[10000,7000],[0,7000],[0,0]],"name":"实验室","label_position":[5000,3500],"show_area":True}],
"objects": [{"id":"bench1","type":"lab_bench","position":[5000,1800],"label":"试验台"}],
"dimensions": [{"id":"dim1","type":"horizontal","start":[0,0],"end":[10000,0],"offset":-600}],
"notes": [{"id":"note1","position":[7000,6000],"text":"尺寸单位mm"}],
}},
}
class Cad2dContractTests(unittest.TestCase):
def test_contract_is_runtime_only_fixed_output_workspace(self) -> None:
contract = get_contract("cad.drawing.author@v1")
normalized, digest = contract.normalize_request(request())
self.assertEqual(contract.display_name, "二维 CAD 工程制图")
self.assertEqual(contract.output_namespace, "cad2d")
self.assertFalse(contract.default_enrollment)
self.assertEqual(contract.input_policy["max_count"], 0)
self.assertEqual(contract.workspace.state_output, "drawing_dxf")
self.assertEqual(contract.workspace.preview_outputs, ("drawing_png",))
self.assertEqual(contract.feature(normalized), "floor_plan")
self.assertEqual(len(digest), 64)
self.assertEqual(set(contract.expected_outputs(normalized)), {
"drawing_dxf", "drawing_svg", "drawing_png", "drawing_pdf", "drawing_recipe",
"drawing_manifest", "validation_report", "provenance",
})
def test_contract_rejects_inputs_scripts_paths_fonts_layers_and_nonfinite(self) -> None:
contract = get_contract("cad.drawing.author@v1")
mutations = [("script", "x"), ("path", "C:/secret"), ("font", "Arial"), ("layer", "custom")]
for key, value in mutations:
value_request = request(); value_request["operation"]["drawing"][key] = value
with self.subTest(key), self.assertRaises(SoftwareContractError):
contract.normalize_request(value_request)
value_request = request(); value_request["operation"]["drawing"]["walls"][0]["start"][0] = math.inf
with self.assertRaises(SoftwareContractError):
contract.normalize_request(value_request)
value_request = request(); value_request["inputs"] = [{"key":"x","artifact_id":str(uuid4())}]
with self.assertRaises(SoftwareContractError):
contract.normalize_request(value_request)
def test_geometry_gates_and_warnings(self) -> None:
value = request()["operation"]["drawing"]
duplicate = json.loads(json.dumps(value)); duplicate["notes"][0]["id"] = "wall1"
with self.assertRaisesRegex(worker.DrawingError, "CAD2D_DUPLICATE_ID"):
worker._validate_and_normalize(duplicate)
outside = json.loads(json.dumps(value)); outside["openings"][0]["offset"] = 9900
with self.assertRaisesRegex(worker.DrawingError, "CAD2D_OPENING_OUTSIDE_WALL"):
worker._validate_and_normalize(outside)
overlap = json.loads(json.dumps(value)); overlap["openings"].append({"id":"door2","wall_id":"wall1","offset":1000,"width":900,"type":"opening"})
with self.assertRaisesRegex(worker.DrawingError, "CAD2D_OPENINGS_OVERLAP"):
worker._validate_and_normalize(overlap)
unclosed = json.loads(json.dumps(value)); unclosed["areas"][0]["boundary"][-1] = [1,0]
with self.assertRaisesRegex(worker.DrawingError, "CAD2D_AREA_NOT_CLOSED"):
worker._validate_and_normalize(unclosed)
collision = json.loads(json.dumps(value)); collision["objects"][0]["position"] = [1000,400]
_, warnings, _ = worker._validate_and_normalize(collision)
self.assertIn("CAD2D_DOOR_SWING_OBJECT_COLLISION", {item["code"] for item in warnings})
def test_worker_writes_reopenable_fixed_package_deterministically(self) -> None:
manifests = []
for _ in range(3):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory); (root / "request").mkdir()
(root / "request" / "request.json").write_text(json.dumps({"job_id":str(uuid4()),"lease_id":str(uuid4()),"request_digest":"a"*64,"request":request()}, ensure_ascii=False), encoding="utf-8")
artifacts = worker.run(root)
self.assertEqual({item["filename"] for item in artifacts}, set(worker.OUTPUT_MEDIA))
reopened = worker._reopen_dxf(root / "output" / "drawing.dxf")
self.assertEqual(reopened["version"], "R2013")
self.assertEqual(reopened["layers"], sorted(worker.LAYERS))
report = json.loads((root / "output" / "validation-report.json").read_text(encoding="utf-8"))
self.assertTrue(report["passed"])
self.assertGreater((root / "output" / "drawing.png").stat().st_size, 1000)
manifests.append(json.loads((root / "output" / "drawing-manifest.json").read_text(encoding="utf-8")))
self.assertEqual(manifests[0], manifests[1])
self.assertEqual(manifests[1], manifests[2])
if __name__ == "__main__":
unittest.main()