You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LightWork3D/tests/python/freecad_exchange_bootstrap_...

376 lines
14 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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()