Files
Qinventor/tests/sketch_workspace_suite.py
T

572 lines
30 KiB
Python

"""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