"""Tests for chord_file_to_midi() in src/midi_export.py. Fixture: tests/fixtures/midi_export_test.chord 4 bars, 4/4 sub=4, C major, no function tag. Chords: C Am7 F G7 (each held 4 positions = one whole bar). Expected MIDI structure: instruments[0] "Chords": 3+4+3+4 = 14 notes, pitches in octave 4–5 [60, 83] instruments[1] "Bass": 4 notes (one per chord event), pitches in octave 2 [36, 47] """ from pathlib import Path import pretty_midi import pytest from src.midi_export import chord_file_to_midi FIXTURES = Path(__file__).parent / "fixtures" TEST_CHORD = FIXTURES / "midi_export_test.chord" # --------------------------------------------------------------------------- # Shared fixture — export once, parse back, share across test methods # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def exported(tmp_path_factory: pytest.TempPathFactory) -> pretty_midi.PrettyMIDI: out = tmp_path_factory.mktemp("midi") / "out.mid" chord_file_to_midi(TEST_CHORD, out) return pretty_midi.PrettyMIDI(str(out)) # --------------------------------------------------------------------------- # Basic output # --------------------------------------------------------------------------- class TestOutputFile: def test_file_is_created(self, tmp_path): out = tmp_path / "out.mid" chord_file_to_midi(TEST_CHORD, out) assert out.exists() and out.stat().st_size > 0 def test_midi_is_parseable(self, tmp_path): out = tmp_path / "out.mid" chord_file_to_midi(TEST_CHORD, out) pm = pretty_midi.PrettyMIDI(str(out)) assert pm is not None # --------------------------------------------------------------------------- # Instrument structure # --------------------------------------------------------------------------- class TestInstruments: def test_two_instruments(self, exported): assert len(exported.instruments) == 2 def test_first_instrument_is_chords(self, exported): assert exported.instruments[0].name == "Chords" def test_second_instrument_is_bass(self, exported): assert exported.instruments[1].name == "Bass" # --------------------------------------------------------------------------- # Note counts # --------------------------------------------------------------------------- class TestNoteCounts: def test_chord_track_note_count(self, exported): # C(3) + Am7(4) + F(3) + G7(4) = 14 assert len(exported.instruments[0].notes) == 14 def test_bass_track_note_count(self, exported): # one bass note per chord event = 4 assert len(exported.instruments[1].notes) == 4 # --------------------------------------------------------------------------- # Octave placement # --------------------------------------------------------------------------- class TestOctaves: def test_chord_notes_in_middle_octave(self, exported): # Root anchored at C4=60; triads/7ths add at most 11 st → max B5=83. for note in exported.instruments[0].notes: assert 60 <= note.pitch <= 83, f"chord pitch {note.pitch} outside [60, 83]" def test_bass_notes_in_low_octave(self, exported): # Bass anchored at C2=36; highest pitch class B2=47. for note in exported.instruments[1].notes: assert 36 <= note.pitch <= 47, f"bass pitch {note.pitch} outside [36, 47]" # --------------------------------------------------------------------------- # Tempo # --------------------------------------------------------------------------- class TestTempo: def test_higher_tempo_shortens_duration(self, tmp_path): slow = tmp_path / "slow.mid" fast = tmp_path / "fast.mid" chord_file_to_midi(TEST_CHORD, slow, tempo=90) chord_file_to_midi(TEST_CHORD, fast, tempo=180) assert ( pretty_midi.PrettyMIDI(str(slow)).get_end_time() > pretty_midi.PrettyMIDI(str(fast)).get_end_time() )