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)
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
"""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)
|