mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-02 16:22:51 +01:00
* Use Python 3.14(.3) in CI and base image Update base image to the latest tag using Python 3.14.3 and update Python version in CI workflows to 3.14. With Python 3.14, backports.zstd is no longer necessary as it's now available in the standard library. * Update wheels ABI in the wheels builder to cp314 * Use explicit Python fix version in GH actions Specify explicitly Python 3.14.3, as the setup-python action otherwise default to 3.14.2 when 3.14.3, leading to different version in CI and in production. * Update Python version references in pyproject.toml * Fix all ruff quoted-annotation (UP037) errors * Revert unquoting of DBus types in tests and ignore UP037 where needed
193 lines
6.1 KiB
Python
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, UP037
|
|
"""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
|