// 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 with 2MB of data to test IO in chunks.
	data := make([]byte, 1<<21)
	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)
		}
	}
}