import importlib 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) class Rotation: def multVec(self, vector): return vector class Placement: def __init__(self, base=None, rotation=None): self.Base = base or Vector() self.Rotation = rotation or Rotation() fake_freecad = types.ModuleType("FreeCAD") fake_freecad.Vector = Vector fake_freecad.Rotation = Rotation fake_freecad.Placement = Placement fake_freecad.ActiveDocument = None fake_freecad.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, ) sys.modules["FreeCAD"] = fake_freecad fake_importgui = types.ModuleType("ImportGui") def insert(name, docName=None, merge=False, useLinkGroup=True): doc = fake_freecad.ActiveDocument obj = doc.addObject("Part::Feature", "ImportedBatchModel") obj.ImportedPath = name return obj fake_importgui.insert = insert sys.modules["ImportGui"] = fake_importgui fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: []) sys.modules["FreeCADGui"] = fake_freecadgui class FakeViewObject: def __init__(self): self.Visibility = True class FakeObject: def __init__(self, name, type_id): self.Name = name self.Label = name self.TypeId = type_id self.PropertiesList = [] self.Group = [] self.InList = [] self.ViewObject = FakeViewObject() self.Placement = sys.modules["FreeCAD"].Placement() self.Shape = None def isDerivedFrom(self, type_name): if self.TypeId == type_name: return True if type_name == "App::DocumentObjectGroup": return self.TypeId == "App::DocumentObjectGroup" if type_name == "App::LocalCoordinateSystem": return self.TypeId in {"Part::LocalCoordinateSystem", "PartDesign::CoordinateSystem"} 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.Objects = [] self.Name = "QETScene" self.recompute_count = 0 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 recompute(self): self.recompute_count += 1 def _reload_modules(*extra_names): for name in ["RoutingNetwork", "WiringObjects", "TemplateSemantics", "TerminalObjects", "BatchAssembly"] + list(extra_names): sys.modules.pop(name, None) terminal_objects = importlib.import_module("TerminalObjects") batch_assembly = importlib.import_module("BatchAssembly") return terminal_objects, batch_assembly class BatchAssemblyTest(unittest.TestCase): def test_create_terminal_block_places_slices_and_local_terminals_along_selected_rail(self): _install_fake_freecad() terminal_objects, batch_assembly = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") report = batch_assembly.create_terminal_block( doc, rail, block_name="XT1", count=3, pitch_mm=5.2, start_offset_mm=10.0, ) self.assertEqual(3, report["created_devices"]) self.assertEqual(3, report["created_terminals"]) self.assertEqual("XT1", report["group"].Label) placements = [item.Placement.Base.x for item in report["devices"]] self.assertEqual([110.0, 115.2, 120.4], placements) terminal_labels = [terminal.Label for terminal in report["terminals"]] self.assertEqual(["XT1:1", "XT1:2", "XT1:3"], terminal_labels) self.assertTrue(all(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"])) self.assertTrue(all(not terminal.ViewObject.Visibility for terminal in report["terminals"])) def test_create_breakers_generates_numbered_devices_and_terminal_labels(self): _install_fake_freecad() terminal_objects, batch_assembly = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") report = batch_assembly.create_breakers( doc, rail, base_name="QF", count=2, pitch_mm=18.0, start_offset_mm=0.0, terminal_numbers=("1", "2", "3", "4", "5", "6"), ) self.assertEqual(["QF1", "QF2"], [device.Label for device in report["devices"]]) self.assertEqual(12, report["created_terminals"]) self.assertTrue(all(device.Name.startswith("QETDevice_") for device in report["devices"])) self.assertEqual(["QF1", "QF2"], [device.QetInstanceId for device in report["devices"]]) self.assertEqual(["QF1", "QF2"], [device.QetElementUuid for device in report["devices"]]) labels = [terminal.Label for terminal in report["terminals"]] self.assertEqual(["QF1:1", "QF1:2", "QF1:3", "QF1:4", "QF1:5", "QF1:6"], labels[:6]) self.assertEqual(["QF2:1", "QF2:2", "QF2:3", "QF2:4", "QF2:5", "QF2:6"], labels[6:]) self.assertEqual([0.0, 18.0], [device.Placement.Base.x for device in report["devices"]]) def test_created_breaker_local_slots_can_be_promoted_to_qet_terminal_uuid(self): _install_fake_freecad() terminal_objects, batch_assembly = _reload_modules("AutoRouting") auto_routing = importlib.import_module("AutoRouting") app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") batch_assembly.create_breakers( doc, rail, base_name="QF", count=1, terminal_numbers=("1", "2"), ) payload = { "project_uuid": "project-1", "wires": [ { "wire_uuid": "wire-1", "start_instance_id": "QF1", "start_terminal_uuid": "terminal-qf1-1", "start_terminal_display": "1", "end_instance_id": "QF1", "end_terminal_uuid": "terminal-qf1-2", "end_terminal_display": "2", } ], } report = auto_routing.bind_wire_task_terminals_from_payload(doc, payload) self.assertEqual(2, report["bound"]) indexed = auto_routing.index_terminals(doc) self.assertIn("terminal-qf1-1", indexed) self.assertIn("terminal-qf1-2", indexed) self.assertFalse(any(key.startswith("local:QF1") for key in indexed)) def test_create_breakers_can_import_model_template_instead_of_placeholder_box(self): _install_fake_freecad() terminal_objects, batch_assembly = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") with tempfile.TemporaryDirectory() as temp_dir: model_path = Path(temp_dir) / "breaker.step" model_path.write_text("fake step", encoding="utf-8") report = batch_assembly.create_breakers( doc, rail, base_name="QF", count=1, model_path=str(model_path), ) imported_children = [child for child in report["devices"][0].Group if getattr(child, "ImportedPath", "")] self.assertEqual(1, len(imported_children)) self.assertEqual(str(model_path), imported_children[0].QetBatchSourceModelPath) self.assertEqual("QF1 模型", imported_children[0].Label) if __name__ == "__main__": unittest.main()