9cd8292acc
Lint & Validate / lint (push) Has been cancelled
P2-R1: ALSA + JACK2 low-latency config (scripts, quirks, tuning) P2-R2: Carla integration (build scripts, 8ch rack config, NAM LV2 support) P2-R3: Plugin manager, categories, blacklist, NAM model support P3-R1: Mixer DSP engine (channel strip, routing matrix, bus mgr, automation) P4-R1: MIDI engine (learn mode, clock sync, HID discovery, mapping store) P4-R2: Network API (OSC server, FastAPI REST, WebSocket, auth, rate limiter) P5-R1: Touchscreen UI evaluation + main entry point docs: Audio stack, Carla integration, MIDI support, UI evaluation tests: Full test suite (292 passing)
174 lines
5.3 KiB
Python
174 lines
5.3 KiB
Python
"""Tests for mapping persistence."""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from src.midi.types import MIDIMapping, MIDIMessageType, ParameterType
|
|
from src.midi.mapping_store import (
|
|
mapping_to_dict,
|
|
mapping_from_dict,
|
|
save_mappings,
|
|
load_mappings,
|
|
list_sessions,
|
|
delete_session,
|
|
DEFAULT_MAPPINGS_DIR,
|
|
DEFAULT_SESSION_NAME,
|
|
)
|
|
|
|
|
|
class TestSerialisation:
|
|
def test_roundtrip_cc_mapping(self):
|
|
original = MIDIMapping(
|
|
msg_type=MIDIMessageType.CONTROL_CHANGE,
|
|
channel=0,
|
|
controller=7,
|
|
param_type=ParameterType.VOLUME,
|
|
param_channel=0,
|
|
invert=True,
|
|
curve="logarithmic",
|
|
label="CH0 Vol",
|
|
)
|
|
d = mapping_to_dict(original)
|
|
restored = mapping_from_dict(d)
|
|
|
|
assert restored.msg_type == original.msg_type
|
|
assert restored.channel == original.channel
|
|
assert restored.controller == original.controller
|
|
assert restored.param_type == original.param_type
|
|
assert restored.param_channel == original.param_channel
|
|
assert restored.invert == original.invert
|
|
assert restored.curve == original.curve
|
|
assert restored.label == original.label
|
|
|
|
def test_roundtrip_nrpn_mapping(self):
|
|
original = MIDIMapping(
|
|
msg_type=MIDIMessageType.CONTROL_CHANGE,
|
|
channel=1,
|
|
is_nrpn=True,
|
|
nrpn_number=258,
|
|
param_type=ParameterType.COMP_THRESHOLD,
|
|
param_channel=3,
|
|
)
|
|
d = mapping_to_dict(original)
|
|
restored = mapping_from_dict(d)
|
|
|
|
assert restored.is_nrpn
|
|
assert restored.nrpn_number == 258
|
|
|
|
def test_roundtrip_with_device(self):
|
|
original = MIDIMapping(
|
|
controller=10,
|
|
source_device="X-Touch",
|
|
)
|
|
d = mapping_to_dict(original)
|
|
restored = mapping_from_dict(d)
|
|
|
|
assert restored.source_device == "X-Touch"
|
|
|
|
def test_roundtrip_disabled(self):
|
|
original = MIDIMapping(enabled=False)
|
|
d = mapping_to_dict(original)
|
|
restored = mapping_from_dict(d)
|
|
assert not restored.enabled
|
|
|
|
def test_dict_format_has_expected_keys(self):
|
|
m = MIDIMapping()
|
|
d = mapping_to_dict(m)
|
|
|
|
assert "midi" in d
|
|
assert "target" in d
|
|
assert "value" in d
|
|
assert "meta" in d
|
|
assert d["midi"]["type"] == "CONTROL_CHANGE"
|
|
assert d["target"]["parameter"] == "volume"
|
|
|
|
|
|
class TestFileIO:
|
|
@pytest.fixture(autouse=True)
|
|
def temp_dir(self, monkeypatch):
|
|
"""Redirect mappings to a temp directory."""
|
|
td = tempfile.mkdtemp(prefix="rpi-mixer-test-")
|
|
monkeypatch.setattr(
|
|
"src.midi.mapping_store.DEFAULT_MAPPINGS_DIR",
|
|
Path(td),
|
|
)
|
|
yield td
|
|
# Cleanup
|
|
import shutil
|
|
shutil.rmtree(td, ignore_errors=True)
|
|
|
|
def test_save_and_load(self):
|
|
mappings = [
|
|
MIDIMapping(
|
|
msg_type=MIDIMessageType.CONTROL_CHANGE,
|
|
channel=0,
|
|
controller=7,
|
|
param_type=ParameterType.VOLUME,
|
|
param_channel=0,
|
|
label="CH0 Vol",
|
|
),
|
|
MIDIMapping(
|
|
msg_type=MIDIMessageType.CONTROL_CHANGE,
|
|
channel=0,
|
|
controller=10,
|
|
param_type=ParameterType.PAN,
|
|
param_channel=0,
|
|
label="CH0 Pan",
|
|
),
|
|
]
|
|
|
|
path = save_mappings(mappings, DEFAULT_SESSION_NAME)
|
|
assert path.exists()
|
|
|
|
loaded = load_mappings(DEFAULT_SESSION_NAME)
|
|
assert len(loaded) == 2
|
|
assert loaded[0].controller == 7
|
|
assert loaded[1].controller == 10
|
|
assert loaded[0].label == "CH0 Vol"
|
|
assert loaded[1].label == "CH0 Pan"
|
|
|
|
def test_missing_session_returns_empty(self):
|
|
loaded = load_mappings("nonexistent-session")
|
|
assert loaded == []
|
|
|
|
def test_list_sessions(self, temp_dir):
|
|
save_mappings([MIDIMapping()], "session-a")
|
|
save_mappings([MIDIMapping()], "session-b")
|
|
|
|
sessions = list_sessions()
|
|
assert "session-a" in sessions
|
|
assert "session-b" in sessions
|
|
|
|
def test_delete_session(self):
|
|
save_mappings([MIDIMapping()], "to-delete")
|
|
assert delete_session("to-delete")
|
|
assert load_mappings("to-delete") == []
|
|
|
|
def test_delete_nonexistent_session(self):
|
|
assert not delete_session("never-existed")
|
|
|
|
def test_save_empty_mappings(self):
|
|
path = save_mappings([], DEFAULT_SESSION_NAME)
|
|
assert path.exists()
|
|
|
|
loaded = load_mappings(DEFAULT_SESSION_NAME)
|
|
assert loaded == []
|
|
|
|
def test_json_is_valid(self):
|
|
"""Saved JSON should be parseable by standard json module."""
|
|
mappings = [
|
|
MIDIMapping(controller=7, param_type=ParameterType.VOLUME, param_channel=0),
|
|
MIDIMapping(controller=10, param_type=ParameterType.PAN, param_channel=1, invert=True),
|
|
]
|
|
path = save_mappings(mappings, DEFAULT_SESSION_NAME)
|
|
|
|
with open(path) as fh:
|
|
doc = json.load(fh)
|
|
|
|
assert doc["version"] == 1
|
|
assert len(doc["mappings"]) == 2
|
|
assert doc["mappings"][0]["target"]["parameter"] == "volume"
|
|
assert doc["mappings"][1]["value"]["invert"] is True
|