cgcardona / muse public
test_muse_attributes.py python
206 lines 8.0 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for maestro/services/muse_attributes.py.
2
3 Covers:
4 - parse_museattributes_file: valid rules, comments, blank lines, malformed lines,
5 unknown strategies.
6 - resolve_strategy: exact track+dimension match, fnmatch wildcard patterns,
7 fallback to MergeStrategy.AUTO when no rule matches.
8 - load_attributes: returns empty list when file not found; reads and parses
9 when the file exists.
10 """
11 from __future__ import annotations
12
13 import pathlib
14
15 import pytest
16
17 from maestro.services.muse_attributes import (
18 MergeStrategy,
19 MuseAttribute,
20 load_attributes,
21 parse_museattributes_file,
22 resolve_strategy,
23 )
24
25
26 # ---------------------------------------------------------------------------
27 # parse_museattributes_file
28 # ---------------------------------------------------------------------------
29
30
31 class TestParseMuseattributesFile:
32 def test_parses_basic_rule(self) -> None:
33 content = "drums/* * ours\n"
34 rules = parse_museattributes_file(content)
35 assert len(rules) == 1
36 assert rules[0].track_pattern == "drums/*"
37 assert rules[0].dimension == "*"
38 assert rules[0].strategy == MergeStrategy.OURS
39
40 def test_parses_multiple_rules(self) -> None:
41 content = (
42 "drums/* * ours\n"
43 "bass/* harmonic theirs\n"
44 "* * auto\n"
45 )
46 rules = parse_museattributes_file(content)
47 assert len(rules) == 3
48 assert rules[0].strategy == MergeStrategy.OURS
49 assert rules[1].strategy == MergeStrategy.THEIRS
50 assert rules[2].strategy == MergeStrategy.AUTO
51
52 def test_ignores_blank_lines(self) -> None:
53 content = "\n\ndrum/* * ours\n\n"
54 rules = parse_museattributes_file(content)
55 assert len(rules) == 1
56
57 def test_ignores_comment_lines(self) -> None:
58 content = "# This is a comment\ndrum/* * ours\n"
59 rules = parse_museattributes_file(content)
60 assert len(rules) == 1
61
62 def test_skips_malformed_lines(self) -> None:
63 content = "bad-line-only-two-tokens ours\n"
64 rules = parse_museattributes_file(content)
65 assert len(rules) == 0
66
67 def test_skips_unknown_strategy(self) -> None:
68 content = "drums/* * unknown_strategy\n"
69 rules = parse_museattributes_file(content)
70 assert len(rules) == 0
71
72 def test_all_valid_strategies(self) -> None:
73 content = (
74 "a * ours\n"
75 "b * theirs\n"
76 "c * union\n"
77 "d * auto\n"
78 "e * manual\n"
79 )
80 rules = parse_museattributes_file(content)
81 strategies = [r.strategy for r in rules]
82 assert MergeStrategy.OURS in strategies
83 assert MergeStrategy.THEIRS in strategies
84 assert MergeStrategy.UNION in strategies
85 assert MergeStrategy.AUTO in strategies
86 assert MergeStrategy.MANUAL in strategies
87
88 def test_strategy_case_insensitive(self) -> None:
89 content = "drums/* * OURS\n"
90 rules = parse_museattributes_file(content)
91 assert len(rules) == 1
92 assert rules[0].strategy == MergeStrategy.OURS
93
94 def test_empty_content_returns_empty_list(self) -> None:
95 assert parse_museattributes_file("") == []
96
97 def test_only_comments_returns_empty_list(self) -> None:
98 content = "# only comments\n# nothing else\n"
99 assert parse_museattributes_file(content) == []
100
101
102 # ---------------------------------------------------------------------------
103 # resolve_strategy
104 # ---------------------------------------------------------------------------
105
106
107 class TestResolveStrategy:
108 def _make_rule(
109 self,
110 track_pattern: str,
111 dimension: str,
112 strategy: MergeStrategy,
113 ) -> MuseAttribute:
114 return MuseAttribute(
115 track_pattern=track_pattern,
116 dimension=dimension,
117 strategy=strategy,
118 )
119
120 def test_exact_match_returns_configured_strategy(self) -> None:
121 attrs = [self._make_rule("drums/kick", "rhythmic", MergeStrategy.OURS)]
122 result = resolve_strategy(attrs, "drums/kick", "rhythmic")
123 assert result == MergeStrategy.OURS
124
125 def test_fnmatch_wildcard_track_matches(self) -> None:
126 attrs = [self._make_rule("drums/*", "*", MergeStrategy.OURS)]
127 assert resolve_strategy(attrs, "drums/kick", "harmonic") == MergeStrategy.OURS
128 assert resolve_strategy(attrs, "drums/snare", "melodic") == MergeStrategy.OURS
129
130 def test_star_track_matches_any(self) -> None:
131 attrs = [self._make_rule("*", "*", MergeStrategy.AUTO)]
132 assert resolve_strategy(attrs, "bass/electric", "harmonic") == MergeStrategy.AUTO
133
134 def test_first_match_wins(self) -> None:
135 attrs = [
136 self._make_rule("drums/*", "*", MergeStrategy.OURS),
137 self._make_rule("*", "*", MergeStrategy.AUTO),
138 ]
139 # drums/hi-hat should match the first rule
140 assert resolve_strategy(attrs, "drums/hi-hat", "rhythmic") == MergeStrategy.OURS
141 # keys/piano should fall through to the second rule
142 assert resolve_strategy(attrs, "keys/piano", "harmonic") == MergeStrategy.AUTO
143
144 def test_no_match_returns_auto(self) -> None:
145 attrs = [self._make_rule("drums/*", "rhythmic", MergeStrategy.OURS)]
146 # Different track, no match
147 result = resolve_strategy(attrs, "bass/electric", "harmonic")
148 assert result == MergeStrategy.AUTO
149
150 def test_empty_attributes_returns_auto(self) -> None:
151 result = resolve_strategy([], "drums/kick", "rhythmic")
152 assert result == MergeStrategy.AUTO
153
154 def test_dimension_wildcard_matches_all_dimensions(self) -> None:
155 attrs = [self._make_rule("bass/*", "*", MergeStrategy.THEIRS)]
156 for dim in ("harmonic", "rhythmic", "melodic", "structural", "dynamic"):
157 assert resolve_strategy(attrs, "bass/electric", dim) == MergeStrategy.THEIRS
158
159 def test_specific_dimension_does_not_match_other(self) -> None:
160 attrs = [self._make_rule("bass/*", "harmonic", MergeStrategy.THEIRS)]
161 assert resolve_strategy(attrs, "bass/electric", "harmonic") == MergeStrategy.THEIRS
162 assert resolve_strategy(attrs, "bass/electric", "rhythmic") == MergeStrategy.AUTO
163
164 def test_theirs_strategy_resolved(self) -> None:
165 attrs = [self._make_rule("keys/*", "harmonic", MergeStrategy.THEIRS)]
166 assert resolve_strategy(attrs, "keys/piano", "harmonic") == MergeStrategy.THEIRS
167
168 def test_union_strategy_resolved(self) -> None:
169 attrs = [self._make_rule("*", "structural", MergeStrategy.UNION)]
170 assert resolve_strategy(attrs, "any_track", "structural") == MergeStrategy.UNION
171
172 def test_manual_strategy_resolved(self) -> None:
173 attrs = [self._make_rule("*", "*", MergeStrategy.MANUAL)]
174 assert resolve_strategy(attrs, "vocals/lead", "melodic") == MergeStrategy.MANUAL
175
176
177 # ---------------------------------------------------------------------------
178 # load_attributes
179 # ---------------------------------------------------------------------------
180
181
182 class TestLoadAttributes:
183 def test_returns_empty_list_when_file_not_found(self, tmp_path: pathlib.Path) -> None:
184 result = load_attributes(tmp_path)
185 assert result == []
186
187 def test_reads_and_parses_museattributes_file(self, tmp_path: pathlib.Path) -> None:
188 attr_file = tmp_path / ".museattributes"
189 attr_file.write_text("drums/* * ours\nbass/* harmonic theirs\n")
190 result = load_attributes(tmp_path)
191 assert len(result) == 2
192 assert result[0].track_pattern == "drums/*"
193 assert result[0].strategy == MergeStrategy.OURS
194 assert result[1].strategy == MergeStrategy.THEIRS
195
196 def test_returns_empty_list_for_empty_file(self, tmp_path: pathlib.Path) -> None:
197 attr_file = tmp_path / ".museattributes"
198 attr_file.write_text("")
199 result = load_attributes(tmp_path)
200 assert result == []
201
202 def test_returns_empty_list_for_comments_only_file(self, tmp_path: pathlib.Path) -> None:
203 attr_file = tmp_path / ".museattributes"
204 attr_file.write_text("# only comments here\n")
205 result = load_attributes(tmp_path)
206 assert result == []