From 50cbb7e982e03be863ad7f1979638520ffc95f55 Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 10 Jul 2026 12:38:51 +0200 Subject: 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. --- tests/test_tui.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'tests') 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) -- cgit v1.3.1-sl0p