import importlib import sys 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 __init__(self, axis=None, angle=None): self.Axis = axis self.Angle = angle class Placement: def __init__(self, base=None, rotation=None): self.Base = base self.Rotation = 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_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None fake_freecadgui.Selection = types.SimpleNamespace(getSelectionEx=lambda: []) sys.modules["FreeCADGui"] = fake_freecadgui def _install_fake_freecad_without_gui_commands(): _install_fake_freecad() sys.modules["FreeCADGui"] = types.ModuleType("FreeCADGui") class FakeViewObject: def __init__(self): self.Visibility = True self.ShapeColor = None class FakeObject: def __init__(self, name, type_id): self.Name = name self.Label = name self.TypeId = type_id self.PropertiesList = [] self.Group = [] self.ViewObject = FakeViewObject() self.Placement = 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) class FakeDocument: def __init__(self): self.Name = "TemplateDoc" self.Objects = [] self.recomputed = False self.saved_path = "" 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.recomputed = True def saveAs(self, path): self.saved_path = path def _reload_modules(): for name in ["TerminalObjects", "TemplateAuthoring"]: sys.modules.pop(name, None) return importlib.import_module("TemplateAuthoring") class TemplateAuthoringTest(unittest.TestCase): def test_template_authoring_command_titles_are_chinese(self): _install_fake_freecad() template_authoring = _reload_modules() self.assertEqual( "添加模板端子", template_authoring.CommandAddTemplateTerminal().GetResources()["MenuText"], ) self.assertEqual( "校验模板端子", template_authoring.CommandValidateTemplateTerminals().GetResources()["MenuText"], ) self.assertEqual( "保存模板为 FCStd", template_authoring.CommandSaveTemplateAsFCStd().GetResources()["MenuText"], ) def test_import_skips_command_registration_when_gui_has_no_add_command(self): _install_fake_freecad_without_gui_commands() template_authoring = _reload_modules() self.assertTrue(hasattr(template_authoring, "save_template_as_fcstd")) def test_create_template_terminal_writes_lcs_semantics(self): _install_fake_freecad() template_authoring = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal = template_authoring.create_template_terminal( doc, "P1", app.Vector(10, 20, 30), terminal_type="primary", ) self.assertEqual("Terminal_P1", terminal.Name) self.assertEqual("P1", terminal.Label) self.assertEqual("Terminal", terminal.Role) self.assertTrue(terminal.CanWire) self.assertEqual("P1", terminal.QetTemplateSlotName) self.assertEqual("P1", terminal.QetTerminalLabel) self.assertEqual("primary", terminal.QetTerminalType) self.assertEqual(10.0, terminal.Placement.Base.x) self.assertEqual(20.0, terminal.Placement.Base.y) self.assertEqual(30.0, terminal.Placement.Base.z) self.assertTrue(doc.recomputed) def test_validate_template_terminals_reports_missing_slot_name(self): _install_fake_freecad() template_authoring = _reload_modules() doc = FakeDocument() terminal = doc.addObject("Part::LocalCoordinateSystem", "BrokenTerminal") terminal.addProperty("App::PropertyString", "Role", "QET Template", "role") terminal.Role = "Terminal" terminal.addProperty( "App::PropertyBool", "CanWire", "QET Template", "can wire", ) terminal.CanWire = True report = template_authoring.validate_template_terminals(doc) self.assertEqual(1, report["total_terminals"]) self.assertEqual(0, report["valid_terminals"]) self.assertEqual(1, len(report["warnings"])) self.assertIn("QetTemplateSlotName", report["warnings"][0]) def test_save_template_as_fcstd_adds_extension_and_saves_valid_template(self): _install_fake_freecad() template_authoring = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() template_authoring.create_template_terminal(doc, "P1", app.Vector(1, 2, 3)) report = template_authoring.save_template_as_fcstd(doc, "D:/tmp/current-transformer") self.assertEqual("D:/tmp/current-transformer.FCStd", doc.saved_path) self.assertEqual("D:/tmp/current-transformer.FCStd", report["path"]) self.assertEqual(1, report["valid_terminals"]) if __name__ == "__main__": unittest.main()