"""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