aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_launch.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_launch.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_launch.py')
-rw-r--r--tests/test_launch.py124
1 files changed, 21 insertions, 103 deletions
diff --git a/tests/test_launch.py b/tests/test_launch.py
index c6ed8f1..d57ee5f 100644
--- a/tests/test_launch.py
+++ b/tests/test_launch.py
@@ -1,12 +1,14 @@
#!/usr/bin/env python3
-"""The launcher's file handling -- the part that deletes things.
+"""The launcher's option handling, and the file handling it must NOT do.
-`_sweep_locks` runs automatically when a database fails to open, and it removes
-files next to the user's binary. That is exactly the kind of code that must not
-be tested by trying it, so it is tested here: which files it takes, which it
-must never take, and what it reports.
+The old `_sweep_locks` deleted `.id0/.id1/.id2/.nam/.til` next to the user's
+binary when a database failed to open. That was only defensible while the TUI
+exclusively owned a private worker; under Code Mode a GUI or another client may
+own the database, so the sweep is gone. Its tests are replaced by one that keeps
+it gone -- deleting a shared database's working files is unrecoverable, and this
+is the cheapest guard against someone reintroducing the "helpful" cleanup.
-Pure: no IDA, no worker, no Textual.
+Pure: no IDA, no Code Mode library, no Textual.
"""
from __future__ import annotations
@@ -20,7 +22,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
#: Read by tests/run.py (--fast skips every NEEDS_IDA file).
NEEDS_IDA = False
-from idatui.launch import _LOCK_SUFFIXES, _load_args, _sweep_locks # noqa: E402
+import idatui.launch as launch # noqa: E402
+from idatui.launch import _load_args # noqa: E402
PASS = FAIL = 0
@@ -41,100 +44,18 @@ def touch(*paths):
fh.write(b"x")
-def t_sweeps_the_scratch_files():
- """IDA unpacks a .i64 into .id0/.id1/.id2/.nam/.til while it is open; a
- hard-killed worker leaves them and the .i64 then refuses to reopen."""
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "echo")
- touch(binary, *[binary + s for s in _LOCK_SUFFIXES])
- n = _sweep_locks(binary)
- check("every unpacked scratch file is swept", n == len(_LOCK_SUFFIXES),
- f"swept {n} of {len(_LOCK_SUFFIXES)}")
- check("none of them survive",
- not any(os.path.exists(binary + s) for s in _LOCK_SUFFIXES))
- check("the binary itself is untouched", os.path.exists(binary))
+def t_no_lock_sweeping():
+ """The launcher must not delete database working files any more.
-
-def t_sweeps_by_stem_too():
- """IDA keys the scratch on the full name or the stem depending on how the
- database was created, so both are swept."""
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "prog.elf")
- stem = os.path.join(d, "prog")
- touch(binary, stem + ".id0", stem + ".nam", binary + ".id1")
- n = _sweep_locks(binary)
- check("scratch named after the stem is swept too", n == 3, f"n={n}")
- check("stem-keyed files are gone",
- not os.path.exists(stem + ".id0")
- and not os.path.exists(stem + ".nam"))
- check("full-name-keyed files are gone", not os.path.exists(binary + ".id1"))
- check("the binary itself is untouched", os.path.exists(binary))
-
-
-def t_never_the_database():
- """The .i64 IS the database. Nothing is saved unless idb_save was called, so
- deleting it throws away every rename and comment in the session."""
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "echo")
- db = binary + ".i64"
- stem_db = os.path.join(d, "echo.i64")
- touch(binary, db, binary + ".id0")
- _sweep_locks(binary)
- check("the .i64 is never swept", os.path.exists(db))
- check("nor the stem-keyed .i64", os.path.exists(stem_db))
- check(".i64 is not in the suffix list", ".i64" not in _LOCK_SUFFIXES,
- str(_LOCK_SUFFIXES))
-
-
-def t_never_the_input_itself():
- """`.til` is both an unpacked-DB suffix and the extension of an IDA type
- library, so `ida-tui mylib.til` used to sweep its own argument out of
- existence -- irreversibly, on a path that runs automatically when an open
- fails. Same for anything named *.id0/*.id1/*.id2/*.nam.
+ Code Mode's registry locks, health probes and IDA itself arbitrate database
+ ownership now. A sweep here would delete files out from under a live GUI.
"""
- for suf in _LOCK_SUFFIXES:
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "mylib" + suf)
- touch(binary)
- _sweep_locks(binary)
- check(f"a binary named *{suf} is not deleted by its own sweep",
- os.path.exists(binary), f"{binary} was removed")
-
-
-def t_relative_path_is_still_the_input():
- """The guard compares absolute paths -- a relative argument names the same
- file and must be protected the same way."""
- with tempfile.TemporaryDirectory() as d:
- cwd = os.getcwd()
- try:
- os.chdir(d)
- touch("mylib.til")
- _sweep_locks("mylib.til")
- check("a relative path to the input is protected too",
- os.path.exists("mylib.til"))
- finally:
- os.chdir(cwd)
-
-
-def t_missing_files_are_fine():
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "nothing-here")
- touch(binary)
- n = _sweep_locks(binary)
- check("sweeping with nothing to sweep reports 0", n == 0, f"n={n}")
- check("and does not raise", True)
-
-
-def t_leaves_the_neighbours_alone():
- with tempfile.TemporaryDirectory() as d:
- binary = os.path.join(d, "echo")
- other = os.path.join(d, "other.id0") # another binary's scratch
- src = os.path.join(d, "echo.c")
- touch(binary, other, src, binary + ".id0")
- _sweep_locks(binary)
- check("another binary's scratch is left alone", os.path.exists(other))
- check("unrelated neighbours are left alone", os.path.exists(src))
- check("our own scratch is still swept", not os.path.exists(binary + ".id0"))
+ check("_sweep_locks is gone", not hasattr(launch, "_sweep_locks"))
+ check("the scratch-suffix list is gone", not hasattr(launch, "_LOCK_SUFFIXES"))
+ src = open(launch.__file__, encoding="utf-8").read()
+ check("the launcher does not remove files at all",
+ "os.remove" not in src and "shutil.rmtree" not in src,
+ "launch.py deletes something again")
def t_load_args():
@@ -154,10 +75,7 @@ def t_load_args():
def main() -> int:
- for fn in (t_sweeps_the_scratch_files, t_sweeps_by_stem_too,
- t_never_the_database, t_never_the_input_itself,
- t_relative_path_is_still_the_input, t_missing_files_are_fine,
- t_leaves_the_neighbours_alone, t_load_args):
+ for fn in (t_no_lock_sweeping, t_load_args):
print(f"\n{fn.__name__}")
try:
fn()