Monorepo for Tangled
1package searchquery
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7)
8
9func TestParseMixed(t *testing.T) {
10 q := Parse(`state:open bug "critical issue" label:good-first-issue fix`)
11 items := q.Items()
12 assert.Equal(t, 5, len(items))
13
14 assert.Equal(t, KindTagValue, items[0].Kind)
15 assert.Equal(t, "state", items[0].Key)
16 assert.Equal(t, "open", items[0].Value)
17
18 assert.Equal(t, KindKeyword, items[1].Kind)
19 assert.Equal(t, "bug", items[1].Raw)
20
21 assert.Equal(t, KindQuoted, items[2].Kind)
22 assert.Equal(t, `"critical issue"`, items[2].Raw)
23
24 assert.Equal(t, KindTagValue, items[3].Kind)
25 assert.Equal(t, "label", items[3].Key)
26 assert.Equal(t, "good-first-issue", items[3].Value)
27
28 assert.Equal(t, KindKeyword, items[4].Kind)
29
30 assert.Equal(t, `state:open bug "critical issue" label:good-first-issue fix`, q.String())
31}
32
33func TestGetSetLifecycle(t *testing.T) {
34 q := Parse("label:bug state:open keyword label:feature label:urgent")
35
36 // Get returns first match
37 val := q.Get("state")
38 assert.NotNil(t, val)
39 assert.Equal(t, "open", *val)
40
41 // Get returns nil for missing key
42 assert.Nil(t, q.Get("author"))
43
44 // Has
45 assert.True(t, q.Has("state"))
46 assert.False(t, q.Has("author"))
47
48 // GetAll
49 assert.Equal(t, []string{"bug", "feature", "urgent"}, q.GetAll("label"))
50 assert.Equal(t, 0, len(q.GetAll("missing")))
51
52 // Set updates existing, preserving position
53 q.Set("state", "closed")
54 assert.Equal(t, "label:bug state:closed keyword label:feature label:urgent", q.String())
55
56 // Set deduplicates
57 q.Set("label", "single")
58 assert.Equal(t, "label:single state:closed keyword", q.String())
59
60 // Set appends new tag
61 q.Set("author", "bob")
62 assert.Equal(t, "label:single state:closed keyword author:bob", q.String())
63}
64
65func TestParseEmpty(t *testing.T) {
66 q := Parse(" ")
67 assert.Equal(t, 0, len(q.Items()))
68 assert.Equal(t, "", q.String())
69}
70
71func TestParseUnclosedQuote(t *testing.T) {
72 q := Parse(`"hello world`)
73 items := q.Items()
74 assert.Equal(t, 1, len(items))
75 assert.Equal(t, KindQuoted, items[0].Kind)
76 assert.Equal(t, `"hello world`, items[0].Raw)
77 assert.Equal(t, "hello world", items[0].Value)
78}
79
80func TestParseLeadingColon(t *testing.T) {
81 q := Parse(":value")
82 items := q.Items()
83 assert.Equal(t, 1, len(items))
84 assert.Equal(t, KindKeyword, items[0].Kind)
85 assert.Equal(t, ":value", items[0].Raw)
86}
87
88func TestParseColonInValue(t *testing.T) {
89 q := Parse("key:value:with:colons")
90 items := q.Items()
91 assert.Equal(t, 1, len(items))
92 assert.Equal(t, "key", items[0].Key)
93 assert.Equal(t, "value:with:colons", items[0].Value)
94}
95
96func TestParseEmptyValue(t *testing.T) {
97 q := Parse("state:")
98 items := q.Items()
99 assert.Equal(t, 1, len(items))
100 assert.Equal(t, KindTagValue, items[0].Kind)
101 assert.Equal(t, "state", items[0].Key)
102 assert.Equal(t, "", items[0].Value)
103}
104
105func TestQuotedKeyValueIsNotTag(t *testing.T) {
106 q := Parse(`"state:open"`)
107 items := q.Items()
108 assert.Equal(t, 1, len(items))
109 assert.Equal(t, KindQuoted, items[0].Kind)
110 assert.Equal(t, "state:open", items[0].Value)
111 assert.False(t, q.Has("state"))
112}
113
114func TestConsecutiveQuotes(t *testing.T) {
115 q := Parse(`"one""two"`)
116 items := q.Items()
117 assert.Equal(t, 2, len(items))
118 assert.Equal(t, `"one"`, items[0].Raw)
119 assert.Equal(t, `"two"`, items[1].Raw)
120}
121
122func TestEscapedQuotes(t *testing.T) {
123 q := Parse(`"hello \"world\""`)
124 items := q.Items()
125 assert.Equal(t, 1, len(items))
126 assert.Equal(t, KindQuoted, items[0].Kind)
127 assert.Equal(t, `"hello \"world\""`, items[0].Raw)
128 assert.Equal(t, `hello "world"`, items[0].Value)
129}
130
131func TestEscapedBackslash(t *testing.T) {
132 q := Parse(`"hello\\"`)
133 items := q.Items()
134 assert.Equal(t, 1, len(items))
135 assert.Equal(t, KindQuoted, items[0].Kind)
136 assert.Equal(t, `hello\`, items[0].Value)
137}
138
139func TestNegatedTag(t *testing.T) {
140 q := Parse("state:open -label:bug keyword -label:wontfix")
141 items := q.Items()
142 assert.Equal(t, 4, len(items))
143
144 assert.False(t, items[0].Negated)
145 assert.Equal(t, "state", items[0].Key)
146
147 assert.True(t, items[1].Negated)
148 assert.Equal(t, KindTagValue, items[1].Kind)
149 assert.Equal(t, "label", items[1].Key)
150 assert.Equal(t, "bug", items[1].Value)
151 assert.Equal(t, "-label:bug", items[1].Raw)
152
153 assert.True(t, items[3].Negated)
154 assert.Equal(t, "wontfix", items[3].Value)
155
156 // Get/GetAll/Has skip negated tags
157 assert.False(t, q.Has("label"))
158 assert.Equal(t, 0, len(q.GetAll("label")))
159
160 // Set doesn't touch negated tags
161 q.Set("label", "feature")
162 assert.Equal(t, "state:open -label:bug keyword -label:wontfix label:feature", q.String())
163}
164
165func TestNegatedBareWordIsKeyword(t *testing.T) {
166 q := Parse("-keyword")
167 items := q.Items()
168 assert.Equal(t, 1, len(items))
169 assert.Equal(t, KindKeyword, items[0].Kind)
170 assert.Equal(t, "-keyword", items[0].Raw)
171}
172
173func TestNegatedQuotedPhrase(t *testing.T) {
174 q := Parse(`-"critical bug" state:open`)
175 items := q.Items()
176 assert.Equal(t, 2, len(items))
177
178 assert.Equal(t, KindQuoted, items[0].Kind)
179 assert.True(t, items[0].Negated)
180 assert.Equal(t, `-"critical bug"`, items[0].Raw)
181 assert.Equal(t, "critical bug", items[0].Value)
182
183 assert.Equal(t, KindTagValue, items[1].Kind)
184 assert.Equal(t, "state", items[1].Key)
185}
186
187func TestNegatedQuotedPhraseAmongOthers(t *testing.T) {
188 q := Parse(`"good phrase" -"bad phrase" keyword`)
189 items := q.Items()
190 assert.Equal(t, 3, len(items))
191
192 assert.Equal(t, KindQuoted, items[0].Kind)
193 assert.False(t, items[0].Negated)
194 assert.Equal(t, "good phrase", items[0].Value)
195
196 assert.Equal(t, KindQuoted, items[1].Kind)
197 assert.True(t, items[1].Negated)
198 assert.Equal(t, "bad phrase", items[1].Value)
199
200 assert.Equal(t, KindKeyword, items[2].Kind)
201}
202
203func TestWhitespaceNormalization(t *testing.T) {
204 q := Parse(" state:open keyword ")
205 assert.Equal(t, "state:open keyword", q.String())
206}
207
208func TestParseConsecutiveColons(t *testing.T) {
209 q := Parse("foo:::bar")
210 items := q.Items()
211 assert.Equal(t, 1, len(items))
212 assert.Equal(t, KindTagValue, items[0].Kind)
213 assert.Equal(t, "foo", items[0].Key)
214 assert.Equal(t, "::bar", items[0].Value)
215}
216
217func TestParseEmptyQuotes(t *testing.T) {
218 q := Parse(`""`)
219 items := q.Items()
220 assert.Equal(t, 1, len(items))
221 assert.Equal(t, KindQuoted, items[0].Kind)
222 assert.Equal(t, `""`, items[0].Raw)
223 assert.Equal(t, "", items[0].Value)
224}
225
226func TestParseBareDash(t *testing.T) {
227 q := Parse("-")
228 items := q.Items()
229 assert.Equal(t, 1, len(items))
230 assert.Equal(t, KindKeyword, items[0].Kind)
231 assert.False(t, items[0].Negated)
232 assert.Equal(t, "-", items[0].Raw)
233}
234
235func TestParseDashColon(t *testing.T) {
236 q := Parse("-:value")
237 items := q.Items()
238 assert.Equal(t, 1, len(items))
239 assert.Equal(t, KindKeyword, items[0].Kind)
240 assert.True(t, items[0].Negated)
241 assert.Equal(t, ":value", items[0].Value)
242}
243
244func TestParseDoubleHyphen(t *testing.T) {
245 q := Parse("--label:bug")
246 items := q.Items()
247 assert.Equal(t, 1, len(items))
248 assert.Equal(t, KindTagValue, items[0].Kind)
249 assert.True(t, items[0].Negated)
250 assert.Equal(t, "-label", items[0].Key)
251 assert.Equal(t, "bug", items[0].Value)
252}
253
254func TestDynamicTags(t *testing.T) {
255 q := Parse("state:open label:bug priority:high severity:critical -priority:low author:alice -severity:minor keyword")
256
257 // Known tags are not included
258 dynamic := q.GetDynamicTags()
259 assert.Equal(t, []string{"priority:high", "severity:critical"}, dynamic)
260
261 negated := q.GetNegatedDynamicTags()
262 assert.Equal(t, []string{"priority:low", "severity:minor"}, negated)
263
264 // Known tags still work as before
265 assert.Equal(t, []string{"bug"}, q.GetAll("label"))
266 val := q.Get("state")
267 assert.NotNil(t, val)
268 assert.Equal(t, "open", *val)
269}
270
271func TestDynamicTagsEmpty(t *testing.T) {
272 q := Parse("state:open label:bug author:alice keyword")
273 assert.Equal(t, 0, len(q.GetDynamicTags()))
274 assert.Equal(t, 0, len(q.GetNegatedDynamicTags()))
275}