from __future__ import annotations import ast import re import subprocess 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_backend import ScdmBackendInfo # noqa: E402 from step_editor.scdm_edit_runner import generate_scdm_edit_script, prepare_scdm_edit_job, run_scdm_edit_job # noqa: E402 from step_editor.scdm_schema import read_json, write_json # noqa: E402 def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _fake_spaceclaim(path: Path) -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_text("fake", encoding="utf-8") return path def _script_path_from_command(command: list[str] | tuple[str, ...]) -> Path: for item in command: if item.startswith("/RunScript="): return Path(item.split("=", 1)[1]) raise AssertionError(f"missing /RunScript argument: {command}") def _job_path_from_script(script_path: Path) -> Path: text = script_path.read_text(encoding="utf-8") match = re.search(r"^JOB_PATH = (.+)$", text, flags=re.MULTILINE) _assert(match is not None, f"generated script should embed JOB_PATH: {script_path}") return Path(ast.literal_eval(match.group(1))) def _successful_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") output_step = Path(str(outputs["outputStep"])) output_step.write_text("ISO-10303-21;\n/* fake SCDM output */\nEND-ISO-10303-21;\n", encoding="utf-8") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake edit finished", "outputStep": str(output_step), "backendOperation": job.get("target", {}).get("backendOperation"), }, ) return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="") def _failure_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") write_json( outputs["error"], { "ok": False, "reason": "fake-failed", "message": "fake SCDM command failed", "backendOperation": job.get("target", {}).get("backendOperation"), }, ) return subprocess.CompletedProcess(command, 7, stdout="", stderr="fake failure") def _missing_output_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake success without STEP", "outputStep": str(outputs["outputStep"]), }, ) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") def _empty_output_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") Path(str(outputs["outputStep"])).write_text("", encoding="utf-8") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake success with empty STEP", "outputStep": str(outputs["outputStep"]), }, ) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") def main() -> int: with tempfile.TemporaryDirectory(prefix="step_editor_scdm_edit_") as temp: root = Path(temp) step_path = root / "sample.step" step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8") backend = ScdmBackendInfo(path=_fake_spaceclaim(root / "SpaceClaim.exe"), source="test", version="v222") signature = { "objectType": "hole", "faceIds": [85, 94], "bodyIndex": 0, "faceOrdinal": 12, "faceOrdinals": [12, 19], "scdmFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85}, {"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 96}, ], "center": [0.5, 1.0, 9.5], "axis": [0.0, 0.0, 1.0], } prepared = prepare_scdm_edit_job( step_path, output_dir=root / "prepared", backend=backend, capability_key="hole.diameter", target_value="0.75", object_id="hole:85-94", object_signature=signature, timeout_seconds=45.0, ) _assert(prepared.get("ok") is True, f"edit job should be prepared: {prepared}") job_path = Path(str(prepared["job_path"])) script_path = Path(str(prepared["script_path"])) _assert(job_path.is_file(), "scdm_edit_job.json should be written") _assert(script_path.is_file(), "scdm_edit.py should be written") job = read_json(job_path) _assert(job.get("adapter") == "spaceclaim-v1", f"bad adapter: {job}") _assert(job.get("schemaVersion") == 1, f"bad schema version: {job}") _assert(job.get("model", {}).get("sourceStep") == str(step_path.resolve(strict=False)), f"bad source path: {job}") _assert(job.get("model", {}).get("rollbackStep") == str(step_path.resolve(strict=False)), f"bad rollback path: {job}") _assert(job.get("target", {}).get("capabilityKey") == "hole.diameter", f"bad capability: {job}") _assert(job.get("target", {}).get("backendOperation") == "change_hole_diameter", f"bad operation: {job}") _assert(job.get("target", {}).get("value") == 0.75, f"target should be numeric: {job}") _assert(job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [85, 94], f"bad signature: {job}") _assert(job.get("execution", {}).get("isolatedProcess") is True, f"job should record isolated execution: {job}") script = script_path.read_text(encoding="utf-8") for token in ( "change_hole_diameter", "move_hole_axis", "move_slot", "move_boss", "pull_face_offset", "fill_feature", "StandardHoles.ModifyHoleRadius", "OffsetFaces.Execute", "Move.Translate", "MoveOptions", "OffsetFaceOptions", "FillOptions", "FillMode", "Delete.Execute", "DocumentSave", "scdmFaceLocators", "result.json", "error.json", ): _assert(token in script, f"generated edit script missing {token}") _assert("Pull.Execute" not in script, "generated edit script should use documented OffsetFaces instead of Pull.Execute") generated = generate_scdm_edit_script(job_path) _assert("JOB_PATH =" in generated and job_path.name in generated, "generated edit script should embed the job path") slot_signature = { "objectType": "slot", "faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30, "faceOrdinals": [30, 31, 32], "globalFaceOrdinal": 40, "globalFaceOrdinals": [40, 41, 42], "center": [1.0, 2.0, 3.0], "axis": [1.0, 0.0, 0.0], } slot_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "slot-position-prepared", backend=backend, capability_key="slot.position", target_value=[1.0, 2.0, 5.0], object_id="slot:30-31-32", object_signature=slot_signature, ) _assert(slot_prepared.get("ok") is True, f"slot.position should be productized and prepare an edit job: {slot_prepared}") slot_job = read_json(slot_prepared["job_path"]) _assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}") _assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}") _assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}") boss_signature = { "objectType": "cylindrical_boss", "faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50, "faceOrdinals": [50, 51, 52], "globalFaceOrdinal": 70, "globalFaceOrdinals": [70, 71, 72], "center": [0.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], } boss_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "boss-position-prepared", backend=backend, capability_key="boss.position", target_value=[2.0, 0.0, 2.0], object_id="boss:50-51-52", object_signature=boss_signature, ) _assert(boss_prepared.get("ok") is True, f"boss.position should be productized and prepare an edit job: {boss_prepared}") boss_job = read_json(boss_prepared["job_path"]) _assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}") _assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}") _assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}") planned = prepare_scdm_edit_job( step_path, output_dir=root / "unsupported", backend=backend, capability_key="slot.width", target_value="1", object_signature=signature, ) _assert( planned.get("ok") is False and planned.get("reason") == "capability-not-productized", f"planned capability should fail early with a roadmap reason: {planned}", ) unsupported = prepare_scdm_edit_job( step_path, output_dir=root / "unsupported-unknown", backend=backend, capability_key="not.real", target_value="1", object_signature=signature, ) _assert(unsupported.get("ok") is False and unsupported.get("reason") == "unsupported-capability", f"unknown capability should fail early: {unsupported}") success = run_scdm_edit_job( step_path, output_dir=root / "success", backend=backend, capability_key="hole.diameter", target_value=0.9, object_id="hole:85-94", object_signature=signature, runner=_successful_runner, ) _assert(success.get("ok") is True, f"fake edit should succeed: {success}") _assert(Path(str(success["output_step"])).is_file(), f"output STEP should exist: {success}") success_backend = success.get("backend") _assert(isinstance(success_backend, dict), f"successful edit should carry backend status: {success}") _assert(success_backend.get("runScriptOk") is True, f"successful edit should mark /RunScript usable: {success_backend}") _assert(success_backend.get("licenseOk") is True, f"successful edit should mark license usable: {success_backend}") failed = run_scdm_edit_job( step_path, output_dir=root / "failed", backend=backend, capability_key="hole.position", target_value=[0.5, 1.0, 6.0], object_id="hole:85-94", object_signature=signature, runner=_failure_runner, ) _assert(failed.get("ok") is False and failed.get("reason") == "fake-failed", f"error.json should drive failure reason: {failed}") slot_success = run_scdm_edit_job( step_path, output_dir=root / "slot-success", backend=backend, capability_key="slot.position", target_value=[1.0, 2.0, 6.0], object_id="slot:30-31-32", object_signature=slot_signature, runner=_successful_runner, ) _assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}") _assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}") boss_success = run_scdm_edit_job( step_path, output_dir=root / "boss-success", backend=backend, capability_key="boss.position", target_value=[3.0, 0.0, 2.0], object_id="boss:50-51-52", object_signature=boss_signature, runner=_successful_runner, ) _assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}") _assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}") missing_output = run_scdm_edit_job( step_path, output_dir=root / "missing-output", backend=backend, capability_key="face.offset", target_value=5, object_id="face:9", object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3}, runner=_missing_output_runner, ) _assert(missing_output.get("ok") is False and missing_output.get("reason") == "missing-output-step", f"missing output STEP should be rejected: {missing_output}") empty_output = run_scdm_edit_job( step_path, output_dir=root / "empty-output", backend=backend, capability_key="face.offset", target_value=5, object_id="face:9", object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3}, runner=_empty_output_runner, ) _assert(empty_output.get("ok") is False and empty_output.get("reason") == "empty-output-step", f"empty output STEP should be rejected: {empty_output}") print("scdm edit runner ok") return 0 if __name__ == "__main__": raise SystemExit(main())