feat: advance FreeCAD exact parity evidence
This commit is contained in:
@@ -1,16 +1,41 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import faulthandler
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
PROBE_SCOPE = os.environ.get("FREECAD_REFERENCE_SCOPE", "full")
|
||||
if PROBE_SCOPE not in {"full", "gui-commands"}:
|
||||
raise RuntimeError("Unknown FreeCAD reference probe scope: " + PROBE_SCOPE)
|
||||
TRACEBACK_SECONDS = int(os.environ.get("FREECAD_REFERENCE_TRACEBACK_SECONDS", "0"))
|
||||
if TRACEBACK_SECONDS > 0:
|
||||
faulthandler.dump_traceback_later(TRACEBACK_SECONDS, repeat=True, file=2)
|
||||
ISOLATED_GUI_CONFIG = os.environ.get("FREECAD_ORACLE_ISOLATED_CONFIG") == "1"
|
||||
if PROBE_SCOPE == "gui-commands" and ISOLATED_GUI_CONFIG:
|
||||
App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").SetBool("FirstTime", False)
|
||||
|
||||
|
||||
def probe_progress(message):
|
||||
print("FREECAD_REFERENCE_PROGRESS=" + message, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def property_metadata(obj):
|
||||
def stable_text(value):
|
||||
text = re.sub(r"0x[0-9a-fA-F]+", "<address>", str(value))
|
||||
text = re.sub(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "<uuid>", text)
|
||||
return re.sub(r"(FreeCAD_Doc_<uuid>_[^/_]+_)\d+", r"\1<run>", text)
|
||||
|
||||
def json_value(value):
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
if value is None or isinstance(value, (bool, int, float)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return stable_text(value)
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [json_value(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
@@ -18,7 +43,7 @@ def property_metadata(obj):
|
||||
# FreeCAD quantities and placements keep a stable textual form while
|
||||
# the report remains valid JSON for every registered property type.
|
||||
try:
|
||||
return str(value)
|
||||
return stable_text(value)
|
||||
except Exception:
|
||||
return "<unserializable>"
|
||||
|
||||
@@ -168,7 +193,7 @@ def probe_module(name):
|
||||
return result
|
||||
|
||||
|
||||
def probe_gui_commands(document, selected_object):
|
||||
def probe_gui_commands():
|
||||
if not bool(getattr(App, "GuiUp", False)):
|
||||
return {"available": False, "workbenches": [], "commands": []}
|
||||
try:
|
||||
@@ -180,61 +205,147 @@ def probe_gui_commands(document, selected_object):
|
||||
workbench_names = sorted(Gui.listWorkbenches().keys())
|
||||
except Exception:
|
||||
workbench_names = []
|
||||
command_observations = {}
|
||||
workbench_filter = os.environ.get("FREECAD_GUI_WORKBENCH_FILTER")
|
||||
if workbench_filter:
|
||||
if workbench_filter not in workbench_names:
|
||||
raise RuntimeError("Unknown GUI workbench filter: " + workbench_filter)
|
||||
workbench_names = [workbench_filter]
|
||||
command_registrations = {}
|
||||
|
||||
def action_enabled(command_id):
|
||||
def command_active(command_id):
|
||||
try:
|
||||
command = Gui.Command.get(command_id)
|
||||
if command is None:
|
||||
return None
|
||||
actions = command.getAction()
|
||||
if not actions:
|
||||
return None
|
||||
return bool(actions[0].isEnabled())
|
||||
# StdCmdExpression dereferences its private QAction in isActive().
|
||||
# Other commands must use their native predicate; QAction enabled
|
||||
# state is only a cached presentation value and misses context changes.
|
||||
if command_id == "Std_Expressions":
|
||||
actions = command.getAction()
|
||||
return bool(actions[0].isEnabled()) if actions else None
|
||||
return bool(command.isActive())
|
||||
except Exception as error:
|
||||
return {"error": str(error)}
|
||||
|
||||
workbenches = []
|
||||
for workbench_name in workbench_names:
|
||||
probe_progress("workbench:start:" + workbench_name)
|
||||
try:
|
||||
Gui.activateWorkbench(workbench_name)
|
||||
Gui.updateGui()
|
||||
command_ids = sorted(set(Gui.listCommands()))
|
||||
Gui.Selection.clearSelection()
|
||||
empty_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
|
||||
Gui.Selection.addSelection(document.Name, selected_object.Name)
|
||||
selected_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
|
||||
Gui.Selection.clearSelection()
|
||||
observations = {
|
||||
command_id: {
|
||||
"emptySelection": empty_selection.get(command_id),
|
||||
"selectedPartBox": selected_selection.get(command_id),
|
||||
}
|
||||
for command_id in command_ids
|
||||
}
|
||||
for command_id, observation in observations.items():
|
||||
command_observations.setdefault(command_id, []).append({"workbench": workbench_name, **observation})
|
||||
for command_id in command_ids:
|
||||
command_registrations.setdefault(command_id, []).append(workbench_name)
|
||||
workbenches.append({"name": workbench_name, "commandCount": len(command_ids), "status": "probed"})
|
||||
probe_progress("workbench:done:" + workbench_name)
|
||||
except Exception as error:
|
||||
try:
|
||||
Gui.Selection.clearSelection()
|
||||
except Exception:
|
||||
pass
|
||||
workbenches.append({"name": workbench_name, "commandCount": 0, "status": "probe-failure", "error": str(error)})
|
||||
probe_progress("workbench:failed:" + workbench_name)
|
||||
|
||||
# Command registration is workbench-scoped, but document/selection state is
|
||||
# intrinsic to each command. Probe the state matrix once after all providers
|
||||
# are loaded instead of repeating the cumulative command set per workbench.
|
||||
for document_name in list(App.listDocuments().keys()):
|
||||
App.closeDocument(document_name)
|
||||
command_ids = sorted(command_registrations.keys())
|
||||
command_universe_sha256 = hashlib.sha256("\n".join(command_ids).encode("utf-8")).hexdigest()
|
||||
if os.environ.get("FREECAD_GUI_REGISTRATION_ONLY") == "1":
|
||||
return {
|
||||
"available": True,
|
||||
"probeMode": "registration-only",
|
||||
"stateProbe": "not-probed",
|
||||
"stateCases": [],
|
||||
"directIsActiveBoundary": "not-applicable",
|
||||
"commandUniverseCount": len(command_ids),
|
||||
"commandUniverseSha256": command_universe_sha256,
|
||||
"stateCommandCount": 0,
|
||||
"shard": None,
|
||||
"workbenches": workbenches,
|
||||
"commands": [
|
||||
{
|
||||
"id": command_id,
|
||||
"registeredWorkbenches": command_registrations[command_id],
|
||||
}
|
||||
for command_id in command_ids
|
||||
],
|
||||
}
|
||||
shard = None
|
||||
state_command_ids = command_ids
|
||||
shard_spec = os.environ.get("FREECAD_GUI_COMMAND_SHARD")
|
||||
if shard_spec:
|
||||
try:
|
||||
shard_index, shard_count = [int(value) for value in shard_spec.split("/", 1)]
|
||||
except Exception:
|
||||
raise RuntimeError("FREECAD_GUI_COMMAND_SHARD must use zero-based INDEX/COUNT syntax")
|
||||
if shard_count < 1 or shard_index < 0 or shard_index >= shard_count:
|
||||
raise RuntimeError("FREECAD_GUI_COMMAND_SHARD is outside its declared range")
|
||||
shard_start = len(command_ids) * shard_index // shard_count
|
||||
shard_end = len(command_ids) * (shard_index + 1) // shard_count
|
||||
state_command_ids = command_ids[shard_start:shard_end]
|
||||
shard = {
|
||||
"index": shard_index,
|
||||
"count": shard_count,
|
||||
"start": shard_start,
|
||||
"end": shard_end,
|
||||
}
|
||||
Gui.Selection.clearSelection()
|
||||
Gui.updateGui()
|
||||
probe_progress("state:start:noDocument")
|
||||
no_document = {command_id: command_active(command_id) for command_id in state_command_ids}
|
||||
probe_progress("state:done:noDocument")
|
||||
context_document = App.newDocument("CommandContext")
|
||||
selected_object = context_document.addObject("Part::Box", "CommandProbeBox")
|
||||
Gui.updateGui()
|
||||
probe_progress("state:start:documentNoSelection")
|
||||
document_no_selection = {command_id: command_active(command_id) for command_id in state_command_ids}
|
||||
probe_progress("state:done:documentNoSelection")
|
||||
Gui.Selection.addSelection(context_document.Name, selected_object.Name)
|
||||
Gui.updateGui()
|
||||
probe_progress("state:start:selectedPartBox")
|
||||
selected_selection = {command_id: command_active(command_id) for command_id in state_command_ids}
|
||||
probe_progress("state:done:selectedPartBox")
|
||||
Gui.Selection.clearSelection()
|
||||
App.closeDocument(context_document.Name)
|
||||
return {
|
||||
"available": True,
|
||||
"probeMode": "state-matrix",
|
||||
"stateProbe": "native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard",
|
||||
"stateCases": ["noDocument", "documentNoSelection", "selectedPartBox"],
|
||||
"directIsActiveBoundary": "Std_Expressions-only-qaction-guard-for-locked-1.1.1-null-qaction-crash",
|
||||
"commandUniverseCount": len(command_ids),
|
||||
"commandUniverseSha256": command_universe_sha256,
|
||||
"stateCommandCount": len(state_command_ids),
|
||||
"shard": shard,
|
||||
"workbenches": workbenches,
|
||||
"commands": [
|
||||
{"id": command_id, "observations": observations}
|
||||
for command_id, observations in sorted(command_observations.items())
|
||||
{
|
||||
"id": command_id,
|
||||
"registeredWorkbenches": command_registrations[command_id],
|
||||
"observations": [{
|
||||
"workbench": "all-registered-runtime-context",
|
||||
"noDocument": no_document.get(command_id),
|
||||
"documentNoSelection": document_no_selection.get(command_id),
|
||||
"selectedPartBox": selected_selection.get(command_id),
|
||||
}],
|
||||
}
|
||||
for command_id in state_command_ids
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
document = App.newDocument("ReferenceProbe")
|
||||
box_object = document.addObject("Part::Box", "CommandProbeBox")
|
||||
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
|
||||
gui_commands = probe_gui_commands(document, box_object)
|
||||
runtime_objects = probe_registered_objects(document)
|
||||
modules = []
|
||||
if PROBE_SCOPE == "full":
|
||||
document = App.newDocument("ReferenceProbe")
|
||||
# Creating one native Part object loads the same runtime type registry used
|
||||
# by the locked desktop baseline before supportedTypes() is enumerated.
|
||||
document.addObject("Part::Box", "TypeRegistryProbeBox")
|
||||
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
|
||||
App.closeDocument(document.Name)
|
||||
gui_commands = probe_gui_commands()
|
||||
version = App.Version()
|
||||
result = {
|
||||
"schemaVersion": 1,
|
||||
@@ -245,20 +356,34 @@ result = {
|
||||
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
||||
"guiUp": bool(getattr(App, "GuiUp", False)),
|
||||
"buildPurpose": "desktop-reference-oracle" if bool(getattr(App, "GuiUp", False)) else "headless-reference-oracle",
|
||||
"moduleCount": len(ALL_REFERENCE_MODULES),
|
||||
"probeScope": PROBE_SCOPE,
|
||||
"oracleSetup": {
|
||||
"isolatedConfig": ISOLATED_GUI_CONFIG,
|
||||
"bimFirstTimeWelcome": "suppressed-before-workbench-activation" if ISOLATED_GUI_CONFIG else "native-user-config",
|
||||
},
|
||||
"determinism": {
|
||||
"schemaVersion": 1,
|
||||
"normalizedProcessFields": ["pointer-address", "uuid", "freecad-document-cache-run"],
|
||||
},
|
||||
"moduleCount": len(modules),
|
||||
"modules": modules,
|
||||
"objects": [probe_object(document, type_id) for type_id in [
|
||||
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||
]],
|
||||
"runtimeObjects": runtime_objects,
|
||||
"guiCommands": gui_commands,
|
||||
}
|
||||
if PROBE_SCOPE == "full":
|
||||
# Workbench activation registers additional GUI-owned document types.
|
||||
# Enumerate them only in the independent object/property scope.
|
||||
document = App.newDocument("RuntimeObjectProbe")
|
||||
document.addObject("Part::Box", "TypeRegistryProbeBox")
|
||||
result["runtimeObjects"] = probe_registered_objects(document)
|
||||
result["objects"] = [probe_object(document, type_id) for type_id in [
|
||||
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||
]]
|
||||
App.closeDocument(document.Name)
|
||||
result["moduleStatusSummary"] = {
|
||||
status: sum(1 for module in result["modules"] if module["runtimeStatus"] == status)
|
||||
for status in sorted({module["runtimeStatus"] for module in result["modules"]})
|
||||
}
|
||||
App.closeDocument(document.Name)
|
||||
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
|
||||
Reference in New Issue
Block a user