Skip to content

Commit b438e8a

Browse files
fix: smoke_test — remove all C++-unsafe unreal calls, thread join, tool execution (EXCEPTION_ACCESS_VIOLATION root causes)
1 parent 1263f7b commit b438e8a

2 files changed

Lines changed: 40 additions & 130 deletions

File tree

Content/Python/UEFN_Toolbelt/smoke_test.py

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,9 @@ def _layer_python() -> None:
135135
else:
136136
_record("Layer 1", "TCP socket bind", False, "ports 8765-8770 all blocked")
137137

138-
# Daemon thread
139-
flag = {"ok": False}
140-
def _worker(): flag["ok"] = True
141-
t = threading.Thread(target=_worker, daemon=True)
142-
t.start(); t.join(timeout=2)
143-
_record("Layer 1", "Daemon thread", flag["ok"])
138+
# Daemon thread class available (creation/join removed — t.join pumps Windows messages
139+
# via WaitForMultipleObjectsEx, which can fire pending Slate callbacks mid-test → crash)
140+
_record("Layer 1", "threading.Thread available", hasattr(threading, "Thread"))
144141

145142
# HTTP server class instantiable (daemon thread round-trip removed — unsafe in UEFN Slate tick)
146143
try:
@@ -179,39 +176,32 @@ def _layer_uefn() -> None:
179176
has_tick = hasattr(unreal, "register_slate_post_tick_callback")
180177
_record("Layer 2", "register_slate_post_tick_callback", has_tick)
181178

182-
# Key subsystems
179+
# Key subsystems — hasattr only, no get_editor_subsystem() call.
180+
# Calling get_editor_subsystem() with a class not registered in UEFN writes to null at C++ level,
181+
# which Python try/except cannot catch. StaticMeshEditorSubsystem in particular may not exist in UEFN.
183182
for name in ["EditorActorSubsystem", "EditorAssetSubsystem",
184183
"LevelEditorSubsystem", "StaticMeshEditorSubsystem"]:
185-
try:
186-
sub = unreal.get_editor_subsystem(getattr(unreal, name))
187-
_record("Layer 2", name, sub is not None)
188-
except Exception as e:
189-
_record("Layer 2", name, False, str(e))
184+
ok = hasattr(unreal, name)
185+
_record("Layer 2", name, ok)
190186

191-
# Key libraries
187+
# Key libraries — attribute existence only
192188
for lib in ["EditorAssetLibrary", "EditorLevelLibrary",
193189
"EditorUtilityLibrary", "MaterialEditingLibrary",
194190
"AutomationLibrary"]:
195191
ok = hasattr(unreal, lib)
196192
_record("Layer 2", lib, ok)
197193

198-
# AutomationLibrary.take_high_res_screenshot signature check
199-
try:
200-
fn = getattr(unreal.AutomationLibrary, "take_high_res_screenshot", None)
201-
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", fn is not None)
202-
except Exception as e:
203-
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", False, str(e))
194+
# AutomationLibrary method check — attribute only, no call
195+
ok = hasattr(unreal, "AutomationLibrary") and \
196+
hasattr(unreal.AutomationLibrary, "take_high_res_screenshot")
197+
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", ok)
204198

205-
# Saved dir writable
199+
# Saved dir path available (no write — Paths.project_saved_dir() is safe)
206200
try:
207201
saved = os.path.join(unreal.Paths.project_saved_dir(), "UEFN_Toolbelt")
208-
os.makedirs(saved, exist_ok=True)
209-
probe = os.path.join(saved, "_smoke.tmp")
210-
with open(probe, "w") as f: f.write("ok")
211-
os.remove(probe)
212-
_record("Layer 2", "Saved/UEFN_Toolbelt/ writable", True, saved)
202+
_record("Layer 2", "Paths.project_saved_dir()", True, saved)
213203
except Exception as e:
214-
_record("Layer 2", "Saved/UEFN_Toolbelt/ writable", False, str(e))
204+
_record("Layer 2", "Paths.project_saved_dir()", False, str(e))
215205

216206

217207
# ─── Layer 3: Toolbelt core ───────────────────────────────────────────────────
@@ -268,50 +258,15 @@ def _layer_toolbelt() -> None:
268258
except Exception as e:
269259
_record("Layer 3", "tb.run() returns values", False, str(e))
270260

271-
# Key tools explicitly registered
261+
# Key tools explicitly registered — registry lookup only, no execution.
262+
# Tool execution is the integration test's job (tb.run("toolbelt_integration_test")).
263+
# Running tools here risks Asset Registry crashes (Quirk #32), Slate callback
264+
# re-registration, and Qt module-level side effects — all can write to null in UEFN.
272265
for tool_name in ["verse_gen_custom", "snapshot_save", "material_apply_preset",
273266
"mcp_start", "scatter_hism", "tag_add"]:
274267
ok = tool_name in tb.registry
275268
_record("Layer 3", f"tool: {tool_name}", ok)
276269

277-
# Execute Safe Tools (No Actor Needed)
278-
safe_tools_to_test = [
279-
# Core utilities
280-
"api_list_subsystems",
281-
"api_search",
282-
"config_list",
283-
"config_get",
284-
"mcp_status",
285-
# mcp_restart intentionally excluded — registers Slate tick callbacks mid-test → crash
286-
"plugin_export_manifest",
287-
"plugin_validate_all",
288-
"plugin_list_custom",
289-
# Scaffold / project
290-
"scaffold_list_templates",
291-
# Snapshots
292-
"snapshot_list",
293-
# Materials
294-
"material_list_presets",
295-
# Text
296-
"text_list_styles",
297-
# Theme
298-
"theme_list",
299-
"theme_get",
300-
# Verse
301-
"verse_list_snippets",
302-
"verse_graph_scan",
303-
# Measurement
304-
"spline_measure",
305-
# LOD / memory (read-only scans)
306-
"lod_audit_folder",
307-
]
308-
for safe_tool in safe_tools_to_test:
309-
try:
310-
tb.run(safe_tool)
311-
_record("Layer 3", f"Execute {safe_tool}", True)
312-
except Exception as e:
313-
_record("Layer 3", f"Execute {safe_tool}", False, str(e))
314-
315270

316271
# ─── Layer 4: MCP bridge ──────────────────────────────────────────────────────
317272

tests/smoke_test.py

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,9 @@ def _layer_python() -> None:
135135
else:
136136
_record("Layer 1", "TCP socket bind", False, "ports 8765-8770 all blocked")
137137

138-
# Daemon thread
139-
flag = {"ok": False}
140-
def _worker(): flag["ok"] = True
141-
t = threading.Thread(target=_worker, daemon=True)
142-
t.start(); t.join(timeout=2)
143-
_record("Layer 1", "Daemon thread", flag["ok"])
138+
# Daemon thread class available (creation/join removed — t.join pumps Windows messages
139+
# via WaitForMultipleObjectsEx, which can fire pending Slate callbacks mid-test → crash)
140+
_record("Layer 1", "threading.Thread available", hasattr(threading, "Thread"))
144141

145142
# HTTP server class instantiable (daemon thread round-trip removed — unsafe in UEFN Slate tick)
146143
try:
@@ -179,39 +176,32 @@ def _layer_uefn() -> None:
179176
has_tick = hasattr(unreal, "register_slate_post_tick_callback")
180177
_record("Layer 2", "register_slate_post_tick_callback", has_tick)
181178

182-
# Key subsystems
179+
# Key subsystems — hasattr only, no get_editor_subsystem() call.
180+
# Calling get_editor_subsystem() with a class not registered in UEFN writes to null at C++ level,
181+
# which Python try/except cannot catch. StaticMeshEditorSubsystem in particular may not exist in UEFN.
183182
for name in ["EditorActorSubsystem", "EditorAssetSubsystem",
184183
"LevelEditorSubsystem", "StaticMeshEditorSubsystem"]:
185-
try:
186-
sub = unreal.get_editor_subsystem(getattr(unreal, name))
187-
_record("Layer 2", name, sub is not None)
188-
except Exception as e:
189-
_record("Layer 2", name, False, str(e))
184+
ok = hasattr(unreal, name)
185+
_record("Layer 2", name, ok)
190186

191-
# Key libraries
187+
# Key libraries — attribute existence only
192188
for lib in ["EditorAssetLibrary", "EditorLevelLibrary",
193189
"EditorUtilityLibrary", "MaterialEditingLibrary",
194190
"AutomationLibrary"]:
195191
ok = hasattr(unreal, lib)
196192
_record("Layer 2", lib, ok)
197193

198-
# AutomationLibrary.take_high_res_screenshot signature check
199-
try:
200-
fn = getattr(unreal.AutomationLibrary, "take_high_res_screenshot", None)
201-
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", fn is not None)
202-
except Exception as e:
203-
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", False, str(e))
194+
# AutomationLibrary method check — attribute only, no call
195+
ok = hasattr(unreal, "AutomationLibrary") and \
196+
hasattr(unreal.AutomationLibrary, "take_high_res_screenshot")
197+
_record("Layer 2", "AutomationLibrary.take_high_res_screenshot", ok)
204198

205-
# Saved dir writable
199+
# Saved dir path available (no write — Paths.project_saved_dir() is safe)
206200
try:
207201
saved = os.path.join(unreal.Paths.project_saved_dir(), "UEFN_Toolbelt")
208-
os.makedirs(saved, exist_ok=True)
209-
probe = os.path.join(saved, "_smoke.tmp")
210-
with open(probe, "w") as f: f.write("ok")
211-
os.remove(probe)
212-
_record("Layer 2", "Saved/UEFN_Toolbelt/ writable", True, saved)
202+
_record("Layer 2", "Paths.project_saved_dir()", True, saved)
213203
except Exception as e:
214-
_record("Layer 2", "Saved/UEFN_Toolbelt/ writable", False, str(e))
204+
_record("Layer 2", "Paths.project_saved_dir()", False, str(e))
215205

216206

217207
# ─── Layer 3: Toolbelt core ───────────────────────────────────────────────────
@@ -268,50 +258,15 @@ def _layer_toolbelt() -> None:
268258
except Exception as e:
269259
_record("Layer 3", "tb.run() returns values", False, str(e))
270260

271-
# Key tools explicitly registered
261+
# Key tools explicitly registered — registry lookup only, no execution.
262+
# Tool execution is the integration test's job (tb.run("toolbelt_integration_test")).
263+
# Running tools here risks Asset Registry crashes (Quirk #32), Slate callback
264+
# re-registration, and Qt module-level side effects — all can write to null in UEFN.
272265
for tool_name in ["verse_gen_custom", "snapshot_save", "material_apply_preset",
273266
"mcp_start", "scatter_hism", "tag_add"]:
274267
ok = tool_name in tb.registry
275268
_record("Layer 3", f"tool: {tool_name}", ok)
276269

277-
# Execute Safe Tools (No Actor Needed)
278-
safe_tools_to_test = [
279-
# Core utilities
280-
"api_list_subsystems",
281-
"api_search",
282-
"config_list",
283-
"config_get",
284-
"mcp_status",
285-
# mcp_restart intentionally excluded — registers Slate tick callbacks mid-test → crash
286-
"plugin_export_manifest",
287-
"plugin_validate_all",
288-
"plugin_list_custom",
289-
# Scaffold / project
290-
"scaffold_list_templates",
291-
# Snapshots
292-
"snapshot_list",
293-
# Materials
294-
"material_list_presets",
295-
# Text
296-
"text_list_styles",
297-
# Theme
298-
"theme_list",
299-
"theme_get",
300-
# Verse
301-
"verse_list_snippets",
302-
"verse_graph_scan",
303-
# Measurement
304-
"spline_measure",
305-
# LOD / memory (read-only scans)
306-
"lod_audit_folder",
307-
]
308-
for safe_tool in safe_tools_to_test:
309-
try:
310-
tb.run(safe_tool)
311-
_record("Layer 3", f"Execute {safe_tool}", True)
312-
except Exception as e:
313-
_record("Layer 3", f"Execute {safe_tool}", False, str(e))
314-
315270

316271
# ─── Layer 4: MCP bridge ──────────────────────────────────────────────────────
317272

0 commit comments

Comments
 (0)