Code Coverage
GR-MCP can collect Python code coverage from flowgraphs running in Docker containers. This is useful for measuring test coverage of GNU Radio applications and embedded Python blocks.
How It Works
- Launch with
enable_coverage=True— container runs flowgraph undercoverage.py - Run your test scenarios via XML-RPC variable control
- Stop the flowgraph gracefully — coverage data is written to disk
- Collect and analyze — generate reports in HTML, XML, or JSON
┌─────────────────────────────────────────────────────────────┐│ Docker Container ││ ┌─────────────────────────────────────────────────────────┐││ │ coverage run flowgraph.py │││ │ └─ writes .coverage.* files on exit │││ └─────────────────────────────────────────────────────────┘││ │ ││ ▼ ││ /tmp/gr-mcp-coverage/{container_name}/ ││ ├─ .coverage ││ ├─ .coverage.{hostname}.{pid}.* ││ └─ htmlcov/ │└─────────────────────────────────────────────────────────────┘Prerequisites
Build the coverage-enabled Docker image:
docker build -f docker/Dockerfile.gnuradio-coverage \ -t gnuradio-coverage:latest docker/Collect Coverage
-
Launch with coverage enabled
enable_runtime_mode()launch_flowgraph(flowgraph_path="/tmp/flowgraph.py",name="coverage-test",enable_coverage=True) -
Run your test scenario
connect_to_container(name="coverage-test")# Exercise the flowgraphset_variable(name="freq", value=100e6)time.sleep(2)set_variable(name="freq", value=200e6)time.sleep(2)# ... more test actions ... -
Stop gracefully (required for coverage data)
stop_flowgraph(name="coverage-test") -
Collect the coverage data
collect_coverage(name="coverage-test")# Returns CoverageDataModel:# container_name: "coverage-test"# coverage_file: "/tmp/gr-mcp-coverage/coverage-test/.coverage"# summary: "Name Stmts Miss Cover\n..."# lines_covered: 150# lines_total: 200# coverage_percent: 75.0 -
Generate a report
generate_coverage_report(name="coverage-test",format="html")# Returns CoverageReportModel:# report_path: "/tmp/gr-mcp-coverage/coverage-test/htmlcov/index.html"
Report Formats
generate_coverage_report(name="coverage-test", format="html")Best for visual inspection — shows line-by-line coverage with highlighting.
generate_coverage_report(name="coverage-test", format="xml")Cobertura-compatible format for CI/CD integration.
generate_coverage_report(name="coverage-test", format="json")Machine-readable format for custom analysis.
Combine Multiple Runs
Aggregate coverage from multiple test scenarios:
# Run first scenariolaunch_flowgraph(..., name="test-1", enable_coverage=True)# ... exercise ...stop_flowgraph(name="test-1")
# Run second scenariolaunch_flowgraph(..., name="test-2", enable_coverage=True)# ... exercise ...stop_flowgraph(name="test-2")
# Combine resultscombined = combine_coverage(names=["test-1", "test-2"])# Returns CoverageDataModel for combined data
# Generate combined reportgenerate_coverage_report(name="combined", format="html")Clean Up Coverage Data
# Delete specific container's coveragedelete_coverage(name="coverage-test")# Returns: 1 (directories deleted)
# Delete old coverage datadelete_coverage(older_than_days=7)# Returns: number of directories deleted
# Delete all coverage datadelete_coverage()# Returns: number of directories deletedExample: Test Suite with Coverage
#!/usr/bin/env python3"""Run a test suite with coverage collection."""
import asyncioimport timefrom fastmcp import Client
TEST_SCENARIOS = [ {"name": "low-freq", "freq": 50e6, "duration": 5}, {"name": "mid-freq", "freq": 500e6, "duration": 5}, {"name": "high-freq", "freq": 2.4e9, "duration": 5},]
async def run_test_suite(): async with Client("gr-mcp") as client: await client.call_tool("enable_runtime_mode", {})
container_names = []
for scenario in TEST_SCENARIOS: name = f"cov-{scenario['name']}" container_names.append(name)
print(f"Running scenario: {scenario['name']}")
# Launch with coverage await client.call_tool("launch_flowgraph", { "flowgraph_path": "/tmp/radio_app.py", "name": name, "enable_coverage": True })
time.sleep(3)
# Connect and run scenario await client.call_tool("connect_to_container", {"name": name}) await client.call_tool("set_variable", { "name": "freq", "value": scenario["freq"] })
time.sleep(scenario["duration"])
# Stop gracefully for coverage await client.call_tool("stop_flowgraph", {"name": name})
# Collect this run's coverage result = await client.call_tool("collect_coverage", {"name": name}) print(f" Coverage: {result.data.coverage_percent}%")
# Combine all runs print("\nCombining coverage from all scenarios...") combined = await client.call_tool("combine_coverage", { "names": container_names }) print(f"Combined coverage: {combined.data.coverage_percent}%")
# Generate final report await client.call_tool("generate_coverage_report", { "name": "combined", "format": "html" }) print("HTML report: /tmp/gr-mcp-coverage/combined/htmlcov/index.html")
# Cleanup containers for name in container_names: await client.call_tool("remove_flowgraph", {"name": name})
if __name__ == "__main__": asyncio.run(run_test_suite())Coverage for Embedded Python Blocks
Coverage includes any embedded Python blocks in your flowgraph:
# Create an embedded blocksource = """import numpy as npfrom gnuradio import gr
class my_block(gr.sync_block): def __init__(self): gr.sync_block.__init__(self, ...)
def work(self, input_items, output_items): # This code path will show in coverage if input_items[0][0] > 0.5: output_items[0][:] = input_items[0] * 2 else: output_items[0][:] = input_items[0] return len(output_items[0])"""
create_embedded_python_block(source_code=source)Coverage reports will show which branches of your embedded block were exercised.
Next Steps
- Running in Docker — Container launch basics
- Runtime Control — Control flowgraphs during tests