diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py index 3738e1897..653104231 100644 --- a/supervisor/utils/dbus.py +++ b/supervisor/utils/dbus.py @@ -30,6 +30,7 @@ from ..exceptions import ( DBusTimeoutError, HassioNotSupportedError, ) +from .sentry import capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -103,6 +104,9 @@ class DBus: return await getattr(proxy_interface, method)(*args) except DBusError as err: raise DBus.from_dbus_error(err) + except Exception as err: # pylint: disable=broad-except + capture_exception(err) + raise DBusFatalError(str(err)) from err def _add_interfaces(self): """Make proxy interfaces out of introspection data.""" diff --git a/tests/conftest.py b/tests/conftest.py index 580634fc4..f690ee203 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,8 +129,41 @@ def mock_get_properties(object_path: str, interface: str) -> str: return load_json_fixture(f"{fixture}.json") +async def mock_init_proxy(self): + """Mock init dbus proxy.""" + filetype = "xml" + fixture = ( + self.object_path.replace("/", "_")[1:] + if self.object_path != DBUS_OBJECT_BASE + else self.bus_name.replace(".", "_") + ) + + if not exists_fixture(f"{fixture}.{filetype}"): + fixture = re.sub(r"_[0-9]+$", "", fixture) + + # special case + if exists_fixture(f"{fixture}_~.{filetype}"): + fixture = f"{fixture}_~" + + # Use dbus-next infrastructure to parse introspection xml + self._proxy_obj = ProxyObject( + self.bus_name, + self.object_path, + load_fixture(f"{fixture}.{filetype}"), + self._bus, + ) + self._add_interfaces() + + if DBUS_INTERFACE_PROPERTIES in self._proxies: + setattr( + self._proxies[DBUS_INTERFACE_PROPERTIES], + "call_get_all", + lambda interface: mock_get_properties(self.object_path, interface), + ) + + @pytest.fixture -def dbus(dbus_bus: MessageBus) -> DBus: +def dbus(dbus_bus: MessageBus) -> list[str]: """Mock DBUS.""" dbus_commands = [] @@ -150,31 +183,6 @@ def dbus(dbus_bus: MessageBus) -> DBus: async def mock_signal___aexit__(self, exc_t, exc_v, exc_tb): pass - async def mock_init_proxy(self): - - filetype = "xml" - fixture = ( - self.object_path.replace("/", "_")[1:] - if self.object_path != DBUS_OBJECT_BASE - else self.bus_name.replace(".", "_") - ) - - if not exists_fixture(f"{fixture}.{filetype}"): - fixture = re.sub(r"_[0-9]+$", "", fixture) - - # special case - if exists_fixture(f"{fixture}_~.{filetype}"): - fixture = f"{fixture}_~" - - # Use dbus-next infrastructure to parse introspection xml - self._proxy_obj = ProxyObject( - self.bus_name, - self.object_path, - load_fixture(f"{fixture}.{filetype}"), - dbus_bus, - ) - self._add_interfaces() - async def mock_call_dbus( proxy_interface: ProxyInterface, method: str, @@ -235,6 +243,15 @@ def dbus(dbus_bus: MessageBus) -> DBus: yield dbus_commands +@pytest.fixture +async def dbus_minimal(dbus_bus: MessageBus) -> MessageBus: + """Mock DBus without mocking call_dbus or signals but handle properties fixture.""" + with patch("supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy), patch( + "supervisor.dbus.manager.MessageBus.connect", return_value=dbus_bus + ): + yield dbus_bus + + @pytest.fixture async def dbus_is_connected(): """Mock DBusInterface.is_connected for tests.""" diff --git a/tests/utils/test_dbus.py b/tests/utils/test_dbus.py index 996054e7e..a857871fc 100644 --- a/tests/utils/test_dbus.py +++ b/tests/utils/test_dbus.py @@ -1,9 +1,12 @@ """Test dbus utility.""" + +from unittest.mock import AsyncMock, Mock + from dbus_fast.aio.message_bus import MessageBus import pytest from supervisor.dbus.const import DBUS_OBJECT_BASE -from supervisor.exceptions import DBusInterfaceError +from supervisor.exceptions import DBusFatalError, DBusInterfaceError from supervisor.utils.dbus import DBus @@ -14,3 +17,26 @@ async def test_missing_properties_interface(dbus_bus: MessageBus, dbus: list[str ) with pytest.raises(DBusInterfaceError): await service.get_properties("test.no.properties.interface") + + +@pytest.mark.parametrize("err", [BrokenPipeError(), EOFError(), OSError()]) +async def test_internal_dbus_errors( + dbus_minimal: MessageBus, + capture_exception: Mock, + err: Exception, +): + """Test internal dbus library errors become dbus error.""" + rauc = await DBus.connect(dbus_minimal, "de.pengutronix.rauc", DBUS_OBJECT_BASE) + setattr( + # pylint: disable=protected-access + rauc._proxies["de.pengutronix.rauc.Installer"], + # pylint: enable=protected-access + "call_mark", + proxy_mock := AsyncMock().call_mark, + ) + proxy_mock.side_effect = err + + with pytest.raises(DBusFatalError): + await rauc.Installer.call_mark("good", "booted") + + capture_exception.assert_called_once_with(err)