diff options
| author | blasty <blasty@local> | 2026-08-10 01:18:27 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-10 01:18:27 +0200 |
| commit | 70a1fc453f64aad246e1074df3303b275a9e3032 (patch) | |
| tree | f05bfca9c1050376d3bb06a71b37dc2b08576d54 /tests | |
| parent | domain: build Head positionally, and drop a dead test from _as_int (diff) | |
| download | ida-tui-70a1fc453f64aad246e1074df3303b275a9e3032.tar.gz ida-tui-70a1fc453f64aad246e1074df3303b275a9e3032.tar.xz ida-tui-70a1fc453f64aad246e1074df3303b275a9e3032.zip | |
segment_index: the row total in one call instead of 458
The listing streams a whole segment for one reason -- to know how many rows
it has, so the scrollbar and paging are right. Even as skeletons that is 458
round trips and 227k rows for a 1.2MB bash, none of which is displayed.
segment_index walks the same items and counts what heads() WOULD emit,
building none of them, and returns the total plus [row, ea] anchors every 500
rows. Measured on bash, same process and database:
segment_index : 228,659 rows, 1 call, 501ms
streaming : 228,659 rows, 458 calls, 1836ms 3.7x
Exactness is the whole point, so it mirrors _rows_for's arithmetic rather
than approximating it: 3 banner rows at a function start, a label row for a
named code head that is not one, the head row, struct members for data, 2
footer rows at a function end, and an undefined run counted as its byte
length because the client presents one collapsed row as that many logical
rows. A count that is off by a handful means the scrollbar lies and a seek
lands on the wrong row, so the test compares against a fully streamed model
in the same process rather than against a tolerance, and checks that every
anchor names the address of the row it claims.
Getting that right took a false alarm worth recording: the count first looked
35 rows short of a model built by the pilot, which turned out to be a
DIFFERENT DATABASE (tests run on a pristine scratch copy). Against the same
database it matches exactly, head for head, with zero differing addresses.
Nothing consumes this yet. Spending it means teaching ListingModel to hold
sparse pages seeked through the anchors instead of one dense array grown from
the segment start, which is a real change to the core view and wants its own
run at it.
Full gate: 1054 passed.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_scenarios.py | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 780459a..b256b80 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -412,6 +412,54 @@ async def s_auto_land(c: Ctx): c.check("auto-land is idempotent (guarded)", app._cur is prev) +@scenario("segment_index") +async def s_segment_index(c: Ctx): + """segment_index must count EXACTLY what streaming the pages produces. + + It exists so the listing can know its row total without fetching every row + (1 call and ~0.5s instead of 458 calls and ~1.8s on bash). That is only + usable if the number is exact: the total sizes the scrollbar, and the + anchors are what a future "seek to row N" would jump through, so being off + by a handful of rows means the bar lies and a jump lands in the wrong place. + + Approximating it is the tempting mistake, which is why this compares against + the real thing rather than a tolerance. + """ + app = c.app + await c.open_biggest("listing") + lv = app.query_one(ListingView) + model = lv.model + if model is None: + c.check("listing model exists", False) + return + await c.wait(lambda: model.complete, 30) + if not model.complete: + c.check("segment streamed for comparison", False) + return + + idx = app.program.client.invoke("segment_index", addr=hex(model.seg_start)) + c.check("segment_index counts exactly what streaming produced", + idx.get("rows") == len(model), + f"index={idx.get('rows')} streamed={len(model)}") + c.check("it reports the same segment", + int(str(idx.get("addr")), 16) == model.seg_start, + f"{idx.get('addr')} vs {model.seg_start:#x}") + anchors = idx.get("anchors") or [] + c.check("anchors cover the segment", + len(anchors) >= max(1, len(model) // 500), + f"{len(anchors)} anchors for {len(model)} rows") + + # Every anchor must name the address of the row it claims, or seeking to it + # would land somewhere else entirely. + bad = [] + for row, ea in anchors: + h = model.get(row) + if h is None or h.ea != int(str(ea), 16): + bad.append((row, ea, hex(h.ea) if h else None)) + c.check("every anchor points at the row it claims", not bad, + f"{len(bad)} wrong, first={bad[:2]}") + + @scenario("skeleton_pages") async def s_skeleton_pages(c: Ctx): """The background grower loads text-less pages; reading one must fill it in. |
