Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions homeassistant/components/backup/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1621,7 +1621,13 @@ def _mkdir_and_generate_backup_contents(
"""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:
Expand Down Expand Up @@ -1659,7 +1665,14 @@ def is_excluded_by_filter(path: PurePath) -> bool:
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,
Expand Down
120 changes: 118 additions & 2 deletions tests/components/backup/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,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,
Expand All @@ -1310,7 +1310,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"])
Expand Down Expand Up @@ -1403,6 +1403,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
Expand Down