aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_codemode_client.py
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-08-14 00:26:19 +0200
committerblasty <peter@haxx.in>2026-08-14 00:26:19 +0200
commitdb3576d7b4174e78222203a7d5246c5ee080049b (patch)
tree6544667596d6209054eab7c8ce03cf31c3c7dad3 /tests/test_codemode_client.py
parentFix: rebuilding the row index on every switch back to the listing (diff)
downloadida-tui-db3576d7b4174e78222203a7d5246c5ee080049b.tar.gz
ida-tui-db3576d7b4174e78222203a7d5246c5ee080049b.tar.xz
ida-tui-db3576d7b4174e78222203a7d5246c5ee080049b.zip
Port to the ida-codemode 0.5.3+ public API
0.5.x turned ida_codemode.__all__ into a real public API and hid the rest: client.py -> handle.py, registry.py -> _registry.py + instances.py, resolver.py -> _resolver.py. Every import we had was from a module that no longer exists, so the TUI could not attach at all. - handle.entry -> handle.instance (RegistryEntry -> DatabaseInstance) - 30 keyword-only options -> DatabaseOpenOptions(...) passed as options= - IdbBusy -> DatabaseBusyError - InstanceDisconnected/ClientError -> DatabaseDisconnected/CodeModeConnectionError - our scan_instances()+idb_key() ownership walks -> find_database_owner() - our FileLock poking (_wait_for_entry_release) -> wait_database_released() - registry.discover_instances() -> discover_databases() + InstanceState _database_exists() is deleted with it: upstream now drops the loader switches itself when reopening an existing IDB (_resolver._build_worker_command), which is the same fix we had client-side. See docs/CODEMODE_UPSTREAM.md section 4 for the one invocation that still slips through. The offline contract suite has to keep running under a stdlib-only python3, where every Code Mode name is bound to None -- so it now injects a strict fake DatabaseOpenOptions and a real DatabaseBusyError exception alongside the fake handle. Without the latter, `except DatabaseBusyError` is `except None`, and the TypeError it raises masks whatever actually failed inside the try. The loader-option names moved inside the options dataclass, so the guard that caught `loading_address` vs `image_base` moved with them (_option_fields_are_real). uv.lock pins 0.6.1; ~/ida-venv and .venv are on 0.6.1 with the ida-domain 0.5.1 / zeromcp 1.8.0 floors it requires. Full gate: 1065 passed.
Diffstat (limited to 'tests/test_codemode_client.py')
-rw-r--r--tests/test_codemode_client.py62
1 files changed, 56 insertions, 6 deletions
diff --git a/tests/test_codemode_client.py b/tests/test_codemode_client.py
index 2f70ba3..5ab95e2 100644
--- a/tests/test_codemode_client.py
+++ b/tests/test_codemode_client.py
@@ -40,7 +40,7 @@ class FakeEntry:
class FakeHandle:
def __init__(self, path: str) -> None:
self.connected = True
- self.entry = FakeEntry(exe_path=path, idb_path=path + ".i64")
+ self.instance = FakeEntry(exe_path=path, idb_path=path + ".i64")
self.waited = None
self.saved = 0
self.closed = False
@@ -58,7 +58,7 @@ class FakeHandle:
def save_database(self):
self.saved += 1
- return {"saved": True, "idb_path": self.entry.idb_path}
+ return {"saved": True, "idb_path": self.instance.idb_path}
def close(self):
self.connected = False
@@ -76,6 +76,28 @@ class FakeDatabaseHandle:
return FakeHandle(path)
+@dataclass(frozen=True)
+class FakeOpenOptions:
+ """Stand-in for DatabaseOpenOptions when the library is not installed.
+
+ Deliberately STRICT (no **kwargs): an option the adapter invents would
+ raise here, and `_option_fields_are_real` checks the surviving names
+ against the real dataclass wherever it is importable.
+ """
+
+ spawn: bool = True
+ startup_timeout: float = 120.0
+ output_database: str | None = None
+ processor: str | None = None
+ image_base: int | None = None
+ file_type: str | None = None
+ new_database: bool = False
+
+
+class FakeBusy(Exception):
+ """Stand-in for DatabaseBusyError: `except None` is a TypeError."""
+
+
def _open_kwargs_are_real(sent: dict):
"""(ok, detail) for the kwargs the adapter passes to DatabaseHandle.open.
@@ -83,7 +105,7 @@ def _open_kwargs_are_real(sent: dict):
"""
try:
import inspect
- from ida_codemode.client import DatabaseHandle as Real
+ from ida_codemode import DatabaseHandle as Real
except ImportError:
return True, "ida_codemode not installed - signature not checked"
accepted = set(inspect.signature(Real.open).parameters)
@@ -91,6 +113,23 @@ def _open_kwargs_are_real(sent: dict):
return not unknown, f"open() rejects {unknown}"
+def _option_fields_are_real(options):
+ """(ok, detail) for the option names the adapter fills in.
+
+ The open() signature no longer names the loader options -- they moved
+ inside DatabaseOpenOptions -- so the `loading_address` class of bug now
+ hides there instead. Check it in the same way.
+ """
+ try:
+ import dataclasses
+ from ida_codemode import DatabaseOpenOptions as Real
+ except ImportError:
+ return True, "ida_codemode not installed - fields not checked"
+ accepted = {field.name for field in dataclasses.fields(Real)}
+ unknown = sorted({f.name for f in dataclasses.fields(options)} - accepted)
+ return not unknown, f"DatabaseOpenOptions rejects {unknown}"
+
+
def main() -> int:
proc, base, file_type = _parse_load_args("-parm:ARMv7-M -b800000 -TRaw")
check("legacy switches map to typed Code Mode options",
@@ -105,6 +144,12 @@ def main() -> int:
original = module.DatabaseHandle
module.DatabaseHandle = FakeDatabaseHandle
+ # The library's own names when it is installed; strict fakes when it is not
+ # (this file must keep running under a stdlib-only python3).
+ original_options = module.DatabaseOpenOptions
+ original_busy = module.DatabaseBusyError
+ module.DatabaseOpenOptions = original_options or FakeOpenOptions
+ module.DatabaseBusyError = original_busy or FakeBusy
try:
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "sample.bin")
@@ -116,10 +161,13 @@ def main() -> int:
handle = client._handle
check("connect delegates database discovery to DatabaseHandle.open",
FakeDatabaseHandle.opened == path and handle is not None)
+ options = FakeDatabaseHandle.kwargs["options"]
check("typed loader options cross the dependency boundary",
- FakeDatabaseHandle.kwargs["processor"] == "arm:ARMv7-A"
- and FakeDatabaseHandle.kwargs["image_base"] == 0x1000,
- FakeDatabaseHandle.kwargs)
+ options.processor == "arm:ARMv7-A"
+ and options.image_base == 0x1000,
+ options)
+ check("every open option exists in the real library",
+ *_option_fields_are_real(options))
# A fake that swallows **kwargs cannot catch a keyword the real
# library does not have -- which is exactly how this port shipped
# `loading_address` (the real name is `image_base`) and would have
@@ -145,6 +193,8 @@ def main() -> int:
client.wait_released(0) is False)
finally:
module.DatabaseHandle = original
+ module.DatabaseOpenOptions = original_options
+ module.DatabaseBusyError = original_busy
client = CodeModeClient(__file__)
try: