restic/internal/fs/fs_local_test.go
Michael Eischer 9a99141a5f fs: remove os.FileInfo from fs.ExtendedFileInfo
Only the `Sys()` value from os.FileInfo is kept as field `sys` to
support Windows. The os.FileInfo removal ensures that for values like
`ModTime` that existed in both data structures there's no more confusion
which value is actually used.
2024-11-30 17:07:36 +01:00

221 lines
5.4 KiB
Go

package fs
import (
"io"
"os"
"path/filepath"
"slices"
"testing"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
type fsLocalMetadataTestcase struct {
name string
follow bool
setup func(t *testing.T, path string)
nodeType restic.NodeType
}
func TestFSLocalMetadata(t *testing.T) {
for _, test := range []fsLocalMetadataTestcase{
{
name: "file",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.WriteFile(path, []byte("example"), 0o600))
},
nodeType: restic.NodeTypeFile,
},
{
name: "directory",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.Mkdir(path, 0o600))
},
nodeType: restic.NodeTypeDir,
},
{
name: "symlink",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.Symlink(path+"old", path))
},
nodeType: restic.NodeTypeSymlink,
},
{
name: "symlink file",
follow: true,
setup: func(t *testing.T, path string) {
rtest.OK(t, os.WriteFile(path+"file", []byte("example"), 0o600))
rtest.OK(t, os.Symlink(path+"file", path))
},
nodeType: restic.NodeTypeFile,
},
} {
runFSLocalTestcase(t, test)
}
}
func runFSLocalTestcase(t *testing.T, test fsLocalMetadataTestcase) {
t.Run(test.name, func(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
test.setup(t, path)
testFs := &Local{}
flags := 0
if !test.follow {
flags |= O_NOFOLLOW
}
f, err := testFs.OpenFile(path, flags, true)
rtest.OK(t, err)
checkMetadata(t, f, path, test.follow, test.nodeType)
rtest.OK(t, f.Close())
})
}
func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType restic.NodeType) {
fi, err := f.Stat()
rtest.OK(t, err)
var fi2 os.FileInfo
if follow {
fi2, err = os.Stat(path)
} else {
fi2, err = os.Lstat(path)
}
rtest.OK(t, err)
assertFIEqual(t, fi2, fi)
node, err := f.ToNode(false)
rtest.OK(t, err)
// ModTime is likely unique per file, thus it provides a good indication that it is from the correct file
rtest.Equals(t, fi.ModTime, node.ModTime, "node ModTime")
rtest.Equals(t, nodeType, node.Type, "node Type")
}
func assertFIEqual(t *testing.T, want os.FileInfo, got *ExtendedFileInfo) {
t.Helper()
rtest.Equals(t, want.Name(), got.Name, "Name")
rtest.Equals(t, want.ModTime(), got.ModTime, "ModTime")
rtest.Equals(t, want.Mode(), got.Mode, "Mode")
rtest.Equals(t, want.Size(), got.Size, "Size")
}
func TestFSLocalRead(t *testing.T) {
testFSLocalRead(t, false)
testFSLocalRead(t, true)
}
func testFSLocalRead(t *testing.T, makeReadable bool) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
f := openReadable(t, path, makeReadable)
checkMetadata(t, f, path, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
rtest.OK(t, f.Close())
}
func openReadable(t *testing.T, path string, useMakeReadable bool) File {
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, useMakeReadable)
rtest.OK(t, err)
if useMakeReadable {
// file was opened as metadataOnly. open for reading
rtest.OK(t, f.MakeReadable())
}
return f
}
func TestFSLocalReaddir(t *testing.T) {
testFSLocalReaddir(t, false)
testFSLocalReaddir(t, true)
}
func testFSLocalReaddir(t *testing.T, makeReadable bool) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
rtest.OK(t, os.Mkdir(path, 0o700))
entries := []string{"testfile"}
rtest.OK(t, os.WriteFile(filepath.Join(path, entries[0]), []byte("example"), 0o600))
f := openReadable(t, path, makeReadable)
checkMetadata(t, f, path, false, restic.NodeTypeDir)
names, err := f.Readdirnames(-1)
rtest.OK(t, err)
slices.Sort(names)
rtest.Equals(t, entries, names, "directory content mismatch")
rtest.OK(t, f.Close())
}
func TestFSLocalReadableRace(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, true)
rtest.OK(t, err)
pathNew := path + "new"
rtest.OK(t, os.Rename(path, pathNew))
err = f.MakeReadable()
if err == nil {
// a file handle based implementation should still work
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
}
rtest.OK(t, f.Close())
}
func TestFSLocalTypeChange(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, true)
rtest.OK(t, err)
// cache metadata
_, err = f.Stat()
rtest.OK(t, err)
pathNew := path + "new"
// rename instead of unlink to let the test also work on windows
rtest.OK(t, os.Rename(path, pathNew))
rtest.OK(t, os.Mkdir(path, 0o700))
rtest.OK(t, f.MakeReadable())
fi, err := f.Stat()
rtest.OK(t, err)
if !fi.Mode.IsDir() {
// a file handle based implementation should still reference the file
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
}
// else:
// path-based implementation
// nothing to test here. stat returned the new file type
rtest.OK(t, f.Close())
}