github.com/AndrewDeryabin/doublestar/v4@v4.0.0-20230123132908-d9476b7d41be/doublestar_test.go (about)

     1  package doublestar
     2  
     3  import (
     4  	"io/fs"
     5  	"log"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  )
    13  
    14  type MatchTest struct {
    15  	pattern, testPath     string // a pattern and path to test the pattern on
    16  	shouldMatch           bool   // true if the pattern should match the path
    17  	shouldMatchGlob       bool   // true if glob should match the path
    18  	expectedErr           error  // an expected error
    19  	expectIOErr           bool   // whether or not to expect an io error
    20  	expectPatternNotExist bool   // whether or not to expect ErrPatternNotExist
    21  	isStandard            bool   // pattern doesn't use any doublestar features (e.g. '**', '{a,b}')
    22  	testOnDisk            bool   // true: test pattern against files in "test" directory
    23  	numResults            int    // number of glob results if testing on disk
    24  	winNumResults         int    // number of glob results on Windows
    25  }
    26  
    27  // Tests which contain escapes and symlinks will not work on Windows
    28  var onWindows = runtime.GOOS == "windows"
    29  
    30  var matchTests = []MatchTest{
    31  	{"*", "", true, true, nil, false, false, true, false, 0, 0},
    32  	{"*", "/", false, false, nil, false, false, true, false, 0, 0},
    33  	{"/*", "/", true, true, nil, false, false, true, false, 0, 0},
    34  	{"/*", "/debug/", false, false, nil, false, false, true, false, 0, 0},
    35  	{"/*", "//", false, false, nil, false, false, true, false, 0, 0},
    36  	{"abc", "abc", true, true, nil, false, false, true, true, 1, 1},
    37  	{"*", "abc", true, true, nil, false, false, true, true, 22, 17},
    38  	{"*c", "abc", true, true, nil, false, false, true, true, 2, 2},
    39  	{"*/", "a/", true, true, nil, false, false, true, false, 0, 0},
    40  	{"a*", "a", true, true, nil, false, false, true, true, 9, 9},
    41  	{"a*", "abc", true, true, nil, false, false, true, true, 9, 9},
    42  	{"a*", "ab/c", false, false, nil, false, false, true, true, 9, 9},
    43  	{"a*/b", "abc/b", true, true, nil, false, false, true, true, 2, 2},
    44  	{"a*/b", "a/c/b", false, false, nil, false, false, true, true, 2, 2},
    45  	{"a*/c/", "a/b", false, false, nil, false, false, false, true, 1, 1},
    46  	{"a*b*c*d*e*", "axbxcxdxe", true, true, nil, false, false, true, true, 3, 3},
    47  	{"a*b*c*d*e*/f", "axbxcxdxe/f", true, true, nil, false, false, true, true, 2, 2},
    48  	{"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, true, nil, false, false, true, true, 2, 2},
    49  	{"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, false, nil, false, false, true, true, 2, 2},
    50  	{"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, false, nil, false, false, true, true, 2, 2},
    51  	{"a*b?c*x", "abxbbxdbxebxczzx", true, true, nil, false, false, true, true, 2, 2},
    52  	{"a*b?c*x", "abxbbxdbxebxczzy", false, false, nil, false, false, true, true, 2, 2},
    53  	{"ab[c]", "abc", true, true, nil, false, false, true, true, 1, 1},
    54  	{"ab[b-d]", "abc", true, true, nil, false, false, true, true, 1, 1},
    55  	{"ab[e-g]", "abc", false, false, nil, false, false, true, true, 0, 0},
    56  	{"ab[^c]", "abc", false, false, nil, false, false, true, true, 0, 0},
    57  	{"ab[^b-d]", "abc", false, false, nil, false, false, true, true, 0, 0},
    58  	{"ab[^e-g]", "abc", true, true, nil, false, false, true, true, 1, 1},
    59  	{"a\\*b", "ab", false, false, nil, false, true, true, !onWindows, 0, 0},
    60  	{"a?b", "a☺b", true, true, nil, false, false, true, true, 1, 1},
    61  	{"a[^a]b", "a☺b", true, true, nil, false, false, true, true, 1, 1},
    62  	{"a[!a]b", "a☺b", true, true, nil, false, false, false, true, 1, 1},
    63  	{"a???b", "a☺b", false, false, nil, false, false, true, true, 0, 0},
    64  	{"a[^a][^a][^a]b", "a☺b", false, false, nil, false, false, true, true, 0, 0},
    65  	{"[a-ζ]*", "α", true, true, nil, false, false, true, true, 20, 17},
    66  	{"*[a-ζ]", "A", false, false, nil, false, false, true, true, 20, 17},
    67  	{"a?b", "a/b", false, false, nil, false, false, true, true, 1, 1},
    68  	{"a*b", "a/b", false, false, nil, false, false, true, true, 1, 1},
    69  	{"[\\]a]", "]", true, true, nil, false, false, true, !onWindows, 2, 2},
    70  	{"[\\-]", "-", true, true, nil, false, false, true, !onWindows, 1, 1},
    71  	{"[x\\-]", "x", true, true, nil, false, false, true, !onWindows, 2, 2},
    72  	{"[x\\-]", "-", true, true, nil, false, false, true, !onWindows, 2, 2},
    73  	{"[x\\-]", "z", false, false, nil, false, false, true, !onWindows, 2, 2},
    74  	{"[\\-x]", "x", true, true, nil, false, false, true, !onWindows, 2, 2},
    75  	{"[\\-x]", "-", true, true, nil, false, false, true, !onWindows, 2, 2},
    76  	{"[\\-x]", "a", false, false, nil, false, false, true, !onWindows, 2, 2},
    77  	{"[]a]", "]", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    78  	// doublestar, like bash, allows these when path.Match() does not
    79  	{"[-]", "-", true, true, nil, false, false, false, !onWindows, 1, 0},
    80  	{"[x-]", "x", true, true, nil, false, false, false, true, 2, 1},
    81  	{"[x-]", "-", true, true, nil, false, false, false, !onWindows, 2, 1},
    82  	{"[x-]", "z", false, false, nil, false, false, false, true, 2, 1},
    83  	{"[-x]", "x", true, true, nil, false, false, false, true, 2, 1},
    84  	{"[-x]", "-", true, true, nil, false, false, false, !onWindows, 2, 1},
    85  	{"[-x]", "a", false, false, nil, false, false, false, true, 2, 1},
    86  	{"[a-b-d]", "a", true, true, nil, false, false, false, true, 3, 2},
    87  	{"[a-b-d]", "b", true, true, nil, false, false, false, true, 3, 2},
    88  	{"[a-b-d]", "-", true, true, nil, false, false, false, !onWindows, 3, 2},
    89  	{"[a-b-d]", "c", false, false, nil, false, false, false, true, 3, 2},
    90  	{"[a-b-x]", "x", true, true, nil, false, false, false, true, 4, 3},
    91  	{"\\", "a", false, false, ErrBadPattern, false, false, true, !onWindows, 0, 0},
    92  	{"[", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    93  	{"[^", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    94  	{"[^bc", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    95  	{"a[", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    96  	{"a[", "ab", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    97  	{"ad[", "ab", false, false, ErrBadPattern, false, false, true, true, 0, 0},
    98  	{"*x", "xxx", true, true, nil, false, false, true, true, 4, 4},
    99  	{"[abc]", "b", true, true, nil, false, false, true, true, 3, 3},
   100  	{"**", "", true, true, nil, false, false, false, false, 38, 38},
   101  	{"a/**", "a", true, true, nil, false, false, false, true, 7, 7},
   102  	{"a/**", "a/", true, true, nil, false, false, false, false, 7, 7},
   103  	{"a/**", "a/b", true, true, nil, false, false, false, true, 7, 7},
   104  	{"a/**", "a/b/c", true, true, nil, false, false, false, true, 7, 7},
   105  	{"**/c", "c", true, true, nil, !onWindows, false, false, true, 5, 4},
   106  	{"**/c", "b/c", true, true, nil, !onWindows, false, false, true, 5, 4},
   107  	{"**/c", "a/b/c", true, true, nil, !onWindows, false, false, true, 5, 4},
   108  	{"**/c", "a/b", false, false, nil, !onWindows, false, false, true, 5, 4},
   109  	{"**/c", "abcd", false, false, nil, !onWindows, false, false, true, 5, 4},
   110  	{"**/c", "a/abc", false, false, nil, !onWindows, false, false, true, 5, 4},
   111  	{"a/**/b", "a/b", true, true, nil, false, false, false, true, 2, 2},
   112  	{"a/**/c", "a/b/c", true, true, nil, false, false, false, true, 2, 2},
   113  	{"a/**/d", "a/b/c/d", true, true, nil, false, false, false, true, 1, 1},
   114  	{"a/\\**", "a/b/c", false, false, nil, false, false, false, !onWindows, 0, 0},
   115  	{"a/\\[*\\]", "a/bc", false, false, nil, false, false, true, !onWindows, 0, 0},
   116  	// this fails the FilepathGlob test on Windows
   117  	{"a/b/c", "a/b//c", false, false, nil, false, false, true, !onWindows, 1, 1},
   118  	// odd: Glob + filepath.Glob return results
   119  	{"a/", "a", false, false, nil, false, false, true, false, 0, 0},
   120  	{"ab{c,d}", "abc", true, true, nil, false, true, false, true, 1, 1},
   121  	{"ab{c,d,*}", "abcde", true, true, nil, false, true, false, true, 5, 5},
   122  	{"ab{c,d}[", "abcd", false, false, ErrBadPattern, false, false, false, true, 0, 0},
   123  	{"a{,bc}", "a", true, true, nil, false, false, false, true, 2, 2},
   124  	{"a{,bc}", "abc", true, true, nil, false, false, false, true, 2, 2},
   125  	{"a/{b/c,c/b}", "a/b/c", true, true, nil, false, false, false, true, 2, 2},
   126  	{"a/{b/c,c/b}", "a/c/b", true, true, nil, false, false, false, true, 2, 2},
   127  	{"a/a*{b,c}", "a/abc", true, true, nil, false, false, false, true, 1, 1},
   128  	{"{a/{b,c},abc}", "a/b", true, true, nil, false, false, false, true, 3, 3},
   129  	{"{a/{b,c},abc}", "a/c", true, true, nil, false, false, false, true, 3, 3},
   130  	{"{a/{b,c},abc}", "abc", true, true, nil, false, false, false, true, 3, 3},
   131  	{"{a/{b,c},abc}", "a/b/c", false, false, nil, false, false, false, true, 3, 3},
   132  	{"{a/ab*}", "a/abc", true, true, nil, false, false, false, true, 1, 1},
   133  	{"{a/*}", "a/b", true, true, nil, false, false, false, true, 3, 3},
   134  	{"{a/abc}", "a/abc", true, true, nil, false, false, false, true, 1, 1},
   135  	{"{a/b,a/c}", "a/c", true, true, nil, false, false, false, true, 2, 2},
   136  	{"abc/**", "abc/b", true, true, nil, false, false, false, true, 3, 3},
   137  	{"**/abc", "abc", true, true, nil, !onWindows, false, false, true, 2, 2},
   138  	{"abc**", "abc/b", false, false, nil, false, false, false, true, 3, 3},
   139  	{"**/*.txt", "abc/【test】.txt", true, true, nil, !onWindows, false, false, true, 1, 1},
   140  	{"**/【*", "abc/【test】.txt", true, true, nil, !onWindows, false, false, true, 1, 1},
   141  	{"**/{a,b}", "a/b", true, true, nil, !onWindows, false, false, true, 5, 5},
   142  	// unfortunately, io/fs can't handle this, so neither can Glob =(
   143  	{"broken-symlink", "broken-symlink", true, true, nil, false, false, true, false, 1, 1},
   144  	{"broken-symlink/*", "a", false, false, nil, false, true, true, true, 0, 0},
   145  	{"broken*/*", "a", false, false, nil, false, false, true, true, 0, 0},
   146  	{"working-symlink/c/*", "working-symlink/c/d", true, true, nil, false, false, true, !onWindows, 1, 1},
   147  	{"working-sym*/*", "working-symlink/c", true, true, nil, false, false, true, !onWindows, 1, 1},
   148  	{"b/**/f", "b/symlink-dir/f", true, true, nil, false, false, false, !onWindows, 2, 2},
   149  	{"*/symlink-dir/*", "b/symlink-dir/f", true, true, nil, !onWindows, false, true, !onWindows, 2, 2},
   150  	{"e/**", "e/**", true, true, nil, false, false, false, !onWindows, 11, 6},
   151  	{"e/**", "e/*", true, true, nil, false, false, false, !onWindows, 11, 6},
   152  	{"e/**", "e/?", true, true, nil, false, false, false, !onWindows, 11, 6},
   153  	{"e/**", "e/[", true, true, nil, false, false, false, true, 11, 6},
   154  	{"e/**", "e/]", true, true, nil, false, false, false, true, 11, 6},
   155  	{"e/**", "e/[]", true, true, nil, false, false, false, true, 11, 6},
   156  	{"e/**", "e/{", true, true, nil, false, false, false, true, 11, 6},
   157  	{"e/**", "e/}", true, true, nil, false, false, false, true, 11, 6},
   158  	{"e/**", "e/\\", true, true, nil, false, false, false, !onWindows, 11, 6},
   159  	{"e/*", "e/*", true, true, nil, false, false, true, !onWindows, 10, 5},
   160  	{"e/?", "e/?", true, true, nil, false, false, true, !onWindows, 7, 4},
   161  	{"e/?", "e/*", true, true, nil, false, false, true, !onWindows, 7, 4},
   162  	{"e/?", "e/[", true, true, nil, false, false, true, true, 7, 4},
   163  	{"e/?", "e/]", true, true, nil, false, false, true, true, 7, 4},
   164  	{"e/?", "e/{", true, true, nil, false, false, true, true, 7, 4},
   165  	{"e/?", "e/}", true, true, nil, false, false, true, true, 7, 4},
   166  	{"e/\\[", "e/[", true, true, nil, false, false, true, !onWindows, 1, 1},
   167  	{"e/[", "e/[", false, false, ErrBadPattern, false, false, true, true, 0, 0},
   168  	{"e/]", "e/]", true, true, nil, false, false, true, true, 1, 1},
   169  	{"e/\\]", "e/]", true, true, nil, false, false, true, !onWindows, 1, 1},
   170  	{"e/\\{", "e/{", true, true, nil, false, false, true, !onWindows, 1, 1},
   171  	{"e/\\}", "e/}", true, true, nil, false, false, true, !onWindows, 1, 1},
   172  	{"e/[\\*\\?]", "e/*", true, true, nil, false, false, true, !onWindows, 2, 2},
   173  	{"e/[\\*\\?]", "e/?", true, true, nil, false, false, true, !onWindows, 2, 2},
   174  	{"e/[\\*\\?]", "e/**", false, false, nil, false, false, true, !onWindows, 2, 2},
   175  	{"e/[\\*\\?]?", "e/**", true, true, nil, false, false, true, !onWindows, 1, 1},
   176  	{"e/{\\*,\\?}", "e/*", true, true, nil, false, false, false, !onWindows, 2, 2},
   177  	{"e/{\\*,\\?}", "e/?", true, true, nil, false, false, false, !onWindows, 2, 2},
   178  	{"e/\\*", "e/*", true, true, nil, false, false, true, !onWindows, 1, 1},
   179  	{"e/\\?", "e/?", true, true, nil, false, false, true, !onWindows, 1, 1},
   180  	{"e/\\?", "e/**", false, false, nil, false, false, true, !onWindows, 1, 1},
   181  	{"*\\}", "}", true, true, nil, false, false, true, !onWindows, 1, 1},
   182  	{"nonexistent-path", "a", false, false, nil, false, true, true, true, 0, 0},
   183  	{"nonexistent-path/", "a", false, false, nil, false, true, true, true, 0, 0},
   184  	{"nonexistent-path/file", "a", false, false, nil, false, true, true, true, 0, 0},
   185  	{"nonexistent-path/*", "a", false, false, nil, false, true, true, true, 0, 0},
   186  	{"nonexistent-path/**", "a", false, false, nil, false, true, true, true, 0, 0},
   187  	{"nopermission/*", "nopermission/file", true, false, nil, true, false, true, !onWindows, 0, 0},
   188  	{"nopermission/dir/", "nopermission/dir", false, false, nil, true, false, true, !onWindows, 0, 0},
   189  	{"nopermission/file", "nopermission/file", true, false, nil, true, false, true, !onWindows, 0, 0},
   190  }
   191  
   192  // Calculate the number of results that we expect
   193  // WithFilesOnly at runtime and memoize them here
   194  var numResultsFilesOnly []int
   195  
   196  // Calculate the number of results that we expect
   197  // WithNoFollow at runtime and memoize them here
   198  var numResultsNoFollow []int
   199  
   200  // Calculate the number of results that we expect with all
   201  // of the options enabled at runtime and memoize them here
   202  var numResultsAllOpts []int
   203  
   204  func TestValidatePattern(t *testing.T) {
   205  	for idx, tt := range matchTests {
   206  		testValidatePatternWith(t, idx, tt)
   207  	}
   208  }
   209  
   210  func testValidatePatternWith(t *testing.T, idx int, tt MatchTest) {
   211  	defer func() {
   212  		if r := recover(); r != nil {
   213  			t.Errorf("#%v. Validate(%#q) panicked: %#v", idx, tt.pattern, r)
   214  		}
   215  	}()
   216  
   217  	result := ValidatePattern(tt.pattern)
   218  	if result != (tt.expectedErr == nil) {
   219  		t.Errorf("#%v. ValidatePattern(%#q) = %v want %v", idx, tt.pattern, result, !result)
   220  	}
   221  }
   222  
   223  func TestMatch(t *testing.T) {
   224  	for idx, tt := range matchTests {
   225  		// Since Match() always uses "/" as the separator, we
   226  		// don't need to worry about the tt.testOnDisk flag
   227  		testMatchWith(t, idx, tt)
   228  	}
   229  }
   230  
   231  func testMatchWith(t *testing.T, idx int, tt MatchTest) {
   232  	defer func() {
   233  		if r := recover(); r != nil {
   234  			t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r)
   235  		}
   236  	}()
   237  
   238  	// Match() always uses "/" as the separator
   239  	ok, err := Match(tt.pattern, tt.testPath)
   240  	if ok != tt.shouldMatch || err != tt.expectedErr {
   241  		t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, tt.pattern, tt.testPath, ok, err, tt.shouldMatch, tt.expectedErr)
   242  	}
   243  
   244  	if tt.isStandard {
   245  		stdOk, stdErr := path.Match(tt.pattern, tt.testPath)
   246  		if ok != stdOk || !compareErrors(err, stdErr) {
   247  			t.Errorf("#%v. Match(%#q, %#q) != path.Match(...). Got %v, %v want %v, %v", idx, tt.pattern, tt.testPath, ok, err, stdOk, stdErr)
   248  		}
   249  	}
   250  }
   251  
   252  func BenchmarkMatch(b *testing.B) {
   253  	b.ReportAllocs()
   254  	for i := 0; i < b.N; i++ {
   255  		for _, tt := range matchTests {
   256  			if tt.isStandard {
   257  				Match(tt.pattern, tt.testPath)
   258  			}
   259  		}
   260  	}
   261  }
   262  
   263  func BenchmarkGoMatch(b *testing.B) {
   264  	b.ReportAllocs()
   265  	for i := 0; i < b.N; i++ {
   266  		for _, tt := range matchTests {
   267  			if tt.isStandard {
   268  				path.Match(tt.pattern, tt.testPath)
   269  			}
   270  		}
   271  	}
   272  }
   273  
   274  func TestPathMatch(t *testing.T) {
   275  	for idx, tt := range matchTests {
   276  		// Even though we aren't actually matching paths on disk, we are using
   277  		// PathMatch() which will use the system's separator. As a result, any
   278  		// patterns that might cause problems on-disk need to also be avoided
   279  		// here in this test.
   280  		if tt.testOnDisk {
   281  			testPathMatchWith(t, idx, tt)
   282  		}
   283  	}
   284  }
   285  
   286  func testPathMatchWith(t *testing.T, idx int, tt MatchTest) {
   287  	defer func() {
   288  		if r := recover(); r != nil {
   289  			t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r)
   290  		}
   291  	}()
   292  
   293  	pattern := filepath.FromSlash(tt.pattern)
   294  	testPath := filepath.FromSlash(tt.testPath)
   295  	ok, err := PathMatch(pattern, testPath)
   296  	if ok != tt.shouldMatch || err != tt.expectedErr {
   297  		t.Errorf("#%v. PathMatch(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr)
   298  	}
   299  
   300  	if tt.isStandard {
   301  		stdOk, stdErr := filepath.Match(pattern, testPath)
   302  		if ok != stdOk || !compareErrors(err, stdErr) {
   303  			t.Errorf("#%v. PathMatch(%#q, %#q) != filepath.Match(...). Got %v, %v want %v, %v", idx, pattern, testPath, ok, err, stdOk, stdErr)
   304  		}
   305  	}
   306  }
   307  
   308  func TestPathMatchFake(t *testing.T) {
   309  	// This test fakes that our path separator is `\\` so we can test what it
   310  	// would be like on Windows - obviously, we don't need to do that if we
   311  	// actually _are_ on Windows, since TestPathMatch will cover it.
   312  	if onWindows {
   313  		return
   314  	}
   315  
   316  	for idx, tt := range matchTests {
   317  		// Even though we aren't actually matching paths on disk, we are using
   318  		// PathMatch() which will use the system's separator. As a result, any
   319  		// patterns that might cause problems on-disk need to also be avoided
   320  		// here in this test.
   321  		// On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
   322  		// So it's not possible to match escaped wild characters.
   323  		if tt.testOnDisk && !strings.Contains(tt.pattern, "\\") {
   324  			testPathMatchFakeWith(t, idx, tt)
   325  		}
   326  	}
   327  }
   328  
   329  func testPathMatchFakeWith(t *testing.T, idx int, tt MatchTest) {
   330  	defer func() {
   331  		if r := recover(); r != nil {
   332  			t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r)
   333  		}
   334  	}()
   335  
   336  	pattern := strings.ReplaceAll(tt.pattern, "/", "\\")
   337  	testPath := strings.ReplaceAll(tt.testPath, "/", "\\")
   338  	ok, err := matchWithSeparator(pattern, testPath, '\\', true)
   339  	if ok != tt.shouldMatch || err != tt.expectedErr {
   340  		t.Errorf("#%v. PathMatch(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr)
   341  	}
   342  }
   343  
   344  func BenchmarkPathMatch(b *testing.B) {
   345  	b.ReportAllocs()
   346  	for i := 0; i < b.N; i++ {
   347  		for _, tt := range matchTests {
   348  			if tt.isStandard && tt.testOnDisk {
   349  				pattern := filepath.FromSlash(tt.pattern)
   350  				testPath := filepath.FromSlash(tt.testPath)
   351  				PathMatch(pattern, testPath)
   352  			}
   353  		}
   354  	}
   355  }
   356  
   357  func BenchmarkGoPathMatch(b *testing.B) {
   358  	b.ReportAllocs()
   359  	for i := 0; i < b.N; i++ {
   360  		for _, tt := range matchTests {
   361  			if tt.isStandard && tt.testOnDisk {
   362  				pattern := filepath.FromSlash(tt.pattern)
   363  				testPath := filepath.FromSlash(tt.testPath)
   364  				filepath.Match(pattern, testPath)
   365  			}
   366  		}
   367  	}
   368  }
   369  
   370  func TestGlob(t *testing.T) {
   371  	doGlobTest(t)
   372  }
   373  
   374  func TestGlobWithFailOnIOErrors(t *testing.T) {
   375  	doGlobTest(t, WithFailOnIOErrors())
   376  }
   377  
   378  func TestGlobWithFailOnPatternNotExist(t *testing.T) {
   379  	doGlobTest(t, WithFailOnPatternNotExist())
   380  }
   381  
   382  func TestGlobWithFilesOnly(t *testing.T) {
   383  	doGlobTest(t, WithFilesOnly())
   384  }
   385  
   386  func TestGlobWithNoFollow(t *testing.T) {
   387  	doGlobTest(t, WithNoFollow())
   388  }
   389  
   390  func TestGlobWithAllOptions(t *testing.T) {
   391  	doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
   392  }
   393  
   394  func doGlobTest(t *testing.T, opts ...GlobOption) {
   395  	glob := newGlob(opts...)
   396  	fsys := os.DirFS("test")
   397  	for idx, tt := range matchTests {
   398  		if tt.testOnDisk {
   399  			testGlobWith(t, idx, tt, glob, opts, fsys)
   400  		}
   401  	}
   402  }
   403  
   404  func testGlobWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) {
   405  	defer func() {
   406  		if r := recover(); r != nil {
   407  			t.Errorf("#%v. Glob(%#q, %#v) panicked: %#v", idx, tt.pattern, g, r)
   408  		}
   409  	}()
   410  
   411  	matches, err := Glob(fsys, tt.pattern, opts...)
   412  	verifyGlobResults(t, idx, "Glob", tt, g, fsys, matches, err)
   413  	if len(opts) == 0 {
   414  		testStandardGlob(t, idx, "Glob", tt, fsys, matches, err)
   415  	}
   416  }
   417  
   418  func TestGlobWalk(t *testing.T) {
   419  	doGlobWalkTest(t)
   420  }
   421  
   422  func TestGlobWalkWithFailOnIOErrors(t *testing.T) {
   423  	doGlobWalkTest(t, WithFailOnIOErrors())
   424  }
   425  
   426  func TestGlobWalkWithFailOnPatternNotExist(t *testing.T) {
   427  	doGlobWalkTest(t, WithFailOnPatternNotExist())
   428  }
   429  
   430  func TestGlobWalkWithFilesOnly(t *testing.T) {
   431  	doGlobWalkTest(t, WithFilesOnly())
   432  }
   433  
   434  func TestGlobWalkWithNoFollow(t *testing.T) {
   435  	doGlobWalkTest(t, WithNoFollow())
   436  }
   437  
   438  func TestGlobWalkWithAllOptions(t *testing.T) {
   439  	doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
   440  }
   441  
   442  func doGlobWalkTest(t *testing.T, opts ...GlobOption) {
   443  	glob := newGlob(opts...)
   444  	fsys := os.DirFS("test")
   445  	for idx, tt := range matchTests {
   446  		if tt.testOnDisk {
   447  			testGlobWalkWith(t, idx, tt, glob, opts, fsys)
   448  		}
   449  	}
   450  }
   451  
   452  func testGlobWalkWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) {
   453  	defer func() {
   454  		if r := recover(); r != nil {
   455  			t.Errorf("#%v. Glob(%#q, %#v) panicked: %#v", idx, tt.pattern, opts, r)
   456  		}
   457  	}()
   458  
   459  	var matches []string
   460  	err := GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error {
   461  		matches = append(matches, p)
   462  		return nil
   463  	}, opts...)
   464  	verifyGlobResults(t, idx, "GlobWalk", tt, g, fsys, matches, err)
   465  	if len(opts) == 0 {
   466  		testStandardGlob(t, idx, "GlobWalk", tt, fsys, matches, err)
   467  	}
   468  }
   469  
   470  func testStandardGlob(t *testing.T, idx int, fn string, tt MatchTest, fsys fs.FS, matches []string, err error) {
   471  	if tt.isStandard {
   472  		stdMatches, stdErr := fs.Glob(fsys, tt.pattern)
   473  		if !compareSlices(matches, stdMatches) || !compareErrors(err, stdErr) {
   474  			t.Errorf("#%v. %v(%#q) != fs.Glob(...). Got %#v, %v want %#v, %v", idx, fn, tt.pattern, matches, err, stdMatches, stdErr)
   475  		}
   476  	}
   477  }
   478  
   479  func TestFilepathGlob(t *testing.T) {
   480  	doFilepathGlobTest(t)
   481  }
   482  
   483  func TestFilepathGlobWithFailOnIOErrors(t *testing.T) {
   484  	doFilepathGlobTest(t, WithFailOnIOErrors())
   485  }
   486  
   487  func TestFilepathGlobWithFailOnPatternNotExist(t *testing.T) {
   488  	doFilepathGlobTest(t, WithFailOnPatternNotExist())
   489  }
   490  
   491  func TestFilepathGlobWithFilesOnly(t *testing.T) {
   492  	doFilepathGlobTest(t, WithFilesOnly())
   493  }
   494  
   495  func TestFilepathGlobWithNoFollow(t *testing.T) {
   496  	doFilepathGlobTest(t, WithNoFollow())
   497  }
   498  
   499  func doFilepathGlobTest(t *testing.T, opts ...GlobOption) {
   500  	glob := newGlob(opts...)
   501  	fsys := os.DirFS("test")
   502  
   503  	// The patterns are relative to the "test" sub-directory.
   504  	defer func() {
   505  		os.Chdir("..")
   506  	}()
   507  	os.Chdir("test")
   508  
   509  	for idx, tt := range matchTests {
   510  		if tt.testOnDisk {
   511  			ttmod := tt
   512  			ttmod.pattern = filepath.FromSlash(tt.pattern)
   513  			ttmod.testPath = filepath.FromSlash(tt.testPath)
   514  			testFilepathGlobWith(t, idx, ttmod, glob, opts, fsys)
   515  		}
   516  	}
   517  }
   518  
   519  func testFilepathGlobWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) {
   520  	defer func() {
   521  		if r := recover(); r != nil {
   522  			t.Errorf("#%v. FilepathGlob(%#q, %#v) panicked: %#v", idx, tt.pattern, g, r)
   523  		}
   524  	}()
   525  
   526  	matches, err := FilepathGlob(tt.pattern, opts...)
   527  	verifyGlobResults(t, idx, "FilepathGlob", tt, g, fsys, matches, err)
   528  
   529  	if tt.isStandard && len(opts) == 0 {
   530  		stdMatches, stdErr := filepath.Glob(tt.pattern)
   531  		if !compareSlices(matches, stdMatches) || !compareErrors(err, stdErr) {
   532  			t.Errorf("#%v. FilepathGlob(%#q, %#v) != filepath.Glob(...). Got %#v, %v want %#v, %v", idx, tt.pattern, g, matches, err, stdMatches, stdErr)
   533  		}
   534  	}
   535  }
   536  
   537  func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob, fsys fs.FS, matches []string, err error) {
   538  	expectedErr := tt.expectedErr
   539  	if g.failOnPatternNotExist && tt.expectPatternNotExist {
   540  		expectedErr = ErrPatternNotExist
   541  	}
   542  
   543  	if g.failOnIOErrors {
   544  		if tt.expectIOErr {
   545  			if err == nil {
   546  				t.Errorf("#%v. %v(%#q, %#v) does not have an error, but should", idx, fn, tt.pattern, g)
   547  			}
   548  			return
   549  		} else if err != nil && err != expectedErr {
   550  			t.Errorf("#%v. %v(%#q, %#v) has error %v, but should not", idx, fn, tt.pattern, g, err)
   551  			return
   552  		}
   553  	}
   554  
   555  	if !g.failOnPatternNotExist || !tt.expectPatternNotExist {
   556  		numResults := tt.numResults
   557  		if onWindows {
   558  			numResults = tt.winNumResults
   559  		}
   560  		if g.filesOnly {
   561  			if g.noFollow {
   562  				numResults = numResultsAllOpts[idx]
   563  			} else {
   564  				numResults = numResultsFilesOnly[idx]
   565  			}
   566  		} else if g.noFollow {
   567  			numResults = numResultsNoFollow[idx]
   568  		}
   569  
   570  		if len(matches) != numResults {
   571  			t.Errorf("#%v. %v(%#q, %#v) = %#v - should have %#v results, got %#v", idx, fn, tt.pattern, g, matches, numResults, len(matches))
   572  		}
   573  		if !g.filesOnly && !g.noFollow && inSlice(tt.testPath, matches) != tt.shouldMatchGlob {
   574  			if tt.shouldMatchGlob {
   575  				t.Errorf("#%v. %v(%#q, %#v) = %#v - doesn't contain %v, but should", idx, fn, tt.pattern, g, matches, tt.testPath)
   576  			} else {
   577  				t.Errorf("#%v. %v(%#q, %#v) = %#v - contains %v, but shouldn't", idx, fn, tt.pattern, g, matches, tt.testPath)
   578  			}
   579  		}
   580  	}
   581  	if err != expectedErr {
   582  		t.Errorf("#%v. %v(%#q, %#v) has error %v, but should be %v", idx, fn, tt.pattern, g, err, expectedErr)
   583  	}
   584  }
   585  
   586  func TestGlobSorted(t *testing.T) {
   587  	fsys := os.DirFS("test")
   588  	expected := []string{"a", "abc", "abcd", "abcde", "abxbbxdbxebxczzx", "abxbbxdbxebxczzy", "axbxcxdxe", "axbxcxdxexxx", "a☺b"}
   589  	matches, err := Glob(fsys, "a*")
   590  	if err != nil {
   591  		t.Errorf("Unexpected error %v", err)
   592  		return
   593  	}
   594  
   595  	if len(matches) != len(expected) {
   596  		t.Errorf("Glob returned %#v; expected %#v", matches, expected)
   597  		return
   598  	}
   599  	for idx, match := range matches {
   600  		if match != expected[idx] {
   601  			t.Errorf("Glob returned %#v; expected %#v", matches, expected)
   602  			return
   603  		}
   604  	}
   605  }
   606  
   607  func BenchmarkGlob(b *testing.B) {
   608  	fsys := os.DirFS("test")
   609  	b.ReportAllocs()
   610  	for i := 0; i < b.N; i++ {
   611  		for _, tt := range matchTests {
   612  			if tt.isStandard && tt.testOnDisk {
   613  				Glob(fsys, tt.pattern)
   614  			}
   615  		}
   616  	}
   617  }
   618  
   619  func BenchmarkGlobWalk(b *testing.B) {
   620  	fsys := os.DirFS("test")
   621  	b.ReportAllocs()
   622  	for i := 0; i < b.N; i++ {
   623  		for _, tt := range matchTests {
   624  			if tt.isStandard && tt.testOnDisk {
   625  				GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error {
   626  					return nil
   627  				})
   628  			}
   629  		}
   630  	}
   631  }
   632  
   633  func BenchmarkGoGlob(b *testing.B) {
   634  	fsys := os.DirFS("test")
   635  	b.ReportAllocs()
   636  	for i := 0; i < b.N; i++ {
   637  		for _, tt := range matchTests {
   638  			if tt.isStandard && tt.testOnDisk {
   639  				fs.Glob(fsys, tt.pattern)
   640  			}
   641  		}
   642  	}
   643  }
   644  
   645  func compareErrors(a, b error) bool {
   646  	if a == nil {
   647  		return b == nil
   648  	}
   649  	return b != nil
   650  }
   651  
   652  func inSlice(s string, a []string) bool {
   653  	for _, i := range a {
   654  		if i == s {
   655  			return true
   656  		}
   657  	}
   658  	return false
   659  }
   660  
   661  func compareSlices(a, b []string) bool {
   662  	if len(a) != len(b) {
   663  		return false
   664  	}
   665  
   666  	diff := make(map[string]int, len(a))
   667  
   668  	for _, x := range a {
   669  		diff[x]++
   670  	}
   671  
   672  	for _, y := range b {
   673  		if _, ok := diff[y]; !ok {
   674  			return false
   675  		}
   676  
   677  		diff[y]--
   678  		if diff[y] == 0 {
   679  			delete(diff, y)
   680  		}
   681  	}
   682  
   683  	return len(diff) == 0
   684  }
   685  
   686  func buildNumResults() {
   687  	testLen := len(matchTests)
   688  	numResultsFilesOnly = make([]int, testLen, testLen)
   689  	numResultsNoFollow = make([]int, testLen, testLen)
   690  	numResultsAllOpts = make([]int, testLen, testLen)
   691  
   692  	fsys := os.DirFS("test")
   693  	g := newGlob()
   694  	for idx, tt := range matchTests {
   695  		if tt.testOnDisk {
   696  			filesOnly := 0
   697  			noFollow := 0
   698  			allOpts := 0
   699  			GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error {
   700  				isDir, _ := g.isDir(fsys, "", p, d)
   701  				if !isDir {
   702  					filesOnly++
   703  				}
   704  
   705  				hasNoFollow := (strings.HasPrefix(tt.pattern, "working-symlink") || !strings.Contains(p, "working-symlink/")) && !strings.Contains(p, "/symlink-dir/")
   706  				if hasNoFollow {
   707  					noFollow++
   708  				}
   709  
   710  				if hasNoFollow && (!isDir || p == "working-symlink") {
   711  					allOpts++
   712  				}
   713  
   714  				return nil
   715  			})
   716  
   717  			numResultsFilesOnly[idx] = filesOnly
   718  			numResultsNoFollow[idx] = noFollow
   719  			numResultsAllOpts[idx] = allOpts
   720  		}
   721  	}
   722  }
   723  
   724  func mkdirp(parts ...string) {
   725  	dirs := path.Join(parts...)
   726  	err := os.MkdirAll(dirs, 0755)
   727  	if err != nil {
   728  		log.Fatalf("Could not create test directories %v: %v\n", dirs, err)
   729  	}
   730  }
   731  
   732  func touch(parts ...string) {
   733  	filename := path.Join(parts...)
   734  	f, err := os.Create(filename)
   735  	if err != nil {
   736  		log.Fatalf("Could not create test file %v: %v\n", filename, err)
   737  	}
   738  	f.Close()
   739  }
   740  
   741  func symlink(oldname, newname string) {
   742  	// since this will only run on non-windows, we can assume "/" as path separator
   743  	err := os.Symlink(oldname, newname)
   744  	if err != nil && !os.IsExist(err) {
   745  		log.Fatalf("Could not create symlink %v -> %v: %v\n", oldname, newname, err)
   746  	}
   747  }
   748  
   749  func exists(parts ...string) bool {
   750  	p := path.Join(parts...)
   751  	_, err := os.Lstat(p)
   752  	return err == nil
   753  }
   754  
   755  func TestMain(m *testing.M) {
   756  	// create the test directory
   757  	mkdirp("test", "a", "b", "c")
   758  	mkdirp("test", "a", "c")
   759  	mkdirp("test", "abc")
   760  	mkdirp("test", "axbxcxdxe", "xxx")
   761  	mkdirp("test", "axbxcxdxexxx")
   762  	mkdirp("test", "b")
   763  	mkdirp("test", "e")
   764  
   765  	// create test files
   766  	touch("test", "a", "abc")
   767  	touch("test", "a", "b", "c", "d")
   768  	touch("test", "a", "c", "b")
   769  	touch("test", "abc", "b")
   770  	touch("test", "abcd")
   771  	touch("test", "abcde")
   772  	touch("test", "abxbbxdbxebxczzx")
   773  	touch("test", "abxbbxdbxebxczzy")
   774  	touch("test", "axbxcxdxe", "f")
   775  	touch("test", "axbxcxdxe", "xxx", "f")
   776  	touch("test", "axbxcxdxexxx", "f")
   777  	touch("test", "axbxcxdxexxx", "fff")
   778  	touch("test", "a☺b")
   779  	touch("test", "b", "c")
   780  	touch("test", "c")
   781  	touch("test", "x")
   782  	touch("test", "xxx")
   783  	touch("test", "z")
   784  	touch("test", "α")
   785  	touch("test", "abc", "【test】.txt")
   786  
   787  	touch("test", "e", "[")
   788  	touch("test", "e", "]")
   789  	touch("test", "e", "{")
   790  	touch("test", "e", "}")
   791  	touch("test", "e", "[]")
   792  
   793  	touch("test", "}")
   794  
   795  	if !onWindows {
   796  		// these files/symlinks won't work on Windows
   797  		touch("test", "-")
   798  		touch("test", "]")
   799  		touch("test", "e", "*")
   800  		touch("test", "e", "**")
   801  		touch("test", "e", "****")
   802  		touch("test", "e", "?")
   803  		touch("test", "e", "\\")
   804  
   805  		symlink("../axbxcxdxe/", "test/b/symlink-dir")
   806  		symlink("/tmp/nonexistant-file-20160902155705", "test/broken-symlink")
   807  		symlink("a/b", "test/working-symlink")
   808  
   809  		if !exists("test", "nopermission") {
   810  			mkdirp("test", "nopermission", "dir")
   811  			touch("test", "nopermission", "file")
   812  			os.Chmod(path.Join("test", "nopermission"), 0)
   813  		}
   814  	}
   815  
   816  	// initialize numResultsFilesOnly
   817  	buildNumResults()
   818  
   819  	os.Exit(m.Run())
   820  }