import importlib import json import sqlite3 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_modules(): fake_freecad = types.ModuleType("FreeCAD") fake_freecad.ActiveDocument = object() fake_freecad.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, PrintLog=lambda *args, **kwargs: None, ) sys.modules["FreeCAD"] = fake_freecad fake_gui = types.ModuleType("FreeCADGui") fake_gui.getMainWindow = lambda: None sys.modules["FreeCADGui"] = fake_gui fake_device_import = types.ModuleType("DeviceImport") fake_device_import.DeviceImportError = RuntimeError fake_device_import.import_devices_from_payload = lambda *args, **kwargs: {} sys.modules["DeviceImport"] = fake_device_import fake_device_preview = types.ModuleType("DevicePreview") fake_device_preview.find_parent_qet_device_object = lambda obj: None fake_device_preview.is_preview_document_name = lambda name: False fake_device_preview.open_preview_for_device_object = lambda obj: None sys.modules["DevicePreview"] = fake_device_preview fake_terminal_import = types.ModuleType("TerminalImport") fake_terminal_import.TerminalImportError = RuntimeError sys.modules["TerminalImport"] = fake_terminal_import calls = [] fake_wiring = types.ModuleType("WiringObjects") fake_wiring.initialize_wiring_scene = lambda doc, project_uuid="": calls.append((doc, project_uuid)) or "root" sys.modules["WiringObjects"] = fake_wiring wire_calls = [] fake_wiring_import = types.ModuleType("WiringImport") fake_wiring_import.WiringImportError = RuntimeError fake_wiring_import.import_wire_tasks_from_payload = ( lambda payload, doc=None: wire_calls.append((payload, doc)) or {"imported_tasks": 1, "updated_tasks": 0, "warnings": []} ) sys.modules["WiringImport"] = fake_wiring_import class _QObject: def __init__(self, *args, **kwargs): pass fake_qt_core = types.SimpleNamespace( QObject=_QObject, QEvent=types.SimpleNamespace(MouseButtonDblClick=1), QTimer=types.SimpleNamespace(singleShot=lambda *args, **kwargs: None), ) fake_qt_widgets = types.SimpleNamespace( QWidget=object, QMessageBox=types.SimpleNamespace( information=lambda *args, **kwargs: None, critical=lambda *args, **kwargs: None, ), ) fake_pyside = types.ModuleType("PySide6") fake_pyside.QtCore = fake_qt_core fake_pyside.QtWidgets = fake_qt_widgets sys.modules["PySide6"] = fake_pyside return fake_freecad, calls, wire_calls class ExchangeBootstrapWiringTest(unittest.TestCase): def test_initialize_wiring_scene_uses_active_document_and_project_uuid(self): app, calls, _wire_calls = _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") result = bootstrap._initialize_wiring_scene({"project_uuid": "project-1"}) self.assertEqual("root", result) self.assertEqual([(app.ActiveDocument, "project-1")], calls) def test_import_wiring_tasks_uses_active_document_and_payload(self): app, _calls, wire_calls = _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = {"project_uuid": "project-1", "wires": [{"wire_id": "wire-1"}]} result = bootstrap._import_wiring_tasks(payload) self.assertEqual({"imported_tasks": 1, "updated_tasks": 0, "warnings": []}, result) self.assertEqual([(payload, app.ActiveDocument)], wire_calls) def test_load_exchange_payload_keeps_wire_entries(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [ { "wire_id": "wire-1", "wire_mark": "W001", "start_terminal_uuid": "terminal-a", "end_terminal_uuid": "terminal-b", } ], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual(1, len(normalized["wires"])) self.assertEqual("wire-1", normalized["wires"][0]["wire_id"]) self.assertEqual("W001", normalized["wires"][0]["wire_mark"]) def test_load_exchange_payload_flattens_nested_device_terminals(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [ { "device_instance_id": "device-inst-1", "display_tag": "QF1", "terminals": [ { "terminal_uuid": "terminal-a", "element_uuid": "element-a", "terminal_display": "P1", } ], } ], "device_models": [ { "device_instance_id": "device-inst-1", "resolved_model_path": r"D:\models\qf1.step", } ], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual("device-inst-1", normalized["devices"][0]["device_instance_id"]) self.assertEqual("element-a", normalized["devices"][0]["element_uuid"]) self.assertEqual(["element-a"], normalized["devices"][0]["element_uuids"]) self.assertNotIn("terminals", normalized) self.assertEqual(1, len(normalized["devices"][0]["terminals"])) self.assertEqual("terminal-a", normalized["devices"][0]["terminals"][0]["terminal_uuid"]) self.assertEqual( "device-inst-1", normalized["devices"][0]["terminals"][0]["device_instance_id"], ) self.assertEqual( "device-inst-1", normalized["device_models"][0]["device_instance_id"], ) def test_load_exchange_payload_preserves_wire_label_and_style_id(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "wire_style_id": 1, "start_terminal_uuid": "terminal-a", "end_terminal_uuid": "terminal-b", } ], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual("N4111", normalized["wires"][0]["wire_label"]) self.assertEqual("1", normalized["wires"][0]["wire_style_id"]) def test_load_exchange_payload_rejects_legacy_root_terminals(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "terminals": [], "device_models": [], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) def test_load_exchange_payload_rejects_non_v2_schema(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "1.2", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) def test_load_exchange_payload_rejects_legacy_device_instance_id_field(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [ { "instance_id": "legacy-device-instance", "display_tag": "QF1", "terminals": [], } ], "device_models": [], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) def test_load_exchange_payload_rejects_legacy_device_level_element_uuid(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [ { "device_instance_id": "device-inst-1", "element_uuid": "legacy-device-element", "display_tag": "QF1", "terminals": [], } ], "device_models": [], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) def test_load_exchange_payload_rejects_legacy_device_model_element_uuid(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [ { "element_uuid": "legacy-device", "resolved_model_path": r"D:\models\legacy.step", } ], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) def test_load_exchange_payload_ignores_legacy_wire_endpoint_instance_ids(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-a", "end_terminal_uuid": "terminal-b", "start_instance_id": "legacy-start", "end_instance_id": "legacy-end", } ], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" path.write_text(json.dumps(payload), encoding="utf-8") normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual("wire-1", normalized["wires"][0]["wire_id"]) self.assertNotIn("start_instance_id", normalized["wires"][0]) self.assertNotIn("end_instance_id", normalized["wires"][0]) def test_load_exchange_payload_detects_wire_properties_database_next_to_json(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [], } with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" db_path = Path(temp_dir) / "project-local.sqlite" path.write_text(json.dumps(payload), encoding="utf-8") connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, line_color TEXT ) """ ) connection.commit() finally: connection.close() normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual(str(db_path), normalized["wire_style_database_path"]) def test_load_exchange_payload_detects_project_datafiles_wire_properties_database(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [{"wire_id": "wire-1", "wire_style_id": "1"}], } with tempfile.TemporaryDirectory() as temp_dir: project_dir = Path(temp_dir) / "project-a" exchange_dir = project_dir / ".qet_freecad" data_dir = project_dir / "datafiles" exchange_dir.mkdir(parents=True) data_dir.mkdir(parents=True) path = exchange_dir / "2d_to_3d.json" db_path = data_dir / "project-local.db" path.write_text(json.dumps(payload), encoding="utf-8") connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, line_color TEXT ) """ ) connection.commit() finally: connection.close() normalized = bootstrap.load_exchange_payload(str(path)) self.assertEqual(str(db_path), normalized["wire_style_database_path"]) def test_exchange_summary_includes_wire_style_database_path(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { "project_uuid": "project-1", "devices": [], "device_models": [], "wires": [], "wire_style_database_path": "D:/project/project-local.sqlite", } summary = bootstrap._build_summary(payload, "D:/project/2d_to_3d.json") self.assertEqual( "D:/project/project-local.sqlite", summary["wire_style_database_path"], ) def test_summary_message_includes_updated_device_label_change_details(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") message = bootstrap._summary_message( { "project_uuid": "project-1", "json_path": r"D:\project\example\.qet_freecad\2d_to_3d.json", "device_count": 1, "terminal_count": 2, "wire_count": 0, "device_model_count": 1, "device_models_with_parts": 1, "missing_device_instances": 0, "missing_terminal_instances": 0, "scene_path": r"D:\project\example\.qet_freecad\QETScene.FCStd", "is_first_open": False, }, import_report={ "document_name": "QETScene", "cabinet_imported": 0, "cabinet_added": 0, "cabinet_reimported": 0, "cabinet_reused": 1, "imported_devices": 1, "updated_devices": 1, "reused_devices": 0, "added_device_details": [ { "label": "QF2", "display_tag": "QF2", "instance_id": "device-inst-2", } ], "updated_device_details": [ { "label": "J3", "display_tag": "J3", "previous_display_tag": "J1", "instance_id": "device-inst-1", "change_types": ["标注"], "added_terminal_uuids": [], "removed_terminal_uuids": [], } ], "imported_without_instance_id": 0, "skipped_missing_model": 0, "skipped_missing_file": 0, "skipped_unsupported_format": 0, "skipped_import_error": 0, "warnings": [], }, stale_report={ "stale_cabinets": 0, "stale_devices": 1, "stale_device_details": [ { "label": "J9", "display_tag": "J9", "instance_id": "device-inst-9", } ], }, ) self.assertIn("新增设备明细:", message) self.assertIn("新增设备:1 (QF2)", message) self.assertIn("QF2 [device-inst-2]", message) self.assertIn("更新设备:1", message) self.assertIn("修改设备:", message) self.assertIn("失效设备:1 (J9)", message) self.assertIn("失效设备明细:", message) self.assertIn("J9 [device-inst-9]", message) self.assertIn("Updated device details:", message) self.assertIn("J3 [device-inst-1] -> 标注 (标注 J1 -> J3)", message) if __name__ == "__main__": unittest.main()