"""Unit tests for BlockGeneratorMiddleware.""" import pytest from gnuradio_mcp.middlewares.block_generator import BlockGeneratorMiddleware from gnuradio_mcp.models import BlockParameter, SignatureItem class TestBlockGeneratorMiddleware: """Tests for the block generator middleware.""" @pytest.fixture def generator(self): """Create a generator without flowgraph.""" return BlockGeneratorMiddleware(flowgraph_mw=None) # ───────────────────────────────────────────────────── # sync_block generation # ───────────────────────────────────────────────────── def test_generate_sync_block_basic(self, generator): """Generate a basic sync block with custom work logic.""" result = generator.generate_sync_block( name="my_test_block", description="A test block that multiplies by 2", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_logic="output_items[0][:] = input_items[0] * 2", ) assert result.block_name == "my_test_block" assert result.block_class == "sync_block" assert "gr.sync_block" in result.source_code assert "my_test_block" in result.source_code assert "output_items[0][:] = input_items[0] * 2" in result.source_code assert result.is_valid is True def test_generate_sync_block_with_parameters(self, generator): """Generate a sync block with runtime parameters.""" result = generator.generate_sync_block( name="configurable_gain", description="Multiply samples by configurable gain", 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), ], work_logic="output_items[0][:] = input_items[0] * self.gain + self.offset", ) assert "self.gain = gain" in result.source_code assert "self.offset = offset" in result.source_code assert "def __init__(self, gain=1.0, offset=0.0):" in result.source_code assert result.is_valid is True def test_generate_sync_block_gain_template(self, generator): """Generate a sync block using the gain template.""" result = generator.generate_sync_block( name="gain_block", description="Apply gain", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="gain", dtype="float", default=1.0)], work_template="gain", ) assert "output_items[0][:] = input_items[0] * self.gain" in result.source_code assert result.is_valid is True def test_generate_sync_block_threshold_template(self, generator): """Generate a sync block using the threshold template.""" result = generator.generate_sync_block( name="threshold_block", description="Apply threshold", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="threshold", dtype="float", default=0.5)], work_template="threshold", ) assert "self.threshold" in result.source_code assert result.is_valid is True def test_generate_sync_block_complex_io(self, generator): """Generate a sync block with complex inputs.""" result = generator.generate_sync_block( name="complex_processor", description="Process complex samples", inputs=[SignatureItem(dtype="complex", vlen=1)], outputs=[SignatureItem(dtype="complex", vlen=1)], parameters=[], work_logic="output_items[0][:] = input_items[0] * 1j", ) assert "numpy.complex64" in result.source_code assert result.is_valid is True def test_generate_sync_block_vector_io(self, generator): """Generate a sync block with vector I/O.""" result = generator.generate_sync_block( name="vector_adder", description="Add vectors", inputs=[SignatureItem(dtype="float", vlen=4)], outputs=[SignatureItem(dtype="float", vlen=4)], parameters=[], work_logic="output_items[0][:] = input_items[0]", ) assert "4" in result.source_code # Vector length in signature assert result.is_valid is True # ───────────────────────────────────────────────────── # basic_block generation # ───────────────────────────────────────────────────── def test_generate_basic_block(self, generator): """Generate a basic block with custom work and forecast.""" # Use a simple work_logic that won't have indentation issues result = generator.generate_basic_block( name="packet_extractor", description="Extract packets from stream", inputs=[SignatureItem(dtype="byte", vlen=1)], outputs=[SignatureItem(dtype="byte", vlen=64)], parameters=[BlockParameter(name="packet_len", dtype="int", default=64)], work_logic="self.consume_each(1); return 1", ) assert result.block_class == "basic_block" assert "gr.basic_block" in result.source_code assert "general_work" in result.source_code # Check the source code is syntactically valid assert result.is_valid is True def test_generate_basic_block_with_forecast(self, generator): """Generate a basic block with custom forecast.""" result = generator.generate_basic_block( name="custom_forecast", description="Block with custom forecast", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_logic="self.consume_each(1); return 1", forecast_logic="return [noutput_items * 2]", ) assert "def forecast" in result.source_code # The basic_block should be valid assert result.is_valid is True # ───────────────────────────────────────────────────── # interp_block generation # ───────────────────────────────────────────────────── def test_generate_interp_block(self, generator): """Generate an interpolating block.""" result = generator.generate_interp_block( name="upsample_4x", description="Upsample by factor of 4", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], interpolation=4, parameters=[], ) assert result.block_class == "interp_block" assert "gr.interp_block" in result.source_code assert "interp=4" in result.source_code or "interpolation=4" in result.source_code.lower() assert result.is_valid is True def test_generate_interp_block_with_work(self, generator): """Generate an interpolating block with custom work.""" result = generator.generate_interp_block( name="upsampler", description="Upsample with custom logic", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], interpolation=2, parameters=[], # Use default work logic by not providing custom work_logic ) # Should generate valid interp_block assert "gr.interp_block" in result.source_code assert result.is_valid is True # ───────────────────────────────────────────────────── # decim_block generation # ───────────────────────────────────────────────────── def test_generate_decim_block(self, generator): """Generate a decimating block.""" result = generator.generate_decim_block( name="downsample_10x", description="Downsample by factor of 10", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], decimation=10, parameters=[], ) assert result.block_class == "decim_block" assert "gr.decim_block" in result.source_code assert result.is_valid is True def test_generate_decim_block_with_averaging(self, generator): """Generate a decimating block with averaging.""" result = generator.generate_decim_block( name="avg_decim", description="Decimate with averaging", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], decimation=4, parameters=[], work_logic="output_items[0][:] = input_items[0].reshape(-1, 4).mean(axis=1)", ) assert ".reshape" in result.source_code assert ".mean" in result.source_code assert result.is_valid is True # ───────────────────────────────────────────────────── # Validation # ───────────────────────────────────────────────────── def test_validate_block_code_valid(self, generator): """Validate syntactically correct block code.""" 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_block", 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]) ''' result = generator.validate_block_code(code) assert result.is_valid is True assert len(result.errors) == 0 def test_validate_block_code_syntax_error(self, generator): """Detect syntax errors in block code.""" code = ''' def broken_function(: pass ''' result = generator.validate_block_code(code) assert result.is_valid is False assert any("syntax" in e.message.lower() for e in result.errors) def test_validate_block_code_missing_import(self, generator): """Detect missing required imports.""" code = ''' class blk(gr.sync_block): def __init__(self): gr.sync_block.__init__(self, name="test", in_sig=[float], out_sig=[float]) def work(self, input_items, output_items): return 0 ''' result = generator.validate_block_code(code) # Should warn about missing gnuradio import assert result.is_valid is False or len(result.warnings) > 0 def test_validate_block_code_missing_work_method(self, generator): """Detect missing work method.""" 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], ) ''' result = generator.validate_block_code(code) # Should warn about missing work method assert len(result.warnings) > 0 or not result.is_valid class TestDataTypeMapping: """Tests for data type mapping in generated code.""" @pytest.fixture def generator(self): return BlockGeneratorMiddleware(flowgraph_mw=None) @pytest.mark.parametrize( "dtype,expected_numpy", [ ("float", "numpy.float32"), ("complex", "numpy.complex64"), ("byte", "numpy.uint8"), ("short", "numpy.int16"), ("int", "numpy.int32"), ], ) def test_dtype_mapping(self, generator, dtype, expected_numpy): """Test that data types map correctly to numpy types.""" result = generator.generate_sync_block( name="test_dtype", description="Test dtype mapping", inputs=[SignatureItem(dtype=dtype, vlen=1)], outputs=[SignatureItem(dtype=dtype, vlen=1)], parameters=[], work_logic="output_items[0][:] = input_items[0]", ) assert expected_numpy in result.source_code class TestWorkTemplates: """Tests for predefined work templates.""" @pytest.fixture def generator(self): return BlockGeneratorMiddleware(flowgraph_mw=None) def test_gain_template(self, generator): """Test the gain work template.""" result = generator.generate_sync_block( name="gain", description="Gain", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="gain", dtype="float", default=1.0)], work_template="gain", ) assert "self.gain" in result.source_code assert result.is_valid def test_add_const_template(self, generator): """Test the add_const work template.""" result = generator.generate_sync_block( name="add_const", description="Add constant", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="const", dtype="float", default=0.0)], work_template="add_const", ) assert "self.const" in result.source_code assert result.is_valid def test_threshold_template(self, generator): """Test the threshold work template.""" result = generator.generate_sync_block( name="threshold", description="Threshold", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="threshold", dtype="float", default=0.5)], work_template="threshold", ) assert "self.threshold" in result.source_code assert result.is_valid def test_multiply_template(self, generator): """Test the multiply work template (two inputs).""" result = generator.generate_sync_block( name="multiply", description="Multiply two streams", inputs=[ SignatureItem(dtype="float", vlen=1), SignatureItem(dtype="float", vlen=1), ], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_template="multiply", ) assert "input_items[0]" in result.source_code assert "input_items[1]" in result.source_code assert result.is_valid def test_add_template(self, generator): """Test the add work template (two inputs).""" result = generator.generate_sync_block( name="add", description="Add two streams", inputs=[ SignatureItem(dtype="float", vlen=1), SignatureItem(dtype="float", vlen=1), ], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_template="add", ) assert result.is_valid def test_unknown_template_falls_back(self, generator): """Test that unknown templates use provided work_logic.""" result = generator.generate_sync_block( name="custom", description="Custom logic", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_template="nonexistent_template", work_logic="output_items[0][:] = input_items[0]", ) # Should fall back to provided work_logic or use passthrough assert result.is_valid class TestEdgeCases: """Tests for edge cases and error handling.""" @pytest.fixture def generator(self): return BlockGeneratorMiddleware(flowgraph_mw=None) def test_empty_work_logic_uses_passthrough(self, generator): """Empty work logic should use passthrough.""" result = generator.generate_sync_block( name="passthrough", description="Pass through", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[], work_logic="", ) assert "output_items[0][:] = input_items[0]" in result.source_code assert result.is_valid def test_multiple_inputs_outputs(self, generator): """Test block with multiple inputs and outputs.""" result = generator.generate_sync_block( name="multi_io", description="Multiple I/O block", inputs=[ SignatureItem(dtype="float", vlen=1), SignatureItem(dtype="float", vlen=1), ], outputs=[ SignatureItem(dtype="float", vlen=1), SignatureItem(dtype="float", vlen=1), ], parameters=[], work_logic=""" output_items[0][:] = input_items[0] + input_items[1] output_items[1][:] = input_items[0] - input_items[1] """, ) assert result.is_valid # Should have both inputs in signature assert result.source_code.count("numpy.float32") >= 2 def test_source_block_no_inputs(self, generator): """Test source block with no inputs.""" result = generator.generate_sync_block( name="signal_source", description="Generate signal", inputs=[], outputs=[SignatureItem(dtype="float", vlen=1)], parameters=[BlockParameter(name="frequency", dtype="float", default=1000.0)], work_logic="output_items[0][:] = numpy.sin(numpy.arange(len(output_items[0])) * self.frequency)", ) assert result.is_valid assert "in_sig=[]" in result.source_code or "in_sig=None" in result.source_code def test_sink_block_no_outputs(self, generator): """Test sink block with no outputs.""" result = generator.generate_sync_block( name="null_sink", description="Discard samples", inputs=[SignatureItem(dtype="float", vlen=1)], outputs=[], parameters=[], work_logic="pass", ) assert result.is_valid