Skip to content

Commit c2d2f5b

Browse files
authored
Zigpy serial protocol (#177)
* Migrate `Gateway` * Migrate API and Application * Bump minimum zigpy version * Get rid of unnecessary probe tests * Fix unit tests * Increase test coverage * Use correct data type when clearing buffer
1 parent 1b5da1d commit c2d2f5b

File tree

8 files changed

+94
-246
lines changed

8 files changed

+94
-246
lines changed

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
17-
"zigpy>=0.60.0",
17+
"zigpy>=0.70.0",
1818
]
1919

2020
[tool.setuptools.packages.find]
@@ -43,6 +43,7 @@ ignore_errors = true
4343

4444
[tool.pytest.ini_options]
4545
asyncio_mode = "auto"
46+
asyncio_default_fixture_loop_scope = "function"
4647

4748
[tool.flake8]
4849
exclude = [".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build"]

tests/async_mock.py

-9
This file was deleted.

tests/test_api.py

+37-101
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
"""Tests for API."""
22

33
import asyncio
4+
from unittest import mock
45

56
import pytest
6-
import serial
77
import zigpy.config
88
import zigpy.exceptions
99
import zigpy.types as t
1010

11-
from zigpy_xbee import api as xbee_api, types as xbee_t, uart
11+
from zigpy_xbee import api as xbee_api, types as xbee_t
1212
from zigpy_xbee.exceptions import ATCommandError, ATCommandException, InvalidCommand
1313
from zigpy_xbee.zigbee.application import ControllerApplication
1414

15-
import tests.async_mock as mock
16-
1715
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE(
1816
{
1917
zigpy.config.CONF_DEVICE_PATH: "/dev/null",
@@ -26,24 +24,49 @@
2624
def api():
2725
"""Sample XBee API fixture."""
2826
api = xbee_api.XBee(DEVICE_CONFIG)
29-
api._uart = mock.MagicMock()
27+
api._uart = mock.AsyncMock()
3028
return api
3129

3230

33-
async def test_connect(monkeypatch):
31+
async def test_connect():
3432
"""Test connect."""
3533
api = xbee_api.XBee(DEVICE_CONFIG)
36-
monkeypatch.setattr(uart, "connect", mock.AsyncMock())
37-
await api.connect()
34+
api._command = mock.AsyncMock(spec=api._command)
35+
36+
with mock.patch("zigpy_xbee.uart.connect"):
37+
await api.connect()
38+
39+
40+
async def test_connect_initial_timeout_success():
41+
"""Test connect, initial command times out."""
42+
api = xbee_api.XBee(DEVICE_CONFIG)
43+
api._at_command = mock.AsyncMock(side_effect=asyncio.TimeoutError)
44+
api.init_api_mode = mock.AsyncMock(return_value=True)
45+
46+
with mock.patch("zigpy_xbee.uart.connect"):
47+
await api.connect()
48+
49+
50+
async def test_connect_initial_timeout_failure():
51+
"""Test connect, initial command times out."""
52+
api = xbee_api.XBee(DEVICE_CONFIG)
53+
api._at_command = mock.AsyncMock(side_effect=asyncio.TimeoutError)
54+
api.init_api_mode = mock.AsyncMock(return_value=False)
55+
56+
with mock.patch("zigpy_xbee.uart.connect") as mock_connect:
57+
with pytest.raises(zigpy.exceptions.APIException):
58+
await api.connect()
3859

60+
assert mock_connect.return_value.disconnect.mock_calls == [mock.call()]
3961

40-
def test_close(api):
62+
63+
async def test_disconnect(api):
4164
"""Test connection close."""
4265
uart = api._uart
43-
api.close()
66+
await api.disconnect()
4467

4568
assert api._uart is None
46-
assert uart.close.call_count == 1
69+
assert uart.disconnect.call_count == 1
4770

4871

4972
def test_commands():
@@ -599,97 +622,10 @@ def test_handle_many_to_one_rri(api):
599622
api._handle_many_to_one_rri(ieee, nwk, 0)
600623

601624

602-
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=mock.AsyncMock)
603-
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
604-
async def test_probe_success(mock_connect, mock_at_cmd):
605-
"""Test device probing."""
606-
607-
res = await xbee_api.XBee.probe(DEVICE_CONFIG)
608-
assert res is True
609-
assert mock_connect.call_count == 1
610-
assert mock_connect.await_count == 1
611-
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
612-
assert mock_at_cmd.call_count == 1
613-
assert mock_connect.return_value.close.call_count == 1
614-
615-
616-
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=True)
617-
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
618-
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
619-
async def test_probe_success_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
620-
"""Test device probing."""
621-
622-
res = await xbee_api.XBee.probe(DEVICE_CONFIG)
623-
assert res is True
624-
assert mock_connect.call_count == 1
625-
assert mock_connect.await_count == 1
626-
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
627-
assert mock_at_cmd.call_count == 1
628-
assert mock_api_mode.call_count == 1
629-
assert mock_connect.return_value.close.call_count == 1
630-
631-
632-
@mock.patch.object(xbee_api.XBee, "init_api_mode")
633-
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
634-
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
635-
@pytest.mark.parametrize(
636-
"exception",
637-
(asyncio.TimeoutError, serial.SerialException, zigpy.exceptions.APIException),
638-
)
639-
async def test_probe_fail(mock_connect, mock_at_cmd, mock_api_mode, exception):
640-
"""Test device probing fails."""
641-
642-
mock_api_mode.side_effect = exception
643-
mock_api_mode.reset_mock()
644-
mock_at_cmd.reset_mock()
645-
mock_connect.reset_mock()
646-
res = await xbee_api.XBee.probe(DEVICE_CONFIG)
647-
assert res is False
648-
assert mock_connect.call_count == 1
649-
assert mock_connect.await_count == 1
650-
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
651-
assert mock_at_cmd.call_count == 1
652-
assert mock_api_mode.call_count == 1
653-
assert mock_connect.return_value.close.call_count == 1
654-
655-
656-
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=False)
657-
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
658-
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
659-
async def test_probe_fail_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
660-
"""Test device probing fails."""
661-
662-
mock_api_mode.reset_mock()
663-
mock_at_cmd.reset_mock()
664-
mock_connect.reset_mock()
665-
res = await xbee_api.XBee.probe(DEVICE_CONFIG)
666-
assert res is False
667-
assert mock_connect.call_count == 1
668-
assert mock_connect.await_count == 1
669-
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
670-
assert mock_at_cmd.call_count == 1
671-
assert mock_api_mode.call_count == 1
672-
assert mock_connect.return_value.close.call_count == 1
673-
674-
675-
@mock.patch.object(xbee_api.XBee, "connect", return_value=mock.MagicMock())
676-
async def test_xbee_new(conn_mck):
677-
"""Test new class method."""
678-
api = await xbee_api.XBee.new(mock.sentinel.application, DEVICE_CONFIG)
679-
assert isinstance(api, xbee_api.XBee)
680-
assert conn_mck.call_count == 1
681-
assert conn_mck.await_count == 1
682-
683-
684-
@mock.patch.object(xbee_api.XBee, "connect", return_value=mock.MagicMock())
685-
async def test_connection_lost(conn_mck):
625+
async def test_connection_lost(api):
686626
"""Test `connection_lost` propagataion."""
687-
api = await xbee_api.XBee.new(mock.sentinel.application, DEVICE_CONFIG)
688-
await api.connect()
689-
690-
app = api._app = mock.MagicMock()
627+
api.set_application(mock.AsyncMock())
691628

692629
err = RuntimeError()
693630
api.connection_lost(err)
694-
695-
app.connection_lost.assert_called_once_with(err)
631+
api._app.connection_lost.assert_called_once_with(err)

tests/test_application.py

+13-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for ControllerApplication."""
22

33
import asyncio
4+
from unittest import mock
45

56
import pytest
67
import zigpy.config as config
@@ -15,8 +16,6 @@
1516
import zigpy_xbee.types as xbee_t
1617
from zigpy_xbee.zigbee import application
1718

18-
import tests.async_mock as mock
19-
2019
APP_CONFIG = {
2120
config.CONF_DEVICE: {
2221
config.CONF_DEVICE_PATH: "/dev/null",
@@ -374,13 +373,12 @@ def init_api_mode_mock():
374373
api_mode = api_config_succeeds
375374
return api_config_succeeds
376375

377-
with mock.patch("zigpy_xbee.api.XBee") as XBee_mock:
378-
api_mock = mock.MagicMock()
379-
api_mock._at_command = mock.AsyncMock(side_effect=_at_command_mock)
380-
api_mock.init_api_mode = mock.AsyncMock(side_effect=init_api_mode_mock)
381-
382-
XBee_mock.new = mock.AsyncMock(return_value=api_mock)
376+
api_mock = mock.MagicMock()
377+
api_mock._at_command = mock.AsyncMock(side_effect=_at_command_mock)
378+
api_mock.init_api_mode = mock.AsyncMock(side_effect=init_api_mode_mock)
379+
api_mock.connect = mock.AsyncMock()
383380

381+
with mock.patch("zigpy_xbee.api.XBee", return_value=api_mock):
384382
await app.connect()
385383

386384
app.form_network = mock.AsyncMock()
@@ -418,23 +416,17 @@ async def test_start_network(app):
418416

419417
async def test_start_network_no_api_mode(app):
420418
"""Test start network when not in API mode."""
421-
await _test_start_network(app, ai_status=0x00, api_mode=False)
422-
assert app.state.node_info.nwk == 0x0000
423-
assert app.state.node_info.ieee == t.EUI64(range(1, 9))
424-
assert app._api.init_api_mode.call_count == 1
425-
assert app._api._at_command.call_count >= 16
419+
with pytest.raises(asyncio.TimeoutError):
420+
await _test_start_network(app, ai_status=0x00, api_mode=False)
426421

427422

428423
async def test_start_network_api_mode_config_fails(app):
429424
"""Test start network when not when API config fails."""
430-
with pytest.raises(zigpy.exceptions.ControllerException):
425+
with pytest.raises(asyncio.TimeoutError):
431426
await _test_start_network(
432427
app, ai_status=0x00, api_mode=False, api_config_succeeds=False
433428
)
434429

435-
assert app._api.init_api_mode.call_count == 1
436-
assert app._api._at_command.call_count == 1
437-
438430

439431
async def test_permit(app):
440432
"""Test permit joins."""
@@ -559,11 +551,11 @@ async def test_force_remove(app):
559551

560552
async def test_shutdown(app):
561553
"""Test application shutdown."""
562-
mack_close = mock.MagicMock()
563-
app._api.close = mack_close
564-
await app.shutdown()
554+
mock_disconnect = mock.AsyncMock()
555+
app._api.disconnect = mock_disconnect
556+
await app.disconnect()
565557
assert app._api is None
566-
assert mack_close.call_count == 1
558+
assert mock_disconnect.call_count == 1
567559

568560

569561
async def test_remote_at_cmd(app, device):

tests/test_uart.py

+9-17
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,12 @@ def test_command_mode_send(gw):
6868
gw._transport.write.assert_called_once_with(data)
6969

7070

71-
def test_close(gw):
71+
async def test_disconnect(gw):
7272
"""Test closing connection."""
73-
gw.close()
74-
assert gw._transport.close.call_count == 1
73+
transport = gw._transport
74+
asyncio.get_running_loop().call_soon(gw.connection_lost, None)
75+
await gw.disconnect()
76+
assert transport.close.call_count == 1
7577

7678

7779
def test_data_received_chunk_frame(gw):
@@ -228,22 +230,12 @@ def test_unescape_underflow(gw):
228230

229231
def test_connection_lost_exc(gw):
230232
"""Test cannection lost callback is called."""
231-
gw._connected_future = asyncio.Future()
232-
233-
gw.connection_lost(ValueError())
234-
235-
conn_lost = gw._api.connection_lost
236-
assert conn_lost.call_count == 1
237-
assert isinstance(conn_lost.call_args[0][0], Exception)
238-
assert gw._connected_future.done()
239-
assert gw._connected_future.exception()
233+
err = RuntimeError()
234+
gw.connection_lost(err)
235+
assert gw._api.connection_lost.mock_calls == [mock.call(err)]
240236

241237

242238
def test_connection_closed(gw):
243239
"""Test connection closed."""
244-
gw._connected_future = asyncio.Future()
245240
gw.connection_lost(None)
246-
247-
assert gw._api.connection_lost.call_count == 0
248-
assert gw._connected_future.done()
249-
assert gw._connected_future.result() is True
241+
assert gw._api.connection_lost.mock_calls == [mock.call(None)]

0 commit comments

Comments
 (0)