Skip to content

Commit 8b1da34

Browse files
committed
Add some tests
1 parent 7b04740 commit 8b1da34

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

tests/test_ash.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,43 @@ async def test_ash_end_to_end(transport_cls: type[FakeTransport]) -> None:
605605
await host.send_data(b"ncp NAKing until failure")
606606

607607

608+
async def test_rstack_cancels_pending_frames() -> None:
609+
"""Test that RSTACK frame cancels pending data frames."""
610+
host_ezsp = MagicMock()
611+
ncp_ezsp = MagicMock()
612+
613+
host = ash.AshProtocol(host_ezsp)
614+
ncp = AshNcpProtocol(ncp_ezsp)
615+
616+
host_transport = FakeTransport(ncp)
617+
ncp_transport = FakeTransport(host)
618+
619+
host.connection_made(host_transport)
620+
ncp.connection_made(ncp_transport)
621+
622+
# Pause the NCP transport so ACKs can't be sent back, creating a pending frame
623+
ncp_transport.paused = True
624+
625+
# Start sending data without awaiting - this will create a pending frame
626+
send_task = asyncio.create_task(host.send_data(b"test data"))
627+
628+
# Give task time to start and create the pending frame
629+
await asyncio.sleep(0.1)
630+
631+
# Verify we have a pending frame
632+
assert len(host._pending_data_frames) == 1
633+
634+
# Trigger RSTACK frame to cancel the pending frame
635+
rstack = ash.RStackFrame(version=2, reset_code=t.NcpResetCode.RESET_POWER_ON)
636+
host.rstack_frame_received(rstack)
637+
638+
# Verify task was cancelled with NcpFailure containing the reset code
639+
with pytest.raises(ash.NcpFailure) as exc_info:
640+
await send_task
641+
642+
assert exc_info.value.code == t.NcpResetCode.RESET_POWER_ON
643+
644+
608645
def test_ncp_failure_comparison() -> None:
609646
exc1 = ash.NcpFailure(code=t.NcpResetCode.ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT)
610647
exc2 = ash.NcpFailure(code=t.NcpResetCode.RESET_POWER_ON)

tests/test_ezsp.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,28 @@ async def test_ezsp_connect_failure(disconnect_mock, reset_mock, version_mock):
304304
assert disconnect_mock.call_count == 1
305305

306306

307+
@pytest.mark.parametrize("failures_before_success", [1, 2, 3, 4])
308+
@patch.object(EZSP, "disconnect", new_callable=AsyncMock)
309+
async def test_ezsp_connect_retry_success(disconnect_mock, failures_before_success):
310+
"""Test connection succeeding after N failures."""
311+
call_count = 0
312+
313+
async def startup_reset_mock():
314+
nonlocal call_count
315+
call_count += 1
316+
if call_count <= failures_before_success:
317+
raise RuntimeError(f"Startup failed (attempt {call_count})")
318+
319+
with patch("bellows.uart.connect"):
320+
ezsp = make_ezsp(version=4)
321+
322+
with patch.object(ezsp, "startup_reset", side_effect=startup_reset_mock):
323+
await ezsp.connect()
324+
325+
assert call_count == failures_before_success + 1
326+
assert disconnect_mock.call_count == 0
327+
328+
307329
async def test_ezsp_newer_version(ezsp_f):
308330
"""Test newer version of ezsp."""
309331
with patch.object(

tests/test_uart.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,18 @@ def on_transport_close():
211211
assert len(threads) == 0
212212

213213

214-
async def test_wait_for_startup_reset(gw):
214+
@pytest.mark.parametrize(
215+
"reset_code",
216+
[
217+
t.NcpResetCode.RESET_SOFTWARE,
218+
t.NcpResetCode.RESET_POWER_ON,
219+
t.NcpResetCode.RESET_WATCHDOG,
220+
t.NcpResetCode.RESET_EXTERNAL,
221+
],
222+
)
223+
async def test_wait_for_startup_reset(gw, reset_code):
215224
loop = asyncio.get_running_loop()
216-
loop.call_later(0.01, gw.reset_received, t.NcpResetCode.RESET_SOFTWARE)
225+
loop.call_later(0.01, gw.reset_received, reset_code)
217226

218227
assert gw._startup_reset_future is None
219228
await gw.wait_for_startup_reset()
@@ -239,8 +248,25 @@ async def test_callbacks(gw):
239248
]
240249

241250

242-
def test_reset_propagation(gw):
243-
gw.reset_received(t.NcpResetCode.ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT)
251+
async def test_error_received_during_reset_ignored(gw):
252+
# Set up a reset future to simulate being in the middle of a reset
253+
loop = asyncio.get_running_loop()
254+
gw._reset_future = loop.create_future()
255+
256+
# Error should be ignored (not trigger failed state)
257+
gw.error_received(t.NcpResetCode.ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT)
258+
assert gw._api.enter_failed_state.call_count == 0
259+
260+
# Clean up
261+
gw._reset_future.cancel()
262+
263+
264+
def test_unexpected_reset_triggers_failed_state(gw):
265+
# When no reset is expected, any reset should trigger failed state
266+
assert gw._reset_future is None
267+
assert gw._startup_reset_future is None
268+
269+
gw.reset_received(t.NcpResetCode.RESET_SOFTWARE)
244270
assert gw._api.enter_failed_state.mock_calls == [
245-
call(t.NcpResetCode.ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT)
271+
call(t.NcpResetCode.RESET_SOFTWARE)
246272
]

0 commit comments

Comments
 (0)