Service node ends its call when the loop cancels every task; test order and timing leaks
_serve_once treated a cancelled call future as finished, so an event loop cancelling all tasks at once (shutdown) never sent the worker a cancel and the call's thread kept asyncio.run waiting for SILENCE_S. The cameras audio test now removes its voice flow and device, the unpaired-frame test waits for its first frame before the unpair, and test_connector no longer inherits the controller an app test bound to demand. Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
This commit is contained in:
@@ -102,6 +102,31 @@ def camera(client: TestClient, superuser_token_headers: dict[str, str]):
|
||||
client.delete(f"{PREFIX}/cam1", headers=superuser_token_headers)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def microphone(client: TestClient, superuser_token_headers: dict[str, str]):
|
||||
"""A paired device with a microphone, wired in flow ``voice``. Yields its header.
|
||||
|
||||
Both go again afterwards: a published flow outlives the module in the
|
||||
flow store, and the next module's app would load it without its device.
|
||||
"""
|
||||
_panels(client, superuser_token_headers, {"panels": [{"id": "mic1"}]})
|
||||
device = _pair(client, superuser_token_headers, "mic1")
|
||||
port = {"port": "mic", "direction": "in", "dtype": "audio"}
|
||||
reported = client.put(
|
||||
f"{PREFIX}/mic1/report", headers=device, json={**REPORT, "ports": [port]}
|
||||
)
|
||||
assert reported.status_code == 200, reported.text
|
||||
node = _device_node(
|
||||
"mic1",
|
||||
node="panel",
|
||||
provides=[{"name": "mic", "port": "mic", "dtype": "audio"}],
|
||||
)
|
||||
_publish_flow(client, superuser_token_headers, "voice", [node])
|
||||
yield device
|
||||
client.delete(f"{FLOWS}/voice", headers=superuser_token_headers)
|
||||
client.delete(f"{PREFIX}/mic1", headers=superuser_token_headers)
|
||||
|
||||
|
||||
def _ticket(client: TestClient, headers: dict[str, str]) -> str:
|
||||
answer = client.post(f"{settings.API_V1_STR}/flows/ws-ticket", headers=headers)
|
||||
return f"{SOCKET}?ticket={answer.json()['ticket']}"
|
||||
@@ -269,24 +294,11 @@ def test_an_unpaired_device_is_closed_and_its_frames_stop(
|
||||
|
||||
|
||||
def test_a_device_streams_audio_chunks_on_its_socket(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
client: TestClient, microphone
|
||||
) -> None:
|
||||
"""A microphone's chunks take the camera's path, in their own family."""
|
||||
_panels(client, superuser_token_headers, {"panels": [{"id": "mic1"}]})
|
||||
device = _pair(client, superuser_token_headers, "mic1")
|
||||
port = {"port": "mic", "direction": "in", "dtype": "audio"}
|
||||
reported = client.put(
|
||||
f"{PREFIX}/mic1/report", headers=device, json={**REPORT, "ports": [port]}
|
||||
)
|
||||
assert reported.status_code == 200, reported.text
|
||||
node = _device_node(
|
||||
"mic1",
|
||||
node="panel",
|
||||
provides=[{"name": "mic", "port": "mic", "dtype": "audio"}],
|
||||
)
|
||||
_publish_flow(client, superuser_token_headers, "voice", [node])
|
||||
controller = client.app.state.flow_controller
|
||||
with client.websocket_connect(SOCKET, headers=device) as ws:
|
||||
with client.websocket_connect(SOCKET, headers=microphone) as ws:
|
||||
ws.receive_json()
|
||||
ws.send_bytes(frame("voice.mic", b"\x00\x01")) # an image on an audio port
|
||||
ws.send_bytes(frame("voice.mic", b"\x00\x02", "audio/L16;rate=16000"))
|
||||
@@ -329,9 +341,13 @@ def test_an_unpaired_device_is_closed_at_its_next_frame(
|
||||
"""Without the event an unpair sends, which a full queue may have lost."""
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
controller = client.app.state.flow_controller
|
||||
with client.websocket_connect(SOCKET, headers=camera) as device:
|
||||
device.receive_json()
|
||||
device.send_bytes(frame("cams.camera", b"\xff\xd8 one"))
|
||||
# The credential is asked as each frame is read, so a bump before the
|
||||
# server has read the first one would refuse that one too.
|
||||
wait_for(lambda: (controller.values().get("cams.camera") or {}).get("value"))
|
||||
client.app.state.device_store.bump("cam1")
|
||||
device.send_bytes(frame("cams.camera", b"\xff\xd8 two"))
|
||||
with pytest.raises(WebSocketDisconnect) as closed:
|
||||
@@ -339,7 +355,7 @@ def test_an_unpaired_device_is_closed_at_its_next_frame(
|
||||
device.receive_json()
|
||||
assert closed.value.code == 1008
|
||||
store = client.app.state.artifact_store
|
||||
current = client.app.state.flow_controller.values()["cams.camera"]["value"]
|
||||
current = controller.values()["cams.camera"]["value"]
|
||||
assert store.volatile.get(current["digest"]) == b"\xff\xd8 one"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user