diff options
| author | blasty <blasty@local> | 2026-07-10 09:53:56 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-10 09:53:56 +0200 |
| commit | 29f8a238d0e316c4a0fe4081c5412a18f5bb3c64 (patch) | |
| tree | f380c21b8c59e78962bf0d6bfe1d2be365ae93c6 /tests | |
| parent | decompiler: fall back to disassembly when decompilation fails (diff) | |
| download | ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.tar.gz ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.tar.xz ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.zip | |
struct editor: C-style CRUD for local types (Ctrl+T)
New StructEditor overlay: left is the list of local structs/unions, right is an
editable C definition. Enter loads a struct (reconstructed as C from its member
layout via type_inspect), Ctrl+S declares it (create or update via declare_type),
Ctrl+N starts a new one, Del deletes, Esc returns to the list then closes.
Domain: Struct model + Program.list_structs / struct_source / declare_type /
delete_type (anonymous $-types filtered).
Delete needs a 'del_type' tool the ida-pro-mcp server doesn't currently expose;
delete_type detects its absence and reports a clear message instead of failing.
Create/read/update work fully.
Pilot checks: open, list, view C, create, update-in-place, delete(-or-report),
close. full suite 84/84.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_tui.py | 63 |
1 files changed, 61 insertions, 2 deletions
diff --git a/tests/test_tui.py b/tests/test_tui.py index 47a0e3e..f2e38ff 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -19,9 +19,10 @@ import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from idatui.app import ( # noqa: E402 - DecompView, DisasmView, FunctionsPanel, IdaTui, SymbolPalette, XrefsScreen, + DecompView, DisasmView, FunctionsPanel, IdaTui, StructEditor, SymbolPalette, + XrefsScreen, ) -from textual.widgets import DataTable, Input, OptionList, Static # noqa: E402 +from textual.widgets import DataTable, Input, OptionList, Static, TextArea # noqa: E402 from rich.text import Text # noqa: E402 PASS = FAIL = 0 @@ -143,6 +144,64 @@ async def run(db): check("found a decompile-failing function", failing is not None, f"pref={app._pref}") + # Struct editor (Ctrl+T): list / view / create / update / delete. + await pilot.press("ctrl+t") + se_open = await wait_until(pilot, lambda: isinstance(app.screen, StructEditor), 10) + check("Ctrl+T opens the struct editor", se_open, + f"screen={type(app.screen).__name__}") + if se_open: + se = app.screen + await wait_until(pilot, lambda: bool(se._structs), 15) + check("struct editor lists existing structs", len(se._structs) > 0, + f"n={len(se._structs)}") + ta = se.query_one(TextArea) + tname = next((s.name for s in se._structs if s.name == "timespec"), + se._structs[0].name) + idx = next(i for i, s in enumerate(se._structs) if s.name == tname) + se.query_one(OptionList).highlighted = idx + se.on_option_list_option_selected(type("E", (), {"option_index": idx})()) + await wait_until(pilot, lambda: tname in ta.text and "{" in ta.text, 15) + check("selecting a struct shows its C definition", + tname in ta.text and "{" in ta.text, f"text={ta.text[:40]!r}") + # create (fixed name so re-runs update in place -> no churn) + sname = "TuiEdTest" + await pilot.press("ctrl+n") + await pilot.pause(0.05) + ta.text = f"struct {sname} {{ int a; char b[8]; }};" + await pilot.press("ctrl+s") + await wait_until(pilot, lambda: any(s.name == sname for s in se._structs), 15) + check("Ctrl+S declares a new struct", + any(s.name == sname for s in se._structs), "not created") + # update it (add a field -> member count grows) + idx = next(i for i, s in enumerate(se._structs) if s.name == sname) + se.on_option_list_option_selected(type("E", (), {"option_index": idx})()) + await wait_until(pilot, lambda: sname in ta.text, 10) + ta.text = f"struct {sname} {{ int a; char b[8]; long c; }};" + await pilot.press("ctrl+s") + await wait_until( + pilot, + lambda: next((s.members for s in se._structs if s.name == sname), 0) == 3, + 15) + check("Ctrl+S updates an existing struct in place", + next((s.members for s in se._structs if s.name == sname), 0) == 3, + "member count not 3") + # delete: removes it if the server supports del_type, else reports it + idx = next(i for i, s in enumerate(se._structs) if s.name == sname) + se.query_one(OptionList).highlighted = idx + se.action_delete() + await wait_until( + pilot, + lambda: not any(s.name == sname for s in se._structs) + or "del_type" in str(se.query_one("#se-status").render()), 15) + st = str(se.query_one("#se-status").render()) + gone = not any(s.name == sname for s in se._structs) + check("delete removes the struct (or reports the missing server tool)", + gone or "del_type" in st, f"gone={gone} status={st!r}") + se.query_one(OptionList).focus() + await pilot.press("escape") + await wait_until(pilot, lambda: not isinstance(app.screen, StructEditor), 10) + check("Esc closes the struct editor", not isinstance(app.screen, StructEditor)) + # Open the biggest function we can find (scan a sample of rows for size). # Simpler: select the row whose Size column is largest among first N. biggest_i, biggest_sz = 0, -1 |
