"""#10 Profile chain: evidence_v2 importer must lower annulus to analytic_contours. The CDSL-only runtime and ``cdsl_schema.json`` accept only three generic profiles (``circle``, ``polygon``, ``analytic_contours``). The legacy ``annulus`` macro is still emitted by ``evidence_v2_to_cdsl._analytic_profile`` for concentric circles, which makes the resulting document schema-invalid and runtime-ineligible. This test suite pins the lowering contract: concentric circles must produce an ``analytic_contours`` profile (outer + inner ring). 中文说明 -------- 这个文件在测试什么(issue #10「10 Profile 链」的回归测试): 1. 背景:CDSL-only 运行时(backend/engine/cdsl_engine)与 cdsl_schema.json 只接受三种通用 profile 类型: circle / polygon / analytic_contours。 而 evidence_v2 导入器(json_to_cdsl/evidence_v2_to_cdsl.py 的 _analytic_profile)在遇到"同心双圆"(垫圈/圆环 annulus 截面)时, 仍会输出旧宏 {type: "annulus"},导致产出的 CDSL 文档 schema 不合法、 运行时不可执行(报 profile_resolution_failed / unsupported_profile)。 2. 本测试套件把"降级契约"固定下来:同心双圆必须降级为 analytic_contours 轮廓(外圆 role=outer + 内圆 role=inner), 并保证 importer 输出能被 CDSL-only 运行时端到端重建为实体。 3. sys.path 说明:把 json_to_cdsl 与 backend/engine 加入搜索路径, 是为了让测试能直接 import 导入器内部的 _analytic_profile(灰盒测试) 以及 CDSL-only 运行时的 rebuild_cdsl(端到端测试)。 """ from __future__ import annotations import math 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")) sys.path.insert(0, str(ROOT / "json_to_cdsl")) try: import build123d # noqa: F401 _HAS_BUILD123D = True except ImportError: _HAS_BUILD123D = False from evidence_v2_to_cdsl import _analytic_profile # noqa: E402 def _circle_segment(center: list[float], radius_mm: float, *, construction: bool = False) -> dict: """One full SolidWorks sketch circle segment (start == end == center). 构造一条 SolidWorks 草图中的完整圆线段(start == end == center, 即闭合圆)。radius_mm 除以 1000 转成米,与导入器内部单位保持一致。 """ return { "geometry": { "segment_type": "swSketchARC", "construction": construction, "start": center, "end": center, "center": center, "direction": 1, "curve": {"type": "circle", "parameters": [*center, 0, 0, 0, 1, radius_mm / 1000.0]}, } } def _ring_cdsl(profile: dict) -> dict: """Assemble a minimal CDSL document whose single feature extrudes a ring profile. 组装一份最简 CDSL 文档:一张草图(ring,携带被测试的 profile)+ 一个拉伸特征(extrude_add_blind)。供端到端测试使用, 验证 importer 产出的 profile 能否被运行时重建为实体。 """ return { "schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "annulus-ring", "meta": {"unit": "mm"}, "geometry": {"sketches": [{ "id": "ring", "workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]}, "profile": profile, }]}, "features": [{ "id": "ring_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 20}, "sketch_id": "ring", }], } class EvidenceV2AnnulusLoweringTests(unittest.TestCase): """annulus 降级契约的回归测试套件。 三个测试用例分别覆盖: 1. test_evidence_v2_emits_analytic_contours_for_annulus 主契约:同心双圆(外 r=10mm,内 r=5mm)必须降级为 analytic_contours, 且恰好携带 outer(10) + inner(5) 两条轮廓。 2. test_evidence_v2_emits_analytic_contours_for_multiple_circles 回归护栏:两个独立(圆心不同、互不包含)的圆本来就输出 analytic_contours,修复 annulus 分支时不得破坏该既有行为。 3. test_evidence_v2_annulus_output_rebuilds_to_ring_solid 端到端:importer 输出的 analytic_contours 必须能被 CDSL-only 运行时 rebuild_cdsl 重建为实体,且体积与圆环公式 π * (r外² - r内²) * 高度 一致(误差 1%)。 """ def test_evidence_v2_emits_analytic_contours_for_annulus(self) -> None: """Concentric circles must lower to a generic ring (outer + inner).""" # 主契约测试:同圆心 (0,0) 的两个圆,外径 10mm、内径 5mm sketch = {"segments": [ _circle_segment([0, 0], 10.0), _circle_segment([0, 0], 5.0), ]} profile = _analytic_profile(sketch) # 1) profile 类型必须是 analytic_contours(而不是旧宏 annulus) self.assertEqual(profile["type"], "analytic_contours") contours = profile["contours"] # 2) 必须恰好有两条轮廓 self.assertEqual(len(contours), 2) # 3) 两条轮廓的角色分别是 outer(外圆)与 inner(内圆) self.assertEqual({contour["role"] for contour in contours}, {"outer", "inner"}) outer = next(contour for contour in contours if contour["role"] == "outer") inner = next(contour for contour in contours if contour["role"] == "inner") # 4) 半径正确:外 10mm / 内 5mm self.assertEqual(outer["segments"][0]["radius_mm"], 10.0) self.assertEqual(inner["segments"][0]["radius_mm"], 5.0) def test_evidence_v2_emits_analytic_contours_for_multiple_circles(self) -> None: """Regression guard: independent circles already lower to contours.""" # 回归护栏:两个圆心不同(相距 30mm)、互不包含的独立圆, # 修复 annulus 分支前后都必须保持输出 analytic_contours(各一条 outer 轮廓) sketch = {"segments": [ _circle_segment([0, 0], 10.0), _circle_segment([30, 0], 5.0), ]} profile = _analytic_profile(sketch) self.assertEqual(profile["type"], "analytic_contours") self.assertEqual(len(profile["contours"]), 2) @unittest.skipUnless(_HAS_BUILD123D, "build123d is not available") def test_evidence_v2_annulus_output_rebuilds_to_ring_solid(self) -> None: """Importer output must be consumed end-to-end by the CDSL-only runtime.""" # 端到端:直接取 _analytic_profile 的输出组装成 CDSL 文档, # 交给 CDSL-only 运行时 rebuild_cdsl 重建实体,再断言体积符合圆环公式。 # 此测试验证修复后运行时不再报 profile_resolution_failed / unsupported_profile。 from cdsl_engine.runtime import rebuild_cdsl sketch = {"segments": [ _circle_segment([0, 0], 10.0), _circle_segment([0, 0], 5.0), ]} profile = _analytic_profile(sketch) cdsl = _ring_cdsl(profile) with tempfile.TemporaryDirectory() as directory: out_step = Path(directory) / "ring.step" result = rebuild_cdsl(cdsl, out_step) # 圆环体积 = π * (r外² - r内²) * 高度 = π * (100 - 25) * 20 expected = math.pi * (10.0 ** 2 - 5.0 ** 2) * 20.0 self.assertAlmostEqual(result["volume_mm3"], expected, delta=expected * 0.01) if __name__ == "__main__": unittest.main()