|
14 | 14 |
|
15 | 15 | """Tests for dbcore query parsing helpers.""" |
16 | 16 |
|
17 | | -import unittest |
18 | | - |
19 | 17 | import pytest |
20 | 18 |
|
21 | 19 | from beets.dbcore import query, sort |
22 | 20 | from beets.dbcore.queryparse import ModelQuery, QueryTerm |
23 | 21 | from beets.test.fixtures import ModelFixture1, SortFixture |
24 | 22 |
|
25 | | - |
26 | | -class QueryParseTest(unittest.TestCase): |
27 | | - def pqp(self, part): |
28 | | - term = QueryTerm.make(part) |
29 | | - return term.field, term.pattern, term.get_query_cls(ModelFixture1) |
30 | | - |
31 | | - def test_one_basic_term(self): |
32 | | - q = "test" |
33 | | - r = (None, "test", query.SubstringQuery) |
34 | | - assert self.pqp(q) == r |
35 | | - |
36 | | - def test_one_keyed_term(self): |
37 | | - q = "test:val" |
38 | | - r = ("test", "val", query.SubstringQuery) |
39 | | - assert self.pqp(q) == r |
40 | | - |
41 | | - def test_colon_at_end(self): |
42 | | - q = "test:" |
43 | | - r = ("test", "", query.SubstringQuery) |
44 | | - assert self.pqp(q) == r |
45 | | - |
46 | | - def test_one_basic_regexp(self): |
47 | | - q = r":regexp" |
48 | | - r = (None, "regexp", query.RegexpQuery) |
49 | | - assert self.pqp(q) == r |
50 | | - |
51 | | - def test_keyed_regexp(self): |
52 | | - q = r"test::regexp" |
53 | | - r = ("test", "regexp", query.RegexpQuery) |
54 | | - assert self.pqp(q) == r |
55 | | - |
56 | | - def test_escaped_colon(self): |
57 | | - q = r"test\:val" |
58 | | - r = (None, "test:val", query.SubstringQuery) |
59 | | - assert self.pqp(q) == r |
60 | | - |
61 | | - def test_escaped_colon_in_regexp(self): |
62 | | - q = r":test\:regexp" |
63 | | - r = (None, "test:regexp", query.RegexpQuery) |
64 | | - assert self.pqp(q) == r |
65 | | - |
66 | | - def test_single_year(self): |
67 | | - q = "year:1999" |
68 | | - r = ("year", "1999", query.NumericQuery) |
69 | | - assert self.pqp(q) == r |
70 | | - |
71 | | - def test_multiple_years(self): |
72 | | - q = "year:1999..2010" |
73 | | - r = ("year", "1999..2010", query.NumericQuery) |
74 | | - assert self.pqp(q) == r |
75 | | - |
76 | | - def test_empty_query_part(self): |
77 | | - q = "" |
78 | | - r = (None, "", query.SubstringQuery) |
79 | | - assert self.pqp(q) == r |
80 | | - |
81 | | - def test_implicit_path(self): |
82 | | - q = "/tmp" |
83 | | - r = ("path", "/tmp", query.PathQuery) |
84 | | - assert self.pqp(q) == r |
85 | | - |
86 | | - |
87 | | -class QueryFromStringsTest(unittest.TestCase): |
88 | | - def qfs(self, strings): |
89 | | - return ModelFixture1.parse_query(strings).query |
90 | | - |
91 | | - def test_zero_parts(self): |
92 | | - q = self.qfs([]) |
93 | | - assert isinstance(q, query.TrueQuery) |
94 | | - |
95 | | - def test_two_parts(self): |
96 | | - q = self.qfs(["foo", "bar:baz"]) |
| 23 | +_p = pytest.param |
| 24 | + |
| 25 | + |
| 26 | +def _parse_query_parts(parts: list[str]): |
| 27 | + return ModelFixture1.parse_query(parts).query |
| 28 | + |
| 29 | + |
| 30 | +def _parse_sort_parts(parts: list[str]): |
| 31 | + return ModelFixture1.parse_query(parts).sort |
| 32 | + |
| 33 | + |
| 34 | +class TestQueryTermParsing: |
| 35 | + @pytest.mark.parametrize( |
| 36 | + "query_string,expected", |
| 37 | + [ |
| 38 | + ("test", (None, "test", query.SubstringQuery)), |
| 39 | + ("test:val", ("test", "val", query.SubstringQuery)), |
| 40 | + ("test:", ("test", "", query.SubstringQuery)), |
| 41 | + (r":regexp", (None, "regexp", query.RegexpQuery)), |
| 42 | + (r"test::regexp", ("test", "regexp", query.RegexpQuery)), |
| 43 | + (r"test\:val", (None, "test:val", query.SubstringQuery)), |
| 44 | + (r":test\:regexp", (None, "test:regexp", query.RegexpQuery)), |
| 45 | + ("year:1999", ("year", "1999", query.NumericQuery)), |
| 46 | + ("year:1999..2010", ("year", "1999..2010", query.NumericQuery)), |
| 47 | + ("", (None, "", query.SubstringQuery)), |
| 48 | + ("/tmp", ("path", "/tmp", query.PathQuery)), |
| 49 | + ], |
| 50 | + ) |
| 51 | + def test_query_term_parsing(self, query_string, expected): |
| 52 | + """Test that various query strings are parsed correctly.""" |
| 53 | + term = QueryTerm.make(query_string) |
| 54 | + result = term.field, term.pattern, term.get_query_cls(ModelFixture1) |
| 55 | + assert result == expected |
| 56 | + |
| 57 | + |
| 58 | +class TestQueryFromParts: |
| 59 | + @pytest.mark.parametrize( |
| 60 | + "query_parts,expected_type", |
| 61 | + [ |
| 62 | + _p([], query.TrueQuery, id="zero_parts"), |
| 63 | + _p([""], query.TrueQuery, id="empty_query_part"), |
| 64 | + _p(["field_one:2..3"], query.NumericQuery, id="fixed_type_query"), |
| 65 | + _p( |
| 66 | + ["some_float_field:2..3"], |
| 67 | + query.NumericQuery, |
| 68 | + id="flex_type_query", |
| 69 | + ), |
| 70 | + ], |
| 71 | + ) |
| 72 | + def test_query_from_parts_types(self, query_parts, expected_type): |
| 73 | + q = _parse_query_parts(query_parts) |
| 74 | + assert isinstance(q, expected_type) |
| 75 | + |
| 76 | + def test_query_from_two_parts_builds_and_query(self): |
| 77 | + q = _parse_query_parts(["foo", "bar:baz"]) |
97 | 78 | assert isinstance(q, query.AndQuery) |
98 | 79 | assert len(q.subqueries) == 2 |
99 | 80 | assert isinstance(q.subqueries[0], query.OrQuery) |
100 | 81 | assert isinstance(q.subqueries[1], query.SubstringQuery) |
101 | 82 |
|
102 | | - def test_parse_fixed_type_query(self): |
103 | | - q = self.qfs(["field_one:2..3"]) |
104 | | - assert isinstance(q, query.NumericQuery) |
105 | | - |
106 | | - def test_parse_flex_type_query(self): |
107 | | - q = self.qfs(["some_float_field:2..3"]) |
108 | | - assert isinstance(q, query.NumericQuery) |
109 | | - |
110 | | - def test_empty_query_part(self): |
111 | | - q = self.qfs([""]) |
112 | | - assert isinstance(q, query.TrueQuery) |
113 | 83 |
|
114 | | - |
115 | | -class SortFromStringsTest(unittest.TestCase): |
116 | | - def sfs(self, strings): |
117 | | - return ModelFixture1.parse_query(strings).sort |
118 | | - |
119 | | - def test_zero_parts(self): |
120 | | - s = self.sfs([]) |
| 84 | +class TestSortFromParts: |
| 85 | + def test_sort_from_zero_parts(self): |
| 86 | + s = _parse_sort_parts([]) |
121 | 87 | assert isinstance(s, sort.NullSort) |
122 | 88 | assert s == sort.NullSort() |
123 | 89 |
|
124 | | - def test_one_parts(self): |
125 | | - s = self.sfs(["field+"]) |
| 90 | + def test_sort_from_one_part(self): |
| 91 | + s = _parse_sort_parts(["field+"]) |
126 | 92 | assert isinstance(s, sort.Sort) |
127 | 93 |
|
128 | | - def test_two_parts(self): |
129 | | - s = self.sfs(["field+", "another_field-"]) |
| 94 | + def test_sort_from_two_parts(self): |
| 95 | + s = _parse_sort_parts(["field+", "another_field-"]) |
130 | 96 | assert isinstance(s, sort.MultipleSort) |
131 | 97 | assert len(s.sorts) == 2 |
132 | 98 |
|
133 | | - def test_fixed_field_sort(self): |
134 | | - s = self.sfs(["field_one+"]) |
135 | | - assert isinstance(s, sort.FixedFieldSort) |
136 | | - assert s == sort.FixedFieldSort("field_one") |
137 | | - |
138 | | - def test_flex_field_sort(self): |
139 | | - s = self.sfs(["flex_field+"]) |
140 | | - assert isinstance(s, sort.SlowFieldSort) |
141 | | - assert s == sort.SlowFieldSort("flex_field") |
142 | | - |
143 | | - def test_special_sort(self): |
144 | | - s = self.sfs(["some_sort+"]) |
145 | | - assert isinstance(s, SortFixture) |
146 | | - |
147 | | - |
148 | | -class ParseSortedQueryTest(unittest.TestCase): |
149 | | - def psq(self, parts): |
150 | | - return ModelFixture1.parse_query(parts.split()) |
151 | | - |
152 | | - def test_and_query(self): |
153 | | - q, s = self.psq("foo bar") |
154 | | - assert isinstance(q, query.AndQuery) |
155 | | - assert isinstance(s, sort.NullSort) |
156 | | - assert len(q.subqueries) == 2 |
157 | | - |
158 | | - def test_or_query(self): |
159 | | - q, s = self.psq("foo , bar") |
160 | | - assert isinstance(q, query.OrQuery) |
161 | | - assert isinstance(s, sort.NullSort) |
162 | | - assert len(q.subqueries) == 4 |
163 | | - |
164 | | - def test_no_space_before_comma_or_query(self): |
165 | | - q, s = self.psq("foo, bar") |
166 | | - assert isinstance(q, query.OrQuery) |
167 | | - assert isinstance(s, sort.NullSort) |
168 | | - assert len(q.subqueries) == 4 |
169 | | - |
170 | | - def test_no_spaces_or_query(self): |
171 | | - q, s = self.psq("foo,bar") |
172 | | - assert isinstance(q, query.OrQuery) |
173 | | - assert isinstance(s, sort.NullSort) |
174 | | - assert len(q.subqueries) == 2 |
175 | | - |
176 | | - def test_trailing_comma_or_query(self): |
177 | | - q, s = self.psq("foo , bar ,") |
178 | | - assert isinstance(q, query.OrQuery) |
179 | | - assert isinstance(s, sort.NullSort) |
180 | | - assert len(q.subqueries) == 5 |
181 | | - |
182 | | - def test_leading_comma_or_query(self): |
183 | | - q, s = self.psq(", foo , bar") |
184 | | - assert isinstance(q, query.OrQuery) |
185 | | - assert isinstance(s, sort.NullSort) |
186 | | - assert len(q.subqueries) == 5 |
187 | | - |
188 | | - def test_only_direction(self): |
189 | | - q, s = self.psq("-") |
190 | | - assert isinstance(q, query.NotQuery) |
191 | | - assert isinstance(s, sort.NullSort) |
192 | | - |
193 | | - |
194 | | -class ParseQueryTest(unittest.TestCase): |
| 99 | + @pytest.mark.parametrize( |
| 100 | + "sort_parts,expected_type,expected_sort", |
| 101 | + [ |
| 102 | + _p( |
| 103 | + ["field_one+"], |
| 104 | + sort.FixedFieldSort, |
| 105 | + sort.FixedFieldSort("field_one"), |
| 106 | + id="fixed_field_sort", |
| 107 | + ), |
| 108 | + _p( |
| 109 | + ["flex_field+"], |
| 110 | + sort.SlowFieldSort, |
| 111 | + sort.SlowFieldSort("flex_field"), |
| 112 | + id="flex_field_sort", |
| 113 | + ), |
| 114 | + _p(["some_sort+"], SortFixture, None, id="special_sort"), |
| 115 | + ], |
| 116 | + ) |
| 117 | + def test_sort_from_parts_types( |
| 118 | + self, sort_parts, expected_type, expected_sort |
| 119 | + ): |
| 120 | + s = _parse_sort_parts(sort_parts) |
| 121 | + assert isinstance(s, expected_type) |
| 122 | + if expected_sort is not None: |
| 123 | + assert s == expected_sort |
| 124 | + |
| 125 | + |
| 126 | +class TestParseSortedQuery: |
| 127 | + @pytest.mark.parametrize( |
| 128 | + "query_str,expected_type,expected_subqueries", |
| 129 | + [ |
| 130 | + _p("foo bar", query.AndQuery, 2, id="and_query"), |
| 131 | + _p("foo , bar", query.OrQuery, 4, id="or_query"), |
| 132 | + _p("foo, bar", query.OrQuery, 4, id="no_space_before_comma_or_query"), |
| 133 | + _p("foo,bar", query.OrQuery, 2, id="no_spaces_or_query"), |
| 134 | + _p("foo , bar ,", query.OrQuery, 5, id="trailing_comma_or_query"), |
| 135 | + _p(", foo , bar", query.OrQuery, 5, id="leading_comma_or_query"), |
| 136 | + _p("-", query.NotQuery, None, id="only_direction"), |
| 137 | + ], |
| 138 | + ) # fmt: skip |
| 139 | + def test_parse_sorted_query( |
| 140 | + self, query_str, expected_type, expected_subqueries |
| 141 | + ): |
| 142 | + """Verify that query strings are parsed into the correct query type.""" |
| 143 | + q = _parse_query_parts(query_str.split()) |
| 144 | + assert isinstance(q, expected_type) |
| 145 | + if expected_subqueries is not None: |
| 146 | + assert len(q.subqueries) == expected_subqueries |
| 147 | + |
| 148 | + |
| 149 | +class TestModelQueryErrors: |
195 | 150 | def test_parse_invalid_query_string(self): |
196 | 151 | with pytest.raises(query.ParsingError): |
197 | 152 | ModelQuery.parse(ModelFixture1, 'foo"') |
0 commit comments