aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_project_ui.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_project_ui.py')
-rw-r--r--tests/test_project_ui.py341
1 files changed, 229 insertions, 112 deletions
diff --git a/tests/test_project_ui.py b/tests/test_project_ui.py
index 863b98a..9b388c8 100644
--- a/tests/test_project_ui.py
+++ b/tests/test_project_ui.py
@@ -21,12 +21,15 @@ import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
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._sync import settle as quiesce # noqa: E402
+from idatui._sync import wait_for
+
+fast_keys() # ~85ms -> ~2ms per keypress; see _fixtures.fast_keys
+from textual.widgets import Input, OptionList, Static # noqa: E402
+
from idatui.app import IdaTui, ProjectPalette # noqa: E402
from idatui.project import Project # noqa: E402
-from textual.widgets import Input, OptionList, Static # noqa: E402
PASS = FAIL = 0
@@ -57,6 +60,7 @@ async def run(bins):
app = IdaTui(keepalive=False, project=proj)
async with app.run_test(size=(140, 44)) as pilot:
+
async def settle(pred, t=180.0):
return await wait_for(pred, pilot.pause, t, 0.05)
@@ -73,114 +77,163 @@ async def run(bins):
waited for explicitly, not hoped for.
"""
return await settle(
- lambda: app.program is not None
- and app._func_index is not None
- and app._func_index.complete
- and app._loading_screen is None
- and len(app.screen_stack) == 1, t)
+ lambda: (
+ app.program is not None
+ and app._func_index is not None
+ and app._func_index.complete
+ and app._loading_screen is None
+ and len(app.screen_stack) == 1
+ ),
+ t,
+ )
# -- boots on the project's first binary ----------------------- #
ok = await usable()
- check("project mode boots on the first binary", ok,
- f"binary={app._binary}")
- check("the active binary is the first one", app._binary == first,
- f"{app._binary}")
+ check("project mode boots on the first binary", ok, f"binary={app._binary}")
+ check(
+ "the active binary is the first one",
+ app._binary == first,
+ f"{app._binary}",
+ )
n_first = len(app._func_index)
check("its functions loaded", n_first > 10, f"n={n_first}")
status = str(app.query_one("#status", Static).render())
- check("the status line names the active binary",
- f"[{first}]" in status, status[:60])
+ check(
+ "the status line names the active binary",
+ f"[{first}]" in status,
+ status[:60],
+ )
# -- the switcher lists the project ---------------------------- #
await pilot.press("ctrl+o")
- opened = await settle(
- lambda: isinstance(app.screen, ProjectPalette), 20)
- check("Ctrl+O opens the binary switcher", opened,
- f"screen={type(app.screen).__name__}")
+ opened = await settle(lambda: isinstance(app.screen, ProjectPalette), 20)
+ check(
+ "Ctrl+O opens the binary switcher",
+ opened,
+ f"screen={type(app.screen).__name__}",
+ )
if not opened:
return
pal = app.screen
- check("the switcher lists every project binary",
- len(pal._results) == 2, f"{[e['label'] for e in pal._results]}")
- check("it marks which one is active",
- any(e["active"] and e["label"] == first for e in pal._results))
- check("it marks the other as not yet opened",
- any(not e["resident"] and e["label"] == second
- for e in pal._results))
+ check(
+ "the switcher lists every project binary",
+ len(pal._results) == 2,
+ f"{[e['label'] for e in pal._results]}",
+ )
+ check(
+ "it marks which one is active",
+ any(e["active"] and e["label"] == first for e in pal._results),
+ )
+ check(
+ "it marks the other as not yet opened",
+ any(not e["resident"] and e["label"] == second for e in pal._results),
+ )
ol = pal.query_one(OptionList)
- check("the switcher opens on the binary you're already in",
- ol.highlighted is not None
- and pal._results[ol.highlighted]["label"] == first,
- f"highlighted={ol.highlighted} "
- f"={pal._results[ol.highlighted]['label'] if ol.highlighted is not None else None} "
- f"want={first}")
+ check(
+ "the switcher opens on the binary you're already in",
+ ol.highlighted is not None
+ and pal._results[ol.highlighted]["label"] == first,
+ f"highlighted={ol.highlighted} "
+ f"={pal._results[ol.highlighted]['label'] if ol.highlighted is not None else None} "
+ f"want={first}",
+ )
# -- switch to the second binary -------------------------------- #
pal.query_one(Input).value = second
await settle(lambda: bool(pal._results), 20)
await pilot.press("enter")
switched = await settle(
- lambda: app._binary == second and app.program is not None
- and app._func_index is not None and app._func_index.complete
- and len(app.screen_stack) == 1)
- check("switching opens the other binary", switched,
- f"binary={app._binary}")
- check("the second binary has its own function index",
- app._func_index is not None and len(app._func_index) > 5,
- f"n={len(app._func_index) if app._func_index else 0}")
- check("both binaries now have live workers",
- sorted(app._pool.resident()) == sorted([first, second]),
- f"{app._pool.resident()}")
+ lambda: (
+ app._binary == second
+ and app.program is not None
+ and app._func_index is not None
+ and app._func_index.complete
+ and len(app.screen_stack) == 1
+ )
+ )
+ check("switching opens the other binary", switched, f"binary={app._binary}")
+ check(
+ "the second binary has its own function index",
+ app._func_index is not None and len(app._func_index) > 5,
+ f"n={len(app._func_index) if app._func_index else 0}",
+ )
+ check(
+ "both binaries now have live workers",
+ sorted(app._pool.resident()) == sorted([first, second]),
+ f"{app._pool.resident()}",
+ )
landed = await settle(lambda: app._cur is not None, 60)
- check("it lands somewhere in the new binary", landed,
- f"cur={app._cur}")
+ check("it lands somewhere in the new binary", landed, f"cur={app._cur}")
where = app._cur.ea if app._cur else None
# -- switch back: resident, so state is restored ---------------- #
await pilot.press("ctrl+o")
- reopened = await settle(
- lambda: isinstance(app.screen, ProjectPalette), 20)
- check("the switcher reopens after a switch", reopened,
- f"screen={type(app.screen).__name__}")
+ reopened = await settle(lambda: isinstance(app.screen, ProjectPalette), 20)
+ check(
+ "the switcher reopens after a switch",
+ reopened,
+ f"screen={type(app.screen).__name__}",
+ )
if not reopened:
return
app.screen.query_one(Input).value = first
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
- and app._func_index.complete
- and len(app.screen_stack) == 1, 120)
- check("switching back returns to the first binary", back,
- f"binary={app._binary}")
- check("its function index came back intact",
- app._func_index is not None and len(app._func_index) == n_first,
- f"n={len(app._func_index) if app._func_index else 0} want={n_first}")
+ back = await settle(
+ lambda: (
+ app._binary == first
+ and app._func_index is not None
+ and app._func_index.complete
+ and len(app.screen_stack) == 1
+ ),
+ 120,
+ )
+ check(
+ "switching back returns to the first binary",
+ back,
+ f"binary={app._binary}",
+ )
+ check(
+ "its function index came back intact",
+ app._func_index is not None and len(app._func_index) == n_first,
+ f"n={len(app._func_index) if app._func_index else 0} want={n_first}",
+ )
# and forward again: the second binary's position was remembered
await pilot.press("ctrl+o")
if not await settle(lambda: isinstance(app.screen, ProjectPalette), 20):
- check("returning to a binary restores where you were", False,
- "switcher did not reopen")
+ check(
+ "returning to a binary restores where you were",
+ False,
+ "switcher did not reopen",
+ )
return
app.screen.query_one(Input).value = second
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)
- check("returning to a binary restores where you were",
- again and app._cur.ea == where,
- f"cur={app._cur.ea if app._cur else None} want={where}")
+ again = await settle(
+ lambda: app._binary == second and app._cur is not None, 120
+ )
+ check(
+ "returning to a binary restores where you were",
+ again and app._cur.ea == where,
+ f"cur={app._cur.ea if app._cur else None} want={where}",
+ )
# -- project-wide symbol search ------------------------------- #
# 'main' exists in BOTH binaries: identical name, so the rank tuple
# ties and a bare sort() would fall through to comparing Hit objects
# ('<' not supported between instances of 'Hit').
from idatui.app import SymbolPalette
- await settle(lambda: app._index is not None
- and len(app._index.counts()) == 2, 60)
- check("both binaries got indexed",
- len(app._index.counts()) == 2, f"{app._index.counts()}")
+
+ await settle(
+ lambda: app._index is not None and len(app._index.counts()) == 2, 60
+ )
+ check(
+ "both binaries got indexed",
+ len(app._index.counts()) == 2,
+ f"{app._index.counts()}",
+ )
await pilot.press("ctrl+n")
if await settle(lambda: isinstance(app.screen, SymbolPalette), 20):
pal = app.screen
@@ -190,12 +243,14 @@ async def run(bins):
# 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.press("f2") # widen to the whole project
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]}")
+ 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 settle(lambda: not isinstance(app.screen, SymbolPalette), 20)
@@ -209,17 +264,31 @@ async def run(bins):
target = app._index.search("main", limit=200)
tgt = next((h for h in target if h.binary == there), None)
if tgt is None:
- check("cross-binary jump records a hop", False, "no hit in the other binary")
+ check(
+ "cross-binary jump records a hop",
+ False,
+ "no hit in the other binary",
+ )
else:
app._switch_then_goto(tgt.binary, tgt.addr)
- jumped = await settle(lambda: app._binary == there
- and app._func_index is not None
- and app._func_index.complete, 180)
- check("a project hit switches to the other binary", jumped,
- f"binary={app._binary} want={there}")
- check("the jump records where it came from",
- len(app._hops) == hops0 + 1 and app._hops[-1] == here,
- f"hops={app._hops}")
+ jumped = await settle(
+ lambda: (
+ app._binary == there
+ and app._func_index is not None
+ and app._func_index.complete
+ ),
+ 180,
+ )
+ check(
+ "a project hit switches to the other binary",
+ jumped,
+ f"binary={app._binary} want={there}",
+ )
+ check(
+ "the jump records where it came from",
+ len(app._hops) == hops0 + 1 and app._hops[-1] == here,
+ f"hops={app._hops}",
+ )
# spend the local history first, then Esc must cross back
for _ in range(6):
if not app._hops or app._binary != there:
@@ -227,10 +296,16 @@ async def run(bins):
await pilot.press("escape")
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}")
- check("the hop is consumed, not repeated",
- not app._hops, f"hops={app._hops}")
+ check(
+ "Esc crosses back to the binary the jump came from",
+ returned,
+ f"binary={app._binary} want={here} hops={app._hops}",
+ )
+ check(
+ "the hop is consumed, not repeated",
+ not app._hops,
+ f"hops={app._hops}",
+ )
# -- xrefs: callers in OTHER project binaries ------------------ #
# xrefs_to only sees this database, so an exported function looks
@@ -238,9 +313,13 @@ async def run(bins):
# The selection rule is "only for a symbol we actually export"; two
# executables share no linkage, so here it must stay quiet.
from idatui.app import XrefsScreen
+
fake = app._foreign_importers(app._cur.ea, "strrchr", None)
- check("no cross-binary callers for a symbol this binary doesn't export",
- fake == [], f"{fake}")
+ check(
+ "no cross-binary callers for a symbol this binary doesn't export",
+ fake == [],
+ f"{fake}",
+ )
# The routing a real cross-binary caller takes: the dialog carries a
# (binary, addr) payload instead of a bare address, and choosing it
@@ -248,31 +327,55 @@ async def run(bins):
# so Esc comes back.
where_from = app._binary
other = first if where_from == second else second
- hit = next((h for h in app._index.search("main", limit=200)
- if h.binary == other), None)
+ hit = next(
+ (h for h in app._index.search("main", limit=200) if h.binary == other),
+ None,
+ )
if hit is None:
- check("a cross-binary xref jumps to the other binary", False,
- "no symbol found in the other binary")
+ check(
+ "a cross-binary xref jumps to the other binary",
+ False,
+ "no symbol found in the other binary",
+ )
else:
hops0 = len(app._hops)
app.push_screen(
- XrefsScreen("xrefs to fake", [((hit.binary, hit.addr),
- f"{hit.addr:08X} import [{hit.binary}]")]),
- app._on_xref_chosen)
+ XrefsScreen(
+ "xrefs to fake",
+ [
+ (
+ (hit.binary, hit.addr),
+ f"{hit.addr:08X} import [{hit.binary}]",
+ )
+ ],
+ ),
+ app._on_xref_chosen,
+ )
await settle(lambda: isinstance(app.screen, XrefsScreen), 20)
await pilot.press("enter")
- jumped = await settle(lambda: app._binary == other
- and app._func_index is not None
- and app._func_index.complete, 180)
- check("a cross-binary xref jumps to the other binary", jumped,
- f"binary={app._binary} want={other}")
- check("and records a hop so Esc returns",
- len(app._hops) == hops0 + 1 and app._hops[-1] == where_from,
- f"hops={app._hops}")
+ jumped = await settle(
+ lambda: (
+ app._binary == other
+ and app._func_index is not None
+ and app._func_index.complete
+ ),
+ 180,
+ )
+ check(
+ "a cross-binary xref jumps to the other binary",
+ jumped,
+ f"binary={app._binary} want={other}",
+ )
+ check(
+ "and records a hop so Esc returns",
+ len(app._hops) == hops0 + 1 and app._hops[-1] == where_from,
+ f"hops={app._hops}",
+ )
app._hops.clear()
# -- and the same toggle for strings --------------------------- #
from idatui.app import StringsPalette
+
await pilot.press("quotation_mark")
if await settle(lambda: isinstance(app.screen, StringsPalette), 30):
pal = app.screen
@@ -282,20 +385,32 @@ async def run(bins):
await pilot.press("f2")
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}")
+ 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 settle(lambda: not isinstance(app.screen, StringsPalette), 20)
# -- the promise: nothing was written next to the sources ---------- #
left = sorted(os.listdir(src))
- check("the source tree stays pristine (no .i64/scratch beside it)",
- left == sorted(os.path.basename(s) for s in srcs), f"{left}")
+ check(
+ "the source tree stays pristine (no .i64/scratch beside it)",
+ left == sorted(os.path.basename(s) for s in srcs),
+ f"{left}",
+ )
staged = sorted(os.listdir(proj.bin_dir))
- check("IDA's artifacts all live in the project sidecar",
- any(f.endswith(".i64") for f in staged), f"{staged}")
+ check(
+ "IDA's artifacts all live in the project sidecar",
+ any(f.endswith(".i64") for f in staged),
+ f"{staged}",
+ )
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0
@@ -303,8 +418,10 @@ async def run(bins):
def main(argv):
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- bins = argv or [os.path.join(repo, "targets", "echo"),
- os.path.join(repo, "targets", "cat")]
+ bins = argv or [
+ os.path.join(repo, "targets", "echo"),
+ os.path.join(repo, "targets", "cat"),
+ ]
for b in bins:
if not os.path.isfile(b):
print(f"no such binary: {b}")