master
go 166 lines 4.88 KB
Raw
1 //go:build (linux || darwin || freebsd) && !nofuse
2
3 // Unit tests for the /mfs FUSE mount.
4 // Generic writable operations are exercised by the shared suite in
5 // fusetest.RunWritableSuite. This file contains the mount factory
6 // and MFS-specific tests only.
7
8 package mfs
9
10 import (
11 "bytes"
12 "context"
13 "crypto/rand"
14 "os"
15 "syscall"
16 "testing"
17
18 "github.com/hanwen/go-fuse/v2/fs"
19 "github.com/hanwen/go-fuse/v2/fuse"
20 "github.com/stretchr/testify/require"
21
22 "github.com/ipfs/kubo/config"
23 "github.com/ipfs/kubo/core"
24 "github.com/ipfs/kubo/core/node"
25 "github.com/ipfs/kubo/fuse/fusetest"
26 fusemnt "github.com/ipfs/kubo/fuse/mount"
27 "github.com/ipfs/kubo/fuse/writable"
28 )
29
30 func testMount(t *testing.T, root fs.InodeEmbedder) string {
31 t.Helper()
32 return fusetest.TestMount(t, root, &fs.Options{
33 EntryTimeout: &mutableCacheTime,
34 AttrTimeout: &mutableCacheTime,
35 MountOptions: fuse.MountOptions{
36 MaxReadAhead: fusemnt.MaxReadAhead,
37 ExtraCapabilities: fusemnt.WritableMountCapabilities,
38 },
39 })
40 }
41
42 func mfsMount(t *testing.T, cfg writable.Config) string {
43 t.Helper()
44 ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
45 require.NoError(t, err)
46
47 mountsCfg := config.Mounts{}
48 if cfg.StoreMtime {
49 mountsCfg.StoreMtime = config.True
50 }
51 if cfg.StoreMode {
52 mountsCfg.StoreMode = config.True
53 }
54 root := NewFileSystem(ipfs, mountsCfg, config.Import{})
55 return testMount(t, root)
56 }
57
58 func TestWritableSuite(t *testing.T) {
59 fusetest.RunWritableSuite(t, mfsMount)
60 }
61
62 // TestPersistence verifies that file data survives unmount and remount
63 // on the same IpfsNode.
64 func TestPersistence(t *testing.T) {
65 ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
66 require.NoError(t, err)
67
68 content := make([]byte, 8196)
69 _, err = rand.Read(content)
70 require.NoError(t, err)
71
72 t.Run("write", func(t *testing.T) {
73 root := NewFileSystem(ipfs, config.Mounts{}, config.Import{})
74 mntDir := testMount(t, root)
75
76 f, err := os.Create(mntDir + "/testpersistence")
77 require.NoError(t, err)
78 _, err = f.Write(content)
79 require.NoError(t, err)
80 require.NoError(t, f.Close())
81 })
82 t.Run("read", func(t *testing.T) {
83 root := NewFileSystem(ipfs, config.Mounts{}, config.Import{})
84 mntDir := testMount(t, root)
85
86 got, err := os.ReadFile(mntDir + "/testpersistence")
87 require.NoError(t, err)
88 require.True(t, bytes.Equal(content, got))
89 })
90 }
91
92 // TestStatBlocks verifies that stat(2) on entries in /mfs populates
93 // st_blocks (used by du and ls -s) consistent with the file size, and
94 // that st_blksize advertises the chunker size MFS will use for writes
95 // so tools can align their I/O buffers.
96 func TestStatBlocks(t *testing.T) {
97 const chunkerStr = "size-65536"
98 const wantBlksize uint32 = 65536
99
100 ipfs, err := core.NewNode(t.Context(), &node.BuildCfg{})
101 require.NoError(t, err)
102
103 kuboCfg := config.Import{UnixFSChunker: *config.NewOptionalString(chunkerStr)}
104 root := NewFileSystem(ipfs, config.Mounts{}, kuboCfg)
105 mntDir := testMount(t, root)
106
107 t.Run("multi-block file", func(t *testing.T) {
108 // >1 MiB ensures the UnixFS DAG has multiple leaves under the
109 // configured 64 KiB chunker.
110 content := make([]byte, 1024*1024+1)
111 _, err := rand.Read(content)
112 require.NoError(t, err)
113 fpath := mntDir + "/big"
114 require.NoError(t, os.WriteFile(fpath, content, 0o644))
115 fusetest.AssertStatBlocks(t, fpath, wantBlksize)
116 })
117
118 t.Run("small single-chunk file", func(t *testing.T) {
119 fpath := mntDir + "/small"
120 require.NoError(t, os.WriteFile(fpath, []byte("hello"), 0o644))
121 fusetest.AssertStatBlocks(t, fpath, wantBlksize)
122 })
123
124 t.Run("directory", func(t *testing.T) {
125 dpath := mntDir + "/d"
126 require.NoError(t, os.Mkdir(dpath, 0o755))
127 info, err := os.Stat(dpath)
128 require.NoError(t, err)
129 st, ok := info.Sys().(*syscall.Stat_t)
130 require.True(t, ok)
131 require.EqualValues(t, 1, st.Blocks, "directory should report 1 nominal block")
132 require.EqualValues(t, wantBlksize, st.Blksize)
133 })
134
135 t.Run("symlink", func(t *testing.T) {
136 const target = "../some/target"
137 lpath := mntDir + "/link"
138 require.NoError(t, os.Symlink(target, lpath))
139 info, err := os.Lstat(lpath)
140 require.NoError(t, err)
141 st, ok := info.Sys().(*syscall.Stat_t)
142 require.True(t, ok)
143 require.EqualValues(t, len(target), st.Size)
144 require.EqualValues(t, 1, st.Blocks)
145 require.EqualValues(t, wantBlksize, st.Blksize)
146 })
147 }
148
149 // TestStatfs verifies that statfs on the /mfs mount reports the disk
150 // space of the repo's backing filesystem. macOS Finder refuses to copy
151 // files onto a volume that reports zero free space.
152 func TestStatfs(t *testing.T) {
153 ipfs, err := core.NewNode(t.Context(), &node.BuildCfg{})
154 require.NoError(t, err)
155
156 // The default in-memory repo returns "" for Path(), so point
157 // RepoPath at a real directory to exercise the syscall path.
158 repoDir := t.TempDir()
159 root := writable.NewDir(ipfs.FilesRoot.GetDirectory(), &writable.Config{
160 DAG: ipfs.DAG,
161 RepoPath: repoDir,
162 })
163 mntDir := testMount(t, root)
164
165 fusetest.AssertStatfsNonZero(t, mntDir)
166 }