import hashlib import json import os import FreeCAD as App import Part FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKLISTHIDDEN_FAILURE_OUTPUT", "") def shape_hash(obj): shape = obj.Shape if shape.isNull(): return None return hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest() def object_set(document): return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects] def snapshot(document, owner, targets): return { "value": [candidate.Name for candidate in owner.Exports], "propertyTypeId": owner.getTypeIdOfProperty("Exports"), "propertyStatus": [str(item) for item in owner.getPropertyStatus("Exports")], "editorMode": [str(item) for item in owner.getEditorMode("Exports")], "ownerState": [str(item) for item in owner.State], "statusString": str(owner.getStatusString()), "ownerShapeNull": bool(owner.Shape.isNull()), "ownerOutList": [candidate.Name for candidate in owner.OutList], "targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets}, "targetShapeHashes": {target.Name: shape_hash(target) for target in targets}, "objectSet": object_set(document), } def exception_snapshot(callback): try: callback() except Exception as error: return {"type": type(error).__name__, "message": str(error)} return None def active_transaction_snapshot(): active = App.getActiveTransaction() return {"name": str(active[0]), "id": int(active[1])} if active else {"name": "", "id": 0} def run(): document = App.newDocument("PropertyLinkListHiddenFailure") foreign_document = App.newDocument("PropertyLinkListHiddenForeign") try: owner = document.addObject("Sketcher::SketchObject", "SketchProbe") first = document.addObject("Part::Feature", "ExportA") first.Shape = Part.makeBox(2.0, 3.0, 4.0) second = document.addObject("Part::Feature", "ExportB") second.Shape = Part.makeCylinder(1.5, 5.0) targets = [first, second] foreign = foreign_document.addObject("Part::Feature", "ForeignExport") foreign.Shape = Part.makeBox(1.0, 1.0, 1.0) owner.Exports = [first, second] document.recompute() document.UndoMode = 1 initial = snapshot(document, owner, targets) failures = [] cases = [ ("string-scalar", "ExportA"), ("string-item", ["ExportA"]), ("integer-scalar", 7), ("integer-item", [7]), ("cross-document", [foreign]), ] for case_id, requested in cases: before = snapshot(document, owner, targets) exception = exception_snapshot(lambda value=requested: setattr(owner, "Exports", value)) after = snapshot(document, owner, targets) failures.append({ "id": case_id, "requested": repr(requested), "exception": exception, "before": before, "after": after, "valuePreserved": before["value"] == after["value"], "documentPreserved": before == after, }) editor_before = snapshot(document, owner, targets) owner.setEditorMode("Exports", ["ReadOnly"]) editor_mode = [str(item) for item in owner.getEditorMode("Exports")] editor_exception = exception_snapshot(lambda: setattr(owner, "Exports", [second])) editor_after = snapshot(document, owner, targets) owner.setEditorMode("Exports", 0) editor_restored = [str(item) for item in owner.getEditorMode("Exports")] owner.Exports = [first, second] document.recompute() owner.setPropertyStatus("Exports", "Immutable") immutable_before = snapshot(document, owner, targets) immutable_exception = exception_snapshot(lambda: setattr(owner, "Exports", [second])) immutable_after = snapshot(document, owner, targets) owner.setPropertyStatus("Exports", "-Immutable") immutable_restored_status = [str(item) for item in owner.getPropertyStatus("Exports")] owner.Exports = [first, second] document.recompute() document.openTransaction("property-linklisthidden-cancel") transaction_before = snapshot(document, owner, targets) owner.Exports = [second, first, second] transaction_edited = snapshot(document, owner, targets) pending_after_edit = bool(document.HasPendingTransaction) active_after_edit = active_transaction_snapshot() document.abortTransaction() transaction_after_abort_before_recompute = snapshot(document, owner, targets) recompute_after_abort = bool(document.recompute()) transaction_after = snapshot(document, owner, targets) return { "schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linklisthidden-failure", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkListHidden", "object": {"name": owner.Name, "typeId": owner.TypeId}, "property": {"name": "Exports", "typeId": owner.getTypeIdOfProperty("Exports")}, "initial": initial, "failures": failures, "editorReadOnly": { "classification": "editor-read-only-python-setter-accepted", "before": editor_before, "editorMode": editor_mode, "exception": editor_exception, "after": editor_after, "pythonBypassesEditorReadOnly": editor_exception is None, "restoredEditorMode": editor_restored, }, "immutable": { "classification": "immutable-python-setter-accepted", "before": immutable_before, "exception": immutable_exception, "after": immutable_after, "pythonBypassesImmutable": immutable_exception is None, "restoredStatus": immutable_restored_status, }, "transaction": { "before": transaction_before, "edited": transaction_edited, "afterAbortBeforeRecompute": transaction_after_abort_before_recompute, "afterAbort": transaction_after, "recomputeAfterAbort": recompute_after_abort, "undoMode": int(document.UndoMode), "pendingAfterEdit": pending_after_edit, "pendingAfterAbort": bool(document.HasPendingTransaction), "activeAfterEdit": active_after_edit, "activeAfterAbort": active_transaction_snapshot(), "restored": transaction_before == transaction_after, }, "cancellationBoundary": { "supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction", }, "documentIntegrity": { "initialObjects": initial["objectSet"], "finalObjects": object_set(document), "objectsPreserved": initial["objectSet"] == object_set(document), "foreignDocumentObjects": object_set(foreign_document), }, } finally: App.closeDocument(foreign_document.Name) App.closeDocument(document.Name) report = run() if not OUTPUT_PATH: raise RuntimeError("FREECAD_PROPERTY_LINKLISTHIDDEN_FAILURE_OUTPUT is required") with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: json.dump(report, handle, indent=2, sort_keys=True) handle.write("\n") print("FREECAD_PROPERTY_LINKLISTHIDDEN_FAILURE_RESULT=" + json.dumps({"status": report["status"], "failureCount": len(report["failures"])}, sort_keys=True))