diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index bd970d7708a..317de85b823 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1620,7 +1620,13 @@ class CoreBackupReaderWriter(BackupReaderWriter): """Generate backup contents and return the size.""" if not tar_file_path: tar_file_path = self.temp_backup_dir / f"{backup_data['slug']}.tar" - make_backup_dir(tar_file_path.parent) + try: + make_backup_dir(tar_file_path.parent) + except OSError as err: + raise BackupReaderWriterError( + f"Failed to create dir {tar_file_path.parent}: " + f"{err} ({err.__class__.__name__})" + ) from err excludes = EXCLUDE_FROM_BACKUP if not database_included: @@ -1658,7 +1664,14 @@ class CoreBackupReaderWriter(BackupReaderWriter): file_filter=is_excluded_by_filter, arcname="data", ) - return (tar_file_path, tar_file_path.stat().st_size) + try: + stat_result = tar_file_path.stat() + except OSError as err: + raise BackupReaderWriterError( + f"Error getting size of {tar_file_path}: " + f"{err} ({err.__class__.__name__})" + ) from err + return (tar_file_path, stat_result.st_size) async def async_receive_backup( self, diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 3c72929cfe0..6e626e63748 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -1311,7 +1311,7 @@ async def test_initiate_backup_with_task_error( (1, None, 1, None, 1, None, 1, OSError("Boom!")), ], ) -async def test_initiate_backup_file_error( +async def test_initiate_backup_file_error_upload_to_agents( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, @@ -1325,7 +1325,7 @@ async def test_initiate_backup_file_error( unlink_call_count: int, unlink_exception: Exception | None, ) -> None: - """Test file error during generate backup.""" + """Test file error during generate backup, while uploading to agents.""" agent_ids = ["test.remote"] await setup_backup_integration(hass, remote_agents=["test.remote"]) @@ -1418,6 +1418,122 @@ async def test_initiate_backup_file_error( assert unlink_mock.call_count == unlink_call_count +@pytest.mark.usefixtures("mock_backup_generation") +@pytest.mark.parametrize( + ( + "mkdir_call_count", + "mkdir_exception", + "atomic_contents_add_call_count", + "atomic_contents_add_exception", + "stat_call_count", + "stat_exception", + "error_message", + ), + [ + (1, OSError("Boom!"), 0, None, 0, None, "Failed to create dir"), + (1, None, 1, OSError("Boom!"), 0, None, "Boom!"), + (1, None, 1, None, 1, OSError("Boom!"), "Error getting size"), + ], +) +async def test_initiate_backup_file_error_create_backup( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + generate_backup_id: MagicMock, + path_glob: MagicMock, + caplog: pytest.LogCaptureFixture, + mkdir_call_count: int, + mkdir_exception: Exception | None, + atomic_contents_add_call_count: int, + atomic_contents_add_exception: Exception | None, + stat_call_count: int, + stat_exception: Exception | None, + error_message: str, +) -> None: + """Test file error during generate backup, while creating backup.""" + agent_ids = ["test.remote"] + + await setup_backup_integration(hass, remote_agents=["test.remote"]) + + ws_client = await hass_ws_client(hass) + + path_glob.return_value = [] + + await ws_client.send_json_auto_id({"type": "backup/info"}) + result = await ws_client.receive_json() + + assert result["success"] is True + assert result["result"] == { + "backups": [], + "agent_errors": {}, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "last_non_idle_event": None, + "next_automatic_backup": None, + "next_automatic_backup_additional": False, + "state": "idle", + } + + await ws_client.send_json_auto_id({"type": "backup/subscribe_events"}) + + result = await ws_client.receive_json() + assert result["event"] == {"manager_state": BackupManagerState.IDLE} + + result = await ws_client.receive_json() + assert result["success"] is True + + with ( + patch( + "homeassistant.components.backup.manager.atomic_contents_add", + side_effect=atomic_contents_add_exception, + ) as atomic_contents_add_mock, + patch("pathlib.Path.mkdir", side_effect=mkdir_exception) as mkdir_mock, + patch("pathlib.Path.stat", side_effect=stat_exception) as stat_mock, + ): + await ws_client.send_json_auto_id( + {"type": "backup/generate", "agent_ids": agent_ids} + ) + + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": None, + "stage": None, + "state": CreateBackupState.IN_PROGRESS, + } + result = await ws_client.receive_json() + assert result["success"] is True + + backup_id = result["result"]["backup_job_id"] + assert backup_id == generate_backup_id.return_value + + await hass.async_block_till_done() + + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": None, + "stage": CreateBackupStage.HOME_ASSISTANT, + "state": CreateBackupState.IN_PROGRESS, + } + + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": "upload_failed", + "stage": None, + "state": CreateBackupState.FAILED, + } + + result = await ws_client.receive_json() + assert result["event"] == {"manager_state": BackupManagerState.IDLE} + + assert atomic_contents_add_mock.call_count == atomic_contents_add_call_count + assert mkdir_mock.call_count == mkdir_call_count + assert stat_mock.call_count == stat_call_count + + assert error_message in caplog.text + + def _mock_local_backup_agent(name: str) -> Mock: local_agent = mock_backup_agent(name) # This makes the local_agent pass isinstance checks for LocalBackupAgent