Private
Public Access
0
0
Files
manual_slop/tests/test_live_gui_filedialog_regression.py
T
ed 21aaf31032 fix(gui_2): graceful fallback when tkinter.filedialog is unloadable
Bug: on Python installs where the tkinter package imports but the
filedialog sub-module fails to load (e.g., missing Tcl/Tk runtime,
embedded Python), every call to filedialog.askopenfilename raised
'AttributeError: module tkinter has no attribute filedialog' at the
frame the Project Settings window's 'Add Project' button was clicked.

Fix: _LazyModule._resolve() now catches AttributeError on the
getattr() attempt, falls back to importlib.import_module('tkinter.filedialog')
(which surfaces the real ImportError cleanly), and finally falls back
to a new _FiledialogStub class that exposes askopenfilename,
askopenfilenames, askdirectory, asksaveasfilename returning safe
empty sentinels (str and tuple). The stub sets available=False so
future UI can detect it and offer an ImGui-based path input.

Tests:
- tests/test_lazymodule_filedialog_fallback.py: 5 unit tests using
  a deliberately-missing sub-module to deterministically exercise
  the fallback path on any Python install
- tests/test_live_gui_filedialog_regression.py: live_gui smoke test
  that opens the Project Settings window via the Hook API and
  asserts no AttributeError in the running app's log
2026-06-07 02:02:41 -04:00

66 lines
2.9 KiB
Python

"""
Live-GUI smoke test for the tkinter.filedialog AttributeError regression.
On Python installs where the Tcl/Tk runtime is missing, the lazy
`tkinter.filedialog` import raises AttributeError, which previously
crashed the Project Settings window and the Add Project button.
The unit-level test in `test_lazymodule_filedialog_fallback.py`
deterministically exercises the fallback path; this live test verifies
the same fix in the actual running app: opening the Project Settings
window via the Hook API must not produce an AttributeError, and the
app must remain responsive (proving no crash on attribute resolution).
"""
import time
from pathlib import Path
import pytest
from src.api_hook_client import ApiHookClient
def test_live_gui_project_settings_opens_without_filedialog_crash(live_gui) -> None:
"""
Regression: the Project Settings window's render call chain ends
in `render_projects_panel` → `filedialog.askopenfilename(...)` on
the "Add Project" click frame. Before the fix, every frame the
Project Settings window was open on a broken tkinter install would
log `AttributeError: module 'tkinter' has no attribute 'filedialog'`.
The fix in `_LazyModule._resolve()` falls back to a `_FiledialogStub`
that returns empty strings.
This test:
1. Opens the Project Settings window via the Hook API
2. Waits several render frames
3. Verifies the window opened (state is reflected back via get_value)
4. Verifies the app is still responsive (status endpoint returns 200)
5. Verifies no AttributeError was logged (the bug would print to
the GUI's stderr, which the live_gui fixture captures to a log)
"""
process, gui_script = live_gui
client = ApiHookClient()
log_path = Path(f"logs/{Path(gui_script).name.replace('.', '_')}_test.log")
log_offset_before = log_path.stat().st_size if log_path.exists() else 0
client.set_value('show_windows["Project Settings"]', True)
time.sleep(2.0)
opened = client.get_value('show_windows["Project Settings"]')
assert opened is True, f"Project Settings window did not open: {opened}"
status = client.get_status()
assert status is not None, "App status endpoint returned None — app is not responsive"
assert status.get("status") == "ok", f"App status not ok: {status}"
time.sleep(1.0)
if log_path.exists():
with log_path.open("r", encoding="utf-8", errors="ignore") as f:
f.seek(log_offset_before)
new_log = f.read()
assert "AttributeError: module 'tkinter' has no attribute 'filedialog'" not in new_log, (
"GUI logged 'AttributeError: module tkinter has no attribute filedialog' "
"after opening Project Settings. The _LazyModule fallback to _FiledialogStub "
"is not working in the live app."
)
assert "AttributeError: module 'tkinter' has no attribute 'filedialog'" not in new_log
if "AttributeError" in new_log:
pytest.fail(f"App logged unexpected AttributeError: {new_log[max(0, new_log.find('AttributeError')-200):new_log.find('AttributeError')+200]}")