1c565da7a0
PR2 of the test_full_live_workflow_imgui_assert fix sequence.
When an ImGui scope mismatch (IM_ASSERT(Missing End())) fires in
immapp.run (e.g. after cumulative state corruption from prior sims'
panel renders), the RuntimeError propagates out of app.run(). The
controller's _io_pool gets shut down via __del__/finalization. The
hook server (separate ThreadingHTTPServer) survives. Subsequent test
clicks fail with 'cannot schedule new futures after shutdown' and
the test times out after 120s with no clear signal of what went
wrong.
This commit:
1. Wraps immapp.run in try/except RuntimeError in gui_2.py:618.
On assertion: logs the error to stderr (NOT silent), records
it on controller._gui_degraded_reason and _last_imgui_assert,
and returns from run() so the hook server keeps serving.
2. Adds _gui_degraded_reason and _last_imgui_assert to
AppController.__init__ (initialized to None).
3. Adds /api/gui_health endpoint in api_hooks.py:148. Returns
{healthy, degraded_reason, last_assert, io_pool_alive}.
4. Adds ApiHookClient.get_gui_health() with the matching unit
tests (3 mocked tests + 1 live test).
Per user feedback 2026-06-08:
- The wrap does NOT silently swallow the error. It logs at ERROR
level and surfaces it via the health endpoint.
- Tests can call client.get_gui_health() to detect a degraded GUI
and fail fast with a clear message.
TDD: tests written first, confirmed to fail, then fix applied.
34/34 unit tests pass. 1/1 live test passes (live_gui health
endpoint reports healthy=True on fresh subprocess).
60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
"""
|
|
Regression tests for the ImGui IM_ASSERT propagation handling.
|
|
|
|
The bug: when `immapp.run` raises a `RuntimeError` (e.g. from an ImGui
|
|
scope mismatch like `IM_ASSERT((0) && "Missing End()")`), the exception
|
|
propagates out of `app.run()` and may cause the controller's `_io_pool`
|
|
to shut down. The hook server thread (separate `ThreadingHTTPServer`)
|
|
survives, but subsequent test clicks fail with
|
|
`RuntimeError: cannot schedule new futures after shutdown`.
|
|
|
|
The fix (per user feedback 2026-06-08): wrap `immapp.run` in a
|
|
`try/except RuntimeError` that:
|
|
1. Does NOT silently swallow the error (user rejected silent failures)
|
|
2. Logs the error at ERROR level
|
|
3. Records the failure on the controller so the `/api/gui_health`
|
|
endpoint can surface it
|
|
4. Does NOT call `self.shutdown()` so the hook server stays alive
|
|
|
|
These tests verify the controller's state attributes exist and that
|
|
the error path correctly records the failure.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def test_app_run_records_degraded_state_on_imgui_assert(mock_app: Any) -> None:
|
|
"""When immapp.run raises RuntimeError, AppController records the
|
|
failure on `gui_degraded_reason` so the health endpoint can surface it.
|
|
|
|
The fix is in `src/gui_2.py:app.run` which wraps `immapp.run` in a
|
|
try/except. The exception is caught, the controller's state is updated,
|
|
and the run() method returns normally (so the GUI process keeps
|
|
responding to the hook server).
|
|
"""
|
|
app = mock_app
|
|
ctrl = app.controller
|
|
# Precondition: degraded reason is None
|
|
assert getattr(ctrl, "_gui_degraded_reason", None) is None
|
|
# Simulate the immapp.run raising IM_ASSERT
|
|
from imgui_bundle import immapp
|
|
with patch.object(immapp, "run", side_effect=RuntimeError("IM_ASSERT((0) && \"Missing End()\")")):
|
|
# Call app.run() — should catch the exception, not propagate
|
|
try:
|
|
app.run()
|
|
except RuntimeError as e:
|
|
pytest.fail(f"app.run() should have caught IM_ASSERT, but raised: {e}")
|
|
# Postcondition: degraded reason is set
|
|
assert ctrl._gui_degraded_reason is not None
|
|
assert "IM_ASSERT" in ctrl._gui_degraded_reason
|
|
# And the last assert contains the full message
|
|
assert ctrl._last_imgui_assert is not None
|
|
assert "Missing End" in ctrl._last_imgui_assert
|