cgcardona / muse public
test_domain_schema.py python
261 lines 10.3 KB
e6786943 feat: upgrade to Python 3.14, drop from __future__ import annotations Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """Tests for domain schema declaration and plugin registry lookup.
2
3 Verifies that:
4 - ``MidiPlugin.schema()`` returns a fully-typed ``DomainSchema``.
5 - All 21 MIDI dimensions are declared with the correct schema types.
6 - Independence flags match the semantic MIDI merge model.
7 - The schema is JSON round-trippable (all values are JSON-serialisable).
8 - ``schema_for()`` in the plugin registry performs the correct lookup.
9 - The protocol assertion still holds after adding ``schema()``.
10 """
11
12 import json
13
14 import pytest
15
16 from muse.core.schema import DomainSchema
17 from muse.domain import MuseDomainPlugin
18 from muse.plugins.midi.plugin import MidiPlugin
19 from muse.plugins.registry import registered_domains, schema_for
20
21 # ---------------------------------------------------------------------------
22 # Expected dimension layout for the 21-dimension MIDI schema
23 # ---------------------------------------------------------------------------
24
25 # (name, independent_merge, schema_kind)
26 _EXPECTED_DIMS: list[tuple[str, bool, str]] = [
27 # Expressive note content
28 ("notes", True, "sequence"),
29 ("pitch_bend", True, "tensor"),
30 ("channel_pressure", True, "tensor"),
31 ("poly_pressure", True, "tensor"),
32 # Named CC controllers
33 ("cc_modulation", True, "tensor"),
34 ("cc_volume", True, "tensor"),
35 ("cc_pan", True, "tensor"),
36 ("cc_expression", True, "tensor"),
37 ("cc_sustain", True, "tensor"),
38 ("cc_portamento", True, "tensor"),
39 ("cc_sostenuto", True, "tensor"),
40 ("cc_soft_pedal", True, "tensor"),
41 ("cc_reverb", True, "tensor"),
42 ("cc_chorus", True, "tensor"),
43 ("cc_other", True, "tensor"),
44 # Patch selection
45 ("program_change", True, "sequence"),
46 # Non-independent timeline metadata
47 ("tempo_map", False, "sequence"),
48 ("time_signatures", False, "sequence"),
49 # Tonal / annotation metadata
50 ("key_signatures", True, "sequence"),
51 ("markers", True, "sequence"),
52 # Track structure (non-independent)
53 ("track_structure", False, "tree"),
54 ]
55
56 _NON_INDEPENDENT = frozenset(
57 name for name, independent, _ in _EXPECTED_DIMS if not independent
58 )
59
60
61 # ---------------------------------------------------------------------------
62 # Fixtures
63 # ---------------------------------------------------------------------------
64
65
66 @pytest.fixture()
67 def midi_plugin() -> MidiPlugin:
68 return MidiPlugin()
69
70
71 @pytest.fixture()
72 def midi_schema(midi_plugin: MidiPlugin) -> DomainSchema:
73 return midi_plugin.schema()
74
75
76 # ===========================================================================
77 # MidiPlugin.schema() — top-level structure
78 # ===========================================================================
79
80
81 class TestMidiPluginSchema:
82 def test_schema_returns_dict(self, midi_schema: DomainSchema) -> None:
83 assert isinstance(midi_schema, dict)
84
85 def test_domain_is_midi(self, midi_schema: DomainSchema) -> None:
86 assert midi_schema["domain"] == "midi"
87
88 def test_schema_version_is_1(self, midi_schema: DomainSchema) -> None:
89 assert midi_schema["schema_version"] == 1
90
91 def test_merge_mode_is_three_way(self, midi_schema: DomainSchema) -> None:
92 assert midi_schema["merge_mode"] == "three_way"
93
94 def test_description_is_non_empty(self, midi_schema: DomainSchema) -> None:
95 assert isinstance(midi_schema["description"], str)
96 assert len(midi_schema["description"]) > 0
97
98 def test_top_level_is_set_schema(self, midi_schema: DomainSchema) -> None:
99 top = midi_schema["top_level"]
100 assert top["kind"] == "set"
101
102 def test_top_level_element_type(self, midi_schema: DomainSchema) -> None:
103 top = midi_schema["top_level"]
104 assert top["kind"] == "set"
105 assert top["element_type"] == "audio_file"
106 assert top["identity"] == "by_content"
107
108
109 # ===========================================================================
110 # 21-dimension layout
111 # ===========================================================================
112
113
114 class TestMidiDimensions:
115 def test_exactly_21_dimensions(self, midi_schema: DomainSchema) -> None:
116 assert len(midi_schema["dimensions"]) == 21
117
118 def test_all_expected_dimension_names_present(self, midi_schema: DomainSchema) -> None:
119 names = {d["name"] for d in midi_schema["dimensions"]}
120 expected = {name for name, _, _ in _EXPECTED_DIMS}
121 assert names == expected
122
123 def test_dimension_order_matches_spec(self, midi_schema: DomainSchema) -> None:
124 names = [d["name"] for d in midi_schema["dimensions"]]
125 expected = [name for name, _, _ in _EXPECTED_DIMS]
126 assert names == expected
127
128 @pytest.mark.parametrize("name,independent,kind", _EXPECTED_DIMS)
129 def test_dimension_independence(
130 self, midi_schema: DomainSchema, name: str, independent: bool, kind: str
131 ) -> None:
132 dim = next(d for d in midi_schema["dimensions"] if d["name"] == name)
133 assert dim["independent_merge"] is independent, (
134 f"Dimension '{name}': expected independent_merge={independent}"
135 )
136
137 @pytest.mark.parametrize("name,independent,kind", _EXPECTED_DIMS)
138 def test_dimension_schema_kind(
139 self, midi_schema: DomainSchema, name: str, independent: bool, kind: str
140 ) -> None:
141 dim = next(d for d in midi_schema["dimensions"] if d["name"] == name)
142 assert dim["schema"]["kind"] == kind, (
143 f"Dimension '{name}': expected schema kind '{kind}', got '{dim['schema']['kind']}'"
144 )
145
146 def test_all_dimensions_have_description(self, midi_schema: DomainSchema) -> None:
147 for dim in midi_schema["dimensions"]:
148 assert isinstance(dim.get("description"), str), (
149 f"Dimension '{dim['name']}' missing description"
150 )
151 assert len(dim["description"]) > 0
152
153 def test_non_independent_set(self, midi_schema: DomainSchema) -> None:
154 non_indep = {
155 d["name"] for d in midi_schema["dimensions"] if not d["independent_merge"]
156 }
157 assert non_indep == _NON_INDEPENDENT
158
159 def test_notes_dimension_sequence_fields(self, midi_schema: DomainSchema) -> None:
160 notes = next(d for d in midi_schema["dimensions"] if d["name"] == "notes")
161 schema = notes["schema"]
162 assert schema["kind"] == "sequence"
163 assert schema["element_type"] == "note_event"
164 assert schema["diff_algorithm"] == "lcs"
165
166 def test_cc_dimensions_are_tensor_float32(self, midi_schema: DomainSchema) -> None:
167 cc_names = {name for name, _, kind in _EXPECTED_DIMS if kind == "tensor"}
168 for dim in midi_schema["dimensions"]:
169 if dim["name"] in cc_names:
170 s = dim["schema"]
171 assert s["kind"] == "tensor"
172 assert s["dtype"] == "float32"
173 assert s["diff_mode"] == "sparse"
174
175 def test_track_structure_is_tree(self, midi_schema: DomainSchema) -> None:
176 ts = next(d for d in midi_schema["dimensions"] if d["name"] == "track_structure")
177 schema = ts["schema"]
178 assert schema["kind"] == "tree"
179 assert schema["node_type"] == "track_node"
180 assert schema["diff_algorithm"] == "zhang_shasha"
181
182
183 # ===========================================================================
184 # JSON round-trip
185 # ===========================================================================
186
187
188 class TestSchemaJsonRoundtrip:
189 def test_schema_is_json_serialisable(self, midi_schema: DomainSchema) -> None:
190 serialised = json.dumps(midi_schema)
191 restored = json.loads(serialised)
192 assert restored["domain"] == midi_schema["domain"]
193 assert restored["schema_version"] == midi_schema["schema_version"]
194 assert len(restored["dimensions"]) == len(midi_schema["dimensions"])
195 assert restored["top_level"]["kind"] == midi_schema["top_level"]["kind"]
196
197 def test_all_dimension_schemas_survive_roundtrip(self, midi_schema: DomainSchema) -> None:
198 serialised = json.dumps(midi_schema)
199 restored = json.loads(serialised)
200 original_kinds = {d["name"]: d["schema"]["kind"] for d in midi_schema["dimensions"]}
201 restored_kinds = {d["name"]: d["schema"]["kind"] for d in restored["dimensions"]}
202 assert original_kinds == restored_kinds
203
204
205 # ===========================================================================
206 # Plugin registry schema lookup
207 # ===========================================================================
208
209
210 class TestPluginRegistrySchemaLookup:
211 def test_schema_for_midi_returns_domain_schema(self) -> None:
212 result = schema_for("midi")
213 assert result is not None
214 assert result["domain"] == "midi"
215
216 def test_schema_for_unknown_domain_returns_none(self) -> None:
217 result = schema_for("nonexistent_domain_xyz")
218 assert result is None
219
220 def test_schema_for_matches_direct_plugin_call(self) -> None:
221 plugin = MidiPlugin()
222 direct = plugin.schema()
223 via_registry = schema_for("midi")
224 assert via_registry is not None
225 assert via_registry["domain"] == direct["domain"]
226 assert via_registry["schema_version"] == direct["schema_version"]
227 assert len(via_registry["dimensions"]) == len(direct["dimensions"])
228
229 def test_registered_domains_contains_midi(self) -> None:
230 assert "midi" in registered_domains()
231
232 def test_music_key_not_in_registry(self) -> None:
233 """Ensure the old 'music' key was fully removed."""
234 assert "music" not in registered_domains()
235
236 def test_schema_for_all_registered_domains_returns_non_none(self) -> None:
237 for domain in registered_domains():
238 result = schema_for(domain)
239 assert result is not None, f"schema_for({domain!r}) returned None"
240
241
242 # ===========================================================================
243 # Protocol conformance
244 # ===========================================================================
245
246
247 class TestProtocolConformance:
248 def test_midi_plugin_satisfies_protocol(self) -> None:
249 plugin = MidiPlugin()
250 assert isinstance(plugin, MuseDomainPlugin)
251
252 def test_schema_method_is_callable(self) -> None:
253 plugin = MidiPlugin()
254 assert callable(plugin.schema)
255
256 def test_schema_returns_domain_schema(self) -> None:
257 plugin = MidiPlugin()
258 result = plugin.schema()
259 assert isinstance(result, dict)
260 assert "domain" in result
261 assert "dimensions" in result