|
|
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": "1.2",
|
|
|
"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]["instance_id"])
|
|
|
self.assertEqual("element-a", normalized["devices"][0]["element_uuid"])
|
|
|
self.assertEqual(["element-a"], normalized["devices"][0]["element_uuids"])
|
|
|
self.assertEqual(1, len(normalized["terminals"]))
|
|
|
self.assertEqual("terminal-a", normalized["terminals"][0]["terminal_uuid"])
|
|
|
self.assertEqual("device-inst-1", normalized["terminals"][0]["instance_id"])
|
|
|
self.assertEqual("device-inst-1", normalized["device_models"][0]["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": "1.2",
|
|
|
"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_detects_wire_properties_database_next_to_json(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"
|
|
|
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": "1.2",
|
|
|
"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": [],
|
|
|
"terminals": [],
|
|
|
"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": 0,
|
|
|
"updated_devices": 1,
|
|
|
"reused_devices": 0,
|
|
|
"added_device_details": [],
|
|
|
"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": [],
|
|
|
},
|
|
|
)
|
|
|
|
|
|
self.assertIn("更新设备:1", message)
|
|
|
self.assertIn("修改设备:", message)
|
|
|
self.assertIn("Updated device details:", message)
|
|
|
self.assertIn("J3 [device-inst-1] -> 标注 (标注 J1 -> J3)", message)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
unittest.main()
|