summaryrefslogtreecommitdiffhomepage
path: root/pkg/lisafs/testsuite
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/lisafs/testsuite')
-rw-r--r--pkg/lisafs/testsuite/BUILD20
-rw-r--r--pkg/lisafs/testsuite/testsuite.go637
2 files changed, 657 insertions, 0 deletions
diff --git a/pkg/lisafs/testsuite/BUILD b/pkg/lisafs/testsuite/BUILD
new file mode 100644
index 000000000..b4a542b3a
--- /dev/null
+++ b/pkg/lisafs/testsuite/BUILD
@@ -0,0 +1,20 @@
+load("//tools:defs.bzl", "go_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+ licenses = ["notice"],
+)
+
+go_library(
+ name = "testsuite",
+ testonly = True,
+ srcs = ["testsuite.go"],
+ deps = [
+ "//pkg/abi/linux",
+ "//pkg/context",
+ "//pkg/lisafs",
+ "//pkg/unet",
+ "@com_github_syndtr_gocapability//capability:go_default_library",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
diff --git a/pkg/lisafs/testsuite/testsuite.go b/pkg/lisafs/testsuite/testsuite.go
new file mode 100644
index 000000000..476ff76a5
--- /dev/null
+++ b/pkg/lisafs/testsuite/testsuite.go
@@ -0,0 +1,637 @@
+// Copyright 2021 The gVisor Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package testsuite provides a integration testing suite for lisafs.
+// These tests are intended for servers serving the local filesystem.
+package testsuite
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "math/rand"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/syndtr/gocapability/capability"
+ "golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/abi/linux"
+ "gvisor.dev/gvisor/pkg/context"
+ "gvisor.dev/gvisor/pkg/lisafs"
+ "gvisor.dev/gvisor/pkg/unet"
+)
+
+// Tester is the client code using this test suite. This interface abstracts
+// away all the caller specific details.
+type Tester interface {
+ // NewServer returns a new instance of the tester server.
+ NewServer(t *testing.T) *lisafs.Server
+
+ // LinkSupported returns true if the backing server supports LinkAt.
+ LinkSupported() bool
+
+ // SetUserGroupIDSupported returns true if the backing server supports
+ // changing UID/GID for files.
+ SetUserGroupIDSupported() bool
+}
+
+// RunAllLocalFSTests runs all local FS tests as subtests.
+func RunAllLocalFSTests(t *testing.T, tester Tester) {
+ for name, testFn := range localFSTests {
+ t.Run(name, func(t *testing.T) {
+ runServerClient(t, tester, testFn)
+ })
+ }
+}
+
+type testFunc func(context.Context, *testing.T, Tester, lisafs.ClientFD)
+
+var localFSTests map[string]testFunc = map[string]testFunc{
+ "Stat": testStat,
+ "RegularFileIO": testRegularFileIO,
+ "RegularFileOpen": testRegularFileOpen,
+ "SetStat": testSetStat,
+ "Allocate": testAllocate,
+ "StatFS": testStatFS,
+ "Unlink": testUnlink,
+ "Symlink": testSymlink,
+ "HardLink": testHardLink,
+ "Walk": testWalk,
+ "Rename": testRename,
+ "Mknod": testMknod,
+ "Getdents": testGetdents,
+}
+
+func runServerClient(t *testing.T, tester Tester, testFn testFunc) {
+ mountPath, err := ioutil.TempDir(os.Getenv("TEST_TMPDIR"), "")
+ if err != nil {
+ t.Fatalf("creation of temporary mountpoint failed: %v", err)
+ }
+ defer os.RemoveAll(mountPath)
+
+ // fsgofer should run with a umask of 0, because we want to preserve file
+ // modes exactly for testing purposes.
+ unix.Umask(0)
+
+ serverSocket, clientSocket, err := unet.SocketPair(false)
+ if err != nil {
+ t.Fatalf("socketpair got err %v expected nil", err)
+ }
+
+ server := tester.NewServer(t)
+ conn, err := server.CreateConnection(serverSocket, false /* readonly */)
+ if err != nil {
+ t.Fatalf("starting connection failed: %v", err)
+ return
+ }
+ server.StartConnection(conn)
+
+ c, root, err := lisafs.NewClient(clientSocket, mountPath)
+ if err != nil {
+ t.Fatalf("client creation failed: %v", err)
+ }
+
+ if !root.ControlFD.Ok() {
+ t.Fatalf("root control FD is not valid")
+ }
+ rootFile := c.NewFD(root.ControlFD)
+
+ ctx := context.Background()
+ testFn(ctx, t, tester, rootFile)
+ closeFD(ctx, t, rootFile)
+
+ c.Close() // This should trigger client and server shutdown.
+ server.Wait()
+}
+
+func closeFD(ctx context.Context, t testing.TB, fdLisa lisafs.ClientFD) {
+ if err := fdLisa.Close(ctx); err != nil {
+ t.Errorf("failed to close FD: %v", err)
+ }
+}
+
+func statTo(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, stat *linux.Statx) {
+ if err := fdLisa.StatTo(ctx, stat); err != nil {
+ t.Fatalf("stat failed: %v", err)
+ }
+}
+
+func openCreateFile(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, name string) (lisafs.ClientFD, linux.Statx, lisafs.ClientFD, int) {
+ child, childFD, childHostFD, err := fdLisa.OpenCreateAt(ctx, name, unix.O_RDWR, 0777, lisafs.UID(unix.Getuid()), lisafs.GID(unix.Getgid()))
+ if err != nil {
+ t.Fatalf("OpenCreateAt failed: %v", err)
+ }
+ if childHostFD == -1 {
+ t.Error("no host FD donated")
+ }
+ client := fdLisa.Client()
+ return client.NewFD(child.ControlFD), child.Stat, fdLisa.Client().NewFD(childFD), childHostFD
+}
+
+func openFile(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, flags uint32, isReg bool) (lisafs.ClientFD, int) {
+ newFD, hostFD, err := fdLisa.OpenAt(ctx, flags)
+ if err != nil {
+ t.Fatalf("OpenAt failed: %v", err)
+ }
+ if hostFD == -1 && isReg {
+ t.Error("no host FD donated")
+ }
+ return fdLisa.Client().NewFD(newFD), hostFD
+}
+
+func unlinkFile(ctx context.Context, t *testing.T, dir lisafs.ClientFD, name string, isDir bool) {
+ var flags uint32
+ if isDir {
+ flags = unix.AT_REMOVEDIR
+ }
+ if err := dir.UnlinkAt(ctx, name, flags); err != nil {
+ t.Errorf("unlinking file %s failed: %v", name, err)
+ }
+}
+
+func symlink(ctx context.Context, t *testing.T, dir lisafs.ClientFD, name, target string) (lisafs.ClientFD, linux.Statx) {
+ linkIno, err := dir.SymlinkAt(ctx, name, target, lisafs.UID(unix.Getuid()), lisafs.GID(unix.Getgid()))
+ if err != nil {
+ t.Fatalf("symlink failed: %v", err)
+ }
+ return dir.Client().NewFD(linkIno.ControlFD), linkIno.Stat
+}
+
+func link(ctx context.Context, t *testing.T, dir lisafs.ClientFD, name string, target lisafs.ClientFD) (lisafs.ClientFD, linux.Statx) {
+ linkIno, err := dir.LinkAt(ctx, target.ID(), name)
+ if err != nil {
+ t.Fatalf("link failed: %v", err)
+ }
+ return dir.Client().NewFD(linkIno.ControlFD), linkIno.Stat
+}
+
+func mkdir(ctx context.Context, t *testing.T, dir lisafs.ClientFD, name string) (lisafs.ClientFD, linux.Statx) {
+ childIno, err := dir.MkdirAt(ctx, name, 0777, lisafs.UID(unix.Getuid()), lisafs.GID(unix.Getgid()))
+ if err != nil {
+ t.Fatalf("mkdir failed: %v", err)
+ }
+ return dir.Client().NewFD(childIno.ControlFD), childIno.Stat
+}
+
+func mknod(ctx context.Context, t *testing.T, dir lisafs.ClientFD, name string) (lisafs.ClientFD, linux.Statx) {
+ nodeIno, err := dir.MknodAt(ctx, name, unix.S_IFREG|0777, lisafs.UID(unix.Getuid()), lisafs.GID(unix.Getgid()), 0, 0)
+ if err != nil {
+ t.Fatalf("mknod failed: %v", err)
+ }
+ return dir.Client().NewFD(nodeIno.ControlFD), nodeIno.Stat
+}
+
+func walk(ctx context.Context, t *testing.T, dir lisafs.ClientFD, names []string) []lisafs.Inode {
+ _, inodes, err := dir.WalkMultiple(ctx, names)
+ if err != nil {
+ t.Fatalf("walk failed while trying to walk components %+v: %v", names, err)
+ }
+ return inodes
+}
+
+func walkStat(ctx context.Context, t *testing.T, dir lisafs.ClientFD, names []string) []linux.Statx {
+ stats, err := dir.WalkStat(ctx, names)
+ if err != nil {
+ t.Fatalf("walk failed while trying to walk components %+v: %v", names, err)
+ }
+ return stats
+}
+
+func writeFD(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, off uint64, buf []byte) error {
+ count, err := fdLisa.Write(ctx, buf, off)
+ if err != nil {
+ return err
+ }
+ if int(count) != len(buf) {
+ t.Errorf("partial write: buf size = %d, written = %d", len(buf), count)
+ }
+ return nil
+}
+
+func readFDAndCmp(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, off uint64, want []byte) {
+ buf := make([]byte, len(want))
+ n, err := fdLisa.Read(ctx, buf, off)
+ if err != nil {
+ t.Errorf("read failed: %v", err)
+ return
+ }
+ if int(n) != len(want) {
+ t.Errorf("partial read: buf size = %d, read = %d", len(want), n)
+ return
+ }
+ if bytes.Compare(buf, want) != 0 {
+ t.Errorf("bytes read differ from what was expected: want = %v, got = %v", want, buf)
+ }
+}
+
+func allocateAndVerify(ctx context.Context, t *testing.T, fdLisa lisafs.ClientFD, off uint64, length uint64) {
+ if err := fdLisa.Allocate(ctx, 0, off, length); err != nil {
+ t.Fatalf("fallocate failed: %v", err)
+ }
+
+ var stat linux.Statx
+ statTo(ctx, t, fdLisa, &stat)
+ if want := off + length; stat.Size != want {
+ t.Errorf("incorrect file size after allocate: expected %d, got %d", off+length, stat.Size)
+ }
+}
+
+func cmpStatx(t *testing.T, want, got linux.Statx) {
+ if got.Mask&unix.STATX_MODE != 0 && want.Mask&unix.STATX_MODE != 0 {
+ if got.Mode != want.Mode {
+ t.Errorf("mode differs: want %d, got %d", want.Mode, got.Mode)
+ }
+ }
+ if got.Mask&unix.STATX_INO != 0 && want.Mask&unix.STATX_INO != 0 {
+ if got.Ino != want.Ino {
+ t.Errorf("inode number differs: want %d, got %d", want.Ino, got.Ino)
+ }
+ }
+ if got.Mask&unix.STATX_NLINK != 0 && want.Mask&unix.STATX_NLINK != 0 {
+ if got.Nlink != want.Nlink {
+ t.Errorf("nlink differs: want %d, got %d", want.Nlink, got.Nlink)
+ }
+ }
+ if got.Mask&unix.STATX_UID != 0 && want.Mask&unix.STATX_UID != 0 {
+ if got.UID != want.UID {
+ t.Errorf("UID differs: want %d, got %d", want.UID, got.UID)
+ }
+ }
+ if got.Mask&unix.STATX_GID != 0 && want.Mask&unix.STATX_GID != 0 {
+ if got.GID != want.GID {
+ t.Errorf("GID differs: want %d, got %d", want.GID, got.GID)
+ }
+ }
+ if got.Mask&unix.STATX_SIZE != 0 && want.Mask&unix.STATX_SIZE != 0 {
+ if got.Size != want.Size {
+ t.Errorf("size differs: want %d, got %d", want.Size, got.Size)
+ }
+ }
+ if got.Mask&unix.STATX_BLOCKS != 0 && want.Mask&unix.STATX_BLOCKS != 0 {
+ if got.Blocks != want.Blocks {
+ t.Errorf("blocks differs: want %d, got %d", want.Blocks, got.Blocks)
+ }
+ }
+ if got.Mask&unix.STATX_ATIME != 0 && want.Mask&unix.STATX_ATIME != 0 {
+ if got.Atime != want.Atime {
+ t.Errorf("atime differs: want %d, got %d", want.Atime, got.Atime)
+ }
+ }
+ if got.Mask&unix.STATX_MTIME != 0 && want.Mask&unix.STATX_MTIME != 0 {
+ if got.Mtime != want.Mtime {
+ t.Errorf("mtime differs: want %d, got %d", want.Mtime, got.Mtime)
+ }
+ }
+ if got.Mask&unix.STATX_CTIME != 0 && want.Mask&unix.STATX_CTIME != 0 {
+ if got.Ctime != want.Ctime {
+ t.Errorf("ctime differs: want %d, got %d", want.Ctime, got.Ctime)
+ }
+ }
+}
+
+func hasCapability(c capability.Cap) bool {
+ caps, err := capability.NewPid2(os.Getpid())
+ if err != nil {
+ return false
+ }
+ if err := caps.Load(); err != nil {
+ return false
+ }
+ return caps.Get(capability.EFFECTIVE, c)
+}
+
+func testStat(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ var rootStat linux.Statx
+ if err := root.StatTo(ctx, &rootStat); err != nil {
+ t.Errorf("stat on the root dir failed: %v", err)
+ }
+
+ if ftype := rootStat.Mode & unix.S_IFMT; ftype != unix.S_IFDIR {
+ t.Errorf("root inode is not a directory, file type = %d", ftype)
+ }
+}
+
+func testRegularFileIO(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ controlFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ // Test Read/Write RPCs.
+ data := make([]byte, 100)
+ rand.Read(data)
+ if err := writeFD(ctx, t, fd, 0, data); err != nil {
+ t.Fatalf("write failed: %v", err)
+ }
+ readFDAndCmp(ctx, t, fd, 0, data)
+ readFDAndCmp(ctx, t, fd, 50, data[50:])
+
+ // Make sure the host FD is configured properly.
+ hostReadData := make([]byte, len(data))
+ if n, err := unix.Pread(hostFD, hostReadData, 0); err != nil {
+ t.Errorf("host read failed: %v", err)
+ } else if n != len(hostReadData) {
+ t.Errorf("partial read: buf size = %d, read = %d", len(hostReadData), n)
+ } else if bytes.Compare(hostReadData, data) != 0 {
+ t.Errorf("bytes read differ from what was expected: want = %v, got = %v", data, hostReadData)
+ }
+
+ // Test syncing the writable FD.
+ if err := fd.Sync(ctx); err != nil {
+ t.Errorf("syncing the FD failed: %v", err)
+ }
+}
+
+func testRegularFileOpen(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ controlFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ // Open a readonly FD and try writing to it to get an EBADF.
+ roFile, roHostFD := openFile(ctx, t, controlFile, unix.O_RDONLY, true /* isReg */)
+ defer closeFD(ctx, t, roFile)
+ defer unix.Close(roHostFD)
+ if err := writeFD(ctx, t, roFile, 0, []byte{1, 2, 3}); err != unix.EBADF {
+ t.Errorf("writing to read only FD should generate EBADF, but got %v", err)
+ }
+}
+
+func testSetStat(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ controlFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ now := time.Now()
+ wantStat := linux.Statx{
+ Mask: unix.STATX_MODE | unix.STATX_ATIME | unix.STATX_MTIME | unix.STATX_SIZE,
+ Mode: 0760,
+ UID: uint32(unix.Getuid()),
+ GID: uint32(unix.Getgid()),
+ Size: 50,
+ Atime: linux.NsecToStatxTimestamp(now.UnixNano()),
+ Mtime: linux.NsecToStatxTimestamp(now.UnixNano()),
+ }
+ if tester.SetUserGroupIDSupported() {
+ wantStat.Mask |= unix.STATX_UID | unix.STATX_GID
+ }
+ failureMask, failureErr, err := controlFile.SetStat(ctx, &wantStat)
+ if err != nil {
+ t.Fatalf("setstat failed: %v", err)
+ }
+ if failureMask != 0 {
+ t.Fatalf("some setstat operations failed: failureMask = %#b, failureErr = %v", failureMask, failureErr)
+ }
+
+ // Verify that attributes were updated.
+ var gotStat linux.Statx
+ statTo(ctx, t, controlFile, &gotStat)
+ if gotStat.Mode&07777 != wantStat.Mode ||
+ gotStat.Size != wantStat.Size ||
+ gotStat.Atime.ToNsec() != wantStat.Atime.ToNsec() ||
+ gotStat.Mtime.ToNsec() != wantStat.Mtime.ToNsec() ||
+ (tester.SetUserGroupIDSupported() && (uint32(gotStat.UID) != wantStat.UID || uint32(gotStat.GID) != wantStat.GID)) {
+ t.Errorf("setStat did not update file correctly: setStat = %+v, stat = %+v", wantStat, gotStat)
+ }
+}
+
+func testAllocate(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ controlFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ allocateAndVerify(ctx, t, fd, 0, 40)
+ allocateAndVerify(ctx, t, fd, 20, 100)
+}
+
+func testStatFS(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ var statFS lisafs.StatFS
+ if err := root.StatFSTo(ctx, &statFS); err != nil {
+ t.Errorf("statfs failed: %v", err)
+ }
+}
+
+func testUnlink(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ controlFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ unlinkFile(ctx, t, root, name, false /* isDir */)
+ if inodes := walk(ctx, t, root, []string{name}); len(inodes) > 0 {
+ t.Errorf("deleted file should not be generating inodes on walk: %+v", inodes)
+ }
+}
+
+func testSymlink(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ target := "/tmp/some/path"
+ name := "symlinkFile"
+ link, linkStat := symlink(ctx, t, root, name, target)
+ defer closeFD(ctx, t, link)
+
+ if linkStat.Mode&unix.S_IFMT != unix.S_IFLNK {
+ t.Errorf("stat return from symlink RPC indicates that the inode is not a symlink: mode = %d", linkStat.Mode)
+ }
+
+ if gotTarget, err := link.ReadLinkAt(ctx); err != nil {
+ t.Fatalf("readlink failed: %v", err)
+ } else if gotTarget != target {
+ t.Errorf("readlink return incorrect target: expected %q, got %q", target, gotTarget)
+ }
+}
+
+func testHardLink(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ if !tester.LinkSupported() {
+ t.Skipf("server does not support LinkAt RPC")
+ }
+ if !hasCapability(capability.CAP_DAC_READ_SEARCH) {
+ t.Skipf("TestHardLink requires CAP_DAC_READ_SEARCH, running as %d", unix.Getuid())
+ }
+ name := "tempFile"
+ controlFile, fileIno, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, controlFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ link, linkStat := link(ctx, t, root, name, controlFile)
+ defer closeFD(ctx, t, link)
+
+ if linkStat.Ino != fileIno.Ino {
+ t.Errorf("hard linked files have different inode numbers: %d %d", linkStat.Ino, fileIno.Ino)
+ }
+ if linkStat.DevMinor != fileIno.DevMinor {
+ t.Errorf("hard linked files have different minor device numbers: %d %d", linkStat.DevMinor, fileIno.DevMinor)
+ }
+ if linkStat.DevMajor != fileIno.DevMajor {
+ t.Errorf("hard linked files have different major device numbers: %d %d", linkStat.DevMajor, fileIno.DevMajor)
+ }
+}
+
+func testWalk(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ // Create 10 nested directories.
+ n := 10
+ curDir := root
+
+ dirNames := make([]string, 0, n)
+ for i := 0; i < n; i++ {
+ name := fmt.Sprintf("tmpdir-%d", i)
+ childDir, _ := mkdir(ctx, t, curDir, name)
+ defer closeFD(ctx, t, childDir)
+ defer unlinkFile(ctx, t, curDir, name, true /* isDir */)
+
+ curDir = childDir
+ dirNames = append(dirNames, name)
+ }
+
+ // Walk all these directories. Add some junk at the end which should not be
+ // walked on.
+ dirNames = append(dirNames, []string{"a", "b", "c"}...)
+ inodes := walk(ctx, t, root, dirNames)
+ if len(inodes) != n {
+ t.Errorf("walk returned the incorrect number of inodes: wanted %d, got %d", n, len(inodes))
+ }
+
+ // Close all control FDs and collect stat results for all dirs including
+ // the root directory.
+ dirStats := make([]linux.Statx, 0, n+1)
+ var stat linux.Statx
+ statTo(ctx, t, root, &stat)
+ dirStats = append(dirStats, stat)
+ for _, inode := range inodes {
+ dirStats = append(dirStats, inode.Stat)
+ closeFD(ctx, t, root.Client().NewFD(inode.ControlFD))
+ }
+
+ // Test WalkStat which additonally returns Statx for root because the first
+ // path component is "".
+ dirNames = append([]string{""}, dirNames...)
+ gotStats := walkStat(ctx, t, root, dirNames)
+ if len(gotStats) != len(dirStats) {
+ t.Errorf("walkStat returned the incorrect number of statx: wanted %d, got %d", len(dirStats), len(gotStats))
+ } else {
+ for i := range gotStats {
+ cmpStatx(t, dirStats[i], gotStats[i])
+ }
+ }
+}
+
+func testRename(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "tempFile"
+ tempFile, _, fd, hostFD := openCreateFile(ctx, t, root, name)
+ defer closeFD(ctx, t, tempFile)
+ defer closeFD(ctx, t, fd)
+ defer unix.Close(hostFD)
+
+ tempDir, _ := mkdir(ctx, t, root, "tempDir")
+ defer closeFD(ctx, t, tempDir)
+
+ // Move tempFile into tempDir.
+ if err := tempFile.RenameTo(ctx, tempDir.ID(), "movedFile"); err != nil {
+ t.Fatalf("rename failed: %v", err)
+ }
+
+ inodes := walkStat(ctx, t, root, []string{"tempDir", "movedFile"})
+ if len(inodes) != 2 {
+ t.Errorf("expected 2 files on walk but only found %d", len(inodes))
+ }
+}
+
+func testMknod(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ name := "namedPipe"
+ pipeFile, pipeStat := mknod(ctx, t, root, name)
+ defer closeFD(ctx, t, pipeFile)
+
+ var stat linux.Statx
+ statTo(ctx, t, pipeFile, &stat)
+
+ if stat.Mode != pipeStat.Mode {
+ t.Errorf("mknod mode is incorrect: want %d, got %d", pipeStat.Mode, stat.Mode)
+ }
+ if stat.UID != pipeStat.UID {
+ t.Errorf("mknod UID is incorrect: want %d, got %d", pipeStat.UID, stat.UID)
+ }
+ if stat.GID != pipeStat.GID {
+ t.Errorf("mknod GID is incorrect: want %d, got %d", pipeStat.GID, stat.GID)
+ }
+}
+
+func testGetdents(ctx context.Context, t *testing.T, tester Tester, root lisafs.ClientFD) {
+ tempDir, _ := mkdir(ctx, t, root, "tempDir")
+ defer closeFD(ctx, t, tempDir)
+ defer unlinkFile(ctx, t, root, "tempDir", true /* isDir */)
+
+ // Create 10 files in tempDir.
+ n := 10
+ fileStats := make(map[string]linux.Statx)
+ for i := 0; i < n; i++ {
+ name := fmt.Sprintf("file-%d", i)
+ newFile, fileStat := mknod(ctx, t, tempDir, name)
+ defer closeFD(ctx, t, newFile)
+ defer unlinkFile(ctx, t, tempDir, name, false /* isDir */)
+
+ fileStats[name] = fileStat
+ }
+
+ // Use opened directory FD for getdents.
+ openDirFile, _ := openFile(ctx, t, tempDir, unix.O_RDONLY, false /* isReg */)
+ defer closeFD(ctx, t, openDirFile)
+
+ dirents := make([]lisafs.Dirent64, 0, n)
+ for i := 0; i < n+2; i++ {
+ gotDirents, err := openDirFile.Getdents64(ctx, 40)
+ if err != nil {
+ t.Fatalf("getdents failed: %v", err)
+ }
+ if len(gotDirents) == 0 {
+ break
+ }
+ for _, dirent := range gotDirents {
+ if dirent.Name != "." && dirent.Name != ".." {
+ dirents = append(dirents, dirent)
+ }
+ }
+ }
+
+ if len(dirents) != n {
+ t.Errorf("got incorrect number of dirents: wanted %d, got %d", n, len(dirents))
+ }
+ for _, dirent := range dirents {
+ stat, ok := fileStats[string(dirent.Name)]
+ if !ok {
+ t.Errorf("received a dirent that was not created: %+v", dirent)
+ continue
+ }
+
+ if dirent.Type != unix.DT_REG {
+ t.Errorf("dirent type of %s is incorrect: %d", dirent.Name, dirent.Type)
+ }
+ if uint64(dirent.Ino) != stat.Ino {
+ t.Errorf("dirent ino of %s is incorrect: want %d, got %d", dirent.Name, stat.Ino, dirent.Ino)
+ }
+ if uint32(dirent.DevMinor) != stat.DevMinor {
+ t.Errorf("dirent dev minor of %s is incorrect: want %d, got %d", dirent.Name, stat.DevMinor, dirent.DevMinor)
+ }
+ if uint32(dirent.DevMajor) != stat.DevMajor {
+ t.Errorf("dirent dev major of %s is incorrect: want %d, got %d", dirent.Name, stat.DevMajor, dirent.DevMajor)
+ }
+ }
+}