1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-04-02 16:22:51 +01:00
Files
supervisor/tests/dbus/test_interface.py
Jan Čermák b1be897439 Use Python 3.14(.3) in CI and base image (#6586)
* 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
2026-03-05 21:11:25 +01:00

244 lines
7.3 KiB
Python

"""Test dbus interface."""
import asyncio
from unittest.mock import patch
from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.service import PropertyAccess, dbus_property, signal
import pytest
from supervisor.dbus.const import DBUS_OBJECT_BASE
from supervisor.dbus.interface import DBusInterface, DBusInterfaceProxy
from supervisor.exceptions import DBusInterfaceError, DBusNotConnectedError
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"
def __init__(self, object_path: str = "/service/test/TestInterface"):
"""Initialize object."""
super().__init__()
self.object_path = object_path
@signal(name="TestSignal")
def test_signal(self, value: str) -> "s": # noqa: F821, UP037
"""Send test signal."""
return value
@dbus_property(access=PropertyAccess.READ, name="TestProp")
def test_prop(self) -> "u": # noqa: F821, UP037
"""Get test property."""
return 4
class ServiceTest(DBusInterfaceProxy):
"""DBus test class."""
bus_name = "service.test.TestInterface"
object_path = "/service/test/TestInterface"
properties_interface = "service.test.TestInterface"
@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
@pytest.fixture(name="proxy")
async def fixture_proxy(
request: pytest.FixtureRequest,
test_service: TestInterface,
dbus_session_bus: MessageBus,
) -> DBusInterfaceProxy:
"""Get a proxy."""
proxy = ServiceTest()
proxy.sync_properties = getattr(request, "param", True)
await proxy.connect(dbus_session_bus)
yield proxy
async def test_dbus_proxy_connect(
proxy: DBusInterfaceProxy, test_service: TestInterface
):
"""Test dbus proxy connect."""
assert proxy.is_connected
assert proxy.properties["TestProp"] == 4
test_service.emit_properties_changed({"TestProp": 1})
await test_service.ping()
assert proxy.properties["TestProp"] == 1
test_service.emit_properties_changed({}, ["TestProp"])
await test_service.ping()
await test_service.ping()
assert proxy.properties["TestProp"] == 4
@pytest.mark.parametrize("proxy", [False], indirect=True)
async def test_dbus_proxy_connect_no_sync(
proxy: DBusInterfaceProxy, test_service: TestInterface
):
"""Test dbus proxy connect with no properties sync."""
assert proxy.is_connected
assert proxy.properties["TestProp"] == 4
test_service.emit_properties_changed({"TestProp": 1})
await test_service.ping()
assert proxy.properties["TestProp"] == 4
@pytest.mark.parametrize("proxy", [False], indirect=True)
async def test_signal_listener_disconnect(
proxy: DBusInterfaceProxy, test_service: TestInterface
):
"""Test disconnect/delete unattaches signal listeners."""
value = None
async def callback(val: str):
nonlocal value
value = val
assert proxy.is_connected
proxy.dbus.on_test_signal(callback)
test_service.test_signal("hello")
await test_service.ping()
assert value == "hello"
proxy.disconnect()
test_service.test_signal("goodbye")
await test_service.ping()
assert value == "hello"
async def test_dbus_connected_no_raise_after_shutdown(
test_service: TestInterface, dbus_session_bus: MessageBus
):
"""Test dbus connected methods do not raise DBusNotConnectedError after shutdown."""
proxy = ServiceTest()
proxy.sync_properties = False
with pytest.raises(DBusNotConnectedError):
await proxy.update()
await proxy.connect(dbus_session_bus)
assert proxy.is_connected
proxy.shutdown()
assert proxy.is_shutdown
assert await proxy.update() is None
async def test_proxy_missing_properties_interface(dbus_session_bus: MessageBus):
"""Test proxy instance disconnects and errors when missing properties interface."""
class NoPropertiesService(DBusInterfaceProxy):
bus_name = "test.no.properties.interface"
object_path = DBUS_OBJECT_BASE
properties_interface = "test.no.properties.interface"
proxy = NoPropertiesService()
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),
pytest.raises(DBusInterfaceError),
):
await proxy.connect(dbus_session_bus)
assert proxy.is_connected is False
async def test_initialize(test_service: TestInterface, dbus_session_bus: MessageBus):
"""Test initialize for reusing connected dbus object."""
class ServiceTestInterfaceOnly(DBusInterface):
bus_name = "service.test.TestInterface"
object_path = "/service/test/TestInterface"
proxy = ServiceTestInterfaceOnly()
assert proxy.is_connected is False
# Not connected
with pytest.raises(ValueError, match="must be a connected DBus object"):
await proxy.initialize(
DBus(
dbus_session_bus,
"service.test.TestInterface",
"/service/test/TestInterface",
)
)
# Connected to wrong bus
await dbus_session_bus.request_name("service.test.TestInterface2")
with pytest.raises(
ValueError,
match="must be a DBus object connected to bus service.test.TestInterface and object /service/test/TestInterface",
):
await proxy.initialize(
await DBus.connect(
dbus_session_bus,
"service.test.TestInterface2",
"/service/test/TestInterface",
)
)
# Connected to wrong object
test_service_2 = TestInterface("/service/test/TestInterface/2")
test_service_2.export(dbus_session_bus)
with pytest.raises(
ValueError,
match="must be a DBus object connected to bus service.test.TestInterface and object /service/test/TestInterface",
):
await proxy.initialize(
await DBus.connect(
dbus_session_bus,
"service.test.TestInterface",
"/service/test/TestInterface/2",
)
)
# Connected to correct object on the correct bus
await proxy.initialize(
await DBus.connect(
dbus_session_bus,
"service.test.TestInterface",
"/service/test/TestInterface",
)
)
assert proxy.is_connected is True
async def test_stop_sync_property_changes(
proxy: DBusInterfaceProxy, test_service: TestInterface
):
"""Test stop sync property changes disables the sync via signal."""
assert proxy.is_connected
assert proxy.properties["TestProp"] == 4
test_service.emit_properties_changed({"TestProp": 1})
await test_service.ping()
assert proxy.properties["TestProp"] == 1
proxy.stop_sync_property_changes()
test_service.emit_properties_changed({"TestProp": 4})
await test_service.ping()
assert proxy.properties["TestProp"] == 1