diff options
| author | blasty <peter@haxx.in> | 2026-08-14 00:26:19 +0200 |
|---|---|---|
| committer | blasty <peter@haxx.in> | 2026-08-14 00:26:19 +0200 |
| commit | db3576d7b4174e78222203a7d5246c5ee080049b (patch) | |
| tree | 6544667596d6209054eab7c8ce03cf31c3c7dad3 /tests/test_codemode_client.py | |
| parent | Fix: rebuilding the row index on every switch back to the listing (diff) | |
| download | ida-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.py | 62 |
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: |
