aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-10 12:38:51 +0200
committerblasty <blasty@local>2026-07-10 12:38:51 +0200
commit50cbb7e982e03be863ad7f1979638520ffc95f55 (patch)
tree4fb97251f1715e7a34dfa14535448bf01875e09f /tests
parentclipboard: keyboard copy (OSC 52 + tmux) since mouse-select is captured (diff)
downloadida-tui-50cbb7e982e03be863ad7f1979638520ffc95f55.tar.gz
ida-tui-50cbb7e982e03be863ad7f1979638520ffc95f55.tar.xz
ida-tui-50cbb7e982e03be863ad7f1979638520ffc95f55.zip
struct editor: loud save errors + guard unsaved edits
A struct with a field IDA's C parser rejects (notably '__unused', a predefined macro) failed to re-declare, but the failure was silent: the edit looked applied, then reloading showed the old definition (lost work). Now: - a failed declare shows a loud, styled 'save failed' status and, when it can, names the offending reserved field ('__unused') instead of IDA's empty 'Failed to parse'; the type is left unchanged and the editor keeps your text. - switching structs / starting new / closing with unsaved edits prompts to discard first (ConfirmScreen), so edits aren't silently reloaded away. Pilot: rejected save is loud/named/non-destructive; switching structs guards unsaved edits. full suite 91/91.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_tui.py22
1 files changed, 22 insertions, 0 deletions
diff --git a/tests/test_tui.py b/tests/test_tui.py
index 6384ec6..e990bef 100644
--- a/tests/test_tui.py
+++ b/tests/test_tui.py
@@ -193,6 +193,28 @@ async def run(db):
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")
+ # A save IDA's C parser rejects (the reserved field name '__unused')
+ # fails loudly and non-destructively, naming the offending field.
+ ta.text = f"struct {sname} {{ int a; char b[8]; long c; int __unused; }};"
+ await pilot.press("ctrl+s")
+ await wait_until(
+ pilot, lambda: "save failed" in str(se.query_one("#se-status").render()), 15)
+ st = str(se.query_one("#se-status").render())
+ check("a rejected save fails loudly, naming the reserved field",
+ "save failed" in st and "__unused" in st, f"status={st!r}")
+ check("a rejected save leaves the struct unchanged",
+ next((s.members for s in se._structs if s.name == sname), 0) == 3,
+ "members changed")
+ check("a rejected save keeps your edited text", "__unused" in ta.text)
+ # Unsaved edits are guarded when switching structs.
+ other = next(i for i, s in enumerate(se._structs) if s.name != sname)
+ se.on_option_list_option_selected(type("E", (), {"option_index": other})())
+ guard = await wait_until(pilot, lambda: isinstance(app.screen, ConfirmScreen), 10)
+ check("unsaved edits prompt before switching structs", guard,
+ f"screen={type(app.screen).__name__}")
+ await pilot.press("enter") # confirm discard -> loads the other (clean)
+ await wait_until(
+ pilot, lambda: isinstance(app.screen, StructEditor) and not se._is_dirty(), 15)
# delete: 'd' asks to confirm, Enter deletes (server del_type). If the
# server lacks del_type, the status reports it instead.
idx = next(i for i, s in enumerate(se._structs) if s.name == sname)