Coverage for tests/tests_rf/conftest.py: 0%

1#!/usr/bin/env python3

2"""Fixtures for testing."""

3

4 import logging

5 import os

6 from collections.abc import AsyncGenerator

7 from pathlib import Path

8 from typing import Final, NoReturn, TypeAlias, TypedDict

9 from unittest.mock import patch

10

11 import pytest

12 import serial as ser # type: ignore[import-untyped]

13 from serial.tools.list_ports import comports # type: ignore[import-untyped]

14

15 from ramses_rf import Gateway

16 from ramses_rf.device import HgiGateway

17 from ramses_tx import exceptions as exc

18 from ramses_tx.address import HGI_DEVICE_ID

19 from ramses_tx.schemas import DeviceIdT

20 from tests_rf.virtual_rf import HgiFwTypes, VirtualRf

21

22#

23 PortStrT: TypeAlias = str

24

25 TEST_DIR = Path(__file__).resolve().parent # TEST_DIR = f"{os.path.dirname(__file__)}"

26

27 SZ_GWY_CONFIG: Final = "gwy_config"

28 SZ_GWY_DEV_ID: Final = "gwy_dev_id"

29

30

31 class _ConfigDictT(TypedDict):

32 disable_discovery: bool

33 disable_qos: bool

34 enforce_known_list: bool

35

36

37 class _GwyConfigDictT(TypedDict):

38 config: _ConfigDictT

39 known_list: dict[DeviceIdT, dict[str, bool]]

40

41

42 _LOGGER = logging.getLogger(__name__)

43

44

45 IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"

46# set -x GITHUB_ACTIONS true

47# set -u GITHUB_ACTIONS

48

49 _global_failed_ports: list[str] = []

50

51

52#######################################################################################

53

54# pytestmark = pytest.mark.asyncio(scope="function") # needed?

55

56

57@pytest.fixture(autouse=True)

58 def 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)

64

65

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

70

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()

79

80# request.addfinalizer(finalize)

81

82

83#######################################################################################

84

85

86@pytest.fixture()

87 async def rf() -> AsyncGenerator[VirtualRf, None]:

88 """Utilize a virtual evofw3-compatible gateway."""

89

90 rf = VirtualRf(2)

91

92 try:

93 yield rf

94 finally:

95 await rf.stop()

96

97

98@pytest.fixture()

99 def fake_evofw3_port(request: pytest.FixtureRequest, rf: VirtualRf) -> PortStrT | None:

100 """Utilize a virtual evofw3-compatible gateway.

101

102 Requires test to supply the gwy_dev_id fixture.

103 """

104

105 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)

106

107 rf.set_gateway(rf.ports[0], gwy_dev_id, fw_type=HgiFwTypes.EVOFW3)

108

109 # with patch("ramses_tx.transport.comports", rf.comports):

110 return rf.ports[0]

111

112

113@pytest.fixture()

114 def fake_ti3410_port(request: pytest.FixtureRequest, rf: VirtualRf) -> PortStrT | None:

115 """Utilize a virtual HGI80-compatible gateway.

116

117 Requires test to supply the gwy_dev_id fixture.

118 """

119

120 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)

121

122 rf.set_gateway(rf.ports[0], gwy_dev_id, fw_type=HgiFwTypes.HGI_80)

123

124 # with patch("ramses_tx.transport.comports", rf.comports):

125 return rf.ports[0]

126

127

128@pytest.fixture() # TODO: remove HACK, below

129 async def mqtt_evofw3_port() -> PortStrT:

130 """Utilize an actual evofw3-compatible gateway."""

131

132 # We could mock the MQTT client at: patch("ramses_tx.transport.MqttTransport"

133 pytest.skip("This test fixture requires an MQTT broker")

134

135 return "mqtt://mqtt_username:mqtt_passw0rd@127.0.0.1" # type: ignore[unreachable]

136

137

138@pytest.fixture() # TODO: remove HACK, below

139 async def real_evofw3_port() -> PortStrT | NoReturn:

140 """Utilize an actual evofw3-compatible gateway."""

141

142 if IN_GITHUB_ACTIONS: # replace with your condition

143 pytest.skip("This test fixture requires physical hardware")

144

145 port_names: list[PortStrT] = [

146 p.device for p in comports() if p.product and "evofw3" in p.product

147 ]

148

149 if port_names:

150 return port_names[0]

151

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]

157

158 pytest.skip("No evofw3-based gateway device found")

159

160

161@pytest.fixture()

162 async def real_ti3410_port() -> PortStrT | NoReturn:

163 """Utilize an actual HGI80-compatible gateway."""

164

165 if IN_GITHUB_ACTIONS: # replace with your condition

166 pytest.skip("This test fixture requires physical hardware")

167

168 port_names: list[PortStrT] = [

169 p.device for p in comports() if p.product and "TUSB3410" in p.product

170 ]

171

172 if port_names:

173 return port_names[0]

174

175 pytest.skip("No ti3410-based gateway device found")

176

177

178#######################################################################################

179

180

181 async def _gateway(gwy_port: PortStrT, gwy_config: _GwyConfigDictT) -> Gateway:

182 """Instantiate a gateway."""

183

184 gwy = Gateway(gwy_port, **gwy_config)

185

186 assert gwy.hgi is None and gwy.devices == []

187

188 await gwy.start()

189 return gwy

190

191

192 async def _fake_gateway(

193 gwy_port: PortStrT, gwy_config: _GwyConfigDictT, rf: VirtualRf

194) -> Gateway:

195 """Wrapper to instantiate a virtual gateway."""

196

197 with patch("ramses_tx.transport.comports", rf.comports):

198 gwy = await _gateway(gwy_port, gwy_config)

199

200 assert gwy._transport # mypy

201 gwy._transport._extra["virtual_rf"] = rf

202 return gwy

203

204

205 async def _real_gateway(gwy_port: PortStrT, gwy_config: _GwyConfigDictT) -> Gateway:

206 """Wrapper to instantiate a physical gateway."""

207

208 global _global_failed_ports

209

210 if gwy_port in _global_failed_ports:

211 pytest.skip(f"Port {gwy_port} previously failed")

212

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

218

219

220@pytest.fixture()

221 async 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).

225

226 Requires test to supply gwy_config & gwy_dev_id (used by fake_evofw3_port) fixtures.

227 """

228

229 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)

230 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)

231

232 gwy = await _fake_gateway(fake_evofw3_port, gwy_config, rf)

233

234 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id == gwy_dev_id

235 assert gwy._protocol._is_evofw3 is True

236

237 try:

238 yield gwy

239 finally:

240 await gwy.stop()

241

242

243@pytest.fixture()

244 async 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).

248

249 Requires test to supply gwy_config & gwy_dev_id (used by fake_ti3410_port) fixtures.

250 """

251

252 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)

253 gwy_dev_id: DeviceIdT = request.getfixturevalue(SZ_GWY_DEV_ID)

254

255 gwy = await _fake_gateway(fake_ti3410_port, gwy_config, rf)

256

257 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id == gwy_dev_id

258 assert gwy._protocol._is_evofw3 is False

259

260 try:

261 yield gwy

262 finally:

263 await gwy.stop()

264

265

266@pytest.fixture()

267 async 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).

271

272 Requires test to supply corresponding gwy_config fixture.

273 """

274

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)

278

279 gwy.get_device(gwy._protocol.hgi_id) # HACK: not instantiated: no puzzle pkts sent

280

281 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)

282 assert gwy._protocol._is_evofw3 is True

283

284 try:

285 yield gwy

286 finally:

287 await gwy.stop()

288

289

290@pytest.fixture()

291 async 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).

295

296 Requires test to supply corresponding gwy_config fixture.

297 """

298

299 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)

300

301 gwy = await _real_gateway(real_evofw3_port, gwy_config)

302

303 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)

304 assert gwy._protocol._is_evofw3 is True

305

306 try:

307 yield gwy

308 finally:

309 await gwy.stop()

310

311

312@pytest.fixture()

313 async 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).

317

318 Requires test to supply corresponding gwy_config fixture.

319 """

320

321 gwy_config: _GwyConfigDictT = request.getfixturevalue(SZ_GWY_CONFIG)

322

323 gwy = await _real_gateway(real_ti3410_port, gwy_config)

324

325 assert isinstance(gwy.hgi, HgiGateway) and gwy.hgi.id not in (None, HGI_DEVICE_ID)

326 assert gwy._protocol._is_evofw3 is False

327

328 try:

329 yield gwy

330 finally:

331 await gwy.stop()