diff options
| author | blasty <blasty@local> | 2026-07-25 23:03:10 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 23:03:10 +0200 |
| commit | b8d9aa4fce78f4523cffaba8b46095d2dfb66310 (patch) | |
| tree | 97e73ec6b027a19cbeafe026aaf8b90bad0773d1 /tests/test_project_ui.py | |
| parent | projects phase 3: follow an import into the binary that implements it (diff) | |
| download | ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.gz ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.xz ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.zip | |
projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs
Three things, all following from phase 3 making cross-binary jumps ordinary.
**A cross-binary jump was a one-way door.** Nav history is per-binary, so
arriving in another binary — a project search hit, or now following an import
into the library that implements it — landed you in an empty history with nothing
to take you back. _switch_then_goto records the binary it came FROM, and
action_back falls through to that hop once local history is spent: Esc walks back
through the function you were in, then the binary you were in. Manual Ctrl+O
switching records nothing, because that isn't navigation.
**Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms
the binary providing the most of this one's imports — where a follow is most
likely to go, so its startup is paid before you ask for it. "Next in the list"
would have been arbitrary; phase 3 gave us something better to ask.
pool.prewarm() refuses rather than making room. Evicting a binary the user
visited to speculatively load one they haven't is a straight downgrade, and it
throws away that binary's caches as well; at a tight budget pre-warm just does
nothing. The cost of a worker that doesn't exist yet can only be estimated, so it
uses the largest resident one (same program, different database) — and if that
estimate proves wrong, the speculative worker is the one evicted, never a chosen
one.
**Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists
the inventory (active / resident / indexed / where Esc returns to) and `switch
{binary,addr?}` makes another active — with an address it takes the search-hit
path, so it records a hop. state gains `binary` and `hops`, which it should have
had the moment project mode existed.
Verified on real sessions: drive binaries/switch against an echo+cat project
pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc
(provider of echo's imports) and warming it after an evict.
tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts
nothing when refusing, ignores unknown labels) and +4 project UI (jump records
the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with
the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool,
33/0 project.
Left open: project-level persistence across sessions.
Diffstat (limited to 'tests/test_project_ui.py')
| -rw-r--r-- | tests/test_project_ui.py | 33 |
1 files changed, 33 insertions, 0 deletions
diff --git a/tests/test_project_ui.py b/tests/test_project_ui.py index 731811d..47e511a 100644 --- a/tests/test_project_ui.py +++ b/tests/test_project_ui.py @@ -168,6 +168,39 @@ async def run(bins): await pilot.press("escape") await pilot.pause(0.2) + # -- a cross-binary jump is not a one-way door ----------------- # + # Nav history is per-binary, so arriving in another binary lands you + # in an empty history. Esc must fall through to the binary you came + # from, or a project search hit (and, since phase 3, following an + # import) strands you. + here, there = app._binary, (first if app._binary == second else second) + hops0 = len(app._hops) + 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") + 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}") + # spend the local history first, then Esc must cross back + for _ in range(6): + if not app._hops or app._binary != there: + break + await pilot.press("escape") + await pilot.pause(0.6) + 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}") + # -- and the same toggle for strings --------------------------- # from idatui.app import StringsPalette await pilot.press("quotation_mark") |
