bcdc26d0bd
PR1 follow-up (the actual IM_ASSERT root cause fix).
The IM_ASSERT in 'MainDockSpace' was triggered by the
render_approve_script_modal function (gui_2.py:4895) calling
imgui.checkbox with a None value for app.ui_approve_modal_preview.
The chain of bugs:
1. AppController.__getattr__ returned None for ANY ui_ attribute
(line 1237-1238). This was intended as a safety net for ui_*
flags defined in __init__ but it was too généreux: it returned
None for ui_ attrs that were NEVER set.
2. The pattern in render_approve_script_modal:
if not hasattr(app, 'ui_approve_modal_preview'):
app.ui_approve_modal_preview = False
_, app.ui_approve_modal_preview = imgui.checkbox(..., app.ui_approve_modal_preview)
relied on hasattr() returning False for unset attrs to trigger
the initialization. But the App.__setattr__ checks
hasattr(self.controller, name) to decide where to route
assignments. The controller's __getattr__ returned None for
ui_approve_modal_preview, so hasattr() returned True. The
App.__setattr__ routed the assignment to the controller.
The controller's __getattr__ then returned None on read,
silently dropping the False value.
3. The next line called imgui.checkbox with None, which raised
a TypeError. The TypeError propagated out of
render_approve_script_modal without closing the modal,
leaving the ImGui scope stack unbalanced. The unbalanced
scope triggered IM_ASSERT(Missing End()) on the next frame.
Fix: AppController.__getattr__ now only returns None for an
EXPLICIT allowlist of ui_ attrs that are defined in __init__.
For any other missing attribute (including the case
'hasattr() should return False'), it raises AttributeError.
The App.__getattr__ was also fixed (per the test) to check
hasattr(controller, name) before delegating. This is defense in
depth in case other __getattr__ patterns are added.
Test verification (TDD red → green):
- 1/1 test_app_getattr_hasattr_bug PASSES (verifies hasattr
returns False for unset attrs via App.__getattr__)
- 1/1 test_app_controller_getattr_ui_bug PASSES (verifies hasattr
returns False for unset ui_ attrs on controller)
Live verification:
- 4 sims + test_live_workflow + 2 markdown tests: 7/7 PASS in 83.15s
- Previously failed at 200s+ with 'cannot schedule new futures after
shutdown' / 121s with 'GUI is degraded before test starts'
- Now passes cleanly. The IM_ASSERT no longer fires.
13/13 related unit tests pass (app_controller_* + app_run_* +
app_getattr_*). No regressions in 51/51 io_pool/warmup/sigint/etc.
unit tests.
41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
"""Regression tests for the App.__getattr__ passthrough bug.
|
|
|
|
The bug: `App.__getattr__` delegates unknown attributes to the
|
|
controller via `getattr(self.controller, name)`. If the controller
|
|
doesn't have the attribute either, `getattr` returns None (the default).
|
|
|
|
This breaks `hasattr(app, 'unknown_attr')`:
|
|
- hasattr() checks the instance dict first
|
|
- If not found, it calls __getattr__
|
|
- __getattr__ returns None instead of raising AttributeError
|
|
- hasattr() sees a non-exception result, returns True
|
|
- Caller's `if not hasattr(...)` is False, never initializes the attr
|
|
|
|
The fix: __getattr__ should raise AttributeError for attributes that
|
|
the controller also doesn't have, so hasattr() works correctly.
|
|
"""
|
|
import pytest
|
|
import sys
|
|
import os
|
|
from typing import Any
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
sys.path.insert(0, ROOT)
|
|
|
|
|
|
def test_hasattr_returns_false_for_unset_attribute(mock_app: Any) -> None:
|
|
"""hasattr(app, 'definitely_unique_test_attr_xyz123') must return False
|
|
when neither App nor controller has the attribute. This is the exact
|
|
pattern that fails in render_approve_script_modal (gui_2.py:4894):
|
|
`if not hasattr(app, 'foo'): app.foo = False`.
|
|
"""
|
|
app = mock_app
|
|
# Use a unique attribute name guaranteed not to exist
|
|
attr_name = "definitely_unique_test_attr_xyz123"
|
|
# Assert hasattr returns False
|
|
assert hasattr(app, attr_name) is False, (
|
|
f"hasattr returned True for {attr_name!r} which is not set on App "
|
|
"or controller. This breaks the `if not hasattr: initialize` "
|
|
"pattern in render_approve_script_modal (gui_2.py:4894)."
|
|
)
|