supervisor/tests/utils/test_dbus.py
Mike Degatano e1c9c8b786
Finish out effort of adding and enabling blockbuster in tests (#5735)
* Finish out effort of adding and enabling blockbuster

* Skip getting addon file size until securetar fixed

* Fix test for devcontainer and blocking I/O

* Fix docker fixture and load_config to post_init
2025-03-07 13:29:24 +01:00

193 lines
6.1 KiB
Python

"""Test dbus utility."""
import asyncio
from unittest.mock import AsyncMock, Mock, patch
from dbus_fast import ErrorType
from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.errors import DBusError as DBusFastDBusError
from dbus_fast.service import method, signal
import pytest
from supervisor.dbus.const import DBUS_OBJECT_BASE
from supervisor.exceptions import (
DBusFatalError,
DBusInterfaceError,
DBusServiceUnkownError,
)
from supervisor.utils.dbus import DBus
from tests.common import load_fixture
from tests.dbus_service_mocks.base import DBusServiceMock
class TestInterface(DBusServiceMock):
"""Test interface."""
__test__ = False
interface = "service.test.TestInterface"
object_path = DBUS_OBJECT_BASE
@method(name="Test")
def test(self, _: "b") -> None: # noqa: F821
"""Do Test method."""
@signal(name="Test")
def signal_test(self) -> None:
"""Signal Test."""
@pytest.fixture(name="test_service")
async def fixture_test_service(dbus_session_bus: MessageBus) -> TestInterface:
"""Export test interface on dbus."""
await dbus_session_bus.request_name("service.test.TestInterface")
service = TestInterface()
service.export(dbus_session_bus)
yield service
async def test_missing_properties_interface(dbus_session_bus: MessageBus):
"""Test introspection missing properties interface."""
def mock_introspect(*args, **kwargs):
"""Return introspection without properties."""
return asyncio.get_running_loop().run_in_executor(
None, load_fixture, "test_no_properties_interface.xml"
)
with patch.object(MessageBus, "introspect", new=mock_introspect):
service = await DBus.connect(
dbus_session_bus, "test.no.properties.interface", DBUS_OBJECT_BASE
)
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(
test_service: TestInterface,
dbus_session_bus: MessageBus,
capture_exception: Mock,
err: Exception,
):
"""Test internal dbus library errors become dbus error."""
test_obj = await DBus.connect(
dbus_session_bus, "service.test.TestInterface", DBUS_OBJECT_BASE
)
setattr(
# pylint: disable=protected-access
test_obj._proxies["service.test.TestInterface"],
# pylint: enable=protected-access
"call_test",
proxy_mock := AsyncMock().call_test,
)
proxy_mock.side_effect = err
with pytest.raises(DBusFatalError):
await test_obj.call_test(True)
capture_exception.assert_called_once_with(err)
async def test_introspect(test_service: TestInterface, dbus_session_bus: MessageBus):
"""Test introspect of dbus object."""
test_obj = DBus(dbus_session_bus, "service.test.TestInterface", DBUS_OBJECT_BASE)
introspection = await test_obj.introspect()
assert {"service.test.TestInterface", "org.freedesktop.DBus.Properties"} <= {
interface.name for interface in introspection.interfaces
}
test_interface = next(
interface
for interface in introspection.interfaces
if interface.name == "service.test.TestInterface"
)
assert "Test" in {method_.name for method_ in test_interface.methods}
async def test_init_proxy(test_service: TestInterface, dbus_session_bus: MessageBus):
"""Test init proxy on already connected object to update interfaces."""
test_obj = await DBus.connect(
dbus_session_bus, "service.test.TestInterface", DBUS_OBJECT_BASE
)
orig_introspection = await test_obj.introspect()
callback_count = 0
def test_callback():
nonlocal callback_count
callback_count += 1
class TestInterface2(TestInterface):
"""Test interface 2."""
interface = "service.test.TestInterface.Test2"
object_path = DBUS_OBJECT_BASE
# Test interfaces and methods match expected
assert "service.test.TestInterface" in test_obj.proxies
assert await test_obj.call_test(True) is None
assert "service.test.TestInterface.Test2" not in test_obj.proxies
# Test basic signal listening works
test_obj.on_test(test_callback)
test_service.signal_test()
await test_service.ping()
assert callback_count == 1
callback_count = 0
# Export the second interface and re-create proxy
test_service_2 = TestInterface2()
test_service_2.export(dbus_session_bus)
await test_obj.init_proxy()
# Test interfaces and methods match expected
assert "service.test.TestInterface" in test_obj.proxies
assert await test_obj.call_test(True) is None
assert "service.test.TestInterface.Test2" in test_obj.proxies
assert await test_obj.Test2.call_test(True) is None
# Test signal listening. First listener should still be attached
test_obj.Test2.on_test(test_callback)
test_service_2.signal_test()
await test_service_2.ping()
assert callback_count == 1
test_service.signal_test()
await test_service.ping()
assert callback_count == 2
callback_count = 0
# Return to original introspection and test interfaces have reset
await test_obj.init_proxy(introspection=orig_introspection)
assert "service.test.TestInterface" in test_obj.proxies
assert "service.test.TestInterface.Test2" not in test_obj.proxies
# Signal listener for second interface should disconnect, first remains
test_service_2.signal_test()
await test_service_2.ping()
assert callback_count == 0
test_service.signal_test()
await test_service.ping()
assert callback_count == 1
callback_count = 0
# Should be able to disconnect first signal listener on new proxy obj
test_obj.off_test(test_callback)
test_service.signal_test()
await test_service.ping()
assert callback_count == 0
def test_from_dbus_error():
"""Test converting DBus fast errors to Supervisor specific errors."""
dbus_fast_error = DBusFastDBusError(
ErrorType.SERVICE_UNKNOWN, "The name is not activatable"
)
assert type(DBus.from_dbus_error(dbus_fast_error)) is DBusServiceUnkownError