Implements complete workflow for generating GNU Radio blocks from descriptions: Block Generation: - generate_sync_block, generate_basic_block, generate_interp_block, generate_decim_block tools for creating different block types - Template-based code generation with customizable work logic - Automatic validation via AST parsing and signature checking Protocol Analysis: - Parse protocol specifications into structured models - Generate decoder pipelines matching modulation to demodulator blocks - Templates for BLE, Zigbee, LoRa, POCSAG, ADS-B protocols OOT Export: - Export generated blocks to full OOT module structure - Generate CMakeLists.txt, block YAML, Python modules - gr_modtool-compatible output Dynamic Tool Registration: - enable_block_dev_mode/disable_block_dev_mode for context management - Tools only registered when needed (reduces LLM context usage) Includes comprehensive test coverage and end-to-end demo.
273 lines
10 KiB
Python
273 lines
10 KiB
Python
"""Integration tests for McpBlockDevProvider."""
|
|
|
|
import pytest
|
|
from fastmcp import Client, FastMCP
|
|
|
|
from gnuradio_mcp.providers.mcp_block_dev import McpBlockDevProvider
|
|
|
|
|
|
@pytest.fixture
|
|
def mcp_app():
|
|
"""Create a FastMCP app with block dev provider."""
|
|
app = FastMCP("Block Dev Test")
|
|
McpBlockDevProvider.create(app)
|
|
return app
|
|
|
|
|
|
class TestDynamicBlockDevMode:
|
|
"""Tests for dynamic block development mode registration."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_block_dev_mode_starts_disabled(self, mcp_app):
|
|
"""Block dev mode should be disabled by default."""
|
|
async with Client(mcp_app) as client:
|
|
result = await client.call_tool(name="get_block_dev_mode")
|
|
assert result.data.enabled is False
|
|
assert result.data.tools_registered == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_enable_block_dev_mode_registers_tools(self, mcp_app):
|
|
"""Enabling block dev mode should register generation tools."""
|
|
async with Client(mcp_app) as client:
|
|
result = await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
assert result.data.enabled is True
|
|
assert "generate_sync_block" in result.data.tools_registered
|
|
assert "validate_block_code" in result.data.tools_registered
|
|
assert "parse_block_prompt" in result.data.tools_registered
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disable_block_dev_mode_removes_tools(self, mcp_app):
|
|
"""Disabling block dev mode should remove generation tools."""
|
|
async with Client(mcp_app) as client:
|
|
# Enable first
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
# Then disable
|
|
result = await client.call_tool(name="disable_block_dev_mode")
|
|
|
|
assert result.data.enabled is False
|
|
assert result.data.tools_registered == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_enable_block_dev_mode_idempotent(self, mcp_app):
|
|
"""Enabling block dev mode multiple times should be idempotent."""
|
|
async with Client(mcp_app) as client:
|
|
result1 = await client.call_tool(name="enable_block_dev_mode")
|
|
result2 = await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
assert result1.data.enabled == result2.data.enabled
|
|
assert set(result1.data.tools_registered) == set(result2.data.tools_registered)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disable_block_dev_mode_idempotent(self, mcp_app):
|
|
"""Disabling block dev mode multiple times should be idempotent."""
|
|
async with Client(mcp_app) as client:
|
|
result1 = await client.call_tool(name="disable_block_dev_mode")
|
|
result2 = await client.call_tool(name="disable_block_dev_mode")
|
|
|
|
assert result1.data.enabled is False
|
|
assert result2.data.enabled is False
|
|
|
|
|
|
class TestBlockDevTools:
|
|
"""Tests for block development tools when enabled."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_sync_block_creates_valid_code(self, mcp_app):
|
|
"""Generate a sync block and verify it validates."""
|
|
async with Client(mcp_app) as client:
|
|
# Enable block dev mode
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
# Generate a block
|
|
result = await client.call_tool(
|
|
name="generate_sync_block",
|
|
arguments={
|
|
"name": "test_gain",
|
|
"description": "Multiply by gain factor",
|
|
"inputs": [{"dtype": "float", "vlen": 1}],
|
|
"outputs": [{"dtype": "float", "vlen": 1}],
|
|
"parameters": [{"name": "gain", "dtype": "float", "default": 1.0}],
|
|
"work_template": "gain",
|
|
},
|
|
)
|
|
|
|
assert result.data.is_valid is True
|
|
assert "gr.sync_block" in result.data.source_code
|
|
assert "self.gain" in result.data.source_code
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_basic_block_creates_valid_code(self, mcp_app):
|
|
"""Generate a basic block and verify it validates."""
|
|
async with Client(mcp_app) as client:
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
result = await client.call_tool(
|
|
name="generate_basic_block",
|
|
arguments={
|
|
"name": "packet_extract",
|
|
"description": "Extract packets",
|
|
"inputs": [{"dtype": "byte", "vlen": 1}],
|
|
"outputs": [{"dtype": "byte", "vlen": 1}],
|
|
"parameters": [],
|
|
"work_logic": "self.consume_each(1); return 1",
|
|
},
|
|
)
|
|
|
|
assert result.data.is_valid is True
|
|
assert "gr.basic_block" in result.data.source_code
|
|
assert "general_work" in result.data.source_code
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_block_code_success(self, mcp_app):
|
|
"""Validate syntactically correct code."""
|
|
async with Client(mcp_app) as client:
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
valid_code = '''
|
|
import numpy
|
|
from gnuradio import gr
|
|
|
|
class blk(gr.sync_block):
|
|
def __init__(self):
|
|
gr.sync_block.__init__(
|
|
self, name="test", in_sig=[numpy.float32], out_sig=[numpy.float32]
|
|
)
|
|
|
|
def work(self, input_items, output_items):
|
|
output_items[0][:] = input_items[0]
|
|
return len(output_items[0])
|
|
'''
|
|
result = await client.call_tool(
|
|
name="validate_block_code",
|
|
arguments={"source_code": valid_code},
|
|
)
|
|
|
|
assert result.data.is_valid is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_block_code_syntax_error(self, mcp_app):
|
|
"""Validate code with syntax errors."""
|
|
async with Client(mcp_app) as client:
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
invalid_code = """
|
|
def broken(:
|
|
pass
|
|
"""
|
|
result = await client.call_tool(
|
|
name="validate_block_code",
|
|
arguments={"source_code": invalid_code},
|
|
)
|
|
|
|
assert result.data.is_valid is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_interp_block(self, mcp_app):
|
|
"""Generate an interpolating block."""
|
|
async with Client(mcp_app) as client:
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
result = await client.call_tool(
|
|
name="generate_interp_block",
|
|
arguments={
|
|
"name": "upsample_2x",
|
|
"description": "Upsample by 2",
|
|
"inputs": [{"dtype": "float", "vlen": 1}],
|
|
"outputs": [{"dtype": "float", "vlen": 1}],
|
|
"interpolation": 2,
|
|
"parameters": [],
|
|
},
|
|
)
|
|
|
|
assert result.data.is_valid is True
|
|
assert "gr.interp_block" in result.data.source_code
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_decim_block(self, mcp_app):
|
|
"""Generate a decimating block."""
|
|
async with Client(mcp_app) as client:
|
|
await client.call_tool(name="enable_block_dev_mode")
|
|
|
|
result = await client.call_tool(
|
|
name="generate_decim_block",
|
|
arguments={
|
|
"name": "downsample_4x",
|
|
"description": "Downsample by 4",
|
|
"inputs": [{"dtype": "float", "vlen": 1}],
|
|
"outputs": [{"dtype": "float", "vlen": 1}],
|
|
"decimation": 4,
|
|
"parameters": [],
|
|
},
|
|
)
|
|
|
|
assert result.data.is_valid is True
|
|
assert "gr.decim_block" in result.data.source_code
|
|
|
|
|
|
class TestBlockDevResources:
|
|
"""Tests for block dev prompt template resources."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sync_block_prompt_resource(self, mcp_app):
|
|
"""Verify sync block prompt resource is available."""
|
|
async with Client(mcp_app) as client:
|
|
resources = await client.list_resources()
|
|
resource_uris = [r.uri for r in resources]
|
|
|
|
# Check that our prompt resources are registered
|
|
assert any("sync-block" in str(uri) for uri in resource_uris)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_basic_block_prompt_resource(self, mcp_app):
|
|
"""Verify basic block prompt resource is available."""
|
|
async with Client(mcp_app) as client:
|
|
resources = await client.list_resources()
|
|
resource_uris = [r.uri for r in resources]
|
|
|
|
assert any("basic-block" in str(uri) for uri in resource_uris)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decoder_chain_prompt_resource(self, mcp_app):
|
|
"""Verify decoder chain prompt resource is available."""
|
|
async with Client(mcp_app) as client:
|
|
resources = await client.list_resources()
|
|
resource_uris = [r.uri for r in resources]
|
|
|
|
assert any("decoder-chain" in str(uri) for uri in resource_uris)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_common_patterns_prompt_resource(self, mcp_app):
|
|
"""Verify common patterns prompt resource is available."""
|
|
async with Client(mcp_app) as client:
|
|
resources = await client.list_resources()
|
|
resource_uris = [r.uri for r in resources]
|
|
|
|
assert any("common-patterns" in str(uri) for uri in resource_uris)
|
|
|
|
|
|
class TestToolNotAvailableWhenDisabled:
|
|
"""Tests that tools are not available when block dev mode is disabled."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_sync_block_not_available_when_disabled(self, mcp_app):
|
|
"""generate_sync_block should not be callable when mode is disabled."""
|
|
async with Client(mcp_app) as client:
|
|
# Ensure disabled
|
|
await client.call_tool(name="disable_block_dev_mode")
|
|
|
|
# List available tools
|
|
tools = await client.list_tools()
|
|
tool_names = [t.name for t in tools]
|
|
|
|
# Generation tools should not be in the list
|
|
assert "generate_sync_block" not in tool_names
|
|
assert "validate_block_code" not in tool_names
|
|
assert "generate_basic_block" not in tool_names
|
|
|
|
# But mode control tools should be
|
|
assert "get_block_dev_mode" in tool_names
|
|
assert "enable_block_dev_mode" in tool_names
|
|
assert "disable_block_dev_mode" in tool_names
|