Files
go-fuse/fuse/test/loopback_test.go

984 lines
22 KiB
Go

// Copyright 2016 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package test
import (
"bytes"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"reflect"
"runtime"
"syscall"
"testing"
"time"
"github.com/hanzoai/go-fuse/v2/fuse"
"github.com/hanzoai/go-fuse/v2/fuse/nodefs"
"github.com/hanzoai/go-fuse/v2/fuse/pathfs"
"github.com/hanzoai/go-fuse/v2/internal/testutil"
"github.com/hanzoai/go-fuse/v2/posixtest"
)
type testCase struct {
// Per-testcase temporary directory, usually in /tmp, named something like
// "$TESTNAME.123456".
tmpDir string
// Backing directory. Lives in tmpDir.
orig string
// Mountpoint. Lives in tmpDir.
mnt string
mountFile string
mountSubdir string
origFile string
origSubdir string
tester *testing.T
state *fuse.Server
pathFs *pathfs.PathNodeFs
connector *nodefs.FileSystemConnector
}
const testTTL = 100 * time.Millisecond
// Mkdir is a utility wrapper for os.Mkdir, aborting the test if it fails.
func (tc *testCase) Mkdir(name string, mode os.FileMode) {
if err := os.Mkdir(name, mode); err != nil {
tc.tester.Fatalf("Mkdir(%q,%v): %v", name, mode, err)
}
}
// WriteFile is a utility wrapper for os.WriteFile, aborting the
// test if it fails.
func (tc *testCase) WriteFile(name string, content []byte, mode os.FileMode) {
if err := os.WriteFile(name, content, mode); err != nil {
if len(content) > 50 {
content = append(content[:50], '.', '.', '.')
}
tc.tester.Fatalf("WriteFile(%q, %q, %o): %v", name, content, mode, err)
}
}
// Create and mount filesystem.
func NewTestCase(t *testing.T) *testCase {
tc := &testCase{}
tc.tester = t
// Make sure system setting does not affect test.
syscall.Umask(0)
const name string = "hello.txt"
const subdir string = "subdir"
var err error
tc.tmpDir = t.TempDir()
tc.orig = tc.tmpDir + "/orig"
tc.mnt = tc.tmpDir + "/mnt"
tc.Mkdir(tc.orig, 0700)
tc.Mkdir(tc.mnt, 0700)
tc.mountFile = filepath.Join(tc.mnt, name)
tc.mountSubdir = filepath.Join(tc.mnt, subdir)
tc.origFile = filepath.Join(tc.orig, name)
tc.origSubdir = filepath.Join(tc.orig, subdir)
var pfs pathfs.FileSystem
pfs = pathfs.NewLoopbackFileSystem(tc.orig)
pfs = pathfs.NewLockingFileSystem(pfs)
tc.pathFs = pathfs.NewPathNodeFs(pfs, &pathfs.PathNodeFsOptions{
ClientInodes: true})
tc.connector = nodefs.NewFileSystemConnector(tc.pathFs.Root(),
&nodefs.Options{
EntryTimeout: testTTL,
AttrTimeout: testTTL,
NegativeTimeout: 0.0,
Debug: testutil.VerboseTest(),
LookupKnownChildren: true,
})
tc.state, err = fuse.NewServer(
tc.connector.RawFS(), tc.mnt, &fuse.MountOptions{
SingleThreaded: true,
Debug: testutil.VerboseTest(),
})
if err != nil {
t.Fatal("NewServer:", err)
}
go tc.state.Serve()
if err := tc.state.WaitMount(); err != nil {
t.Fatal("WaitMount", err)
}
return tc
}
// Unmount and del.
func (tc *testCase) Cleanup() {
err := tc.state.Unmount()
if err != nil {
tc.tester.Fatalf("Unmount failed: %v", err)
}
}
func (tc *testCase) rootNode() *nodefs.Inode {
return tc.pathFs.Root().Inode()
}
////////////////
// Tests.
func TestOpenUnreadable(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
_, err := os.Open(tc.mnt + "/doesnotexist")
if err == nil {
t.Errorf("open non-existent should raise error")
}
}
func TestReadThrough(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
content := randomData(125)
tc.WriteFile(tc.origFile, content, 0700)
var mode uint32 = 0757
err := os.Chmod(tc.mountFile, os.FileMode(mode))
if err != nil {
t.Fatalf("Chmod failed: %v", err)
}
fi, err := os.Lstat(tc.mountFile)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
if uint32(fi.Mode().Perm()) != mode {
t.Errorf("Wrong mode %o != %o", int(fi.Mode().Perm()), mode)
}
// Open (for read), read.
f, err := os.Open(tc.mountFile)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
var buf [1024]byte
slice := buf[:]
n, err := f.Read(slice)
CompareSlices(t, slice[:n], content)
}
func TestRemove(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
contents := []byte{1, 2, 3}
tc.WriteFile(tc.origFile, []byte(contents), 0700)
err := os.Remove(tc.mountFile)
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
_, err = os.Lstat(tc.origFile)
if err == nil {
t.Errorf("Lstat() after delete should have generated error.")
}
}
func TestWriteThrough(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
// Create (for write), write.
f, err := os.OpenFile(tc.mountFile, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer f.Close()
content := randomData(125)
n, err := f.Write(content)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if n != len(content) {
t.Errorf("Write mismatch: %v of %v", n, len(content))
}
fi, err := os.Lstat(tc.origFile)
if err != nil {
t.Fatalf("Lstat(%q): %v", tc.origFile, err)
}
if fi.Mode().Perm() != 0644 {
t.Errorf("create mode error %o", fi.Mode()&0777)
}
f, err = os.Open(tc.origFile)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
var buf [1024]byte
slice := buf[:]
n, err = f.Read(slice)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
CompareSlices(t, slice[:n], content)
}
func TestLinkCreate(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
content := randomData(125)
tc.WriteFile(tc.origFile, content, 0700)
tc.Mkdir(tc.origSubdir, 0777)
// Link.
mountSubfile := filepath.Join(tc.mountSubdir, "subfile")
err := os.Link(tc.mountFile, mountSubfile)
if err != nil {
t.Fatalf("Link failed: %v", err)
}
var subStat, stat syscall.Stat_t
err = syscall.Lstat(mountSubfile, &subStat)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
err = syscall.Lstat(tc.mountFile, &stat)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
if stat.Nlink != 2 {
t.Errorf("Expect 2 links: %v", stat)
}
if stat.Ino != subStat.Ino {
t.Errorf("Link succeeded, but inode numbers different: %v %v", stat.Ino, subStat.Ino)
}
readback, err := os.ReadFile(mountSubfile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
CompareSlices(t, readback, content)
err = os.Remove(tc.mountFile)
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
_, err = os.ReadFile(mountSubfile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
}
func randomData(size int) []byte {
return bytes.Repeat([]byte{'x'}, size)
}
// Deal correctly with hard links implied by matching client inode
// numbers.
func TestLinkExisting(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
c := randomData(5)
tc.WriteFile(tc.orig+"/file1", c, 0644)
err := os.Link(tc.orig+"/file1", tc.orig+"/file2")
if err != nil {
t.Fatalf("Link failed: %v", err)
}
var s1, s2 syscall.Stat_t
err = syscall.Lstat(tc.mnt+"/file1", &s1)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
err = syscall.Lstat(tc.mnt+"/file2", &s2)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
if s1.Ino != s2.Ino {
t.Errorf("linked files should have identical inodes %v %v", s1.Ino, s2.Ino)
}
back, err := os.ReadFile(tc.mnt + "/file1")
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
CompareSlices(t, back, c)
}
// Deal correctly with hard links implied by matching client inode
// numbers.
func TestLinkForget(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
c := "hello"
tc.WriteFile(tc.orig+"/file1", []byte(c), 0644)
err := os.Link(tc.orig+"/file1", tc.orig+"/file2")
if err != nil {
t.Fatalf("Link failed: %v", err)
}
for _, fn := range []string{"file1", "file2"} {
var s syscall.Stat_t
err = syscall.Lstat(tc.mnt+"/"+fn, &s)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
tc.pathFs.ForgetClientInodes()
}
// Now, the backing files are still hardlinked, but go-fuse's
// view of them should not be because of the
// ForgetClientInodes call. To prove this, we swap out the
// files in the backing store, and prove that they are
// distinct by truncating to different lengths.
for _, fn := range []string{"file1", "file2"} {
fn = tc.orig + "/" + fn
if err := os.Remove(fn); err != nil {
t.Fatalf("Remove: %v", err)
}
tc.WriteFile(fn, []byte(c), 0644)
}
for i, fn := range []string{"file1", "file2"} {
fn = tc.mnt + "/" + fn
if err := os.Truncate(fn, int64(i)); err != nil {
t.Fatalf("Truncate: %v", err)
}
}
for i, fn := range []string{"file1", "file2"} {
var s syscall.Stat_t
err = syscall.Lstat(tc.mnt+"/"+fn, &s)
if err != nil {
t.Fatalf("Lstat failed: %v", err)
}
if s.Size != int64(i) {
t.Errorf("Lstat(%q): got size %d, want %d", fn, s.Size, i)
}
}
}
func TestPosix(t *testing.T) {
tests := []string{
"SymlinkReadlink",
"MkdirRmdir",
"RenameOverwriteDestNoExist",
"RenameOverwriteDestExist",
"ReadDir",
"AppendWrite",
}
for _, k := range tests {
f := posixtest.All[k]
if f == nil {
t.Fatalf("test %s missing", k)
}
t.Run(k, func(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
f(t, tc.mnt)
})
}
}
// Flaky test, due to rename race condition.
func TestDelRename(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
sd := tc.mnt + "/testDelRename"
tc.Mkdir(sd, 0755)
d := sd + "/dest"
tc.WriteFile(d, []byte("blabla"), 0644)
f, err := os.Open(d)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
if err := os.Remove(d); err != nil {
t.Fatalf("Remove failed: %v", err)
}
s := sd + "/src"
tc.WriteFile(s, []byte("blabla"), 0644)
if err := os.Rename(s, d); err != nil {
t.Fatalf("Rename failed: %v", err)
}
}
func TestAccess(t *testing.T) {
if os.Geteuid() == 0 {
t.Log("Skipping TestAccess() as root.")
return
}
tc := NewTestCase(t)
defer tc.Cleanup()
contents := []byte{1, 2, 3}
tc.WriteFile(tc.origFile, []byte(contents), 0700)
if err := os.Chmod(tc.origFile, 0); err != nil {
t.Fatalf("Chmod failed: %v", err)
}
// Ugh - copied from unistd.h
const W_OK uint32 = 2
if errCode := syscall.Access(tc.mountFile, W_OK); errCode != syscall.EACCES {
t.Errorf("Expected EACCES for non-writable, %v %v", errCode, syscall.EACCES)
}
if err := os.Chmod(tc.origFile, 0222); err != nil {
t.Fatalf("Chmod failed: %v", err)
}
if errCode := syscall.Access(tc.mountFile, W_OK); errCode != nil {
t.Errorf("Expected no error code for writable. %v", errCode)
}
}
func TestMknod(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
if errNo := syscall.Mknod(tc.mountFile, syscall.S_IFIFO|0777, 0); errNo != nil {
t.Errorf("Mknod %v", errNo)
}
if fi, err := os.Lstat(tc.origFile); err != nil {
t.Errorf("Lstat(%q): %v", tc.origFile, err)
} else if fi.Mode()&os.ModeNamedPipe == 0 {
t.Errorf("Expected FIFO filetype, got %x", fi.Mode())
}
}
// Test that READDIR works even if the directory is renamed after the OPENDIR.
// This checks that the fix for https://github.com/hanwen/go-fuse/issues/252
// does not break this case.
func TestReaddirRename(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
tc.Mkdir(tc.origSubdir, 0777)
tc.WriteFile(tc.origSubdir+"/file.txt", []byte("foo"), 0700)
dir, err := os.Open(tc.mountSubdir)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer dir.Close()
err = os.Rename(tc.mountSubdir, tc.mountSubdir+".2")
if err != nil {
t.Fatalf("Rename failed: %v", err)
}
names, err := dir.Readdirnames(-1)
if err != nil {
t.Fatalf("Readdirnames failed: %v", err)
}
if len(names) != 1 || names[0] != "file.txt" {
t.Fatalf("incorrect directory listing: %v", names)
}
}
func TestFSync(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
contents := []byte{1, 2, 3}
tc.WriteFile(tc.origFile, []byte(contents), 0700)
f, err := os.OpenFile(tc.mountFile, os.O_WRONLY, 0)
if err != nil {
t.Fatalf("OpenFile(%q): %v", tc.mountFile, err)
}
defer f.Close()
if _, err := f.WriteString("hello there"); err != nil {
t.Fatalf("WriteString failed: %v", err)
}
// How to really test fsync ?
err = syscall.Fsync(int(f.Fd()))
if err != nil {
t.Errorf("fsync returned: %v", err)
}
}
func TestReadZero(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
tc.WriteFile(tc.origFile, []byte{}, 0644)
back, err := os.ReadFile(tc.mountFile)
if err != nil {
t.Fatalf("ReadFile(%q): %v", tc.mountFile, err)
} else if len(back) != 0 {
t.Errorf("content length: got %d want %d", len(back), 0)
}
}
func CompareSlices(t *testing.T, got, want []byte) {
if len(got) != len(want) {
t.Errorf("content length: got %d want %d", len(got), len(want))
return
}
for i := range want {
if want[i] != got[i] {
t.Errorf("content mismatch byte %d, got %d want %d.", i, got[i], want[i])
break
}
}
}
// Check that reading large files doesn't lead to large allocations.
func TestReadLargeMemCheck(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
content := randomData(385 * 1023)
tc.WriteFile(tc.origFile, []byte(content), 0644)
f, err := os.Open(tc.mountFile)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
buf := make([]byte, len(content)+1024)
f.Read(buf)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
f.Close()
runtime.GC()
var before, after runtime.MemStats
N := 100
runtime.ReadMemStats(&before)
for i := 0; i < N; i++ {
f, _ := os.Open(tc.mountFile)
f.Read(buf)
f.Close()
}
runtime.ReadMemStats(&after)
delta := int((after.TotalAlloc - before.TotalAlloc))
delta = (delta - 40000) / N
t.Logf("bytes per read loop: %d", delta)
}
func TestReadLarge(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
content := randomData(385 * 1023)
tc.WriteFile(tc.origFile, []byte(content), 0644)
back, err := os.ReadFile(tc.mountFile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
CompareSlices(t, back, content)
}
func TestWriteLarge(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
content := randomData(385 * 1023)
tc.WriteFile(tc.mountFile, []byte(content), 0644)
back, err := os.ReadFile(tc.origFile)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
CompareSlices(t, back, content)
}
func randomLengthString(length int) string {
r := rand.Intn(length)
b := make([]byte, r)
for i := 0; i < r; i++ {
b[i] = byte(i%10) + byte('0')
}
return string(b)
}
func TestLargeDirRead(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
created := 100
names := make([]string, created)
subdir := filepath.Join(tc.orig, "readdirSubdir")
tc.Mkdir(subdir, 0700)
longname := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
nameSet := make(map[string]bool)
for i := 0; i < created; i++ {
// Should vary file name length.
base := fmt.Sprintf("file%d.%s", i,
randomLengthString(len(longname)))
name := filepath.Join(subdir, base)
if nameSet[base] {
t.Fatalf("duplicate name %q", base)
}
nameSet[base] = true
tc.WriteFile(name, []byte("bla"), 0777)
names[i] = name
}
dir, err := os.Open(filepath.Join(tc.mnt, "readdirSubdir"))
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer dir.Close()
// Chunked read.
total := 0
readSet := make(map[string]bool)
for {
namesRead, err := dir.Readdirnames(200)
if len(namesRead) == 0 || err == io.EOF {
break
}
if err != nil {
t.Fatalf("Readdirnames failed: %v", err)
}
for _, v := range namesRead {
readSet[v] = true
}
total += len(namesRead)
}
if total != created {
t.Errorf("readdir mismatch got %v wanted %v", total, created)
}
for k := range nameSet {
_, ok := readSet[k]
if !ok {
t.Errorf("Name %v not found in output", k)
}
}
}
func ioctl(fd int, cmd int, arg uintptr) (int, int) {
r0, _, e1 := syscall.Syscall(
syscall.SYS_IOCTL, uintptr(fd), uintptr(cmd), uintptr(arg))
val := int(r0)
errno := int(e1)
return val, errno
}
func TestIoctl(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
f, err := os.OpenFile(filepath.Join(tc.mnt, "hello.txt"),
os.O_WRONLY|os.O_CREATE, 0777)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer f.Close()
ioctl(int(f.Fd()), 0x5401, 42)
}
// This test is racy. If an external process consumes space while this
// runs, we may see spurious differences between the two statfs() calls.
func TestNonVerboseStatFs(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
empty := syscall.Statfs_t{}
s1 := empty
if err := syscall.Statfs(tc.orig, &s1); err != nil {
t.Fatal("statfs orig", err)
}
s2 := syscall.Statfs_t{}
if err := syscall.Statfs(tc.mnt, &s2); err != nil {
t.Fatal("statfs mnt", err)
}
clearStatfs(&s1)
clearStatfs(&s2)
if !reflect.DeepEqual(s1, s2) {
t.Errorf("statfs mismatch %#v != %#v", s1, s2)
}
}
func TestNonVerboseFStatFs(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
fOrig, err := os.OpenFile(tc.orig+"/file", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer fOrig.Close()
empty := syscall.Statfs_t{}
s1 := empty
if errno := syscall.Fstatfs(int(fOrig.Fd()), &s1); errno != nil {
t.Fatal("statfs orig", err)
}
fMnt, err := os.OpenFile(tc.mnt+"/file", os.O_RDWR, 0644)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer fMnt.Close()
s2 := empty
if errno := syscall.Fstatfs(int(fMnt.Fd()), &s2); errno != nil {
t.Fatal("statfs mnt", err)
}
clearStatfs(&s1)
clearStatfs(&s2)
if !reflect.DeepEqual(s1, s2) {
t.Errorf("statfs mismatch: %#v != %#v", s1, s2)
}
}
func TestOriginalIsSymlink(t *testing.T) {
tmpDir := t.TempDir()
orig := tmpDir + "/orig"
err := os.Mkdir(orig, 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
link := tmpDir + "/link"
mnt := tmpDir + "/mnt"
if err := os.Mkdir(mnt, 0755); err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
if err := os.Symlink("orig", link); err != nil {
t.Fatalf("Symlink failed: %v", err)
}
fs := pathfs.NewLoopbackFileSystem(link)
nfs := pathfs.NewPathNodeFs(fs, nil)
state, _, err := nodefs.MountRoot(mnt, nfs.Root(), nil)
if err != nil {
t.Fatalf("MountNodeFileSystem failed: %v", err)
}
defer state.Unmount()
go state.Serve()
if err := state.WaitMount(); err != nil {
t.Fatal("WaitMount", err)
}
if _, err := os.Lstat(mnt); err != nil {
t.Fatalf("Lstat failed: %v", err)
}
}
func TestDoubleOpen(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
tc.WriteFile(tc.orig+"/file", []byte("blabla"), 0644)
roFile, err := os.Open(tc.mnt + "/file")
if err != nil {
t.Fatalf(" failed: %v", err)
}
defer roFile.Close()
rwFile, err := os.OpenFile(tc.mnt+"/file", os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
t.Fatalf("OpenFile failed: %v", err)
}
defer rwFile.Close()
}
// Check that chgrp(1) works
func TestChgrp(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
f, err := os.Create(tc.mnt + "/file")
if err != nil {
t.Fatalf("Create failed: %v", err)
}
defer f.Close()
err = f.Chown(-1, os.Getgid())
if err != nil {
t.Errorf("Chown failed: %v", err)
}
}
func TestLookupKnownChildrenAttrCopied(t *testing.T) {
tc := NewTestCase(t)
defer tc.Cleanup()
if err := os.WriteFile(tc.mountFile, []byte("hello"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
fi, err := os.Lstat(tc.mountFile)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
mode := fi.Mode()
time.Sleep(2 * testTTL)
if fi, err = os.Lstat(tc.mountFile); err != nil {
t.Fatalf("Lstat: %v", err)
} else if fi.Mode() != mode {
t.Fatalf("got mode %o, want %o", fi.Mode(), mode)
}
}
// Check that loopback Utimens() works as expected.
// Called by TestLoopbackFileUtimens and TestLoopbackFileSystemUtimens.
//
// Parameters:
//
// path ........ path to the backing file
// utimensFn ... Utimens() function that acts on the backing file
func testLoopbackUtimens(t *testing.T, path string, utimensFn func(atime *time.Time, mtime *time.Time) fuse.Status) {
// Arbitrary date: 05/02/2018 @ 7:57pm (UTC)
t0sec := int64(1525291058)
// Read original timestamp
var st syscall.Stat_t
err := syscall.Stat(path, &st)
if err != nil {
t.Fatal("Stat", err)
}
// FromStat handles the differently-named Stat_t fields on Linux and
// Darwin
var a1 fuse.Attr
a1.FromStat(&st)
// Change atime, keep mtime
t0 := time.Unix(t0sec, 0)
status := utimensFn(&t0, nil)
if !status.Ok() {
t.Fatal("utimensFn", status)
}
err = syscall.Stat(path, &st)
if err != nil {
t.Fatal(err)
}
var a2 fuse.Attr
a2.FromStat(&st)
if a1.Mtime != a2.Mtime {
t.Errorf("mtime has changed: %v -> %v", a1.Mtime, a2.Mtime)
}
if a2.Atime != uint64(t0.Unix()) {
t.Errorf("wrong atime: got %v want %v", a2.Atime, t0.Unix())
}
// Change mtime, keep atime
t1 := time.Unix(t0sec+123, 0)
status = utimensFn(nil, &t1)
if !status.Ok() {
t.Fatal("utimensFn", status)
}
err = syscall.Stat(path, &st)
if err != nil {
t.Fatal("Stat", err)
}
var a3 fuse.Attr
a3.FromStat(&st)
if a2.Atime != a3.Atime {
t.Errorf("atime has changed: %v -> %v", a2.Atime, a3.Atime)
}
if a3.Mtime != uint64(t1.Unix()) {
t.Errorf("got mtime %v, want %v", a3.Mtime, t1.Unix())
}
// Change both mtime and atime
ta := time.Unix(t0sec+456, 0)
tm := time.Unix(t0sec+789, 0)
status = utimensFn(&ta, &tm)
if !status.Ok() {
t.Fatal("utimensFn", status)
}
err = syscall.Stat(path, &st)
if err != nil {
t.Fatal("Stat", err)
}
var a4 fuse.Attr
a4.FromStat(&st)
if a4.Atime != uint64(ta.Unix()) {
t.Errorf("got atime %v, want %v", a4.Atime, ta.Unix())
}
if a4.Mtime != uint64(tm.Unix()) {
t.Errorf("got mtime %v, want %v", a4.Mtime, tm.Unix())
}
}
// Check that loopbackFileSystem.Utimens() works as expected
func TestPathfsLoopbackFileSystemUtimens(t *testing.T) {
fs := pathfs.NewLoopbackFileSystem(os.TempDir())
f, err := os.CreateTemp("", "TestLoopbackFileSystemUtimens")
if err != nil {
t.Fatal(err)
}
path := f.Name()
name := filepath.Base(path)
f.Close()
defer syscall.Unlink(path)
utimensFn := func(atime *time.Time, mtime *time.Time) fuse.Status {
return fs.Utimens(name, atime, mtime, nil)
}
testLoopbackUtimens(t, path, utimensFn)
}
// Check that loopbackFile.Utimens() works as expected
func TestNodefsLoopbackFileUtimens(t *testing.T) {
f2, err := os.CreateTemp("", "TestLoopbackFileUtimens")
if err != nil {
t.Fatal(err)
}
path := f2.Name()
defer os.Remove(path)
defer f2.Close()
f := nodefs.NewLoopbackFile(f2)
utimensFn := func(atime *time.Time, mtime *time.Time) fuse.Status {
return f.Utimens(atime, mtime)
}
testLoopbackUtimens(t, path, utimensFn)
}