Improve error handling in CoreBackupReaderWriter (#139508)

This commit is contained in:
Erik Montnemery 2025-02-28 14:25:35 +01:00 committed by GitHub
parent 030a1460de
commit 228a4eb391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 61 additions and 12 deletions

View File

@ -14,6 +14,7 @@ from itertools import chain
import json
from pathlib import Path, PurePath
import shutil
import sys
import tarfile
import time
from typing import IO, TYPE_CHECKING, Any, Protocol, TypedDict, cast
@ -308,6 +309,12 @@ class DecryptOnDowloadNotSupported(BackupManagerError):
_message = "On-the-fly decryption is not supported for this backup."
class BackupManagerExceptionGroup(BackupManagerError, ExceptionGroup):
"""Raised when multiple exceptions occur."""
error_code = "multiple_errors"
class BackupManager:
"""Define the format that backup managers can have."""
@ -1605,10 +1612,24 @@ class CoreBackupReaderWriter(BackupReaderWriter):
)
finally:
# Inform integrations the backup is done
# If there's an unhandled exception, we keep it so we can rethrow it in case
# the post backup actions also fail.
unhandled_exc = sys.exception()
try:
await manager.async_post_backup_actions()
except BackupManagerError as err:
raise BackupReaderWriterError(str(err)) from err
try:
await manager.async_post_backup_actions()
except BackupManagerError as err:
raise BackupReaderWriterError(str(err)) from err
except Exception as err:
if not unhandled_exc:
raise
# If there's an unhandled exception, we wrap both that and the exception
# from the post backup actions in an ExceptionGroup so the caller is
# aware of both exceptions.
raise BackupManagerExceptionGroup(
f"Multiple errors when creating backup: {unhandled_exc}, {err}",
[unhandled_exc, err],
) from None
def _mkdir_and_generate_backup_contents(
self,

View File

@ -8,6 +8,7 @@ from dataclasses import replace
from io import StringIO
import json
from pathlib import Path
import re
import tarfile
from typing import Any
from unittest.mock import (
@ -35,6 +36,7 @@ from homeassistant.components.backup.agent import BackupAgentError
from homeassistant.components.backup.const import DATA_MANAGER
from homeassistant.components.backup.manager import (
BackupManagerError,
BackupManagerExceptionGroup,
BackupManagerState,
CreateBackupStage,
CreateBackupState,
@ -1646,34 +1648,60 @@ async def test_exception_platform_pre(hass: HomeAssistant) -> None:
assert str(err.value) == "Error during pre-backup: Test exception"
@pytest.mark.parametrize(
("unhandled_error", "expected_exception", "expected_msg"),
[
(None, BackupManagerError, "Error during post-backup: Test exception"),
(
HomeAssistantError("Boom"),
BackupManagerExceptionGroup,
(
"Multiple errors when creating backup: Error during pre-backup: Boom, "
"Error during post-backup: Test exception (2 sub-exceptions)"
),
),
(
Exception("Boom"),
BackupManagerExceptionGroup,
(
"Multiple errors when creating backup: Error during pre-backup: Boom, "
"Error during post-backup: Test exception (2 sub-exceptions)"
),
),
],
)
@pytest.mark.usefixtures("mock_backup_generation")
async def test_exception_platform_post(hass: HomeAssistant) -> None:
async def test_exception_platform_post(
hass: HomeAssistant,
unhandled_error: Exception | None,
expected_exception: type[Exception],
expected_msg: str,
) -> None:
"""Test exception in post step."""
async def _mock_step(hass: HomeAssistant) -> None:
raise HomeAssistantError("Test exception")
remote_agent = mock_backup_agent("remote")
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_pre_backup=AsyncMock(),
async_post_backup=_mock_step,
# We let the pre_backup fail to test that unhandled errors are not discarded
# when post backup fails
async_pre_backup=AsyncMock(side_effect=unhandled_error),
async_post_backup=AsyncMock(
side_effect=HomeAssistantError("Test exception")
),
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
),
)
await setup_backup_integration(hass)
with pytest.raises(BackupManagerError) as err:
with pytest.raises(expected_exception, match=re.escape(expected_msg)):
await hass.services.async_call(
DOMAIN,
"create",
blocking=True,
)
assert str(err.value) == "Error during post-backup: Test exception"
@pytest.mark.parametrize(
(