diff --git a/lib/fs/fakefs.go b/lib/fs/fakefs.go index 40beb6fe..bf756707 100644 --- a/lib/fs/fakefs.go +++ b/lib/fs/fakefs.go @@ -47,12 +47,14 @@ const randomBlockShift = 14 // 128k // maxsize=n to generate files up to a total of n MiB (default 0) // sizeavg=n to set the average size of random files, in bytes (default 1<<20) // seed=n to set the initial random seed (default 0) +// insens=b "true" makes filesystem case-insensitive Windows- or OSX-style (default false) // // - Two fakefs:s pointing at the same root path see the same files. // type fakefs struct { - mut sync.Mutex - root *fakeEntry + mut sync.Mutex + root *fakeEntry + insens bool } var ( @@ -90,6 +92,10 @@ func newFakeFilesystem(root string) *fakefs { maxsize, _ := strconv.Atoi(params.Get("maxsize")) sizeavg, _ := strconv.Atoi(params.Get("sizeavg")) seed, _ := strconv.Atoi(params.Get("seed")) + + if params.Get("insens") == "true" { + fs.insens = true + } if sizeavg == 0 { sizeavg = 1 << 20 } @@ -149,6 +155,9 @@ type fakeEntry struct { func (fs *fakefs) entryForName(name string) *fakeEntry { // bug: lookup doesn't work through symlinks. + if fs.insens { + name = UnicodeLowercase(name) + } name = filepath.ToSlash(name) if name == "." || name == "/" { @@ -232,6 +241,11 @@ func (fs *fakefs) create(name string) (*fakeEntry, error) { mode: 0666, mtime: time.Now(), } + + if fs.insens { + base = UnicodeLowercase(base) + } + entry.children[base] = new return new, nil } @@ -241,6 +255,9 @@ func (fs *fakefs) Create(name string) (File, error) { if err != nil { return nil, err } + if fs.insens { + return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name)}, nil + } return &fakeFile{fakeEntry: entry}, nil } @@ -264,8 +281,8 @@ func (fs *fakefs) DirNames(name string) ([]string, error) { } names := make([]string, 0, len(entry.children)) - for name := range entry.children { - names = append(names, name) + for _, child := range entry.children { + names = append(names, child.name) } return names, nil @@ -279,7 +296,13 @@ func (fs *fakefs) Lstat(name string) (FileInfo, error) { if entry == nil { return nil, os.ErrNotExist } - return &fakeFileInfo{*entry}, nil + + info := &fakeFileInfo{*entry} + if fs.insens { + info.name = filepath.Base(name) + } + + return info, nil } func (fs *fakefs) Mkdir(name string, perm FileMode) error { @@ -289,17 +312,22 @@ func (fs *fakefs) Mkdir(name string, perm FileMode) error { dir := filepath.Dir(name) base := filepath.Base(name) entry := fs.entryForName(dir) + key := base + if entry == nil { return os.ErrNotExist } if entry.entryType != fakeEntryTypeDir { return os.ErrExist } - if _, ok := entry.children[base]; ok { + if fs.insens { + key = UnicodeLowercase(key) + } + if _, ok := entry.children[key]; ok { return os.ErrExist } - entry.children[base] = &fakeEntry{ + entry.children[key] = &fakeEntry{ name: base, entryType: fakeEntryTypeDir, mode: perm, @@ -315,7 +343,12 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error { comps := strings.Split(name, "/") entry := fs.root for _, comp := range comps { - next, ok := entry.children[comp] + key := comp + if fs.insens { + key = UnicodeLowercase(key) + } + + next, ok := entry.children[key] if !ok { new := &fakeEntry{ @@ -325,7 +358,7 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error { mtime: time.Now(), children: make(map[string]*fakeEntry), } - entry.children[comp] = new + entry.children[key] = new next = new } else if next.entryType != fakeEntryTypeDir { return errors.New("not a directory") @@ -344,28 +377,37 @@ func (fs *fakefs) Open(name string) (File, error) { if entry == nil || entry.entryType != fakeEntryTypeFile { return nil, os.ErrNotExist } + + if fs.insens { + return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name)}, nil + } return &fakeFile{fakeEntry: entry}, nil } func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) { - fs.mut.Lock() - defer fs.mut.Unlock() - if flags&os.O_CREATE == 0 { return fs.Open(name) } + fs.mut.Lock() + defer fs.mut.Unlock() + dir := filepath.Dir(name) base := filepath.Base(name) entry := fs.entryForName(dir) + key := base + if entry == nil { return nil, os.ErrNotExist } else if entry.entryType != fakeEntryTypeDir { return nil, errors.New("not a directory") } + if fs.insens { + key = UnicodeLowercase(key) + } if flags&os.O_EXCL != 0 { - if _, ok := entry.children[base]; ok { + if _, ok := entry.children[key]; ok { return nil, os.ErrExist } } @@ -376,7 +418,7 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) mtime: time.Now(), } - entry.children[base] = newEntry + entry.children[key] = newEntry return &fakeFile{fakeEntry: newEntry}, nil } @@ -397,6 +439,10 @@ func (fs *fakefs) Remove(name string) error { fs.mut.Lock() defer fs.mut.Unlock() + if fs.insens { + name = UnicodeLowercase(name) + } + entry := fs.entryForName(name) if entry == nil { return os.ErrNotExist @@ -414,9 +460,13 @@ func (fs *fakefs) RemoveAll(name string) error { fs.mut.Lock() defer fs.mut.Unlock() + if fs.insens { + name = UnicodeLowercase(name) + } + entry := fs.entryForName(filepath.Dir(name)) if entry == nil { - return os.ErrNotExist + return nil // all tested real systems exibit this behaviour } // RemoveAll is easy when the file system uses garbage collection under @@ -429,12 +479,20 @@ func (fs *fakefs) Rename(oldname, newname string) error { fs.mut.Lock() defer fs.mut.Unlock() + oldKey := filepath.Base(oldname) + newKey := filepath.Base(newname) + + if fs.insens { + oldKey = UnicodeLowercase(oldKey) + newKey = UnicodeLowercase(newKey) + } + p0 := fs.entryForName(filepath.Dir(oldname)) if p0 == nil { return os.ErrNotExist } - entry := p0.children[filepath.Base(oldname)] + entry := p0.children[oldKey] if entry == nil { return os.ErrNotExist } @@ -444,13 +502,24 @@ func (fs *fakefs) Rename(oldname, newname string) error { return os.ErrNotExist } - dst, ok := p1.children[filepath.Base(newname)] - if ok && dst.entryType == fakeEntryTypeDir { - return errors.New("is a directory") + dst, ok := p1.children[newKey] + if ok { + if fs.insens && newKey == oldKey { + // case-only in-place rename + entry.name = filepath.Base(newname) + return nil + } + + if dst.entryType == fakeEntryTypeDir { + return errors.New("is a directory") + } } - p1.children[filepath.Base(newname)] = entry - delete(p0.children, filepath.Base(oldname)) + p1.children[newKey] = entry + entry.name = filepath.Base(newname) + + delete(p0.children, oldKey) + return nil } @@ -500,18 +569,30 @@ func (fs *fakefs) URI() string { } func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool { - return fi1.Name() == fi2.Name() + // BUG: real systems base file sameness on path, inodes, etc + // we try our best, but FileInfo just doesn't have enough data + // so there be false positives, especially on Windows + // where ModTime is not that precise + var ok bool + if fs.insens { + ok = UnicodeLowercase(fi1.Name()) == UnicodeLowercase(fi2.Name()) + } else { + ok = fi1.Name() == fi2.Name() + } + + return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group() } // fakeFile is the representation of an open file. We don't care if it's // opened for reading or writing, it's all good. type fakeFile struct { *fakeEntry - mut sync.Mutex - rng io.Reader - seed int64 - offset int64 - seedOffs int64 + mut sync.Mutex + rng io.Reader + seed int64 + offset int64 + seedOffs int64 + presentedName string // present (i.e. != "") on insensitive fs only } func (f *fakeFile) Close() error { @@ -674,6 +755,9 @@ func (f *fakeFile) WriteAt(p []byte, off int64) (int, error) { } func (f *fakeFile) Name() string { + if f.presentedName != "" { + return f.presentedName + } return f.name } @@ -690,7 +774,12 @@ func (f *fakeFile) Truncate(size int64) error { } func (f *fakeFile) Stat() (FileInfo, error) { - return &fakeFileInfo{*f.fakeEntry}, nil + info := &fakeFileInfo{*f.fakeEntry} + if f.presentedName != "" { + info.name = f.presentedName + } + + return info, nil } func (f *fakeFile) Sync() error { diff --git a/lib/fs/fakefs_test.go b/lib/fs/fakefs_test.go index 6601554c..7040b35c 100644 --- a/lib/fs/fakefs_test.go +++ b/lib/fs/fakefs_test.go @@ -8,9 +8,16 @@ package fs import ( "bytes" + "fmt" "io" "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "sort" "testing" + "time" ) func TestFakeFS(t *testing.T) { @@ -123,13 +130,11 @@ func TestFakeFS(t *testing.T) { } } -func TestFakeFSRead(t *testing.T) { +func testFakeFSRead(t *testing.T, fs Filesystem) { // Test some basic aspects of the fakefs - - fs := newFakeFilesystem("/foo/bar/baz") - // Create fd, _ := fs.Create("test") + defer fd.Close() fd.Truncate(3 * 1 << randomBlockShift) // Read @@ -172,3 +177,734 @@ func TestFakeFSRead(t *testing.T) { t.Error("data mismatch") } } + +type testFS struct { + name string + fs Filesystem +} + +type test struct { + name string + impl func(t *testing.T, fs Filesystem) +} + +func TestFakeFSCaseSensitive(t *testing.T) { + var tests = []test{ + {"Read", testFakeFSRead}, + {"OpenFile", testFakeFSOpenFile}, + {"RemoveAll", testFakeFSRemoveAll}, + {"Remove", testFakeFSRemove}, + {"Rename", testFakeFSRename}, + {"Mkdir", testFakeFSMkdir}, + {"SameFile", testFakeFSSameFile}, + {"DirNames", testDirNames}, + {"FileName", testFakeFSFileName}, + } + var filesystems = []testFS{ + {"fakefs", newFakeFilesystem("/foo")}, + } + + testDir, sensitive := createTestDir(t) + defer removeTestDir(t, testDir) + if sensitive { + filesystems = append(filesystems, testFS{runtime.GOOS, newBasicFilesystem(testDir)}) + } + + runTests(t, tests, filesystems) +} + +func TestFakeFSCaseInsensitive(t *testing.T) { + var tests = []test{ + {"Read", testFakeFSRead}, + {"OpenFile", testFakeFSOpenFile}, + {"RemoveAll", testFakeFSRemoveAll}, + {"Remove", testFakeFSRemove}, + {"Mkdir", testFakeFSMkdir}, + {"SameFile", testFakeFSSameFile}, + {"DirNames", testDirNames}, + {"FileName", testFakeFSFileName}, + {"GeneralInsens", testFakeFSCaseInsensitive}, + {"MkdirAllInsens", testFakeFSCaseInsensitiveMkdirAll}, + {"StatInsens", testFakeFSStatInsens}, + {"RenameInsens", testFakeFSRenameInsensitive}, + {"MkdirInsens", testFakeFSMkdirInsens}, + {"OpenFileInsens", testFakeFSOpenFileInsens}, + {"RemoveAllInsens", testFakeFSRemoveAllInsens}, + {"RemoveInsens", testFakeFSRemoveInsens}, + {"SameFileInsens", testFakeFSSameFileInsens}, + {"CreateInsens", testFakeFSCreateInsens}, + {"FileNameInsens", testFakeFSFileNameInsens}, + } + + var filesystems = []testFS{ + {"fakefs", newFakeFilesystem("/foobar?insens=true")}, + } + + testDir, sensitive := createTestDir(t) + defer removeTestDir(t, testDir) + if !sensitive { + filesystems = append(filesystems, testFS{runtime.GOOS, newBasicFilesystem(testDir)}) + } + + runTests(t, tests, filesystems) +} + +func createTestDir(t *testing.T) (string, bool) { + t.Helper() + + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("could not create temporary dir for testing: %s", err) + } + + if fd, err := os.Create(filepath.Join(testDir, ".stfolder")); err != nil { + t.Fatalf("could not create .stfolder: %s", err) + } else { + fd.Close() + } + + var sensitive bool + + if f, err := os.Open(filepath.Join(testDir, ".STfolder")); err != nil { + sensitive = true + } else { + defer f.Close() + } + + return testDir, sensitive +} + +func removeTestDir(t *testing.T, testDir string) { + t.Helper() + + if err := os.RemoveAll(testDir); err != nil { + t.Fatalf("could not remove test directory: %s", err) + } +} + +func runTests(t *testing.T, tests []test, filesystems []testFS) { + for _, filesystem := range filesystems { + for _, test := range tests { + name := fmt.Sprintf("%s_%s", test.name, filesystem.name) + t.Run(name, func(t *testing.T) { + test.impl(t, filesystem.fs) + if err := cleanup(filesystem.fs); err != nil { + t.Errorf("cleanup failed: %s", err) + } + }) + } + } +} + +func testFakeFSCaseInsensitive(t *testing.T, fs Filesystem) { + bs1 := []byte("test") + + err := fs.Mkdir("/fUbar", 0755) + if err != nil { + t.Fatal(err) + } + + fd1, err := fs.Create("fuBAR/SISYPHOS") + if err != nil { + t.Fatalf("could not create file: %s", err) + } + + defer fd1.Close() + + _, err = fd1.Write(bs1) + if err != nil { + t.Fatal(err) + } + + // Try reading from the same file with different filenames + fd2, err := fs.Open("Fubar/Sisyphos") + if err != nil { + t.Fatalf("could not open file by its case-differing filename: %s", err) + } + + defer fd2.Close() + + if _, err := fd2.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + + bs2, err := ioutil.ReadAll(fd2) + if err != nil { + t.Fatal(err) + } + + if len(bs1) != len(bs2) { + t.Errorf("wrong number of bytes, expected %d, got %d", len(bs1), len(bs2)) + } +} + +func testFakeFSCaseInsensitiveMkdirAll(t *testing.T, fs Filesystem) { + err := fs.MkdirAll("/fOO/Bar/bAz", 0755) + if err != nil { + t.Fatal(err) + } + + fd, err := fs.OpenFile("/foo/BaR/BaZ/tESt", os.O_CREATE, 0644) + if err != nil { + t.Fatal(err) + } + + if err = fd.Close(); err != nil { + t.Fatal(err) + } + + if err = fs.Rename("/FOO/BAR/baz/tesT", "/foo/baR/BAZ/Qux"); err != nil { + t.Fatal(err) + } +} + +func testDirNames(t *testing.T, fs Filesystem) { + filenames := []string{"fOO", "Bar", "baz"} + for _, filename := range filenames { + if fd, err := fs.Create("/" + filename); err != nil { + t.Errorf("Could not create %s: %s", filename, err) + } else { + fd.Close() + } + } + + assertDir(t, fs, "/", filenames) +} + +func assertDir(t *testing.T, fs Filesystem, directory string, filenames []string) { + t.Helper() + got, err := fs.DirNames(directory) + if err != nil { + t.Fatal(err) + } + + if path.Clean(directory) == "/" { + filenames = append(filenames, ".stfolder") + } + sort.Strings(filenames) + sort.Strings(got) + + if len(filenames) != len(got) { + t.Errorf("want %s, got %s", filenames, got) + return + } + + for i := range filenames { + if filenames[i] != got[i] { + t.Errorf("want %s, got %s", filenames, got) + return + } + } +} + +func testFakeFSStatInsens(t *testing.T, fs Filesystem) { + // this is to test that neither fs.Stat nor fd.Stat change the filename + // both in directory and in previous Stat results + fd1, err := fs.Create("aAa") + if err != nil { + t.Fatal(err) + } + defer fd1.Close() + + info1, err := fs.Stat("AAA") + if err != nil { + t.Fatal(err) + } + + if _, err = fs.Stat("AaA"); err != nil { + t.Fatal(err) + } + + info2, err := fd1.Stat() + if err != nil { + t.Fatal(err) + } + + fd2, err := fs.Open("aaa") + if err != nil { + t.Fatal(err) + } + defer fd2.Close() + + if _, err = fd2.Stat(); err != nil { + t.Fatal(err) + } + + if info1.Name() != "AAA" { + t.Errorf("want AAA, got %s", info1.Name()) + } + + if info2.Name() != "aAa" { + t.Errorf("want aAa, got %s", info2.Name()) + } + + assertDir(t, fs, "/", []string{"aAa"}) +} + +func testFakeFSFileName(t *testing.T, fs Filesystem) { + var testCases = []struct { + create string + open string + }{ + {"bar", "bar"}, + } + + for _, testCase := range testCases { + if fd, err := fs.Create(testCase.create); err != nil { + t.Fatal(err) + } else { + fd.Close() + } + + fd, err := fs.Open(testCase.open) + if err != nil { + t.Fatal(err) + } + + defer fd.Close() + + if got := fd.Name(); got != testCase.open { + t.Errorf("want %s, got %s", testCase.open, got) + } + } +} + +func testFakeFSFileNameInsens(t *testing.T, fs Filesystem) { + var testCases = []struct { + create string + open string + }{ + {"BaZ", "bAz"}, + } + + for _, testCase := range testCases { + fd, err := fs.Create(testCase.create) + if err != nil { + t.Fatal(err) + } + fd.Close() + + fd, err = fs.Open(testCase.open) + if err != nil { + t.Fatal(err) + } + + defer fd.Close() + + if got := fd.Name(); got != testCase.open { + t.Errorf("want %s, got %s", testCase.open, got) + } + } +} + +func testFakeFSRename(t *testing.T, fs Filesystem) { + if err := fs.MkdirAll("/foo/bar/baz", 0755); err != nil { + t.Fatal(err) + } + + fd, err := fs.Create("/foo/bar/baz/qux") + if err != nil { + t.Fatal(err) + } + fd.Close() + + if err := fs.Rename("/foo/bar/baz/qux", "/foo/notthere/qux"); err == nil { + t.Errorf("rename to non-existent dir gave no error") + } + + if err := fs.MkdirAll("/baz/bar/foo", 0755); err != nil { + t.Fatal(err) + } + + if err := fs.Rename("/foo/bar/baz/qux", "/baz/bar/foo/qux"); err != nil { + t.Fatal(err) + } + + var dirs = []struct { + dir string + files []string + }{ + {dir: "/", files: []string{"foo", "baz"}}, + {dir: "/foo", files: []string{"bar"}}, + {dir: "/foo/bar/baz", files: []string{}}, + {dir: "/baz/bar/foo", files: []string{"qux"}}, + } + + for _, dir := range dirs { + assertDir(t, fs, dir.dir, dir.files) + } + + if err := fs.Rename("/baz/bar/foo", "/baz/bar/FOO"); err != nil { + t.Fatal(err) + } + + assertDir(t, fs, "/baz/bar", []string{"FOO"}) + assertDir(t, fs, "/baz/bar/FOO", []string{"qux"}) +} + +func testFakeFSRenameInsensitive(t *testing.T, fs Filesystem) { + if err := fs.MkdirAll("/baz/bar/foo", 0755); err != nil { + t.Fatal(err) + } + + if err := fs.MkdirAll("/foO/baR/baZ", 0755); err != nil { + t.Fatal(err) + } + + fd, err := fs.Create("/BAZ/BAR/FOO/QUX") + if err != nil { + t.Fatal(err) + } + + fd.Close() + + if err := fs.Rename("/Baz/bAr/foO/QuX", "/Foo/Bar/Baz/qUUx"); err != nil { + t.Fatal(err) + } + + var dirs = []struct { + dir string + files []string + }{ + {dir: "/", files: []string{"foO", "baz"}}, + {dir: "/foo", files: []string{"baR"}}, + {dir: "/foo/bar/baz", files: []string{"qUUx"}}, + {dir: "/baz/bar/foo", files: []string{}}, + } + + for _, dir := range dirs { + assertDir(t, fs, dir.dir, dir.files) + } + + // not checking on darwin due to https://github.com/golang/go/issues/35222 + if runtime.GOOS != "darwin" { + if err := fs.Rename("/foo/bar/BAZ", "/FOO/BAR/bAz"); err != nil { + t.Errorf("Could not perform in-place case-only directory rename: %s", err) + } + + assertDir(t, fs, "/foo/bar", []string{"bAz"}) + assertDir(t, fs, "/fOO/bAr/baz", []string{"qUUx"}) + } + + if err := fs.Rename("foo/bar/baz/quux", "foo/bar/BaZ/Quux"); err != nil { + t.Errorf("File rename failed: %s", err) + } + + assertDir(t, fs, "/FOO/BAR/BAZ", []string{"Quux"}) +} + +func testFakeFSMkdir(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/foo", 0755); err != nil { + t.Fatal(err) + } + + if _, err := fs.Stat("/foo"); err != nil { + t.Fatal(err) + } + + if err := fs.Mkdir("/foo", 0755); err == nil { + t.Errorf("got no error while creating existing directory") + } +} + +func testFakeFSMkdirInsens(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/foo", 0755); err != nil { + t.Fatal(err) + } + + if _, err := fs.Stat("/Foo"); err != nil { + t.Fatal(err) + } + + if err := fs.Mkdir("/FOO", 0755); err == nil { + t.Errorf("got no error while creating existing directory") + } +} + +func testFakeFSOpenFile(t *testing.T, fs Filesystem) { + fd, err := fs.OpenFile("foobar", os.O_RDONLY, 0664) + if err == nil { + fd.Close() + t.Fatalf("got no error opening a non-existing file") + } + + fd, err = fs.OpenFile("foobar", os.O_RDWR|os.O_CREATE, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() + + fd, err = fs.OpenFile("foobar", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0664) + if err == nil { + fd.Close() + t.Fatalf("created an existing file while told not to") + } + + fd, err = fs.OpenFile("foobar", os.O_RDWR|os.O_CREATE, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() + + fd, err = fs.OpenFile("foobar", os.O_RDWR, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() +} + +func testFakeFSOpenFileInsens(t *testing.T, fs Filesystem) { + fd, err := fs.OpenFile("FooBar", os.O_RDONLY, 0664) + if err == nil { + fd.Close() + t.Fatalf("got no error opening a non-existing file") + } + + fd, err = fs.OpenFile("fOObar", os.O_RDWR|os.O_CREATE, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() + + fd, err = fs.OpenFile("fOoBaR", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0664) + if err == nil { + fd.Close() + t.Fatalf("created an existing file while told not to") + } + + fd, err = fs.OpenFile("FoObAr", os.O_RDWR|os.O_CREATE, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() + + fd, err = fs.OpenFile("FOOBAR", os.O_RDWR, 0664) + if err != nil { + t.Fatal(err) + } + fd.Close() +} + +func testFakeFSRemoveAll(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/foo", 0755); err != nil { + t.Fatal(err) + } + + filenames := []string{"bar", "baz", "qux"} + for _, filename := range filenames { + fd, err := fs.Create("/foo/" + filename) + if err != nil { + t.Fatalf("Could not create %s: %s", filename, err) + } else { + fd.Close() + } + } + + if err := fs.RemoveAll("/foo"); err != nil { + t.Fatal(err) + } + + if _, err := fs.Stat("/foo"); err == nil { + t.Errorf("this should be an error, as file doesn not exist anymore") + } + + if err := fs.RemoveAll("/foo/bar"); err != nil { + t.Errorf("real systems don't return error here") + } +} + +func testFakeFSRemoveAllInsens(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/Foo", 0755); err != nil { + t.Fatal(err) + } + + filenames := []string{"bar", "baz", "qux"} + for _, filename := range filenames { + fd, err := fs.Create("/FOO/" + filename) + if err != nil { + t.Fatalf("Could not create %s: %s", filename, err) + } + fd.Close() + } + + if err := fs.RemoveAll("/fOo"); err != nil { + t.Errorf("Could not remove dir: %s", err) + } + + if _, err := fs.Stat("/foo"); err == nil { + t.Errorf("this should be an error, as file doesn not exist anymore") + } + + if err := fs.RemoveAll("/foO/bAr"); err != nil { + t.Errorf("real systems don't return error here") + } +} + +func testFakeFSRemove(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/Foo", 0755); err != nil { + t.Fatal(err) + } + + fd, err := fs.Create("/Foo/Bar") + if err != nil { + t.Fatal(err) + } else { + fd.Close() + } + + if err := fs.Remove("/Foo"); err == nil { + t.Errorf("not empty, should give error") + } + + if err := fs.Remove("/Foo/Bar"); err != nil { + t.Fatal(err) + } + + if err := fs.Remove("/Foo"); err != nil { + t.Fatal(err) + } +} + +func testFakeFSRemoveInsens(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/Foo", 0755); err != nil { + t.Fatal(err) + } + + fd, err := fs.Create("/Foo/Bar") + if err != nil { + t.Fatal(err) + } + fd.Close() + + if err := fs.Remove("/FOO"); err == nil { + t.Errorf("not empty, should give error") + } + + if err := fs.Remove("/Foo/BaR"); err != nil { + t.Fatal(err) + } + + if err := fs.Remove("/FoO"); err != nil { + t.Fatal(err) + } +} + +func testFakeFSSameFile(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/Foo", 0755); err != nil { + t.Fatal(err) + } + + filenames := []string{"Bar", "Baz", "/Foo/Bar"} + for _, filename := range filenames { + if fd, err := fs.Create(filename); err != nil { + t.Fatalf("Could not create %s: %s", filename, err) + } else { + fd.Close() + if runtime.GOOS == "windows" { + time.Sleep(1 * time.Millisecond) + } + } + } + + testCases := []struct { + f1 string + f2 string + want bool + }{ + {"Bar", "Baz", false}, + {"Bar", "/Foo/Bar", false}, + {"Bar", "Bar", true}, + } + + for _, test := range testCases { + assertSameFile(t, fs, test.f1, test.f2, test.want) + } +} + +func testFakeFSSameFileInsens(t *testing.T, fs Filesystem) { + if err := fs.Mkdir("/Foo", 0755); err != nil { + t.Fatal(err) + } + + filenames := []string{"Bar", "Baz"} + for _, filename := range filenames { + fd, err := fs.Create(filename) + if err != nil { + t.Errorf("Could not create %s: %s", filename, err) + } + fd.Close() + } + + testCases := []struct { + f1 string + f2 string + want bool + }{ + {"bAr", "baZ", false}, + {"baz", "BAZ", true}, + } + + for _, test := range testCases { + assertSameFile(t, fs, test.f1, test.f2, test.want) + } +} + +func assertSameFile(t *testing.T, fs Filesystem, f1, f2 string, want bool) { + t.Helper() + + fi1, err := fs.Stat(f1) + if err != nil { + t.Fatal(err) + } + + fi2, err := fs.Stat(f2) + if err != nil { + t.Fatal(err) + } + + got := fs.SameFile(fi1, fi2) + if got != want { + t.Errorf("for \"%s\" and \"%s\" want SameFile %v, got %v", f1, f2, want, got) + } +} + +func testFakeFSCreateInsens(t *testing.T, fs Filesystem) { + fd1, err := fs.Create("FOO") + if err != nil { + t.Fatal(err) + } + + defer fd1.Close() + + fd2, err := fs.Create("fOo") + if err != nil { + t.Fatal(err) + } + + defer fd2.Close() + + if fd1.Name() != "FOO" { + t.Errorf("name of the file created as \"FOO\" is %s", fd1.Name()) + } + + if fd2.Name() != "fOo" { + t.Errorf("name of created file \"fOo\" is %s", fd2.Name()) + } + + // one would expect DirNames to show the last variant, but in fact it shows + // the original one + assertDir(t, fs, "/", []string{"FOO"}) +} + +func cleanup(fs Filesystem) error { + filenames, _ := fs.DirNames("/") + for _, filename := range filenames { + if filename != ".stfolder" { + if err := fs.RemoveAll(filename); err != nil { + return err + } + } + } + + return nil +} diff --git a/lib/fs/folding_test.go b/lib/fs/folding_test.go index bbce66ac..f8f83820 100644 --- a/lib/fs/folding_test.go +++ b/lib/fs/folding_test.go @@ -38,6 +38,8 @@ func TestUnicodeLowercase(t *testing.T) { {"汉语/漢語 or 中文", "汉语/漢語 or 中文"}, // Niether katakana as far as I can tell. {"チャーハン", "チャーハン"}, + // Some special unicode characters, however, are folded by OSes + {"\u212A", "k"}, } for _, tc := range cases { res := UnicodeLowercase(tc[0])