From 8c9490c0cc1712f61dfdd2efaaee93d33e1825f9 Mon Sep 17 00:00:00 2001 From: blasty Date: Thu, 6 Aug 2026 23:48:04 +0200 Subject: tests: a guard that actually regresses on the resync storm The split resync loop (f898350) had no test. Two attempts at one were worthless and are not in this commit: a scroll-based guard passed with the bug reintroduced, and a constructed anchor -- inside the loaded function, outside its mapped span -- skipped, because on this target the map covers the whole function. The real trigger is the race window while the decomp map lags the decompiler re-pointing, which is tedious to force but wide open in split_view's own flow. So split_view counts lookup_funcs across its body and bounds it. Verified both ways, which is the only reason it's worth having: 29,227 calls with the bug put back, under 500 with the fix. The bound is loose because the bug was three orders of magnitude out, not a near miss. 733 checks, 139s. --- README.md | 10 +++++++--- tests/test_scenarios.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d857989..bc514c5 100644 --- a/README.md +++ b/README.md @@ -198,12 +198,16 @@ See `docs/RPC.md` for the full protocol. `tests/run.py` is the front door — it runs every suite and prints one table: ```sh -python3 tests/run.py # everything (needs IDA; ~3 min) -python3 tests/run.py --fast # only the no-IDA suites — ~0.5s, runs anywhere -python3 tests/run.py --list # what would run, and whether it needs IDA +python3 tests/run.py --fast # 257 checks, ~0.5s, any python3 — between edits python3 tests/run.py trace -x # only files matching "trace", stop at first failure +python3 tests/run.py # all 733 checks, ~2m20s — before a commit +python3 tests/run.py --list # what would run, and whether it needs IDA ``` +It runs the suites **serially on purpose**: idalib contends hard enough that +running them 4-up took the suite from 153s to 296s and got three of them killed +mid-analysis. See the note in `tests/run.py`. + Test files come in two kinds and **each one declares which** with a module-level `NEEDS_IDA` marker (`run.py` reads it without importing the file, and refuses to run if a file doesn't have one): diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index a1ffaad..6c22c7c 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -635,6 +635,33 @@ async def s_strings(c: Ctx): @scenario("split_view") async def s_split_view(c: Ctx): app, lst, dec = c.app, c.lst, c.dec + # Count worker lookups across this whole scenario. _sync_split used to bounce + # off _apply_resync and back for as long as the decomp map lagged the + # decompiler -- one thread worker and one lookup_funcs round trip per + # iteration, 23,665 of them in this scenario alone (four distinct + # addresses; 21,156 for one of them), and an idle split view pegging the + # worker in the live app. The race window is real work to reproduce + # deliberately, but it is wide open in the flow below, so the cheap guard is + # to count. The bound is loose because the bug was three orders of magnitude + # out, not a near miss. + _lookups = {"n": 0} + _orig_call = c.prog.client.call + + def _counting(name, *a, **kw): + if name == "lookup_funcs": + _lookups["n"] += 1 + return _orig_call(name, *a, **kw) + + c.prog.client.call = _counting + try: + await _split_view_body(c, app, lst, dec) + finally: + c.prog.client.call = _orig_call + c.check("split view doesn't storm the worker with function lookups", + _lookups["n"] < 500, f"{_lookups['n']} lookup_funcs calls") + + +async def _split_view_body(c: Ctx, app, lst, dec): await c.open_biggest("listing") await c.press("s") shown = await c.wait(lambda: app._split and lst.display and dec.display, 20) -- cgit v1.3.1-sl0p