aboutsummaryrefslogtreecommitdiffstats
path: root/experiments/graph_smoke.py
diff options
context:
space:
mode:
Diffstat (limited to 'experiments/graph_smoke.py')
-rw-r--r--experiments/graph_smoke.py107
1 files changed, 107 insertions, 0 deletions
diff --git a/experiments/graph_smoke.py b/experiments/graph_smoke.py
new file mode 100644
index 0000000..1b6e21e
--- /dev/null
+++ b/experiments/graph_smoke.py
@@ -0,0 +1,107 @@
+"""End-to-end smoke for the graph path: the `flowchart` tool -> domain ->
+idatui.graph layout, through a real idalib worker.
+
+ ~/ida-venv/bin/python experiments/graph_smoke.py [funcname]
+
+Wants: VERDICT: OK
+"""
+import os
+import shutil
+import sys
+import time
+
+REPO = os.path.expanduser("~/dev/ida-tui-maybe")
+sys.path.insert(0, REPO)
+os.chdir(REPO)
+
+src, tmp = f"{REPO}/targets/echo", "/tmp/echo_graph"
+shutil.copy(src, tmp)
+for e in ".i64 .id0 .id1 .id2 .nam .til".split():
+ try:
+ os.remove(tmp + e)
+ except OSError:
+ pass
+
+from idatui.worker_client import WorkerClient # noqa: E402
+from idatui.domain import Program # noqa: E402
+from idatui import graph as G # noqa: E402
+
+want = sys.argv[1] if len(sys.argv) > 1 else "main"
+print("spawning worker + opening echo\u2026", flush=True)
+t = time.time()
+cl = WorkerClient(tmp)
+cl.connect(progress=lambda m: None)
+print(f" worker ready in {time.time()-t:.2f}s", flush=True)
+prog = Program(cl)
+
+ok = True
+ea = prog.resolve(want)
+print(f"resolve({want!r}) = {ea:#x}", flush=True)
+
+t = time.time()
+fc = prog.flowchart(ea)
+print(f"flowchart() -> {time.time()-t:.2f}s", flush=True)
+if fc is None:
+ print("VERDICT: FAIL (no flowchart)")
+ raise SystemExit(1)
+
+print(f" {fc.name} @ {fc.func_ea:#x}: {len(fc.blocks)} blocks, entry={fc.entry}")
+nrows = sum(len(b.rows) for b in fc.blocks)
+print(f" {nrows} listing rows attached")
+ok &= nrows > 0
+if not nrows:
+ print(" !! no rows attached to any block")
+
+empty = [b for b in fc.blocks if not b.rows]
+if empty:
+ ok = False
+ print(f" !! {len(empty)} blocks have NO rows, e.g. "
+ f"{[hex(b.start) for b in empty[:4]]}")
+
+b0 = fc.blocks[fc.entry]
+print(f" entry block {b0.start:#x}-{b0.end:#x}:")
+for h in b0.rows[:5]:
+ tags = ",".join(k for k, _ in (h.spans or ()))[:40]
+ print(f" {h.ea:#010x} {h.kind:<7} {h.text[:42]!r} spans[{tags}]")
+ok &= any(h.spans for b in fc.blocks for h in b.rows)
+if not any(h.spans for b in fc.blocks for h in b.rows):
+ print(" !! no colour spans came through \u2014 highlighting would be dead")
+
+# rows must tile the block exactly, or boxes will have holes
+for b in fc.blocks:
+ eas = [h.ea for h in b.rows]
+ if eas and (min(eas) < b.start or max(eas) >= b.end):
+ ok = False
+ print(f" !! block {b.start:#x} has rows outside its range")
+
+blocks = [G.Block(id=b.id, start=b.start, end=b.end, succs=list(b.succs))
+ for b in fc.blocks]
+
+
+def sizer(b):
+ src_b = fc.blocks[b.id]
+ w = max([len(f"loc_{b.start:X}")]
+ + [len(h.text) + 12 for h in src_b.rows]) + 4
+ return (w, len(src_b.rows) + 3)
+
+
+t = time.time()
+lay = G.layout(blocks, sizer, entry=fc.entry)
+print(f"layout() -> {(time.time()-t)*1000:.0f} ms {lay.stats}", flush=True)
+print(f" canvas {lay.width}x{lay.height}")
+ok &= len(lay.nodes) == len(blocks)
+
+# every block must be reachable in the drawing, and rows must index it
+covered = {n.id for r in range(lay.height) for n in lay.nodes_at_row(r)}
+ok &= covered == {b.id for b in blocks}
+if covered != {b.id for b in blocks}:
+ print(f" !! row index misses {sorted({b.id for b in blocks} - covered)[:5]}")
+
+hits = sum(1 for r in range(min(lay.height, 400))
+ if lay.painting.cells_at_row(r, 0, lay.width))
+print(f" {hits} of the first {min(lay.height,400)} rows carry edge cells")
+ok &= hits > 0
+
+cl.close()
+print("VERDICT:", "OK" if ok else "FAIL")
+raise SystemExit(0 if ok else 1)