Files
cadSet/SimpleCADAPI/test/test_fusion360_translator.py

265 lines
9.7 KiB
Python

"""Host-independent tests for the Fusion 360 translator backend."""
from __future__ import annotations
import ast
import copy
import importlib
import math
import sys
import unittest
import simplecadapi as scad
from simplecadapi import GraphSession
from simplecadapi.translator.errors import TranslationRequestError
def _rectangle_extrusion_model() -> str:
with GraphSession() as session:
profile = scad.make_rectangle_rface(width=2.0, height=1.0)
scad.extrude_rsolid(
profile=profile,
direction=(0.0, 0.0, 1.0),
distance=3.0,
)
return scad.export_model_json(session=session)
class TestFusion360Translator(unittest.TestCase):
def test_backend_imports_without_target_runtime_modules(self):
before = set(sys.modules)
fusion = importlib.import_module("simplecadapi.translator.fusion360_translator")
self.assertNotIn("adsk.core", set(sys.modules) - before)
self.assertNotIn("adsk.fusion", set(sys.modules) - before)
self.assertEqual(fusion.CAPABILITIES.backend_id, "fusion360")
def test_supported_graph_emits_deterministic_compilable_script(self):
model_json = _rectangle_extrusion_model()
from simplecadapi.translator.fusion360_translator import Fusion360Translator
translator = Fusion360Translator(document_name="ContractFusion")
first = translator.translate_model_json(model_json)
second = translator.translate_model_json(model_json)
self.assertEqual(first.content, second.content)
compile(first.content, "<fusion360-script>", "exec")
self.assertFalse(first.metadata["target_runtime_validated"])
self.assertTrue(translator.capabilities.targets[0].requires_external_runtime)
def test_payload_uses_canonical_periodic_curve_frames(self):
with GraphSession() as session:
scad.make_angle_arc_rwire(
(0.0, 0.0, 0.0),
2.0,
0.0,
math.pi / 2.0,
normal=(1.0, 0.0, 0.0),
)
scad.make_helix_rwire(
1.0,
3.0,
2.0,
dir=(1.0, 0.0, 0.0),
)
from simplecadapi.translator.fusion360_translator import Fusion360Translator
script = (
Fusion360Translator()
.translate_model_json(scad.export_model_json(session))
.content
)
module = ast.parse(script)
model_payload = next(
ast.literal_eval(node.value)
for node in module.body
if isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == "MODEL_PAYLOAD"
for target in node.targets
)
)
curve_nodes = {
node["op"]: node
for node in model_payload["graph"]["nodes"]
if node["op"] in {"make_angle_arc_redge", "make_helix_redge"}
}
for node in curve_nodes.values():
self.assertEqual(node["params"]["_kernel_x_axis"], [-0.0, 0.0, 1.0])
self.assertEqual(node["params"]["_kernel_y_axis"], [0.0, -1.0, 0.0])
self.assertIn("Circle3D.createByCenter", script)
self.assertIn("SweepOrientationTypes.ParallelOrientationType", script)
def test_fallback_scripts_are_deterministic_and_result_scoped(self):
with GraphSession() as session:
profile = scad.make_rectangle_rface(
1.0,
2.0,
center=(2.0, 0.0, 0.0),
normal=(0.0, 1.0, 0.0),
)
scad.revolve_rsolid(profile, axis=(0.0, 1.0, 0.0))
scad.make_box_rsolid(width=4.0, height=5.0, depth=6.0)
model_json = scad.export_model_json(session)
payload = scad.import_model_json(model_json)
graph = payload["graph"]
result_id = next(
node.node_id
for node in graph.topological_order()
if node.op == "make_revolve_rsolid"
)
box_id = next(
node.node_id
for node in graph.topological_order()
if node.op == "make_box_rsolid"
)
from simplecadapi.translator.fusion360_translator import Fusion360Translator
translator = Fusion360Translator(
result_node_ids=[result_id], source_kernel_fallback=True
)
first = translator.translate_model_json(model_json).content
second = translator.translate_model_json(model_json).content
self.assertEqual(first, second)
module = ast.parse(first)
fallback_steps = next(
ast.literal_eval(node.value)
for node in module.body
if isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == "SOURCE_KERNEL_STEPS"
for target in node.targets
)
)
self.assertIn(result_id, fallback_steps)
self.assertNotIn(box_id, fallback_steps)
def test_implicit_results_prefer_active_assembly_state(self):
from simplecadapi.topology import OperationGraph
from simplecadapi.translator.fusion360_translator import Fusion360Translator
graph = OperationGraph("assembly_states")
locked = graph.add_node(
"make_compound_from_assembly_rcompound",
{"assembly_id": "fixture_locked", "component_count": 1},
node_id="locked_result",
)
operating = graph.add_node(
"make_compound_from_assembly_rcompound",
{"assembly_id": "fixture_operating", "component_count": 1},
node_id="operating_result",
)
payload = {
"schema_version": "2.0",
"graph": graph,
"leaf_ids": [locked.node_id, operating.node_id],
}
script = Fusion360Translator().translate_model_payload_to_script(
payload, graph=graph
)
self.assertIn("ACTIVE_RESULT_STATE = 'operating'", script)
self.assertIn("RESULT_NODE_IDS = ['operating_result']", script)
def test_fallback_accepts_legacy_metadata_only_selector_nodes(self):
with GraphSession() as session:
profile = scad.make_rectangle_rface(3.0, 3.0)
solid = scad.extrude_rsolid(profile, (0.0, 0.0, 1.0), 3.0)
scad.fillet_rsolid(solid, [solid.get_edges(0)], 0.2)
payload = scad.import_model_json(scad.export_model_json(session))
graph_data = copy.deepcopy(payload["graph"].to_dict())
selector_ids = {
node["node_id"]
for node in graph_data["nodes"]
if node["op"] == "make_select_redge"
}
for node in graph_data["nodes"]:
if node["node_id"] in selector_ids:
node["inputs"] = []
else:
node["inputs"] = [
input_ref
for input_ref in node.get("inputs", [])
if input_ref not in selector_ids
]
graph_data["edges"] = [
edge
for edge in graph_data.get("edges", [])
if edge[0] not in selector_ids and edge[1] not in selector_ids
]
from simplecadapi.topology import OperationGraph
from simplecadapi.translator.fusion360_translator import Fusion360Translator
legacy_graph = OperationGraph.from_dict(graph_data)
result_id = next(
node.node_id
for node in legacy_graph.nodes
if node.op == "make_fillet_rsolid"
)
legacy_payload = {
"schema_version": "2.0",
"graph": legacy_graph,
"leaf_ids": [result_id],
}
script = Fusion360Translator(
source_kernel_fallback=True
).translate_model_payload_to_script(legacy_payload, graph=legacy_graph)
module = ast.parse(script)
fallback_steps = next(
ast.literal_eval(node.value)
for node in module.body
if isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == "SOURCE_KERNEL_STEPS"
for target in node.targets
)
)
self.assertIn(result_id, fallback_steps)
def test_explicit_result_only_executes_its_dependency_closure(self):
with GraphSession() as session:
profile = scad.make_rectangle_rface(width=2.0, height=1.0)
scad.extrude_rsolid(profile, (0.0, 0.0, 1.0), 3.0)
scad.make_box_rsolid(width=1.0, height=2.0, depth=3.0)
model_json = scad.export_model_json(session=session)
payload = scad.import_model_json(model_json)
graph = payload["graph"]
extrusion_id = next(
node.node_id
for node in graph.topological_order()
if node.op == "make_extrude_rsolid"
)
from simplecadapi.translator.fusion360_translator import Fusion360Translator
script = (
Fusion360Translator(result_node_ids=[extrusion_id])
.translate_model_json(model_json)
.content
)
self.assertIn(f"RESULT_NODE_IDS = ['{extrusion_id}']", script)
self.assertIn("active_node_ids = self._result_dependency_ids", script)
self.assertIn("not in active_node_ids", script)
def test_unsupported_result_operation_is_rejected_before_host_execution(self):
with GraphSession() as session:
scad.make_box_rsolid(width=1.0, height=2.0, depth=3.0)
from simplecadapi.translator.fusion360_translator import Fusion360Translator
with self.assertRaises(TranslationRequestError):
Fusion360Translator().translate_model_json(scad.export_model_json(session))
if __name__ == "__main__":
unittest.main()