from __future__ import annotations import sys import tempfile from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.scdm_result_validator import ( # noqa: E402 build_scdm_id_mapping, check_scdm_summary_delta, check_scdm_unedited_objects, check_scdm_target, match_scdm_object_by_signature, rewrite_scdm_relation_formula_ids, validate_scdm_edit_result, ) def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float, float, float]) -> dict[str, object]: return { "objectId": object_id, "objectType": "hole", "geometrySignature": { "objectType": "hole", "faceIds": [face_id], "surfaceType": "cylinder", "center": list(center), "axis": [0.0, 0.0, 1.0], "diameter": diameter, }, "capabilities": [ {"key": "hole.diameter", "currentValue": diameter}, {"key": "hole.position", "currentValue": list(center)}, ], } def _cache(*objects: dict[str, object]) -> dict[str, object]: return { "schemaVersion": 1, "source": "SCDM", "objects": list(objects), "diagnostics": {}, } def _cache_with_summary(summary: dict[str, int], *objects: dict[str, object]) -> dict[str, object]: cache = _cache(*objects) cache["diagnostics"] = {"raw_summary": dict(summary)} return cache def main() -> int: with tempfile.TemporaryDirectory(prefix="step_editor_scdm_validate_") as temp: root = Path(temp) output_step = root / "result.step" output_step.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8") before = _cache( _hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)), _hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)), ) after = _cache( _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), _hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 9.5)), ) before_signature = before["objects"][0]["geometrySignature"] # type: ignore[index] match = match_scdm_object_by_signature(before_signature, after, capability_key="hole.diameter") _assert(match.get("status") == "unique", f"changed diameter should still match by center/axis/type: {match}") _assert(match.get("object", {}).get("objectId") == "hole:90", f"wrong match: {match}") ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, before_cache=before, after_cache=after, capability_key="hole.diameter", expected_target=0.75, edited_object_id="hole:85", brep_validator=lambda path: {"ok": path.is_file(), "reason": "ok"}, ) _assert(ok.get("ok") is True, f"validated edit should pass: {ok}") _assert(ok.get("targetCheck", {}).get("ok") is True, f"target diameter should be checked: {ok}") _assert(ok.get("topologyCheck", {}).get("ok") is True, f"unchanged objects should be checked: {ok}") drift_after = _cache(_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5))) drift = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, before_cache=before, after_cache=drift_after, capability_key="hole.diameter", expected_target=0.75, edited_object_id="hole:85", ) _assert(drift.get("ok") is False and drift.get("reason") == "unexpected-object-drift", f"missing unrelated hole should fail: {drift}") direct_drift = check_scdm_unedited_objects(before, drift_after, edited_object_id="hole:85", edited_signature=before_signature) _assert(direct_drift.get("ok") is False and direct_drift.get("checked") == 1, f"direct drift check should inspect one unedited object: {direct_drift}") mismatch = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, after_cache=after, capability_key="hole.diameter", expected_target=0.9, ) _assert(mismatch.get("ok") is False and mismatch.get("reason") == "target-mismatch", f"wrong target should fail: {mismatch}") missing = validate_scdm_edit_result({"ok": True, "output_step": str(root / "missing.step")}) _assert(missing.get("ok") is False and missing.get("reason") == "missing-output-step", f"missing result STEP should fail: {missing}") mapping = build_scdm_id_mapping(before, after, capability_key="hole.diameter") _assert(mapping.get("faceIdMap") == {85: 90, 87: 91}, f"face IDs should remap through signatures: {mapping}") rewritten = rewrite_scdm_relation_formula_ids("Face87.直径 = Face85.半径", mapping) _assert(rewritten == "Face91.直径 = Face90.半径", f"formula IDs should follow SCDM remap: {rewritten}") position_after = _cache(_hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 6.0))) position_check = check_scdm_target(position_after["objects"][0], capability_key="hole.position", expected_target=[2.0, 1.0, 6.0]) _assert(position_check.get("ok") is True, f"position target should pass: {position_check}") summary_before = _cache_with_summary( {"bodyCount": 13, "objectCount": 554, "faceCount": 158, "edgeCount": 396}, _hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)), ) summary_after_ok = _cache_with_summary( {"bodyCount": 13, "objectCount": 550, "faceCount": 157, "edgeCount": 390}, _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), ) summary_ok = check_scdm_summary_delta(summary_before, summary_after_ok, capability_key="hole.diameter") _assert(summary_ok.get("ok") is True, f"small summary changes should pass: {summary_ok}") summary_after_bad = _cache_with_summary( {"bodyCount": 13, "objectCount": 80, "faceCount": 20, "edgeCount": 45}, _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), ) summary_bad = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=summary_before["objects"][0]["geometrySignature"], # type: ignore[index] before_cache=summary_before, after_cache=summary_after_bad, capability_key="hole.diameter", expected_target=0.75, ) _assert(summary_bad.get("ok") is False and summary_bad.get("reason") == "summary-drift", f"large summary drift should fail: {summary_bad}") summary_fill = check_scdm_summary_delta(summary_before, summary_after_bad, capability_key="feature.fill") _assert(summary_fill.get("ok") is None and summary_fill.get("reason") == "skipped-command-feature", f"fill should skip summary count guard: {summary_fill}") ambiguous_after = _cache( _hole("hole:100", 100, diameter=0.75, center=(0.5, 1.0, 9.5)), _hole("hole:101", 101, diameter=0.75, center=(0.5, 1.0, 9.5)), ) ambiguous = match_scdm_object_by_signature(before_signature, ambiguous_after, capability_key="hole.diameter") _assert(ambiguous.get("status") == "multiple", f"ambiguous matches should be reported: {ambiguous}") print("scdm result validator ok") return 0 if __name__ == "__main__": raise SystemExit(main())