Files
reaper-control/tests/test_reaper_scripting.py
renato97 b08dcccca2 feat: reascript-first — built-in REAPER plugin insertion via ReaScript API
Hybrid pipeline: RPPBuilder writes VST3/MIDI/audio skeleton, ReaScript
handles built-in plugins (ReaEQ, ReaComp) via TrackFX_AddByName +
TrackFX_SetParam with multi-action dispatch, adaptive API check, and
builtin plugin auto-detection from PLUGIN_REGISTRY.

326 tests (298 existing + 28 new), 12/12 spec scenarios compliant.
2026-05-04 09:38:58 -03:00

828 lines
30 KiB
Python

"""Tests for src/reaper_scripting/ — command protocol and ReaScriptGenerator."""
from __future__ import annotations
import ast
import sys
from pathlib import Path
import json
import tempfile
sys.path.insert(0, str(Path(__file__).parents[1]))
import pytest
from src.reaper_scripting.commands import (
ProtocolVersionError,
ReaScriptCommand,
ReaScriptResult,
read_result,
write_command,
)
from src.reaper_scripting import ReaScriptGenerator
from src.core.schema import (
SongDefinition,
SongMeta,
TrackDef,
PluginDef,
)
from src.reaper_builder import (
RPPBuilder,
REAPER_BUILTINS,
get_builtin_plugins,
)
# ------------------------------------------------------------------
# Phase 1: Command/Result Protocol
# ------------------------------------------------------------------
class TestCommandSerialization:
"""Test JSON round-trip for ReaScriptCommand."""
def test_write_command_produces_json_file(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "cmd.json"
write_command(path, cmd)
assert path.exists()
def test_roundtrip_command_fields(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="verify_fx",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=60,
track_calibration=[
{"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []},
],
)
path = tmp_path / "cmd.json"
write_command(path, cmd)
text = path.read_text(encoding="utf-8")
data = json.loads(text)
assert data["version"] == 1
assert data["action"] == "verify_fx"
assert data["rpp_path"] == "C:/song.rpp"
assert data["render_path"] == "C:/song.wav"
assert data["timeout"] == 60
assert len(data["track_calibration"]) == 1
def test_read_result_roundtrip(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "cmd.json"
write_command(path, cmd)
result = read_result(path)
assert result.version == 1
assert result.status == "ok"
assert result.integrated_lufs is None
assert result.fx_errors == []
class TestVersionMismatch:
"""Test ProtocolVersionError on version mismatch."""
def test_version_mismatch_raises(self, tmp_path):
path = tmp_path / "bad_result.json"
path.write_text(
json.dumps({"version": 2, "status": "ok", "message": "", "fx_errors": []}),
encoding="utf-8",
)
with pytest.raises(ProtocolVersionError) as exc_info:
read_result(path)
assert "expected 1, got 2" in str(exc_info.value)
class TestMissingFile:
"""Test FileNotFoundError on missing file."""
def test_missing_command_raises_file_not_found(self):
with pytest.raises(FileNotFoundError):
read_result(Path("nonexistent.json"))
def test_missing_result_raises_file_not_found(self, tmp_path):
nonexistent = tmp_path / "does_not_exist.json"
with pytest.raises(FileNotFoundError):
read_result(nonexistent)
# ------------------------------------------------------------------
# Phase 2: ReaScriptGenerator
# ------------------------------------------------------------------
class TestReaScriptGeneratorOutput:
"""Test that ReaScriptGenerator produces valid Python code."""
def test_generate_produces_file(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
assert path.exists()
def test_generate_output_is_valid_python(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Must not raise
tree = ast.parse(source)
assert tree is not None
def test_contains_required_api_calls(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
required = [
"Main_openProject",
"TrackFX_GetCount",
"TrackFX_GetFXName",
"SetMediaTrackInfo_Value",
"CreateTrackSend",
"Main_RenderFile",
"CalcMediaSrcLoudness",
"GetFunctionMetadata",
]
for api in required:
assert api in source, f"{api} not found in generated script"
def test_script_reads_command_path(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Must reference the command JSON path
assert "fl_control_command.json" in source
assert "fl_control_result.json" in source
def test_script_has_handrolled_json_parser(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Must NOT use Python's json module
assert "import json" not in source
# Must have hand-rolled parser
assert "parse_json" in source
assert "write_json" in source
def test_script_includes_api_check_function(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="verify_fx",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
assert "check_api" in source
assert "GetFunctionMetadata" in source
def test_verify_fx_action_iterates_tracks(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="verify_fx",
rpp_path="C:/song.rpp",
render_path="",
timeout=60,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Must iterate all tracks to verify FX
assert "CountTracks" in source
assert "GetTrack" in source
assert "fx_errors" in source
def test_calibrate_action_sets_track_volume_pan(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[
{"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []},
{
"track_index": 1,
"volume": 0.7,
"pan": -0.3,
"sends": [{"dest_track_index": 5, "level": 0.05}],
},
],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Volume and pan setting
assert "D_VOL" in source
assert "D_PAN" in source
# Send creation
assert "CreateTrackSend" in source
def test_error_handling_writes_error_result(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Must handle errors gracefully and write error status
assert '"status": "error"' in source or "'status': 'error'" in source
assert "write_json" in source
def test_script_has_main_function(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
assert "def main():" in source
assert "main()" in source
# ------------------------------------------------------------------
# Phase 4: Multi-Action Dispatch, Adaptive API, JSON Round-Trip
# ------------------------------------------------------------------
class TestMultiActionDispatch:
"""4.1 Multi-action dispatch generates correct per-action functions."""
@pytest.mark.parametrize("action_list,expected_funcs,unexpected_funcs", [
# calibrate only → calibrate function present, render/add_plugins NOT emitted
(["calibrate"], ["def calibrate("], ["def add_plugins(", "def configure_fx_params(", "def do_render("]),
# add_plugins only → add_plugins and find_track present, calibrate/render NOT
(["add_plugins"], ["def add_plugins(", "def find_track("], ["def calibrate(", "def configure_fx_params(", "def do_render("]),
# add_plugins + verify_fx → both present, calibrate/render NOT
(["add_plugins", "verify_fx"], ["def add_plugins(", "def verify_fx(", "def find_track("], ["def calibrate(", "def do_render("]),
# full pipeline → all 4 action functions present
(["add_plugins", "configure_fx_params", "calibrate", "render"],
["def add_plugins(", "def configure_fx_params(", "def calibrate(", "def do_render("], []),
])
def test_per_action_functions_conditional(self, tmp_path, action_list, expected_funcs, unexpected_funcs):
cmd = ReaScriptCommand(
version=1,
action=action_list,
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
for func in expected_funcs:
assert func in source, f"Expected '{func}' in output for actions={action_list}"
for func in unexpected_funcs:
assert func not in source, f"Unexpected '{func}' in output for actions={action_list}"
def test_string_action_backward_compat(self, tmp_path):
"""String action 'calibrate' produces same output as list ['calibrate']."""
# Generate with string
cmd_str = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path_str = tmp_path / "str_action.py"
gen = ReaScriptGenerator()
gen.generate(path_str, cmd_str)
source_str = path_str.read_text(encoding="utf-8")
# Generate with list
cmd_list = ReaScriptCommand(
version=1,
action=["calibrate"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path_list = tmp_path / "list_action.py"
gen.generate(path_list, cmd_list)
source_list = path_list.read_text(encoding="utf-8")
# Both should be identical
assert source_str == source_list, (
"String action 'calibrate' should produce same output as list ['calibrate']"
)
def test_empty_action_defaults_to_calibrate(self, tmp_path):
"""Empty action list defaults to ['calibrate']."""
cmd = ReaScriptCommand(
version=1,
action=[],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
assert "def calibrate(" in source
# Render functions are NOT emitted since action defaults to ["calibrate"] only
assert "def do_render(" not in source
def test_dispatch_loop_present_with_multiple_actions(self, tmp_path):
"""When multiple actions present, dispatch loop iterates them."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "verify_fx", "calibrate"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Each action should appear in the dispatch if/elif chain
assert '"add_plugins"' in source
assert '"verify_fx"' in source
assert '"calibrate"' in source
# action_list variable exists
assert "action_list" in source
class TestAdaptiveApiCheck:
"""4.2 Adaptive check_api requires FX APIs only with add_plugins."""
def test_check_api_requires_fx_apis_with_add_plugins(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action=["add_plugins"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
assert "TrackFX_AddByName" in source
assert "TrackFX_SetParam" in source
assert "check_api" in source
def test_check_api_requires_fx_apis_with_configure_fx_params(self, tmp_path):
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "configure_fx_params"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
assert "TrackFX_AddByName" in source
assert "TrackFX_SetParam" in source
def test_check_api_omits_fx_apis_for_calibrate_only(self, tmp_path):
"""String 'calibrate' (backward compat) should NOT include FX APIs."""
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Verify check_api exists but does NOT include FX APIs
assert "check_api" in source
# The check_api function defines required list; TrackFX_AddByName should NOT be in that list
# Find the check_api function body
check_api_start = source.find("def check_api():")
check_api_end = source.find("def ", check_api_start + 1)
if check_api_end == -1:
check_api_section = source[check_api_start:]
else:
check_api_section = source[check_api_start:check_api_end]
assert "TrackFX_AddByName" not in check_api_section
assert "TrackFX_SetParam" not in check_api_section
def test_check_api_includes_fx_apis_with_add_plugins_in_list(self, tmp_path):
"""When add_plugins is in the action list, FX APIs are required."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "calibrate"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
)
path = tmp_path / "phase2.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
check_api_start = source.find("def check_api():")
check_api_end = source.find("def ", check_api_start + 1)
if check_api_end == -1:
check_api_section = source[check_api_start:]
else:
check_api_section = source[check_api_start:check_api_end]
assert "TrackFX_AddByName" in check_api_section
assert "TrackFX_SetParam" in check_api_section
class TestPluginsToAddRoundTrip:
"""4.3 plugins_to_add/added_plugins JSON round-trip serialization."""
def test_write_read_roundtrip_plugins_to_add(self, tmp_path):
"""Write command with plugins_to_add, read back, verify fields survive."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "configure_fx_params", "calibrate"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
plugins_to_add=[
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
{"track_name": "Lead", "fx_name": "ReaComp", "params": {"0": -18.0}},
],
)
cmd_path = tmp_path / "cmd.json"
write_command(cmd_path, cmd)
# Read back raw JSON
raw = json.loads(cmd_path.read_text(encoding="utf-8"))
assert "plugins_to_add" in raw
assert len(raw["plugins_to_add"]) == 2
assert raw["plugins_to_add"][0]["track_name"] == "Bass"
assert raw["plugins_to_add"][0]["fx_name"] == "ReaEQ"
assert raw["plugins_to_add"][0]["params"]["2"] == 200.0
def test_read_result_includes_added_plugins(self, tmp_path):
"""Read a result JSON with added_plugins field."""
data = {
"version": 1,
"status": "ok",
"message": "",
"fx_errors": [],
"tracks_verified": 2,
"added_plugins": [
{"fx_name": "ReaEQ", "instance_id": 0, "track_name": "Bass", "status": "ok"},
{"fx_name": "ReaComp", "instance_id": 0, "track_name": "Lead", "status": "ok"},
],
}
res_path = tmp_path / "result.json"
res_path.write_text(json.dumps(data), encoding="utf-8")
result = read_result(res_path)
assert len(result.added_plugins) == 2
assert result.added_plugins[0]["fx_name"] == "ReaEQ"
assert result.added_plugins[0]["instance_id"] == 0
assert result.added_plugins[1]["status"] == "ok"
def test_write_command_omits_empty_plugins_to_add(self, tmp_path):
"""When plugins_to_add is empty, it should NOT appear in JSON."""
cmd = ReaScriptCommand(
version=1,
action="calibrate",
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
plugins_to_add=[],
)
cmd_path = tmp_path / "cmd.json"
write_command(cmd_path, cmd)
raw = json.loads(cmd_path.read_text(encoding="utf-8"))
assert "plugins_to_add" not in raw
def test_read_result_handles_missing_added_plugins(self, tmp_path):
"""Old result JSON without added_plugins should default to empty list."""
data = {
"version": 1,
"status": "ok",
"message": "",
"fx_errors": [],
"tracks_verified": 0,
}
res_path = tmp_path / "result.json"
res_path.write_text(json.dumps(data), encoding="utf-8")
result = read_result(res_path)
assert result.added_plugins == []
class TestBuiltinPluginSkipping:
"""4.4 builtin=True PluginDef skipped in RPP .rpp output, listed in command."""
def test_builtin_plugin_not_in_rpp_output(self, tmp_path):
"""A PluginDef with builtin=True is excluded from .rpp VST elements."""
song = SongDefinition(
meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
tracks=[
TrackDef(
name="Bass",
volume=0.85,
plugins=[
PluginDef(
name="ReaEQ",
path="VST: ReaEQ (Cockos)",
index=0,
params={2: 200.0, 5: 3.0},
builtin=True,
),
PluginDef(
name="Serum_2",
path="VST3i: Serum 2 (Xfer Records)",
index=1,
builtin=False,
),
],
),
],
)
builder = RPPBuilder(song)
output_path = tmp_path / "test_builtin.rpp"
builder.write(output_path)
rpp_content = output_path.read_text(encoding="utf-8")
# ReaEQ (builtin=True) should NOT appear in the VST chain
assert "ReaEQ" not in rpp_content, (
"Builtin plugin ReaEQ should NOT be written to .rpp VST section"
)
# Serum_2 (builtin=False) SHOULD appear
assert "Serum 2" in rpp_content, (
"Non-builtin plugin Serum 2 SHOULD be written to .rpp"
)
def test_builtin_plugin_in_get_builtin_plugins(self):
"""get_builtin_plugins() returns builtin=True plugins for the command."""
song = SongDefinition(
meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
tracks=[
TrackDef(
name="Bass",
plugins=[
PluginDef(
name="ReaEQ",
path="VST: ReaEQ (Cockos)",
index=0,
params={2: 200.0, 5: 3.0},
builtin=True,
),
PluginDef(
name="ReaComp",
path="VST: ReaComp (Cockos)",
index=1,
params={0: -18.0},
builtin=False,
),
],
),
],
)
builtins = get_builtin_plugins(song)
# ReaEQ has builtin=True → should appear
assert len(builtins) >= 1
reaeq_entries = [b for b in builtins if b["fx_name"] == "ReaEQ"]
assert len(reaeq_entries) == 1
assert reaeq_entries[0]["track_name"] == "Bass"
# ReaComp with builtin=False BUT name in REAPER_BUILTINS → should also appear
assert "ReaComp" in REAPER_BUILTINS
reacomp_entries = [b for b in builtins if b["fx_name"] == "ReaComp"]
assert len(reacomp_entries) == 1
def test_non_builtin_not_in_reaper_builtins(self):
"""A non-builtin, non-Cockos plugin name is NOT in REAPER_BUILTINS."""
assert "Serum_2" not in REAPER_BUILTINS
assert "Omnisphere" not in REAPER_BUILTINS
# But Cockos plugins ARE
assert "ReaEQ" in REAPER_BUILTINS
assert "ReaComp" in REAPER_BUILTINS
def test_params_survive_in_get_builtin_plugins(self):
"""Plugin params are correctly converted to string-keyed dict."""
song = SongDefinition(
meta=SongMeta(bpm=95, key="Am", title="Test Params"),
tracks=[
TrackDef(
name="Bass",
plugins=[
PluginDef(
name="ReaEQ",
path="VST: ReaEQ (Cockos)",
index=0,
params={2: 200.0, 5: 3.0},
builtin=True,
),
],
),
],
)
builtins = get_builtin_plugins(song)
reaeq = builtins[0]
assert reaeq["params"] == {"2": 200.0, "5": 3.0}
# ------------------------------------------------------------------
# Phase 5: Generated Code Content Assertions
# ------------------------------------------------------------------
class TestAddPluginsSourceContent:
"""Verify add_plugins generated source contains correct ReaScript API calls."""
@pytest.fixture
def source_with_add_plugins(self, tmp_path: Path) -> str:
"""Generate script with add_plugins action and return source."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
plugins_to_add=[
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
],
)
path = tmp_path / "content_test.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
return path.read_text(encoding="utf-8")
def test_add_plugins_calls_trackfx_addbyname(self, source_with_add_plugins: str):
"""Insert ReaEQ on target track — TrackFX_AddByName is called."""
assert "RPR_TrackFX_AddByName" in source_with_add_plugins
def test_add_plugins_handles_track_not_found(self, source_with_add_plugins: str):
"""Track not found — error written, script continues."""
assert "track not found" in source_with_add_plugins.lower()
assert "continue" in source_with_add_plugins
def test_add_plugins_verifies_with_getfxname(self, source_with_add_plugins: str):
"""Plugin loaded successfully — GetFXName case-insensitive match."""
assert "RPR_TrackFX_GetFXName" in source_with_add_plugins
assert ".lower()" in source_with_add_plugins # case-insensitive comparison
def test_add_plugins_records_ok_status(self, source_with_add_plugins: str):
"""Plugin loaded — 'ok' status recorded in results."""
assert '"status": "ok"' in source_with_add_plugins
def test_add_plugins_records_failed_status(self, source_with_add_plugins: str):
"""Plugin failed to load — 'failed to load' status recorded."""
assert "failed to load" in source_with_add_plugins
class TestConfigureFxParamsSourceContent:
"""Verify configure_fx_params generated source contains correct parameter setting."""
@pytest.fixture
def source_with_configure(self, tmp_path: Path) -> str:
"""Generate script with configure_fx_params action and return source."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "configure_fx_params"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
plugins_to_add=[
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
],
)
path = tmp_path / "content_test.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
return path.read_text(encoding="utf-8")
def test_configure_calls_setparam(self, source_with_configure: str):
"""Set ReaEQ frequency and gain — TrackFX_SetParam called."""
assert "RPR_TrackFX_SetParam" in source_with_configure
def test_configure_iterates_params_dict(self, source_with_configure: str):
"""Iterates params dict, converts string keys to int indices."""
assert "int(param_idx_str)" in source_with_configure
def test_configure_finds_fx_by_name(self, source_with_configure: str):
"""Looks up FX by name to get target index before setting params."""
assert "RPR_TrackFX_GetCount" in source_with_configure
assert "RPR_TrackFX_GetFXName" in source_with_configure
class TestNoBareExcept:
"""Verify generated script template has no bare except clauses."""
def test_no_bare_except_in_generated_source(self, tmp_path: Path):
"""Generated ReaScript must not contain bare 'except:' (only 'except ...')."""
cmd = ReaScriptCommand(
version=1,
action=["add_plugins", "configure_fx_params", "calibrate", "render"],
rpp_path="C:/song.rpp",
render_path="C:/song.wav",
timeout=120,
track_calibration=[],
plugins_to_add=[
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
],
)
path = tmp_path / "no_bare_except.py"
gen = ReaScriptGenerator()
gen.generate(path, cmd)
source = path.read_text(encoding="utf-8")
# Check no bare "except:" (without exception type)
lines = source.split("\n")
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith("except:") and len(stripped) == len("except:"):
pytest.fail(
f"Bare 'except:' found at line {i + 1}: {line}"
)