aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-29 23:39:58 +0200
committerblasty <blasty@local>2026-07-29 23:39:58 +0200
commit8188a495b2eed6f120e551e7bda7f16cbe97e952 (patch)
tree803682a46d33389a37f640876b9c34f80a91999f /tests
parenttrace: memory at time T — stack in the dock, live bytes in hex (diff)
downloadida-tui-8188a495b2eed6f120e551e7bda7f16cbe97e952.tar.gz
ida-tui-8188a495b2eed6f120e551e7bda7f16cbe97e952.tar.xz
ida-tui-8188a495b2eed6f120e551e7bda7f16cbe97e952.zip
trace: seek verbs — next/previous execution, and who set this register
M3. Stepping walks time; seeking jumps to the next time THIS thing was touched, which is what makes a trace more than a very long single-step log. `>` / `<` — next/previous execution of whatever the focused view addresses. One pair of keys, two questions, because what's on screen already says which: * listing: the instruction under the cursor. "When else did this run?" * pseudocode: the whole C line, as the union of its instructions' executions. A line is not one address, and falling back to its single /*ea*/ marker would answer a narrower question — usually none at all, since most lines have no marker. * hex: the byte under the cursor, via memory_accesses. It says where you landed ("execution of 0x3160: 2 of 2 @ t=320") and, at either end, that you're AT the end rather than silently doing nothing — a key that does nothing is indistinguishable from a broken one. `W` — the registers with the instruction that set each to its current value, and the distance back. Enter seeks to that write, f seeks forward. Backward is the direction people want: you notice a bad value after it has been used. This is the question a trace exists to answer and it was already in the model (last_write/next_write), untested in anger until now. tests: +13 trace UI (38) — > and < move between the two executions of a repeated instruction, the status names which execution it is, both edges report instead of moving, W opens, and choosing a register lands on an instruction that REALLY wrote it (checked against the trace's own changed-set, not just the timestamp matching). Two things the tests taught me, both recorded: * focus() does not make a view active outside split mode — Tab does. My first seek test pressed > while _active was still "decomp", so it asked the pseudocode about a line with no instructions. * TODO gets a new entry: a stray late navigation to the entry function arrives after a seek and wins, leaving the cursor on 'start' while the pc is elsewhere. Same shape as the stale-decomp-result bug fixed in 756589a, which got a sequence guard the listing path never did. 212/0 scenarios, 35/0 model, 12/0 differential.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_trace_ui.py84
1 files changed, 82 insertions, 2 deletions
diff --git a/tests/test_trace_ui.py b/tests/test_trace_ui.py
index b672143..b08ca02 100644
--- a/tests/test_trace_ui.py
+++ b/tests/test_trace_ui.py
@@ -14,10 +14,10 @@ import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from textual.widgets import Static # noqa: E402
+from textual.widgets import Input, OptionList, Static # noqa: E402
from idatui.app import (DecompView, IdaTui, ListingView, # noqa: E402
- TraceDock)
+ RegWriteScreen, TraceDock)
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRACER = os.path.expanduser(
@@ -286,6 +286,86 @@ async def run() -> int:
mapped > 3 and missed == 0,
f"{mapped} mapped, {missed} not followed")
+ # -- seeking, as opposed to stepping ---------------------------- #
+ # "When else did this instruction run?" — the question that makes a
+ # trace more than a very long single-step log.
+ hot = max(t.by_ip, key=lambda a: len(t.by_ip[a]))
+ stamps = list(t.by_ip[hot])
+ db = hot + t.slide
+ if len(stamps) < 2 or lst.model is None:
+ check("found an address executed more than once", False,
+ f"{len(stamps)} executions")
+ else:
+ if app._split:
+ app.action_toggle_split()
+ await pilot.pause(1.0)
+ if app._active != "listing":
+ # focus() does NOT make a view active outside split mode;
+ # Tab is what switches which one is showing.
+ await pilot.press("tab")
+ await wait(lambda: app._active == "listing", pilot, 60)
+ app._seek(stamps[0])
+ await pilot.pause(1.2)
+ lst.focus()
+ row = lst.model.index_of_ea(db)
+ lst.cursor = row
+ lst._scroll_cursor_into_view()
+ await pilot.pause(0.4)
+ check("cursor is on the repeated instruction",
+ lst._cursor_ea() == db, f"{lst._cursor_ea():#x} vs {db:#x}")
+ await pilot.press(">")
+ await pilot.pause(1.0)
+ check("> seeks to the next execution of it",
+ app._t == stamps[1], f"t={app._t}, expected {stamps[1]}")
+ status = str(app.query_one("#status", Static).render())
+ check("and says which execution this is",
+ f"2 of {len(stamps)}" in status, status[:80])
+ lst.cursor = row
+ await pilot.pause(0.2)
+ await pilot.press("<")
+ await pilot.pause(1.0)
+ check("< seeks back to the previous one",
+ app._t == stamps[0], f"t={app._t}, expected {stamps[0]}")
+ # An edge must SAY it's an edge rather than silently doing
+ # nothing, which is indistinguishable from a broken key.
+ lst.cursor = row
+ await pilot.pause(0.2)
+ await pilot.press("<")
+ await pilot.pause(1.0)
+ status = str(app.query_one("#status", Static).render())
+ check("and the first execution says so instead of moving",
+ app._t == stamps[0] and "first" in status, status[:80])
+
+ # -- "which instruction set this register?" ---------------------- #
+ app._seek(min(200, t.length - 1))
+ await pilot.pause(1.0)
+ lst.focus()
+ await pilot.press("W")
+ opened = await wait(lambda: isinstance(app.screen, RegWriteScreen),
+ pilot, 20)
+ check("W lists the registers and where each was set", opened,
+ f"screen={type(app.screen).__name__}")
+ if opened:
+ sc = app.screen
+ pick = next((k for k, (n, v, l, x) in enumerate(sc._rows)
+ if l is not None and l != app._t), None)
+ if pick is None:
+ check("a register was set by an earlier instruction", False)
+ await pilot.press("escape")
+ else:
+ name, _v, last, _n = sc._rows[pick]
+ sc.query_one(OptionList).highlighted = pick
+ await pilot.pause(0.3)
+ await pilot.press("enter")
+ await wait(lambda: app._t == last, pilot, 30)
+ check("choosing one seeks to the write that set it",
+ app._t == last, f"t={app._t}, expected {last}")
+ # The real check: that instruction must actually have
+ # written the register we asked about.
+ check("and that instruction really wrote it",
+ name in t.changed(app._t),
+ f"{name} not in {sorted(t.changed(app._t))}")
+
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0