aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_graph.py
diff options
context:
space:
mode:
authoridatui <user@clank>2026-08-09 13:55:31 +0200
committeridatui <user@clank>2026-08-09 13:55:31 +0200
commitd7966c178047d190faba7100a40385eb00f5894b (patch)
treef4a2796d166adb41f85518601c6de44266102504 /tests/test_graph.py
parentGraph: name the interpreter when pytriskel is missing (diff)
downloadida-tui-d7966c178047d190faba7100a40385eb00f5894b.tar.gz
ida-tui-d7966c178047d190faba7100a40385eb00f5894b.tar.xz
ida-tui-d7966c178047d190faba7100a40385eb00f5894b.zip
Graph: fix the triskel fallbacks, and say why when it still falls back
"engine triskel -> native+triskel-failed" was unreadable and, worse, gave no reason: the cause went to a logger a TUI user never sees. Dumped all 400 functions of bin/ls (echo's 128 were not enough) and swept them at three zoom levels: 6 of 1200 layouts fell back, all of them my own _verify tripping over a detour that could not be placed. - the detour jumped to the nearest side of the FIRST box in the way, which in a dense layout is usually inside the next box along. It now collects every box the run passes and picks the nearest genuinely free line. - it skipped the first and last segments because they carry the port and the arrowhead. But that is exactly where the failures were: triskel is happy to park a block directly above its successor and drive the final approach straight through it. Those segments may now move ALONG their own box's border, which is free almost every time. - repairs are swept to a fixed point: moving one segment stretches its neighbours, which can push those into a box. 0/1200 fallbacks after that. Then the bigger corpus turned up a second, genuinely upstream defect: superimposing SESE regions can leave two blocks a couple of columns into each other (2 of ls's 400 functions, in float space, before rounding). Cosmetic in a PNG; here the boxes are made of text, so one block's disassembly overwrites another's. _verify now checks it and falls back, which is the right trade. Reporting, so this is never mute again: - stats["engine_error"] carries the reason, the status line shows it, and the label is "native (triskel failed)". - the corpus test asserts fallbacks are rare AND explained, rather than asserting they never happen. Also lowered AUTO_TRISKEL_MAX_BLOCKS 250 -> 180. Layout runs on every zoom keypress and triskel knees hard past ~175 blocks (174: 66ms, 233: 489ms, 329: 555ms). The old cap allowed a 489ms stall. The corpus timing check now measures only sizes `auto` can actually reach, plus a 5s ceiling so nothing blows up quadratically when forced.
Diffstat (limited to 'tests/test_graph.py')
-rw-r--r--tests/test_graph.py38
1 files changed, 36 insertions, 2 deletions
diff --git a/tests/test_graph.py b/tests/test_graph.py
index d882f94..96d4f85 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -211,14 +211,38 @@ def t_corpus(path: str) -> None:
print(f"\ncorpus: {len(recs)} functions from {path}")
worst_ms = 0.0
worst_name = ""
+ fellback: list[tuple[str, str | None]] = []
+ worst_any_ms = 0.0
+ worst_any_name = ""
t0 = time.perf_counter()
for rec in recs:
blocks = [G.Block(id=b["id"], start=b["start"], end=b["end"],
succs=[(d, k) for d, k in b["succs"]])
for b in rec["blocks"]]
lay = layout(blocks, sizer)
- if lay.stats["ms"] > worst_ms:
+ # A SILENT fallback is the failure mode that matters here: the engine
+ # under test quietly stops being the engine under test, and every
+ # invariant below then passes for the wrong reason. `ls` main (329
+ # blocks) used to fall back on all three zoom levels because a final
+ # approach was routed through the block above its target.
+ #
+ # Falling back is legitimate -- it is how an upstream layout defect is
+ # kept off the screen -- so this asserts it is rare and explained,
+ # not that it never happens.
+ if ENGINE != "auto" and lay.stats["engine"] != ENGINE:
+ fellback.append((rec["name"], lay.stats.get("engine_error")))
+ check(bool(lay.stats.get("engine_error")),
+ f"corpus {rec['name']}: a fallback must record its reason")
+ # Time the engine only on the functions it would actually be ASKED for.
+ # `auto` hands anything over AUTO_TRISKEL_MAX_BLOCKS to native, and the
+ # view refuses to draw past 400 blocks at all, so a forced triskel run
+ # on a 495-block monster times a call the app cannot make.
+ reachable = (ENGINE != "triskel"
+ or len(blocks) <= G.AUTO_TRISKEL_MAX_BLOCKS)
+ if reachable and lay.stats["ms"] > worst_ms:
worst_ms, worst_name = lay.stats["ms"], rec["name"]
+ if lay.stats["ms"] > worst_any_ms:
+ worst_any_ms, worst_any_name = lay.stats["ms"], rec["name"]
check(no_box_overlap(lay), f"corpus {rec['name']}: boxes must not overlap")
check(len(lay.nodes) == len(blocks),
f"corpus {rec['name']}: every block is placed")
@@ -230,7 +254,17 @@ def t_corpus(path: str) -> None:
total = (time.perf_counter() - t0) * 1000
print(f" laid out {len(recs)} functions in {total:.0f} ms "
f"(worst {worst_ms:.0f} ms: {worst_name})")
- check(worst_ms < 2000, f"corpus: worst layout under 2s ({worst_ms:.0f} ms)")
+ if fellback:
+ print(f" {len(fellback)} fell back to native:")
+ for name, why in fellback:
+ print(f" {name}: {why}")
+ check(len(fellback) <= max(2, len(recs) // 20),
+ f"corpus: {ENGINE} fell back on {len(fellback)}/{len(recs)} functions")
+ check(worst_ms < 2000, f"corpus: worst REACHABLE layout under 2s "
+ f"({worst_ms:.0f} ms: {worst_name})")
+ # Nothing may blow up quadratically even when forced past its own limits.
+ check(worst_any_ms < 5000, f"corpus: worst layout at any size under 5s "
+ f"({worst_any_ms:.0f} ms: {worst_any_name})")
def main() -> int: