112 lines
6.4 KiB
Python
112 lines
6.4 KiB
Python
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()
|