"""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 BlockParameter, 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 class TestGenerateGrcYaml: """Tests for the standalone generate_grc_yaml method.""" @pytest.fixture def exporter(self): return OOTExporterMiddleware() @pytest.fixture def gain_block(self): return GeneratedBlockCode( source_code=''' import numpy from gnuradio import gr class blk(gr.sync_block): """Configurable gain block.""" def __init__(self, gain=1.0): gr.sync_block.__init__( self, name="configurable_gain", in_sig=[numpy.float32], out_sig=[numpy.float32], ) self.gain = gain def set_gain(self, gain): 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="configurable_gain", 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, description="Gain factor", min_value=0.0), ], is_valid=True, ) def test_returns_success(self, exporter, gain_block): """generate_grc_yaml returns a successful result.""" result = exporter.generate_grc_yaml(gain_block) assert result.success is True assert result.yaml_content assert result.filename == "custom_configurable_gain.block.yml" assert result.block_id == "custom_configurable_gain" def test_yaml_has_flags(self, exporter, gain_block): """Generated YAML includes flags: [ python ].""" result = exporter.generate_grc_yaml(gain_block) assert "flags: [ python ]" in result.yaml_content def test_yaml_has_callbacks(self, exporter, gain_block): """Setter methods are detected as GRC callbacks.""" result = exporter.generate_grc_yaml(gain_block) assert "set_gain(${gain})" in result.yaml_content assert "callbacks:" in result.yaml_content def test_yaml_has_asserts_from_min_value(self, exporter, gain_block): """Parameter min_value generates GRC asserts.""" result = exporter.generate_grc_yaml(gain_block) assert "gain >= 0.0" in result.yaml_content def test_yaml_has_parameter_dtype_mapping(self, exporter, gain_block): """Python 'float' maps to GRC 'real' dtype.""" result = exporter.generate_grc_yaml(gain_block) assert "dtype: real" in result.yaml_content def test_yaml_has_documentation(self, exporter, gain_block): """Docstring is included as documentation.""" result = exporter.generate_grc_yaml(gain_block) assert "Configurable gain block" in result.yaml_content def test_custom_module_name(self, exporter, gain_block): """Module name changes block ID and import.""" result = exporter.generate_grc_yaml(gain_block, module_name="dsp") assert result.block_id == "dsp_configurable_gain" assert "from dsp import configurable_gain" in result.yaml_content def test_custom_category(self, exporter, gain_block): """Category can be overridden.""" result = exporter.generate_grc_yaml(gain_block, category="Signal Processing") assert "category: [Signal Processing]" in result.yaml_content def test_notes_for_no_setters(self, exporter): """Notes warn about missing setter methods.""" block = 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="no_setters", 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="no_setters", 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)], is_valid=True, ) result = exporter.generate_grc_yaml(block) assert any("set_*()" in note for note in result.notes) def test_file_format_1(self, exporter, gain_block): """YAML includes file_format: 1.""" result = exporter.generate_grc_yaml(gain_block) assert "file_format: 1" in result.yaml_content