|
6 | 6 | import pytest |
7 | 7 | from bleak.exc import BleakDBusError, BleakError |
8 | 8 |
|
9 | | -from ..ble_interface import BLEClient, BLEInterface |
| 9 | +from ..ble_interface import BLE_DISCONNECT_TIMEOUT, BLEClient, BLEInterface |
10 | 10 |
|
11 | 11 |
|
12 | 12 | @pytest.mark.unit |
@@ -88,11 +88,45 @@ def test_ble_receive_disconnect_mid_read_unwinds_cleanly(): |
88 | 88 |
|
89 | 89 | @pytest.mark.unit |
90 | 90 | def test_ble_client_disconnect_swallows_stalled_teardown(): |
91 | | - """BLEClient.disconnect must not propagate a stalled-teardown timeout, |
92 | | - so BLEInterface.close() can always finish.""" |
| 91 | + """BLEClient.disconnect must bound the wait with BLE_DISCONNECT_TIMEOUT and |
| 92 | + not propagate a stalled-teardown timeout, so BLEInterface.close() can always |
| 93 | + finish.""" |
93 | 94 | client = object.__new__(BLEClient) |
94 | 95 | client.bleak_client = MagicMock() |
95 | | - with patch.object(BLEClient, "async_await", side_effect=FutureTimeoutError()): |
| 96 | + with patch.object( |
| 97 | + BLEClient, "async_await", side_effect=FutureTimeoutError() |
| 98 | + ) as async_await: |
96 | 99 | client.disconnect() # must not raise |
| 100 | + # the wait must actually be bounded, not left unbounded |
| 101 | + assert async_await.call_args.kwargs["timeout"] == BLE_DISCONNECT_TIMEOUT |
97 | 102 | with patch.object(BLEClient, "async_await", side_effect=BleakError("gone")): |
98 | 103 | client.disconnect() # must not raise |
| 104 | + |
| 105 | + |
| 106 | +@pytest.mark.unit |
| 107 | +def test_ble_client_close_bounds_event_thread_join(): |
| 108 | + """BLEClient.close must bound the event-loop thread join so a stuck loop |
| 109 | + cannot block teardown forever.""" |
| 110 | + client = object.__new__(BLEClient) |
| 111 | + client._eventThread = MagicMock() |
| 112 | + # Force a plain (non-async) mock for the coroutine method so we don't create |
| 113 | + # an un-awaited coroutine when close() calls it. |
| 114 | + with patch.object(BLEClient, "async_run") as async_run, patch.object( |
| 115 | + BLEClient, "_stop_event_loop", new=MagicMock() |
| 116 | + ): |
| 117 | + client.close() |
| 118 | + async_run.assert_called_once() |
| 119 | + client._eventThread.join.assert_called_once_with(timeout=BLE_DISCONNECT_TIMEOUT) |
| 120 | + |
| 121 | + |
| 122 | +@pytest.mark.unit |
| 123 | +def test_ble_client_async_await_cancels_future_on_timeout(): |
| 124 | + """On timeout, async_await must cancel the pending future so a stalled |
| 125 | + coroutine is not left running on the event loop.""" |
| 126 | + client = object.__new__(BLEClient) |
| 127 | + future = MagicMock() |
| 128 | + future.result.side_effect = FutureTimeoutError() |
| 129 | + with patch.object(BLEClient, "async_run", return_value=future): |
| 130 | + with pytest.raises(FutureTimeoutError): |
| 131 | + client.async_await("coro", timeout=BLE_DISCONNECT_TIMEOUT) |
| 132 | + future.cancel.assert_called_once() |
0 commit comments