From 9f0a06a71852713c66be1f925d4fb18181a52b97 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 18 Nov 2024 20:33:59 +0100 Subject: [PATCH] Include backup agent error in response to WS command backup/details (#130892) --- homeassistant/components/backup/manager.py | 40 ++++++++++++++----- homeassistant/components/backup/websocket.py | 7 +++- .../backup/snapshots/test_websocket.ambr | 37 +++++++++++++++++ tests/components/backup/test_manager.py | 5 ++- tests/components/backup/test_websocket.py | 24 +++++++++++ 5 files changed, 100 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 7ea7fe2fd02..d7792f859e0 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -221,7 +221,9 @@ class BaseBackupManager(abc.ABC, Generic[_BackupT]): """ @abc.abstractmethod - async def async_get_backup(self, *, slug: str, **kwargs: Any) -> _BackupT | None: + async def async_get_backup( + self, *, slug: str, **kwargs: Any + ) -> tuple[_BackupT | None, dict[str, Exception]]: """Get a backup.""" @abc.abstractmethod @@ -314,25 +316,41 @@ class BackupManager(BaseBackupManager[Backup]): return (backups, agent_errors) - async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None: + async def async_get_backup( + self, *, slug: str, **kwargs: Any + ) -> tuple[Backup | None, dict[str, Exception]]: """Return a backup.""" backup: Backup | None = None + agent_errors: dict[str, Exception] = {} + agent_ids = list(self.backup_agents.keys()) - for agent_id, agent in self.backup_agents.items(): - if not (agent_backup := await agent.async_get_backup(slug=slug)): + get_backup_results = await asyncio.gather( + *( + agent.async_get_backup(slug=slug) + for agent in self.backup_agents.values() + ), + return_exceptions=True, + ) + for idx, result in enumerate(get_backup_results): + if isinstance(result, BackupAgentError): + agent_errors[agent_ids[idx]] = result + continue + if isinstance(result, BaseException): + raise result + if not result: continue if backup is None: backup = Backup( - slug=agent_backup.slug, - name=agent_backup.name, - date=agent_backup.date, + slug=result.slug, + name=result.name, + date=result.date, agent_ids=[], - size=agent_backup.size, - protected=agent_backup.protected, + size=result.size, + protected=result.protected, ) - backup.agent_ids.append(agent_id) + backup.agent_ids.append(agent_ids[idx]) - return backup + return (backup, agent_errors) async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None: """Remove a backup.""" diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 8aa5df9441f..ad9421c28b7 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -67,10 +67,15 @@ async def handle_details( msg: dict[str, Any], ) -> None: """Get backup details for a specific slug.""" - backup = await hass.data[DATA_MANAGER].async_get_backup(slug=msg["slug"]) + backup, agent_errors = await hass.data[DATA_MANAGER].async_get_backup( + slug=msg["slug"] + ) connection.send_result( msg["id"], { + "agent_errors": { + agent_id: str(err) for agent_id, err in agent_errors.items() + }, "backup": backup, }, ) diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 9b285f4c1df..3daaf14dc96 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -283,6 +283,8 @@ dict({ 'id': 1, 'result': dict({ + 'agent_errors': dict({ + }), 'backup': dict({ 'agent_ids': list([ 'backup.local', @@ -302,12 +304,47 @@ dict({ 'id': 1, 'result': dict({ + 'agent_errors': dict({ + }), 'backup': None, }), 'success': True, 'type': 'result', }) # --- +# name: test_details_with_errors[BackupAgentUnreachableError] + dict({ + 'id': 1, + 'result': dict({ + 'agent_errors': dict({ + 'domain.test': 'The backup agent is unreachable.', + }), + 'backup': dict({ + 'agent_ids': list([ + 'backup.local', + ]), + 'date': '1970-01-01T00:00:00.000Z', + 'name': 'Test', + 'protected': False, + 'size': 0.0, + 'slug': 'abc123', + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_details_with_errors[side_effect0] + dict({ + 'error': dict({ + 'code': 'home_assistant_error', + 'message': 'Boom!', + }), + 'id': 1, + 'success': False, + 'type': 'result', + }) +# --- # name: test_generate[with_hassio-None] dict({ 'error': dict({ diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index f60bf588e02..16e93f6e561 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -217,8 +217,11 @@ async def test_getting_backup_that_does_not_exist( local_agent._loaded_backups = True with patch("pathlib.Path.exists", return_value=False): - backup = await manager.async_get_backup(slug=TEST_LOCAL_BACKUP.slug) + backup, agent_errors = await manager.async_get_backup( + slug=TEST_LOCAL_BACKUP.slug + ) assert backup is None + assert agent_errors == {} assert ( f"Removing tracked backup ({TEST_LOCAL_BACKUP.slug}) that " diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 05930c6697f..8ef077e6462 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -112,6 +112,30 @@ async def test_details( assert await client.receive_json() == snapshot +@pytest.mark.parametrize( + "side_effect", [HomeAssistantError("Boom!"), BackupAgentUnreachableError] +) +async def test_details_with_errors( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + side_effect: Exception, + snapshot: SnapshotAssertion, +) -> None: + """Test getting backup info with one unavailable agent.""" + await setup_backup_integration(hass, with_hassio=False, backups=[TEST_LOCAL_BACKUP]) + hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test") + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with ( + patch("pathlib.Path.exists", return_value=True), + patch.object(BackupAgentTest, "async_get_backup", side_effect=side_effect), + ): + await client.send_json_auto_id({"type": "backup/details", "slug": "abc123"}) + assert await client.receive_json() == snapshot + + @pytest.mark.parametrize( "with_hassio", [