Files
pythonocc-step-editor/scripts/verify_scdm_edit_runner.py
T

352 lines
15 KiB
Python

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())