diff options
| author | blasty <blasty@local> | 2026-07-25 14:04:53 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 14:04:53 +0200 |
| commit | 1f1440285d8caf995abfc49c0849f3ad3000c64e (patch) | |
| tree | 43710761e57eed5a64742039669086b8d954c20f /tests | |
| parent | tests: fix the "filter flake" and the view_toggle cascade — suite is 187/0 (diff) | |
| download | ida-tui-1f1440285d8caf995abfc49c0849f3ad3000c64e.tar.gz ida-tui-1f1440285d8caf995abfc49c0849f3ad3000c64e.tar.xz ida-tui-1f1440285d8caf995abfc49c0849f3ad3000c64e.zip | |
exit: ask before quitting with unsaved database changes
Quitting used to be silent AND inconsistent. Single-binary mode closed the worker
with save=False, so renames/types/comments were DROPPED without a word; project
mode did the opposite and saved everything silently via the pool. Neither told
you anything.
Now 'q'/ctrl+q routes through action_quit: clean databases exit immediately, and
anything unsaved raises a QuitScreen naming the affected databases with
s save & quit d discard & quit Esc cancel
Saving happens with an overlay up, because writing a large .i64 takes seconds and
doing it during teardown would look like a hang with no UI left to explain it.
_dirty_labels() covers project mode too: the active binary plus any still-resident
one that was edited. Evicted binaries were already saved on the way out, so they
can't be silently lost.
on_unmount now distinguishes an explicit choice from an unexpected teardown:
_save_on_exit is None (crash/kill -> save defensively, including single-binary
mode which previously discarded), False (user chose discard, or we already saved).
Verified end-to-end across two sessions on a temp copy: rename + 's' -> the rename
is still there on reopen; rename + 'd' -> it is not. Plus a quit_guard scenario
(clean exits immediately, dirty asks, Esc cancels).
Also hardens Ctx.open(view="decomp"): F5/Tab only decompiles from a focused code
pane and the listing may still be settling, so a swallowed Tab surfaced much later
as "pseudocode view shows: active=listing". It now retries instead of assuming the
first Tab takes. Full suite 191/0, green twice in a row.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_scenarios.py | 42 |
1 files changed, 36 insertions, 6 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index e88547b..c1d315f 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -26,8 +26,8 @@ import traceback sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from idatui.app import ( # noqa: E402 ConfirmScreen, DecompView, DisasmView, FunctionsPanel, HexView, IdaTui, - HelpScreen, ListingView, StringsPalette, StructEditor, SymbolPalette, - XrefsScreen, _str_display, _word_occurrences, + HelpScreen, ListingView, QuitScreen, StringsPalette, StructEditor, + SymbolPalette, XrefsScreen, _str_display, _word_occurrences, ) from textual.widgets import ( # noqa: E402 DataTable, Input, OptionList, Static, TextArea, @@ -173,10 +173,17 @@ class Ctx: await self.wait(lambda: self.lst.total > 0 and self.lst._cursor_ea() == fn.addr, t) if view == "decomp": - self.lst.focus() - await self.press("tab") # F5/Tab -> decompile the function at cursor - await self.wait(lambda: self.app._active == "decomp" - and self.dec.loaded_ea == fn.addr, t) + # F5/Tab only decompiles from a focused code pane, and the listing + # may still be settling from the open above — a swallowed Tab used to + # surface much later as "pseudocode view shows: active=listing". + # Retry rather than assume the first one takes. + for _ in range(3): + self.lst.focus() + await self.pause(0.05) + await self.press("tab") + if await self.wait(lambda: self.app._active == "decomp" + and self.dec.loaded_ea == fn.addr, max(t / 3, 5)): + break return fn async def open_biggest(self, view="listing"): @@ -341,6 +348,29 @@ async def s_command_palette(c: Ctx): await c.wait(lambda: app._active != "hex", 5) +@scenario("quit_guard") +async def s_quit_guard(c: Ctx): + app = c.app + c.check("a clean database reports nothing unsaved", app._dirty_labels() == [], + f"{app._dirty_labels()}") + app._dirty = True # as an edit would + c.check("an edited database is reported unsaved", + len(app._dirty_labels()) == 1, f"{app._dirty_labels()}") + await c.press("q") + asked = await c.wait(lambda: isinstance(app.screen, QuitScreen), 10) + c.check("quitting with unsaved changes asks first", asked and app.is_running, + f"screen={type(app.screen).__name__} running={app.is_running}") + if not asked: + return + await c.press("escape") + await c.wait(lambda: not isinstance(app.screen, QuitScreen), 10) + c.check("Esc cancels the quit and stays put", + app.is_running and not isinstance(app.screen, QuitScreen)) + # leave it clean so the rest of the suite (and teardown) isn't affected + app._dirty = False + app._save_on_exit = False + + @scenario("help") async def s_help(c: Ctx): app = c.app |
