@@ -0,0 +1,170 @@
""" #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 ( )