glob_test.gno
10.84 Kb · 355 lines
1package glob
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func TestMatchLiteralsAndSingles(t *testing.T) {
10 cases := []struct {
11 pattern string
12 name string
13 want bool
14 }{
15 {"", "", true},
16 {"", "a", false},
17 {"a", "", false},
18 {"abc", "abc", true},
19 {"abc", "abd", false},
20 {"abc", "abcd", false},
21 {"?", "a", true},
22 {"?", "", false},
23 {"a?c", "abc", true},
24 {"a?c", "ac", false},
25 {"???", "abc", true},
26 {"???", "ab", false},
27 // '?' is one BYTE, not one rune. Documented, and pinned here so the
28 // claim in the README cannot drift away from the code.
29 {"?", "é", false},
30 {"??", "é", true},
31 }
32 for _, tc := range cases {
33 got := Match(tc.pattern, tc.name)
34 uassert.Equal(t, tc.want, got, "Match("+tc.pattern+", "+tc.name+")")
35 }
36}
37
38func TestMatchStar(t *testing.T) {
39 cases := []struct {
40 pattern string
41 name string
42 want bool
43 }{
44 {"*", "", true},
45 {"*", "anything", true},
46 {"*", "with/slashes/inside", true},
47 {"a*", "a", true},
48 {"a*", "abc", true},
49 {"a*", "b", false},
50 {"*c", "abc", true},
51 {"*c", "abd", false},
52 {"a*c", "ac", true},
53 {"a*c", "abc", true},
54 {"a*c", "abbbbc", true},
55 {"a*c", "abbbbd", false},
56 {"**", "abc", true},
57 {"a**c", "abc", true},
58 // The pattern a naive recursive matcher takes exponential time on.
59 // Here it is a correctness case; the cost claim is in the README.
60 {"a*a*a*a*a*a*b", "aaaaaaaaaaaaaaaaaaaaaaaa", false},
61 {"a*a*a*a*a*a*b", "aaaaaaaaaaaaaaaaaaaaaaab", true},
62 }
63 for _, tc := range cases {
64 got := Match(tc.pattern, tc.name)
65 uassert.Equal(t, tc.want, got, "Match("+tc.pattern+", "+tc.name+")")
66 }
67}
68
69func TestMatchClass(t *testing.T) {
70 cases := []struct {
71 pattern string
72 name string
73 want bool
74 }{
75 {"[abc]", "b", true},
76 {"[abc]", "d", false},
77 {"[a-z]", "q", true},
78 {"[a-z]", "Q", false},
79 {"[a-zA-Z0-9]", "7", true},
80 {"[!a-z]", "Q", true},
81 {"[!a-z]", "q", false},
82 {"[^a-z]", "Q", true},
83 {"g1[a-z0-9]*", "g1manfred", true},
84 {"g1[a-z0-9]*", "g1MANFRED", false},
85 // A '-' at either end of a class is a literal, not a range.
86 {"[-a]", "-", true},
87 {"[a-]", "-", true},
88 {"[a-]", "a", true},
89 {"[a-]", "b", false},
90 // A ']' inside a class has to be escaped; there is no POSIX
91 // leading-']' rule here.
92 {`[\]]`, "]", true},
93 {`[\]]`, "x", false},
94 // A class never matches an empty name: it consumes exactly one byte.
95 {"[abc]", "", false},
96 }
97 for _, tc := range cases {
98 got := Match(tc.pattern, tc.name)
99 uassert.Equal(t, tc.want, got, "Match("+tc.pattern+", "+tc.name+")")
100 }
101}
102
103func TestMatchEscape(t *testing.T) {
104 cases := []struct {
105 pattern string
106 name string
107 want bool
108 }{
109 {`\*`, "*", true},
110 {`\*`, "abc", false},
111 {`a\*b`, "a*b", true},
112 {`a\*b`, "axb", false},
113 {`\?`, "?", true},
114 {`\?`, "a", false},
115 {`\[a]`, "[a]", true},
116 {`\\`, `\`, true},
117 }
118 for _, tc := range cases {
119 got := Match(tc.pattern, tc.name)
120 uassert.Equal(t, tc.want, got, "Match("+tc.pattern+", "+tc.name+")")
121 }
122}
123
124func TestValid(t *testing.T) {
125 cases := []struct {
126 pattern string
127 want bool
128 }{
129 {"", true},
130 {"abc", true},
131 {"a*b?c", true},
132 {"[a-z]", true},
133 {"[!a-z]", true},
134 {`[\]]`, true},
135 {`a\*b`, true},
136 {"**/*.gno", true},
137 // Unterminated class.
138 {"[a-z", false},
139 {"[", false},
140 // Empty class: '[]' is not a class, and '[!]' negates nothing.
141 {"[]", false},
142 {"[!]", false},
143 // Dangling escape, including one that eats the closing bracket.
144 {`abc\`, false},
145 {`[a\`, false},
146 }
147 for _, tc := range cases {
148 uassert.Equal(t, tc.want, Valid(tc.pattern), "Valid("+tc.pattern+")")
149 }
150}
151
152// TestInvalidPatternMatchesNothing pins the decision that replaces an error
153// return: Match has no way to report a bad pattern, so a bad pattern is inert
154// rather than permissive. The failure mode of the alternative is an allowlist
155// entry with a typo that matches everything.
156func TestInvalidPatternMatchesNothing(t *testing.T) {
157 for _, pattern := range []string{"[a-z", "[]", `abc\`} {
158 uassert.False(t, Match(pattern, "abc"), "Match("+pattern+", abc)")
159 uassert.False(t, Match(pattern, ""), "Match("+pattern+", empty)")
160 uassert.False(t, MatchPath(pattern, "abc"), "MatchPath("+pattern+", abc)")
161 }
162}
163
164func TestMatchPathSingleStarStopsAtSeparator(t *testing.T) {
165 cases := []struct {
166 pattern string
167 name string
168 want bool
169 }{
170 {"*", "abc", true},
171 {"*", "a/b", false},
172 {"?", "/", false},
173 {"a/*", "a/b", true},
174 {"a/*", "a/b/c", false},
175 {"a/*/c", "a/b/c", true},
176 {"a/*/c", "a/c", false},
177 {"[a-z]/x", "q/x", true},
178 {"[a-z]/x", "q/y", false},
179 // The allowlist case this package exists for.
180 {"gno.land/r/moul/*", "gno.land/r/moul/home", true},
181 {"gno.land/r/moul/*", "gno.land/r/moul/x/daily", false},
182 {"gno.land/r/moul/*", "gno.land/r/other/home", false},
183 }
184 for _, tc := range cases {
185 got := MatchPath(tc.pattern, tc.name)
186 uassert.Equal(t, tc.want, got, "MatchPath("+tc.pattern+", "+tc.name+")")
187 }
188}
189
190func TestMatchPathDoubleStarCrossesSeparator(t *testing.T) {
191 cases := []struct {
192 pattern string
193 name string
194 want bool
195 }{
196 {"**", "", true},
197 {"**", "a", true},
198 {"**", "a/b/c", true},
199 {"a/**", "a/b", true},
200 {"a/**", "a/b/c/d", true},
201 {"a/**", "b/c", false},
202 // A '**' segment matches zero segments too, which is the case a
203 // segment-by-segment walk gets wrong.
204 {"a/**/b", "a/b", true},
205 {"a/**/b", "a/x/b", true},
206 {"a/**/b", "a/x/y/b", true},
207 {"a/**/b", "a/x/y/c", false},
208 {"**/c", "a/b/c", true},
209 {"**/c", "c", true},
210 {"**/c", "a/b/d", false},
211 {"gno.land/r/moul/**", "gno.land/r/moul/x/daily/globdemo", true},
212 // '**' only crosses as a whole segment. Glued to anything else it is
213 // an ordinary star and stays inside its own segment.
214 {"a/**x/b", "a/y/x/b", false},
215 {"a/**x/b", "a/yx/b", true},
216 }
217 for _, tc := range cases {
218 got := MatchPath(tc.pattern, tc.name)
219 uassert.Equal(t, tc.want, got, "MatchPath("+tc.pattern+", "+tc.name+")")
220 }
221}
222
223// TestMatchPathIsNotMatch pins the one difference a caller is most likely to
224// assume away: the same pattern and name answer differently in the two modes.
225func TestMatchPathIsNotMatch(t *testing.T) {
226 uassert.True(t, Match("gno.land/*", "gno.land/r/moul/home"), "Match crosses")
227 uassert.False(t, MatchPath("gno.land/*", "gno.land/r/moul/home"), "MatchPath does not")
228}
229
230func TestFilter(t *testing.T) {
231 names := []string{"alpha.gno", "beta.gno", "alpha_test.gno", "README.md"}
232
233 got := Filter("*.gno", names)
234 uassert.Equal(t, 3, len(got))
235 uassert.Equal(t, "alpha.gno", got[0])
236 uassert.Equal(t, "beta.gno", got[1])
237 uassert.Equal(t, "alpha_test.gno", got[2])
238
239 got = Filter("*_test.gno", names)
240 uassert.Equal(t, 1, len(got))
241 uassert.Equal(t, "alpha_test.gno", got[0])
242
243 // No match returns an empty slice, never nil, so a caller can range and
244 // len it without a guard.
245 got = Filter("*.txt", names)
246 uassert.Equal(t, 0, len(got))
247 uassert.True(t, got != nil, "Filter returns a non-nil empty slice")
248
249 // An invalid pattern filters everything out rather than letting it through.
250 uassert.Equal(t, 0, len(Filter("[a-z", names)))
251}
252
253func TestFilterPath(t *testing.T) {
254 paths := []string{
255 "gno.land/r/moul/home",
256 "gno.land/r/moul/x/daily/globdemo",
257 "gno.land/p/moul/x/daily/glob",
258 "gno.land/r/other/home",
259 }
260
261 got := FilterPath("gno.land/r/moul/*", paths)
262 uassert.Equal(t, 1, len(got))
263 uassert.Equal(t, "gno.land/r/moul/home", got[0])
264
265 got = FilterPath("gno.land/r/moul/**", paths)
266 uassert.Equal(t, 2, len(got))
267 uassert.Equal(t, "gno.land/r/moul/home", got[0])
268 uassert.Equal(t, "gno.land/r/moul/x/daily/globdemo", got[1])
269
270 got = FilterPath("gno.land/*/moul/**", paths)
271 uassert.Equal(t, 3, len(got))
272}
273
274func TestAny(t *testing.T) {
275 patterns := []string{"*.gno", "*.md"}
276 uassert.True(t, Any(patterns, "a.gno"))
277 uassert.True(t, Any(patterns, "README.md"))
278 uassert.False(t, Any(patterns, "a.txt"))
279 uassert.False(t, Any(nil, "a.gno"), "no pattern matches nothing")
280 // One bad entry does not make the whole list permissive, and does not
281 // disable the good entries either.
282 uassert.True(t, Any([]string{"[a-z", "*.gno"}, "a.gno"))
283 uassert.False(t, Any([]string{"[a-z"}, "a.gno"))
284}
285
286func TestAnyPath(t *testing.T) {
287 allow := []string{
288 "gno.land/r/moul/home",
289 "gno.land/r/moul/x/daily/**",
290 }
291 uassert.True(t, AnyPath(allow, "gno.land/r/moul/home"))
292 uassert.True(t, AnyPath(allow, "gno.land/r/moul/x/daily/globdemo"))
293 uassert.False(t, AnyPath(allow, "gno.land/r/moul/blog"))
294 // The prefix trap a hand-rolled HasPrefix allowlist walks into: a longer
295 // sibling name is not a child path.
296 uassert.False(t, AnyPath(allow, "gno.land/r/moul/homeless"))
297 uassert.False(t, AnyPath(allow, "gno.land/r/moul/x/dailyevil/thing"))
298}
299
300// TestStarIsGreedyButBacktracks is the property a single-backtrack-point
301// matcher has to get right: the first star takes everything it can, then gives
302// bytes back one at a time until the tail fits.
303func TestStarIsGreedyButBacktracks(t *testing.T) {
304 uassert.True(t, Match("*.gno", "a.b.c.gno"))
305 uassert.True(t, Match("*ab*", "xxabyy"))
306 uassert.True(t, Match("*ab*cd", "xxabyycd"))
307 uassert.False(t, Match("*ab*cd", "xxabyycde"))
308 uassert.True(t, Match("a*b*c*d", "abcd"))
309 uassert.True(t, Match("a*b*c*d", "a1b2c3d"))
310 uassert.False(t, Match("a*b*c*d", "a1b2c3"))
311}
312
313// TestMatchPathSeparatorInsidePatternSyntax pins the one place the two layers
314// of this package can disagree: a '/' that is escaped, or that sits inside a
315// character class, belongs to its segment and must not split the pattern.
316// Splitting there cuts an escape pair or a bracket expression in half, and the
317// matcher then reads past the end of the piece it was handed.
318//
319// No name segment can contain a separator, so such a pattern matches nothing.
320// The requirement is that it answers rather than aborts: a Render that panics
321// on a stored pattern is a page that is broken for good.
322func TestMatchPathSeparatorInsidePatternSyntax(t *testing.T) {
323 cases := []struct {
324 pattern string
325 name string
326 }{
327 {`a\/b`, "ax/b"},
328 {`a\/b`, "a/b"},
329 {`a\/b`, "ab"},
330 {`a\/b`, ""},
331 {"[/]", "x/y"},
332 {"[/]", "x"},
333 {"[a-/]", "x"},
334 {`**/a\/b`, "x/y"},
335 {`a\/b/c`, "a/b/c"},
336 }
337 for _, tc := range cases {
338 uassert.False(t, MatchPath(tc.pattern, tc.name),
339 "MatchPath("+tc.pattern+", "+tc.name+")")
340 }
341
342 // FilterPath and AnyPath split the same way and must be total too.
343 uassert.Equal(t, 0, len(FilterPath(`a\/b`, []string{"ax/b", "a/b"})))
344 uassert.Equal(t, 0, len(FilterPath("[/]", []string{"x/y", "x"})))
345 uassert.False(t, AnyPath([]string{"[/]", `a\/b`}, "x/y"))
346
347 // A separator that is neither escaped nor in a class still splits, and a
348 // class holding one still works on the segment it belongs to.
349 uassert.True(t, MatchPath(`a\*b/c`, "a*b/c"), `MatchPath(a\*b/c, a*b/c)`)
350 uassert.True(t, MatchPath("[abc]/x", "b/x"), "MatchPath([abc]/x, b/x)")
351 // A negated class holding the separator is the one shape here that does
352 // match: it is a single segment meaning "any byte but a separator", which
353 // is what every name segment is.
354 uassert.True(t, MatchPath("[!/]", "x"), "MatchPath([!/], x)")
355}