Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}