123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- // Copyright (C) 2020 The Syncthing Authors.
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this file,
- // You can obtain one at https://mozilla.org/MPL/2.0/.
- package fs
- import (
- "errors"
- "fmt"
- "path/filepath"
- "runtime"
- "sort"
- "strings"
- "testing"
- "time"
- )
- func TestRealCase(t *testing.T) {
- // Verify realCase lookups on various underlying filesystems.
- t.Run("fake-sensitive", func(t *testing.T) {
- testRealCase(t, newFakeFilesystem(t.Name()))
- })
- t.Run("fake-insensitive", func(t *testing.T) {
- testRealCase(t, newFakeFilesystem(t.Name()+"?insens=true"))
- })
- t.Run("actual", func(t *testing.T) {
- fsys, _ := setup(t)
- testRealCase(t, fsys)
- })
- }
- func newCaseFilesystem(fsys Filesystem) *caseFilesystem {
- return globalCaseFilesystemRegistry.get(fsys).(*caseFilesystem)
- }
- func testRealCase(t *testing.T, fsys Filesystem) {
- testFs := newCaseFilesystem(fsys)
- comps := []string{"Foo", "bar", "BAZ", "bAs"}
- path := filepath.Join(comps...)
- testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0o777)
- fd, err := testFs.Create(path)
- if err != nil {
- t.Fatal(err)
- }
- fd.Close()
- for i, tc := range []struct {
- in string
- len int
- }{
- {path, 4},
- {strings.ToLower(path), 4},
- {strings.ToUpper(path), 4},
- {"foo", 1},
- {"FOO", 1},
- {"foO", 1},
- {filepath.Join("Foo", "bar"), 2},
- {filepath.Join("Foo", "bAr"), 2},
- {filepath.Join("FoO", "bar"), 2},
- {filepath.Join("foo", "bar", "BAZ"), 3},
- {filepath.Join("Foo", "bar", "bAz"), 3},
- {filepath.Join("foo", "bar", "BAZ"), 3}, // Repeat on purpose
- } {
- out, err := testFs.realCase(tc.in)
- if err != nil {
- t.Error(err)
- } else if exp := filepath.Join(comps[:tc.len]...); out != exp {
- t.Errorf("tc %v: Expected %v, got %v", i, exp, out)
- }
- }
- }
- func TestRealCaseSensitive(t *testing.T) {
- // Verify that realCase returns the best on-disk case for case sensitive
- // systems. Test is skipped if the underlying fs is insensitive.
- t.Run("fake-sensitive", func(t *testing.T) {
- testRealCaseSensitive(t, newFakeFilesystem(t.Name()))
- })
- t.Run("actual", func(t *testing.T) {
- fsys, _ := setup(t)
- testRealCaseSensitive(t, fsys)
- })
- }
- func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
- testFs := newCaseFilesystem(fsys)
- names := make([]string, 2)
- names[0] = "foo"
- names[1] = strings.ToUpper(names[0])
- for _, n := range names {
- if err := testFs.MkdirAll(n, 0o777); err != nil {
- if IsErrCaseConflict(err) {
- t.Skip("Filesystem is case-insensitive")
- }
- t.Fatal(err)
- }
- }
- for _, n := range names {
- if rn, err := testFs.realCase(n); err != nil {
- t.Error(err)
- } else if rn != n {
- t.Errorf("Got %v, expected %v", rn, n)
- }
- }
- }
- func TestCaseFSStat(t *testing.T) {
- // Verify that a Stat() lookup behaves in a case sensitive manner
- // regardless of the underlying fs.
- t.Run("fake-sensitive", func(t *testing.T) {
- testCaseFSStat(t, newFakeFilesystem(t.Name()))
- })
- t.Run("fake-insensitive", func(t *testing.T) {
- testCaseFSStat(t, newFakeFilesystem(t.Name()+"?insens=true"))
- })
- t.Run("actual", func(t *testing.T) {
- fsys, _ := setup(t)
- testCaseFSStat(t, fsys)
- })
- }
- func testCaseFSStat(t *testing.T, fsys Filesystem) {
- fd, err := fsys.Create("foo")
- if err != nil {
- t.Fatal(err)
- }
- fd.Close()
- // Check if the underlying fs is sensitive or not
- sensitive := true
- if _, err = fsys.Stat("FOO"); err == nil {
- sensitive = false
- }
- testFs := newCaseFilesystem(fsys)
- _, err = testFs.Stat("FOO")
- if sensitive {
- if IsNotExist(err) {
- t.Log("pass: case sensitive underlying fs")
- } else {
- t.Error("expected NotExist, not", err, "for sensitive fs")
- }
- } else if IsErrCaseConflict(err) {
- t.Log("pass: case insensitive underlying fs")
- } else {
- t.Error("expected ErrCaseConflict, not", err, "for insensitive fs")
- }
- }
- func BenchmarkWalkCaseFakeFS100k(b *testing.B) {
- const entries = 100_000
- fsys, paths, err := fakefsForBenchmark(entries, 0)
- if err != nil {
- b.Fatal(err)
- }
- b.Run("rawfs", func(b *testing.B) {
- var fakefs *fakeFS
- if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok {
- fakefs = ffs.(*fakeFS)
- }
- fakefs.resetCounters()
- benchmarkWalkFakeFS(b, fsys, paths, 0, "")
- fakefs.reportMetricsPerOp(b)
- fakefs.reportMetricsPer(b, entries, "entry")
- b.ReportAllocs()
- })
- b.Run("casefs", func(b *testing.B) {
- // Construct the casefs manually or it will get cached and the benchmark is invalid.
- casefs := &caseFilesystem{
- Filesystem: fsys,
- realCaser: &defaultRealCaser{
- fs: fsys,
- cache: newCaseCache(),
- },
- }
- var fakefs *fakeFS
- if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok {
- fakefs = ffs.(*fakeFS)
- }
- fakefs.resetCounters()
- benchmarkWalkFakeFS(b, casefs, paths, 0, "")
- fakefs.reportMetricsPerOp(b)
- fakefs.reportMetricsPer(b, entries, "entry")
- b.ReportAllocs()
- })
- var otherOpPath string
- sep := string(PathSeparator)
- longest := 0
- for _, p := range paths {
- if length := len(strings.Split(p, sep)); length > longest {
- otherOpPath = p
- longest = length
- }
- }
- otherOpEvery := 1000
- b.Run(fmt.Sprintf("casefs-otherOpEvery%v", otherOpEvery), func(b *testing.B) {
- // Construct the casefs manually or it will get cached and the benchmark is invalid.
- casefs := &caseFilesystem{
- Filesystem: fsys,
- realCaser: &defaultRealCaser{
- fs: fsys,
- cache: newCaseCache(),
- },
- }
- var fakefs *fakeFS
- if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok {
- fakefs = ffs.(*fakeFS)
- }
- fakefs.resetCounters()
- benchmarkWalkFakeFS(b, casefs, paths, otherOpEvery, otherOpPath)
- fakefs.reportMetricsPerOp(b)
- fakefs.reportMetricsPer(b, entries, "entry")
- b.ReportAllocs()
- })
- }
- func benchmarkWalkFakeFS(b *testing.B, fsys Filesystem, paths []string, otherOpEvery int, otherOpPath string) {
- // Simulate a scanner pass over the filesystem. First walk it to
- // discover all names, then stat each name individually to check if it's
- // been deleted or not (pretending that they all existed in the
- // database).
- var ms0 runtime.MemStats
- runtime.ReadMemStats(&ms0)
- t0 := time.Now()
- for i := 0; i < b.N; i++ {
- if err := doubleWalkFSWithOtherOps(fsys, paths, otherOpEvery, otherOpPath); err != nil {
- b.Fatal(err)
- }
- }
- t1 := time.Now()
- var ms1 runtime.MemStats
- runtime.ReadMemStats(&ms1)
- // We add metrics per path entry
- b.ReportMetric(float64(t1.Sub(t0))/float64(b.N)/float64(len(paths)), "ns/entry")
- b.ReportMetric(float64(ms1.Mallocs-ms0.Mallocs)/float64(b.N)/float64(len(paths)), "allocs/entry")
- b.ReportMetric(float64(ms1.TotalAlloc-ms0.TotalAlloc)/float64(b.N)/float64(len(paths)), "B/entry")
- }
- func TestStressCaseFS(t *testing.T) {
- // Exercise a bunch of parallel operations for stressing out race
- // conditions in the realnamer cache etc.
- const limit = 10 * time.Second
- if testing.Short() {
- t.Skip("long test")
- }
- fsys, paths, err := fakefsForBenchmark(10_000, 0)
- if err != nil {
- t.Fatal(err)
- }
- for i := 0; i < runtime.NumCPU()/2+1; i++ {
- t.Run(fmt.Sprintf("walker-%d", i), func(t *testing.T) {
- // Walk the filesystem and stat everything
- t.Parallel()
- t0 := time.Now()
- for time.Since(t0) < limit {
- if err := doubleWalkFS(fsys, paths); err != nil {
- t.Fatal(err)
- }
- }
- })
- t.Run(fmt.Sprintf("toucher-%d", i), func(t *testing.T) {
- // Touch all the things
- t.Parallel()
- t0 := time.Now()
- for time.Since(t0) < limit {
- for _, p := range paths {
- now := time.Now()
- if err := fsys.Chtimes(p, now, now); err != nil {
- t.Fatal(err)
- }
- }
- }
- })
- }
- }
- func doubleWalkFS(fsys Filesystem, paths []string) error {
- return doubleWalkFSWithOtherOps(fsys, paths, 0, "")
- }
- func doubleWalkFSWithOtherOps(fsys Filesystem, paths []string, otherOpEvery int, otherOpPath string) error {
- i := 0
- if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
- i++
- if otherOpEvery != 0 && i%otherOpEvery == 0 {
- // l.Infoln("AAA", otherOpPath)
- if _, err := fsys.Lstat(otherOpPath); err != nil {
- return err
- }
- }
- // l.Infoln("CCC", path)
- return err
- }); err != nil {
- return err
- }
- for _, p := range paths {
- for p != "." {
- i++
- if otherOpEvery != 0 && i%otherOpEvery == 0 {
- if _, err := fsys.Lstat(otherOpPath); err != nil {
- // l.Infoln("AAA", otherOpPath)
- return err
- }
- }
- // l.Infoln("CCC", p)
- if _, err := fsys.Lstat(p); err != nil {
- return err
- }
- p = filepath.Dir(p)
- }
- }
- return nil
- }
- func fakefsForBenchmark(nfiles int, latency time.Duration) (Filesystem, []string, error) {
- fsys := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("fakefsForBenchmark?files=%d&insens=true&latency=%s", nfiles, latency))
- var paths []string
- if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
- paths = append(paths, path)
- return err
- }); err != nil {
- return nil, nil, err
- }
- if len(paths) < nfiles {
- return nil, nil, errors.New("didn't find enough stuff")
- }
- sort.Strings(paths)
- return fsys, paths, nil
- }
|