Skip to content

Commit

Permalink
Use zigpy SerialProtocol (#256)
Browse files Browse the repository at this point in the history
* Use zigpy flow control

* Bump minimum zigpy version

* Upgrade pytest-asyncio

* Use `znp.disconnect` instead of `znp.close`

* Try to improve joining test reliability

* Try to wait for device initialization tasks
  • Loading branch information
puddly authored Oct 27, 2024
1 parent 06e3054 commit 353b94e
Show file tree
Hide file tree
Showing 19 changed files with 103 additions and 156 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"
license = {text = "GPL-3.0"}
requires-python = ">=3.8"
dependencies = [
"zigpy>=0.69.0",
"zigpy>=0.70.0",
"async_timeout",
"voluptuous",
"coloredlogs",
Expand Down Expand Up @@ -63,6 +63,7 @@ timeout = 20
log_format = "%(asctime)s.%(msecs)03d %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.flake8]
exclude = ".venv,.git,.tox,docs,venv,bin,lib,deps,build"
Expand Down
24 changes: 12 additions & 12 deletions tests/api/test_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def test_connect_no_test(make_znp_server):
# Nothing will be sent
assert znp_server._uart.data_received.call_count == 0

znp.close()
await znp.disconnect()


@pytest.mark.parametrize("work_after_attempt", [1, 2, 3])
Expand All @@ -44,7 +44,7 @@ def ping_rsp(req):

await znp.connect(test_port=True)

znp.close()
await znp.disconnect()


async def test_connect_skip_bootloader_batched_rsp(make_znp_server, mocker):
Expand Down Expand Up @@ -82,7 +82,7 @@ def ping_rsp(req):

await znp.connect(test_port=True)

znp.close()
await znp.disconnect()


async def test_connect_skip_bootloader_failure(make_znp_server):
Expand All @@ -92,7 +92,7 @@ async def test_connect_skip_bootloader_failure(make_znp_server):
with pytest.raises(asyncio.TimeoutError):
await znp.connect(test_port=True)

znp.close()
await znp.disconnect()


async def test_connect_skip_bootloader_rts_dtr_pins(make_znp_server, mocker):
Expand All @@ -112,7 +112,7 @@ async def test_connect_skip_bootloader_rts_dtr_pins(make_znp_server, mocker):
assert serial._mock_dtr_prop.mock_calls == [call(False), call(False), call(False)]
assert serial._mock_rts_prop.mock_calls == [call(False), call(True), call(False)]

znp.close()
await znp.disconnect()


async def test_connect_skip_bootloader_config(make_znp_server, mocker):
Expand All @@ -133,24 +133,24 @@ async def test_connect_skip_bootloader_config(make_znp_server, mocker):
assert serial._mock_dtr_prop.called is False
assert serial._mock_rts_prop.called is False

znp.close()
await znp.disconnect()


async def test_api_close(connected_znp, mocker):
znp, znp_server = connected_znp
uart = znp._uart
mocker.spy(uart, "close")

znp.close()
await znp.disconnect()

# Make sure our UART was actually closed
assert znp._uart is None
assert znp._app is None
assert uart.close.call_count == 1

# ZNP.close should not throw any errors if called multiple times
znp.close()
znp.close()
# ZNP.disconnect should not throw any errors if called multiple times
await znp.disconnect()
await znp.disconnect()

def dict_minus(d, minus):
return {k: v for k, v in d.items() if k not in minus}
Expand All @@ -165,8 +165,8 @@ def dict_minus(d, minus):
znp2.__dict__, ignored_keys
)

znp2.close()
znp2.close()
await znp2.disconnect()
await znp2.disconnect()

assert dict_minus(znp.__dict__, ignored_keys) == dict_minus(
znp2.__dict__, ignored_keys
Expand Down
14 changes: 7 additions & 7 deletions tests/api/test_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from zigpy_znp.api import OneShotResponseListener, CallbackResponseListener


async def test_resolve(event_loop, mocker):
async def test_resolve(mocker):
callback = mocker.Mock()
callback_listener = CallbackResponseListener(
[c.SYS.Ping.Rsp(partial=True)], callback
)

future = event_loop.create_future()
future = asyncio.get_running_loop().create_future()
one_shot_listener = OneShotResponseListener([c.SYS.Ping.Rsp(partial=True)], future)

match = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
Expand Down Expand Up @@ -42,9 +42,9 @@ async def test_resolve(event_loop, mocker):
assert one_shot_listener.cancel()


async def test_cancel(event_loop):
async def test_cancel():
# Cancelling a one-shot listener prevents it from being fired
future = event_loop.create_future()
future = asyncio.get_running_loop().create_future()
one_shot_listener = OneShotResponseListener([c.SYS.Ping.Rsp(partial=True)], future)
one_shot_listener.cancel()

Expand All @@ -55,13 +55,13 @@ async def test_cancel(event_loop):
await future


async def test_multi_cancel(event_loop, mocker):
async def test_multi_cancel(mocker):
callback = mocker.Mock()
callback_listener = CallbackResponseListener(
[c.SYS.Ping.Rsp(partial=True)], callback
)

future = event_loop.create_future()
future = asyncio.get_running_loop().create_future()
one_shot_listener = OneShotResponseListener([c.SYS.Ping.Rsp(partial=True)], future)

match = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
Expand Down Expand Up @@ -93,7 +93,7 @@ async def test_api_cancel_listeners(connected_znp, mocker):
)

assert not future.done()
znp.close()
await znp.disconnect()

with pytest.raises(asyncio.CancelledError):
await future
Expand Down
10 changes: 5 additions & 5 deletions tests/api/test_network_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def test_state_transfer(from_device, to_device, make_connected_znp):
formed_znp, _ = await make_connected_znp(server_cls=from_device)

await formed_znp.load_network_info()
formed_znp.close()
await formed_znp.disconnect()

empty_znp, _ = await make_connected_znp(server_cls=to_device)

Expand Down Expand Up @@ -72,15 +72,15 @@ async def test_broken_cc2531_load_state(device, make_connected_znp, caplog):
await znp.load_network_info()
assert "inconsistent" in caplog.text

znp.close()
await znp.disconnect()


@pytest.mark.parametrize("device", [FormedZStack3CC2531])
async def test_state_write_tclk_zstack3(device, make_connected_znp, caplog):
formed_znp, _ = await make_connected_znp(server_cls=device)

await formed_znp.load_network_info()
formed_znp.close()
await formed_znp.disconnect()

empty_znp, _ = await make_connected_znp(server_cls=device)

Expand All @@ -106,7 +106,7 @@ async def test_state_write_tclk_zstack3(device, make_connected_znp, caplog):
async def test_write_settings_fast(device, make_connected_znp):
formed_znp, _ = await make_connected_znp(server_cls=FormedLaunchpadCC26X2R1)
await formed_znp.load_network_info()
formed_znp.close()
await formed_znp.disconnect()

znp, _ = await make_connected_znp(server_cls=device)

Expand All @@ -126,7 +126,7 @@ async def test_write_settings_fast(device, make_connected_znp):
async def test_formation_failure_on_corrupted_nvram(device, make_connected_znp):
formed_znp, _ = await make_connected_znp(server_cls=FormedLaunchpadCC26X2R1)
await formed_znp.load_network_info()
formed_znp.close()
await formed_znp.disconnect()

znp, znp_server = await make_connected_znp(server_cls=device)

Expand Down
24 changes: 12 additions & 12 deletions tests/api/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse


async def test_callback_rsp(connected_znp, event_loop):
async def test_callback_rsp(connected_znp):
znp, znp_server = connected_znp

def send_responses():
Expand All @@ -20,7 +20,7 @@ def send_responses():
c.AF.DataConfirm.Callback(Endpoint=56, TSN=1, Status=t.Status.SUCCESS)
)

event_loop.call_soon(send_responses)
asyncio.get_running_loop().call_soon(send_responses)

# The UART sometimes replies with a SRSP and an AREQ faster than
# we can register callbacks for both. This method is a workaround.
Expand Down Expand Up @@ -150,7 +150,7 @@ async def replier(req):
assert len(znp._unhandled_command.mock_calls) == 0


async def test_callback_rsp_cleanup_concurrent(connected_znp, event_loop, mocker):
async def test_callback_rsp_cleanup_concurrent(connected_znp, mocker):
znp, znp_server = connected_znp

mocker.spy(znp, "_unhandled_command")
Expand All @@ -163,7 +163,7 @@ def send_responses():
znp_server.send(c.SYS.OSALTimerExpired.Callback(Id=0xAB))
znp_server.send(c.SYS.OSALTimerExpired.Callback(Id=0xCD))

event_loop.call_soon(send_responses)
asyncio.get_running_loop().call_soon(send_responses)

callback_rsp = await znp.request_callback_rsp(
request=c.UTIL.TimeAlive.Req(),
Expand All @@ -183,7 +183,7 @@ def send_responses():
]


async def test_znp_request_kwargs(connected_znp, event_loop):
async def test_znp_request_kwargs(connected_znp):
znp, znp_server = connected_znp

# Invalid format
Expand All @@ -196,7 +196,7 @@ async def test_znp_request_kwargs(connected_znp, event_loop):

# Valid format, valid name
ping_rsp = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
event_loop.call_soon(znp_server.send, ping_rsp)
asyncio.get_running_loop().call_soon(znp_server.send, ping_rsp)
assert (
await znp.request(c.SYS.Ping.Req(), RspCapabilities=t.MTCapabilities.SYS)
) == ping_rsp
Expand Down Expand Up @@ -227,7 +227,7 @@ async def test_znp_request_kwargs(connected_znp, event_loop):
)


async def test_znp_request_not_recognized(connected_znp, event_loop):
async def test_znp_request_not_recognized(connected_znp):
znp, _ = connected_znp

# An error is raise when a bad request is sent
Expand All @@ -237,11 +237,11 @@ async def test_znp_request_not_recognized(connected_znp, event_loop):
)

with pytest.raises(CommandNotRecognized):
event_loop.call_soon(znp.frame_received, unknown_rsp.to_frame())
asyncio.get_running_loop().call_soon(znp.frame_received, unknown_rsp.to_frame())
await znp.request(request)


async def test_znp_request_wrong_params(connected_znp, event_loop):
async def test_znp_request_wrong_params(connected_znp):
znp, _ = connected_znp

# You cannot specify response kwargs for responses with no response
Expand All @@ -250,14 +250,14 @@ async def test_znp_request_wrong_params(connected_znp, event_loop):

# An error is raised when a response with bad params is received
with pytest.raises(InvalidCommandResponse):
event_loop.call_soon(
asyncio.get_running_loop().call_soon(
znp.frame_received,
c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS).to_frame(),
)
await znp.request(c.SYS.Ping.Req(), RspCapabilities=t.MTCapabilities.APP)


async def test_znp_sreq_srsp(connected_znp, event_loop):
async def test_znp_sreq_srsp(connected_znp):
znp, _ = connected_znp

# Each SREQ must have a corresponding SRSP, so this will fail
Expand All @@ -267,7 +267,7 @@ async def test_znp_sreq_srsp(connected_znp, event_loop):

# This will work
ping_rsp = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
event_loop.call_soon(znp.frame_received, ping_rsp.to_frame())
asyncio.get_running_loop().call_soon(znp.frame_received, ping_rsp.to_frame())

await znp.request(c.SYS.Ping.Req())

Expand Down
6 changes: 3 additions & 3 deletions tests/api/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ async def test_wait_responses_empty(connected_znp):
await znp.wait_for_responses([])


async def test_response_callback_simple(connected_znp, event_loop, mocker):
async def test_response_callback_simple(connected_znp, mocker):
znp, _ = connected_znp

sync_callback = mocker.Mock()
Expand All @@ -207,7 +207,7 @@ async def test_response_callback_simple(connected_znp, event_loop, mocker):
sync_callback.assert_called_once_with(good_response)


async def test_response_callbacks(connected_znp, event_loop, mocker):
async def test_response_callbacks(connected_znp, mocker):
znp, _ = connected_znp

sync_callback = mocker.Mock()
Expand Down Expand Up @@ -270,7 +270,7 @@ async def async_callback(response):
assert len(async_callback_responses) == 3


async def test_wait_for_responses(connected_znp, event_loop):
async def test_wait_for_responses(connected_znp):
znp, _ = connected_znp

response1 = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
Expand Down
15 changes: 15 additions & 0 deletions tests/application/test_joining.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
await app.shutdown()


@mock.patch(
"zigpy.device.Device._initialize",
new=zigpy.device.Device._initialize.__wrapped__, # to disable retries
)
@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_on_zdo_device_join(device, make_application, mocker):
app, znp_server = make_application(server_cls=device)
Expand All @@ -204,6 +208,10 @@ async def test_on_zdo_device_join(device, make_application, mocker):
await app.shutdown()


@mock.patch(
"zigpy.device.Device._initialize",
new=zigpy.device.Device._initialize.__wrapped__, # to disable retries
)
@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_on_zdo_device_join_and_announce_fast(device, make_application, mocker):
app, znp_server = make_application(server_cls=device)
Expand Down Expand Up @@ -258,8 +266,12 @@ async def test_on_zdo_device_join_and_announce_fast(device, make_application, mo
# Everything is cleaned up
assert not app._join_announce_tasks

app.get_device(ieee=ieee).cancel_initialization()
await app.shutdown()

with pytest.raises(asyncio.CancelledError):
await app.get_device(ieee=ieee)._initialize_task


@mock.patch("zigpy_znp.zigbee.application.DEVICE_JOIN_MAX_DELAY", new=0.1)
@mock.patch(
Expand Down Expand Up @@ -329,3 +341,6 @@ async def test_on_zdo_device_join_and_announce_slow(device, make_application, mo

Check failure on line 341 in tests/application/test_joining.py

View workflow job for this annotation

GitHub Actions / shared-ci / Run tests Python 3.11.0

test_on_zdo_device_join_and_announce_fast[FormedLaunchpadCC26X2R1] pytest.PytestUnraisableExceptionWarning: Exception ignored in: <coroutine object Device._initialize at 0x7ff7a6ea4040> Traceback (most recent call last): File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/device.py", line 381, in request return await req.result ^^^^^^^^^^^^^^^^ GeneratorExit During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/device.py", line 258, in _initialize await self.get_node_descriptor() File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/device.py", line 220, in get_node_descriptor status, _, node_desc = await self.zdo.Node_Desc_req( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/contextlib.py", line 222, in __aexit__ await self.gen.athrow(typ, value, traceback) File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/device.py", line 127, in _limit_concurrency yield File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/device.py", line 377, in request with self._pending.new(sequence) as req: File "/home/runner/work/zigpy-znp/zigpy-znp/venv/lib/python3.11/site-packages/zigpy/util.py", line 289, in __exit__ self.result.cancel() File "/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/asyncio/base_events.py", line 758, in call_soon self._check_closed() File "/opt/hostedtoolcache/Python/3.11.0/x64/lib/python3.11/asyncio/base_events.py", line 519, in _check_closed raise RuntimeError('Event loop is closed') RuntimeError: Event loop is closed
app.get_device(ieee=ieee).cancel_initialization()
await app.shutdown()

with pytest.raises(asyncio.CancelledError):
await app.get_device(ieee=ieee)._initialize_task
Loading

0 comments on commit 353b94e

Please sign in to comment.