Browse Source

lib/fs: Add case-insensitive fakefs (#6074)

Evgeny Kuznetsov 5 năm trước cách đây
mục cha
commit
1c277fc096
3 tập tin đã thay đổi với 859 bổ sung32 xóa
  1. 117 28
      lib/fs/fakefs.go
  2. 740 4
      lib/fs/fakefs_test.go
  3. 2 0
      lib/fs/folding_test.go

+ 117 - 28
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 {

+ 740 - 4
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
+}

+ 2 - 0
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])