Coverage for tests/tests_rf/conftest.py: 0%
142 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
1#!/usr/bin/env python3
2"""Fixtures for testing."""
4import logging
5import os
6from collections.abc import AsyncGenerator
7from pathlib import Path
8from typing import Final, NoReturn, TypeAlias, TypedDict
9from unittest.mock import patch
11import pytest
12import serial as ser # type: ignore[import-untyped]
13from serial.tools.list_ports import comports # type: ignore[import-untyped]
15from ramses_rf import Gateway
16from ramses_rf.device import HgiGateway
17from ramses_tx import exceptions as exc
18from ramses_tx.address import HGI_DEVICE_ID
19from ramses_tx.schemas import DeviceIdT
20from tests_rf.virtual_rf import HgiFwTypes, VirtualRf
22#
23PortStrT: TypeAlias = str
25TEST_DIR = Path(__file__).resolve().parent # TEST_DIR = f"{os.path.dirname(__file__)}"
27SZ_GWY_CONFIG: Final = "gwy_config"
28SZ_GWY_DEV_ID: Final = "gwy_dev_id"
31class _ConfigDictT(TypedDict):
32 disable_discovery: bool
33 disable_qos: bool
34 enforce_known_list: bool
37class _GwyConfigDictT(TypedDict):
38 config: _ConfigDictT
39 known_list: dict[DeviceIdT, dict[str, bool]]
42_LOGGER = logging.getLogger(__name__)
45IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
46# set -x GITHUB_ACTIONS true
47# set -u GITHUB_ACTIONS
49_global_failed_ports: list[str] = []
52#######################################################################################
54# pytestmark = pytest.mark.asyncio(scope="function") # needed?
57@pytest.fixture(autouse=True)
58def patches_for_tests(monkeypatch: pytest.MonkeyPatch) -> None:
59 monkeypatch.setattr("ramses_tx.protocol._DBG_DISABLE_IMPERSONATION_ALERTS", True)
60 monkeypatch.setattr("ramses_tx.transport._DBG_DISABLE_DUTY_CYCLE_LIMIT", True)
61 monkeypatch.setattr("ramses_tx.transport.MIN_INTER_WRITE_GAP", 0)
62 monkeypatch.setattr("ramses_tx.transport._DEFAULT_TIMEOUT_MQTT", 2)
63 monkeypatch.setattr("ramses_tx.transport._DEFAULT_TIMEOUT_PORT", 0.5)
66# TODO: add teardown to cleanup orphan MessageIndex thread
67# @pytest.fixture(scope="session", autouse=True)
68# def close_timer_threads(request: pytest.FixtureRequest) -> None:
69# import threading
71# def finalize() -> None:
72# running_timer_threads = [
73# thread
74# for thread in threading.enumerate()
75# if isinstance(thread, threading.Timer)
76# ]
77# for timer in running_timer_threads:
78# timer.cancel()
80# request.addfinalizer(finalize)
83#######################################################################################
86@pytest.fixture()
87async def rf() -> AsyncGenerator[VirtualRf, None]:
88 """Utilize a virtual evofw3-compatible gateway."""
90 rf = VirtualRf(2)
92 try:
93 yield rf
94 finally:
95 await rf.stop()
98@pytest.fixture()
99def fake_evofw3_port(request: pytest.FixtureRequest, rf: VirtualRf) -> PortStrT | None:
100 """Utilize a virtual evofw3-compatible gateway.
102 Requires test to supply the gwy_dev_id fixture.
103 """
105 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)
107 rf.set_gateway(rf.ports[0], gwy_dev_id, fw_type=HgiFwTypes.EVOFW3)
109 # with patch("ramses_tx.transport.comports", rf.comports):
110 return rf.ports[0]
113@pytest.fixture()
114def fake_ti3410_port(request: pytest.FixtureRequest, rf: VirtualRf) -> PortStrT | None:
115 """Utilize a virtual HGI80-compatible gateway.
117 Requires test to supply the gwy_dev_id fixture.
118 """
120 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)
122 rf.set_gateway(rf.ports[0], gwy_dev_id, fw_type=HgiFwTypes.HGI_80)
124 # with patch("ramses_tx.transport.comports", rf.comports):
125 return rf.ports[0]
128@pytest.fixture() # TODO: remove HACK, below
129async def mqtt_evofw3_port() -> PortStrT:
130 """Utilize an actual evofw3-compatible gateway."""
132 # We could mock the MQTT client at: patch("ramses_tx.transport.MqttTransport"
133 pytest.skip("This test fixture requires an MQTT broker")
135 return "mqtt://mqtt_username:mqtt_passw0rd@127.0.0.1" # type: ignore[unreachable]
138@pytest.fixture() # TODO: remove HACK, below
139async def real_evofw3_port() -> PortStrT | NoReturn:
140 """Utilize an actual evofw3-compatible gateway."""
142 if IN_GITHUB_ACTIONS: # replace with your condition
143 pytest.skip("This test fixture requires physical hardware")
145 port_names: list[PortStrT] = [
146 p.device for p in comports() if p.product and "evofw3" in p.product
147 ]
149 if port_names:
150 return port_names[0]
152 if port_names := [
153 p.device for p in comports() if p.name[:6] == "ttyACM"
154 ]: # HACK: evofw3-esp
155 _LOGGER.warning(f"Assuming {port_names[0]} is evofw3-compatible")
156 return port_names[0]
158 pytest.skip("No evofw3-based gateway device found")
161@pytest.fixture()
162async def real_ti3410_port() -> PortStrT | NoReturn:
163 """Utilize an actual HGI80-compatible gateway."""
165 if IN_GITHUB_ACTIONS: # replace with your condition
166 pytest.skip("This test fixture requires physical hardware")
168 port_names: list[PortStrT] = [
169 p.device for p in comports() if p.product and "TUSB3410" in p.product
170 ]
172 if port_names:
173 return port_names[0]
175 pytest.skip("No ti3410-based gateway device found")
178#######################################################################################
181async def _gateway(gwy_port: PortStrT, gwy_config: _GwyConfigDictT) -> Gateway:
182 """Instantiate a gateway."""
184 gwy = Gateway(gwy_port, **gwy_config)
186 assert gwy.hgi is None and gwy.devices == []
188 await gwy.start()
189 return gwy
192async def _fake_gateway(
193 gwy_port: PortStrT, gwy_config: _GwyConfigDictT, rf: VirtualRf
194) -> Gateway:
195 """Wrapper to instantiate a virtual gateway."""
197 with patch("ramses_tx.transport.comports", rf.comports):
198 gwy = await _gateway(gwy_port, gwy_config)
200 assert gwy._transport # mypy
201 gwy._transport._extra["virtual_rf"] = rf
202 return gwy
205async def _real_gateway(gwy_port: PortStrT, gwy_config: _GwyConfigDictT) -> Gateway:
206 """Wrapper to instantiate a physical gateway."""
208 global _global_failed_ports
210 if gwy_port in _global_failed_ports:
211 pytest.skip(f"Port {gwy_port} previously failed")
213 try:
214 return await _gateway(gwy_port, gwy_config)
215 except (ser.SerialException, exc.TransportError) as err:
216 _global_failed_ports.append(gwy_port)
217 pytest.xfail(str(err)) # not skip, as we had determined port exists elsewhere
220@pytest.fixture()
221async def fake_evofw3(
222 fake_evofw3_port: PortStrT, request: pytest.FixtureRequest, rf: VirtualRf
223) -> AsyncGenerator[Gateway, None]:
224 """Utilize a virtual evofw3-compatible gateway (discovered by fake_evofw3_port).
226 Requires test to supply gwy_config & gwy_dev_id (used by fake_evofw3_port) fixtures.
227 """
229 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)
230 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)
232 gwy = await _fake_gateway(fake_evofw3_port, gwy_config, rf)
234 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id == gwy_dev_id
235 assert gwy._protocol._is_evofw3 is True
237 try:
238 yield gwy
239 finally:
240 await gwy.stop()
243@pytest.fixture()
244async def fake_ti3410(
245 fake_ti3410_port: PortStrT, request: pytest.FixtureRequest, rf: VirtualRf
246) -> AsyncGenerator[Gateway, None]:
247 """Utilize a virtual HGI80-compatible gateway (discovered by fake_ti3410_port).
249 Requires test to supply gwy_config & gwy_dev_id (used by fake_ti3410_port) fixtures.
250 """
252 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)
253 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)
255 gwy = await _fake_gateway(fake_ti3410_port, gwy_config, rf)
257 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id == gwy_dev_id
258 assert gwy._protocol._is_evofw3 is False
260 try:
261 yield gwy
262 finally:
263 await gwy.stop()
266@pytest.fixture()
267async def mqtt_evofw3(
268 mqtt_evofw3_port: PortStrT, request: pytest.FixtureRequest
269) -> AsyncGenerator[Gateway, None]:
270 """Utilize an actual evofw3-compatible gateway (discovered by mqtt_evofw3_port).
272 Requires test to supply corresponding gwy_config fixture.
273 """
275 # TODO: pytest.skip() if the MQTT broker is available
276 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)
277 gwy = await _gateway(mqtt_evofw3_port, gwy_config)
279 gwy.get_device(gwy._protocol.hgi_id) # HACK: not instantiated: no puzzle pkts sent
281 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)
282 assert gwy._protocol._is_evofw3 is True
284 try:
285 yield gwy
286 finally:
287 await gwy.stop()
290@pytest.fixture()
291async def real_evofw3(
292 real_evofw3_port: PortStrT, request: pytest.FixtureRequest
293) -> AsyncGenerator[Gateway, None]:
294 """Utilize an actual evofw3-compatible gateway (discovered by real_evofw3_port).
296 Requires test to supply corresponding gwy_config fixture.
297 """
299 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)
301 gwy = await _real_gateway(real_evofw3_port, gwy_config)
303 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)
304 assert gwy._protocol._is_evofw3 is True
306 try:
307 yield gwy
308 finally:
309 await gwy.stop()
312@pytest.fixture()
313async def real_ti3410(
314 real_ti3410_port: PortStrT, request: pytest.FixtureRequest
315) -> AsyncGenerator[Gateway, None]:
316 """Utilize an actual HGI80-compatible gateway (discovered by real_ti3410_port).
318 Requires test to supply corresponding gwy_config fixture.
319 """
321 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)
323 gwy = await _real_gateway(real_ti3410_port, gwy_config)
325 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)
326 assert gwy._protocol._is_evofw3 is False
328 try:
329 yield gwy
330 finally:
331 await gwy.stop()