aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_scenarios.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-07 15:14:30 +0200
committeruser <user@clank>2026-08-07 15:14:30 +0200
commit72fce7da1a1fd6527e389ffeb0f951157523589a (patch)
tree3f08a83f0d99f3d3fc7069b7be00d235bab82817 /tests/test_scenarios.py
parentStop tracking 157MB of core dumps, and ignore them (diff)
parentdocs: upstream findings for the ida-codemode maintainers (diff)
downloadida-tui-72fce7da1a1fd6527e389ffeb0f951157523589a.tar.gz
ida-tui-72fce7da1a1fd6527e389ffeb0f951157523589a.tar.xz
ida-tui-72fce7da1a1fd6527e389ffeb0f951157523589a.zip
Merge the IDA Code Mode port
Replaces the private idalib worker (idatui/worker.py + worker_client.py, with server/patch_server.py injecting tools into ida-pro-mcp) with an ordinary client of ida_codemode.client.DatabaseHandle. A database open by an IDA GUI is reused; otherwise Code Mode starts or shares a managed idalib worker. The TUI no longer owns an IDA process, and closing it releases only its lease. Based on Duncan Ogilvie's port, rebased onto ~150 commits of local work it predated. The rebase itself was mechanical; landing it was not. Nine defects had to be fixed before the feature set was whole again, none of which the patch's own tests could catch: - DatabaseHandle.open() takes image_base, not loading_address: every connect() would have raised TypeError on the first call - five operations our tree had grown were simply missing (flowchart, so the graph view was dead; op_format/pc_nums/pc_num_format, so 'o'/'O' were; survey_binary) - set_comments wrote only the disassembly comment, so comments never appeared in pseudocode - xref_query returned rows in raw IDA order, and 'follow the call' silently followed the fall-through instead - rename accepted one edit per category, so bulk symbol import was dead - decompile ran decomp_map's full per-column ctree sweep to fill in a per-line address anchor - heads shipped without operand extents or the digest protocol - the package became unimportable without ida_codemode installed, which killed the offline test suites Verified against the pre-codemode tag rather than against assumptions: the full suite is 788 passed / 0 failed, and the pilot's 301 checks match the old backend exactly. Performance is within 2x on the listing hot path and faster on decompile, disasm and connect, after fixing two runtime costs that are documented for upstream in docs/CODEMODE_UPSTREAM.md. Test runtime came down from ~9m20s to 115s along the way -- not by removing checks, but by removing four kinds of waiting-on-a-guess that were also hiding real failures.
Diffstat (limited to 'tests/test_scenarios.py')
-rw-r--r--tests/test_scenarios.py34
1 files changed, 21 insertions, 13 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 9b2cc7e..e19ea11 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -694,18 +694,18 @@ async def s_split_view(c: Ctx):
# 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
+ _orig_call = c.prog.client.invoke
def _counting(name, *a, **kw):
if name == "lookup_funcs":
_lookups["n"] += 1
return _orig_call(name, *a, **kw)
- c.prog.client.call = _counting
+ c.prog.client.invoke = _counting
try:
await _split_view_body(c, app, lst, dec)
finally:
- c.prog.client.call = _orig_call
+ c.prog.client.invoke = _orig_call
c.check("split view doesn't storm the worker with function lookups",
_lookups["n"] < 500, f"{_lookups['n']} lookup_funcs calls")
@@ -1586,14 +1586,14 @@ async def s_decomp_nav(c: Ctx):
old_ea = dec._line_ea(drow)
if old_ea is not None and dsym in old_line:
tmp = f"stale_{os.getpid()}"
- app.program.client.call("rename", batch={"func": {"addr": hex(dstale), "name": tmp}})
+ app.program.client.invoke("rename", batch={"func": {"addr": hex(dstale), "name": tmp}})
app.program.bump_names()
d2 = len(app._nav)
app._follow_decomp(old_line, dsym, old_ea)
await c.wait(lambda: len(app._nav) > d2, 25)
c.check("decomp follow works with a stale name (ea-marker fallback)",
app._cur.ea == dstale, f"cur={app._cur.ea:#x} want={dstale:#x}")
- app.program.client.call("rename", batch={"func": {"addr": hex(dstale), "name": dsym}})
+ app.program.client.invoke("rename", batch={"func": {"addr": hex(dstale), "name": dsym}})
app.program.bump_names()
@@ -1674,7 +1674,7 @@ async def s_rename(c: Ctx):
c.check("rename updates the function name",
app._func_index.by_addr(dtarget).name == newname,
app._func_index.by_addr(dtarget).name)
- rr = app.program.client.call("rename", batch={"func": {"addr": hex(dtarget), "name": dsym}})
+ rr = app.program.client.invoke("rename", batch={"func": {"addr": hex(dtarget), "name": dsym}})
c.check("rename reverted cleanly",
rr.get("summary", {}).get("ok", 0) == 1, str(rr.get("summary")))
# goto label refuse
@@ -1724,7 +1724,7 @@ async def s_rename(c: Ctx):
and any(cnote in t for t in dec._texts), 25)
c.check("comment appears in the pseudocode after ';'",
any(cnote in t for t in dec._texts), "comment not shown")
- app.program.client.call("set_comments", items=[{"addr": hex(cea), "comment": ""}])
+ app.program.client.invoke("set_comments", items=[{"addr": hex(cea), "comment": ""}])
else:
c.check("found a pseudocode line to comment", False, "no marker line")
@@ -1766,7 +1766,7 @@ async def s_comment_func(c: Ctx):
la is not None and lb is not None and lb > la
and a not in dec._texts[lb],
f"la={la} lb={lb}")
- app.program.client.call("set_comments", items=[{"addr": hex(fn.addr), "comment": ""}])
+ app.program.client.invoke("set_comments", items=[{"addr": hex(fn.addr), "comment": ""}])
@scenario("retype")
@@ -2031,7 +2031,7 @@ async def s_rename_history(c: Ctx):
await c.pause(0.15)
c.check("caller pseudocode shows renamed callee after 'back'",
any(hnew in tx for tx in dec._texts), "pseudocode still stale")
- app.program.client.call("rename", batch={"func": {"addr": hex(htarget), "name": hsym}})
+ app.program.client.invoke("rename", batch={"func": {"addr": hex(htarget), "name": hsym}})
@scenario("region_define")
@@ -2250,7 +2250,7 @@ async def s_listing_name_addr(c: Ctx):
finally:
# revert: drop the label and restore raw bytes at A
try:
- c.prog.client.call("rename", batch={"data": {"addr": hex(A + 1), "new": ""}})
+ c.prog.client.invoke("rename", batch={"data": {"addr": hex(A + 1), "new": ""}})
except Exception: # noqa: BLE001
pass
c.prog.undefine(A, size=8)
@@ -2327,7 +2327,7 @@ async def s_listing_struct_expand(c: Ctx):
c.check("found a data address for the struct test", False)
return
try:
- c.prog.client.call(
+ c.prog.client.invoke(
"declare_type",
decls=["struct TuiExpandS { int a; char b[4]; short c; };"])
c.prog.make_data(A, "TuiExpandS")
@@ -3206,7 +3206,7 @@ async def s_graph_rename(c: Ctx):
f"resolve({new}) -> {got if got is None else hex(got)} want {ea:#x}")
# revert, so the suite stays idempotent
if got is not None:
- app.program.client.call(
+ app.program.client.invoke(
"rename", batch={"data": {"addr": hex(ea), "new": ""}})
app.program.bump_names()
@@ -3259,7 +3259,7 @@ async def run(binary, only=None):
async def _run_on(binary, only=None):
- # Own idalib worker: opens the binary in-process over a unix socket.
+ # Code Mode attaches a registered GUI or starts/reuses a managed worker.
app = IdaTui(open_path=binary, keepalive=False)
async with app.run_test(size=(140, 44)) as pilot:
c = Ctx(app, pilot)
@@ -3279,6 +3279,14 @@ async def _run_on(binary, only=None):
print(f"── {name} ({asyncio.get_event_loop().time() - _t0:.1f}s) CRASHED")
c.check("scenario did not crash", False, f"{type(e).__name__}: {e}")
traceback.print_exc()
+ # Headless run_test does not reliably emit App.Unmount; explicitly release
+ # the lease. Then wait through the managed worker's final-lease grace and
+ # IDB close so Windows can remove this suite's TemporaryDirectory safely.
+ if app.program is not None:
+ app.program.close()
+ if app.client is not None:
+ app.client.close()
+ await asyncio.to_thread(app.client.wait_released, 45.0)
def main(argv):