"""Native camera and Qt sketch-workspace regressions. The drawing case starts with an empty native sketch. Rectangle geometry, edge selection, both driving dimensions and dimension-list editing use actual Qt events. Camera fixtures use the model API; they do not substitute for drawing. """ import json import hashlib import math import os import traceback 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 PySide import QtTest except ImportError: try: from PySide6 import QtTest, QtOpenGLWidgets except ImportError: from PySide2 import QtTest def run(output): import qi_commands as commands import qi_model as model import qi_ui as ui os.makedirs(output, exist_ok=True) path = os.path.join(output, "sketch-workspace-suite.json") report = {"status": "running", "passed": 0, "failed": 0, "checks": [], "input_delivery": "Qt mouse/key events on native CAD widgets", "qt_cursor_positioning": True, "physical_desktop_input": False} created = [] initial_active = App.ActiveDocument.Name if App.ActiveDocument else None main = Gui.getMainWindow() previous_size = main.size() preferences = [] context = {} def settle(ms=120): QtWidgets.QApplication.processEvents() QtTest.QTest.qWait(ms) def wait_for(predicate, message, timeout=1800): elapsed = QtCore.QElapsedTimer() elapsed.start() while not predicate() and elapsed.elapsed() < timeout: settle(40) assert predicate(), {"message": message, "waited_ms": elapsed.elapsed(), "editing": getattr(commands._editing(), "Name", None), "dimension_shortcut_enabled": ribbon.shortcuts["QI_Dimension"].isEnabled(), "native_dimension_active": Gui.Command.get("Sketcher_Dimension").isActive()} return elapsed.elapsed() def write(): with open(path, "w", encoding="utf-8") as stream: json.dump(report, stream, ensure_ascii=False, indent=2, default=str) def capture(name): destination = os.path.join(output, name + ".png") assert main.grab().save(destination), destination return destination def check(name, function): try: evidence = function() report["checks"].append({"name": name, "passed": True, "evidence": evidence}) report["passed"] += 1 except Exception: failure = {"name": name, "passed": False, "error": traceback.format_exc()} if "hole_pick_diagnostics" in context: failure["hole_pick_diagnostics"] = context["hole_pick_diagnostics"] try: failure["screenshot"] = capture("sketch-workspace-failure-%d" % (report["failed"] + 1)) except Exception: failure["capture_error"] = traceback.format_exc() report["checks"].append(failure) report["failed"] += 1 write() def set_preference(group_path, key, value): group = App.ParamGet(group_path) present = any(entry[1] == key for entry in (group.GetContents() or [])) preferences.append((group, key, present, group.GetBool(key))) group.SetBool(key, value) def own_document(title): document = model.new_part(title) created.append(document.Name) return document def close_owned(document): if document.Name in App.listDocuments(): if App.ActiveDocument == document and commands._editing(): Gui.activeDocument().resetEdit() settle() App.closeDocument(document.Name) def renderer(): candidates = [widget for widget in main.findChildren(QtWidgets.QWidget) if widget.isVisible() and widget.metaObject().className() == "QOpenGLWidget" and widget.parent() and widget.parent().metaObject().className() == "Gui::View3DInventorViewer"] assert candidates, "Native OpenGL drafting viewport is missing" widget = max(candidates, key=lambda item: item.width() * item.height()) if "QtOpenGLWidgets" in globals(): import shiboken6 widget = shiboken6.wrapInstance(shiboken6.getCppPointer(widget)[0], QtOpenGLWidgets.QOpenGLWidget) return widget def camera_frame(view): node = view.getCameraNode() return {"position": list(node.position.getValue().getValue()), "orientation": list(node.orientation.getValue().getValue()), "height": float(node.height.getValue())} def assert_camera(actual, expected, message): assert abs(actual["height"] - expected["height"]) <= 1e-4, (message, actual, expected) assert max(abs(a - b) for a, b in zip(actual["position"], expected["position"])) <= 1e-4, (message, actual, expected) a, b = actual["orientation"], expected["orientation"] cosine = abs(sum(x * y for x, y in zip(a, b))) / math.sqrt(sum(x*x for x in a) * sum(y*y for y in b)) assert abs(1 - cosine) <= 1e-7, (message, actual, expected) def native_camera_roundtrip(): document = own_document("Камера при завершении и отмене") try: sketch = model.create_sketch_profile("rectangle", 40, 25, doc=document) view = Gui.activeDocument().activeView() view.setCameraType("Orthographic") result = [] for command in ("Sketcher_LeaveSketch", "QI_FinishSketch", "QI_CancelSketch"): view.setCameraOrientation(App.Rotation(App.Vector(1, 2, 3), 31).Q) node = view.getCameraNode() node.position.setValue(75.0, 60.0, 320.0) node.height.setValue(233.0) settle(400) original = camera_frame(view) original_count = sketch.GeometryCount Gui.activeDocument().setEdit(sketch.Name) settle(900) if command == "QI_CancelSketch": sketch.addGeometry(Part.LineSegment(App.Vector(70, 0, 0), App.Vector(70, 20, 0)), True) document.recompute() Gui.runCommand(command, 0) settle(400) first = camera_frame(view) settle(1100) second = camera_frame(view) assert commands._editing() is None assert_camera(first, original, command + " must preserve native RestoreCamera") assert_camera(second, first, command + " changed camera after its initial layout settled") assert sketch.GeometryCount == original_count result.append({"command": command, "original": original, "after_400ms": first, "after_1500ms": second}) result.append({"screenshot": capture("sketch-camera-restored")}) return result finally: close_owned(document) def empty_sketch(): document = own_document("Построение эскиза мышью") chooser = ui.choose_plane ui.choose_plane = lambda: "XY" try: Gui.runCommand("QI_NewSketch", 0) finally: ui.choose_plane = chooser settle(1000) sketch = commands._editing() assert sketch and sketch.Document == document and sketch.GeometryCount == 0 assert main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList") is not None context.update(document=document, sketch=sketch) view = Gui.activeDocument().activeView() first = camera_frame(view) assert 80 <= first["height"] <= 240, ("Initial sketch scale is unsuitable for drafting millimetre-sized parts", first) settle(1100) second = camera_frame(view) assert_camera(second, first, "Empty sketch camera changed while idle") return {"geometry_count": 0, "plane_selection": "XY test fixture chooser", "camera_after_layout": first, "camera_after_idle": second} def pinned_controls(): assert commands._editing() == context["sketch"] results = [] for width, height in ((1366, 768), (1440, 940)): main.resize(width, height) settle(500) assert main.width() == width and main.height() == height, ("Window cannot reach requested size", main.size()) states = [] for title in ("Эскиз", "Вид", "3D-модель", "Эскиз"): index = next(i for i in range(ribbon.tabs.count()) if ribbon.tabs.tabText(i) == title) QtTest.QTest.mouseClick(ribbon.tabs.tabBar(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, ribbon.tabs.tabBar().tabRect(index).center()) settle(100) widget = renderer() rectangle = widget.mapTo(main, QtCore.QPoint()) states.append([rectangle.x(), rectangle.y(), widget.width(), widget.height()]) for name in ("QI_FinishSketch", "QI_CancelSketch"): buttons = ribbon.buttons.get(name, []) assert buttons, "Missing sketch completion command: " + name assert any(button.isVisible() and button.isEnabled() and button.visibleRegion().contains(button.rect()) for button in buttons), (name, width, title) assert ribbon.height() == 174 assert all(state == states[0] for state in states), ("Changing ribbon tab moved the CAD viewport", states) results.append({"window_size": [width, height], "viewport_rectangles": states, "screenshot": capture("sketch-workspace-%dx%d" % (width, height))}) return results def move(widget, point): global_point = widget.mapToGlobal(point) QtGui.QCursor.setPos(global_point) event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, QtCore.QPointF(point), QtCore.QPointF(global_point), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier) QtWidgets.QApplication.sendEvent(widget, event) def click_canvas(point, wait=80): widget = renderer() move(widget, point) settle(wait) QtTest.QTest.mousePress(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, point) settle(60) QtTest.QTest.mouseRelease(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, point) settle(wait) def mouse_rectangle(): sketch = context["sketch"] assert commands._editing() == sketch and sketch.GeometryCount == 0 button = ribbon.buttons["QI_Rectangle"][0] assert button.isVisible() and button.isEnabled() QtTest.QTest.mouseClick(button, QtCore.Qt.LeftButton) settle(100) widget = renderer() click_canvas(QtCore.QPoint(round(widget.width() * .28), round(widget.height() * .64))) click_canvas(QtCore.QPoint(round(widget.width() * .54), round(widget.height() * .36))) QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape) settle(250) wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(), "Dimension shortcut did not become available after leaving the rectangle tool") assert sketch.GeometryCount == 4 assert all(geometry.__class__.__name__ == "LineSegment" for geometry in sketch.Geometry) assert Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).isClosed() assert commands._editing() == sketch return {"geometry_count": 4, "closed_native_wire": True, "screenshot": capture("sketch-mouse-rectangle")} def project(local_point): sketch = context["sketch"] widget = renderer() view = Gui.activeDocument().activeView() projected = view.getPointOnScreen(sketch.getGlobalPlacement().multVec(local_point)) size = view.getViewer().getSoRenderManager().getViewportRegion().getViewportSizePixels().getValue() return QtCore.QPoint(round(float(projected[0]) * widget.width() / size[0]), round(widget.height() - float(projected[1]) * widget.height() / size[1])) def arm_datum(text): existing = {id(widget) for widget in QtWidgets.QApplication.topLevelWidgets()} evidence = {"value": text, "dialogs": [], "timed_out": False} timer = QtCore.QTimer(main) attempts = [0] def enter(): attempts[0] += 1 dialogs = [widget for widget in QtWidgets.QApplication.topLevelWidgets() if isinstance(widget, QtWidgets.QDialog) and widget.isVisible() and id(widget) not in existing] for dialog in dialogs: edits = [edit for edit in dialog.findChildren(QtWidgets.QLineEdit) if edit.isVisible() and edit.isEnabled()] if edits: edit = edits[0] edit.setFocus() edit.selectAll() QtTest.QTest.keyClicks(edit, text) QtTest.QTest.keyClick(edit, QtCore.Qt.Key_Return) evidence["dialogs"].append(dialog.windowTitle()) timer.stop() return if attempts[0] >= 40: evidence["timed_out"] = True timer.stop() for dialog in dialogs: dialog.reject() timer.timeout.connect(enter) timer.start(100) return timer, evidence def finish_datum(timer, evidence): settle(350) timer.stop() timer.deleteLater() assert evidence["dialogs"] and not evidence["timed_out"], ("Native datum dialog was not exercised", evidence) return evidence def set_mouse_dimension(horizontal, value): sketch = context["sketch"] geometry = [(index, item) for index, item in enumerate(sketch.Geometry) if abs((item.StartPoint.y - item.EndPoint.y) if horizontal else (item.StartPoint.x - item.EndPoint.x)) < 1e-6] index, edge = max(geometry, key=lambda item: item[1].StartPoint.y if horizontal else item[1].StartPoint.x) expected = "Edge%d" % (index + 1) Gui.Selection.clearSelection() point = project(edge.StartPoint * .35 + edge.EndPoint * .65) widget = renderer() move(widget, point) settle(160) preselection = Gui.Selection.getPreselection() assert getattr(preselection, "ObjectName", None) == sketch.Name and expected in preselection.SubElementNames click_canvas(point) assert any(item.Object == sketch and expected in item.SubElementNames for item in Gui.Selection.getSelectionEx()), expected widget.setFocus(QtCore.Qt.OtherFocusReason) main.activateWindow() QtWidgets.QApplication.setActiveWindow(main) settle(150) target = QtWidgets.QApplication.focusWidget() assert target and (target == widget or target.isAncestorOf(widget) or widget.isAncestorOf(target)) shortcut_wait_ms = wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(), "Dimension shortcut did not reflect the native selection state") timer, evidence = arm_datum("%s mm" % value) try: QtTest.QTest.keyClick(target, QtCore.Qt.Key_D) settle(100) placement = point + (QtCore.QPoint(0, -50) if horizontal else QtCore.QPoint(60, 0)) assert widget.rect().contains(placement), ("Dimension placement leaves viewport", placement, widget.rect()) click_canvas(placement) finish_datum(timer, evidence) finally: timer.stop() QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape) settle(450) wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(), "Dimension shortcut did not become available after leaving the dimension tool") matches = [index for index, constraint in enumerate(sketch.Constraints) if constraint.Type in ("Distance", "DistanceX", "DistanceY") and constraint.Driving and abs(abs(constraint.Value) - value) < 1e-5] assert matches and sketch.solve() == 0, [(item.Type, item.Value) for item in sketch.Constraints] return {"edge": expected, "value_mm": value, "constraint_indices": matches, "input_selection": "native viewport Qt mouse events", "native_editor": evidence, "shortcut_wait_ms": shortcut_wait_ms} def dimensions_and_list_edit(): sketch = context["sketch"] assert sketch.GeometryCount == 4 dimensions = [set_mouse_dimension(True, 40), set_mouse_dimension(False, 25)] bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox assert abs(bounds.XLength - 40) < 1e-5 and abs(bounds.YLength - 25) < 1e-5, bounds settle(800) table = main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList") assert table is not None and table.isVisible() and table.rowCount() == 2 values = [] row40 = None for row in range(table.rowCount()): index = int(table.item(row, 0).data(QtCore.Qt.UserRole)) value = abs(float(sketch.getDatum(index).Value)) values.append({"constraint_index": index, "value_mm": value, "display": table.item(row, 1).text()}) if abs(value - 40) < 1e-5: row40 = row assert row40 is not None and any(abs(item["value_mm"] - 25) < 1e-5 for item in values) item = table.item(row40, 0) table.scrollToItem(item) QtTest.QTest.mouseClick(table.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, table.visualItemRect(item).center()) settle(150) index = int(item.data(QtCore.Qt.UserRole)) expected = "Constraint%d" % (index + 1) assert any(selection.Object == sketch and expected in selection.SubElementNames for selection in Gui.Selection.getSelectionEx()), "Dimension list did not select native constraint" timer, edit = arm_datum("60 mm") try: QtTest.QTest.mouseDClick(table.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, table.visualItemRect(item).center()) finish_datum(timer, edit) finally: timer.stop() settle(800) assert abs(abs(float(sketch.getDatum(index).Value)) - 60) < 1e-5 bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox assert abs(bounds.XLength - 60) < 1e-5 and abs(bounds.YLength - 25) < 1e-5 and sketch.solve() == 0 evidence = {"native_dimensions": dimensions, "dimension_list": values, "native_selected_constraint": expected, "edited_value_mm": 60, "native_edit_dialog": edit, "screenshot": capture("sketch-two-dimensions-edited")} Gui.runCommand("QI_FinishSketch", 0) settle(450) assert commands._editing() is None return evidence def mouse_hole_and_native_pad(): document, sketch = context["document"], context["sketch"] assert document.Name in App.listDocuments() and sketch.GeometryCount == 4 bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox assert abs(bounds.XLength - 60) < 1e-5 and abs(bounds.YLength - 25) < 1e-5, { "message": "The hole workflow requires the previous dimension workflow to finish at 60x25 mm", "actual_width": bounds.XLength, "actual_height": bounds.YLength, "native_constraints": [(item.Type, item.Value) for item in sketch.Constraints]} Gui.activeDocument().setEdit(sketch.Name) settle(900) circle_button = ribbon.buttons["QI_Circle"][0] assert circle_button.isVisible() and circle_button.isEnabled() QtTest.QTest.mouseClick(circle_button, QtCore.Qt.LeftButton) settle(100) click_canvas(project(bounds.Center)) click_canvas(project(bounds.Center + App.Vector(5, 0, 0))) QtTest.QTest.keyClick(renderer(), QtCore.Qt.Key_Escape) settle(350) assert sketch.GeometryCount == 5, "Circle tool did not add exactly one native geometry" circle_index = 4 circle = sketch.Geometry[circle_index] assert circle.__class__.__name__ == "Circle" expected_edge = "Edge%d" % (circle_index + 1) local_pick = circle.Center + App.Vector(circle.Radius / math.sqrt(2), circle.Radius / math.sqrt(2), 0) point = project(local_pick) widget = renderer() view = Gui.activeDocument().activeView() physical = view.getViewer().getSoRenderManager().getViewportRegion().getViewportSizePixels().getValue() focus = QtWidgets.QApplication.focusWidget() diagnostics = {"circle_center": list(circle.Center), "circle_radius": circle.Radius, "local_pick_point": list(local_pick), "world_pick_point": list(sketch.getGlobalPlacement().multVec(local_pick)), "projected_physical": list(view.getPointOnScreen(sketch.getGlobalPlacement().multVec(local_pick))), "physical_viewport_size": list(physical), "qt_pick": [point.x(), point.y()], "qt_renderer_size": [widget.width(), widget.height()], "qt_renderer_global_origin": [widget.mapToGlobal(QtCore.QPoint()).x(), widget.mapToGlobal(QtCore.QPoint()).y()], "qt_dpr": widget.devicePixelRatioF(), "camera": camera_frame(view), "editing": getattr(commands._editing(), "Name", None), "expected_edge": expected_edge, "active_workbench": Gui.activeWorkbench().name(), "geometry_count": sketch.GeometryCount, "focus_widget": focus.metaObject().className() if focus else None, "focus_object_name": focus.objectName() if focus else None, "tool_hint": ribbon.hint.text(), "main_active": main.isActiveWindow()} context["hole_pick_diagnostics"] = diagnostics assert widget.rect().contains(point), diagnostics Gui.Selection.clearSelection() move(widget, point) settle(160) preselection = Gui.Selection.getPreselection() diagnostics["hover_preselection"] = { "object_name": getattr(preselection, "ObjectName", None), "document_name": getattr(preselection, "DocumentName", None), "subelements": list(getattr(preselection, "SubElementNames", []))} diagnostics["before_pick_screenshot"] = capture("sketch-hole-before-radius-pick") framebuffer = view.getViewer().grabFramebuffer() framebuffer_path = os.path.join(output, "sketch-hole-before-radius-pick-native.png") assert not framebuffer.isNull() and framebuffer.save(framebuffer_path), diagnostics diagnostics["native_framebuffer"] = framebuffer_path # Retain the provisional hover evidence, then require the actual native # edge selection after the Qt gesture. Never substitute addSelection(). click_canvas(point) selection = Gui.Selection.getSelectionEx() diagnostics["selection_after_click"] = [ {"object_name": item.Object.Name, "subelements": list(item.SubElementNames)} for item in selection] with open(os.path.join(output, "sketch-hole-pick-diagnostics.json"), "w", encoding="utf-8") as stream: json.dump(diagnostics, stream, ensure_ascii=False, indent=2, default=str) assert any(item.Object == sketch and expected_edge in item.SubElementNames for item in selection), diagnostics button = ribbon.buttons["QI_Dimension"][0] menu = button.menu() assert menu is not None action = next((item for item in menu.actions() if item.text() == commands.COMMANDS["QI_DimensionRadius"].title), None) assert action is not None, "Radius command is missing from the dimension menu" menu.popup(button.mapToGlobal(QtCore.QPoint(0, button.height()))) settle(100) assert menu.isVisible() wait_for(action.isEnabled, "Radius action did not become available in the open dimension menu") timer, datum = arm_datum("5 mm") try: QtTest.QTest.mouseClick(menu, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, menu.actionGeometry(action).center()) finish_datum(timer, datum) finally: timer.stop() menu.hide() QtTest.QTest.keyClick(renderer(), QtCore.Qt.Key_Escape) settle(450) radius_constraints = [index for index, constraint in enumerate(sketch.Constraints) if constraint.Type == "Radius" and constraint.First == circle_index and constraint.Driving and abs(constraint.Value - 5) < 1e-5] assert radius_constraints and sketch.solve() == 0 circle = sketch.Geometry[circle_index] assert abs(circle.Radius - 5) < 1e-5 assert bounds.XMin < circle.Center.x - circle.Radius < circle.Center.x + circle.Radius < bounds.XMax assert bounds.YMin < circle.Center.y - circle.Radius < circle.Center.y + circle.Radius < bounds.YMax settle(450) table = main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList") assert table is not None and table.isVisible() and table.rowCount() == 3 sketch_evidence = {"circle_input": "Qt Circle button and native viewport mouse events", "radius_command_input": "Qt click on Radius action in the dimension menu", "native_circle_index": circle_index, "native_center": list(circle.Center), "rectangle_center": list(bounds.Center), "center_is_pixel_positioned": True, "radius_mm": circle.Radius, "fully_inside_rectangle": True, "radius_constraints": radius_constraints, "native_datum_dialog": datum, "native_circle_selection": diagnostics, "screenshot": capture("sketch-rectangle-with-hole")} Gui.runCommand("QI_FinishSketch", 0) settle(450) assert commands._editing() is None expected_volume = (60 * 25 - math.pi * 5**2) * 8 feature = model.pad(sketch, 8) feature_name, sketch_name = feature.Name, sketch.Name def verify_solid(candidate): assert candidate is not None and candidate.TypeId == "PartDesign::Pad" assert candidate.Shape.isValid() and len(candidate.Shape.Solids) == 1 assert abs(candidate.Shape.Volume - expected_volume) < 1e-5, (candidate.Shape.Volume, expected_volume) return candidate.Shape.Volume volume = verify_solid(feature) undo_before = document.UndoCount assert undo_before > 0 document.undo() document.recompute() assert document.getObject(feature_name) is None assert document.getObject(sketch_name).GeometryCount == 5 assert document.RedoCount > 0 document.redo() document.recompute() redo_volume = verify_solid(document.getObject(feature_name)) project_path = os.path.join(output, "rectangle-with-hole-60x25-r5-pad8.FCStd") model.save_project(document, project_path) assert os.path.isfile(project_path) and os.path.getsize(project_path) > 0 with open(project_path, "rb") as stream: project_sha = hashlib.sha256(stream.read()).hexdigest() close_owned(document) reopened = App.openDocument(project_path) created.append(reopened.Name) reopened.recompute() reopened_volume = verify_solid(reopened.getObject(feature_name)) assert reopened.getObject(sketch_name).GeometryCount == 5 return {"sketch": sketch_evidence, "pad_creation": "qi_model.pad native PartDesign::Pad API", "pad_length_mm": 8, "expected_volume_mm3": expected_volume, "volume_mm3": volume, "undo_removed_pad": True, "redo_volume_mm3": redo_volume, "saved_project": project_path, "saved_sha256": project_sha, "reopened_volume_mm3": reopened_volume} try: commands.register_commands() main.resize(1440, 940) main.hide() main.showNormal() main.raise_() main.activateWindow() assert QtTest.QTest.qWaitForWindowExposed(main.windowHandle(), 5000) ribbon = ui.show_ribbon() set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher/General", "RestoreCamera", True) set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "LeaveSketchWithEscape", False) set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "ShowDialogOnDistanceConstraint", True) set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "ContinuousCreationMode", True) check("Native RestoreCamera survives QInventor Finish and Cancel without delayed changes", native_camera_roundtrip) check("Empty native XY sketch enters the stable workspace and stays idle", empty_sketch) check("Pinned Finish/Cancel and CAD viewport stay stable at two window sizes", pinned_controls) check("Qt mouse input draws a real closed native rectangle from empty sketch", mouse_rectangle) check("D creates 40x25 dimensions; the dimension list selects and edits the native datum", dimensions_and_list_edit) check("Qt Circle and Radius menu create a hole; native Pad8 volume, Undo/Redo and FCStd reopening agree", mouse_hole_and_native_pad) except Exception: report["failed"] += 1 report["checks"].append({"name": "Workspace suite setup", "passed": False, "error": traceback.format_exc()}) finally: for name in reversed(created): if name in App.listDocuments(): try: close_owned(App.getDocument(name)) except Exception: report["failed"] += 1 report["checks"].append({"name": "Owned test document cleanup", "passed": False, "document": name, "error": traceback.format_exc()}) for group, key, present, value in reversed(preferences): group.SetBool(key, value) if present else group.RemBool(key) if initial_active in App.listDocuments(): App.setActiveDocument(initial_active) main.resize(previous_size) report["status"] = "passed" if not report["failed"] else "failed" write() return report