import hashlib import json import os import FreeCAD as App import Part FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT", "") def value_of(owner): return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support] def shape_hash(obj): shape = obj.Shape return None if shape.isNull() else hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest() def snapshot(document, owner, targets, recompute=True): if recompute: document.recompute() return { "value": value_of(owner), "propertyTypeId": owner.getTypeIdOfProperty("Support"), "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], "editorMode": [str(item) for item in owner.getEditorMode("Support")], "ownerState": [str(item) for item in owner.State], "statusString": str(owner.getStatusString()), "ownerShapeHash": shape_hash(owner), "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": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], } 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_case(): document = App.newDocument("PropertyLinkSubListGlobalFailure") external = App.newDocument("PropertyLinkSubListGlobalExternal") try: owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe") first = document.addObject("Part::Feature", "SourceBox") first.Shape = Part.makeBox(4.0, 5.0, 6.0) second = document.addObject("Part::Feature", "SecondBox") second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0)) foreign = external.addObject("Part::Feature", "ForeignBox") foreign.Shape = Part.makeBox(1.0, 1.0, 1.0) targets = [first, second] baseline_value = [(first, ("Face1", "Face2")), (second, "Face1")] owner.Support = baseline_value document.recompute() document.UndoMode = 1 initial = snapshot(document, owner, targets) inputs = [] candidates = [ ("scalar", "SourceBox"), ("singlePairShorthand", (first, "Face1")), ("stringObject", [("SourceBox", "Face1")]), ("integerObject", [(7, "Face1")]), ] for name, candidate in candidates: owner.Support = baseline_value document.recompute() before = snapshot(document, owner, targets) exception = exception_snapshot(lambda value=candidate: setattr(owner, "Support", value)) after = snapshot(document, owner, targets) inputs.append({"id": name, "requested": repr(candidate), "exception": exception, "before": before, "after": after, "valuePreserved": before["value"] == after["value"], "documentPreserved": before == after}) owner.Support = baseline_value document.recompute() cross_before = snapshot(document, owner, targets) cross_exception = exception_snapshot(lambda: setattr(owner, "Support", [(foreign, "Face1")])) cross_after = snapshot(document, owner, targets) cross_document = {"exception": cross_exception, "before": cross_before, "after": cross_after, "valuePreserved": cross_before["value"] == cross_after["value"], "documentPreserved": cross_before == cross_after} owner.setPropertyStatus("Support", "ReadOnly") read_only_before = snapshot(document, owner, targets) read_only_exception = exception_snapshot(lambda: setattr(owner, "Support", [(second, "Face2")])) read_only_after = snapshot(document, owner, targets) read_only = {"exception": read_only_exception, "before": read_only_before, "after": read_only_after, "pythonBypassesEditorReadOnly": read_only_exception is None} owner.setPropertyStatus("Support", "-ReadOnly") owner.Support = baseline_value document.recompute() owner.setPropertyStatus("Support", "Immutable") immutable_before = snapshot(document, owner, targets) immutable_exception = exception_snapshot(lambda: setattr(owner, "Support", [(first, "Face3")])) immutable_after = snapshot(document, owner, targets) immutable = {"exception": immutable_exception, "before": immutable_before, "after": immutable_after, "pythonBypassesImmutable": immutable_exception is None} owner.setPropertyStatus("Support", "-Immutable") owner.Support = baseline_value document.recompute() document.openTransaction("property-linksublistglobal-cancel") transaction_before = snapshot(document, owner, targets) owner.Support = [(second, "Face1")] transaction_edited = snapshot(document, owner, targets, recompute=False) pending_after_edit = bool(document.HasPendingTransaction) active_after_edit = active_transaction_snapshot() document.abortTransaction() transaction_after_abort_before_recompute = snapshot(document, owner, targets, recompute=False) recompute_after_abort = bool(document.recompute()) transaction_after = snapshot(document, owner, targets) return { "objectTypeId": "PartDesign::ShapeBinder", "objectName": "ShapeBinderProbe", "propertyName": "Support", "initial": initial, "inputs": inputs, "crossDocument": cross_document, "readOnly": read_only, "immutable": immutable, "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}, "documentIntegrity": {"initialObjects": initial["objectSet"], "finalObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "objectsPreserved": initial["objectSet"] == [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "foreignDocumentObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in external.Objects]}, } finally: App.closeDocument(external.Name) App.closeDocument(document.Name) case = run_case() report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-failure", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "case": case, "cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"}} if not OUTPUT_PATH: raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_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_LINKSUBLISTGLOBAL_FAILURE_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))