Rework radio lock handling

This commit is contained in:
Jack Kingsman
2026-02-10 18:00:41 -08:00
parent de683e77ab
commit b8ea31666f
9 changed files with 569 additions and 349 deletions
+91
View File
@@ -0,0 +1,91 @@
"""Tests for shared radio operation locking behavior."""
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.radio import RadioOperationBusyError, radio_manager
from app.radio_sync import is_polling_paused
@pytest.fixture(autouse=True)
def reset_radio_operation_state():
"""Reset shared radio operation lock state before/after each test."""
prev_meshcore = radio_manager._meshcore
radio_manager._operation_lock = None
radio_manager._meshcore = None
import app.radio_sync as radio_sync
radio_sync._polling_pause_count = 0
yield
radio_manager._operation_lock = None
radio_manager._meshcore = prev_meshcore
radio_sync._polling_pause_count = 0
class TestRadioOperationLock:
"""Validate shared radio operation lock semantics."""
@pytest.mark.asyncio
async def test_non_blocking_fails_when_lock_held_by_other_task(self):
started = asyncio.Event()
release = asyncio.Event()
async def holder():
async with radio_manager.radio_operation("holder"):
started.set()
await release.wait()
holder_task = asyncio.create_task(holder())
await started.wait()
with pytest.raises(RadioOperationBusyError):
async with radio_manager.radio_operation("contender", blocking=False):
pass
release.set()
await holder_task
@pytest.mark.asyncio
async def test_suspend_auto_fetch_stops_and_restarts(self):
mc = MagicMock()
mc.stop_auto_message_fetching = AsyncMock()
mc.start_auto_message_fetching = AsyncMock()
radio_manager._meshcore = mc
async with radio_manager.radio_operation(
"auto_fetch_toggle",
suspend_auto_fetch=True,
):
pass
mc.stop_auto_message_fetching.assert_awaited_once()
mc.start_auto_message_fetching.assert_awaited_once()
@pytest.mark.asyncio
async def test_lock_released_when_auto_fetch_restart_is_cancelled(self):
mc = MagicMock()
mc.stop_auto_message_fetching = AsyncMock()
mc.start_auto_message_fetching = AsyncMock(side_effect=asyncio.CancelledError())
radio_manager._meshcore = mc
with pytest.raises(asyncio.CancelledError):
async with radio_manager.radio_operation(
"cancelled_restart",
suspend_auto_fetch=True,
):
pass
async with radio_manager.radio_operation("after_cancel", blocking=False):
pass
@pytest.mark.asyncio
async def test_pause_polling_toggles_state(self):
assert not is_polling_paused()
async with radio_manager.radio_operation("pause_polling", pause_polling=True):
assert is_polling_paused()
assert not is_polling_paused()
+5 -5
View File
@@ -156,7 +156,7 @@ class TestRebootAndReconnect:
mock_rm.meshcore = MagicMock()
mock_rm.meshcore.commands.reboot = AsyncMock()
with patch("app.radio.radio_manager", mock_rm):
with patch("app.routers.radio.radio_manager", mock_rm):
result = await reboot_radio()
assert result["status"] == "ok"
@@ -169,7 +169,7 @@ class TestRebootAndReconnect:
mock_rm.meshcore = None
mock_rm.is_reconnecting = True
with patch("app.radio.radio_manager", mock_rm):
with patch("app.routers.radio.radio_manager", mock_rm):
result = await reboot_radio()
assert result["status"] == "pending"
@@ -184,7 +184,7 @@ class TestRebootAndReconnect:
mock_rm.reconnect = AsyncMock(return_value=True)
mock_rm.post_connect_setup = AsyncMock()
with patch("app.radio.radio_manager", mock_rm):
with patch("app.routers.radio.radio_manager", mock_rm):
result = await reboot_radio()
assert result["status"] == "ok"
@@ -197,7 +197,7 @@ class TestRebootAndReconnect:
mock_rm = MagicMock()
mock_rm.is_connected = True
with patch("app.radio.radio_manager", mock_rm):
with patch("app.routers.radio.radio_manager", mock_rm):
result = await reconnect_radio()
assert result["status"] == "ok"
@@ -210,7 +210,7 @@ class TestRebootAndReconnect:
mock_rm.is_reconnecting = False
mock_rm.reconnect = AsyncMock(return_value=False)
with patch("app.radio.radio_manager", mock_rm):
with patch("app.routers.radio.radio_manager", mock_rm):
with pytest.raises(HTTPException) as exc:
await reconnect_radio()