aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_project_ui.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 21:23:44 +0200
committerblasty <blasty@local>2026-08-07 21:23:44 +0200
commit32ab74b03e9a50274396b4a32aae8baece7628a1 (patch)
tree547d9d1efe77411faca8e3df3e2fdad147fa8c5d /tests/test_project_ui.py
parentStruct editor: '/' fuzzy-filters the struct list (diff)
downloadida-tui-32ab74b03e9a50274396b4a32aae8baece7628a1.tar.gz
ida-tui-32ab74b03e9a50274396b4a32aae8baece7628a1.tar.xz
ida-tui-32ab74b03e9a50274396b4a32aae8baece7628a1.zip
tests: gate on real signals, not sleeps (117s -> 49s)
The suite spent its time in two kinds of guess. **Flat pauses.** ~140 `pause(d)` calls were 20.4s of the pilot's 62s, and `test_trace_ui` was 13.5s of `pilot.pause(1.0)` out of 19.6s. `Ctx.pause` is now `settle` (`d` is the upper bound, not the cost) and the other suites' sleeps became gates on the thing the check is about. `Ctx.sleep` stays for what a timer really drives. **Textual's keypress path.** `Pilot.press` calls `wait_for_idle` twice per key, which sleeps in 20ms granules until process time stops advancing -- 84ms per keypress here, 23s of the pilot's 43s. `_fixtures.fast_keys()` replaces it with the gate the suites already use: send the keys, then settle. Deleting the heuristic *without* that broke nine checks, so it was doing a job, badly. Four checks turned out to be riding on those sleeps: they read geometry or a repaint (`si.region`, `gv._minimap_rect()`, glyphs off `gv.render_line`, a repaint trace), and a settled app has not necessarily been laid out or painted. They now wait for the frame. The debounced function filter (`set_timer(0.08)`) likewise waits for its effect. Also fixed two waits on signals that never arrive: the comment wait in `rename` carried a `dec.loaded_ea == app._cur.ea` conjunct that cost 9s of timeout and then let the check pass vacuously, and `listing_view` -- the one entry under "Known-flaky" -- waited on `lst.total`, which is true before a single row exists. `--profile` reports, per scenario, seconds settling / waiting / pressing, and names any wait that expired with its line number. It is how the above was found and how the next 20s should be. Verified: 4 full `tests/run.py` runs, 800 passed each, 49.0-49.2s (was 117.4s); 4 consecutive pilot runs, 313 passed each, 21.2s (was 63.7s).
Diffstat (limited to 'tests/test_project_ui.py')
-rw-r--r--tests/test_project_ui.py30
1 files changed, 19 insertions, 11 deletions
diff --git a/tests/test_project_ui.py b/tests/test_project_ui.py
index 7e93c81..863b98a 100644
--- a/tests/test_project_ui.py
+++ b/tests/test_project_ui.py
@@ -19,7 +19,11 @@ import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from idatui._sync import wait_for # noqa: E402
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from _fixtures import fast_keys # noqa: E402
+from idatui._sync import settle as quiesce, wait_for # noqa: E402
+
+fast_keys() # ~85ms -> ~2ms per keypress; see _fixtures.fast_keys
from idatui.app import IdaTui, ProjectPalette # noqa: E402
from idatui.project import Project # noqa: E402
from textual.widgets import Input, OptionList, Static # noqa: E402
@@ -113,7 +117,7 @@ async def run(bins):
# -- switch to the second binary -------------------------------- #
pal.query_one(Input).value = second
- await pilot.pause(0.2)
+ await settle(lambda: bool(pal._results), 20)
await pilot.press("enter")
switched = await settle(
lambda: app._binary == second and app.program is not None
@@ -141,7 +145,7 @@ async def run(bins):
if not reopened:
return
app.screen.query_one(Input).value = first
- await pilot.pause(0.2)
+ await settle(lambda: bool(app.screen._results), 20)
await pilot.press("enter")
back = await settle(lambda: app._binary == first
and app._func_index is not None
@@ -160,7 +164,7 @@ async def run(bins):
"switcher did not reopen")
return
app.screen.query_one(Input).value = second
- await pilot.pause(0.2)
+ await settle(lambda: bool(app.screen._results), 20)
await pilot.press("enter")
again = await settle(lambda: app._binary == second
and app._cur is not None, 120)
@@ -181,15 +185,19 @@ async def run(bins):
if await settle(lambda: isinstance(app.screen, SymbolPalette), 20):
pal = app.screen
pal.query_one(Input).value = "main"
- await pilot.pause(0.3)
+ await settle(lambda: bool(pal._results), 20)
+ # The palette re-applies inside the key handler, so the gate is
+ # quiescence -- NOT "a foreign binary appeared", which is the
+ # thing under test and would sit out its whole timeout on the
+ # day it breaks.
await pilot.press("f2") # widen to the whole project
- await pilot.pause(0.4)
+ await quiesce(app)
names = [(b, n) for b, _, n in pal._results]
check("project scope finds a name shared by both binaries",
len({b for b, n in names if n == "main"}) == 2,
f"{names[:6]}")
await pilot.press("escape")
- await pilot.pause(0.2)
+ await settle(lambda: not isinstance(app.screen, SymbolPalette), 20)
# -- a cross-binary jump is not a one-way door ----------------- #
# Nav history is per-binary, so arriving in another binary lands you
@@ -217,7 +225,7 @@ async def run(bins):
if not app._hops or app._binary != there:
break
await pilot.press("escape")
- await pilot.pause(0.6)
+ await quiesce(app)
returned = await settle(lambda: app._binary == here, 180)
check("Esc crosses back to the binary the jump came from",
returned, f"binary={app._binary} want={here} hops={app._hops}")
@@ -269,17 +277,17 @@ async def run(bins):
if await settle(lambda: isinstance(app.screen, StringsPalette), 30):
pal = app.screen
pal.query_one(Input).value = "usage"
- await pilot.pause(0.3)
+ await settle(lambda: bool(pal._results), 20)
local = {b for b, _, _ in pal._results}
await pilot.press("f2")
- await pilot.pause(0.4)
+ await quiesce(app)
wide = {b for b, _, _ in pal._results}
check("strings: local scope is this binary only", local == {None},
f"{local}")
check("strings: F2 widens across the project",
len(wide) >= 2 and None not in wide, f"{wide}")
await pilot.press("escape")
- await pilot.pause(0.2)
+ await settle(lambda: not isinstance(app.screen, StringsPalette), 20)
# -- the promise: nothing was written next to the sources ---------- #
left = sorted(os.listdir(src))