ida-code 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ida_code-0.2.1 → ida_code-0.2.2}/CHANGELOG.md +14 -1
- {ida_code-0.2.1 → ida_code-0.2.2}/PKG-INFO +6 -2
- {ida_code-0.2.1 → ida_code-0.2.2}/README.md +4 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/pyproject.toml +2 -2
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/server.py +3 -2
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/session.py +39 -2
- ida_code-0.2.2/tests/test_e2e.py +79 -0
- ida_code-0.2.2/tests/test_session.py +125 -0
- ida_code-0.2.1/tests/test_session.py +0 -59
- {ida_code-0.2.1 → ida_code-0.2.2}/.gitignore +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/LICENSE +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/__init__.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/_search_utils.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/comments.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/config.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/doc_search.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/example_search.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/executor.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/guidelines.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/macho.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/prompts.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/snapshots.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/structures.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/undo.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/src/ida_code/variables.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/__init__.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_comments.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_doc_search.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_example_search.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_executor.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_macho.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_search_utils.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_structures.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_undo.py +0 -0
- {ida_code-0.2.1 → ida_code-0.2.2}/tests/test_variables.py +0 -0
|
@@ -4,7 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.2.2] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **End-to-end regression test** — `tests/test_e2e.py` opens a real binary through fastmcp's in-process `Client` + `FastMCPTransport`, asserting the call returns within 15s. Catches future regressions that route idalib off the main thread (which would hang). Auto-skips when idalib isn't available.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Pin fastmcp back to `>=2.0,<3`** — v3 dispatches sync tool functions to `anyio.to_thread.run_sync`, but idalib hangs indefinitely when called from a non-main thread, so every idalib-touching tool wedged. v2 runs sync tools on the main thread and works in 0.7s on the same call. Reverts the v2→v3 bump from 0.2.1.
|
|
16
|
+
- **`open_database` refuses paths with unpacked fragments present** — if `.id0`/`.id1`/`.id2`/`.nam`/`.til` files exist for the target and `overwrite=False`, raise a clear `ToolError` listing them instead of letting idalib return an opaque `-1`. The message warns that the fragments may belong to another active IDA instance and only suggests `overwrite=True` if nothing else owns them.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`open_database` overwrite cleans up unpacked fragments** — `overwrite=True` now also deletes `.id0`, `.id1`, `.id2`, `.nam`, and `.til` files, not just `.i64`/`.idb`. A failed open could leave these partial fragments behind, after which every subsequent attempt would fail immediately with a generic `-1` because IDA refused to overwrite the half-written unpacked database.
|
|
8
21
|
|
|
9
22
|
## [0.2.1] - 2026-05-05
|
|
10
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ida-code
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: MCP server for AI-assisted IDAPython scripting via idalib
|
|
5
5
|
Project-URL: Homepage, https://github.com/Dil4rd/ida-code
|
|
6
6
|
Project-URL: Repository, https://github.com/Dil4rd/ida-code
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Security
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Disassemblers
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: fastmcp<
|
|
24
|
+
Requires-Dist: fastmcp<3,>=2.0
|
|
25
25
|
Requires-Dist: lief>=0.15
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -156,6 +156,10 @@ uv sync --extra dev
|
|
|
156
156
|
uv run pytest
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
## Known issues
|
|
160
|
+
|
|
161
|
+
See [KNOWN_ISSUES.md](KNOWN_ISSUES.md) for caveats and workarounds (e.g. why we pin `fastmcp<3`).
|
|
162
|
+
|
|
159
163
|
The test suite covers the executor, doc/example search, comments, snapshots, structures, undo, variables, and Mach-O parsing. Tests that need idalib are skipped if it's not available.
|
|
160
164
|
|
|
161
165
|
## Credits
|
|
@@ -127,6 +127,10 @@ uv sync --extra dev
|
|
|
127
127
|
uv run pytest
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
## Known issues
|
|
131
|
+
|
|
132
|
+
See [KNOWN_ISSUES.md](KNOWN_ISSUES.md) for caveats and workarounds (e.g. why we pin `fastmcp<3`).
|
|
133
|
+
|
|
130
134
|
The test suite covers the executor, doc/example search, comments, snapshots, structures, undo, variables, and Mach-O parsing. Tests that need idalib are skipped if it's not available.
|
|
131
135
|
|
|
132
136
|
## Credits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ida-code"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "MCP server for AI-assisted IDAPython scripting via idalib"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Topic :: Software Development :: Disassemblers",
|
|
21
21
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
22
|
]
|
|
23
|
-
dependencies = ["fastmcp>=
|
|
23
|
+
dependencies = ["fastmcp>=2.0,<3", "lief>=0.15"]
|
|
24
24
|
|
|
25
25
|
[project.urls]
|
|
26
26
|
Homepage = "https://github.com/Dil4rd/ida-code"
|
|
@@ -66,8 +66,9 @@ def open_database(
|
|
|
66
66
|
Returns summary info (architecture, segments, entry points, function count).
|
|
67
67
|
If a database is already open, it is closed first.
|
|
68
68
|
|
|
69
|
-
Set overwrite=True to delete any existing .i64/.idb database and
|
|
70
|
-
|
|
69
|
+
Set overwrite=True to delete any existing .i64/.idb database (and any
|
|
70
|
+
unpacked .id0/.id1/.id2/.nam/.til fragments left by a failed open)
|
|
71
|
+
and force a fresh analysis from the original binary.
|
|
71
72
|
|
|
72
73
|
*timeout* limits auto-analysis wait time in seconds (default 0 = unlimited).
|
|
73
74
|
When the timeout expires the database stays open with partial analysis
|
|
@@ -149,6 +149,17 @@ def open(
|
|
|
149
149
|
|
|
150
150
|
if overwrite:
|
|
151
151
|
_remove_existing_databases(open_path)
|
|
152
|
+
else:
|
|
153
|
+
fragments = _list_unpacked_fragments(open_path)
|
|
154
|
+
if fragments:
|
|
155
|
+
raise ToolError(
|
|
156
|
+
f"Unpacked database fragments exist: {fragments}. "
|
|
157
|
+
f"This usually means another IDA instance has this database "
|
|
158
|
+
f"open, or a previous open failed and left them behind. "
|
|
159
|
+
f"If no other IDA process is using this database, re-call "
|
|
160
|
+
f"with overwrite=True to clean them up — but doing so while "
|
|
161
|
+
f"another IDA has it open will destroy that session's work."
|
|
162
|
+
)
|
|
152
163
|
|
|
153
164
|
use_polling = auto_analysis and timeout > 0
|
|
154
165
|
run_auto = auto_analysis and not use_polling
|
|
@@ -263,11 +274,37 @@ def _collect_summary(path: str) -> dict:
|
|
|
263
274
|
}
|
|
264
275
|
|
|
265
276
|
|
|
277
|
+
_DB_EXTENSIONS = (".i64", ".idb", ".id0", ".id1", ".id2", ".nam", ".til")
|
|
278
|
+
_UNPACKED_FRAGMENT_EXTS = (".id0", ".id1", ".id2", ".nam", ".til")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _list_unpacked_fragments(path: str) -> list[str]:
|
|
282
|
+
"""Return any unpacked database fragments existing for *path*.
|
|
283
|
+
|
|
284
|
+
These exist while a database is open in IDA, and can also be left
|
|
285
|
+
behind when a previous open failed mid-unpacking. Their presence
|
|
286
|
+
makes ``idapro.open_database`` refuse the path with rc=-1.
|
|
287
|
+
"""
|
|
288
|
+
from pathlib import Path
|
|
289
|
+
p = Path(path)
|
|
290
|
+
found = []
|
|
291
|
+
for ext in _UNPACKED_FRAGMENT_EXTS:
|
|
292
|
+
for candidate in {p.with_suffix(ext), Path(str(p) + ext)}:
|
|
293
|
+
if candidate.is_file():
|
|
294
|
+
found.append(str(candidate))
|
|
295
|
+
return found
|
|
296
|
+
|
|
297
|
+
|
|
266
298
|
def _remove_existing_databases(path: str) -> None:
|
|
267
|
-
"""Remove existing IDA database files
|
|
299
|
+
"""Remove existing IDA database files and unpacked fragments.
|
|
300
|
+
|
|
301
|
+
Includes the unpacked fragment extensions (.id0/.id1/.id2/.nam/.til) so
|
|
302
|
+
that a half-written database from a previously failed open does not
|
|
303
|
+
block the next attempt.
|
|
304
|
+
"""
|
|
268
305
|
from pathlib import Path
|
|
269
306
|
p = Path(path)
|
|
270
|
-
for ext in
|
|
307
|
+
for ext in _DB_EXTENSIONS:
|
|
271
308
|
for candidate in {p.with_suffix(ext), Path(str(p) + ext)}:
|
|
272
309
|
if candidate.is_file():
|
|
273
310
|
candidate.unlink()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""End-to-end test exercising the MCP server through fastmcp's in-process
|
|
2
|
+
client. Regression guard: any change that pushes idalib calls onto a
|
|
3
|
+
worker thread (e.g. a future fastmcp version doing async-hygiene
|
|
4
|
+
dispatch of sync tools) hangs idalib and trips the asyncio timeout
|
|
5
|
+
below — failing fast instead of hanging CI indefinitely.
|
|
6
|
+
|
|
7
|
+
Auto-skipped when idalib isn't loadable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import shutil
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
# Importing ida_code.session has the side effect of inserting idalib's
|
|
18
|
+
# python dir into sys.path and `import idapro`. If idalib isn't present
|
|
19
|
+
# the import fails — skip the whole module instead of erroring.
|
|
20
|
+
try:
|
|
21
|
+
from ida_code import session
|
|
22
|
+
from ida_code.server import mcp
|
|
23
|
+
except ImportError as exc:
|
|
24
|
+
pytest.skip(f"idalib not available: {exc}", allow_module_level=True)
|
|
25
|
+
|
|
26
|
+
from fastmcp.client import Client # noqa: E402
|
|
27
|
+
from fastmcp.client.transports import FastMCPTransport # noqa: E402
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _venv_binary() -> Path:
|
|
31
|
+
"""A small native extension shipped in the venv — analyzes in ~2s cold."""
|
|
32
|
+
import charset_normalizer.md as _md
|
|
33
|
+
return Path(_md.__file__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def target(tmp_path):
|
|
38
|
+
src = _venv_binary()
|
|
39
|
+
if not src.is_file():
|
|
40
|
+
pytest.skip(f"venv binary not found: {src}")
|
|
41
|
+
dst = tmp_path / "target.so"
|
|
42
|
+
shutil.copy(src, dst)
|
|
43
|
+
yield str(dst)
|
|
44
|
+
# Make sure no database stays open across tests.
|
|
45
|
+
if session.get_state() == session.State.DATABASE_OPEN:
|
|
46
|
+
try:
|
|
47
|
+
session.close()
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _open_and_close(target_path: str) -> float:
|
|
53
|
+
async with Client(FastMCPTransport(mcp)) as client:
|
|
54
|
+
t0 = time.monotonic()
|
|
55
|
+
r = await asyncio.wait_for(
|
|
56
|
+
client.call_tool("open_database", {"path": target_path}),
|
|
57
|
+
timeout=30,
|
|
58
|
+
)
|
|
59
|
+
elapsed = time.monotonic() - t0
|
|
60
|
+
|
|
61
|
+
data = r.data if hasattr(r, "data") else r
|
|
62
|
+
assert isinstance(data, dict), f"unexpected result type: {type(data)}"
|
|
63
|
+
assert data.get("function_count", 0) > 0, f"no functions analyzed: {data}"
|
|
64
|
+
|
|
65
|
+
await asyncio.wait_for(
|
|
66
|
+
client.call_tool("close_database", {}),
|
|
67
|
+
timeout=10,
|
|
68
|
+
)
|
|
69
|
+
return elapsed
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_open_database_via_in_process_client(target):
|
|
73
|
+
"""Open a real binary through the MCP tool surface and verify it returns
|
|
74
|
+
a populated summary in well under the timeout. A future regression that
|
|
75
|
+
routes idalib off the main thread would hit the 30s ``wait_for``."""
|
|
76
|
+
elapsed = asyncio.run(_open_and_close(target))
|
|
77
|
+
# Generous bound: 16KB binary, ~2s cold standalone. Anything near the
|
|
78
|
+
# 30s ceiling means dispatch is wrong, not just slow.
|
|
79
|
+
assert elapsed < 15, f"open took {elapsed:.1f}s — possible thread regression"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Unit tests for ida_code.session.
|
|
2
|
+
|
|
3
|
+
These tests work without idalib — they mock the session module globals
|
|
4
|
+
and os.path.isfile to test the file-existence guard logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from fastmcp.exceptions import ToolError
|
|
13
|
+
|
|
14
|
+
from ida_code import session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestRequireOpen:
|
|
18
|
+
def setup_method(self):
|
|
19
|
+
"""Save and restore session module globals around each test."""
|
|
20
|
+
self._orig_state = session._state
|
|
21
|
+
self._orig_db_file_path = session._db_file_path
|
|
22
|
+
self._orig_orphaned = session._orphaned
|
|
23
|
+
|
|
24
|
+
def teardown_method(self):
|
|
25
|
+
session._state = self._orig_state
|
|
26
|
+
session._db_file_path = self._orig_db_file_path
|
|
27
|
+
session._orphaned = self._orig_orphaned
|
|
28
|
+
|
|
29
|
+
def test_raises_when_no_database(self):
|
|
30
|
+
session._state = session.State.NO_DATABASE
|
|
31
|
+
with pytest.raises(ToolError, match="No database is open"):
|
|
32
|
+
session.require_open()
|
|
33
|
+
|
|
34
|
+
@patch("ida_code.session.os.path.isfile", return_value=True)
|
|
35
|
+
def test_passes_when_open_and_file_exists(self, mock_isfile):
|
|
36
|
+
session._state = session.State.DATABASE_OPEN
|
|
37
|
+
session._db_file_path = "/tmp/test.i64"
|
|
38
|
+
session.require_open() # should not raise
|
|
39
|
+
mock_isfile.assert_called_once_with("/tmp/test.i64")
|
|
40
|
+
|
|
41
|
+
@patch("ida_code.session.os.path.isfile", return_value=False)
|
|
42
|
+
@patch("ida_code.executor.reset")
|
|
43
|
+
def test_resets_state_when_file_missing(self, mock_reset, mock_isfile):
|
|
44
|
+
session._state = session.State.DATABASE_OPEN
|
|
45
|
+
session._db_file_path = "/tmp/gone.i64"
|
|
46
|
+
session._orphaned = False
|
|
47
|
+
|
|
48
|
+
with pytest.raises(ToolError, match="moved or deleted"):
|
|
49
|
+
session.require_open()
|
|
50
|
+
|
|
51
|
+
assert session._state == session.State.NO_DATABASE
|
|
52
|
+
assert session._orphaned is True
|
|
53
|
+
assert session._db_file_path is None
|
|
54
|
+
mock_reset.assert_called_once()
|
|
55
|
+
|
|
56
|
+
def test_passes_when_db_file_path_is_none(self):
|
|
57
|
+
"""Graceful degradation: if _db_file_path was never set, skip the
|
|
58
|
+
file check and rely only on the state enum."""
|
|
59
|
+
session._state = session.State.DATABASE_OPEN
|
|
60
|
+
session._db_file_path = None
|
|
61
|
+
session.require_open() # should not raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestUnpackedFragmentPrecheck:
|
|
65
|
+
"""`open()` must refuse paths with leftover unpacked fragments unless
|
|
66
|
+
overwrite=True — calling idapro on such a path returns rc=-1 and can
|
|
67
|
+
leave idalib's internal state corrupted."""
|
|
68
|
+
|
|
69
|
+
def setup_method(self):
|
|
70
|
+
self._orig_state = session._state
|
|
71
|
+
|
|
72
|
+
def teardown_method(self):
|
|
73
|
+
session._state = self._orig_state
|
|
74
|
+
|
|
75
|
+
def test_lists_unpacked_fragments(self):
|
|
76
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
77
|
+
binary = os.path.join(tmp, "x.so")
|
|
78
|
+
open(binary, "w").close()
|
|
79
|
+
for ext in (".id0", ".id1", ".nam"):
|
|
80
|
+
open(binary + ext, "w").close()
|
|
81
|
+
found = session._list_unpacked_fragments(binary)
|
|
82
|
+
assert sorted(found) == sorted(binary + ext for ext in (".id0", ".id1", ".nam"))
|
|
83
|
+
|
|
84
|
+
def test_returns_empty_for_clean_path(self):
|
|
85
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
86
|
+
binary = os.path.join(tmp, "x.so")
|
|
87
|
+
open(binary, "w").close()
|
|
88
|
+
assert session._list_unpacked_fragments(binary) == []
|
|
89
|
+
|
|
90
|
+
def test_does_not_flag_packed_databases(self):
|
|
91
|
+
"""A `.i64` next to the binary is a valid warm cache, not a fragment."""
|
|
92
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
93
|
+
binary = os.path.join(tmp, "x.so")
|
|
94
|
+
open(binary, "w").close()
|
|
95
|
+
open(binary + ".i64", "w").close()
|
|
96
|
+
assert session._list_unpacked_fragments(binary) == []
|
|
97
|
+
|
|
98
|
+
def test_open_raises_with_fragments_no_overwrite(self):
|
|
99
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
100
|
+
binary = os.path.join(tmp, "x.so")
|
|
101
|
+
open(binary, "w").close()
|
|
102
|
+
open(binary + ".id0", "w").close()
|
|
103
|
+
|
|
104
|
+
session._state = session.State.NO_DATABASE
|
|
105
|
+
with patch("ida_code.session.idapro.open_database") as mock_open:
|
|
106
|
+
with pytest.raises(ToolError, match="Unpacked database fragments"):
|
|
107
|
+
session.open(binary, auto_analysis=False)
|
|
108
|
+
mock_open.assert_not_called() # idalib must not be invoked
|
|
109
|
+
|
|
110
|
+
def test_open_proceeds_with_fragments_when_overwrite_true(self):
|
|
111
|
+
"""overwrite=True clears fragments and proceeds with the open."""
|
|
112
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
113
|
+
binary = os.path.join(tmp, "x.so")
|
|
114
|
+
open(binary, "w").close()
|
|
115
|
+
open(binary + ".id0", "w").close()
|
|
116
|
+
|
|
117
|
+
session._state = session.State.NO_DATABASE
|
|
118
|
+
with patch("ida_code.session.idapro.open_database", return_value=0), \
|
|
119
|
+
patch("ida_code.session._collect_summary", return_value={"path": binary}), \
|
|
120
|
+
patch("ida_code.executor.reset"):
|
|
121
|
+
# Should not raise — fragment is cleaned up before open.
|
|
122
|
+
session.open(binary, auto_analysis=False, overwrite=True)
|
|
123
|
+
# Fragment file should have been removed.
|
|
124
|
+
assert not os.path.isfile(binary + ".id0")
|
|
125
|
+
session._state = session.State.NO_DATABASE # reset for teardown
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""Unit tests for ida_code.session.require_open().
|
|
2
|
-
|
|
3
|
-
These tests work without idalib — they mock the session module globals
|
|
4
|
-
and os.path.isfile to test the file-existence guard logic.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from unittest.mock import patch
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
from fastmcp.exceptions import ToolError
|
|
11
|
-
|
|
12
|
-
from ida_code import session
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestRequireOpen:
|
|
16
|
-
def setup_method(self):
|
|
17
|
-
"""Save and restore session module globals around each test."""
|
|
18
|
-
self._orig_state = session._state
|
|
19
|
-
self._orig_db_file_path = session._db_file_path
|
|
20
|
-
self._orig_orphaned = session._orphaned
|
|
21
|
-
|
|
22
|
-
def teardown_method(self):
|
|
23
|
-
session._state = self._orig_state
|
|
24
|
-
session._db_file_path = self._orig_db_file_path
|
|
25
|
-
session._orphaned = self._orig_orphaned
|
|
26
|
-
|
|
27
|
-
def test_raises_when_no_database(self):
|
|
28
|
-
session._state = session.State.NO_DATABASE
|
|
29
|
-
with pytest.raises(ToolError, match="No database is open"):
|
|
30
|
-
session.require_open()
|
|
31
|
-
|
|
32
|
-
@patch("ida_code.session.os.path.isfile", return_value=True)
|
|
33
|
-
def test_passes_when_open_and_file_exists(self, mock_isfile):
|
|
34
|
-
session._state = session.State.DATABASE_OPEN
|
|
35
|
-
session._db_file_path = "/tmp/test.i64"
|
|
36
|
-
session.require_open() # should not raise
|
|
37
|
-
mock_isfile.assert_called_once_with("/tmp/test.i64")
|
|
38
|
-
|
|
39
|
-
@patch("ida_code.session.os.path.isfile", return_value=False)
|
|
40
|
-
@patch("ida_code.executor.reset")
|
|
41
|
-
def test_resets_state_when_file_missing(self, mock_reset, mock_isfile):
|
|
42
|
-
session._state = session.State.DATABASE_OPEN
|
|
43
|
-
session._db_file_path = "/tmp/gone.i64"
|
|
44
|
-
session._orphaned = False
|
|
45
|
-
|
|
46
|
-
with pytest.raises(ToolError, match="moved or deleted"):
|
|
47
|
-
session.require_open()
|
|
48
|
-
|
|
49
|
-
assert session._state == session.State.NO_DATABASE
|
|
50
|
-
assert session._orphaned is True
|
|
51
|
-
assert session._db_file_path is None
|
|
52
|
-
mock_reset.assert_called_once()
|
|
53
|
-
|
|
54
|
-
def test_passes_when_db_file_path_is_none(self):
|
|
55
|
-
"""Graceful degradation: if _db_file_path was never set, skip the
|
|
56
|
-
file check and rely only on the state enum."""
|
|
57
|
-
session._state = session.State.DATABASE_OPEN
|
|
58
|
-
session._db_file_path = None
|
|
59
|
-
session.require_open() # should not raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|