github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/filter/filter_test.go (about)

     1  package filter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/fstest/mockobject"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestNewFilterDefault(t *testing.T) {
    19  	f, err := NewFilter(nil)
    20  	require.NoError(t, err)
    21  	assert.False(t, f.Opt.DeleteExcluded)
    22  	assert.Equal(t, fs.SizeSuffix(-1), f.Opt.MinSize)
    23  	assert.Equal(t, fs.SizeSuffix(-1), f.Opt.MaxSize)
    24  	assert.Len(t, f.fileRules.rules, 0)
    25  	assert.Len(t, f.dirRules.rules, 0)
    26  	assert.Len(t, f.metaRules.rules, 0)
    27  	assert.Nil(t, f.files)
    28  	assert.True(t, f.InActive())
    29  }
    30  
    31  // testFile creates a temp file with the contents
    32  func testFile(t *testing.T, contents string) string {
    33  	out, err := os.CreateTemp("", "filter_test")
    34  	require.NoError(t, err)
    35  	defer func() {
    36  		err := out.Close()
    37  		require.NoError(t, err)
    38  	}()
    39  	_, err = out.Write([]byte(contents))
    40  	require.NoError(t, err)
    41  	s := out.Name()
    42  	return s
    43  }
    44  
    45  func TestNewFilterForbiddenMixOfFilesFromAndFilterRule(t *testing.T) {
    46  	Opt := DefaultOpt
    47  
    48  	// Set up the input
    49  	Opt.FilterRule = []string{"- filter1", "- filter1b"}
    50  	Opt.FilesFrom = []string{testFile(t, "#comment\nfiles1\nfiles2\n")}
    51  
    52  	rm := func(p string) {
    53  		err := os.Remove(p)
    54  		if err != nil {
    55  			t.Logf("error removing %q: %v", p, err)
    56  		}
    57  	}
    58  	// Reset the input
    59  	defer func() {
    60  		rm(Opt.FilesFrom[0])
    61  	}()
    62  
    63  	_, err := NewFilter(&Opt)
    64  	require.Error(t, err)
    65  	require.Contains(t, err.Error(), "the usage of --files-from overrides all other filters")
    66  }
    67  
    68  func TestNewFilterForbiddenMixOfFilesFromRawAndFilterRule(t *testing.T) {
    69  	Opt := DefaultOpt
    70  
    71  	// Set up the input
    72  	Opt.FilterRule = []string{"- filter1", "- filter1b"}
    73  	Opt.FilesFromRaw = []string{testFile(t, "#comment\nfiles1\nfiles2\n")}
    74  
    75  	rm := func(p string) {
    76  		err := os.Remove(p)
    77  		if err != nil {
    78  			t.Logf("error removing %q: %v", p, err)
    79  		}
    80  	}
    81  	// Reset the input
    82  	defer func() {
    83  		rm(Opt.FilesFromRaw[0])
    84  	}()
    85  
    86  	_, err := NewFilter(&Opt)
    87  	require.Error(t, err)
    88  	require.Contains(t, err.Error(), "the usage of --files-from-raw overrides all other filters")
    89  }
    90  
    91  func TestNewFilterWithFilesFromAlone(t *testing.T) {
    92  	Opt := DefaultOpt
    93  
    94  	// Set up the input
    95  	Opt.FilesFrom = []string{testFile(t, "#comment\nfiles1\nfiles2\n")}
    96  
    97  	rm := func(p string) {
    98  		err := os.Remove(p)
    99  		if err != nil {
   100  			t.Logf("error removing %q: %v", p, err)
   101  		}
   102  	}
   103  	// Reset the input
   104  	defer func() {
   105  		rm(Opt.FilesFrom[0])
   106  	}()
   107  
   108  	f, err := NewFilter(&Opt)
   109  	require.NoError(t, err)
   110  	assert.Len(t, f.files, 2)
   111  	for _, name := range []string{"files1", "files2"} {
   112  		_, ok := f.files[name]
   113  		if !ok {
   114  			t.Errorf("Didn't find file %q in f.files", name)
   115  		}
   116  	}
   117  }
   118  
   119  func TestNewFilterWithFilesFromRaw(t *testing.T) {
   120  	Opt := DefaultOpt
   121  
   122  	// Set up the input
   123  	Opt.FilesFromRaw = []string{testFile(t, "#comment\nfiles1\nfiles2\n")}
   124  
   125  	rm := func(p string) {
   126  		err := os.Remove(p)
   127  		if err != nil {
   128  			t.Logf("error removing %q: %v", p, err)
   129  		}
   130  	}
   131  	// Reset the input
   132  	defer func() {
   133  		rm(Opt.FilesFromRaw[0])
   134  	}()
   135  
   136  	f, err := NewFilter(&Opt)
   137  	require.NoError(t, err)
   138  	assert.Len(t, f.files, 3)
   139  	for _, name := range []string{"#comment", "files1", "files2"} {
   140  		_, ok := f.files[name]
   141  		if !ok {
   142  			t.Errorf("Didn't find file %q in f.files", name)
   143  		}
   144  	}
   145  }
   146  
   147  func TestNewFilterFullExceptFilesFromOpt(t *testing.T) {
   148  	Opt := DefaultOpt
   149  
   150  	mins := fs.SizeSuffix(100 * 1024)
   151  	maxs := fs.SizeSuffix(1000 * 1024)
   152  
   153  	// Set up the input
   154  	Opt.DeleteExcluded = true
   155  	Opt.FilterRule = []string{"- filter1", "- filter1b"}
   156  	Opt.FilterFrom = []string{testFile(t, "#comment\n+ filter2\n- filter3\n")}
   157  	Opt.ExcludeRule = []string{"exclude1"}
   158  	Opt.ExcludeFrom = []string{testFile(t, "#comment\nexclude2\nexclude3\n")}
   159  	Opt.IncludeRule = []string{"include1"}
   160  	Opt.IncludeFrom = []string{testFile(t, "#comment\ninclude2\ninclude3\n")}
   161  	Opt.MinSize = mins
   162  	Opt.MaxSize = maxs
   163  
   164  	rm := func(p string) {
   165  		err := os.Remove(p)
   166  		if err != nil {
   167  			t.Logf("error removing %q: %v", p, err)
   168  		}
   169  	}
   170  	// Reset the input
   171  	defer func() {
   172  		rm(Opt.FilterFrom[0])
   173  		rm(Opt.ExcludeFrom[0])
   174  		rm(Opt.IncludeFrom[0])
   175  	}()
   176  
   177  	f, err := NewFilter(&Opt)
   178  	require.NoError(t, err)
   179  	assert.True(t, f.Opt.DeleteExcluded)
   180  	assert.Equal(t, f.Opt.MinSize, mins)
   181  	assert.Equal(t, f.Opt.MaxSize, maxs)
   182  	got := f.DumpFilters()
   183  	want := `--- File filter rules ---
   184  + (^|/)include1$
   185  + (^|/)include2$
   186  + (^|/)include3$
   187  - (^|/)exclude1$
   188  - (^|/)exclude2$
   189  - (^|/)exclude3$
   190  - (^|/)filter1$
   191  - (^|/)filter1b$
   192  + (^|/)filter2$
   193  - (^|/)filter3$
   194  - ^.*$
   195  --- Directory filter rules ---
   196  + ^.*$
   197  - ^.*$`
   198  	assert.Equal(t, want, got)
   199  	assert.False(t, f.InActive())
   200  }
   201  
   202  type includeTest struct {
   203  	in      string
   204  	size    int64
   205  	modTime int64
   206  	want    bool
   207  }
   208  
   209  func testInclude(t *testing.T, f *Filter, tests []includeTest) {
   210  	for _, test := range tests {
   211  		got := f.Include(test.in, test.size, time.Unix(test.modTime, 0), nil)
   212  		assert.Equal(t, test.want, got, fmt.Sprintf("in=%q, size=%v, modTime=%v", test.in, test.size, time.Unix(test.modTime, 0)))
   213  	}
   214  }
   215  
   216  type includeDirTest struct {
   217  	in   string
   218  	want bool
   219  }
   220  
   221  func testDirInclude(t *testing.T, f *Filter, tests []includeDirTest) {
   222  	for _, test := range tests {
   223  		got, err := f.IncludeDirectory(context.Background(), nil)(test.in)
   224  		require.NoError(t, err)
   225  		assert.Equal(t, test.want, got, test.in)
   226  	}
   227  }
   228  
   229  func TestNewFilterIncludeFiles(t *testing.T) {
   230  	f, err := NewFilter(nil)
   231  	require.NoError(t, err)
   232  	err = f.AddFile("file1.jpg")
   233  	require.NoError(t, err)
   234  	err = f.AddFile("/file2.jpg")
   235  	require.NoError(t, err)
   236  	assert.Equal(t, FilesMap{
   237  		"file1.jpg": {},
   238  		"file2.jpg": {},
   239  	}, f.files)
   240  	assert.Equal(t, FilesMap{}, f.dirs)
   241  	testInclude(t, f, []includeTest{
   242  		{"file1.jpg", 0, 0, true},
   243  		{"file2.jpg", 1, 0, true},
   244  		{"potato/file2.jpg", 2, 0, false},
   245  		{"file3.jpg", 3, 0, false},
   246  	})
   247  	assert.False(t, f.InActive())
   248  }
   249  
   250  func TestNewFilterIncludeFilesDirs(t *testing.T) {
   251  	f, err := NewFilter(nil)
   252  	require.NoError(t, err)
   253  	for _, path := range []string{
   254  		"path/to/dir/file1.png",
   255  		"/path/to/dir/file2.png",
   256  		"/path/to/file3.png",
   257  		"/path/to/dir2/file4.png",
   258  	} {
   259  		err = f.AddFile(path)
   260  		require.NoError(t, err)
   261  	}
   262  	assert.Equal(t, FilesMap{
   263  		"path":         {},
   264  		"path/to":      {},
   265  		"path/to/dir":  {},
   266  		"path/to/dir2": {},
   267  	}, f.dirs)
   268  	testDirInclude(t, f, []includeDirTest{
   269  		{"path", true},
   270  		{"path/to", true},
   271  		{"path/to/", true},
   272  		{"/path/to", true},
   273  		{"/path/to/", true},
   274  		{"path/to/dir", true},
   275  		{"path/to/dir2", true},
   276  		{"path/too", false},
   277  		{"path/three", false},
   278  		{"four", false},
   279  	})
   280  }
   281  
   282  func TestNewFilterHaveFilesFrom(t *testing.T) {
   283  	f, err := NewFilter(nil)
   284  	require.NoError(t, err)
   285  
   286  	assert.Equal(t, false, f.HaveFilesFrom())
   287  
   288  	require.NoError(t, f.AddFile("file"))
   289  
   290  	assert.Equal(t, true, f.HaveFilesFrom())
   291  }
   292  
   293  func TestNewFilterMakeListR(t *testing.T) {
   294  	f, err := NewFilter(nil)
   295  	require.NoError(t, err)
   296  
   297  	// Check error if no files
   298  	listR := f.MakeListR(context.Background(), nil)
   299  	err = listR(context.Background(), "", nil)
   300  	assert.EqualError(t, err, errFilesFromNotSet.Error())
   301  
   302  	// Add some files
   303  	for _, path := range []string{
   304  		"path/to/dir/file1.png",
   305  		"/path/to/dir/file2.png",
   306  		"/path/to/file3.png",
   307  		"/path/to/dir2/file4.png",
   308  		"notfound",
   309  	} {
   310  		err = f.AddFile(path)
   311  		require.NoError(t, err)
   312  	}
   313  
   314  	assert.Equal(t, 5, len(f.files))
   315  
   316  	// NewObject function for MakeListR
   317  	newObjects := FilesMap{}
   318  	var newObjectMu sync.Mutex
   319  	NewObject := func(ctx context.Context, remote string) (fs.Object, error) {
   320  		newObjectMu.Lock()
   321  		defer newObjectMu.Unlock()
   322  		if remote == "notfound" {
   323  			return nil, fs.ErrorObjectNotFound
   324  		} else if remote == "error" {
   325  			return nil, assert.AnError
   326  		}
   327  		newObjects[remote] = struct{}{}
   328  		return mockobject.New(remote), nil
   329  
   330  	}
   331  
   332  	// Callback for ListRFn
   333  	listRObjects := FilesMap{}
   334  	var callbackMu sync.Mutex
   335  	listRcallback := func(entries fs.DirEntries) error {
   336  		callbackMu.Lock()
   337  		defer callbackMu.Unlock()
   338  		for _, entry := range entries {
   339  			listRObjects[entry.Remote()] = struct{}{}
   340  		}
   341  		return nil
   342  	}
   343  
   344  	// Make the listR and call it
   345  	listR = f.MakeListR(context.Background(), NewObject)
   346  	err = listR(context.Background(), "", listRcallback)
   347  	require.NoError(t, err)
   348  
   349  	// Check that the correct objects were created and listed
   350  	want := FilesMap{
   351  		"path/to/dir/file1.png":  {},
   352  		"path/to/dir/file2.png":  {},
   353  		"path/to/file3.png":      {},
   354  		"path/to/dir2/file4.png": {},
   355  	}
   356  	assert.Equal(t, want, newObjects)
   357  	assert.Equal(t, want, listRObjects)
   358  
   359  	// Now check an error is returned from NewObject
   360  	require.NoError(t, f.AddFile("error"))
   361  	err = listR(context.Background(), "", listRcallback)
   362  	require.EqualError(t, err, assert.AnError.Error())
   363  
   364  	// The checker will exit by the error above
   365  	ci := fs.GetConfig(context.Background())
   366  	ci.Checkers = 1
   367  
   368  	// Now check an error is returned from NewObject
   369  	require.NoError(t, f.AddFile("error"))
   370  	err = listR(context.Background(), "", listRcallback)
   371  	require.EqualError(t, err, assert.AnError.Error())
   372  }
   373  
   374  func TestNewFilterMinSize(t *testing.T) {
   375  	f, err := NewFilter(nil)
   376  	require.NoError(t, err)
   377  	f.Opt.MinSize = 100
   378  	testInclude(t, f, []includeTest{
   379  		{"file1.jpg", 100, 0, true},
   380  		{"file2.jpg", 101, 0, true},
   381  		{"potato/file2.jpg", 99, 0, false},
   382  	})
   383  	assert.False(t, f.InActive())
   384  }
   385  
   386  func TestNewFilterMaxSize(t *testing.T) {
   387  	f, err := NewFilter(nil)
   388  	require.NoError(t, err)
   389  	f.Opt.MaxSize = 100
   390  	testInclude(t, f, []includeTest{
   391  		{"file1.jpg", 100, 0, true},
   392  		{"file2.jpg", 101, 0, false},
   393  		{"potato/file2.jpg", 99, 0, true},
   394  	})
   395  	assert.False(t, f.InActive())
   396  }
   397  
   398  func TestNewFilterMinAndMaxAge(t *testing.T) {
   399  	f, err := NewFilter(nil)
   400  	require.NoError(t, err)
   401  	f.ModTimeFrom = time.Unix(1440000002, 0)
   402  	f.ModTimeTo = time.Unix(1440000003, 0)
   403  	testInclude(t, f, []includeTest{
   404  		{"file1.jpg", 100, 1440000000, false},
   405  		{"file2.jpg", 101, 1440000001, false},
   406  		{"file3.jpg", 102, 1440000002, true},
   407  		{"potato/file1.jpg", 98, 1440000003, true},
   408  		{"potato/file2.jpg", 99, 1440000004, false},
   409  	})
   410  	assert.False(t, f.InActive())
   411  }
   412  
   413  func TestNewFilterMinAge(t *testing.T) {
   414  	f, err := NewFilter(nil)
   415  	require.NoError(t, err)
   416  	f.ModTimeTo = time.Unix(1440000002, 0)
   417  	testInclude(t, f, []includeTest{
   418  		{"file1.jpg", 100, 1440000000, true},
   419  		{"file2.jpg", 101, 1440000001, true},
   420  		{"file3.jpg", 102, 1440000002, true},
   421  		{"potato/file1.jpg", 98, 1440000003, false},
   422  		{"potato/file2.jpg", 99, 1440000004, false},
   423  	})
   424  	assert.False(t, f.InActive())
   425  }
   426  
   427  func TestNewFilterMaxAge(t *testing.T) {
   428  	f, err := NewFilter(nil)
   429  	require.NoError(t, err)
   430  	f.ModTimeFrom = time.Unix(1440000002, 0)
   431  	testInclude(t, f, []includeTest{
   432  		{"file1.jpg", 100, 1440000000, false},
   433  		{"file2.jpg", 101, 1440000001, false},
   434  		{"file3.jpg", 102, 1440000002, true},
   435  		{"potato/file1.jpg", 98, 1440000003, true},
   436  		{"potato/file2.jpg", 99, 1440000004, true},
   437  	})
   438  	assert.False(t, f.InActive())
   439  }
   440  
   441  func TestNewFilterMatches(t *testing.T) {
   442  	f, err := NewFilter(nil)
   443  	require.NoError(t, err)
   444  	add := func(s string) {
   445  		err := f.AddRule(s)
   446  		require.NoError(t, err)
   447  	}
   448  	add("+ cleared")
   449  	add("!")
   450  	add("- /file1.jpg")
   451  	add("+ /file2.png")
   452  	add("+ /*.jpg")
   453  	add("- /*.png")
   454  	add("- /potato")
   455  	add("+ /sausage1")
   456  	add("+ /sausage2*")
   457  	add("+ /sausage3**")
   458  	add("+ /a/*.jpg")
   459  	add("- *")
   460  	testInclude(t, f, []includeTest{
   461  		{"cleared", 100, 0, false},
   462  		{"file1.jpg", 100, 0, false},
   463  		{"file2.png", 100, 0, true},
   464  		{"FILE2.png", 100, 0, false},
   465  		{"afile2.png", 100, 0, false},
   466  		{"file3.jpg", 101, 0, true},
   467  		{"file4.png", 101, 0, false},
   468  		{"potato", 101, 0, false},
   469  		{"sausage1", 101, 0, true},
   470  		{"sausage1/potato", 101, 0, false},
   471  		{"sausage2potato", 101, 0, true},
   472  		{"sausage2/potato", 101, 0, false},
   473  		{"sausage3/potato", 101, 0, true},
   474  		{"a/one.jpg", 101, 0, true},
   475  		{"a/one.png", 101, 0, false},
   476  		{"unicorn", 99, 0, false},
   477  	})
   478  	testDirInclude(t, f, []includeDirTest{
   479  		{"sausage1", false},
   480  		{"sausage2", false},
   481  		{"sausage2/sub", false},
   482  		{"sausage2/sub/dir", false},
   483  		{"sausage3", true},
   484  		{"SAUSAGE3", false},
   485  		{"sausage3/sub", true},
   486  		{"sausage3/sub/dir", true},
   487  		{"sausage4", false},
   488  		{"a", true},
   489  	})
   490  	assert.False(t, f.InActive())
   491  }
   492  
   493  func TestNewFilterMatchesIgnoreCase(t *testing.T) {
   494  	f, err := NewFilter(nil)
   495  	require.NoError(t, err)
   496  	f.Opt.IgnoreCase = true
   497  	add := func(s string) {
   498  		err := f.AddRule(s)
   499  		require.NoError(t, err)
   500  	}
   501  	add("+ /file2.png")
   502  	add("+ /sausage3**")
   503  	add("- *")
   504  	testInclude(t, f, []includeTest{
   505  		{"file2.png", 100, 0, true},
   506  		{"FILE2.png", 100, 0, true},
   507  	})
   508  	testDirInclude(t, f, []includeDirTest{
   509  		{"sausage3", true},
   510  		{"SAUSAGE3", true},
   511  	})
   512  	assert.False(t, f.InActive())
   513  }
   514  
   515  func TestNewFilterMatchesRegexp(t *testing.T) {
   516  	f, err := NewFilter(nil)
   517  	require.NoError(t, err)
   518  	add := func(s string) {
   519  		err := f.AddRule(s)
   520  		require.NoError(t, err)
   521  	}
   522  	add(`+ /{{file\d+\.png}}`)
   523  	add(`+ *.{{(?i)jpg}}`)
   524  	add(`- *`)
   525  	testInclude(t, f, []includeTest{
   526  		{"file2.png", 100, 0, true},
   527  		{"sub/file2.png", 100, 0, false},
   528  		{"file123.png", 100, 0, true},
   529  		{"File123.png", 100, 0, false},
   530  		{"something.jpg", 100, 0, true},
   531  		{"deep/path/something.JPG", 100, 0, true},
   532  		{"something.gif", 100, 0, false},
   533  	})
   534  	testDirInclude(t, f, []includeDirTest{
   535  		{"anything at all", true},
   536  	})
   537  	assert.False(t, f.InActive())
   538  }
   539  
   540  type includeTestMetadata struct {
   541  	in       string
   542  	metadata fs.Metadata
   543  	want     bool
   544  }
   545  
   546  func testIncludeMetadata(t *testing.T, f *Filter, tests []includeTestMetadata) {
   547  	for _, test := range tests {
   548  		got := f.Include(test.in, 0, time.Time{}, test.metadata)
   549  		assert.Equal(t, test.want, got, fmt.Sprintf("in=%q, metadata=%+v", test.in, test.metadata))
   550  	}
   551  }
   552  
   553  func TestNewFilterMetadataInclude(t *testing.T) {
   554  	f, err := NewFilter(nil)
   555  	require.NoError(t, err)
   556  	add := func(s string) {
   557  		err := f.metaRules.AddRule(s)
   558  		require.NoError(t, err)
   559  	}
   560  	add(`+ t*=t*`)
   561  	add(`- *`)
   562  	testIncludeMetadata(t, f, []includeTestMetadata{
   563  		{"nil", nil, false},
   564  		{"empty", fs.Metadata{}, false},
   565  		{"ok1", fs.Metadata{"thing": "thang"}, true},
   566  		{"ok2", fs.Metadata{"thing1": "thang1"}, true},
   567  		{"missing", fs.Metadata{"Thing1": "Thang1"}, false},
   568  	})
   569  	assert.False(t, f.InActive())
   570  }
   571  
   572  func TestNewFilterMetadataExclude(t *testing.T) {
   573  	f, err := NewFilter(nil)
   574  	require.NoError(t, err)
   575  	add := func(s string) {
   576  		err := f.metaRules.AddRule(s)
   577  		require.NoError(t, err)
   578  	}
   579  	add(`- thing=thang`)
   580  	add(`+ *`)
   581  	testIncludeMetadata(t, f, []includeTestMetadata{
   582  		{"nil", nil, true},
   583  		{"empty", fs.Metadata{}, true},
   584  		{"ok1", fs.Metadata{"thing": "thang"}, false},
   585  		{"missing1", fs.Metadata{"thing1": "thang1"}, true},
   586  	})
   587  	assert.False(t, f.InActive())
   588  }
   589  
   590  func TestFilterAddDirRuleOrFileRule(t *testing.T) {
   591  	for _, test := range []struct {
   592  		included bool
   593  		glob     string
   594  		want     string
   595  	}{
   596  		{
   597  			false,
   598  			"potato",
   599  			`--- File filter rules ---
   600  - (^|/)potato$
   601  --- Directory filter rules ---`,
   602  		},
   603  		{
   604  			true,
   605  			"potato",
   606  			`--- File filter rules ---
   607  + (^|/)potato$
   608  --- Directory filter rules ---
   609  + ^.*$`,
   610  		},
   611  		{
   612  			false,
   613  			"potato/",
   614  			`--- File filter rules ---
   615  - (^|/)potato/.*$
   616  --- Directory filter rules ---
   617  - (^|/)potato/.*$`,
   618  		},
   619  		{
   620  			true,
   621  			"potato/",
   622  			`--- File filter rules ---
   623  --- Directory filter rules ---
   624  + (^|/)potato/$`,
   625  		},
   626  		{
   627  			false,
   628  			"*",
   629  			`--- File filter rules ---
   630  - (^|/)[^/]*$
   631  --- Directory filter rules ---
   632  - ^.*$`,
   633  		},
   634  		{
   635  			true,
   636  			"*",
   637  			`--- File filter rules ---
   638  + (^|/)[^/]*$
   639  --- Directory filter rules ---
   640  + ^.*$`,
   641  		},
   642  		{
   643  			false,
   644  			".*{,/**}",
   645  			`--- File filter rules ---
   646  - (^|/)\.[^/]*(|/.*)$
   647  --- Directory filter rules ---
   648  - (^|/)\.[^/]*(|/.*)$`,
   649  		},
   650  		{
   651  			true,
   652  			"a/b/c/d",
   653  			`--- File filter rules ---
   654  + (^|/)a/b/c/d$
   655  --- Directory filter rules ---
   656  + (^|/)a/b/c/$
   657  + (^|/)a/b/$
   658  + (^|/)a/$`,
   659  		},
   660  	} {
   661  		f, err := NewFilter(nil)
   662  		require.NoError(t, err)
   663  		err = f.Add(test.included, test.glob)
   664  		require.NoError(t, err)
   665  		got := f.DumpFilters()
   666  		assert.Equal(t, test.want, got, fmt.Sprintf("Add(%v, %q)", test.included, test.glob))
   667  	}
   668  }
   669  
   670  func testFilterForEachLine(t *testing.T, useStdin, raw bool) {
   671  	file := testFile(t, `; comment
   672  one
   673  # another comment
   674  
   675  
   676  two
   677   # indented comment
   678  three  
   679  four    
   680  five
   681    six  `)
   682  	defer func() {
   683  		err := os.Remove(file)
   684  		require.NoError(t, err)
   685  	}()
   686  	lines := []string{}
   687  	fileName := file
   688  	if useStdin {
   689  		in, err := os.Open(file)
   690  		require.NoError(t, err)
   691  		oldStdin := os.Stdin
   692  		os.Stdin = in
   693  		defer func() {
   694  			os.Stdin = oldStdin
   695  			_ = in.Close()
   696  		}()
   697  		fileName = "-"
   698  	}
   699  	err := forEachLine(fileName, raw, func(s string) error {
   700  		lines = append(lines, s)
   701  		return nil
   702  	})
   703  	require.NoError(t, err)
   704  	if raw {
   705  		assert.Equal(t, "; comment,one,# another comment,,,two, # indented comment,three  ,four    ,five,  six  ",
   706  			strings.Join(lines, ","))
   707  	} else {
   708  		assert.Equal(t, "one,two,three,four,five,six", strings.Join(lines, ","))
   709  	}
   710  }
   711  
   712  func TestFilterForEachLine(t *testing.T) {
   713  	testFilterForEachLine(t, false, false)
   714  }
   715  
   716  func TestFilterForEachLineStdin(t *testing.T) {
   717  	testFilterForEachLine(t, true, false)
   718  }
   719  
   720  func TestFilterForEachLineWithRaw(t *testing.T) {
   721  	testFilterForEachLine(t, false, true)
   722  }
   723  
   724  func TestFilterForEachLineStdinWithRaw(t *testing.T) {
   725  	testFilterForEachLine(t, true, true)
   726  }
   727  
   728  func TestFilterMatchesFromDocs(t *testing.T) {
   729  	for _, test := range []struct {
   730  		glob       string
   731  		included   bool
   732  		file       string
   733  		ignoreCase bool
   734  	}{
   735  		{"file.jpg", true, "file.jpg", false},
   736  		{"file.jpg", true, "directory/file.jpg", false},
   737  		{"file.jpg", false, "afile.jpg", false},
   738  		{"file.jpg", false, "directory/afile.jpg", false},
   739  		{"/file.jpg", true, "file.jpg", false},
   740  		{"/file.jpg", false, "afile.jpg", false},
   741  		{"/file.jpg", false, "directory/file.jpg", false},
   742  		{"*.jpg", true, "file.jpg", false},
   743  		{"*.jpg", true, "directory/file.jpg", false},
   744  		{"*.jpg", false, "file.jpg/anotherfile.png", false},
   745  		{"dir/**", true, "dir/file.jpg", false},
   746  		{"dir/**", true, "dir/dir1/dir2/file.jpg", false},
   747  		{"dir/**", false, "directory/file.jpg", false},
   748  		{"dir/**", false, "adir/file.jpg", false},
   749  		{"l?ss", true, "less", false},
   750  		{"l?ss", true, "lass", false},
   751  		{"l?ss", false, "floss", false},
   752  		{"h[ae]llo", true, "hello", false},
   753  		{"h[ae]llo", true, "hallo", false},
   754  		{"h[ae]llo", false, "hullo", false},
   755  		{"{one,two}_potato", true, "one_potato", false},
   756  		{"{one,two}_potato", true, "two_potato", false},
   757  		{"{one,two}_potato", false, "three_potato", false},
   758  		{"{one,two}_potato", false, "_potato", false},
   759  		{"\\*.jpg", true, "*.jpg", false},
   760  		{"\\\\.jpg", true, "\\.jpg", false},
   761  		{"\\[one\\].jpg", true, "[one].jpg", false},
   762  		{"potato", true, "potato", false},
   763  		{"potato", false, "POTATO", false},
   764  		{"potato", true, "potato", true},
   765  		{"potato", true, "POTATO", true},
   766  	} {
   767  		f, err := NewFilter(nil)
   768  		require.NoError(t, err)
   769  		if test.ignoreCase {
   770  			f.Opt.IgnoreCase = true
   771  		}
   772  		err = f.Add(true, test.glob)
   773  		require.NoError(t, err)
   774  		err = f.Add(false, "*")
   775  		require.NoError(t, err)
   776  		included := f.Include(test.file, 0, time.Unix(0, 0), nil)
   777  		if included != test.included {
   778  			t.Errorf("%q match %q: want %v got %v", test.glob, test.file, test.included, included)
   779  		}
   780  	}
   781  }
   782  
   783  func TestNewFilterUsesDirectoryFilters(t *testing.T) {
   784  	for i, test := range []struct {
   785  		rules []string
   786  		want  bool
   787  	}{
   788  		{
   789  			rules: []string{},
   790  			want:  false,
   791  		},
   792  		{
   793  			rules: []string{
   794  				"+ *",
   795  			},
   796  			want: false,
   797  		},
   798  		{
   799  			rules: []string{
   800  				"+ *.jpg",
   801  				"- *",
   802  			},
   803  			want: false,
   804  		},
   805  		{
   806  			rules: []string{
   807  				"- *.jpg",
   808  			},
   809  			want: false,
   810  		},
   811  		{
   812  			rules: []string{
   813  				"- *.jpg",
   814  				"+ *",
   815  			},
   816  			want: false,
   817  		},
   818  		{
   819  			rules: []string{
   820  				"+ dir/*.jpg",
   821  				"- *",
   822  			},
   823  			want: true,
   824  		},
   825  		{
   826  			rules: []string{
   827  				"+ dir/**",
   828  			},
   829  			want: true,
   830  		},
   831  		{
   832  			rules: []string{
   833  				"- dir/**",
   834  			},
   835  			want: true,
   836  		},
   837  		{
   838  			rules: []string{
   839  				"- /dir/**",
   840  			},
   841  			want: true,
   842  		},
   843  	} {
   844  		what := fmt.Sprintf("#%d", i)
   845  		f, err := NewFilter(nil)
   846  		require.NoError(t, err)
   847  		for _, rule := range test.rules {
   848  			err := f.AddRule(rule)
   849  			require.NoError(t, err, what)
   850  		}
   851  		got := f.UsesDirectoryFilters()
   852  		assert.Equal(t, test.want, got, fmt.Sprintf("%s: %s", what, f.DumpFilters()))
   853  	}
   854  }
   855  
   856  func TestGetConfig(t *testing.T) {
   857  	ctx := context.Background()
   858  
   859  	// Check nil
   860  	//lint:ignore SA1012 false positive when running staticcheck, we want to test passing a nil Context and therefore ignore lint suggestion to use context.TODO
   861  	//nolint:staticcheck // Don't include staticcheck when running golangci-lint to avoid SA1012
   862  	config := GetConfig(nil)
   863  	assert.Equal(t, globalConfig, config)
   864  
   865  	// Check empty config
   866  	config = GetConfig(ctx)
   867  	assert.Equal(t, globalConfig, config)
   868  
   869  	// Check adding a config
   870  	ctx2, config2 := AddConfig(ctx)
   871  	require.NoError(t, config2.AddRule("+ *.jpg"))
   872  	assert.NotEqual(t, config2, config)
   873  
   874  	// Check can get config back
   875  	config2ctx := GetConfig(ctx2)
   876  	assert.Equal(t, config2, config2ctx)
   877  
   878  	// Check ReplaceConfig
   879  	f, err := NewFilter(nil)
   880  	require.NoError(t, err)
   881  	ctx3 := ReplaceConfig(ctx, f)
   882  	assert.Equal(t, globalConfig, GetConfig(ctx3))
   883  }