github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/fsnodefuse/readdirplus_test.go (about) 1 package fsnodefuse 2 3 import ( 4 "context" 5 "fmt" 6 "math/rand" 7 "os" 8 "sync/atomic" 9 "testing" 10 "time" 11 12 "github.com/Schaudge/grailbase/file/fsnode" 13 "github.com/grailbio/testutil" 14 "github.com/hanwen/go-fuse/v2/fs" 15 "github.com/hanwen/go-fuse/v2/fuse" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 "golang.org/x/sync/errgroup" 19 ) 20 21 // TestReaddirplus verifies that servicing a READDIRPLUS request does not 22 // trigger calls to (fsnode.Parent).Child. Note that this test uses 23 // (*os.File).Readdirnames to trigger the READDIRPLUS request. 24 func TestReaddirplus(t *testing.T) { 25 const NumChildren = 1000 26 children := makeTestChildren(NumChildren) 27 root := newParent("root", children) 28 withMounted(t, root, func(mountDir string) { 29 err := checkDir(t, children, mountDir) 30 require.NoError(t, err) 31 assert.Equal(t, int64(0), root.childCalls) 32 }) 33 } 34 35 // TestReaddirplusConcurrent verifies that servicing many concurrent 36 // READDIRPLUS requests does not trigger any calls to (fsnode.Parent).Child. 37 // Note that this test uses (*os.File).Readdirnames to trigger the READDIRPLUS 38 // requests. 39 func TestReaddirplusConcurrent(t *testing.T) { 40 const ( 41 NumIter = 20 42 MaxNumChildren = 1000 43 MaxConcurrentReaddirs = 100 44 ) 45 // Note that specifying a constant seed does not make this test 46 // deterministic, as the concurrent READDIRPLUS callers race 47 // non-deterministically. 48 r := rand.New(rand.NewSource(0)) 49 for i := 0; i < NumIter; i++ { 50 var ( 51 numChildren = r.Intn(MaxNumChildren-1) + 1 52 concurrentReaddirs = r.Intn(MaxConcurrentReaddirs-2) + 2 53 children = makeTestChildren(numChildren) 54 root = newParent("root", children) 55 ) 56 t.Run(fmt.Sprintf("iter%02d", i), func(t *testing.T) { 57 t.Logf( 58 "numChildren=%d concurrentReaddirs=%d", 59 numChildren, 60 concurrentReaddirs, 61 ) 62 withMounted(t, root, func(mountDir string) { 63 var grp errgroup.Group 64 for j := 0; j < concurrentReaddirs; j++ { 65 grp.Go(func() error { 66 return checkDir(t, children, mountDir) 67 }) 68 } 69 require.NoError(t, grp.Wait()) 70 assert.Equal(t, int64(0), root.childCalls) 71 }) 72 }) 73 } 74 } 75 76 func makeTestChildren(n int) []fsnode.T { 77 children := make([]fsnode.T, n) 78 for i := range children { 79 children[i] = fsnode.ConstLeaf( 80 fsnode.NewRegInfo(fmt.Sprintf("%04d", i)), 81 []byte{}, 82 ) 83 } 84 return children 85 } 86 87 // withMounted sets up and tears down a FUSE mount for root. 88 // f is called with the path where root is mounted. 89 func withMounted(t *testing.T, root fsnode.T, f func(rootPath string)) { 90 mountDir, cleanUp := testutil.TempDir(t, "", "fsnodefuse-testreaddirplus") 91 defer cleanUp() 92 server, err := fs.Mount(mountDir, NewRoot(root), &fs.Options{ 93 MountOptions: fuse.MountOptions{ 94 FsName: "test", 95 DisableXAttrs: true, 96 }, 97 }) 98 require.NoError(t, err, "mounting %q", mountDir) 99 defer func() { 100 assert.NoError(t, server.Unmount(), 101 "unmount of FUSE mounted at %q failed; may need manual cleanup", 102 mountDir, 103 ) 104 }() 105 f(mountDir) 106 } 107 108 func checkDir(t *testing.T, children []fsnode.T, path string) (err error) { 109 var want []string 110 for _, c := range children { 111 want = append(want, c.Info().Name()) 112 } 113 f, err := os.Open(path) 114 if err != nil { 115 return err 116 } 117 defer func() { assert.NoError(t, f.Close()) }() 118 // Use Readdirnames instead of Readdir because Readdir adds an extra call 119 // lstat outside of the READDIRPLUS operation. 120 got, err := f.Readdirnames(0) 121 // Sanity check that the names of the entries match the children. 122 assert.ElementsMatch(t, want, got) 123 return err 124 } 125 126 type parent struct { 127 fsnode.Parent 128 childCalls int64 129 } 130 131 func (p *parent) Child(ctx context.Context, name string) (fsnode.T, error) { 132 atomic.AddInt64(&p.childCalls, 1) 133 return p.Parent.Child(ctx, name) 134 } 135 136 // CacheableFor implements fsnode.Cacheable. 137 func (p parent) CacheableFor() time.Duration { 138 return fsnode.CacheableFor(p.Parent) 139 } 140 141 func newParent(name string, children []fsnode.T) *parent { 142 return &parent{ 143 Parent: fsnode.NewParent( 144 fsnode.NewDirInfo("root"), 145 fsnode.ConstChildren(children...), 146 ), 147 } 148 }