gr-mcp/tests/unit/test_xmlrpc_middleware.py
Ryan Malloy 2084c41228 runtime: Phase 1 Docker + XML-RPC control
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)
2026-01-27 09:48:44 -07:00

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()