Files
cdsl-cad/backend/tests/test_engine_extrude_draft_contract.py

183 lines
7.9 KiB
Python
Raw Permalink 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.
"""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_cdslstrict),
带 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()