Add RuntimeProvider with 17 MCP tools for controlling running flowgraphs: - Container lifecycle: launch, list, stop, remove - Connection: connect by URL or container name - Variable control: list, get, set via XML-RPC introspection - Flowgraph execution: start, stop, lock, unlock - Visual feedback: screenshot capture, container logs Docker is optional - 10 tools work without it for external flowgraphs. Includes: - DockerMiddleware wrapping docker.DockerClient - XmlRpcMiddleware wrapping xmlrpc.client.ServerProxy - Dockerfile with Xvfb + ImageMagick + VNC for headless QT - 29 new unit tests (71 total)
119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
"""Unit tests for XmlRpcMiddleware with mocked ServerProxy."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from gnuradio_mcp.middlewares.xmlrpc import XmlRpcMiddleware
|
|
from gnuradio_mcp.models import ConnectionInfoModel, VariableModel
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_proxy():
|
|
proxy = MagicMock()
|
|
proxy.system.listMethods.return_value = [
|
|
"system.listMethods",
|
|
"system.methodHelp",
|
|
"get_frequency",
|
|
"set_frequency",
|
|
"get_amplitude",
|
|
"set_amplitude",
|
|
"get_waveform",
|
|
"start",
|
|
"stop",
|
|
"lock",
|
|
"unlock",
|
|
]
|
|
return proxy
|
|
|
|
|
|
@pytest.fixture
|
|
def xmlrpc_mw(mock_proxy):
|
|
return XmlRpcMiddleware(mock_proxy, "http://localhost:8080")
|
|
|
|
|
|
class TestConnect:
|
|
def test_connect_success(self):
|
|
with patch("gnuradio_mcp.middlewares.xmlrpc.xmlrpc.client") as mock_xmlrpc:
|
|
mock_proxy = MagicMock()
|
|
mock_xmlrpc.ServerProxy.return_value = mock_proxy
|
|
mock_xmlrpc.Transport.return_value = MagicMock()
|
|
|
|
mw = XmlRpcMiddleware.connect("http://localhost:8080")
|
|
assert mw is not None
|
|
mock_proxy.system.listMethods.assert_called_once()
|
|
|
|
def test_connect_failure(self):
|
|
with patch("gnuradio_mcp.middlewares.xmlrpc.xmlrpc.client") as mock_xmlrpc:
|
|
mock_proxy = MagicMock()
|
|
mock_proxy.system.listMethods.side_effect = ConnectionRefusedError()
|
|
mock_xmlrpc.ServerProxy.return_value = mock_proxy
|
|
mock_xmlrpc.Transport.return_value = MagicMock()
|
|
|
|
with pytest.raises(ConnectionRefusedError):
|
|
XmlRpcMiddleware.connect("http://localhost:8080")
|
|
|
|
|
|
class TestConnectionInfo:
|
|
def test_get_connection_info(self, xmlrpc_mw, mock_proxy):
|
|
result = xmlrpc_mw.get_connection_info(container_name="test", xmlrpc_port=8080)
|
|
assert isinstance(result, ConnectionInfoModel)
|
|
assert result.url == "http://localhost:8080"
|
|
assert result.container_name == "test"
|
|
# Should exclude system.* methods
|
|
assert "system.listMethods" not in result.methods
|
|
assert "get_frequency" in result.methods
|
|
|
|
|
|
class TestListVariables:
|
|
def test_list_variables(self, xmlrpc_mw, mock_proxy):
|
|
mock_proxy.get_frequency.return_value = 1e6
|
|
mock_proxy.get_amplitude.return_value = 0.5
|
|
|
|
result = xmlrpc_mw.list_variables()
|
|
assert len(result) == 2
|
|
assert all(isinstance(v, VariableModel) for v in result)
|
|
|
|
names = {v.name for v in result}
|
|
assert "frequency" in names
|
|
assert "amplitude" in names
|
|
# waveform has get_ but no set_, should be excluded
|
|
assert "waveform" not in names
|
|
|
|
def test_list_variables_with_error(self, xmlrpc_mw, mock_proxy):
|
|
"""If get_* fails, variable should still appear with None value."""
|
|
mock_proxy.get_frequency.side_effect = Exception("timeout")
|
|
mock_proxy.get_amplitude.return_value = 0.5
|
|
|
|
result = xmlrpc_mw.list_variables()
|
|
freq_var = next(v for v in result if v.name == "frequency")
|
|
assert freq_var.value is None
|
|
|
|
|
|
class TestGetSetVariable:
|
|
def test_get_variable(self, xmlrpc_mw, mock_proxy):
|
|
mock_proxy.get_frequency.return_value = 1e6
|
|
assert xmlrpc_mw.get_variable("frequency") == 1e6
|
|
|
|
def test_set_variable(self, xmlrpc_mw, mock_proxy):
|
|
assert xmlrpc_mw.set_variable("frequency", 2e6) is True
|
|
mock_proxy.set_frequency.assert_called_once_with(2e6)
|
|
|
|
|
|
class TestFlowgraphControl:
|
|
def test_start(self, xmlrpc_mw, mock_proxy):
|
|
assert xmlrpc_mw.start() is True
|
|
mock_proxy.start.assert_called_once()
|
|
|
|
def test_stop(self, xmlrpc_mw, mock_proxy):
|
|
assert xmlrpc_mw.stop() is True
|
|
mock_proxy.stop.assert_called_once()
|
|
|
|
def test_lock(self, xmlrpc_mw, mock_proxy):
|
|
assert xmlrpc_mw.lock() is True
|
|
mock_proxy.lock.assert_called_once()
|
|
|
|
def test_unlock(self, xmlrpc_mw, mock_proxy):
|
|
assert xmlrpc_mw.unlock() is True
|
|
mock_proxy.unlock.assert_called_once()
|