183 lines
7.9 KiB
Python
183 lines
7.9 KiB
Python
"""Extrude draft contracts distinguish executable and unsupported extents.
|
||
|
||
中文说明
|
||
--------
|
||
这个文件在测试什么(issue #2「draft 被 schema 接受但 runtime 未执行」的回归测试):
|
||
|
||
CADFS lowering now maps a single-sided blind draft to
|
||
``Build123dGeometryAdapter.extrude_taper``. The CDSL machine schema uses
|
||
``angle_deg`` plus ``pull_direction`` for that executable contract.
|
||
|
||
A two-sided or non-blind draft has no defined neutral-plane semantics in
|
||
the current CDSL runtime. It must remain in the input document but stop
|
||
with ``unsupported_draft_extent`` before execution. Non-canonical draft
|
||
fields are protocol errors and are rejected by the machine schema.
|
||
|
||
4. sys.path 说明:把 backend/engine 加入搜索路径,是为了直接 import
|
||
cdsl_engine 包做端到端测试(与 test_engine_revolve_reverse.py 风格
|
||
一致)。
|
||
|
||
函数功能一览
|
||
------------
|
||
_rectangle() 构造 XY 平面内的矩形轮廓(2D 多边形)。
|
||
_extrude_cdsl() 构造最小 extrude 文档;draft 参数决定
|
||
是否携带 draft 字段、atomic_id 可切换
|
||
三种 extrude 原子。
|
||
_rebuild() 在临时目录内调用 rebuild_cdsl(strict),
|
||
带 draft 时预期抛 ValueError。
|
||
_validate_against_cdsl_schema() 对整张文档跑 cdsl_schema.json 校验。
|
||
RevolveReverseContractTests (见各测试方法 docstring)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT / "backend"))
|
||
sys.path.insert(0, str(ROOT / "backend" / "engine"))
|
||
|
||
import jsonschema # noqa: E402
|
||
|
||
import cdsl_engine # noqa: E402
|
||
from cdsl_engine.batch_rebuild import analyze_document # noqa: E402
|
||
from cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl # noqa: E402
|
||
|
||
# cdsl_schema.json 路径:随 cdsl_engine 包部署。
|
||
_SCHEMA_PATH = Path(cdsl_engine.__file__).parent / "cdsl_schema.json"
|
||
_SCHEMA = json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试夹具
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _rectangle(minimum: list[float], maximum: list[float]) -> dict:
|
||
"""XY 平面内的矩形轮廓(2D 多边形),顶点逆时针。
|
||
|
||
用于拉伸特征的最小闭合轮廓:x∈[minimum[0], maximum[0]]、
|
||
y∈[minimum[1], maximum[1]]。
|
||
"""
|
||
return {"type": "polygon", "vertices": [
|
||
[minimum[0], minimum[1]], [maximum[0], minimum[1]],
|
||
[maximum[0], maximum[1]], [minimum[0], maximum[1]],
|
||
]}
|
||
|
||
|
||
def _extrude_cdsl(*, atomic_id: str = "extrude_add_blind", with_draft: bool = True) -> dict:
|
||
"""构造最小 extrude 文档。
|
||
|
||
- with_draft=True 时 params 携带可执行的单侧盲向 draft 对象;
|
||
- with_draft=False 时完全不写 draft 字段(回归护栏用)。
|
||
- atomic_id 可切换 extrude_add_blind / extrude_add_two_sided /
|
||
extrude_cut_blind 三种原子,验证同一 blocker 检查对全类生效。
|
||
"""
|
||
params: dict = {"distance_mm": 10.0}
|
||
if atomic_id == "extrude_add_two_sided":
|
||
params["reverse_distance_mm"] = 10.0
|
||
if with_draft:
|
||
params["draft"] = {"angle_deg": 5.0, "pull_direction": True}
|
||
return {
|
||
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part",
|
||
"part_id": "draft-contract", "meta": {"unit": "mm"},
|
||
"geometry": {"sketches": [{
|
||
"id": "base",
|
||
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
||
"profile": _rectangle([-5, -5], [5, 5]),
|
||
}]},
|
||
"features": [{
|
||
"id": "base_add", "atomic_id": atomic_id, "depends_on": [], "sketch_id": "base",
|
||
"params": params, "execution_status": "supported",
|
||
}],
|
||
}
|
||
|
||
|
||
def _validate_against_cdsl_schema(doc: dict) -> None:
|
||
"""对整张 CDSL 文档跑 cdsl_schema.json 校验;任何字段不通过都会抛 ValidationError。"""
|
||
jsonschema.validate(instance=doc, schema=_SCHEMA)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试套件
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ExtrudeDraftContractTests(unittest.TestCase):
|
||
"""draft 假接受陷阱「文档格式-能力边界-运行时」三方合同的回归测试。"""
|
||
|
||
def test_blind_draft_extrude_is_runtime_eligible(self) -> None:
|
||
"""A canonical single-sided blind draft reaches the taper executor."""
|
||
analysis = analyze_cdsl(_extrude_cdsl(with_draft=True))
|
||
|
||
self.assertTrue(analysis.runtime_eligible)
|
||
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
|
||
self.assertNotIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
|
||
|
||
def test_draft_free_extrude_stays_eligible(self) -> None:
|
||
"""回归护栏:不带 draft 的 extrude 特征仍必须 runtime_eligible。
|
||
|
||
修复前/修复后均应通过。这条测试防止我们把检查加过头——
|
||
一旦把"携带 draft"错写成"所有 extrude 都拒绝",护栏会变红。
|
||
"""
|
||
analysis = analyze_cdsl(_extrude_cdsl(with_draft=False))
|
||
|
||
self.assertTrue(analysis.runtime_eligible)
|
||
|
||
def test_draft_passes_machine_schema(self) -> None:
|
||
"""Canonical draft input is a valid CDSL document for each extrude form."""
|
||
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_blind", with_draft=True))
|
||
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
|
||
_validate_against_cdsl_schema(_extrude_cdsl(atomic_id="extrude_cut_blind", with_draft=True))
|
||
|
||
def test_two_sided_draft_is_explicitly_blocked(self) -> None:
|
||
analysis = analyze_cdsl(_extrude_cdsl(atomic_id="extrude_add_two_sided", with_draft=True))
|
||
|
||
self.assertFalse(analysis.runtime_eligible)
|
||
result = next(item for item in analysis.feature_results if item.feature_id == "base_add")
|
||
self.assertIn("unsupported_draft_extent", [blocker.code for blocker in result.blockers])
|
||
|
||
def test_blind_draft_rebuilds_and_batch_reports_an_artifact(self) -> None:
|
||
"""The supported branch builds a STEP artifact through both entry points."""
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
out_step = Path(directory) / "part.step"
|
||
rebuild_cdsl(_extrude_cdsl(with_draft=True), out_step)
|
||
self.assertTrue(out_step.exists())
|
||
|
||
cdsl = _extrude_cdsl(with_draft=True)
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
out_step = Path(directory) / "part.step"
|
||
report = _analyze_inline(cdsl, out_step)
|
||
self.assertTrue(out_step.exists())
|
||
self.assertTrue(report["runtime_eligible"])
|
||
self.assertTrue(report["built"])
|
||
|
||
def test_noncanonical_draft_is_rejected_by_machine_schema(self) -> None:
|
||
cdsl = _extrude_cdsl(with_draft=True)
|
||
cdsl["features"][0]["params"]["draft"] = {"angle_deg": 5.0, "direction": "toward_sketch"}
|
||
with self.assertRaises(jsonschema.ValidationError):
|
||
_validate_against_cdsl_schema(cdsl)
|
||
|
||
|
||
def _analyze_inline(cdsl: dict, out_step: Path) -> dict:
|
||
"""把 cdsl 写入临时 json 后走 analyze_document(与批量层同一入口)。
|
||
|
||
analyze_document 接受文件路径,这里把内存中的文档落地成临时文件,
|
||
保证测试走的路径与 batch_rebuild 完全一致。
|
||
"""
|
||
import tempfile as _tf
|
||
|
||
with _tf.TemporaryDirectory() as directory:
|
||
source = Path(directory) / "draft.cdsl.json"
|
||
source.write_text(json.dumps(cdsl), encoding="utf-8")
|
||
return analyze_document(source, out_step=out_step)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|