Files
reaper-control/tests/test_core_schema.py
renato97 623af69483 fix: REAPER playback — D_VOL removed, Ozone filenames corrected, ReaEQ removed, MIDI quantized
- D_VOL: removed from _build_clip() — not valid at REAPER item level
- Ozone 12: fixed 21 PLUGIN_REGISTRY entries with correct .vst3 filenames
- ReaEQ: removed _calibrate_eq() — built-in plugin format incompatible
- MIDI: quantized all notes to 16th grid (120 ticks at 960 PPQ)

298/298 tests. 0 D_VOL, 0 ReaEQ, all notes on grid, Ozone filenames correct.
2026-05-04 00:55:08 -03:00

213 lines
8.2 KiB
Python

"""Tests for src/core/schema.py — SongDefinition, TrackDef, ClipDef, MidiNote, CCEvent."""
import dataclasses
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1]))
import pytest
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, CCEvent
class TestCCEvent:
"""Test CCEvent dataclass (Phase 1: schema)."""
def test_ccevent_round_trip(self):
"""CCEvent round-trips correctly through dataclass construction."""
evt = CCEvent(controller=11, time=0.5, value=50)
assert evt.controller == 11
assert evt.time == 0.5
assert evt.value == 50
def test_ccevent_defaults(self):
"""CCEvent has no default fields — all are required."""
from dataclasses import fields as dc_fields
field_names = [f.name for f in dc_fields(CCEvent)]
no_default = []
for f in dc_fields(CCEvent):
if f.default is f.default_factory is dataclasses.MISSING:
no_default.append(f.name)
assert "controller" in no_default
assert "time" in no_default
assert "value" in no_default
def test_clipdef_with_midi_cc(self):
"""ClipDef with midi_cc field."""
cc = [CCEvent(11, 0.0, 50), CCEvent(11, 0.18, 127)]
clip = ClipDef(position=0.0, length=16.0, name="Test", midi_cc=cc)
assert len(clip.midi_cc) == 2
assert clip.midi_cc[0].controller == 11
assert clip.midi_cc[0].time == 0.0
assert clip.midi_cc[0].value == 50
def test_clipdef_midi_cc_default_is_empty(self):
"""ClipDef.midi_cc defaults to empty list."""
clip = ClipDef(position=0.0, length=16.0, name="Test")
assert clip.midi_cc == []
def test_song_validate_empty_midi_cc(self):
"""Song.validate() passes with empty midi_cc (no regression)."""
meta = SongMeta(bpm=95, key="Am")
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
clip = ClipDef(position=0.0, length=16.0, name="Test", midi_notes=[note], midi_cc=[])
track = TrackDef(name="Test", clips=[clip])
song = SongDefinition(meta=meta, tracks=[track])
errors = song.validate()
assert errors == []
class TestSongDefinitionInstantiation:
"""Test SongDefinition instantiation with valid data."""
def test_song_definition_with_valid_data(self):
"""SongDefinition instantiates with meta and tracks."""
meta = SongMeta(bpm=95, key="Am", title="Test Song")
song = SongDefinition(meta=meta, tracks=[])
assert song.meta.bpm == 95
assert song.meta.key == "Am"
assert song.meta.title == "Test Song"
assert song.tracks == []
def test_song_definition_with_multiple_tracks(self):
"""SongDefinition accepts multiple TrackDef entries."""
meta = SongMeta(bpm=95, key="Am")
track1 = TrackDef(name="Kick", volume=0.85)
track2 = TrackDef(name="Bass", volume=0.80)
song = SongDefinition(meta=meta, tracks=[track1, track2])
assert len(song.tracks) == 2
assert song.tracks[0].name == "Kick"
assert song.tracks[1].name == "Bass"
class TestTrackDefWithAudioClip:
"""Test TrackDef with audio clip (sample_path)."""
def test_track_with_audio_clip(self):
"""TrackDef with audio clip has audio_path set."""
clip = ClipDef(
position=0.0,
length=16.0,
name="Kick Loop",
audio_path="C:/samples/kick.wav",
)
track = TrackDef(name="Drums", clips=[clip])
assert len(track.clips) == 1
assert track.clips[0].is_audio
assert track.clips[0].audio_path == "C:/samples/kick.wav"
assert not track.clips[0].is_midi
def test_track_with_multiple_audio_clips(self):
"""TrackDef can hold multiple audio clips."""
clip1 = ClipDef(position=0.0, length=16.0, audio_path="C:/samples/kick.wav")
clip2 = ClipDef(position=16.0, length=16.0, audio_path="C:/samples/kick2.wav")
track = TrackDef(name="Drums", clips=[clip1, clip2])
assert len(track.clips) == 2
assert track.clips[0].audio_path == "C:/samples/kick.wav"
assert track.clips[1].audio_path == "C:/samples/kick2.wav"
class TestClipDefWithMidiNotes:
"""Test ClipDef with MIDI notes."""
def test_clip_with_midi_notes(self):
"""ClipDef with midi_notes is identified as MIDI clip."""
note = MidiNote(pitch=36, start=0.0, duration=1.0, velocity=100)
clip = ClipDef(
position=0.0,
length=16.0,
name="Kick Pattern",
midi_notes=[note],
)
assert clip.is_midi
assert not clip.is_audio
assert len(clip.midi_notes) == 1
assert clip.midi_notes[0].pitch == 36
def test_clip_with_multiple_midi_notes(self):
"""ClipDef can hold multiple MidiNote entries."""
notes = [
MidiNote(pitch=36, start=0.0, duration=0.25, velocity=115),
MidiNote(pitch=36, start=1.5, duration=0.25, velocity=105),
MidiNote(pitch=38, start=2.0, duration=0.15, velocity=100),
]
clip = ClipDef(position=0.0, length=16.0, name="Drum Pattern", midi_notes=notes)
assert len(clip.midi_notes) == 3
assert clip.midi_notes[0].pitch == 36
assert clip.midi_notes[1].pitch == 36
assert clip.midi_notes[2].pitch == 38
class TestValidationNegativeBPM:
"""Test validation: negative BPM raises ValueError."""
def test_negative_bpm_raises_value_error(self):
"""SongDefinition.validate() returns error for negative BPM."""
meta = SongMeta(bpm=-10, key="Am")
song = SongDefinition(meta=meta, tracks=[])
errors = song.validate()
assert any("bpm" in e.lower() for e in errors)
def test_zero_bpm_raises_value_error(self):
"""SongDefinition.validate() returns error for zero BPM."""
meta = SongMeta(bpm=0, key="Am")
song = SongDefinition(meta=meta, tracks=[])
errors = song.validate()
assert any("bpm" in e.lower() for e in errors)
def test_valid_bpm_passes(self):
"""SongDefinition.validate() passes for BPM 20-999."""
meta = SongMeta(bpm=95, key="Am")
song = SongDefinition(meta=meta, tracks=[])
errors = song.validate()
assert not any("bpm" in e.lower() for e in errors)
class TestMidiNote:
"""Test MidiNote dataclass."""
def test_midi_note_defaults(self):
"""MidiNote has sensible defaults for velocity."""
note = MidiNote(pitch=60, start=0.0, duration=1.0)
assert note.pitch == 60
assert note.start == 0.0
assert note.duration == 1.0
assert note.velocity == 64 # default
def test_midi_note_explicit_velocity(self):
"""MidiNote accepts explicit velocity."""
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=127)
assert note.velocity == 127
def test_midi_note_velocity_clamping(self):
"""MidiNote does NOT clamp — accepts any int (caller's responsibility)."""
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=200)
assert note.velocity == 200
class TestClipDefVolMult:
"""Test ClipDef.vol_mult default and behavior."""
def test_vol_mult_default_is_one(self):
"""ClipDef.vol_mult defaults to 1.0."""
clip = ClipDef(position=0.0, length=16.0, name="Test")
assert clip.vol_mult == 1.0
def test_vol_mult_custom_value(self):
"""ClipDef.vol_mult accepts custom value."""
clip = ClipDef(position=0.0, length=16.0, name="Test", vol_mult=0.7)
assert clip.vol_mult == 0.7
def test_audio_clip_vol_mult_default_is_one(self):
"""Audio clip with default vol_mult=1.0 has no volume effect."""
clip = ClipDef(position=0.0, length=16.0, audio_path="test.wav", vol_mult=1.0)
assert clip.is_audio
assert clip.vol_mult == 1.0
def test_midi_clip_vol_mult_default_is_one(self):
"""MIDI clip with default vol_mult=1.0 has no velocity scaling."""
note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
clip = ClipDef(position=0.0, length=16.0, midi_notes=[note], vol_mult=1.0)
assert clip.is_midi
assert clip.vol_mult == 1.0