diff options
| author | blasty <blasty@local> | 2026-07-29 23:39:58 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-29 23:39:58 +0200 |
| commit | 8188a495b2eed6f120e551e7bda7f16cbe97e952 (patch) | |
| tree | 803682a46d33389a37f640876b9c34f80a91999f /tests | |
| parent | trace: memory at time T — stack in the dock, live bytes in hex (diff) | |
| download | ida-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.py | 84 |
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 |
