Files
Web_FreeCAD_Bitbybit/scripts/freecad-property-status-oracle.py
wangdequan 5bbd7b9d4f
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: advance FreeCAD exact parity evidence
2026-08-14 22:39:16 -04:00

256 lines
11 KiB
Python

import json
import os
import tempfile
import zipfile
import xml.etree.ElementTree as ET
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
class PropertyObserver:
def __init__(self, document_name):
self.document_name = document_name
self.events = []
def clear(self):
self.events = []
def slotOpenTransaction(self, document, name):
if document.Name == self.document_name:
self.events.append({"type": "transaction.opened", "name": str(name)})
def slotCommitTransaction(self, document):
if document.Name == self.document_name:
self.events.append({"type": "transaction.committed"})
def slotAbortTransaction(self, document):
if document.Name == self.document_name:
self.events.append({"type": "transaction.aborted"})
def slotBeforeChangeObject(self, obj, prop):
if obj.Document and obj.Document.Name == self.document_name:
self.events.append({"type": "property.before-change", "object": obj.Name, "property": str(prop)})
def slotChangedObject(self, obj, prop):
if obj.Document and obj.Document.Name == self.document_name:
self.events.append({"type": "property.changed", "object": obj.Name, "property": str(prop)})
def slotAppendDynamicProperty(self, obj, prop):
if getattr(obj, "Document", None) and obj.Document.Name == self.document_name:
self.events.append({"type": "property.added", "object": obj.Name, "property": str(prop)})
def slotRemoveDynamicProperty(self, obj, prop):
if getattr(obj, "Document", None) and obj.Document.Name == self.document_name:
self.events.append({"type": "property.removed", "object": obj.Name, "property": str(prop)})
def object_state(obj):
return {
"state": [str(value) for value in obj.State],
"mustExecute": bool(obj.MustExecute),
"documentTouched": bool(obj.Document.isTouched()),
}
def change_case(case_id, attr=0, runtime_status=None):
document = App.newDocument("PropertyStatus_" + case_id)
try:
obj = document.addObject("App::FeatureTest", "Feature")
obj.addProperty("App::PropertyInteger", "Value", "Oracle", "Status probe", attr)
if runtime_status:
obj.setPropertyStatus("Value", runtime_status)
document.recompute()
document.purgeTouched()
before = object_state(obj)
obj.Value = 7
changed = object_state(obj)
recomputed = int(document.recompute())
after_recompute = object_state(obj)
return {
"id": case_id,
"attr": int(attr),
"runtimeStatus": runtime_status,
"reportedStatus": [str(value) for value in obj.getPropertyStatus("Value")],
"reportedType": [str(value) for value in obj.getTypeOfProperty("Value")],
"before": before,
"changed": changed,
"recomputeCount": recomputed,
"afterRecompute": after_recompute,
}
finally:
App.closeDocument(document.Name)
def locked_dynamic_case():
document = App.newDocument("PropertyStatus_LockDynamic")
observer = None
try:
document.UndoMode = 1
obj = document.addObject("App::FeaturePython", "Feature")
obj.addProperty("App::PropertyInteger", "LockedValue", "Oracle")
obj.setPropertyStatus("LockedValue", "LockDynamic")
observer = PropertyObserver(document.Name)
App.addDocumentObserver(observer)
document.openTransaction("dynamic-property-mutation")
obj.addProperty("App::PropertyInteger", "MutableValue", "Oracle")
obj.MutableValue = 3
failures = {}
for operation, callback in {
"remove": lambda: obj.removeProperty("LockedValue"),
"rename": lambda: obj.renameProperty("LockedValue", "RenamedValue"),
}.items():
try:
callback()
failures[operation] = None
except Exception as error:
failures[operation] = {"type": type(error).__name__, "message": str(error)}
obj.renameProperty("MutableValue", "RenamedValue")
removed_mutable = bool(obj.removeProperty("RenamedValue"))
document.commitTransaction()
return {
"reportedStatus": [str(value) for value in obj.getPropertyStatus("LockedValue")],
"propertyStillPresent": "LockedValue" in obj.PropertiesList,
"renamedPropertyPresent": "RenamedValue" in obj.PropertiesList,
"failures": failures,
"mutableRenameAndRemove": removed_mutable and "MutableValue" not in obj.PropertiesList and "RenamedValue" not in obj.PropertiesList,
"observerEvents": observer.events,
"undoAvailable": document.UndoCount == 1,
}
finally:
if observer:
App.removeDocumentObserver(observer)
App.closeDocument(document.Name)
def partial_trigger_case():
document = App.newDocument("PropertyStatus_PartialTrigger")
observer = PropertyObserver(document.Name)
App.addDocumentObserver(observer)
try:
document.UndoMode = 1
obj = document.addObject("PartDesign::SubShapeBinder", "Binder")
document.recompute()
document.purgeTouched()
observer.clear()
document.openTransaction("partial-trigger-mutation")
obj.PartialLoad = True
recomputed = int(document.recompute())
document.commitTransaction()
return {
"reportedStatus": [str(value) for value in obj.getPropertyStatus("PartialLoad")],
"value": bool(obj.PartialLoad),
"recomputeCount": recomputed,
"state": object_state(obj),
"observerEvents": observer.events,
"undoAvailable": document.UndoCount == 1,
}
finally:
App.removeDocumentObserver(observer)
App.closeDocument(document.Name)
def archive_case():
with tempfile.TemporaryDirectory(prefix="freecad-property-status-") as temp_dir:
path = os.path.join(temp_dir, "PropertyStatus.FCStd")
document = App.newDocument("PropertyStatusArchive")
try:
obj = document.addObject("App::FeaturePython", "Feature")
target = document.addObject("App::FeaturePython", "Target")
obj.addProperty("App::PropertyInteger", "Persisted", "Oracle")
obj.Persisted = 11
obj.setPropertyStatus("Persisted", "Output")
obj.addProperty("App::PropertyInteger", "RuntimeTransient", "Oracle")
obj.RuntimeTransient = 12
obj.setPropertyStatus("RuntimeTransient", "Transient")
obj.addProperty("App::PropertyInteger", "TypeTransient", "Oracle", "", int(App.PropertyType.Prop_Transient))
obj.TypeTransient = 13
obj.addProperty("App::PropertyInteger", "NoPersist", "Oracle", "", int(App.PropertyType.Prop_NoPersist))
obj.NoPersist = 14
obj.addProperty("App::PropertyFloatConstraint", "FloatConstraint", "Oracle")
obj.FloatConstraint = 0.25
obj.addProperty("App::PropertyQuantityConstraint", "QuantityConstraint", "Oracle")
obj.QuantityConstraint = 12.5
obj.addProperty("App::PropertyPrecision", "Precision", "Oracle")
obj.Precision = 0.001
obj.addProperty("App::PropertyLinkHidden", "HiddenLink", "Oracle")
obj.HiddenLink = target
document.recompute()
document.saveAs(path)
finally:
App.closeDocument(document.Name)
with zipfile.ZipFile(path, "r") as archive:
document_xml = archive.read("Document.xml").decode("utf-8")
xml_root = ET.fromstring(document_xml)
xml_properties = {
element.attrib.get("name"): element
for element in xml_root.findall("./ObjectData/Object/Properties/Property")
}
def native_float_property(name, expected):
element = xml_properties.get(name)
child = element.find("Float") if element is not None else None
return bool(element is not None and element.attrib.get("type") == "App::Property" + name and child is not None and abs(float(child.attrib["value"]) - expected) < 1e-12)
reopened = App.openDocument(path)
try:
obj = reopened.getObject("Feature")
return {
"xml": {
"persistedProperty": '<Property name="Persisted"' in document_xml,
"runtimeTransientProperty": '<Property name="RuntimeTransient"' in document_xml,
"typeTransientProperty": '<Property name="TypeTransient"' in document_xml,
"noPersistProperty": '<Property name="NoPersist"' in document_xml,
"runtimeTransientHasValue": '<Integer value="12"' in document_xml,
"typeTransientHasValue": '<Integer value="13"' in document_xml,
"floatConstraintNative": native_float_property("FloatConstraint", 0.25),
"quantityConstraintNative": native_float_property("QuantityConstraint", 12.5),
"precisionNative": native_float_property("Precision", 0.001),
"hiddenLinkNative": xml_properties.get("HiddenLink") is not None and xml_properties["HiddenLink"].attrib.get("type") == "App::PropertyLinkHidden" and xml_properties["HiddenLink"].find("Link") is not None and xml_properties["HiddenLink"].find("Link").attrib.get("value") == "Target",
},
"reopened": {
"properties": sorted(str(value) for value in obj.PropertiesList if value in {"Persisted", "RuntimeTransient", "TypeTransient", "NoPersist"}),
"values": {
"Persisted": int(obj.Persisted),
"RuntimeTransient": int(obj.RuntimeTransient),
"TypeTransient": int(obj.TypeTransient),
},
"statuses": {
name: [str(value) for value in obj.getPropertyStatus(name)]
for name in ["Persisted", "RuntimeTransient", "TypeTransient"]
},
"codecValues": {
"FloatConstraint": float(obj.FloatConstraint),
"QuantityConstraint": float(obj.QuantityConstraint.Value),
"Precision": float(obj.Precision),
"HiddenLink": obj.HiddenLink.Name,
},
},
}
finally:
App.closeDocument(reopened.Name)
cases = [
change_case("ordinary"),
change_case("runtime-output", runtime_status="Output"),
change_case("type-output", attr=int(App.PropertyType.Prop_Output)),
change_case("runtime-no-recompute", runtime_status="NoRecompute"),
change_case("type-no-recompute", attr=int(App.PropertyType.Prop_NoRecompute)),
]
report = {
"schemaVersion": 2,
"baselineId": "freecad-1.1.1-property-status-oracle",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"cases": cases,
"lockDynamic": locked_dynamic_case(),
"partialTrigger": partial_trigger_case(),
"fcstd": archive_case(),
}
print("FREECAD_PROPERTY_STATUS_ORACLE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))