syncthing/lib/fs/fakefs_test.go

939 lines
19 KiB
Go

// Copyright (C) 2018 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 (
"bytes"
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"testing"
"time"
)
func TestFakeFS(t *testing.T) {
// Test some basic aspects of the fakeFS
fs := newFakeFilesystem("/foo/bar/baz")
// MkdirAll
err := fs.MkdirAll("dira/dirb", 0755)
if err != nil {
t.Fatal(err)
}
_, err = fs.Stat("dira/dirb")
if err != nil {
t.Fatal(err)
}
// Mkdir
err = fs.Mkdir("dira/dirb/dirc", 0755)
if err != nil {
t.Fatal(err)
}
_, err = fs.Stat("dira/dirb/dirc")
if err != nil {
t.Fatal(err)
}
// Create
fd, err := fs.Create("/dira/dirb/test")
if err != nil {
t.Fatal(err)
}
// Write
_, err = fd.Write([]byte("hello"))
if err != nil {
t.Fatal(err)
}
// Stat on fd
info, err := fd.Stat()
if err != nil {
t.Fatal(err)
}
if info.Name() != "test" {
t.Error("wrong name:", info.Name())
}
if info.Size() != 5 {
t.Error("wrong size:", info.Size())
}
// Stat on fs
info, err = fs.Stat("dira/dirb/test")
if err != nil {
t.Fatal(err)
}
if info.Name() != "test" {
t.Error("wrong name:", info.Name())
}
if info.Size() != 5 {
t.Error("wrong size:", info.Size())
}
// Seek
_, err = fd.Seek(1, io.SeekStart)
if err != nil {
t.Fatal(err)
}
// Read
bs0, err := io.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
if len(bs0) != 4 {
t.Error("wrong number of bytes:", len(bs0))
}
// Read again, same data hopefully
_, err = fd.Seek(0, io.SeekStart)
if err != nil {
t.Fatal(err)
}
bs1, err := io.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(bs0, bs1[1:]) {
t.Error("wrong data")
}
// Create symlink
if err := fs.CreateSymlink("foo", "dira/dirb/symlink"); err != nil {
t.Fatal(err)
}
if str, err := fs.ReadSymlink("dira/dirb/symlink"); err != nil {
t.Fatal(err)
} else if str != "foo" {
t.Error("Wrong symlink destination", str)
}
// Chown
if err := fs.Lchown("dira", 1234, 5678); err != nil {
t.Fatal(err)
}
if info, err := fs.Lstat("dira"); err != nil {
t.Fatal(err)
} else if info.Owner() != 1234 || info.Group() != 5678 {
t.Error("Wrong owner/group")
}
}
func testFakeFSRead(t *testing.T, fs Filesystem) {
// Test some basic aspects of the fakeFS
// Create
fd, _ := fs.Create("test")
defer fd.Close()
fd.Truncate(3 * 1 << randomBlockShift)
// Read
fd.Seek(0, io.SeekStart)
bs0, err := io.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
if len(bs0) != 3*1<<randomBlockShift {
t.Error("wrong number of bytes:", len(bs0))
}
// Read again, starting at an odd offset
fd.Seek(0, io.SeekStart)
buf0 := make([]byte, 12345)
n, _ := fd.Read(buf0)
if n != len(buf0) {
t.Fatal("short read")
}
buf1, err := io.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
if len(buf1) != 3*1<<randomBlockShift-len(buf0) {
t.Error("wrong number of bytes:", len(buf1))
}
bs1 := append(buf0, buf1...)
if !bytes.Equal(bs0, bs1) {
t.Error("data mismatch")
}
// Read large block with ReadAt
bs2 := make([]byte, 3*1<<randomBlockShift)
_, err = fd.ReadAt(bs2, 0)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(bs0, bs2) {
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 := os.MkdirTemp("", "")
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 := io.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 wrapperType, but in fact it shows
// the original one
assertDir(t, fs, "/", []string{"FOO"})
}
func TestReadWriteContent(t *testing.T) {
fs := newFakeFilesystem("foo?content=true")
fd, err := fs.Create("file")
if err != nil {
t.Fatal(err)
}
if _, err := fd.Write([]byte("foo")); err != nil {
t.Fatal(err)
}
if _, err := fd.WriteAt([]byte("bar"), 5); err != nil {
t.Fatal(err)
}
expected := []byte("foo\x00\x00bar")
buf := make([]byte, len(expected)-1)
n, err := fd.ReadAt(buf, 1) // note offset one byte
if err != nil {
t.Fatal(err)
}
if n != len(expected)-1 {
t.Fatal("wrong number of bytes read")
}
if !bytes.Equal(buf[:n], expected[1:]) {
fmt.Printf("%d %q\n", n, buf[:n])
t.Error("wrong data in file")
}
}
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
}