Spaces:
Running
Running
| """Tests for midi.py — MIDI event utilities.""" | |
| import io | |
| import mido | |
| import pytest | |
| from midi import events_to_midbytes, validate_event | |
| # --------------------------------------------------------------------------- | |
| # validate_event | |
| # --------------------------------------------------------------------------- | |
| class TestValidateEvent: | |
| def test_valid_event(self): | |
| assert validate_event( | |
| {"type": "note_on", "note": 60, "velocity": 100, "time": 0.0} | |
| ) | |
| def test_missing_type(self): | |
| assert not validate_event({"note": 60, "velocity": 100, "time": 0.0}) | |
| def test_missing_note(self): | |
| assert not validate_event({"type": "note_on", "velocity": 100, "time": 0.0}) | |
| def test_missing_velocity(self): | |
| assert not validate_event({"type": "note_on", "note": 60, "time": 0.0}) | |
| def test_missing_time(self): | |
| assert not validate_event({"type": "note_on", "note": 60, "velocity": 100}) | |
| def test_empty_dict(self): | |
| assert not validate_event({}) | |
| # --------------------------------------------------------------------------- | |
| # events_to_midbytes | |
| # --------------------------------------------------------------------------- | |
| class TestEventsToMidbytes: | |
| def test_returns_valid_midi_bytes(self, single_note_events): | |
| mid_bytes = events_to_midbytes(single_note_events) | |
| assert isinstance(mid_bytes, bytes) | |
| assert len(mid_bytes) > 0 | |
| # Should start with MThd header. | |
| assert mid_bytes[:4] == b"MThd" | |
| def test_round_trip_note_count(self, c_major_chord_events): | |
| """The exported MIDI file should contain the same number of note messages.""" | |
| mid_bytes = events_to_midbytes(c_major_chord_events) | |
| mid = mido.MidiFile(file=io.BytesIO(mid_bytes)) | |
| messages = [msg for msg in mid.tracks[0] if msg.type in ("note_on", "note_off")] | |
| assert len(messages) == len(c_major_chord_events) | |
| def test_preserves_note_values(self, melody_events): | |
| mid_bytes = events_to_midbytes(melody_events) | |
| mid = mido.MidiFile(file=io.BytesIO(mid_bytes)) | |
| note_ons = [msg.note for msg in mid.tracks[0] if msg.type == "note_on"] | |
| expected = [e["note"] for e in melody_events if e["type"] == "note_on"] | |
| assert note_ons == expected | |
| def test_empty_events(self): | |
| mid_bytes = events_to_midbytes([]) | |
| assert isinstance(mid_bytes, bytes) | |
| # Should still be a valid MIDI file (header + empty track). | |
| assert mid_bytes[:4] == b"MThd" | |
| def test_custom_tempo(self, single_note_events): | |
| mid_bytes = events_to_midbytes(single_note_events, tempo_bpm=60) | |
| mid = mido.MidiFile(file=io.BytesIO(mid_bytes)) | |
| tempo_msgs = [msg for msg in mid.tracks[0] if msg.type == "set_tempo"] | |
| assert len(tempo_msgs) == 1 | |
| assert mido.tempo2bpm(tempo_msgs[0].tempo) == pytest.approx(60, abs=0.01) | |
| def test_skips_malformed_events(self): | |
| events = [ | |
| {"type": "note_on", "note": 60, "velocity": 100, "time": 0.0}, | |
| {"bad": "event"}, # no type/note/time | |
| {"type": "note_off", "note": 60, "velocity": 0, "time": 0.5}, | |
| ] | |
| mid_bytes = events_to_midbytes(events) | |
| mid = mido.MidiFile(file=io.BytesIO(mid_bytes)) | |
| messages = [msg for msg in mid.tracks[0] if msg.type in ("note_on", "note_off")] | |
| assert len(messages) == 2 | |