kubo/fuse/mfs/mfs_test.go
Sergey Gorbunov 7c844bacea
feat(fuse): Expose MFS as FUSE mount point (#10781)
* Add MFS command line options, extend existing mount functions for MFS, set defaults.

* Directory listing and file stat.

* Add a read-only MFS view.

* Add mkdir and interface checks.

* Add remove and rename functionality.

* Implement all required write interfaces.

* Adjust mount  functions for other architechtures.

* Merge branch 'master' into feat/10710-mfs-fuse-mount

* Write a basic read/write test.

* Write more basic tests, add a mutex to the file object, fix modtime.

* Add a concurrency test, remove mutexes from file and directory structures.

* Refactor naming(mfdir -> mfsdir) and add documentation.

* Add CID retrieval through ipfs_cid xattr.

* Add docs, add xattr listing, fix bugs for mv and stat, refactor.

* Add MFS command line options, extend existing mount functions for MFS, set defaults.

* docs phrasing

* docs: Mounts.MFS

* docs: warn about lazy-loaded DAGs

* test: TEST_FUSE=1 ./t0030-mount.sh -v

---------

Co-authored-by: Guillaume Michel <guillaumemichel@users.noreply.github.com>
Co-authored-by: guillaumemichel <guillaume@michel.id>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
2025-05-06 21:55:53 +02:00

343 lines
6.2 KiB
Go

//go:build !nofuse && !openbsd && !netbsd && !plan9
// +build !nofuse,!openbsd,!netbsd,!plan9
package mfs
import (
"bytes"
"context"
"crypto/rand"
"errors"
iofs "io/fs"
"os"
"slices"
"strconv"
"testing"
"time"
"bazil.org/fuse"
"bazil.org/fuse/fs"
"bazil.org/fuse/fs/fstestutil"
"github.com/ipfs/kubo/core"
"github.com/ipfs/kubo/core/node"
"github.com/libp2p/go-libp2p-testing/ci"
)
// Create an Ipfs.Node, a filesystem and a mount point.
func setUp(t *testing.T, ipfs *core.IpfsNode) (fs.FS, *fstestutil.Mount) {
if ci.NoFuse() {
t.Skip("Skipping FUSE tests")
}
if ipfs == nil {
var err error
ipfs, err = core.NewNode(context.Background(), &node.BuildCfg{})
if err != nil {
t.Fatal(err)
}
}
fs := NewFileSystem(ipfs)
mnt, err := fstestutil.MountedT(t, fs, nil)
if err == fuse.ErrOSXFUSENotFound {
t.Skip(err)
}
if err != nil {
t.Fatal(err)
}
return fs, mnt
}
// Test reading and writing a file.
func TestReadWrite(t *testing.T) {
_, mnt := setUp(t, nil)
defer mnt.Close()
path := mnt.Dir + "/testrw"
content := make([]byte, 8196)
_, err := rand.Read(content)
if err != nil {
t.Fatal(err)
}
t.Run("write", func(t *testing.T) {
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(content)
if err != nil {
t.Fatal(err)
}
})
t.Run("read", func(t *testing.T) {
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
buf := make([]byte, 8196)
l, err := f.Read(buf)
if err != nil {
t.Fatal(err)
}
if bytes.Equal(content, buf[:l]) != true {
t.Fatal("read and write not equal")
}
})
}
// Test creating a directory.
func TestMkdir(t *testing.T) {
_, mnt := setUp(t, nil)
defer mnt.Close()
path := mnt.Dir + "/foo/bar/baz/qux/quux"
t.Run("write", func(t *testing.T) {
err := os.MkdirAll(path, iofs.ModeDir)
if err != nil {
t.Fatal(err)
}
})
t.Run("read", func(t *testing.T) {
stat, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if !stat.IsDir() {
t.Fatal("not dir")
}
})
}
// Test file persistence across mounts.
func TestPersistence(t *testing.T) {
ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
if err != nil {
t.Fatal(err)
}
content := make([]byte, 8196)
_, err = rand.Read(content)
if err != nil {
t.Fatal(err)
}
t.Run("write", func(t *testing.T) {
_, mnt := setUp(t, ipfs)
defer mnt.Close()
path := mnt.Dir + "/testpersistence"
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(content)
if err != nil {
t.Fatal(err)
}
})
t.Run("read", func(t *testing.T) {
_, mnt := setUp(t, ipfs)
defer mnt.Close()
path := mnt.Dir + "/testpersistence"
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
buf := make([]byte, 8196)
l, err := f.Read(buf)
if err != nil {
t.Fatal(err)
}
if bytes.Equal(content, buf[:l]) != true {
t.Fatal("read and write not equal")
}
})
}
// Test getting the file attributes.
func TestAttr(t *testing.T) {
_, mnt := setUp(t, nil)
defer mnt.Close()
path := mnt.Dir + "/testattr"
content := make([]byte, 8196)
_, err := rand.Read(content)
if err != nil {
t.Fatal(err)
}
t.Run("write", func(t *testing.T) {
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(content)
if err != nil {
t.Fatal(err)
}
})
t.Run("read", func(t *testing.T) {
fi, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if fi.IsDir() {
t.Fatal("file is a directory")
}
if fi.ModTime().After(time.Now()) {
t.Fatal("future modtime")
}
if time.Since(fi.ModTime()) > time.Second {
t.Fatal("past modtime")
}
if fi.Name() != "testattr" {
t.Fatal("invalid filename")
}
if fi.Size() != 8196 {
t.Fatal("invalid size")
}
})
}
// Test concurrent access to the filesystem.
func TestConcurrentRW(t *testing.T) {
_, mnt := setUp(t, nil)
defer mnt.Close()
files := 5
fileWorkers := 5
path := mnt.Dir + "/testconcurrent"
content := make([][]byte, files)
for i := range content {
content[i] = make([]byte, 8196)
_, err := rand.Read(content[i])
if err != nil {
t.Fatal(err)
}
}
t.Run("write", func(t *testing.T) {
errs := make(chan (error), 1)
for i := 0; i < files; i++ {
go func() {
var err error
defer func() { errs <- err }()
f, err := os.Create(path + strconv.Itoa(i))
if err != nil {
return
}
defer f.Close()
_, err = f.Write(content[i])
if err != nil {
return
}
}()
}
for i := 0; i < files; i++ {
err := <-errs
if err != nil {
t.Fatal(err)
}
}
})
t.Run("read", func(t *testing.T) {
errs := make(chan (error), 1)
for i := 0; i < files*fileWorkers; i++ {
go func() {
var err error
defer func() { errs <- err }()
f, err := os.Open(path + strconv.Itoa(i/fileWorkers))
if err != nil {
return
}
defer f.Close()
buf := make([]byte, 8196)
l, err := f.Read(buf)
if err != nil {
return
}
if bytes.Equal(content[i/fileWorkers], buf[:l]) != true {
err = errors.New("read and write not equal")
return
}
}()
}
for i := 0; i < files; i++ {
err := <-errs
if err != nil {
t.Fatal(err)
}
}
})
}
// Test ipfs_cid extended attribute
func TestMFSRootXattr(t *testing.T) {
ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
if err != nil {
t.Fatal(err)
}
fs, mnt := setUp(t, ipfs)
defer mnt.Close()
node, err := fs.Root()
if err != nil {
t.Fatal(err)
}
root := node.(*Dir)
listReq := fuse.ListxattrRequest{}
listRes := fuse.ListxattrResponse{}
err = root.Listxattr(context.Background(), &listReq, &listRes)
if err != nil {
t.Fatal(err)
}
if slices.Compare(listRes.Xattr, []byte("ipfs_cid\x00")) != 0 {
t.Fatal("list xattr returns invalid value")
}
getReq := fuse.GetxattrRequest{
Name: "ipfs_cid",
}
getRes := fuse.GetxattrResponse{}
err = root.Getxattr(context.Background(), &getReq, &getRes)
if err != nil {
t.Fatal(err)
}
ipldNode, err := ipfs.FilesRoot.GetDirectory().GetNode()
if err != nil {
t.Fatal(err)
}
if slices.Compare(getRes.Xattr, []byte(ipldNode.Cid().String())) != 0 {
t.Fatal("xattr cid not equal to mfs root cid")
}
}