import importlib import json import sys import tempfile import types import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange" if str(MODULE_DIR) not in sys.path: sys.path.insert(0, str(MODULE_DIR)) def _install_fake_freecad(): class Vector: def __init__(self, x=0.0, y=0.0, z=0.0): self.x = float(x) self.y = float(y) self.z = float(z) fake_freecad = types.ModuleType("FreeCAD") fake_freecad.Vector = Vector fake_freecad.ActiveDocument = None fake_freecad.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, ) fake_freecad.addDocumentObserver = lambda observer: None sys.modules["FreeCAD"] = fake_freecad fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None sys.modules["FreeCADGui"] = fake_freecadgui fake_importgui = types.ModuleType("ImportGui") fake_importgui.insert = lambda *args, **kwargs: None sys.modules["ImportGui"] = fake_importgui fake_device_preview = types.ModuleType("DevicePreview") fake_device_preview.find_main_exchange_document = lambda preferred_name="": None sys.modules["DevicePreview"] = fake_device_preview class FakeObject: def __init__(self, name, type_id="App::DocumentObjectGroup"): self.Name = name self.Label = name self.TypeId = type_id self.PropertiesList = [] self.Group = [] self.InList = [] def isDerivedFrom(self, type_name): if self.TypeId == type_name: return True if type_name == "App::DocumentObjectGroup": return self.TypeId in {"App::DocumentObjectGroup", "App::Part"} return False def addProperty(self, prop_type, prop_name, group_name, description): if prop_name not in self.PropertiesList: self.PropertiesList.append(prop_name) def addObject(self, child): if child not in self.Group: self.Group.append(child) if self not in child.InList: child.InList.append(self) class FakeDocument: def __init__(self): self.Name = "QETScene" self.FileName = "" self.Objects = [] def addObject(self, type_name, name): obj = FakeObject(name, type_name) self.Objects.append(obj) return obj def getObject(self, name): for obj in self.Objects: if obj.Name == name: return obj return None def _reload_writeback(): for name in ["DeviceImport", "TerminalObjects", "ExchangeWriteBack"]: sys.modules.pop(name, None) return importlib.import_module("ExchangeWriteBack"), importlib.import_module("TerminalObjects") class ExchangeWriteBackManualWireTest(unittest.TestCase): def test_write_back_skips_local_terminal_bindings(self): _install_fake_freecad() exchange_writeback, terminal_objects = _reload_writeback() doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device = doc.addObject("App::Part", "QETDevice_device_1") root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1") terminal_group = terminal_objects.ensure_terminal_group( doc, device, project_uuid="project-1", instance_id="instance-1", ) qet_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_real") terminal_group.addObject(qet_terminal) terminal_objects.set_terminal_semantics( qet_terminal, "project-1", "device-1", "terminal-real", "instance-1", ) local_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_local") terminal_group.addObject(local_terminal) terminal_objects.set_terminal_semantics( local_terminal, "project-1", "device-1", "local:instance-1:P1", "instance-1", ) with tempfile.TemporaryDirectory() as temp_dir: scene_path = str(Path(temp_dir) / "scene.FCStd") report = exchange_writeback.write_back_document( doc, scene_path=scene_path, payload={"project_uuid": "project-1"}, ) payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8")) self.assertEqual( [{"terminal_uuid": "terminal-real", "instance_id": "instance-1"}], report["terminals"], ) self.assertEqual(report["terminals"], payload["terminals"]) def test_write_back_includes_manual_wires_with_route_points(self): _install_fake_freecad() exchange_writeback, terminal_objects = _reload_writeback() app = sys.modules["FreeCAD"] doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device = doc.addObject("App::Part", "QETDevice_device_1") root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1") wire_group = terminal_objects.ensure_wire_group( doc, device, project_uuid="project-1", instance_id="instance-1", ) wire = doc.addObject("Part::Feature", "QETWire_terminal_1_terminal_2") wire_group.addObject(wire) terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-1") terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-2") terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-1") terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-2") terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual") wire.Shape = ( app.Vector(1, 2, 3), app.Vector(4, 5, 6), ) with tempfile.TemporaryDirectory() as temp_dir: scene_path = str(Path(temp_dir) / "scene.FCStd") report = exchange_writeback.write_back_document( doc, scene_path=scene_path, payload={"project_uuid": "project-1"}, ) payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8")) self.assertEqual(1, len(report["manual_wires"])) self.assertEqual(1, len(payload["manual_wires"])) self.assertEqual( { "start_terminal_uuid": "terminal-1", "end_terminal_uuid": "terminal-2", "start_instance_id": "instance-1", "end_instance_id": "instance-2", "route_type": "Manual", "points": [ {"x": 1.0, "y": 2.0, "z": 3.0}, {"x": 4.0, "y": 5.0, "z": 6.0}, ], }, payload["manual_wires"][0], ) def test_write_back_skips_manual_wires_with_local_terminal_endpoints(self): _install_fake_freecad() exchange_writeback, terminal_objects = _reload_writeback() app = sys.modules["FreeCAD"] doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device = doc.addObject("App::Part", "QETDevice_device_1") root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1") wire_group = terminal_objects.ensure_wire_group( doc, device, project_uuid="project-1", instance_id="instance-1", ) wire = doc.addObject("Part::Feature", "QETWire_local_terminal") wire_group.addObject(wire) terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "local:instance-1:P1") terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-2") terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-1") terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-2") terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual") wire.Shape = ( app.Vector(1, 2, 3), app.Vector(4, 5, 6), ) with tempfile.TemporaryDirectory() as temp_dir: scene_path = str(Path(temp_dir) / "scene.FCStd") report = exchange_writeback.write_back_document( doc, scene_path=scene_path, payload={"project_uuid": "project-1"}, ) payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8")) self.assertEqual([], report["manual_wires"]) self.assertEqual([], payload["manual_wires"]) if __name__ == "__main__": unittest.main()