feat: add generate_grc_yaml tool for GNU Radio Companion block definitions
Blocks generated by gr-mcp now have a first-class path to GRC's graphical editor. Previously, .block.yml was only produced as a side-effect of full OOT export — now clients can generate YAML standalone to preview, customize, or use without a full OOT module. - New MCP tool: generate_grc_yaml (registered in block dev mode) - New model: GrcYamlResult with yaml_content, notes, filename - Enhanced YAML generation: flags, asserts from min/max bounds, parameter descriptions, docstring extraction - New prompt resource: prompts://block-generation/grc-yaml - 10 new unit tests for GRC YAML generation (475 total, 7 skipped)
This commit is contained in:
parent
3fe862109d
commit
ce785029f4
@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from gnuradio_mcp.models import (
|
||||
GeneratedBlockCode,
|
||||
GrcYamlResult,
|
||||
OOTExportResult,
|
||||
OOTSkeletonResult,
|
||||
)
|
||||
@ -146,15 +147,15 @@ from importlib import import_module
|
||||
{imports}
|
||||
'''
|
||||
|
||||
BLOCK_YML_TEMPLATE = '''id: {module_name}_{block_name}
|
||||
BLOCK_YML_TEMPLATE = '''id: {block_id}
|
||||
label: {block_label}
|
||||
category: [{module_name}]
|
||||
category: [{category}]
|
||||
flags: [ python ]
|
||||
|
||||
templates:
|
||||
imports: from {module_name} import {block_name}
|
||||
make: {module_name}.{block_name}({make_args})
|
||||
imports: {import_line}
|
||||
make: {make_line}
|
||||
{callbacks}
|
||||
|
||||
parameters:
|
||||
{parameters}
|
||||
|
||||
@ -163,7 +164,7 @@ inputs:
|
||||
|
||||
outputs:
|
||||
{outputs}
|
||||
|
||||
{asserts}
|
||||
documentation: |-
|
||||
{documentation}
|
||||
|
||||
@ -410,7 +411,7 @@ class OOTExporterMiddleware:
|
||||
# Generate and write .block.yml
|
||||
grc_dir = base_path / "grc"
|
||||
yml_content = self._generate_block_yml(
|
||||
generated, module_name, block_name
|
||||
generated, module_name, block_name, None
|
||||
)
|
||||
yml_file = grc_dir / f"{module_name}_{block_name}.block.yml"
|
||||
yml_file.write_text(yml_content)
|
||||
@ -513,70 +514,144 @@ class OOTExporterMiddleware:
|
||||
# Block YAML Generation
|
||||
# ──────────────────────────────────────────
|
||||
|
||||
def generate_grc_yaml(
|
||||
self,
|
||||
generated: GeneratedBlockCode,
|
||||
module_name: str = "custom",
|
||||
category: str | None = None,
|
||||
) -> GrcYamlResult:
|
||||
"""Generate a GRC YAML block definition (.block.yml).
|
||||
|
||||
Produces a YAML file that makes a Python block usable in
|
||||
GNU Radio Companion's graphical editor. Without this file,
|
||||
the block can only be used programmatically.
|
||||
|
||||
Args:
|
||||
generated: Block code from generate_*_block tools
|
||||
module_name: Module namespace (e.g., "custom" → id becomes "custom_blockname")
|
||||
category: GRC palette category (default: module_name)
|
||||
|
||||
Returns:
|
||||
GrcYamlResult with YAML content and metadata.
|
||||
"""
|
||||
module_name = self._sanitize_module_name(module_name)
|
||||
block_name = self._sanitize_block_name(generated.block_name)
|
||||
|
||||
try:
|
||||
yaml_content = self._generate_block_yml(
|
||||
generated, module_name, block_name, category
|
||||
)
|
||||
block_id = f"{module_name}_{block_name}"
|
||||
filename = f"{block_id}.block.yml"
|
||||
|
||||
notes = []
|
||||
if not generated.parameters:
|
||||
notes.append("No parameters defined — block has no configurable properties in GRC.")
|
||||
if not any(
|
||||
m for m in self._parse_block_source(generated.source_code).get("callbacks", [])
|
||||
):
|
||||
notes.append(
|
||||
"No set_*() methods found — parameters won't update at runtime. "
|
||||
"Add setter methods to enable GRC callback support."
|
||||
)
|
||||
|
||||
return GrcYamlResult(
|
||||
success=True,
|
||||
block_id=block_id,
|
||||
yaml_content=yaml_content,
|
||||
filename=filename,
|
||||
notes=notes,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to generate GRC YAML")
|
||||
return GrcYamlResult(
|
||||
success=False,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
def _generate_block_yml(
|
||||
self,
|
||||
generated: GeneratedBlockCode,
|
||||
module_name: str,
|
||||
block_name: str,
|
||||
category: str | None = None,
|
||||
) -> str:
|
||||
"""Generate .block.yml content for a block."""
|
||||
# Parse source to extract info
|
||||
block_info = self._parse_block_source(generated.source_code)
|
||||
|
||||
# Build label
|
||||
block_id = f"{module_name}_{block_name}"
|
||||
block_label = block_name.replace("_", " ").title()
|
||||
cat = category or module_name
|
||||
|
||||
# Build make args
|
||||
# Import and make lines
|
||||
import_line = f"from {module_name} import {block_name}"
|
||||
make_args = ", ".join([
|
||||
f"{p.name}=${{{p.name}}}" for p in generated.parameters
|
||||
]) if generated.parameters else ""
|
||||
make_line = f"{module_name}.{block_name}({make_args})"
|
||||
|
||||
# Build callbacks section
|
||||
# Callbacks from detected setter methods
|
||||
callbacks = ""
|
||||
if block_info.get("callbacks"):
|
||||
callbacks = "callbacks:\n" + "\n".join([
|
||||
cb_lines = "\n".join([
|
||||
f" - set_{name}(${{{name}}})" for name in block_info["callbacks"]
|
||||
])
|
||||
callbacks = f" callbacks:\n{cb_lines}\n"
|
||||
|
||||
# Build parameters section
|
||||
# Parameters
|
||||
params_yml = ""
|
||||
for p in generated.parameters:
|
||||
params_yml += f"""- id: {p.name}
|
||||
label: {p.name.replace('_', ' ').title()}
|
||||
dtype: {self._python_to_grc_dtype(p.dtype)}
|
||||
default: {repr(p.default)}
|
||||
"""
|
||||
grc_dtype = self._python_to_grc_dtype(p.dtype)
|
||||
params_yml += f"- id: {p.name}\n"
|
||||
params_yml += f" label: {p.name.replace('_', ' ').title()}\n"
|
||||
params_yml += f" dtype: {grc_dtype}\n"
|
||||
params_yml += f" default: {repr(p.default)}\n"
|
||||
if p.description:
|
||||
params_yml += f" # {p.description}\n"
|
||||
|
||||
# Build inputs section
|
||||
# Asserts from parameter min/max bounds
|
||||
asserts_yml = ""
|
||||
assert_lines = []
|
||||
for p in generated.parameters:
|
||||
if p.min_value is not None:
|
||||
assert_lines.append(f"- ${{ {p.name} >= {p.min_value} }}")
|
||||
if p.max_value is not None:
|
||||
assert_lines.append(f"- ${{ {p.name} <= {p.max_value} }}")
|
||||
if assert_lines:
|
||||
asserts_yml = "asserts:\n" + "\n".join(assert_lines) + "\n"
|
||||
|
||||
# Inputs
|
||||
inputs_yml = ""
|
||||
for i, inp in enumerate(generated.inputs):
|
||||
inputs_yml += f"""- label: in{i}
|
||||
domain: stream
|
||||
dtype: {inp.dtype}
|
||||
vlen: {inp.vlen}
|
||||
"""
|
||||
inputs_yml += f"- label: in{i}\n"
|
||||
inputs_yml += f" domain: stream\n"
|
||||
inputs_yml += f" dtype: {inp.dtype}\n"
|
||||
if inp.vlen > 1:
|
||||
inputs_yml += f" vlen: {inp.vlen}\n"
|
||||
|
||||
# Build outputs section
|
||||
# Outputs
|
||||
outputs_yml = ""
|
||||
for i, out in enumerate(generated.outputs):
|
||||
outputs_yml += f"""- label: out{i}
|
||||
domain: stream
|
||||
dtype: {out.dtype}
|
||||
vlen: {out.vlen}
|
||||
"""
|
||||
outputs_yml += f"- label: out{i}\n"
|
||||
outputs_yml += f" domain: stream\n"
|
||||
outputs_yml += f" dtype: {out.dtype}\n"
|
||||
if out.vlen > 1:
|
||||
outputs_yml += f" vlen: {out.vlen}\n"
|
||||
|
||||
# Documentation
|
||||
# Documentation from docstring or generation prompt
|
||||
doc = block_info.get("doc", generated.generation_prompt or "Custom block")
|
||||
|
||||
return BLOCK_YML_TEMPLATE.format(
|
||||
module_name=module_name,
|
||||
block_name=block_name,
|
||||
block_id=block_id,
|
||||
block_label=block_label,
|
||||
make_args=make_args,
|
||||
category=cat,
|
||||
import_line=import_line,
|
||||
make_line=make_line,
|
||||
callbacks=callbacks,
|
||||
parameters=params_yml or "[]",
|
||||
inputs=inputs_yml or "[]",
|
||||
outputs=outputs_yml or "[]",
|
||||
asserts=asserts_yml,
|
||||
documentation=doc.replace("\n", "\n "),
|
||||
)
|
||||
|
||||
|
||||
@ -677,6 +677,21 @@ class IQAnalysisResult(BaseModel):
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class GrcYamlResult(BaseModel):
|
||||
"""Result of GRC YAML block definition generation.
|
||||
|
||||
Contains the YAML content for a .block.yml file that makes a
|
||||
block usable in GNU Radio Companion's graphical editor.
|
||||
"""
|
||||
|
||||
success: bool
|
||||
block_id: str = "" # e.g. "custom_my_gain"
|
||||
yaml_content: str = ""
|
||||
filename: str = "" # e.g. "custom_my_gain.block.yml"
|
||||
error: str | None = None
|
||||
notes: list[str] = [] # Warnings or suggestions
|
||||
|
||||
|
||||
class OOTExportResult(BaseModel):
|
||||
"""Result of exporting an embedded block to full OOT module.
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from gnuradio_mcp.prompts.sync_block import SYNC_BLOCK_PROMPT
|
||||
from gnuradio_mcp.prompts.basic_block import BASIC_BLOCK_PROMPT
|
||||
from gnuradio_mcp.prompts.decoder_chain import DECODER_CHAIN_PROMPT
|
||||
from gnuradio_mcp.prompts.common_patterns import COMMON_PATTERNS_PROMPT
|
||||
from gnuradio_mcp.prompts.grc_yaml import GRC_YAML_PROMPT
|
||||
from gnuradio_mcp.prompts.protocol_templates import (
|
||||
PROTOCOL_TEMPLATES,
|
||||
BLUETOOTH_LE_TEMPLATE,
|
||||
@ -24,6 +25,7 @@ __all__ = [
|
||||
"BASIC_BLOCK_PROMPT",
|
||||
"DECODER_CHAIN_PROMPT",
|
||||
"COMMON_PATTERNS_PROMPT",
|
||||
"GRC_YAML_PROMPT",
|
||||
# Protocol templates
|
||||
"PROTOCOL_TEMPLATES",
|
||||
"BLUETOOTH_LE_TEMPLATE",
|
||||
|
||||
124
src/gnuradio_mcp/prompts/grc_yaml.py
Normal file
124
src/gnuradio_mcp/prompts/grc_yaml.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""GRC YAML block definition guidance for LLM-assisted generation."""
|
||||
|
||||
GRC_YAML_PROMPT = """# GRC Block Definition (.block.yml) Guide
|
||||
|
||||
GNU Radio Companion (GRC) requires a `.block.yml` file for each block to
|
||||
appear in the graphical editor. Without this file, blocks are only
|
||||
available programmatically.
|
||||
|
||||
## When to Generate GRC YAML
|
||||
|
||||
Generate a .block.yml when:
|
||||
- The user will use GNU Radio Companion's graphical editor
|
||||
- The block is being exported to an OOT module
|
||||
- The block needs a GUI properties dialog
|
||||
|
||||
You do NOT need a .block.yml for:
|
||||
- Blocks used only in Python scripts
|
||||
- Embedded Python blocks (epy_block) — GRC handles these internally
|
||||
- Temporary/test blocks
|
||||
|
||||
## Key Fields
|
||||
|
||||
```yaml
|
||||
id: module_block # Unique ID, format: {module}_{block}
|
||||
label: Block Name # Display name in GRC palette
|
||||
category: [Module Name] # GRC palette category
|
||||
flags: [ python ] # Implementation language
|
||||
|
||||
templates:
|
||||
imports: from module import block # Python import
|
||||
make: module.block(gain=${gain}) # Constructor with ${param} substitution
|
||||
callbacks: # Runtime update methods
|
||||
- set_gain(${gain})
|
||||
|
||||
parameters:
|
||||
- id: gain
|
||||
label: Gain
|
||||
dtype: real # real, int, string, bool, enum, raw, complex
|
||||
default: 1.0
|
||||
|
||||
inputs:
|
||||
- label: in0
|
||||
domain: stream # stream (samples) or message (PDUs)
|
||||
dtype: float # float, complex, byte, short, int
|
||||
|
||||
outputs:
|
||||
- label: out0
|
||||
domain: stream
|
||||
dtype: float
|
||||
|
||||
asserts: # Parameter validation
|
||||
- ${ gain >= 0 }
|
||||
|
||||
documentation: |-
|
||||
Block description shown in GRC tooltips.
|
||||
|
||||
file_format: 1
|
||||
```
|
||||
|
||||
## GRC dtype vs Python dtype
|
||||
|
||||
| Python type | GRC parameter dtype | GRC port dtype |
|
||||
|-------------|--------------------:|---------------:|
|
||||
| float | real | float |
|
||||
| int | int | int |
|
||||
| str | string | — |
|
||||
| bool | bool | — |
|
||||
| complex | complex | complex |
|
||||
| bytes | — | byte |
|
||||
|
||||
## Callbacks (Runtime Updates)
|
||||
|
||||
For parameters to update at runtime in GRC, the block must have
|
||||
setter methods and the YAML must list them as callbacks:
|
||||
|
||||
```python
|
||||
# Python block
|
||||
def set_gain(self, gain):
|
||||
self.gain = gain
|
||||
```
|
||||
|
||||
```yaml
|
||||
# YAML definition
|
||||
templates:
|
||||
callbacks:
|
||||
- set_gain(${gain})
|
||||
```
|
||||
|
||||
## Enum Parameters
|
||||
|
||||
```yaml
|
||||
- id: mode
|
||||
label: Mode
|
||||
dtype: enum
|
||||
default: 'low'
|
||||
options: ['low', 'medium', 'high']
|
||||
option_labels: ['Low Power', 'Medium Power', 'High Power']
|
||||
```
|
||||
|
||||
## Message Ports
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
- domain: message
|
||||
id: pdu_in
|
||||
optional: true
|
||||
|
||||
outputs:
|
||||
- domain: message
|
||||
id: pdu_out
|
||||
```
|
||||
|
||||
## File Location
|
||||
|
||||
Install .block.yml files to `$GR_DATA_DIR/grc/blocks/` (typically
|
||||
`/usr/share/gnuradio/grc/blocks/`). OOT modules handle this via
|
||||
CMake — use `export_block_to_oot()` for the full workflow.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Generate block code: `generate_sync_block(...)`
|
||||
2. Generate GRC YAML: `generate_grc_yaml(source_code, block_name, ...)`
|
||||
3. (Optional) Export to OOT: `export_block_to_oot(...)` — includes YAML
|
||||
"""
|
||||
@ -14,6 +14,7 @@ from gnuradio_mcp.models import (
|
||||
BlockTestResult,
|
||||
DecoderPipelineModel,
|
||||
GeneratedBlockCode,
|
||||
GrcYamlResult,
|
||||
IQAnalysisResult,
|
||||
OOTExportResult,
|
||||
OOTSkeletonResult,
|
||||
@ -654,6 +655,65 @@ if __name__ == "__main__":
|
||||
|
||||
return self._protocol_analyzer.get_missing_oot_modules(pipeline)
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# GRC YAML Generation
|
||||
# ──────────────────────────────────────────
|
||||
|
||||
def generate_grc_yaml(
|
||||
self,
|
||||
source_code: str,
|
||||
block_name: str,
|
||||
module_name: str = "custom",
|
||||
inputs: list[dict[str, Any]] | None = None,
|
||||
outputs: list[dict[str, Any]] | None = None,
|
||||
parameters: list[dict[str, Any]] | None = None,
|
||||
category: str | None = None,
|
||||
) -> GrcYamlResult:
|
||||
"""Generate a GRC YAML block definition for GNU Radio Companion.
|
||||
|
||||
Produces a .block.yml file that makes a Python block appear in
|
||||
GRC's block palette with configurable parameters, typed ports,
|
||||
and runtime callbacks. Without this file, blocks can only be
|
||||
used programmatically — not in GRC's graphical editor.
|
||||
|
||||
Args:
|
||||
source_code: Python source code of the block
|
||||
block_name: Block name (e.g., "my_gain")
|
||||
module_name: Module namespace (default "custom")
|
||||
inputs: Input port specs [{"dtype": "float", "vlen": 1}]
|
||||
If not provided, extracted from code (defaults to 1x float)
|
||||
outputs: Output port specs (same format as inputs)
|
||||
parameters: Block params [{"name": "gain", "dtype": "float", "default": 1.0}]
|
||||
category: GRC palette category (default: module_name)
|
||||
|
||||
Returns:
|
||||
GrcYamlResult with YAML content. Save as
|
||||
grc/{module}_{block}.block.yml in your OOT module.
|
||||
|
||||
Note:
|
||||
For blocks to appear in GRC, the .block.yml must be
|
||||
installed to $GR_DATA_DIR/grc/blocks/ (typically
|
||||
/usr/share/gnuradio/grc/blocks/). OOT modules do this
|
||||
via CMake — see export_block_to_oot() for the full workflow.
|
||||
"""
|
||||
input_items = [SignatureItem(**inp) for inp in (inputs or [{"dtype": "float", "vlen": 1}])]
|
||||
output_items = [SignatureItem(**out) for out in (outputs or [{"dtype": "float", "vlen": 1}])]
|
||||
param_items = [BlockParameter(**p) for p in (parameters or [])]
|
||||
|
||||
generated = GeneratedBlockCode(
|
||||
source_code=source_code,
|
||||
block_name=block_name,
|
||||
block_class="sync_block",
|
||||
inputs=input_items,
|
||||
outputs=output_items,
|
||||
parameters=param_items,
|
||||
is_valid=True,
|
||||
)
|
||||
|
||||
return self._oot_exporter.generate_grc_yaml(
|
||||
generated, module_name, category
|
||||
)
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# OOT Module Export
|
||||
# ──────────────────────────────────────────
|
||||
|
||||
@ -104,6 +104,7 @@ class McpBlockDevProvider:
|
||||
- validate_block_code: Static code analysis
|
||||
- test_block_in_docker: Isolated testing (if Docker available)
|
||||
- inject_block: Add generated block to flowgraph
|
||||
- generate_grc_yaml: Create .block.yml for GNU Radio Companion
|
||||
- parse_protocol_spec: Extract protocol params from description
|
||||
- generate_decoder_chain: Generate block pipeline from protocol
|
||||
- analyze_iq_file: Detect signals and modulation in IQ captures
|
||||
@ -117,6 +118,7 @@ class McpBlockDevProvider:
|
||||
- Create protocol-specific decoders
|
||||
- Build and test new DSP algorithms
|
||||
- Analyze captured signals and auto-generate decoders
|
||||
- Generate GRC block definitions for graphical editor use
|
||||
- Export blocks to distributable OOT modules
|
||||
"""
|
||||
if self._block_dev_enabled:
|
||||
@ -205,6 +207,9 @@ class McpBlockDevProvider:
|
||||
# Signal analysis tools (Phase 4)
|
||||
self._add_tool("analyze_iq_file", p.analyze_iq_file)
|
||||
|
||||
# GRC YAML generation
|
||||
self._add_tool("generate_grc_yaml", p.generate_grc_yaml)
|
||||
|
||||
# OOT export tools (Phase 5)
|
||||
self._add_tool("generate_oot_skeleton", p.generate_oot_skeleton)
|
||||
self._add_tool("export_block_to_oot", p.export_block_to_oot)
|
||||
@ -474,6 +479,7 @@ class McpBlockDevProvider:
|
||||
BASIC_BLOCK_PROMPT,
|
||||
COMMON_PATTERNS_PROMPT,
|
||||
DECODER_CHAIN_PROMPT,
|
||||
GRC_YAML_PROMPT,
|
||||
SYNC_BLOCK_PROMPT,
|
||||
)
|
||||
|
||||
@ -513,7 +519,16 @@ class McpBlockDevProvider:
|
||||
def get_common_patterns_prompt() -> str:
|
||||
return COMMON_PATTERNS_PROMPT
|
||||
|
||||
logger.info("Registered 4 prompt template resources")
|
||||
@self._mcp.resource(
|
||||
"prompts://block-generation/grc-yaml",
|
||||
name="grc_yaml_prompt",
|
||||
description="Guide for generating GRC YAML block definitions (.block.yml)",
|
||||
mime_type="text/markdown",
|
||||
)
|
||||
def get_grc_yaml_prompt() -> str:
|
||||
return GRC_YAML_PROMPT
|
||||
|
||||
logger.info("Registered 5 prompt template resources")
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# Factory
|
||||
|
||||
@ -6,7 +6,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from gnuradio_mcp.middlewares.oot_exporter import OOTExporterMiddleware
|
||||
from gnuradio_mcp.models import GeneratedBlockCode, SignatureItem
|
||||
from gnuradio_mcp.models import BlockParameter, GeneratedBlockCode, SignatureItem
|
||||
|
||||
|
||||
class TestOOTExporterMiddleware:
|
||||
@ -317,3 +317,122 @@ class TestOOTExporterYAMLGeneration:
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user