gabriel / muse public
test_music_query.py python
216 lines 7.2 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Tests for muse.plugins.midi._music_query — tokenizer, parser, evaluator."""
2
3 import datetime
4
5 import pytest
6
7 from muse.core.commits import CommitRecord
8 from muse.plugins.midi._midi_query import (
9 AndNode,
10 EqNode,
11 NotNode,
12 OrNode,
13 QueryContext,
14 evaluate_node,
15 parse_query,
16 )
17 from muse.plugins.midi._query import NoteInfo
18 from muse.plugins.midi.midi_diff import NoteKey
19
20
21 def _make_commit(
22 agent_id: str = "",
23 author: str = "human",
24 model_id: str = "",
25 toolchain_id: str = "",
26 ) -> CommitRecord:
27 return CommitRecord(
28 commit_id="deadbeef" * 8,
29 branch="main",
30 snapshot_id="snap123",
31 message="test",
32 author=author,
33 committed_at=datetime.datetime.now(datetime.timezone.utc),
34 agent_id=agent_id,
35 model_id=model_id,
36 toolchain_id=toolchain_id,
37 )
38
39
40 def _make_note(pitch: int = 60, velocity: int = 80, channel: int = 0) -> NoteInfo:
41 return NoteInfo.from_note_key(
42 NoteKey(
43 pitch=pitch,
44 velocity=velocity,
45 start_tick=0,
46 duration_ticks=480,
47 channel=channel,
48 ),
49 ticks_per_beat=480,
50 )
51
52
53 def _make_ctx(
54 notes: list[NoteInfo] | None = None,
55 bar: int = 1,
56 track: str = "piano.mid",
57 chord: str = "Cmaj",
58 commit: CommitRecord | None = None,
59 ) -> QueryContext:
60 return QueryContext(
61 commit=commit or _make_commit(),
62 track=track,
63 bar=bar,
64 notes=notes or [_make_note()],
65 chord=chord,
66 ticks_per_beat=480,
67 )
68
69
70 # ---------------------------------------------------------------------------
71 # Tokenizer / parser
72 # ---------------------------------------------------------------------------
73
74
75 class TestParser:
76 def test_simple_eq_parses(self) -> None:
77 node = parse_query("bar == 4")
78 assert isinstance(node, EqNode)
79 assert node.field == "bar"
80 assert node.op == "=="
81 assert node.value == 4
82
83 def test_and_produces_and_node(self) -> None:
84 node = parse_query("bar == 1 and note.pitch > 60")
85 assert isinstance(node, AndNode)
86
87 def test_or_produces_or_node(self) -> None:
88 node = parse_query("bar == 1 or bar == 2")
89 assert isinstance(node, OrNode)
90
91 def test_not_produces_not_node(self) -> None:
92 node = parse_query("not bar == 4")
93 assert isinstance(node, NotNode)
94
95 def test_parentheses_group_correctly(self) -> None:
96 node = parse_query("(bar == 1 or bar == 2) and note.pitch > 60")
97 assert isinstance(node, AndNode)
98 assert isinstance(node.left, OrNode)
99
100 def test_string_value_parses(self) -> None:
101 node = parse_query("note.pitch_class == 'C'")
102 assert isinstance(node, EqNode)
103 assert node.value == "C"
104
105 def test_double_quoted_string_parses(self) -> None:
106 node = parse_query('track == "piano.mid"')
107 assert isinstance(node, EqNode)
108 assert node.value == "piano.mid"
109
110 def test_float_value_parses(self) -> None:
111 node = parse_query("note.duration > 0.5")
112 assert isinstance(node, EqNode)
113 assert isinstance(node.value, float)
114
115 def test_invalid_query_raises_value_error(self) -> None:
116 with pytest.raises(ValueError):
117 parse_query("bar !!! 4")
118
119 def test_incomplete_query_raises_value_error(self) -> None:
120 with pytest.raises(ValueError):
121 parse_query("bar ==")
122
123
124 # ---------------------------------------------------------------------------
125 # Evaluator — field resolution and comparison
126 # ---------------------------------------------------------------------------
127
128
129 class TestEvaluator:
130 def test_bar_eq_match(self) -> None:
131 assert evaluate_node(parse_query("bar == 4"), _make_ctx(bar=4))
132
133 def test_bar_eq_no_match(self) -> None:
134 assert not evaluate_node(parse_query("bar == 4"), _make_ctx(bar=3))
135
136 def test_note_pitch_gt(self) -> None:
137 ctx = _make_ctx(notes=[_make_note(pitch=65)])
138 assert evaluate_node(parse_query("note.pitch > 60"), ctx)
139
140 def test_note_pitch_gt_false(self) -> None:
141 ctx = _make_ctx(notes=[_make_note(pitch=55)])
142 assert not evaluate_node(parse_query("note.pitch > 60"), ctx)
143
144 def test_note_velocity_lte(self) -> None:
145 ctx = _make_ctx(notes=[_make_note(velocity=80)])
146 assert evaluate_node(parse_query("note.velocity <= 80"), ctx)
147
148 def test_note_pitch_class_match(self) -> None:
149 # Middle C = pitch 60 = C
150 ctx = _make_ctx(notes=[_make_note(pitch=60)])
151 assert evaluate_node(parse_query("note.pitch_class == 'C'"), ctx)
152
153 def test_track_match(self) -> None:
154 ctx = _make_ctx(track="strings.mid")
155 assert evaluate_node(parse_query("track == 'strings.mid'"), ctx)
156
157 def test_chord_match(self) -> None:
158 ctx = _make_ctx(chord="Fmin")
159 assert evaluate_node(parse_query("harmony.chord == 'Fmin'"), ctx)
160
161 def test_author_match(self) -> None:
162 commit = _make_commit(author="alice")
163 ctx = _make_ctx(commit=commit)
164 assert evaluate_node(parse_query("author == 'alice'"), ctx)
165
166 def test_agent_id_match(self) -> None:
167 commit = _make_commit(agent_id="counterpoint-bot")
168 ctx = _make_ctx(commit=commit)
169 assert evaluate_node(parse_query("agent_id == 'counterpoint-bot'"), ctx)
170
171 def test_and_both_must_match(self) -> None:
172 ctx = _make_ctx(notes=[_make_note(pitch=65)], bar=4)
173 assert evaluate_node(parse_query("note.pitch > 60 and bar == 4"), ctx)
174 assert not evaluate_node(parse_query("note.pitch > 60 and bar == 5"), ctx)
175
176 def test_or_one_must_match(self) -> None:
177 ctx = _make_ctx(bar=2)
178 assert evaluate_node(parse_query("bar == 1 or bar == 2"), ctx)
179 assert not evaluate_node(parse_query("bar == 1 or bar == 3"), ctx)
180
181 def test_not_negates(self) -> None:
182 ctx = _make_ctx(bar=4)
183 assert not evaluate_node(parse_query("not bar == 4"), ctx)
184 assert evaluate_node(parse_query("not bar == 5"), ctx)
185
186 def test_multiple_notes_any_match(self) -> None:
187 # If any note in the bar matches, the predicate matches.
188 ctx = _make_ctx(notes=[_make_note(pitch=55), _make_note(pitch=65)])
189 assert evaluate_node(parse_query("note.pitch > 60"), ctx)
190
191 def test_unknown_field_returns_false(self) -> None:
192 ctx = _make_ctx()
193 assert not evaluate_node(parse_query("nonexistent == 'x'"), ctx)
194
195 def test_harmony_quality_min(self) -> None:
196 ctx = _make_ctx(chord="Amin")
197 assert evaluate_node(parse_query("harmony.quality == 'min'"), ctx)
198
199 def test_harmony_quality_dim7(self) -> None:
200 ctx = _make_ctx(chord="Bdim7")
201 assert evaluate_node(parse_query("harmony.quality == 'dim7'"), ctx)
202
203
204 # ---------------------------------------------------------------------------
205 # Note channel field
206 # ---------------------------------------------------------------------------
207
208
209 class TestNoteChannel:
210 def test_channel_eq(self) -> None:
211 ctx = _make_ctx(notes=[_make_note(channel=2)])
212 assert evaluate_node(parse_query("note.channel == 2"), ctx)
213
214 def test_channel_neq(self) -> None:
215 ctx = _make_ctx(notes=[_make_note(channel=3)])
216 assert not evaluate_node(parse_query("note.channel == 2"), ctx)
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago