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.
320 lines
11 KiB
Python
320 lines
11 KiB
Python
"""Unit tests for OOTExporterMiddleware."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from gnuradio_mcp.middlewares.oot_exporter import OOTExporterMiddleware
|
|
from gnuradio_mcp.models import GeneratedBlockCode, SignatureItem
|
|
|
|
|
|
class TestOOTExporterMiddleware:
|
|
"""Tests for OOT module export functionality."""
|
|
|
|
@pytest.fixture
|
|
def exporter(self):
|
|
"""Create an OOT exporter instance."""
|
|
return OOTExporterMiddleware()
|
|
|
|
@pytest.fixture
|
|
def sample_block(self):
|
|
"""Create a sample generated block for testing."""
|
|
return GeneratedBlockCode(
|
|
source_code='''
|
|
import numpy
|
|
from gnuradio import gr
|
|
|
|
class blk(gr.sync_block):
|
|
def __init__(self, gain=1.0):
|
|
gr.sync_block.__init__(
|
|
self,
|
|
name="test_gain",
|
|
in_sig=[numpy.float32],
|
|
out_sig=[numpy.float32],
|
|
)
|
|
self.gain = gain
|
|
|
|
def work(self, input_items, output_items):
|
|
output_items[0][:] = input_items[0] * self.gain
|
|
return len(output_items[0])
|
|
''',
|
|
block_name="test_gain",
|
|
block_class="sync_block",
|
|
inputs=[SignatureItem(dtype="float", vlen=1)],
|
|
outputs=[SignatureItem(dtype="float", vlen=1)],
|
|
is_valid=True,
|
|
)
|
|
|
|
def test_generate_oot_skeleton_creates_directory(self, exporter):
|
|
"""Generate OOT skeleton creates proper directory structure."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create skeleton - output_dir is the module root itself
|
|
module_dir = Path(tmpdir) / "gr-custom"
|
|
result = exporter.generate_oot_skeleton(
|
|
module_name="custom",
|
|
output_dir=str(module_dir),
|
|
author="Test Author",
|
|
)
|
|
|
|
# Check result
|
|
assert result.success is True
|
|
assert result.module_name == "custom"
|
|
|
|
# Check directory exists
|
|
assert module_dir.exists()
|
|
|
|
# Check required files
|
|
assert (module_dir / "CMakeLists.txt").exists()
|
|
assert (module_dir / "python" / "custom" / "__init__.py").exists()
|
|
assert (module_dir / "grc").exists()
|
|
|
|
def test_generate_oot_skeleton_creates_cmake(self, exporter):
|
|
"""Generate OOT skeleton creates valid CMakeLists.txt."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-mymodule"
|
|
exporter.generate_oot_skeleton(
|
|
module_name="mymodule",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
cmake_path = module_dir / "CMakeLists.txt"
|
|
cmake_content = cmake_path.read_text()
|
|
|
|
assert "cmake_minimum_required" in cmake_content
|
|
assert "project(" in cmake_content
|
|
assert "find_package(Gnuradio" in cmake_content
|
|
|
|
def test_generate_oot_skeleton_creates_python_init(self, exporter):
|
|
"""Generate OOT skeleton creates Python __init__.py."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-testmod"
|
|
exporter.generate_oot_skeleton(
|
|
module_name="testmod",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
init_path = module_dir / "python" / "testmod" / "__init__.py"
|
|
init_content = init_path.read_text()
|
|
|
|
# Should have basic module setup
|
|
assert "testmod" in init_content or "__init__" in str(init_path)
|
|
|
|
def test_export_block_to_oot(self, exporter, sample_block):
|
|
"""Export a generated block to OOT module structure."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-custom"
|
|
result = exporter.export_block_to_oot(
|
|
generated=sample_block,
|
|
module_name="custom",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
# Check block file exists
|
|
block_path = module_dir / "python" / "custom" / "test_gain.py"
|
|
assert block_path.exists()
|
|
|
|
# Check GRC yaml exists
|
|
yaml_files = list((module_dir / "grc").glob("*.block.yml"))
|
|
assert len(yaml_files) > 0
|
|
|
|
def test_export_block_creates_yaml(self, exporter, sample_block):
|
|
"""Export creates valid GRC YAML file."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-custom"
|
|
exporter.export_block_to_oot(
|
|
generated=sample_block,
|
|
module_name="custom",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
yaml_path = module_dir / "grc" / "custom_test_gain.block.yml"
|
|
if yaml_path.exists():
|
|
yaml_content = yaml_path.read_text()
|
|
|
|
assert "id:" in yaml_content
|
|
assert "label:" in yaml_content
|
|
assert "templates:" in yaml_content
|
|
|
|
def test_export_to_existing_module(self, exporter, sample_block):
|
|
"""Export to an existing OOT module adds the block."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-existing"
|
|
|
|
# First create the skeleton
|
|
exporter.generate_oot_skeleton(
|
|
module_name="existing",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
# Then export a block to it
|
|
result = exporter.export_block_to_oot(
|
|
generated=sample_block,
|
|
module_name="existing",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
# Block should be added
|
|
block_path = module_dir / "python" / "existing" / "test_gain.py"
|
|
assert block_path.exists()
|
|
|
|
|
|
class TestOOTExporterEdgeCases:
|
|
"""Tests for edge cases in OOT export."""
|
|
|
|
@pytest.fixture
|
|
def exporter(self):
|
|
return OOTExporterMiddleware()
|
|
|
|
def test_sanitize_module_name_strips_gr_prefix(self, exporter):
|
|
"""Module names have gr- prefix stripped."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-mytest"
|
|
result = exporter.generate_oot_skeleton(
|
|
module_name="gr-mytest", # Has gr- prefix which should be removed
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
# The sanitized module_name should not have gr- prefix
|
|
assert result.module_name == "mytest"
|
|
|
|
def test_sanitize_module_name_replaces_dashes(self, exporter):
|
|
"""Module names with dashes are sanitized to underscores."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-my-module"
|
|
result = exporter.generate_oot_skeleton(
|
|
module_name="my-module-name",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
# Dashes replaced with underscores for valid Python identifier
|
|
assert "_" in result.module_name or result.module_name.isalnum()
|
|
|
|
def test_export_complex_block(self, exporter):
|
|
"""Export a block with complex I/O."""
|
|
complex_block = GeneratedBlockCode(
|
|
source_code='''
|
|
import numpy
|
|
from gnuradio import gr
|
|
|
|
class blk(gr.sync_block):
|
|
def __init__(self):
|
|
gr.sync_block.__init__(
|
|
self,
|
|
name="complex_processor",
|
|
in_sig=[numpy.complex64],
|
|
out_sig=[numpy.complex64],
|
|
)
|
|
|
|
def work(self, input_items, output_items):
|
|
output_items[0][:] = input_items[0] * 1j
|
|
return len(output_items[0])
|
|
''',
|
|
block_name="complex_processor",
|
|
block_class="sync_block",
|
|
inputs=[SignatureItem(dtype="complex", vlen=1)],
|
|
outputs=[SignatureItem(dtype="complex", vlen=1)],
|
|
is_valid=True,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-complex_test"
|
|
result = exporter.export_block_to_oot(
|
|
generated=complex_block,
|
|
module_name="complex_test",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
def test_export_block_with_parameters(self, exporter):
|
|
"""Export a block with multiple parameters."""
|
|
from gnuradio_mcp.models import BlockParameter
|
|
|
|
param_block = GeneratedBlockCode(
|
|
source_code='''
|
|
import numpy
|
|
from gnuradio import gr
|
|
|
|
class blk(gr.sync_block):
|
|
def __init__(self, gain=1.0, offset=0.0, threshold=0.5):
|
|
gr.sync_block.__init__(
|
|
self,
|
|
name="multi_param",
|
|
in_sig=[numpy.float32],
|
|
out_sig=[numpy.float32],
|
|
)
|
|
self.gain = gain
|
|
self.offset = offset
|
|
self.threshold = threshold
|
|
|
|
def work(self, input_items, output_items):
|
|
scaled = input_items[0] * self.gain + self.offset
|
|
output_items[0][:] = numpy.where(scaled > self.threshold, scaled, 0.0)
|
|
return len(output_items[0])
|
|
''',
|
|
block_name="multi_param",
|
|
block_class="sync_block",
|
|
inputs=[SignatureItem(dtype="float", vlen=1)],
|
|
outputs=[SignatureItem(dtype="float", vlen=1)],
|
|
parameters=[
|
|
BlockParameter(name="gain", dtype="float", default=1.0),
|
|
BlockParameter(name="offset", dtype="float", default=0.0),
|
|
BlockParameter(name="threshold", dtype="float", default=0.5),
|
|
],
|
|
is_valid=True,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-param_test"
|
|
result = exporter.export_block_to_oot(
|
|
generated=param_block,
|
|
module_name="param_test",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
|
|
class TestOOTExporterYAMLGeneration:
|
|
"""Tests specifically for YAML file generation."""
|
|
|
|
@pytest.fixture
|
|
def exporter(self):
|
|
return OOTExporterMiddleware()
|
|
|
|
def test_yaml_has_required_fields(self, exporter):
|
|
"""Generated YAML has all required GRC fields."""
|
|
block = GeneratedBlockCode(
|
|
source_code="...",
|
|
block_name="my_block",
|
|
block_class="sync_block",
|
|
inputs=[SignatureItem(dtype="float", vlen=1)],
|
|
outputs=[SignatureItem(dtype="float", vlen=1)],
|
|
is_valid=True,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
module_dir = Path(tmpdir) / "gr-test"
|
|
exporter.export_block_to_oot(
|
|
generated=block,
|
|
module_name="test",
|
|
output_dir=str(module_dir),
|
|
)
|
|
|
|
yaml_path = module_dir / "grc" / "test_my_block.block.yml"
|
|
if yaml_path.exists():
|
|
yaml_content = yaml_path.read_text()
|
|
|
|
# Required GRC YAML fields
|
|
assert "id:" in yaml_content
|
|
assert "label:" in yaml_content
|
|
assert "category:" in yaml_content
|
|
assert "templates:" in yaml_content
|