Disconnect from Bluetooth devices before the server stops
The polling threads are daemon threads, so a stop ended the server in the middle of a Bluetooth read. The read got no disconnect, and the adapter kept an open connection or a scan in progress. The server now sets the stop event at exit, closes its socket, and waits at most 20 seconds for the polling threads. A Bluetooth read that sees the stop event cancels its current step, disconnects, and returns. A read that waits for the adapter does not connect after a stop.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Server routing and caching tests with synthetic device identifiers."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
@@ -71,7 +72,7 @@ def test_collection_dispatches_to_the_selected_backend(monkeypatch):
|
||||
assert server.collect_device_data(server.DeviceState(ser=port), 2) is serial_data
|
||||
serial_read.assert_called_once_with(port, 2)
|
||||
assert server.collect_device_data(server.DeviceState(ecoflow=config), 3) is bluetooth_data
|
||||
bluetooth_read.assert_called_once_with(config, 3)
|
||||
bluetooth_read.assert_called_once_with(config, 3, None)
|
||||
assert server.collect_device_data(server.DeviceState()) is None
|
||||
|
||||
|
||||
@@ -104,7 +105,7 @@ def test_poll_interval_depends_on_backend(monkeypatch, bluetooth, interval):
|
||||
assert state.timestamp == 100
|
||||
assert state.polled.is_set()
|
||||
assert stop.waits == [interval]
|
||||
read.assert_called_once_with(state, 2)
|
||||
read.assert_called_once_with(state, 2, stop)
|
||||
|
||||
|
||||
def test_offline_bluetooth_does_not_retry_at_once_or_serve_stale_data(monkeypatch):
|
||||
@@ -151,7 +152,7 @@ def test_polling_threads_do_not_wait_for_each_other_and_stop(monkeypatch):
|
||||
offline_started, release = threading.Event(), threading.Event()
|
||||
working, offline = server.DeviceState(ser=Mock()), server.DeviceState(ser=Mock())
|
||||
|
||||
def read(device, debug=0):
|
||||
def read(device, debug=0, stop=None):
|
||||
if device is offline:
|
||||
offline_started.set()
|
||||
release.wait(5)
|
||||
@@ -172,3 +173,51 @@ def test_polling_threads_do_not_wait_for_each_other_and_stop(monkeypatch):
|
||||
for thread in threads:
|
||||
thread.join(5)
|
||||
assert not any(thread.is_alive() for thread in threads)
|
||||
|
||||
|
||||
def test_stop_polling_waits_for_the_bluetooth_disconnect(monkeypatch):
|
||||
events = []
|
||||
|
||||
def bluetooth_read(config, debug=0, stop=None):
|
||||
if stop.is_set():
|
||||
return False
|
||||
events.append("read started")
|
||||
assert stop.wait(5)
|
||||
events.append("disconnected")
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(server, "ecoflow_collect_data", bluetooth_read)
|
||||
# The second device waits for the first one. It must not start a read.
|
||||
devices = {
|
||||
name: server.DeviceState(ecoflow=initialise_device(ADDRESS, SERIAL, "test-user"))
|
||||
for name in ("first", "second")
|
||||
}
|
||||
stop = threading.Event()
|
||||
threads = server.start_polling(devices, stop)
|
||||
while not events:
|
||||
time.sleep(0.01)
|
||||
server.stop_polling(threads, stop)
|
||||
assert events == ["read started", "disconnected"]
|
||||
assert not any(thread.is_alive() for thread in threads)
|
||||
|
||||
|
||||
def test_stop_polling_reports_a_read_that_does_not_end(monkeypatch, capsys):
|
||||
started, release = threading.Event(), threading.Event()
|
||||
|
||||
def stuck_read(device, debug=0, stop=None):
|
||||
started.set()
|
||||
release.wait(5)
|
||||
|
||||
monkeypatch.setattr(server, "collect_device_data", stuck_read)
|
||||
monkeypatch.setattr(server, "POLL_STOP_TIMEOUT", 0.05)
|
||||
stop = threading.Event()
|
||||
threads = server.start_polling({"stuck": server.DeviceState(ser=Mock())}, stop)
|
||||
try:
|
||||
assert started.wait(5)
|
||||
server.stop_polling(threads, stop)
|
||||
assert "server: no clean stop in 0.05 seconds for poll-stuck" in capsys.readouterr().out
|
||||
finally:
|
||||
release.set()
|
||||
for thread in threads:
|
||||
thread.join(5)
|
||||
assert not any(thread.is_alive() for thread in threads)
|
||||
|
||||
Reference in New Issue
Block a user