package archiver import ( "context" "os" "path/filepath" "runtime" "testing" "github.com/google/go-cmp/cmp" "github.com/restic/restic/internal/fs" rtest "github.com/restic/restic/internal/test" ) func TestScanner(t *testing.T) { var tests = []struct { name string src TestDir want map[string]ScanStats selFn SelectFunc }{ { name: "include-all", src: TestDir{ "other": TestFile{Content: "another file"}, "work": TestDir{ "foo": TestFile{Content: "foo"}, "foo.txt": TestFile{Content: "foo text file"}, "subdir": TestDir{ "other": TestFile{Content: "other in subdir"}, "bar.txt": TestFile{Content: "bar.txt in subdir"}, }, }, }, want: map[string]ScanStats{ filepath.FromSlash("other"): {Files: 1, Bytes: 12}, filepath.FromSlash("work/foo"): {Files: 2, Bytes: 15}, filepath.FromSlash("work/foo.txt"): {Files: 3, Bytes: 28}, filepath.FromSlash("work/subdir/bar.txt"): {Files: 4, Bytes: 45}, filepath.FromSlash("work/subdir/other"): {Files: 5, Bytes: 60}, filepath.FromSlash("work/subdir"): {Files: 5, Dirs: 1, Bytes: 60}, filepath.FromSlash("work"): {Files: 5, Dirs: 2, Bytes: 60}, filepath.FromSlash(""): {Files: 5, Dirs: 2, Bytes: 60}, }, }, { name: "select-txt", src: TestDir{ "other": TestFile{Content: "another file"}, "work": TestDir{ "foo": TestFile{Content: "foo"}, "foo.txt": TestFile{Content: "foo text file"}, "subdir": TestDir{ "other": TestFile{Content: "other in subdir"}, "bar.txt": TestFile{Content: "bar.txt in subdir"}, }, }, }, selFn: func(item string, fi os.FileInfo, fs fs.FS) bool { if fi.IsDir() { return true } if filepath.Ext(item) == ".txt" { return true } return false }, want: map[string]ScanStats{ filepath.FromSlash("work/foo.txt"): {Files: 1, Bytes: 13}, filepath.FromSlash("work/subdir/bar.txt"): {Files: 2, Bytes: 30}, filepath.FromSlash("work/subdir"): {Files: 2, Dirs: 1, Bytes: 30}, filepath.FromSlash("work"): {Files: 2, Dirs: 2, Bytes: 30}, filepath.FromSlash(""): {Files: 2, Dirs: 2, Bytes: 30}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() tempdir := rtest.TempDir(t) TestCreateFiles(t, tempdir, test.src) back := rtest.Chdir(t, tempdir) defer back() cur, err := os.Getwd() if err != nil { t.Fatal(err) } sc := NewScanner(fs.Track{FS: fs.Local{}}) if test.selFn != nil { sc.Select = test.selFn } results := make(map[string]ScanStats) sc.Result = func(item string, s ScanStats) { var p string var err error if item != "" { p, err = filepath.Rel(cur, item) if err != nil { panic(err) } } results[p] = s } err = sc.Scan(ctx, []string{"."}) if err != nil { t.Fatal(err) } if !cmp.Equal(test.want, results) { t.Error(cmp.Diff(test.want, results)) } }) } } func TestScannerError(t *testing.T) { var tests = []struct { name string unix bool src TestDir result ScanStats selFn SelectFunc errFn func(t testing.TB, item string, err error) error resFn func(t testing.TB, item string, s ScanStats) prepare func(t testing.TB) }{ { name: "no-error", src: TestDir{ "other": TestFile{Content: "another file"}, "work": TestDir{ "foo": TestFile{Content: "foo"}, "foo.txt": TestFile{Content: "foo text file"}, "subdir": TestDir{ "other": TestFile{Content: "other in subdir"}, "bar.txt": TestFile{Content: "bar.txt in subdir"}, }, }, }, result: ScanStats{Files: 5, Dirs: 2, Bytes: 60}, }, { name: "unreadable-dir", unix: true, src: TestDir{ "other": TestFile{Content: "another file"}, "work": TestDir{ "foo": TestFile{Content: "foo"}, "foo.txt": TestFile{Content: "foo text file"}, "subdir": TestDir{ "other": TestFile{Content: "other in subdir"}, "bar.txt": TestFile{Content: "bar.txt in subdir"}, }, }, }, result: ScanStats{Files: 3, Dirs: 1, Bytes: 28}, prepare: func(t testing.TB) { err := os.Chmod(filepath.Join("work", "subdir"), 0000) if err != nil { t.Fatal(err) } }, errFn: func(t testing.TB, item string, err error) error { if item == filepath.FromSlash("work/subdir") { return nil } return err }, }, { name: "removed-item", src: TestDir{ "bar": TestFile{Content: "bar"}, "baz": TestFile{Content: "baz"}, "foo": TestFile{Content: "foo"}, "other": TestFile{Content: "other"}, }, result: ScanStats{Files: 3, Dirs: 0, Bytes: 11}, resFn: func(t testing.TB, item string, s ScanStats) { if item == "bar" { err := os.Remove("foo") if err != nil { t.Fatal(err) } } }, errFn: func(t testing.TB, item string, err error) error { if item == "foo" { t.Logf("ignoring error for %v: %v", item, err) return nil } return err }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.unix && runtime.GOOS == "windows" { t.Skipf("skip on windows") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() tempdir := rtest.TempDir(t) TestCreateFiles(t, tempdir, test.src) back := rtest.Chdir(t, tempdir) defer back() cur, err := os.Getwd() if err != nil { t.Fatal(err) } if test.prepare != nil { test.prepare(t) } sc := NewScanner(fs.Track{FS: fs.Local{}}) if test.selFn != nil { sc.Select = test.selFn } var stats ScanStats sc.Result = func(item string, s ScanStats) { if item == "" { stats = s return } if test.resFn != nil { p, relErr := filepath.Rel(cur, item) if relErr != nil { panic(relErr) } test.resFn(t, p, s) } } if test.errFn != nil { sc.Error = func(item string, err error) error { p, relErr := filepath.Rel(cur, item) if relErr != nil { panic(relErr) } return test.errFn(t, p, err) } } err = sc.Scan(ctx, []string{"."}) if err != nil { t.Fatal(err) } if stats != test.result { t.Errorf("wrong final result, want\n %#v\ngot:\n %#v", test.result, stats) } }) } } func TestScannerCancel(t *testing.T) { src := TestDir{ "bar": TestFile{Content: "bar"}, "baz": TestFile{Content: "baz"}, "foo": TestFile{Content: "foo"}, "other": TestFile{Content: "other"}, } result := ScanStats{Files: 2, Dirs: 0, Bytes: 6} ctx, cancel := context.WithCancel(context.Background()) defer cancel() tempdir := rtest.TempDir(t) TestCreateFiles(t, tempdir, src) back := rtest.Chdir(t, tempdir) defer back() cur, err := os.Getwd() if err != nil { t.Fatal(err) } sc := NewScanner(fs.Track{FS: fs.Local{}}) var lastStats ScanStats sc.Result = func(item string, s ScanStats) { lastStats = s if item == filepath.Join(cur, "baz") { t.Logf("found baz") cancel() } } err = sc.Scan(ctx, []string{"."}) if err != nil { t.Errorf("unexpected error %v found", err) } if lastStats != result { t.Errorf("wrong final result, want\n %#v\ngot:\n %#v", result, lastStats) } }