github.com/grailbio/base@v0.0.11/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/grailbio/base/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  }