mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-08 09:43:03 +02:00
Add radio lock acquire around missing spots, and validate
This commit is contained in:
@@ -6,6 +6,7 @@ and add/remove from radio operations.
|
||||
Uses httpx.AsyncClient with real in-memory SQLite database.
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import httpx
|
||||
@@ -21,6 +22,11 @@ KEY_B = "bb" * 32 # bbbb...bb
|
||||
KEY_C = "cc" * 32 # cccc...cc
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _noop_radio_operation(*_args, **_kwargs):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db():
|
||||
"""Create an in-memory test database with schema + migrations."""
|
||||
@@ -219,6 +225,7 @@ class TestDeleteContact:
|
||||
with patch("app.routers.contacts.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
response = await client.delete(f"/api/contacts/{KEY_A}")
|
||||
|
||||
@@ -248,6 +255,7 @@ class TestDeleteContact:
|
||||
with patch("app.routers.contacts.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
response = await client.delete(f"/api/contacts/{KEY_A}")
|
||||
|
||||
|
||||
@@ -48,6 +48,40 @@ class TestRadioOperationLock:
|
||||
release.set()
|
||||
await holder_task
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocking_waits_and_acquires_after_release(self):
|
||||
holder_entered = asyncio.Event()
|
||||
holder_release = asyncio.Event()
|
||||
contender_entered = asyncio.Event()
|
||||
order: list[str] = []
|
||||
|
||||
async def holder():
|
||||
async with radio_manager.radio_operation("holder"):
|
||||
order.append("holder_enter")
|
||||
holder_entered.set()
|
||||
await holder_release.wait()
|
||||
order.append("holder_exit")
|
||||
|
||||
async def contender():
|
||||
await holder_entered.wait()
|
||||
async with radio_manager.radio_operation("contender"):
|
||||
order.append("contender_enter")
|
||||
contender_entered.set()
|
||||
|
||||
holder_task = asyncio.create_task(holder())
|
||||
contender_task = asyncio.create_task(contender())
|
||||
|
||||
await holder_entered.wait()
|
||||
await asyncio.sleep(0.02)
|
||||
assert not contender_entered.is_set()
|
||||
|
||||
holder_release.set()
|
||||
await asyncio.wait_for(contender_entered.wait(), timeout=1.0)
|
||||
|
||||
await holder_task
|
||||
await contender_task
|
||||
assert order == ["holder_enter", "holder_exit", "contender_enter"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suspend_auto_fetch_stops_and_restarts(self):
|
||||
mc = MagicMock()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for radio router endpoint logic."""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -18,6 +20,7 @@ from app.routers.radio import (
|
||||
set_private_key,
|
||||
update_radio_config,
|
||||
)
|
||||
from app.radio import RadioManager
|
||||
|
||||
|
||||
def _radio_result(event_type=EventType.OK, payload=None):
|
||||
@@ -27,6 +30,11 @@ def _radio_result(event_type=EventType.OK, payload=None):
|
||||
return result
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _noop_radio_operation(*_args, **_kwargs):
|
||||
yield
|
||||
|
||||
|
||||
def _mock_meshcore_with_info():
|
||||
mc = MagicMock()
|
||||
mc.self_info = {
|
||||
@@ -147,6 +155,34 @@ class TestAdvertise:
|
||||
|
||||
assert exc.value.status_code == 500
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_advertise_calls_are_serialized(self):
|
||||
active = 0
|
||||
max_active = 0
|
||||
|
||||
async def fake_send(*, force: bool):
|
||||
nonlocal active, max_active
|
||||
assert force is True
|
||||
active += 1
|
||||
max_active = max(max_active, active)
|
||||
await asyncio.sleep(0.05)
|
||||
active -= 1
|
||||
return True
|
||||
|
||||
isolated_manager = RadioManager()
|
||||
with (
|
||||
patch("app.routers.radio.require_connected"),
|
||||
patch("app.routers.radio.radio_manager", isolated_manager),
|
||||
patch(
|
||||
"app.routers.radio.do_send_advertisement",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=fake_send,
|
||||
),
|
||||
):
|
||||
await asyncio.gather(send_advertisement(), send_advertisement())
|
||||
|
||||
assert max_active == 1
|
||||
|
||||
|
||||
class TestRebootAndReconnect:
|
||||
@pytest.mark.asyncio
|
||||
@@ -155,6 +191,7 @@ class TestRebootAndReconnect:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = MagicMock()
|
||||
mock_rm.meshcore.commands.reboot = AsyncMock()
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
with patch("app.routers.radio.radio_manager", mock_rm):
|
||||
result = await reboot_radio()
|
||||
@@ -168,6 +205,7 @@ class TestRebootAndReconnect:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
mock_rm.is_reconnecting = True
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
with patch("app.routers.radio.radio_manager", mock_rm):
|
||||
result = await reboot_radio()
|
||||
@@ -183,6 +221,7 @@ class TestRebootAndReconnect:
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.reconnect = AsyncMock(return_value=True)
|
||||
mock_rm.post_connect_setup = AsyncMock()
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
with patch("app.routers.radio.radio_manager", mock_rm):
|
||||
result = await reboot_radio()
|
||||
@@ -196,6 +235,7 @@ class TestRebootAndReconnect:
|
||||
async def test_reconnect_returns_already_connected(self):
|
||||
mock_rm = MagicMock()
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
with patch("app.routers.radio.radio_manager", mock_rm):
|
||||
result = await reconnect_radio()
|
||||
@@ -209,6 +249,7 @@ class TestRebootAndReconnect:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.is_reconnecting = False
|
||||
mock_rm.reconnect = AsyncMock(return_value=False)
|
||||
mock_rm.radio_operation = _noop_radio_operation
|
||||
|
||||
with patch("app.routers.radio.radio_manager", mock_rm):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
|
||||
Reference in New Issue
Block a user