mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Improve error handling in CoreBackupReaderWriter (#139508)
This commit is contained in:
parent
030a1460de
commit
228a4eb391
@ -14,6 +14,7 @@ from itertools import chain
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
from typing import IO, TYPE_CHECKING, Any, Protocol, TypedDict, cast
|
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."
|
_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:
|
class BackupManager:
|
||||||
"""Define the format that backup managers can have."""
|
"""Define the format that backup managers can have."""
|
||||||
|
|
||||||
@ -1605,10 +1612,24 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# Inform integrations the backup is done
|
# 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:
|
try:
|
||||||
await manager.async_post_backup_actions()
|
try:
|
||||||
except BackupManagerError as err:
|
await manager.async_post_backup_actions()
|
||||||
raise BackupReaderWriterError(str(err)) from err
|
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(
|
def _mkdir_and_generate_backup_contents(
|
||||||
self,
|
self,
|
||||||
|
@ -8,6 +8,7 @@ from dataclasses import replace
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import tarfile
|
import tarfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import (
|
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.const import DATA_MANAGER
|
||||||
from homeassistant.components.backup.manager import (
|
from homeassistant.components.backup.manager import (
|
||||||
BackupManagerError,
|
BackupManagerError,
|
||||||
|
BackupManagerExceptionGroup,
|
||||||
BackupManagerState,
|
BackupManagerState,
|
||||||
CreateBackupStage,
|
CreateBackupStage,
|
||||||
CreateBackupState,
|
CreateBackupState,
|
||||||
@ -1646,34 +1648,60 @@ async def test_exception_platform_pre(hass: HomeAssistant) -> None:
|
|||||||
assert str(err.value) == "Error during pre-backup: Test exception"
|
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")
|
@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."""
|
"""Test exception in post step."""
|
||||||
|
|
||||||
async def _mock_step(hass: HomeAssistant) -> None:
|
|
||||||
raise HomeAssistantError("Test exception")
|
|
||||||
|
|
||||||
remote_agent = mock_backup_agent("remote")
|
remote_agent = mock_backup_agent("remote")
|
||||||
await setup_backup_platform(
|
await setup_backup_platform(
|
||||||
hass,
|
hass,
|
||||||
domain="test",
|
domain="test",
|
||||||
platform=Mock(
|
platform=Mock(
|
||||||
async_pre_backup=AsyncMock(),
|
# We let the pre_backup fail to test that unhandled errors are not discarded
|
||||||
async_post_backup=_mock_step,
|
# 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]),
|
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await setup_backup_integration(hass)
|
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(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"create",
|
"create",
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert str(err.value) == "Error during post-backup: Test exception"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user