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_...

524 lines
20 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": "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()