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:
		
							parent
							
								
									6171e0ca0b
								
							
						
					
					
						commit
						77d4a2e955
					
				
					 3 changed files with 84 additions and 9 deletions
				
			
		| 
						 | 
					@ -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}"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue