mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
261 lines
6.5 KiB
Go
261 lines
6.5 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"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"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"
|
|
)
|
|
|
|
type cacheFs struct {
|
|
pathfs.FileSystem
|
|
}
|
|
|
|
func (fs *cacheFs) Open(name string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
|
|
f, c := fs.FileSystem.Open(name, flags, context)
|
|
if !c.Ok() {
|
|
return f, c
|
|
}
|
|
return &nodefs.WithFlags{
|
|
File: f,
|
|
FuseFlags: fuse.FOPEN_KEEP_CACHE,
|
|
}, c
|
|
|
|
}
|
|
|
|
func setupCacheTest(t *testing.T) (string, *pathfs.PathNodeFs, func()) {
|
|
dir := t.TempDir()
|
|
os.Mkdir(dir+"/mnt", 0755)
|
|
os.Mkdir(dir+"/orig", 0755)
|
|
|
|
fs := &cacheFs{
|
|
pathfs.NewLoopbackFileSystem(dir + "/orig"),
|
|
}
|
|
pfs := pathfs.NewPathNodeFs(fs, &pathfs.PathNodeFsOptions{Debug: testutil.VerboseTest()})
|
|
|
|
mntOpts := &fuse.MountOptions{
|
|
// ask kernel not to invalidate file data automatically
|
|
ExplicitDataCacheControl: true,
|
|
|
|
Debug: testutil.VerboseTest(),
|
|
}
|
|
|
|
opts := nodefs.NewOptions()
|
|
opts.AttrTimeout = 10 * time.Millisecond
|
|
opts.Debug = testutil.VerboseTest()
|
|
state, _, err := nodefs.Mount(dir+"/mnt", pfs.Root(), mntOpts, opts)
|
|
if err != nil {
|
|
t.Fatalf("MountNodeFileSystem failed: %v", err)
|
|
}
|
|
go state.Serve()
|
|
if err := state.WaitMount(); err != nil {
|
|
t.Fatal("WaitMount", err)
|
|
}
|
|
return dir, pfs, func() {
|
|
state.Unmount()
|
|
}
|
|
}
|
|
|
|
func TestFopenKeepCache(t *testing.T) {
|
|
if runtime.GOOS == "darwin" {
|
|
t.Skip("FOPEN_KEEP_CACHE is broken on Darwin.")
|
|
}
|
|
|
|
wd, pathfs, clean := setupCacheTest(t)
|
|
defer clean()
|
|
|
|
// x{read,write}File reads/writes file@path and fail on error
|
|
xreadFile := func(path string) string {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(data)
|
|
}
|
|
xwriteFile := func(path, data string) {
|
|
if err := os.WriteFile(path, []byte(data), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// xstat stats path and fails on error
|
|
xstat := func(path string) os.FileInfo {
|
|
st, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return st
|
|
}
|
|
|
|
// Without CAP_EXPLICIT_INVAL_DATA Linux FUSE client automatically
|
|
// invalidates cache of a file if it sees size change. As workaround we
|
|
// keep len(before) == len(after) to avoid that codepath.
|
|
//
|
|
// With CAP_EXPLICIT_INVAL_DATA - even when data cache stays the same,
|
|
// but st_size changes, Linux reads file content as dcache[:st_size],
|
|
// i.e. either truncated (st_size↓) or extended (st_size↑). Here
|
|
// len(before) == len(after) also helps to avoid dealing with those
|
|
// peculiarities.
|
|
before := "before"
|
|
after := "afterX"
|
|
if len(before) != len(after) {
|
|
t.Fatal("len(before) != len(after)")
|
|
}
|
|
|
|
xwriteFile(wd+"/orig/file.txt", before)
|
|
mtimeBefore := xstat(wd + "/orig/file.txt").ModTime()
|
|
c := xreadFile(wd + "/mnt/file.txt")
|
|
if c != before {
|
|
t.Fatalf("ReadFile: got %q, want %q", c, before)
|
|
}
|
|
|
|
// sleep a bit to make sure mtime of file for before and after are different
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
xwriteFile(wd+"/orig/file.txt", after)
|
|
mtimeAfter := xstat(wd + "/orig/file.txt").ModTime()
|
|
if δ := mtimeAfter.Sub(mtimeBefore); δ == 0 {
|
|
t.Fatal("mtime(orig/before) == mtime(orig/after)")
|
|
}
|
|
|
|
// sleep enough time for file attributes to expire; restat the file after.
|
|
// this forces kernel client to relookup/regetattr the file and reread the attributes.
|
|
//
|
|
// this way we make sure the kernel knows updated size/mtime before we
|
|
// try to read the file next time, which should invalidate data cache
|
|
// if CAP_EXPLICIT_INVAL_DATA was not negotiated.
|
|
time.Sleep(100 * time.Millisecond)
|
|
_ = xstat(wd + "/mnt/file.txt")
|
|
|
|
c = xreadFile(wd + "/mnt/file.txt")
|
|
if c != before {
|
|
t.Fatalf("ReadFile: got %q, want cached %q", c, before)
|
|
}
|
|
|
|
if minor := pathfs.Connector().Server().KernelSettings().Minor; minor < 12 {
|
|
t.Skipf("protocol v%d has no notify support.", minor)
|
|
}
|
|
|
|
code := pathfs.FileNotify("file.txt", 0, 0) // invalidate direntry and whole data cache
|
|
if !code.Ok() {
|
|
t.Errorf("FileNotify: %v", code)
|
|
}
|
|
|
|
c = xreadFile(wd + "/mnt/file.txt")
|
|
if c != after {
|
|
t.Fatalf("ReadFile: got %q after notify, want %q", c, after)
|
|
}
|
|
}
|
|
|
|
type nonseekFs struct {
|
|
pathfs.FileSystem
|
|
Length int
|
|
}
|
|
|
|
func (fs *nonseekFs) GetAttr(name string, context *fuse.Context) (fi *fuse.Attr, status fuse.Status) {
|
|
if name == "file" {
|
|
return &fuse.Attr{Mode: fuse.S_IFREG | 0644}, fuse.OK
|
|
}
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
func (fs *nonseekFs) Open(name string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) {
|
|
if name != "file" {
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
data := bytes.Repeat([]byte{42}, fs.Length)
|
|
f := nodefs.NewDataFile(data)
|
|
return &nodefs.WithFlags{
|
|
File: f,
|
|
FuseFlags: fuse.FOPEN_NONSEEKABLE,
|
|
}, fuse.OK
|
|
}
|
|
|
|
func TestNonseekable(t *testing.T) {
|
|
fs := &nonseekFs{FileSystem: pathfs.NewDefaultFileSystem()}
|
|
fs.Length = 200 * 1024
|
|
|
|
dir := t.TempDir()
|
|
nfs := pathfs.NewPathNodeFs(fs, nil)
|
|
opts := nodefs.NewOptions()
|
|
opts.Debug = testutil.VerboseTest()
|
|
state, _, err := nodefs.MountRoot(dir, nfs.Root(), opts)
|
|
if err != nil {
|
|
t.Fatalf("failed: %v", err)
|
|
}
|
|
defer state.Unmount()
|
|
|
|
go state.Serve()
|
|
if err := state.WaitMount(); err != nil {
|
|
t.Fatal("WaitMount", err)
|
|
}
|
|
|
|
f, err := os.Open(dir + "/file")
|
|
if err != nil {
|
|
t.Fatalf("failed: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
b := make([]byte, 200)
|
|
n, err := f.ReadAt(b, 20)
|
|
if err == nil || n > 0 {
|
|
t.Errorf("file was opened nonseekable, but seek successful")
|
|
}
|
|
}
|
|
|
|
func TestGetAttrRace(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.Mkdir(dir+"/mnt", 0755)
|
|
os.Mkdir(dir+"/orig", 0755)
|
|
|
|
fs := pathfs.NewLoopbackFileSystem(dir + "/orig")
|
|
pfs := pathfs.NewPathNodeFs(fs, &pathfs.PathNodeFsOptions{Debug: testutil.VerboseTest()})
|
|
state, _, err := nodefs.MountRoot(dir+"/mnt", pfs.Root(),
|
|
&nodefs.Options{Debug: testutil.VerboseTest()})
|
|
if err != nil {
|
|
t.Fatalf("MountNodeFileSystem failed: %v", err)
|
|
}
|
|
go state.Serve()
|
|
if err := state.WaitMount(); err != nil {
|
|
t.Fatal("WaitMount", err)
|
|
}
|
|
|
|
defer state.Unmount()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
n := 100
|
|
wg.Add(n)
|
|
var statErr error
|
|
for i := 0; i < n; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
fn := dir + "/mnt/file"
|
|
err := os.WriteFile(fn, []byte{42}, 0644)
|
|
if err != nil {
|
|
statErr = err
|
|
return
|
|
}
|
|
_, err = os.Lstat(fn)
|
|
if err != nil {
|
|
statErr = err
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if statErr != nil {
|
|
t.Error(statErr)
|
|
}
|
|
}
|