156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
|
|
"""D17 Human-in-Loop 闭环:compute_fingerprint 分桶边界值测试。
|
|||
|
|
|
|||
|
|
compute_fingerprint 是跨任务匹配的核心函数,纯函数,单独覆盖各分桶边界。
|
|||
|
|
D17 验收:fingerprint 字典结构稳定 + 分桶边界值与 plan §5.1 一致。
|
|||
|
|
"""
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from moldinsight.services.experience_feedback_service import compute_fingerprint
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _geo(volume: float = 50000.0, dims=(80, 60, 40), faces: int = 250, undercuts: int = 0):
|
|||
|
|
"""构造测试用 geometry_data 字典。dims 单位 mm,volume 单位 mm³。"""
|
|||
|
|
return {
|
|||
|
|
"volume": volume,
|
|||
|
|
"bounding_box": {
|
|||
|
|
"dimensions": list(dims),
|
|||
|
|
},
|
|||
|
|
"topology_faces": faces,
|
|||
|
|
"undercut_count": undercuts,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── bbox_aspect 分桶(按 sorted_dims mid/min 比)──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("dims,expected", [
|
|||
|
|
# ratio = mid/min
|
|||
|
|
((100, 100, 100), "compact"), # ratio=1.0 → compact(1.0 ≤ r < 1.5)
|
|||
|
|
((100, 130, 100), "compact"), # ratio=1.0(去重后)
|
|||
|
|
((60, 80, 100), "compact"), # ratio=1.0 → compact
|
|||
|
|
((40, 80, 100), "slab"), # ratio=80/40=2.0 → slab(1.5 ≤ r < 3.0)
|
|||
|
|
((20, 80, 100), "elongated"), # ratio=80/20=4.0 → elongated(3.0 ≤ r < 6.0)
|
|||
|
|
((10, 80, 100), "long_bar"), # ratio=80/10=8 → long_bar(≥ 6.0)
|
|||
|
|
((0, 0, 0), "compact"), # 零值默认
|
|||
|
|
])
|
|||
|
|
def test_bbox_aspect_bucket(dims, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(dims=dims), "ABS", False)
|
|||
|
|
assert fp["bbox_aspect"] == expected, f"dims={dims} → expected {expected}, got {fp['bbox_aspect']}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── volume_bucket 分桶(mm³ → cm³,buckets: xs<10 / s<100 / m<500 / l<2000 / xl≥2000)──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("volume_mm3,expected", [
|
|||
|
|
(0, "xs"), # 0 cm³
|
|||
|
|
(5000, "xs"), # 5 cm³
|
|||
|
|
(9999, "xs"), # 边界 9.99 cm³ → xs
|
|||
|
|
(10000, "s"), # 10 cm³ → s(10 ≤ v < 100)
|
|||
|
|
(50000, "s"), # 50 cm³
|
|||
|
|
(99999, "s"), # 边界 99.99 cm³
|
|||
|
|
(100000, "m"), # 100 cm³ → m(100 ≤ v < 500)
|
|||
|
|
(250000, "m"), # 250 cm³
|
|||
|
|
(499999, "m"), # 边界 499.99 cm³
|
|||
|
|
(500000, "l"), # 500 cm³ → l(500 ≤ v < 2000)
|
|||
|
|
(1000000, "l"), # 1000 cm³
|
|||
|
|
(1999999, "l"), # 边界 1999.99 cm³
|
|||
|
|
(2000000, "xl"), # 2000 cm³ → xl(v ≥ 2000)
|
|||
|
|
(10000000, "xl"), # 10000 cm³
|
|||
|
|
])
|
|||
|
|
def test_volume_bucket(volume_mm3, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(volume=volume_mm3), "ABS", False)
|
|||
|
|
assert fp["volume_bucket"] == expected, f"volume={volume_mm3}mm³ → expected {expected}, got {fp['volume_bucket']}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── face_bucket 分桶 ──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("faces,expected", [
|
|||
|
|
(0, "simple"), # 0 面
|
|||
|
|
(99, "simple"), # 边界 99
|
|||
|
|
(100, "normal"), # 边界 100
|
|||
|
|
(499, "normal"), # 边界 499
|
|||
|
|
(500, "complex"), # 边界 500
|
|||
|
|
(1999, "complex"), # 边界 1999
|
|||
|
|
(2000, "dense"), # 边界 2000
|
|||
|
|
(10000, "dense"),
|
|||
|
|
])
|
|||
|
|
def test_face_bucket(faces, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(faces=faces), "ABS", False)
|
|||
|
|
assert fp["face_bucket"] == expected, f"faces={faces} → expected {expected}, got {fp['face_bucket']}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── undercut_class 分桶 ──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("undercuts,expected", [
|
|||
|
|
(0, "none"), # 0 → none
|
|||
|
|
(1, "mild"), # 1 → mild
|
|||
|
|
(3, "mild"), # 边界 3
|
|||
|
|
(4, "moderate"), # 边界 4
|
|||
|
|
(8, "moderate"), # 边界 8
|
|||
|
|
(9, "heavy"), # 边界 9
|
|||
|
|
(50, "heavy"),
|
|||
|
|
])
|
|||
|
|
def test_undercut_class(undercuts, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(undercuts=undercuts), "ABS", False)
|
|||
|
|
assert fp["undercut_class"] == expected, f"undercuts={undercuts} → expected {expected}, got {fp['undercut_class']}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── material_family 分桶 ──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("material,expected", [
|
|||
|
|
("ABS", "abs"),
|
|||
|
|
("ABS+PC", "abs"), # 子串匹配
|
|||
|
|
("PP", "pp"),
|
|||
|
|
("PA66", "pa"),
|
|||
|
|
("AlSi10Mg", "foam"), # "al" + "si"
|
|||
|
|
("AlSi12", "foam"),
|
|||
|
|
("Aluminium Alloy", "other"), # 只有 al 无 si
|
|||
|
|
("POM", "other"),
|
|||
|
|
("", "other"),
|
|||
|
|
])
|
|||
|
|
def test_material_family(material, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(), material, False)
|
|||
|
|
assert fp["material_family"] == expected, f"material={material} → expected {expected}, got {fp['material_family']}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── is_foam 字段 ──
|
|||
|
|
|
|||
|
|
@pytest.mark.parametrize("is_foam,expected", [
|
|||
|
|
(True, "true"),
|
|||
|
|
(False, "false"),
|
|||
|
|
])
|
|||
|
|
def test_is_foam(is_foam, expected):
|
|||
|
|
fp = compute_fingerprint(_geo(), "ABS", is_foam)
|
|||
|
|
assert fp["is_foam"] == expected
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── 字典键完整性 ──
|
|||
|
|
|
|||
|
|
def test_fingerprint_has_all_keys():
|
|||
|
|
fp = compute_fingerprint(_geo(), "ABS", False)
|
|||
|
|
expected_keys = {
|
|||
|
|
"bbox_aspect", "volume_bucket", "face_bucket",
|
|||
|
|
"undercut_class", "material_family", "is_foam",
|
|||
|
|
}
|
|||
|
|
assert set(fp.keys()) == expected_keys
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── 空 geometry_data 容错 ──
|
|||
|
|
|
|||
|
|
def test_empty_geometry_data_returns_safe_defaults():
|
|||
|
|
fp = compute_fingerprint(None, "ABS", False)
|
|||
|
|
# 不应抛异常,所有键存在且为合法 bucket 值
|
|||
|
|
assert fp["bbox_aspect"] == "compact"
|
|||
|
|
assert fp["volume_bucket"] == "xs"
|
|||
|
|
assert fp["face_bucket"] == "simple"
|
|||
|
|
assert fp["undercut_class"] == "none"
|
|||
|
|
assert fp["material_family"] == "abs"
|
|||
|
|
assert fp["is_foam"] == "false"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_partial_geometry_data():
|
|||
|
|
"""只有 bounding_box 没有 topology_faces,应走 fallback 0。"""
|
|||
|
|
fp = compute_fingerprint(
|
|||
|
|
{"bounding_box": {"dimensions": [100, 100, 100]}},
|
|||
|
|
"ABS",
|
|||
|
|
False,
|
|||
|
|
)
|
|||
|
|
assert fp["face_bucket"] == "simple" # 0 面 → simple
|