"""Real Qt insertion, nested material-aware BOM, CSV and FCStd workflows.""" import csv import hashlib import io import json import math from pathlib import Path import re import sys import time import traceback import uuid import zipfile import FreeCAD as App import FreeCADGui as Gui import Part from PySide import QtCore, QtGui try: from PySide import QtWidgets except ImportError: QtWidgets = QtGui try: from PySide6 import QtTest except ImportError: from PySide2 import QtTest SOURCE = Path(__file__).resolve().parents[1] / "src" / "QInventor" if str(SOURCE) not in sys.path: sys.path.insert(0, str(SOURCE)) def _near(actual, expected, rel_tol=1e-9): assert math.isclose(float(actual), float(expected), rel_tol=rel_tol, abs_tol=1e-11), (actual, expected) def _settle(ms=80): QtWidgets.QApplication.processEvents() QtTest.QTest.qWait(ms) def _click(widget): QtTest.QTest.mouseClick(widget, QtCore.Qt.LeftButton) _settle() def _combo(combo, data): index = combo.findData(data) assert index >= 0, (combo.objectName(), data) combo.setFocus() QtTest.QTest.keyClick(combo, QtCore.Qt.Key_Home) for _ in range(index): QtTest.QTest.keyClick(combo, QtCore.Qt.Key_Down) _settle() assert combo.currentData() == data def _items(tree): def descend(item): yield item for index in range(item.childCount()): yield from descend(item.child(index)) for index in range(tree.topLevelItemCount()): yield from descend(tree.topLevelItem(index)) def _display_rows(dialog): return [dict(item.data(0, QtCore.Qt.UserRole)) for item in _items(dialog.tree)] def _document_state(doc): # Compare all native payloads, excluding ZIP creation timestamps only. contents = [] for obj in doc.Objects: with zipfile.ZipFile(io.BytesIO(bytes(obj.dumpContent()))) as archive: members = tuple((member.filename, archive.read(member)) for member in archive.infolist()) contents.append((obj.Name, members, tuple(obj.State))) return tuple(contents), doc.UndoCount, doc.RedoCount, Gui.getDocument(doc.Name).Modified def run(output_dir): import qi_bom import qi_commands as commands import qi_model as model output = Path(output_dir).absolute() output.mkdir(parents=True, exist_ok=True) original_docs = set(App.listDocuments()) active_name = App.ActiveDocument.Name if App.ActiveDocument else None dialogs = [] fixture = None report_path = output / "bom-gui-report.json" report = {"status": "running", "passed": 0, "failed": 0, "checks": [], "input_delivery": "Qt native Assembly insertion list, BOM controls, native file and property dialogs", "fixture_boundary": "native Assembly::AssemblyLink/App::Link instances of saved FCStd sources", "os_pointer_access": False} main = Gui.getMainWindow() main.resize(1440, 940) main.hide() main.showNormal() main.raise_() main.activateWindow() assert QtTest.QTest.qWaitForWindowExposed(main.windowHandle(), 5000) commands.register_commands() def write(): report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str), encoding="utf-8") def check(name, action): report["running_case"] = name write() try: report["checks"].append({"name": name, "passed": True, "evidence": action()}) report["passed"] += 1 except Exception: report["checks"].append({"name": name, "passed": False, "error": traceback.format_exc()}) report["failed"] += 1 write() def capture(dialog, name): _settle(120) path = output / (name + ".png") assert dialog.isVisible() and dialog.grab().save(str(path)) return str(path) def task_buttons(): boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible() and box.button(QtWidgets.QDialogButtonBox.Ok)] assert boxes, "Native task dialog has no actual OK/Cancel buttons" return boxes[-1] def object_reference(obj): if obj is None: return None if not hasattr(obj, "Document") and hasattr(obj, "Object"): obj = obj.Object if not hasattr(obj, "Document"): return {"native_class": type(obj).__name__, "name": str(getattr(obj, "Name", "")), "representation": repr(obj)} return {"document": obj.Document.Name, "name": obj.Name, "type": obj.TypeId, "label": obj.Label} def native_context(): import UtilsAssembly gui_doc = Gui.ActiveDocument view = gui_doc.ActiveView if gui_doc else None return {"app_document": App.ActiveDocument.Name if App.ActiveDocument else None, "gui_document": gui_doc.Document.Name if gui_doc else None, "in_edit": {name: object_reference(Gui.getDocument(name).getInEdit()) for name in App.listDocuments()}, "selection": [object_reference(obj) for obj in Gui.Selection.getSelection()], "active_assembly": object_reference(UtilsAssembly.activeAssembly()), "view_assembly": object_reference(view.getActiveObject("assembly")) if view else None, "view_part": object_reference(view.getActiveObject("part")) if view else None, "assembly_groups": {name + "#" + obj.Name: [object_reference(member) for member in obj.Group] for name, document in App.listDocuments().items() for obj in document.Objects if obj.TypeId == "Assembly::AssemblyObject"}} def make_fixture(): nonlocal fixture if fixture is not None: return fixture attempt = {"number": len(report.setdefault("fixture_attempts", [])) + 1, "initial_docs": list(App.listDocuments()), "clicks": []} fixture_directory = output / ("bom-gui-fixture-" + uuid.uuid4().hex) fixture_directory.mkdir() attempt["fixture_directory"] = str(fixture_directory) report["fixture_attempts"].append(attempt) write() parts = model.new_part("Детали спецификации") housing = model.create_box(10, 10, 10, parts) housing.Label = "Корпус" housing.QIPartNumber = "GUI-HOUSING" model.set_material(housing, "Алюминий") bolt = model.create_cylinder(1, 10, parts) bolt.Label = "Болт" bolt.QIPartNumber = "GUI-BOLT" model.set_material(bolt, "Сталь") parts_path = Path(model.save_project(parts, fixture_directory / "bom-gui-parts.FCStd")) subdoc = model.new_assembly("Подсборка спецификации") sub = subdoc.getObject("Assembly") sub.Label = "Подсборка" sub_path = Path(model.save_project(subdoc, fixture_directory / "bom-gui-subassembly.FCStd")) base = model.add_component(sub, housing) model.ground_component(sub, base) model.add_component(sub, bolt, App.Placement(App.Vector(15, 0, 0), App.Rotation())) model.add_component(sub, bolt, App.Placement(App.Vector(20, 0, 0), App.Rotation())) subdoc.recompute() assert sub.solve(False) == 0 model.save_project(subdoc, sub_path) doc = model.new_assembly("Сборка спецификации") assembly = doc.getObject("Assembly") root_path = Path(model.save_project(doc, fixture_directory / "bom-gui-assembly.FCStd")) construction = doc.addObject("App::Part", "Construction") construction.Label = "Вспомогательные построения" construction_sketch = construction.newObject("Sketcher::SketchObject", "ConstructionSketch") construction_sketch.addGeometry(Part.LineSegment(App.Vector(0, 0, 0), App.Vector(5, 0, 0)), True) # A valid but unused prototype must never become an extra physical root. definitions = doc.addObject("App::Part", "Definitions") definitions.Label = "Определения компонентов" prototype = doc.addObject("Assembly::AssemblyObject", "PrototypeAssembly") prototype.Type = "Assembly" prototype.Label = "Определение подсборки" prototype.newObject("Assembly::JointGroup", "Joints") definitions.addObject(prototype) model.add_component(prototype, housing) model.add_component(prototype, bolt) definitions.Visibility = False doc.recompute() gui_doc = Gui.getDocument(doc.Name) attempt["before_set_edit"] = native_context() attempt["expected_assembly"] = object_reference(assembly) write() assert gui_doc.setEdit(assembly.Name) attempt["after_set_edit"] = native_context() write() import Preferences preferences = Preferences.preferences() old_ground = preferences.GetInt("GroundFirstPart", 0) preferences.SetInt("GroundFirstPart", 1) try: Gui.runCommand("Assembly_InsertLink", 0) _settle(200) attempt["after_native_command"] = native_context() write() trees = [tree for tree in main.findChildren(QtWidgets.QTreeWidget, "partList") if tree.isVisible()] assert len(trees) == 1, "Native Assembly insertion task was not shown" tree = trees[0] tree.expandAll() for source in (sub, sub, bolt): candidates = [] for item in _items(tree): target = item.data(0, QtCore.Qt.UserRole) if target is not None and getattr(target, "Name", None) == source.Name \ and getattr(getattr(target, "Document", None), "Name", None) == source.Document.Name: candidates.append(item) assert len(candidates) == 1, "Native insertion source missing: " + source.Label item = candidates[0] tree.scrollToItem(item) _settle() rectangle = tree.visualItemRect(item) assert rectangle.isValid() and not rectangle.isEmpty() clicked_item = tree.itemAt(rectangle.center()) event = {"requested_source": object_reference(source), "item_source": object_reference(item.data(0, QtCore.Qt.UserRole)), "item_at_click": object_reference(clicked_item.data(0, QtCore.Qt.UserRole)) if clicked_item else None, "rectangle": [rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height()], "viewport": [tree.viewport().width(), tree.viewport().height()], "before": native_context()} attempt["clicks"].append(event) write() QtTest.QTest.mouseClick(tree.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, rectangle.center()) _settle(150) event["after"] = native_context() write() attempt["before_accept"] = native_context() write() _click(task_buttons().button(QtWidgets.QDialogButtonBox.Ok)) attempt["after_accept"] = native_context() write() assert not Gui.Control.activeDialog() finally: preferences.SetInt("GroundFirstPart", old_ground) if gui_doc.getInEdit(): gui_doc.resetEdit() doc.recompute() links = [member for member in assembly.Group if member.isDerivedFrom("App::Link") or member.isDerivedFrom("Assembly::AssemblyLink")] attempt["actual_links"] = [object_reference(link) for link in links] attempt["before_count_assert"] = native_context() write() assert len(links) == 3 assert sum(link.isDerivedFrom("Assembly::AssemblyLink") for link in links) == 2 assert assembly.solve(False) == 0 model.save_project(doc, root_path) fixture = {"doc": doc, "assembly": assembly, "parts": parts, "housing": housing, "bolt": bolt, "subdoc": subdoc, "sub": sub, "parts_path": parts_path, "sub_path": sub_path, "root_path": root_path, "prototype": prototype, "links": links, "construction": construction} return fixture def open_bom(doc): App.setActiveDocument(doc.Name) Gui.Selection.clearSelection() assert not Gui.Selection.getSelection() Gui.runCommand("QI_BOM", 0) _settle(120) candidates = [dialog for dialog in main.findChildren(QtWidgets.QDialog, "QI_BOMTable") if dialog.isVisible() and getattr(dialog, "document", None) == doc] assert len(candidates) == 1, "QI_BOM did not open a document table without selection" dialog = candidates[0] dialogs.append(dialog) assert not dialog.isModal() and dialog.export_button.isEnabled(), dialog.status.text() return dialog def flat_quantities(dialog, expected_bolts=5, expected_housings=2): rows = _display_rows(dialog) assert len(rows) == 2 and all(not row["is_assembly"] for row in rows) by_number = {row["part_number"]: row for row in rows} assert set(by_number) == {"GUI-BOLT", "GUI-HOUSING"} _near(by_number["GUI-BOLT"]["quantity"], expected_bolts) _near(by_number["GUI-HOUSING"]["quantity"], expected_housings) assert by_number["GUI-BOLT"]["material"] == "Сталь" assert by_number["GUI-HOUSING"]["material"] == "Алюминий" assert len({row["source_id"] for row in rows}) == 2 _near(sum(row["mass_kg"] for row in rows), dialog.summary_data["mass_kg"]) return by_number def export_with_native_dialog(dialog, path): original_attribute = QtWidgets.QApplication.testAttribute(QtCore.Qt.AA_DontUseNativeDialogs) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_DontUseNativeDialogs, True) evidence = {"dialog_seen": False, "save_clicked": False, "errors": []} timer = QtCore.QTimer() started = time.monotonic() def choose(): try: for widget in QtWidgets.QApplication.topLevelWidgets(): if isinstance(widget, QtWidgets.QMessageBox) and widget.isVisible(): evidence["errors"].append(widget.windowTitle() + ": " + widget.text()) timer.stop() widget.reject() return if not isinstance(widget, QtWidgets.QFileDialog) or not widget.isVisible(): continue evidence["dialog_seen"] = True widget.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, True) widget.setDirectory(str(path.parent)) editor = widget.findChild(QtWidgets.QLineEdit, "fileNameEdit") assert editor is not None editor.setFocus() editor.selectAll() QtTest.QTest.keyClick(editor, QtCore.Qt.Key_Backspace) QtTest.QTest.keyClicks(editor, path.name) boxes = widget.findChildren(QtWidgets.QDialogButtonBox) button = next((box.button(QtWidgets.QDialogButtonBox.Save) for box in boxes if box.button(QtWidgets.QDialogButtonBox.Save)), None) assert button is not None timer.stop() evidence["save_clicked"] = True QtTest.QTest.mouseClick(button, QtCore.Qt.LeftButton) return if time.monotonic() - started > 10: timer.stop() evidence["errors"].append("Save dialog did not appear") modal = QtWidgets.QApplication.activeModalWidget() if isinstance(modal, QtWidgets.QDialog): evidence["errors"].append("Unexpected modal: " + modal.windowTitle()) modal.reject() except Exception: evidence["errors"].append(traceback.format_exc()) timer.stop() modal = QtWidgets.QApplication.activeModalWidget() if isinstance(modal, QtWidgets.QDialog): modal.reject() timer.timeout.connect(choose) timer.start(30) try: _click(dialog.export_button) finally: timer.stop() timer.deleteLater() QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_DontUseNativeDialogs, original_attribute) assert not evidence["errors"], evidence assert evidence["dialog_seen"] and evidence["save_clicked"], evidence assert "сохранена" in dialog.status.text() and path.is_file(), dialog.status.text() with path.open(encoding="utf-8-sig", newline="") as stream: rows = list(csv.DictReader(stream, delimiter=";")) assert len(rows) == len(dialog.rows) return rows def native_insert_and_views(): data = make_fixture() doc, assembly = data["doc"], data["assembly"] material_rejections = [] for target in (assembly, next(link for link in data["links"] if link.isDerivedFrom("Assembly::AssemblyLink"))): App.setActiveDocument(doc.Name) Gui.Selection.clearSelection() Gui.Selection.addSelection(target) _settle() before = {name: _document_state(document) for name, document in App.listDocuments().items()} input_dialogs = [] timer = QtCore.QTimer() def reject_unexpected_material_dialog(): for widget in QtWidgets.QApplication.topLevelWidgets(): if isinstance(widget, QtWidgets.QInputDialog) and widget.isVisible(): input_dialogs.append(widget.windowTitle()) widget.reject() timer.timeout.connect(reject_unexpected_material_dialog) timer.start(30) rejection = None try: try: commands._material() except ValueError as error: rejection = str(error) finally: timer.stop() timer.deleteLater() assert rejection and "Выберите конечную деталь" in rejection, rejection assert not input_dialogs, input_dialogs after = {name: _document_state(document) for name, document in App.listDocuments().items()} assert after == before, "Rejected assembly material changed native metadata or history" material_rejections.append({"type": target.TypeId, "message": rejection}) dialog = open_bom(doc) assert dialog.mode_selector.currentData() == "flat" assert dialog.root_selector.currentData() == "" assert dialog.root_selector.findData(data["prototype"].Name) < 0 assert qi_bom.bom_rows(doc, root=data["construction"]) == [] assert qi_bom.bom_summary(doc, root=data["construction"])["components"] == 0 by_number = flat_quantities(dialog) expected_mass = 2 * data["housing"].Shape.Volume * model.MATERIALS["Алюминий"] * 1e-9 \ + 5 * data["bolt"].Shape.Volume * model.MATERIALS["Сталь"] * 1e-9 _near(dialog.summary_data["mass_kg"], expected_mass) assert dialog.summary_data["components"] == 7 assert set(dialog.summary_data["materials"]) == {"Сталь", "Алюминий"} assert "Масса:" in dialog.summary.text() and "," in dialog.summary.text() hidden_link = data["links"][-1] hidden_link.Visibility = False _click(dialog.refresh_button) flat_quantities(dialog) _near(dialog.summary_data["mass_kg"], expected_mass) hidden_link.Visibility = True flat_screenshot = capture(dialog, "bom-gui-flat") _combo(dialog.mode_selector, "structured") rows = _display_rows(dialog) assert any(row["is_assembly"] for row in rows) assert any(row["level"] > 0 and row["parent_position"] for row in rows) for item in _items(dialog.tree): row = item.data(0, QtCore.Qt.UserRole) assert item.text(0) == row["position"] assert not (item.flags() & QtCore.Qt.ItemIsEditable) if row["parent_position"]: assert item.parent() is not None and item.parent().text(0) == row["parent_position"] _near(dialog.summary_data["mass_kg"], expected_mass) leaf_bolts = [row for row in rows if not row["is_assembly"] and row["part_number"] == "GUI-BOLT"] _near(sum(row["quantity_total"] for row in leaf_bolts), 5) assert any(row["quantity"] == 2 and row["quantity_total"] == 4 for row in leaf_bolts) structured_screenshot = capture(dialog, "bom-gui-structured") _combo(dialog.root_selector, assembly.Name) _near(dialog.summary_data["mass_kg"], expected_mass) nested = next(link for link in data["links"] if link.isDerivedFrom("Assembly::AssemblyLink")) assert dialog.root_selector.findData(nested.Name) >= 0 _combo(dialog.root_selector, nested.Name) assert dialog.summary_data["components"] == 3 _combo(dialog.root_selector, assembly.Name) dialog.close() return {"native_inserted_subassemblies": 2, "native_inserted_direct_bolts": 1, "flat_quantities": {number: row["quantity"] for number, row in by_number.items()}, "mixed_mass_kg": expected_mass, "prototype_excluded": True, "construction_sketch_container_has_no_BOM_rows": True, "assembly_material_rejected_before_chooser_without_mutation": material_rejections, "physical_counts_ignore_visibility": True, "structured_local_and_total_quantities": True, "selected_live_nested_instance": True, "screenshots": [flat_screenshot, structured_screenshot]} def properties_and_refresh_export_reopen(): data = make_fixture() doc, assembly = data["doc"], data["assembly"] App.setActiveDocument(doc.Name) Gui.Selection.clearSelection() properties = {} properties_errors = [] timer = QtCore.QTimer() started = time.monotonic() def close_properties(): try: for widget in QtWidgets.QApplication.topLevelWidgets(): if isinstance(widget, QtWidgets.QMessageBox) and widget.isVisible(): properties_errors.append(widget.windowTitle() + ": " + widget.text()) timer.stop() widget.reject() return if isinstance(widget, QtWidgets.QDialog) and widget.isVisible() \ and widget.windowTitle().startswith("Физические свойства"): table = widget.findChild(QtWidgets.QTableWidget) if table is None: continue properties.update({table.item(row, 0).text(): table.item(row, 1).text() for row in range(table.rowCount())}) box = widget.findChild(QtWidgets.QDialogButtonBox) assert box is not None and box.button(QtWidgets.QDialogButtonBox.Close) is not None timer.stop() QtTest.QTest.mouseClick(box.button(QtWidgets.QDialogButtonBox.Close), QtCore.Qt.LeftButton) return if time.monotonic() - started > 10: properties_errors.append("Physical properties dialog did not appear") timer.stop() modal = QtWidgets.QApplication.activeModalWidget() if isinstance(modal, QtWidgets.QDialog): properties_errors.append("Unexpected modal: " + modal.windowTitle()) modal.reject() except Exception: properties_errors.append(traceback.format_exc()) timer.stop() modal = QtWidgets.QApplication.activeModalWidget() if isinstance(modal, QtWidgets.QDialog): modal.reject() timer.timeout.connect(close_properties) timer.start(30) try: Gui.runCommand("QI_Properties", 0) finally: timer.stop() timer.deleteLater() assert not properties_errors, properties_errors assert properties and "Материалы деталей" in properties and "Средняя плотность, кг/м³" in properties assert "Центр масс, мм" in properties and "Экземпляров деталей" in properties assert not any("_" in label for label in properties), properties assert properties["Экземпляров деталей"] == "7" expected = qi_bom.bom_summary(doc, root=assembly) _near(float(properties["Масса, кг"].replace(",", ".")), expected["mass_kg"], rel_tol=1e-5) dialog = open_bom(doc) _combo(dialog.root_selector, assembly.Name) old_mass = dialog.summary_data["mass_kg"] model.set_dimensions(data["housing"], {"QIHeight": 15}) data["parts"].recompute() _click(dialog.refresh_button) assert "обновлена" in dialog.status.text(), dialog.status.text() assert dialog.summary_data["mass_kg"] > old_mass flat_quantities(dialog) # Export performs its own refresh after an additional native source edit. model.set_dimensions(data["housing"], {"QIHeight": 20}) flat_path = output / "bom-gui-flat.csv" flat_csv = export_with_native_dialog(dialog, flat_path) csv_by_number = {row["Обозначение"]: row for row in flat_csv} assert csv_by_number["GUI-BOLT"]["Количество"] == "5" assert csv_by_number["GUI-HOUSING"]["Количество"] == "2" expected_mass = qi_bom.bom_summary(doc, root=assembly)["mass_kg"] _near(dialog.summary_data["mass_kg"], expected_mass) _near(sum(float(row["Масса, кг"].replace(",", ".")) for row in flat_csv), expected_mass) _combo(dialog.mode_selector, "structured") structured_path = output / "bom-gui-structured.csv" structured_csv = export_with_native_dialog(dialog, structured_path) assert any(row["Родитель"] and int(row["Уровень"]) > 0 for row in structured_csv) assert any(row["Обозначение"] == "GUI-BOLT" and row["Количество"] == "2" and row["Всего"] == "4" for row in structured_csv) screenshot = capture(dialog, "bom-gui-updated-and-exported") dialog.close() model.save_project(data["parts"], data["parts_path"]) model.save_project(data["subdoc"], data["sub_path"]) model.save_project(doc, data["root_path"]) for name in (doc.Name, data["subdoc"].Name, data["parts"].Name): gui_doc = Gui.getDocument(name) if gui_doc.getInEdit(): gui_doc.resetEdit() App.closeDocument(name) restored = App.openDocument(str(data["root_path"])) restored_dialog = open_bom(restored) flat_quantities(restored_dialog) _near(restored_dialog.summary_data["mass_kg"], expected_mass) assert all(source_id.split("#", 1)[0] in App.listDocuments() for source_id in (row["source_id"] for row in restored_dialog.rows)) restored_screenshot = capture(restored_dialog, "bom-gui-reopened") restored_dialog.close() data["restored"] = restored return {"mass_properties_russian_labels": properties, "source_edit_refresh": True, "export_refreshes_current_model": True, "native_save_dialog_keyboard_and_button": True, "flat_csv": str(flat_path), "structured_csv": str(structured_path), "FCStd_external_sources_reopened": True, "mass_kg_after_reopen": expected_mass, "screenshots": [screenshot, restored_screenshot]} def failed_source_closed_document_and_native_task(): data = make_fixture() doc = data.get("restored") or data["doc"] dialog = open_bom(doc) rows = flat_quantities(dialog) housing_id = rows["GUI-HOUSING"]["source_id"] source_doc_name, source_name = housing_id.split("#", 1) source_doc = App.getDocument(source_doc_name) housing = source_doc.getObject(source_name) pad = housing.Tip profile = pad.Profile profile_object = profile[0] if isinstance(profile, (tuple, list)) else profile csv_path = output / "bom-gui-flat.csv" old_hash = hashlib.sha256(csv_path.read_bytes()).hexdigest() source_doc.openTransaction("Ошибка исходной операции перед экспортом") pad.Profile = None source_doc.recompute() source_doc.commitTransaction() assert "Invalid" in pad.State and not pad.Shape.isNull() unexpected_file_dialog = [] timer = QtCore.QTimer() def reject_unexpected_save(): for widget in QtWidgets.QApplication.topLevelWidgets(): if isinstance(widget, QtWidgets.QFileDialog) and widget.isVisible(): unexpected_file_dialog.append(widget.windowTitle()) widget.reject() timer.timeout.connect(reject_unexpected_save) timer.start(30) try: _click(dialog.export_button) finally: timer.stop() timer.deleteLater() assert not unexpected_file_dialog and not dialog.export_button.isEnabled() assert "не обновлена" in dialog.status.text() and re.search(r"[А-Яа-я]", dialog.status.text()) assert hashlib.sha256(csv_path.read_bytes()).hexdigest() == old_hash source_doc.openTransaction("Восстановить исходную операцию") pad.Profile = profile source_doc.recompute() source_doc.commitTransaction() _click(dialog.refresh_button) assert dialog.export_button.isEnabled(), dialog.status.text() App.setActiveDocument(source_doc.Name) source_gui = Gui.getDocument(source_doc.Name) # FCStd external dependencies load without a viewport. Open the native # view before entering the real sketch task, as an engineer does. if not source_gui.mdiViewsOfType("Gui::View3DInventor"): assert "Std_ViewCreate" in Gui.listCommands() Gui.runCommand("Std_ViewCreate", 0) _settle() assert source_gui.mdiViewsOfType("Gui::View3DInventor") assert source_gui.setEdit(profile_object.Name) _settle() _click(dialog.refresh_button) assert not dialog.export_button.isEnabled() and "Завершите" in dialog.status.text() source_gui.resetEdit() App.setActiveDocument(doc.Name) _click(dialog.refresh_button) assert dialog.export_button.isEnabled(), dialog.status.text() App.closeDocument(doc.Name) _click(dialog.refresh_button) assert dialog.isVisible() and "закрыт" in dialog.status.text() and not dialog.export_button.isEnabled() dialog.close() return {"invalid_cached_source_blocks_export": True, "previous_csv_unchanged": old_hash, "native_task_blocks_refresh": True, "closed_document_refuses_refresh_and_export": True} try: for name, action in (("Native insertion, flat and structured quantities, materials and nested scope", native_insert_and_views), ("Russian mass properties, native source refresh, actual CSV dialogs and FCStd reopen", properties_and_refresh_export_reopen), ("Invalid cached source, native task and closed document protect BOM export", failed_source_closed_document_and_native_task)): check(name, action) finally: for dialog in dialogs: try: dialog.close() except RuntimeError: pass if Gui.Control.activeDialog(): boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible() and box.button(QtWidgets.QDialogButtonBox.Cancel)] if boxes: _click(boxes[-1].button(QtWidgets.QDialogButtonBox.Cancel)) else: Gui.Control.closeDialog() Gui.Selection.clearSelection() for name in set(App.listDocuments()) - original_docs: gui_doc = Gui.getDocument(name) if gui_doc.getInEdit(): gui_doc.resetEdit() _settle() for name in set(App.listDocuments()) - original_docs: App.closeDocument(name) if active_name and active_name in App.listDocuments(): App.setActiveDocument(active_name) report.pop("running_case", None) report["status"] = "passed" if report["failed"] == 0 else "failed" report["count"] = len(report["checks"]) write() return report