forked from 1b-agent/opencadstudio-upstream
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought over from huaninstratech/OpenCADStudio so the specification no longer runs ahead of the code: - entity CRUD: entities_create, entities_delete, entities_transform, entities_copy_to, and `where` filters on query - blocks and export: block_define, block_delete, a normalised wblock - publishing: layout_create, page_setup_set, plot (per_page included) - metadata: sysvar, xdata_set / xdata_get, file_identity - collections: group_create, selection_set_save / load - control plane: batch, events, view_focus, close - the REST transport: `--http <port>`, the /api/v1 routes and their OpenAPI 3 description - person-in-the-loop picks: user_select and getpoint, hosted by the GUI over the same REST routes, with on-screen pending signals Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e, bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045, 37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc, fd5bc14d, 485fac61 and 2c5fc994. Left out on purpose: 37de5a46 also removed the donation surface (the Patreon fetch, the donation prompt, the Start-page supporters rail, the DONATE command and its locale strings) and replaced the About dialog's call to action; that is the fork's own product decision and stays out. The fork's version bump and its unrelated IFC/BIM work are not part of this. Adapted while porting: the REST server and the GUI bridge are desktop-only and a template start is refused in the web build, so the wasm target still compiles; the two plot tests read the PDF through the shared stream helper, because exported streams are Flate-compressed; the benches set the new crosshair field; comments and docs describe each operation in its own terms rather than by another product's API names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Black-box smoke test for the headless automation server (`--serve`).
|
|
|
|
Exercises the ops an external client needs: state discovery, the wblock
|
|
export, the plot-to-PDF export, and the capability advertisement. Uses only
|
|
the Python standard library; treats the executable as a black box.
|
|
|
|
python3 docs/automation/serve_smoke.py target/debug/OpenCADStudio
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
class Serve:
|
|
def __init__(self, exe: Path):
|
|
self.process = subprocess.Popen(
|
|
[str(exe), "--serve"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
self.ready = json.loads(self.process.stdout.readline())
|
|
assert self.ready["ok"] is True, self.ready
|
|
assert self.ready["session_id"], self.ready
|
|
|
|
def send(self, request: dict) -> dict:
|
|
self.process.stdin.write(json.dumps(request) + "\n")
|
|
self.process.stdin.flush()
|
|
return json.loads(self.process.stdout.readline())
|
|
|
|
def close(self) -> None:
|
|
self.process.stdin.close()
|
|
self.process.wait(timeout=30)
|
|
|
|
|
|
def main() -> None:
|
|
exe = Path(sys.argv[1] if len(sys.argv) > 1 else "target/debug/OpenCADStudio").resolve()
|
|
out_dir = Path(tempfile.mkdtemp(prefix="ocs_serve_smoke_"))
|
|
server = Serve(exe)
|
|
try:
|
|
state = server.send({"protocol": 1, "op": "state"})
|
|
document_id = state["document_id"]
|
|
|
|
# Draw something to export and plot.
|
|
server.send({"op": "new"})
|
|
for request_id, cmd in [("run-1", "LINE 0,0 100,80"), ("run-2", "CIRCLE 50,40 20")]:
|
|
response = server.send({"op": "run", "cmd": cmd})
|
|
assert response["ok"] is True, response
|
|
|
|
# Read-only ops stay on the legacy (non-envelope) shape.
|
|
summary = server.send({"op": "entities"})
|
|
assert summary["total"] == 2, summary
|
|
|
|
# wblock: export the two entities to a standalone DXF.
|
|
export = out_dir / "wblock.dxf"
|
|
response = server.send({
|
|
"protocol": 1,
|
|
"op": "wblock",
|
|
"request_id": "smoke-wblock-1",
|
|
"document_id": document_id,
|
|
"path": str(export),
|
|
"handles": [
|
|
entity["handle"]
|
|
for entity in server.send({"op": "query", "detail": "summary"})["entities"]
|
|
],
|
|
})
|
|
assert response["ok"] is True, response
|
|
assert response["status"] == "completed", response
|
|
assert response["result"]["entities"] == 2, response
|
|
assert export.exists()
|
|
|
|
# plot: render model space to a one-page PDF.
|
|
pdf = out_dir / "plot.pdf"
|
|
response = server.send({
|
|
"protocol": 1,
|
|
"op": "plot",
|
|
"request_id": "smoke-plot-1",
|
|
"document_id": document_id,
|
|
"path": str(pdf),
|
|
})
|
|
assert response["ok"] is True, response
|
|
assert response["result"]["pages"] == 1, response
|
|
assert pdf.read_bytes()[:4] == b"%PDF"
|
|
|
|
# The file operations are discoverable through capabilities.
|
|
capabilities = server.send({"op": "capabilities"})
|
|
operations = capabilities["operations"]
|
|
assert operations["wblock"] is True, operations
|
|
assert operations["plot"] is True, operations
|
|
assert operations["embed_image"] is True, operations
|
|
print("serve smoke: OK")
|
|
finally:
|
|
server.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|