aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 13:51:23 +0200
committerblasty <blasty@local>2026-07-25 13:51:23 +0200
commit4e6289c266bd1255487a131e95019fc20d9452b6 (patch)
tree316131685b57beea1230e761dab81d8ca4203e48
parentui: drop the footer cheatsheet; F1 opens a key reference instead (diff)
downloadida-tui-4e6289c266bd1255487a131e95019fc20d9452b6.tar.gz
ida-tui-4e6289c266bd1255487a131e95019fc20d9452b6.tar.xz
ida-tui-4e6289c266bd1255487a131e95019fc20d9452b6.zip
tests: fix the "filter flake" and the view_toggle cascade — suite is 187/0
Neither was a flake; both were test bugs that happened to be timing/binary dependent. 1. filter: the scenario hardcoded the glob 'sub_1*', which matches NOTHING in a binary whose code never reaches 0x1xxx — echo's functions are sub_2xxx.. sub_7xxx, so it failed deterministically, every run. Derive the glob from real names instead (first sub_ prefix present) and assert the row count equals the expected match count, which is stronger than the old 0 < n < total. A self-check asserts the derived glob actually matches something, so this can't silently rot again on another binary. 2. view_toggle: "tab switches to disassembly" asserted after a fixed pause(0.1), but decomp -> listing runs through _toggle_to_listing, a BACKGROUND WORKER — _active only flips once the listing model has loaded. The pause held in short runs and lost the race in a full one, which is why this looked like collateral from the filter failure. Wait for the state instead. Two related hardenings: focus the decomp pane before Tab (elsewhere Tab is focus-next, silently leaving us in the decompiler), and wait for the listing cursor to carry an ea before the F5 check (F5 legitimately no-ops on an unaddressed row). Full suite now 187 passed / 0 failed, the first fully green run — previously 174-182 with 2-6 "known flakes" whose count drifted with machine load.
-rw-r--r--tests/test_scenarios.py37
1 files changed, 29 insertions, 8 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index c294a9f..e88547b 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -17,6 +17,7 @@ Uses ~/ida-venv python (has textual).
--list print scenario names and exit
"""
import asyncio
+import fnmatch
import os
import re
import sys
@@ -807,15 +808,25 @@ async def s_filter(c: Ctx):
await c.wait(lambda: app._cur and app._cur.ea == fn.addr, 20)
c.check("selecting a table row opens that function",
bool(app._cur) and app._cur.ea == fn.addr, f"cur={app._cur.ea if app._cur else None}")
- # filter round-trip
+ # filter round-trip. Derive the glob from real names: this used to hardcode
+ # 'sub_1*', which matches NOTHING in a binary whose code never reaches
+ # 0x1xxx (echo's functions are sub_2xxx..sub_7xxx) — a deterministic failure
+ # that looked like a flake, and left the table empty for the next scenario.
+ subs = sorted(f.name for f in c.all_funcs() if f.name.startswith("sub_"))
+ term = (subs[0][:5] + "*") if subs else ""
+ want = sum(1 for f in c.all_funcs()
+ if fnmatch.fnmatch(f.name.lower(), term.lower())) if term else 0
+ c.check("picked a glob that actually matches (test self-check)",
+ 0 < want < nfuncs, f"term={term!r} want={want} of {nfuncs}")
table.focus()
await c.press("slash")
await c.pause(0.05)
- for ch in "sub_1*":
+ for ch in term:
await c.press(ch if ch != "*" else "asterisk")
await c.press("enter")
- filtered = await c.wait(lambda: 0 < table.row_count < nfuncs, 15)
- c.check("filter narrowed the list", filtered, f"rows={table.row_count} of {nfuncs}")
+ filtered = await c.wait(lambda: table.row_count == want, 15)
+ c.check("filter narrowed the list to exactly the matches", filtered,
+ f"term={term!r} rows={table.row_count} want={want} of {nfuncs}")
# pane toggle
left = app.query_one("#left", FunctionsPanel)
await c.press("ctrl+b")
@@ -841,11 +852,21 @@ async def s_view_toggle(c: Ctx):
styled = any(seg.style is not None and seg.style.color is not None
for strip in dec._strips[:min(dec.total, 200)] for seg in strip)
c.check("pseudocode is syntax-highlighted", styled)
+ dec.focus() # Tab only toggles the view from a code pane; elsewhere it's
+ await c.pause(0.05) # focus-next, which would silently leave us in decomp
await c.press("tab")
- await c.pause(0.1)
- c.check("tab switches to disassembly",
- app._active == "listing" and dis.display and not dec.display,
- f"active={app._active}")
+ # decomp -> listing runs through _toggle_to_listing, a background worker, so
+ # _active only flips once the listing model has loaded. A fixed pause held in
+ # a short run and lost the race in a full one.
+ switched = await c.wait(lambda: app._active == "listing" and dis.display
+ and not dec.display, 20)
+ c.check("tab switches to disassembly", switched,
+ f"active={app._active} split={app._split} "
+ f"focus={type(app.focused).__name__} "
+ f"lst={dis.display} dec={dec.display}")
+ # _toggle_to_listing repositions asynchronously; F5 below reads the listing
+ # cursor's ea and no-ops if it isn't on an addressed row yet.
+ await c.wait(lambda: c.lst._cursor_ea() is not None, 10)
# F5/Tab from the listing must raise the 'decompiling…' overlay synchronously,
# BEFORE the background decompile runs (regression: it used to decompile first
# in _decomp_from_listing, so the overlay only flashed once cached).