"""Integration tests for scripts/compose.py — end-to-end compose workflow.""" import sys from pathlib import Path from unittest.mock import patch, MagicMock sys.path.insert(0, str(Path(__file__).parents[1])) import pytest from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote from src.reaper_builder import RPPBuilder # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def compose_via_builder( genre: str = "reggaeton", bpm: float = 95.0, key: str = "Am", output_path: str = "output/track.rpp", ) -> SongDefinition: """Build a SongDefinition the same way scripts/compose.py does, return it. This lets us test the compose logic without hitting the filesystem for samples. """ import json from pathlib import Path as P _ROOT = P(__file__).parent.parent from src.composer.rhythm import get_notes from src.composer.melodic import bass_tresillo, lead_hook, chords_block, pad_sustain from src.composer.converters import rhythm_to_midi, melodic_to_midi genre_path = _ROOT / "knowledge" / "genres" / f"{genre.lower()}_2009.json" with open(genre_path, "r", encoding="utf-8") as f: genre_config = json.load(f) from scripts.compose import ( build_section_tracks, create_return_tracks, EFFECT_ALIASES, build_fx_chain, build_sampler_plugin, ) from src.selector import SampleSelector index_path = _ROOT / "data" / "sample_index.json" selector = SampleSelector(str(index_path)) tracks, sections = build_section_tracks(genre_config, selector, key, bpm) return_tracks = create_return_tracks() meta = SongMeta(bpm=bpm, key=key, title=f"{genre.capitalize()} Track") return SongDefinition(meta=meta, tracks=tracks + return_tracks, sections=sections) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestComposeRppOutput: """Tests for compose workflow producing valid .rpp output.""" def test_compose_produces_rpp_file(self, tmp_path): """main() with valid args produces a .rpp file at the output path.""" output = tmp_path / "track.rpp" # Mock SampleSelector.select_one so we don't need actual sample files with patch("scripts.compose.SampleSelector") as mock_selector_cls: mock_selector = MagicMock() mock_selector.select_one.return_value = None # audio_path stays None mock_selector_cls.return_value = mock_selector from scripts.compose import main import sys original_argv = sys.argv try: sys.argv = [ "compose", "--genre", "reggaeton", "--bpm", "95", "--key", "Am", "--output", str(output), ] main() finally: sys.argv = original_argv assert output.exists(), f"Expected {output} to exist" def test_compose_rpp_has_min_6_tracks(self, tmp_path): """The .rpp output contains at least 6 = 6, f"Expected >= 6 tracks, got {track_count}" def test_compose_has_fxchain(self, tmp_path): """The .rpp output contains FXCHAIN elements.""" output = tmp_path / "track.rpp" with patch("scripts.compose.SampleSelector") as mock_selector_cls: mock_selector = MagicMock() mock_selector.select_one.return_value = None mock_selector_cls.return_value = mock_selector from scripts.compose import main import sys original_argv = sys.argv try: sys.argv = [ "compose", "--genre", "reggaeton", "--bpm", "95", "--key", "Am", "--output", str(output), ] main() finally: sys.argv = original_argv content = output.read_text(encoding="utf-8") assert "FXCHAIN" in content, "Expected FXCHAIN in output" def test_compose_invalid_bpm_raises(self): """main() with bpm=0 raises ValueError.""" from scripts.compose import main import sys original_argv = sys.argv try: sys.argv = [ "compose", "--genre", "reggaeton", "--bpm", "0", "--key", "Am", "--output", "output/track.rpp", ] with pytest.raises(ValueError, match="bpm must be > 0"): main() finally: sys.argv = original_argv def test_compose_negative_bpm_raises(self): """main() with bpm=-10 raises ValueError.""" from scripts.compose import main import sys original_argv = sys.argv try: sys.argv = [ "compose", "--genre", "reggaeton", "--bpm", "-10", "--key", "Am", "--output", "output/track.rpp", ] with pytest.raises(ValueError, match="bpm must be > 0"): main() finally: sys.argv = original_argv class TestSectionBuilderIntegration: """Test section builder integration with SongDefinition.""" def test_build_section_tracks_returns_tracks_and_sections(self): """build_section_tracks returns (tracks, sections) tuple.""" import json from pathlib import Path as P _ROOT = P(__file__).parent.parent from scripts.compose import build_section_tracks from src.selector import SampleSelector genre_path = _ROOT / "knowledge" / "genres" / "reggaeton_2009.json" with open(genre_path, "r", encoding="utf-8") as f: genre_config = json.load(f) index_path = _ROOT / "data" / "sample_index.json" selector = SampleSelector(str(index_path)) # Pass explicit sections_data since JSON now uses templates format sections_data = genre_config.get("structure", {}).get("templates", {}).get("extracted_real_tracks", []) tracks, sections = build_section_tracks(genre_config, selector, "Am", 95.0, sections_data=sections_data) assert len(tracks) > 0, "Expected at least one track" assert len(sections) > 0, "Expected at least one section" # Sections should have names from the genre config valid_names = {"intro", "verse", "build", "pre_chorus", "chorus", "drop", "break", "gap", "bridge", "outro", "verse2", "chorus2", "chorus3"} for sec in sections: assert sec.name in valid_names, f"Unexpected section name: {sec.name}" def test_song_definition_has_sections_field(self): """SongDefinition has a sections field.""" from src.core.schema import SongDefinition, SongMeta, SectionDef meta = SongMeta(bpm=95, key="Am") song = SongDefinition( meta=meta, tracks=[], sections=[SectionDef(name="intro", bars=4, energy=0.3)], ) assert len(song.sections) == 1 assert song.sections[0].name == "intro"