Enhance entry addition process: implement immediate file save after adding an entry and handle save failure by removing the entry for improved reliability.

This commit is contained in:
Philip Henning 2025-08-17 19:26:30 +02:00
parent 6171e0ca0b
commit 77d4a2e955
3 changed files with 84 additions and 9 deletions

View file

@ -296,7 +296,18 @@ class HostsManager:
try: try:
# Add the new entry at the end # Add the new entry at the end
hosts_file.entries.append(entry) hosts_file.entries.append(entry)
return True, "Entry added successfully"
# Save the file immediately
save_success, save_message = self.save_hosts_file(hosts_file)
if not save_success:
# If save fails, remove the entry that was just added
hosts_file.entries.pop()
return False, f"Failed to save after adding entry: {save_message}"
canonical_hostname = (
entry.hostnames[0] if entry.hostnames else entry.ip_address
)
return True, f"Entry added: {canonical_hostname}"
except Exception as e: except Exception as e:
return False, f"Error adding entry: {e}" return False, f"Error adding entry: {e}"

View file

@ -7,7 +7,7 @@ all the handlers and provides the primary user interface.
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Horizontal, Vertical
from textual.widgets import Header, Static, DataTable, Input, Checkbox, Label from textual.widgets import Header, Static, DataTable, Input, Checkbox
from textual.reactive import reactive from textual.reactive import reactive
from ..core.parser import HostsParser from ..core.parser import HostsParser
@ -94,7 +94,9 @@ class HostsManagerApp(App):
# Details display form (disabled inputs) # Details display form (disabled inputs)
with Vertical(id="entry-details-display", classes="entry-form"): with Vertical(id="entry-details-display", classes="entry-form"):
with Vertical(classes="default-section section-no-top-margin") as ip_address: with Vertical(
classes="default-section section-no-top-margin"
) as ip_address:
ip_address.border_title = "IP Address" ip_address.border_title = "IP Address"
yield Input( yield Input(
placeholder="No entry selected", placeholder="No entry selected",
@ -118,18 +120,23 @@ class HostsManagerApp(App):
placeholder="No entry selected", placeholder="No entry selected",
id="details-comment-input", id="details-comment-input",
disabled=True, disabled=True,
classes="default-input", classes="default-input",
) )
with Vertical(classes="default-section") as active: with Vertical(classes="default-section") as active:
active.border_title = "Active" active.border_title = "Active"
yield Checkbox( yield Checkbox(
"Active", id="details-active-checkbox", disabled=True, classes="default-checkbox" "Active",
id="details-active-checkbox",
disabled=True,
classes="default-checkbox",
) )
# Edit form (initially hidden) # Edit form (initially hidden)
with Vertical(id="entry-edit-form", classes="entry-form hidden"): with Vertical(id="entry-edit-form", classes="entry-form hidden"):
with Vertical(classes="default-section section-no-top-margin") as ip_address: with Vertical(
classes="default-section section-no-top-margin"
) as ip_address:
ip_address.border_title = "IP Address" ip_address.border_title = "IP Address"
yield Input( yield Input(
placeholder="Enter IP address", placeholder="Enter IP address",
@ -151,11 +158,13 @@ class HostsManagerApp(App):
placeholder="Enter comment (optional)", placeholder="Enter comment (optional)",
id="comment-input", id="comment-input",
classes="default-input", classes="default-input",
) )
with Vertical(classes="default-section") as active: with Vertical(classes="default-section") as active:
active.border_title = "Active" active.border_title = "Active"
yield Checkbox("Active", id="active-checkbox", classes="default-checkbox") yield Checkbox(
"Active", id="active-checkbox", classes="default-checkbox"
)
# Status bar for error/temporary messages (overlay, doesn't affect layout) # Status bar for error/temporary messages (overlay, doesn't affect layout)
yield Static("", id="status-bar", classes="status-bar hidden") yield Static("", id="status-bar", classes="status-bar hidden")

View file

@ -634,3 +634,58 @@ class TestHostsManager:
finally: finally:
# Clean up # Clean up
Path(temp_path).unlink() Path(temp_path).unlink()
def test_add_entry_success(self):
"""Test successfully adding an entry with immediate save."""
manager = HostsManager()
manager.edit_mode = True
# Mock the save operation to succeed
with patch.object(manager, "save_hosts_file") as mock_save:
mock_save.return_value = (True, "File saved successfully")
hosts_file = HostsFile()
test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])
success, message = manager.add_entry(hosts_file, test_entry)
assert success
assert "Entry added: testhost" in message
assert len(hosts_file.entries) == 1
assert hosts_file.entries[0] == test_entry
mock_save.assert_called_once_with(hosts_file)
def test_add_entry_save_failure(self):
"""Test adding an entry when save fails."""
manager = HostsManager()
manager.edit_mode = True
# Mock the save operation to fail
with patch.object(manager, "save_hosts_file") as mock_save:
mock_save.return_value = (False, "Permission denied")
hosts_file = HostsFile()
test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])
success, message = manager.add_entry(hosts_file, test_entry)
assert not success
assert "Failed to save after adding entry" in message
assert (
len(hosts_file.entries) == 0
) # Entry should be removed on save failure
mock_save.assert_called_once_with(hosts_file)
def test_add_entry_not_in_edit_mode(self):
"""Test adding an entry when not in edit mode."""
manager = HostsManager()
# edit_mode defaults to False
hosts_file = HostsFile()
test_entry = HostEntry(ip_address="192.168.1.100", hostnames=["testhost"])
success, message = manager.add_entry(hosts_file, test_entry)
assert not success
assert "Not in edit mode" in message
assert len(hosts_file.entries) == 0