refactor: migrate from FL Studio to REAPER with rpp library

Replace FL Studio binary .flp output with REAPER text-based .rpp output
using the rpp Python library (Perlence/rpp).

- Add core/schema.py: DAW-agnostic data types (SongDefinition, TrackDef,
  ClipDef, MidiNote, PluginDef)
- Add reaper_builder/: RPP file generation via rpp.Element + headless
  render via reaper.exe CLI
- Add composer/converters.py: bridge rhythm.py/melodic.py note dicts
  to core.schema MidiNote objects
- Rewrite scripts/compose.py: real generator pipeline with --render flag
- Delete src/flp_builder/, src/scanner/, mcp/, flstudio-mcp/, old scripts
- Add 40 passing tests (schema, builder, converters, compose, render)
This commit is contained in:
renato97
2026-05-03 09:13:35 -03:00
parent 1e2316a5a4
commit af6d61c8a1
47 changed files with 1589 additions and 4990 deletions

105
tests/test_render.py Normal file
View File

@@ -0,0 +1,105 @@
"""Tests for src/reaper_builder/render.py — render_project."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1]))
import pytest
from unittest.mock import patch, MagicMock
from src.reaper_builder.render import render_project
class TestRenderProjectFileNotFound:
"""Test render_project raises FileNotFoundError when reaper.exe path doesn't exist."""
def test_render_project_raises_file_not_found_for_nonexistent_reaper_exe(self):
"""FileNotFoundError is raised when reaper.exe does not exist."""
nonexistent = Path("C:/Program Files/NONEXISTENT_REAPER/reaper.exe")
if nonexistent.exists():
pytest.skip("Nonexistent path actually exists — cannot test this")
with pytest.raises(FileNotFoundError):
render_project(
rpp_path="input.rpp",
output_wav="output.wav",
reaper_exe=nonexistent,
)
def test_render_project_raises_file_not_found_default_path(self):
"""FileNotFoundError raised when default reaper.exe doesn't exist and none provided."""
# Patch DEFAULT_REAPER_EXE to a definitely-nonexistent path
with patch("src.reaper_builder.render.DEFAULT_REAPER_EXE", Path("/no/such/reaper.exe")):
with pytest.raises(FileNotFoundError):
render_project(
rpp_path="input.rpp",
output_wav="output.wav",
reaper_exe=None,
)
class TestRenderProjectSubprocessError:
"""Test render_project raises RuntimeError when subprocess returns non-zero exit code."""
def test_render_project_raises_runtime_error_on_nonzero_exit(self):
"""RuntimeError is raised when subprocess.run returns non-zero returncode."""
from src.reaper_builder.render import DEFAULT_REAPER_EXE
# Check if reaper exists — if not, skip
if not DEFAULT_REAPER_EXE.exists():
pytest.skip(f"REAPER not installed at {DEFAULT_REAPER_EXE}")
# Create a minimal valid .rpp for this test
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".rpp", delete=False, encoding="utf-8") as f:
rpp_path = f.name
f.write('<REAPER_PROJECT 0.1 "6.0" 0\n>\n')
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
wav_path = f.name
try:
# Mock subprocess.run to return non-zero
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
mock_result.stderr = "Test error"
with patch("subprocess.run", return_value=mock_result):
with pytest.raises(RuntimeError) as exc_info:
render_project(rpp_path=rpp_path, output_wav=wav_path)
assert "1" in str(exc_info.value) or "failed" in str(exc_info.value).lower()
finally:
Path(rpp_path).unlink(missing_ok=True)
Path(wav_path).unlink(missing_ok=True)
def test_render_project_raises_runtime_error_with_error_message(self):
"""RuntimeError output includes stdout/stderr from REAPER failure."""
from src.reaper_builder.render import DEFAULT_REAPER_EXE
if not DEFAULT_REAPER_EXE.exists():
pytest.skip(f"REAPER not installed at {DEFAULT_REAPER_EXE}")
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".rpp", delete=False, encoding="utf-8") as f:
rpp_path = f.name
f.write('<REAPER_PROJECT 0.1 "6.0" 0\n>\n')
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
wav_path = f.name
try:
mock_result = MagicMock()
mock_result.returncode = 2
mock_result.stdout = "standard output text"
mock_result.stderr = "error output text"
with patch("subprocess.run", return_value=mock_result):
with pytest.raises(RuntimeError) as exc_info:
render_project(rpp_path=rpp_path, output_wav=wav_path)
# Error message should include the exit code or stderr
err_str = str(exc_info.value)
assert "2" in err_str or "error output text" in err_str
finally:
Path(rpp_path).unlink(missing_ok=True)
Path(wav_path).unlink(missing_ok=True)