summaryrefslogtreecommitdiffhomepage
path: root/pkg/p9/p9test
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/p9/p9test')
-rw-r--r--pkg/p9/p9test/BUILD88
-rw-r--r--pkg/p9/p9test/client_test.go2242
-rw-r--r--pkg/p9/p9test/p9test.go329
3 files changed, 0 insertions, 2659 deletions
diff --git a/pkg/p9/p9test/BUILD b/pkg/p9/p9test/BUILD
deleted file mode 100644
index 7ca67cb19..000000000
--- a/pkg/p9/p9test/BUILD
+++ /dev/null
@@ -1,88 +0,0 @@
-load("//tools:defs.bzl", "go_binary", "go_library", "go_test")
-
-package(licenses = ["notice"])
-
-alias(
- name = "mockgen",
- actual = "@com_github_golang_mock//mockgen:mockgen",
-)
-
-MOCK_SRC_PACKAGE = "gvisor.dev/gvisor/pkg/p9"
-
-# mockgen_reflect is a source file that contains mock generation code that
-# imports the p9 package and generates a specification via reflection. The
-# usual generation path must be split into two distinct parts because the full
-# source tree is not available to all build targets. Only declared depencies
-# are available (and even then, not the Go source files).
-genrule(
- name = "mockgen_reflect",
- testonly = 1,
- outs = ["mockgen_reflect.go"],
- cmd = (
- "$(location :mockgen) " +
- "-package p9test " +
- "-prog_only " + MOCK_SRC_PACKAGE + " " +
- "Attacher,File > $@"
- ),
- tools = [":mockgen"],
-)
-
-# mockgen_exec is the binary that includes the above reflection generator.
-# Running this binary will emit an encoded version of the p9 Attacher and File
-# structures. This is consumed by the mocks genrule, below.
-go_binary(
- name = "mockgen_exec",
- testonly = 1,
- srcs = ["mockgen_reflect.go"],
- deps = [
- "//pkg/p9",
- "@com_github_golang_mock//mockgen/model:go_default_library",
- ],
-)
-
-# mocks consumes the encoded output above, and generates the full source for a
-# set of mocks. These are included directly in the p9test library.
-genrule(
- name = "mocks",
- testonly = 1,
- outs = ["mocks.go"],
- cmd = (
- "$(location :mockgen) " +
- "-package p9test " +
- "-exec_only $(location :mockgen_exec) " + MOCK_SRC_PACKAGE + " File > $@"
- ),
- tools = [
- ":mockgen",
- ":mockgen_exec",
- ],
-)
-
-go_library(
- name = "p9test",
- srcs = [
- "mocks.go",
- "p9test.go",
- ],
- visibility = ["//:sandbox"],
- deps = [
- "//pkg/fd",
- "//pkg/log",
- "//pkg/p9",
- "//pkg/sync",
- "//pkg/unet",
- "@com_github_golang_mock//gomock:go_default_library",
- ],
-)
-
-go_test(
- name = "client_test",
- size = "medium",
- srcs = ["client_test.go"],
- library = ":p9test",
- deps = [
- "//pkg/fd",
- "//pkg/p9",
- "//pkg/sync",
- "@com_github_golang_mock//gomock:go_default_library",
- ],
-)
diff --git a/pkg/p9/p9test/client_test.go b/pkg/p9/p9test/client_test.go
deleted file mode 100644
index 6e7bb3db2..000000000
--- a/pkg/p9/p9test/client_test.go
+++ /dev/null
@@ -1,2242 +0,0 @@
-// Copyright 2018 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 p9test
-
-import (
- "bytes"
- "fmt"
- "io"
- "math/rand"
- "os"
- "reflect"
- "strings"
- "syscall"
- "testing"
- "time"
-
- "github.com/golang/mock/gomock"
- "gvisor.dev/gvisor/pkg/fd"
- "gvisor.dev/gvisor/pkg/p9"
- "gvisor.dev/gvisor/pkg/sync"
-)
-
-func TestPanic(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- // Create a new root.
- d := h.NewDirectory(nil)(nil)
- defer d.Close() // Needed manually.
- h.Attacher.EXPECT().Attach().Return(d, nil).Do(func() {
- // Panic here, and ensure that we get back EFAULT.
- panic("handler")
- })
-
- // Attach to the client.
- if _, err := c.Attach("/"); err != syscall.EFAULT {
- t.Fatalf("got attach err %v, want EFAULT", err)
- }
-}
-
-func TestAttachNoLeak(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- // Create a new root.
- d := h.NewDirectory(nil)(nil)
- h.Attacher.EXPECT().Attach().Return(d, nil).Times(1)
-
- // Attach to the client.
- f, err := c.Attach("/")
- if err != nil {
- t.Fatalf("got attach err %v, want nil", err)
- }
-
- // Don't close the file. This should be closed automatically when the
- // client disconnects. The mock asserts that everything is closed
- // exactly once. This statement just removes the unused variable error.
- _ = f
-}
-
-func TestBadAttach(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- // Return an error on attach.
- h.Attacher.EXPECT().Attach().Return(nil, syscall.EINVAL).Times(1)
-
- // Attach to the client.
- if _, err := c.Attach("/"); err != syscall.EINVAL {
- t.Fatalf("got attach err %v, want syscall.EINVAL", err)
- }
-}
-
-func TestWalkAttach(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- // Create a new root.
- d := h.NewDirectory(map[string]Generator{
- "a": h.NewDirectory(map[string]Generator{
- "b": h.NewFile(),
- }),
- })(nil)
- h.Attacher.EXPECT().Attach().Return(d, nil).Times(1)
-
- // Attach to the client as a non-root, and ensure that the walk above
- // occurs as expected. We should get back b, and all references should
- // be dropped when the file is closed.
- f, err := c.Attach("/a/b")
- if err != nil {
- t.Fatalf("got attach err %v, want nil", err)
- }
- defer f.Close()
-
- // Check that's a regular file.
- if _, _, attr, err := f.GetAttr(p9.AttrMaskAll()); err != nil {
- t.Errorf("got err %v, want nil", err)
- } else if !attr.Mode.IsRegular() {
- t.Errorf("got mode %v, want regular file", err)
- }
-}
-
-// newTypeMap returns a new type map dictionary.
-func newTypeMap(h *Harness) map[string]Generator {
- return map[string]Generator{
- "directory": h.NewDirectory(map[string]Generator{}),
- "file": h.NewFile(),
- "symlink": h.NewSymlink(),
- "block-device": h.NewBlockDevice(),
- "character-device": h.NewCharacterDevice(),
- "named-pipe": h.NewNamedPipe(),
- "socket": h.NewSocket(),
- }
-}
-
-// newRoot returns a new root filesystem.
-//
-// This is set up in a deterministic way for testing most operations.
-//
-// The represented file system looks like:
-// - file
-// - symlink
-// - directory
-// ...
-// + one
-// - file
-// - symlink
-// - directory
-// ...
-// + two
-// - file
-// - symlink
-// - directory
-// ...
-// + three
-// - file
-// - symlink
-// - directory
-// ...
-func newRoot(h *Harness, c *p9.Client) (*Mock, p9.File) {
- root := newTypeMap(h)
- one := newTypeMap(h)
- two := newTypeMap(h)
- three := newTypeMap(h)
- one["two"] = h.NewDirectory(two) // Will be nested in one.
- root["one"] = h.NewDirectory(one) // Top level.
- root["three"] = h.NewDirectory(three) // Alternate top-level.
-
- // Create a new root.
- rootBackend := h.NewDirectory(root)(nil)
- h.Attacher.EXPECT().Attach().Return(rootBackend, nil)
-
- // Attach to the client.
- r, err := c.Attach("/")
- if err != nil {
- h.t.Fatalf("got attach err %v, want nil", err)
- }
-
- return rootBackend, r
-}
-
-func allInvalidNames(from string) []string {
- return []string{
- from + "/other",
- from + "/..",
- from + "/.",
- from + "/",
- "other/" + from,
- "/" + from,
- "./" + from,
- "../" + from,
- ".",
- "..",
- "/",
- "",
- }
-}
-
-func TestWalkInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Run relevant tests.
- for name := range newTypeMap(h) {
- // These are all the various ways that one might attempt to
- // construct compound paths. They should all be rejected, as
- // any compound that contains a / is not allowed, as well as
- // the singular paths of '.' and '..'.
- if _, _, err := root.Walk([]string{".", name}); err != syscall.EINVAL {
- t.Errorf("Walk through . %s wanted EINVAL, got %v", name, err)
- }
- if _, _, err := root.Walk([]string{"..", name}); err != syscall.EINVAL {
- t.Errorf("Walk through . %s wanted EINVAL, got %v", name, err)
- }
- if _, _, err := root.Walk([]string{name, "."}); err != syscall.EINVAL {
- t.Errorf("Walk through %s . wanted EINVAL, got %v", name, err)
- }
- if _, _, err := root.Walk([]string{name, ".."}); err != syscall.EINVAL {
- t.Errorf("Walk through %s .. wanted EINVAL, got %v", name, err)
- }
- for _, invalidName := range allInvalidNames(name) {
- if _, _, err := root.Walk([]string{invalidName}); err != syscall.EINVAL {
- t.Errorf("Walk through %s wanted EINVAL, got %v", invalidName, err)
- }
- }
- wantErr := syscall.EINVAL
- if name == "directory" {
- // We can attempt a walk through a directory. However,
- // we should never see a file named "other", so we
- // expect this to return ENOENT.
- wantErr = syscall.ENOENT
- }
- if _, _, err := root.Walk([]string{name, "other"}); err != wantErr {
- t.Errorf("Walk through %s/other wanted %v, got %v", name, wantErr, err)
- }
-
- // Do a successful walk.
- _, f, err := root.Walk([]string{name})
- if err != nil {
- t.Errorf("Walk to %s wanted nil, got %v", name, err)
- }
- defer f.Close()
- local := h.Pop(f)
-
- // Check that the file matches.
- _, localMask, localAttr, localErr := local.GetAttr(p9.AttrMaskAll())
- if _, mask, attr, err := f.GetAttr(p9.AttrMaskAll()); mask != localMask || attr != localAttr || err != localErr {
- t.Errorf("GetAttr got (%v, %v, %v), wanted (%v, %v, %v)",
- mask, attr, err, localMask, localAttr, localErr)
- }
-
- // Ensure we can't walk backwards.
- if _, _, err := f.Walk([]string{"."}); err != syscall.EINVAL {
- t.Errorf("Walk through %s/. wanted EINVAL, got %v", name, err)
- }
- if _, _, err := f.Walk([]string{".."}); err != syscall.EINVAL {
- t.Errorf("Walk through %s/.. wanted EINVAL, got %v", name, err)
- }
- }
-}
-
-// fileGenerator is a function to generate files via walk or create.
-//
-// Examples are:
-// - walkHelper
-// - walkAndOpenHelper
-// - createHelper
-type fileGenerator func(*Harness, string, p9.File) (*Mock, *Mock, p9.File)
-
-// walkHelper walks to the given file.
-//
-// The backends of the parent and walked file are returned, as well as the
-// walked client file.
-func walkHelper(h *Harness, name string, dir p9.File) (parentBackend *Mock, walkedBackend *Mock, walked p9.File) {
- _, parent, err := dir.Walk(nil)
- if err != nil {
- h.t.Fatalf("Walk(nil) got err %v, want nil", err)
- }
- defer parent.Close()
- parentBackend = h.Pop(parent)
-
- _, walked, err = parent.Walk([]string{name})
- if err != nil {
- h.t.Fatalf("Walk(%s) got err %v, want nil", name, err)
- }
- walkedBackend = h.Pop(walked)
-
- return parentBackend, walkedBackend, walked
-}
-
-// walkAndOpenHelper additionally opens the walked file, if possible.
-func walkAndOpenHelper(h *Harness, name string, dir p9.File) (*Mock, *Mock, p9.File) {
- parentBackend, walkedBackend, walked := walkHelper(h, name, dir)
- if p9.CanOpen(walkedBackend.Attr.Mode) {
- // Open for all file types that we can. We stick to a read-only
- // open here because directories may not be opened otherwise.
- walkedBackend.EXPECT().Open(p9.ReadOnly).Times(1)
- if _, _, _, err := walked.Open(p9.ReadOnly); err != nil {
- h.t.Errorf("got open err %v, want nil", err)
- }
- } else {
- // ... or assert an error for others.
- if _, _, _, err := walked.Open(p9.ReadOnly); err != syscall.EINVAL {
- h.t.Errorf("got open err %v, want EINVAL", err)
- }
- }
- return parentBackend, walkedBackend, walked
-}
-
-// createHelper creates the given file and returns the parent directory,
-// created file and client file, which must be closed when done.
-func createHelper(h *Harness, name string, dir p9.File) (*Mock, *Mock, p9.File) {
- // Clone the directory first, since Create replaces the existing file.
- // We change the type after calling create.
- _, dirThenFile, err := dir.Walk(nil)
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
-
- // Create a new server-side file. On the server-side, the a new file is
- // returned from a create call. The client will reuse the same file,
- // but we still expect the normal chain of closes. This complicates
- // things a bit because the "parent" will always chain to the cloned
- // dir above.
- dirBackend := h.Pop(dirThenFile) // New backend directory.
- newFile := h.NewFile()(dirBackend) // New file with backend parent.
- dirBackend.EXPECT().Create(name, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, newFile, newFile.QID, uint32(0), nil)
-
- // Create via the client.
- _, dirThenFile, _, _, err = dirThenFile.Create(name, p9.ReadOnly, 0, 0, 0)
- if err != nil {
- h.t.Fatalf("got create err %v, want nil", err)
- }
-
- // Ensure subsequent walks succeed.
- dirBackend.AddChild(name, h.NewFile())
- return dirBackend, newFile, dirThenFile
-}
-
-// deprecatedRemover allows us to access the deprecated Remove operation within
-// the p9.File client object.
-type deprecatedRemover interface {
- Remove() error
-}
-
-// checkDeleted asserts that relevant methods fail for an unlinked file.
-//
-// This function will close the file at the end.
-func checkDeleted(h *Harness, file p9.File) {
- defer file.Close() // See doc.
-
- if _, _, _, err := file.Open(p9.ReadOnly); err != syscall.EINVAL {
- h.t.Errorf("open while deleted, got %v, want EINVAL", err)
- }
- if _, _, _, _, err := file.Create("created", p9.ReadOnly, 0, 0, 0); err != syscall.EINVAL {
- h.t.Errorf("create while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Symlink("old", "new", 0, 0); err != syscall.EINVAL {
- h.t.Errorf("symlink while deleted, got %v, want EINVAL", err)
- }
- // N.B. This link is technically invalid, but if a call to link is
- // actually made in the backend then the mock will panic.
- if err := file.Link(file, "new"); err != syscall.EINVAL {
- h.t.Errorf("link while deleted, got %v, want EINVAL", err)
- }
- if err := file.RenameAt("src", file, "dst"); err != syscall.EINVAL {
- h.t.Errorf("renameAt while deleted, got %v, want EINVAL", err)
- }
- if err := file.UnlinkAt("file", 0); err != syscall.EINVAL {
- h.t.Errorf("unlinkAt while deleted, got %v, want EINVAL", err)
- }
- if err := file.Rename(file, "dst"); err != syscall.EINVAL {
- h.t.Errorf("rename while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Readlink(); err != syscall.EINVAL {
- h.t.Errorf("readlink while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Mkdir("dir", p9.ModeDirectory, 0, 0); err != syscall.EINVAL {
- h.t.Errorf("mkdir while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Mknod("dir", p9.ModeDirectory, 0, 0, 0, 0); err != syscall.EINVAL {
- h.t.Errorf("mknod while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Readdir(0, 1); err != syscall.EINVAL {
- h.t.Errorf("readdir while deleted, got %v, want EINVAL", err)
- }
- if _, err := file.Connect(p9.ConnectFlags(0)); err != syscall.EINVAL {
- h.t.Errorf("connect while deleted, got %v, want EINVAL", err)
- }
-
- // The remove method is technically deprecated, but we want to ensure
- // that it still checks for deleted appropriately. We must first clone
- // the file because remove is equivalent to close.
- _, newFile, err := file.Walk(nil)
- if err == syscall.EBUSY {
- // We can't walk from here because this reference is open
- // already. Okay, we will also have unopened cases through
- // TestUnlink, just skip the remove operation for now.
- return
- } else if err != nil {
- h.t.Fatalf("clone failed, got %v, want nil", err)
- }
- if err := newFile.(deprecatedRemover).Remove(); err != syscall.EINVAL {
- h.t.Errorf("remove while deleted, got %v, want EINVAL", err)
- }
-}
-
-// deleter is a function to remove a file.
-type deleter func(parent p9.File, name string) error
-
-// unlinkAt is a deleter.
-func unlinkAt(parent p9.File, name string) error {
- // Call unlink. Note that a filesystem may normally impose additional
- // constaints on unlinkat success, such as ensuring that a directory is
- // empty, requiring AT_REMOVEDIR in flags to remove a directory, etc.
- // None of that is required internally (entire trees can be marked
- // deleted when this operation succeeds), so the mock will succeed.
- return parent.UnlinkAt(name, 0)
-}
-
-// remove is a deleter.
-func remove(parent p9.File, name string) error {
- // See notes above re: remove.
- _, newFile, err := parent.Walk([]string{name})
- if err != nil {
- // Should not be expected.
- return err
- }
-
- // Do the actual remove.
- if err := newFile.(deprecatedRemover).Remove(); err != nil {
- return err
- }
-
- // Ensure that the remove closed the file.
- if err := newFile.(deprecatedRemover).Remove(); err != syscall.EBADF {
- return syscall.EBADF // Propagate this code.
- }
-
- return nil
-}
-
-// unlinkHelper unlinks the noted path, and ensures that all relevant
-// operations on that path, acquired from multiple paths, start failing.
-func unlinkHelper(h *Harness, root p9.File, targetNames []string, targetGen fileGenerator, deleteFn deleter) {
- // name is the file to be unlinked.
- name := targetNames[len(targetNames)-1]
-
- // Walk to the directory containing the target.
- _, parent, err := root.Walk(targetNames[:len(targetNames)-1])
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer parent.Close()
- parentBackend := h.Pop(parent)
-
- // Walk to or generate the target file.
- _, _, target := targetGen(h, name, parent)
- defer checkDeleted(h, target)
-
- // Walk to a second reference.
- _, second, err := parent.Walk([]string{name})
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer checkDeleted(h, second)
-
- // Walk to a third reference, from the start.
- _, third, err := root.Walk(targetNames)
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer checkDeleted(h, third)
-
- // This will be translated in the backend to an unlinkat.
- parentBackend.EXPECT().UnlinkAt(name, uint32(0)).Return(nil)
-
- // Actually perform the deletion.
- if err := deleteFn(parent, name); err != nil {
- h.t.Fatalf("got delete err %v, want nil", err)
- }
-}
-
-func unlinkTest(t *testing.T, targetNames []string, targetGen fileGenerator) {
- t.Run(fmt.Sprintf("unlinkAt(%s)", strings.Join(targetNames, "/")), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- unlinkHelper(h, root, targetNames, targetGen, unlinkAt)
- })
- t.Run(fmt.Sprintf("remove(%s)", strings.Join(targetNames, "/")), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- unlinkHelper(h, root, targetNames, targetGen, remove)
- })
-}
-
-func TestUnlink(t *testing.T) {
- // Unlink all files.
- for name := range newTypeMap(nil) {
- unlinkTest(t, []string{name}, walkHelper)
- unlinkTest(t, []string{name}, walkAndOpenHelper)
- unlinkTest(t, []string{"one", name}, walkHelper)
- unlinkTest(t, []string{"one", name}, walkAndOpenHelper)
- unlinkTest(t, []string{"one", "two", name}, walkHelper)
- unlinkTest(t, []string{"one", "two", name}, walkAndOpenHelper)
- }
-
- // Unlink a directory.
- unlinkTest(t, []string{"one"}, walkHelper)
- unlinkTest(t, []string{"one"}, walkAndOpenHelper)
- unlinkTest(t, []string{"one", "two"}, walkHelper)
- unlinkTest(t, []string{"one", "two"}, walkAndOpenHelper)
-
- // Unlink created files.
- unlinkTest(t, []string{"created"}, createHelper)
- unlinkTest(t, []string{"one", "created"}, createHelper)
- unlinkTest(t, []string{"one", "two", "created"}, createHelper)
-}
-
-func TestUnlinkAtInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if err := root.UnlinkAt(invalidName, 0); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-// expectRenamed asserts an ordered sequence of rename calls, based on all the
-// elements in elements being the source, and the first element therein
-// changing to dstName, parented at dstParent.
-func expectRenamed(file *Mock, elements []string, dstParent *Mock, dstName string) *gomock.Call {
- if len(elements) > 0 {
- // Recurse to the parent, if necessary.
- call := expectRenamed(file.parent, elements[:len(elements)-1], dstParent, dstName)
-
- // Recursive case: this element is unchanged, but should have
- // it's hook called after the parent.
- return file.EXPECT().Renamed(file.parent, elements[len(elements)-1]).Do(func(p p9.File, _ string) {
- file.parent = p.(*Mock)
- }).After(call)
- }
-
- // Base case: this is the changed element.
- return file.EXPECT().Renamed(dstParent, dstName).Do(func(p p9.File, name string) {
- file.parent = p.(*Mock)
- })
-}
-
-// renamer is a rename function.
-type renamer func(h *Harness, srcParent, dstParent p9.File, origName, newName string, selfRename bool) error
-
-// renameAt is a renamer.
-func renameAt(_ *Harness, srcParent, dstParent p9.File, srcName, dstName string, selfRename bool) error {
- return srcParent.RenameAt(srcName, dstParent, dstName)
-}
-
-// rename is a renamer.
-func rename(h *Harness, srcParent, dstParent p9.File, srcName, dstName string, selfRename bool) error {
- _, f, err := srcParent.Walk([]string{srcName})
- if err != nil {
- return err
- }
- defer f.Close()
- if !selfRename {
- backend := h.Pop(f)
- backend.EXPECT().Renamed(gomock.Any(), dstName).Do(func(p p9.File, name string) {
- backend.parent = p.(*Mock) // Required for close ordering.
- })
- }
- return f.Rename(dstParent, dstName)
-}
-
-// renameHelper executes a rename, and asserts that all relevant elements
-// receive expected notifications. If overwriting a file, this includes
-// ensuring that the target has been appropriately marked as unlinked.
-func renameHelper(h *Harness, root p9.File, srcNames []string, dstNames []string, target fileGenerator, renameFn renamer) {
- // Walk to the directory containing the target.
- srcQID, targetParent, err := root.Walk(srcNames[:len(srcNames)-1])
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer targetParent.Close()
- targetParentBackend := h.Pop(targetParent)
-
- // Walk to or generate the target file.
- _, targetBackend, src := target(h, srcNames[len(srcNames)-1], targetParent)
- defer src.Close()
-
- // Walk to a second reference.
- _, second, err := targetParent.Walk([]string{srcNames[len(srcNames)-1]})
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer second.Close()
- secondBackend := h.Pop(second)
-
- // Walk to a third reference, from the start.
- _, third, err := root.Walk(srcNames)
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer third.Close()
- thirdBackend := h.Pop(third)
-
- // Find the common suffix to identify the rename parent.
- var (
- renameDestPath []string
- renameSrcPath []string
- selfRename bool
- )
- for i := 1; i <= len(srcNames) && i <= len(dstNames); i++ {
- if srcNames[len(srcNames)-i] != dstNames[len(dstNames)-i] {
- // Take the full prefix of dstNames up until this
- // point, including the first mismatched name. The
- // first mismatch must be the renamed entry.
- renameDestPath = dstNames[:len(dstNames)-i+1]
- renameSrcPath = srcNames[:len(srcNames)-i+1]
-
- // Does the renameDestPath fully contain the
- // renameSrcPath here? If yes, then this is a mismatch.
- // We can't rename the src to some subpath of itself.
- if len(renameDestPath) > len(renameSrcPath) &&
- reflect.DeepEqual(renameDestPath[:len(renameSrcPath)], renameSrcPath) {
- renameDestPath = nil
- renameSrcPath = nil
- continue
- }
- break
- }
- }
- if len(renameSrcPath) == 0 || len(renameDestPath) == 0 {
- // This must be a rename to self, or a tricky look-alike. This
- // happens iff we fail to find a suitable divergence in the two
- // paths. It's a true self move if the path length is the same.
- renameDestPath = dstNames
- renameSrcPath = srcNames
- selfRename = len(srcNames) == len(dstNames)
- }
-
- // Walk to the source parent.
- _, srcParent, err := root.Walk(renameSrcPath[:len(renameSrcPath)-1])
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer srcParent.Close()
- srcParentBackend := h.Pop(srcParent)
-
- // Walk to the destination parent.
- _, dstParent, err := root.Walk(renameDestPath[:len(renameDestPath)-1])
- if err != nil {
- h.t.Fatalf("got walk err %v, want nil", err)
- }
- defer dstParent.Close()
- dstParentBackend := h.Pop(dstParent)
-
- // expectedErr is the result of the rename operation.
- var expectedErr error
-
- // Walk to the target file, if one exists.
- dstQID, dst, err := root.Walk(renameDestPath)
- if err == nil {
- if !selfRename && srcQID[0].Type == dstQID[0].Type {
- // If there is a destination file, and is it of the
- // same type as the source file, then we expect the
- // rename to succeed. We expect the destination file to
- // be deleted, so we run a deletion test on it in this
- // case.
- defer checkDeleted(h, dst)
- } else {
- if !selfRename {
- // If the type is different than the
- // destination, then we expect the rename to
- // fail. We expect ensure that this is
- // returned.
- expectedErr = syscall.EINVAL
- } else {
- // This is the file being renamed to itself.
- // This is technically allowed and a no-op, but
- // all the triggers will fire.
- }
- dst.Close()
- }
- }
- dstName := renameDestPath[len(renameDestPath)-1] // Renamed element.
- srcName := renameSrcPath[len(renameSrcPath)-1] // Renamed element.
- if expectedErr == nil && !selfRename {
- // Expect all to be renamed appropriately. Note that if this is
- // a final file being renamed, then we expect the file to be
- // called with the new parent. If not, then we expect the
- // rename hook to be called, but the parent will remain
- // unchanged.
- elements := srcNames[len(renameSrcPath):]
- expectRenamed(targetBackend, elements, dstParentBackend, dstName)
- expectRenamed(secondBackend, elements, dstParentBackend, dstName)
- expectRenamed(thirdBackend, elements, dstParentBackend, dstName)
-
- // The target parent has also been opened, and may be moved
- // directly or indirectly.
- if len(elements) > 1 {
- expectRenamed(targetParentBackend, elements[:len(elements)-1], dstParentBackend, dstName)
- }
- }
-
- // Expect the rename if it's not the same file. Note that like unlink,
- // renames are always translated to the at variant in the backend.
- if !selfRename {
- srcParentBackend.EXPECT().RenameAt(srcName, dstParentBackend, dstName).Return(expectedErr)
- }
-
- // Perform the actual rename; everything has been lined up.
- if err := renameFn(h, srcParent, dstParent, srcName, dstName, selfRename); err != expectedErr {
- h.t.Fatalf("got rename err %v, want %v", err, expectedErr)
- }
-}
-
-func renameTest(t *testing.T, srcNames []string, dstNames []string, target fileGenerator) {
- t.Run(fmt.Sprintf("renameAt(%s->%s)", strings.Join(srcNames, "/"), strings.Join(dstNames, "/")), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- renameHelper(h, root, srcNames, dstNames, target, renameAt)
- })
- t.Run(fmt.Sprintf("rename(%s->%s)", strings.Join(srcNames, "/"), strings.Join(dstNames, "/")), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- renameHelper(h, root, srcNames, dstNames, target, rename)
- })
-}
-
-func TestRename(t *testing.T) {
- // In-directory rename, simple case.
- for name := range newTypeMap(nil) {
- // Within the root.
- renameTest(t, []string{name}, []string{"renamed"}, walkHelper)
- renameTest(t, []string{name}, []string{"renamed"}, walkAndOpenHelper)
-
- // Within a subdirectory.
- renameTest(t, []string{"one", name}, []string{"one", "renamed"}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"one", "renamed"}, walkAndOpenHelper)
- }
-
- // ... with created files.
- renameTest(t, []string{"created"}, []string{"renamed"}, createHelper)
- renameTest(t, []string{"one", "created"}, []string{"one", "renamed"}, createHelper)
-
- // Across directories.
- for name := range newTypeMap(nil) {
- // Down one level.
- renameTest(t, []string{"one", name}, []string{"one", "two", "renamed"}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"one", "two", "renamed"}, walkAndOpenHelper)
-
- // Up one level.
- renameTest(t, []string{"one", "two", name}, []string{"one", "renamed"}, walkHelper)
- renameTest(t, []string{"one", "two", name}, []string{"one", "renamed"}, walkAndOpenHelper)
-
- // Across at the same level.
- renameTest(t, []string{"one", name}, []string{"three", "renamed"}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"three", "renamed"}, walkAndOpenHelper)
- }
-
- // ... with created files.
- renameTest(t, []string{"one", "created"}, []string{"one", "two", "renamed"}, createHelper)
- renameTest(t, []string{"one", "two", "created"}, []string{"one", "renamed"}, createHelper)
- renameTest(t, []string{"one", "created"}, []string{"three", "renamed"}, createHelper)
-
- // Renaming parents.
- for name := range newTypeMap(nil) {
- // Rename a parent.
- renameTest(t, []string{"one", name}, []string{"renamed", name}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"renamed", name}, walkAndOpenHelper)
-
- // Rename a super parent.
- renameTest(t, []string{"one", "two", name}, []string{"renamed", name}, walkHelper)
- renameTest(t, []string{"one", "two", name}, []string{"renamed", name}, walkAndOpenHelper)
- }
-
- // ... with created files.
- renameTest(t, []string{"one", "created"}, []string{"renamed", "created"}, createHelper)
- renameTest(t, []string{"one", "two", "created"}, []string{"renamed", "created"}, createHelper)
-
- // Over existing files, including itself.
- for name := range newTypeMap(nil) {
- for other := range newTypeMap(nil) {
- // Overwrite the noted file (may be itself).
- renameTest(t, []string{"one", name}, []string{"one", other}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"one", other}, walkAndOpenHelper)
-
- // Overwrite other files in another directory.
- renameTest(t, []string{"one", name}, []string{"one", "two", other}, walkHelper)
- renameTest(t, []string{"one", name}, []string{"one", "two", other}, walkAndOpenHelper)
- }
-
- // Overwrite by moving the parent.
- renameTest(t, []string{"three", name}, []string{"one", name}, walkHelper)
- renameTest(t, []string{"three", name}, []string{"one", name}, walkAndOpenHelper)
-
- // Create over the types.
- renameTest(t, []string{"one", "created"}, []string{"one", name}, createHelper)
- renameTest(t, []string{"one", "created"}, []string{"one", "two", name}, createHelper)
- renameTest(t, []string{"three", "created"}, []string{"one", name}, createHelper)
- }
-}
-
-func TestRenameInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if err := root.Rename(root, invalidName); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-func TestRenameAtInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if err := root.RenameAt(invalidName, root, "okay"); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- if err := root.RenameAt("okay", root, invalidName); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-// TestRenameSecondOrder tests that indirect rename targets continue to receive
-// Renamed calls after a rename of its renamed parent. i.e.,
-//
-// 1. Create /one/file
-// 2. Create /directory
-// 3. Rename /one -> /directory/one
-// 4. Rename /directory -> /three/foo
-// 5. file from (1) should still receive Renamed.
-//
-// This is a regression test for b/135219260.
-func TestRenameSecondOrder(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- rootBackend, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to /one.
- _, oneBackend, oneFile := walkHelper(h, "one", root)
- defer oneFile.Close()
-
- // Walk to and generate /one/file.
- //
- // walkHelper re-walks to oneFile, so we need the second backend,
- // which will also receive Renamed calls.
- oneSecondBackend, fileBackend, fileFile := walkHelper(h, "file", oneFile)
- defer fileFile.Close()
-
- // Walk to and generate /directory.
- _, directoryBackend, directoryFile := walkHelper(h, "directory", root)
- defer directoryFile.Close()
-
- // Rename /one to /directory/one.
- rootBackend.EXPECT().RenameAt("one", directoryBackend, "one").Return(nil)
- expectRenamed(oneBackend, []string{}, directoryBackend, "one")
- expectRenamed(oneSecondBackend, []string{}, directoryBackend, "one")
- expectRenamed(fileBackend, []string{}, oneBackend, "file")
- if err := renameAt(h, root, directoryFile, "one", "one", false); err != nil {
- h.t.Fatalf("got rename err %v, want nil", err)
- }
-
- // Walk to /three.
- _, threeBackend, threeFile := walkHelper(h, "three", root)
- defer threeFile.Close()
-
- // Rename /directory to /three/foo.
- rootBackend.EXPECT().RenameAt("directory", threeBackend, "foo").Return(nil)
- expectRenamed(directoryBackend, []string{}, threeBackend, "foo")
- expectRenamed(oneBackend, []string{}, directoryBackend, "one")
- expectRenamed(oneSecondBackend, []string{}, directoryBackend, "one")
- expectRenamed(fileBackend, []string{}, oneBackend, "file")
- if err := renameAt(h, root, threeFile, "directory", "foo", false); err != nil {
- h.t.Fatalf("got rename err %v, want nil", err)
- }
-}
-
-func TestReadlink(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to the file normally.
- _, f, err := root.Walk([]string{name})
- if err != nil {
- t.Fatalf("walk failed: got %v, wanted nil", err)
- }
- defer f.Close()
- backend := h.Pop(f)
-
- const symlinkTarget = "symlink-target"
-
- if backend.Attr.Mode.IsSymlink() {
- // This should only go through on symlinks.
- backend.EXPECT().Readlink().Return(symlinkTarget, nil)
- }
-
- // Attempt a Readlink operation.
- target, err := f.Readlink()
- if err != nil && err != syscall.EINVAL {
- t.Errorf("readlink got %v, wanted EINVAL", err)
- } else if err == nil && target != symlinkTarget {
- t.Errorf("readlink got %v, wanted %v", target, symlinkTarget)
- }
- })
- }
-}
-
-// fdTest is a wrapper around operations that may send file descriptors. This
-// asserts that the file descriptors are working as intended.
-func fdTest(t *testing.T, sendFn func(*fd.FD) *fd.FD) {
- // Create a pipe that we can read from.
- r, w, err := os.Pipe()
- if err != nil {
- t.Fatalf("unable to create pipe: %v", err)
- }
- defer r.Close()
- defer w.Close()
-
- // Attempt to send the write end.
- wFD, err := fd.NewFromFile(w)
- if err != nil {
- t.Fatalf("unable to convert file: %v", err)
- }
- defer wFD.Close() // This is a copy.
-
- // Send wFD and receive newFD.
- newFD := sendFn(wFD)
- defer newFD.Close()
-
- // Attempt to write.
- const message = "hello"
- if _, err := newFD.Write([]byte(message)); err != nil {
- t.Fatalf("write got %v, wanted nil", err)
- }
-
- // Should see the message on our end.
- buffer := []byte(message)
- if _, err := io.ReadFull(r, buffer); err != nil {
- t.Fatalf("read got %v, wanted nil", err)
- }
- if string(buffer) != message {
- t.Errorf("got message %v, wanted %v", string(buffer), message)
- }
-}
-
-func TestConnect(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- // Catch all the non-socket cases.
- if !backend.Attr.Mode.IsSocket() {
- // This has been set up to fail if Connect is called.
- if _, err := f.Connect(p9.ConnectFlags(0)); err != syscall.EINVAL {
- t.Errorf("connect got %v, wanted EINVAL", err)
- }
- return
- }
-
- // Ensure the fd exchange works.
- fdTest(t, func(send *fd.FD) *fd.FD {
- backend.EXPECT().Connect(p9.ConnectFlags(0)).Return(send, nil)
- recv, err := backend.Connect(p9.ConnectFlags(0))
- if err != nil {
- t.Fatalf("connect got %v, wanted nil", err)
- }
- return recv
- })
- })
- }
-}
-
-func TestReaddir(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- // Catch all the non-directory cases.
- if !backend.Attr.Mode.IsDir() {
- // This has also been set up to fail if Readdir is called.
- if _, err := f.Readdir(0, 1); err != syscall.EINVAL {
- t.Errorf("readdir got %v, wanted EINVAL", err)
- }
- return
- }
-
- // Ensure that readdir works for directories.
- if _, err := f.Readdir(0, 1); err != syscall.EINVAL {
- t.Errorf("readdir got %v, wanted EINVAL", err)
- }
- if _, _, _, err := f.Open(p9.ReadWrite); err != syscall.EISDIR {
- t.Errorf("readdir got %v, wanted EISDIR", err)
- }
- if _, _, _, err := f.Open(p9.WriteOnly); err != syscall.EISDIR {
- t.Errorf("readdir got %v, wanted EISDIR", err)
- }
- backend.EXPECT().Open(p9.ReadOnly).Times(1)
- if _, _, _, err := f.Open(p9.ReadOnly); err != nil {
- t.Errorf("readdir got %v, wanted nil", err)
- }
- backend.EXPECT().Readdir(uint64(0), uint32(1)).Times(1)
- if _, err := f.Readdir(0, 1); err != nil {
- t.Errorf("readdir got %v, wanted nil", err)
- }
- })
- }
-}
-
-func TestOpen(t *testing.T) {
- type openTest struct {
- name string
- flags p9.OpenFlags
- err error
- match func(p9.FileMode) bool
- }
-
- cases := []openTest{
- {
- name: "not-openable-read-only",
- flags: p9.ReadOnly,
- err: syscall.EINVAL,
- match: func(mode p9.FileMode) bool { return !p9.CanOpen(mode) },
- },
- {
- name: "not-openable-write-only",
- flags: p9.WriteOnly,
- err: syscall.EINVAL,
- match: func(mode p9.FileMode) bool { return !p9.CanOpen(mode) },
- },
- {
- name: "not-openable-read-write",
- flags: p9.ReadWrite,
- err: syscall.EINVAL,
- match: func(mode p9.FileMode) bool { return !p9.CanOpen(mode) },
- },
- {
- name: "directory-read-only",
- flags: p9.ReadOnly,
- err: nil,
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- },
- {
- name: "directory-read-write",
- flags: p9.ReadWrite,
- err: syscall.EISDIR,
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- },
- {
- name: "directory-write-only",
- flags: p9.WriteOnly,
- err: syscall.EISDIR,
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- },
- {
- name: "read-only",
- flags: p9.ReadOnly,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) },
- },
- {
- name: "write-only",
- flags: p9.WriteOnly,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) && !mode.IsDir() },
- },
- {
- name: "read-write",
- flags: p9.ReadWrite,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) && !mode.IsDir() },
- },
- {
- name: "directory-read-only-truncate",
- flags: p9.ReadOnly | p9.OpenTruncate,
- err: syscall.EISDIR,
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- },
- {
- name: "read-only-truncate",
- flags: p9.ReadOnly | p9.OpenTruncate,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) && !mode.IsDir() },
- },
- {
- name: "write-only-truncate",
- flags: p9.WriteOnly | p9.OpenTruncate,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) && !mode.IsDir() },
- },
- {
- name: "read-write-truncate",
- flags: p9.ReadWrite | p9.OpenTruncate,
- err: nil,
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) && !mode.IsDir() },
- },
- }
-
- // Open(flags OpenFlags) (*fd.FD, QID, uint32, error)
- // - only works on Regular, NamedPipe, BLockDevice, CharacterDevice
- // - returning a file works as expected
- for name := range newTypeMap(nil) {
- for _, tc := range cases {
- t.Run(fmt.Sprintf("%s-%s", tc.name, name), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- // Does this match the case?
- if !tc.match(backend.Attr.Mode) {
- t.SkipNow()
- }
-
- // Ensure open-required operations fail.
- if _, err := f.ReadAt([]byte("hello"), 0); err != syscall.EINVAL {
- t.Errorf("readAt got %v, wanted EINVAL", err)
- }
- if _, err := f.WriteAt(make([]byte, 6), 0); err != syscall.EINVAL {
- t.Errorf("writeAt got %v, wanted EINVAL", err)
- }
- if err := f.FSync(); err != syscall.EINVAL {
- t.Errorf("fsync got %v, wanted EINVAL", err)
- }
- if _, err := f.Readdir(0, 1); err != syscall.EINVAL {
- t.Errorf("readdir got %v, wanted EINVAL", err)
- }
-
- // Attempt the given open.
- if tc.err != nil {
- // We expect an error, just test and return.
- if _, _, _, err := f.Open(tc.flags); err != tc.err {
- t.Fatalf("open with flags %v got %v, want %v", tc.flags, err, tc.err)
- }
- return
- }
-
- // Run an FD test, since we expect success.
- fdTest(t, func(send *fd.FD) *fd.FD {
- backend.EXPECT().Open(tc.flags).Return(send, p9.QID{}, uint32(0), nil).Times(1)
- recv, _, _, err := f.Open(tc.flags)
- if err != tc.err {
- t.Fatalf("open with flags %v got %v, want %v", tc.flags, err, tc.err)
- }
- return recv
- })
-
- // If the open was successful, attempt another one.
- if _, _, _, err := f.Open(tc.flags); err != syscall.EINVAL {
- t.Errorf("second open with flags %v got %v, want EINVAL", tc.flags, err)
- }
-
- // Ensure that all illegal operations fail.
- if _, _, err := f.Walk(nil); err != syscall.EINVAL && err != syscall.EBUSY {
- t.Errorf("walk got %v, wanted EINVAL or EBUSY", err)
- }
- if _, _, _, _, err := f.WalkGetAttr(nil); err != syscall.EINVAL && err != syscall.EBUSY {
- t.Errorf("walkgetattr got %v, wanted EINVAL or EBUSY", err)
- }
- })
- }
- }
-}
-
-func TestClose(t *testing.T) {
- type closeTest struct {
- name string
- closeFn func(backend *Mock, f p9.File)
- }
-
- cases := []closeTest{
- {
- name: "close",
- closeFn: func(_ *Mock, f p9.File) {
- f.Close()
- },
- },
- {
- name: "remove",
- closeFn: func(backend *Mock, f p9.File) {
- // Allow the rename call in the parent, automatically translated.
- backend.parent.EXPECT().UnlinkAt(gomock.Any(), gomock.Any()).Times(1)
- f.(deprecatedRemover).Remove()
- },
- },
- }
-
- for name := range newTypeMap(nil) {
- for _, tc := range cases {
- t.Run(fmt.Sprintf("%s(%s)", tc.name, name), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
-
- // Close via the prescribed method.
- tc.closeFn(backend, f)
-
- // Everything should fail with EBADF.
- if _, _, err := f.Walk(nil); err != syscall.EBADF {
- t.Errorf("walk got %v, wanted EBADF", err)
- }
- if _, err := f.StatFS(); err != syscall.EBADF {
- t.Errorf("statfs got %v, wanted EBADF", err)
- }
- if _, _, _, err := f.GetAttr(p9.AttrMaskAll()); err != syscall.EBADF {
- t.Errorf("getattr got %v, wanted EBADF", err)
- }
- if err := f.SetAttr(p9.SetAttrMask{}, p9.SetAttr{}); err != syscall.EBADF {
- t.Errorf("setattrk got %v, wanted EBADF", err)
- }
- if err := f.Rename(root, "new-name"); err != syscall.EBADF {
- t.Errorf("rename got %v, wanted EBADF", err)
- }
- if err := f.Close(); err != syscall.EBADF {
- t.Errorf("close got %v, wanted EBADF", err)
- }
- if _, _, _, err := f.Open(p9.ReadOnly); err != syscall.EBADF {
- t.Errorf("open got %v, wanted EBADF", err)
- }
- if _, err := f.ReadAt([]byte("hello"), 0); err != syscall.EBADF {
- t.Errorf("readAt got %v, wanted EBADF", err)
- }
- if _, err := f.WriteAt(make([]byte, 6), 0); err != syscall.EBADF {
- t.Errorf("writeAt got %v, wanted EBADF", err)
- }
- if err := f.FSync(); err != syscall.EBADF {
- t.Errorf("fsync got %v, wanted EBADF", err)
- }
- if _, _, _, _, err := f.Create("new-file", p9.ReadWrite, 0, 0, 0); err != syscall.EBADF {
- t.Errorf("create got %v, wanted EBADF", err)
- }
- if _, err := f.Mkdir("new-directory", 0, 0, 0); err != syscall.EBADF {
- t.Errorf("mkdir got %v, wanted EBADF", err)
- }
- if _, err := f.Symlink("old-name", "new-name", 0, 0); err != syscall.EBADF {
- t.Errorf("symlink got %v, wanted EBADF", err)
- }
- if err := f.Link(root, "new-name"); err != syscall.EBADF {
- t.Errorf("link got %v, wanted EBADF", err)
- }
- if _, err := f.Mknod("new-block-device", 0, 0, 0, 0, 0); err != syscall.EBADF {
- t.Errorf("mknod got %v, wanted EBADF", err)
- }
- if err := f.RenameAt("old-name", root, "new-name"); err != syscall.EBADF {
- t.Errorf("renameAt got %v, wanted EBADF", err)
- }
- if err := f.UnlinkAt("name", 0); err != syscall.EBADF {
- t.Errorf("unlinkAt got %v, wanted EBADF", err)
- }
- if _, err := f.Readdir(0, 1); err != syscall.EBADF {
- t.Errorf("readdir got %v, wanted EBADF", err)
- }
- if _, err := f.Readlink(); err != syscall.EBADF {
- t.Errorf("readlink got %v, wanted EBADF", err)
- }
- if err := f.Flush(); err != syscall.EBADF {
- t.Errorf("flush got %v, wanted EBADF", err)
- }
- if _, _, _, _, err := f.WalkGetAttr(nil); err != syscall.EBADF {
- t.Errorf("walkgetattr got %v, wanted EBADF", err)
- }
- if _, err := f.Connect(p9.ConnectFlags(0)); err != syscall.EBADF {
- t.Errorf("connect got %v, wanted EBADF", err)
- }
- })
- }
- }
-}
-
-// onlyWorksOnOpenThings is a helper test method for operations that should
-// only work on files that have been explicitly opened.
-func onlyWorksOnOpenThings(h *Harness, t *testing.T, name string, root p9.File, mode p9.OpenFlags, expectedErr error, fn func(backend *Mock, f p9.File, shouldSucceed bool) error) {
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- // Does it work before opening?
- if err := fn(backend, f, false); err != syscall.EINVAL {
- t.Errorf("operation got %v, wanted EINVAL", err)
- }
-
- // Is this openable?
- if !p9.CanOpen(backend.Attr.Mode) {
- return // Nothing to do.
- }
-
- // If this is a directory, we can't handle writing.
- if backend.Attr.Mode.IsDir() && (mode == p9.ReadWrite || mode == p9.WriteOnly) {
- return // Skip.
- }
-
- // Open the file.
- backend.EXPECT().Open(mode)
- if _, _, _, err := f.Open(mode); err != nil {
- t.Fatalf("open got %v, wanted nil", err)
- }
-
- // Attempt the operation.
- if err := fn(backend, f, expectedErr == nil); err != expectedErr {
- t.Fatalf("operation got %v, wanted %v", err, expectedErr)
- }
-}
-
-func TestRead(t *testing.T) {
- type readTest struct {
- name string
- mode p9.OpenFlags
- err error
- }
-
- cases := []readTest{
- {
- name: "read-only",
- mode: p9.ReadOnly,
- err: nil,
- },
- {
- name: "read-write",
- mode: p9.ReadWrite,
- err: nil,
- },
- {
- name: "write-only",
- mode: p9.WriteOnly,
- err: syscall.EPERM,
- },
- }
-
- for name := range newTypeMap(nil) {
- for _, tc := range cases {
- t.Run(fmt.Sprintf("%s-%s", tc.name, name), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- const message = "hello"
-
- onlyWorksOnOpenThings(h, t, name, root, tc.mode, tc.err, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if !shouldSucceed {
- _, err := f.ReadAt([]byte(message), 0)
- return err
- }
-
- // Prepare for the call to readAt in the backend.
- backend.EXPECT().ReadAt(gomock.Any(), uint64(0)).Do(func(p []byte, offset uint64) {
- copy(p, message)
- }).Return(len(message), nil)
-
- // Make the client call.
- p := make([]byte, 2*len(message)) // Double size.
- n, err := f.ReadAt(p, 0)
-
- // Sanity check result.
- if err != nil {
- return err
- }
- if n != len(message) {
- t.Fatalf("message length incorrect, got %d, want %d", n, len(message))
- }
- if !bytes.Equal(p[:n], []byte(message)) {
- t.Fatalf("message incorrect, got %v, want %v", p, []byte(message))
- }
- return nil // Success.
- })
- })
- }
- }
-}
-
-func TestWrite(t *testing.T) {
- type writeTest struct {
- name string
- mode p9.OpenFlags
- err error
- }
-
- cases := []writeTest{
- {
- name: "read-only",
- mode: p9.ReadOnly,
- err: syscall.EPERM,
- },
- {
- name: "read-write",
- mode: p9.ReadWrite,
- err: nil,
- },
- {
- name: "write-only",
- mode: p9.WriteOnly,
- err: nil,
- },
- }
-
- for name := range newTypeMap(nil) {
- for _, tc := range cases {
- t.Run(fmt.Sprintf("%s-%s", tc.name, name), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- const message = "hello"
-
- onlyWorksOnOpenThings(h, t, name, root, tc.mode, tc.err, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if !shouldSucceed {
- _, err := f.WriteAt([]byte(message), 0)
- return err
- }
-
- // Prepare for the call to readAt in the backend.
- var output []byte // Saved by Do below.
- backend.EXPECT().WriteAt(gomock.Any(), uint64(0)).Do(func(p []byte, offset uint64) {
- output = p
- }).Return(len(message), nil)
-
- // Make the client call.
- n, err := f.WriteAt([]byte(message), 0)
-
- // Sanity check result.
- if err != nil {
- return err
- }
- if n != len(message) {
- t.Fatalf("message length incorrect, got %d, want %d", n, len(message))
- }
- if !bytes.Equal(output, []byte(message)) {
- t.Fatalf("message incorrect, got %v, want %v", output, []byte(message))
- }
- return nil // Success.
- })
- })
- }
- }
-}
-
-func TestFSync(t *testing.T) {
- for name := range newTypeMap(nil) {
- for _, mode := range []p9.OpenFlags{p9.ReadOnly, p9.WriteOnly, p9.ReadWrite} {
- t.Run(fmt.Sprintf("%s-%s", mode, name), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnOpenThings(h, t, name, root, mode, nil, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if shouldSucceed {
- backend.EXPECT().FSync().Times(1)
- }
- return f.FSync()
- })
- })
- }
- }
-}
-
-func TestFlush(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- backend.EXPECT().Flush()
- f.Flush()
- })
- }
-}
-
-// onlyWorksOnDirectories is a helper test method for operations that should
-// only work on unopened directories, such as create, mkdir and symlink.
-func onlyWorksOnDirectories(h *Harness, t *testing.T, name string, root p9.File, fn func(backend *Mock, f p9.File, shouldSucceed bool) error) {
- // Walk to the file normally.
- _, backend, f := walkHelper(h, name, root)
- defer f.Close()
-
- // Only directories support mknod.
- if !backend.Attr.Mode.IsDir() {
- if err := fn(backend, f, false); err != syscall.EINVAL {
- t.Errorf("operation got %v, wanted EINVAL", err)
- }
- return // Nothing else to do.
- }
-
- // Should succeed.
- if err := fn(backend, f, true); err != nil {
- t.Fatalf("operation got %v, wanted nil", err)
- }
-
- // Open the directory.
- backend.EXPECT().Open(p9.ReadOnly).Times(1)
- if _, _, _, err := f.Open(p9.ReadOnly); err != nil {
- t.Fatalf("open got %v, wanted nil", err)
- }
-
- // Should not work again.
- if err := fn(backend, f, false); err != syscall.EINVAL {
- t.Fatalf("operation got %v, wanted EINVAL", err)
- }
-}
-
-func TestCreate(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnDirectories(h, t, name, root, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if !shouldSucceed {
- _, _, _, _, err := f.Create("new-file", p9.ReadWrite, 0, 1, 2)
- return err
- }
-
- // If the create is going to succeed, then we
- // need to create a new backend file, and we
- // clone to ensure that we don't close the
- // original.
- _, newF, err := f.Walk(nil)
- if err != nil {
- t.Fatalf("clone got %v, wanted nil", err)
- }
- defer newF.Close()
- newBackend := h.Pop(newF)
-
- // Run a regular FD test to validate that path.
- fdTest(t, func(send *fd.FD) *fd.FD {
- // Return the send FD on success.
- newFile := h.NewFile()(backend) // New file with the parent backend.
- newBackend.EXPECT().Create("new-file", p9.ReadWrite, p9.FileMode(0), p9.UID(1), p9.GID(2)).Return(send, newFile, p9.QID{}, uint32(0), nil)
-
- // Receive the fd back.
- recv, _, _, _, err := newF.Create("new-file", p9.ReadWrite, 0, 1, 2)
- if err != nil {
- t.Fatalf("create got %v, wanted nil", err)
- }
- return recv
- })
-
- // The above will fail via normal test flow, so
- // we can assume that it passed.
- return nil
- })
- })
- }
-}
-
-func TestCreateInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if _, _, _, _, err := root.Create(invalidName, p9.ReadWrite, 0, 0, 0); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-func TestMkdir(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnDirectories(h, t, name, root, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if shouldSucceed {
- backend.EXPECT().Mkdir("new-directory", p9.FileMode(0), p9.UID(1), p9.GID(2))
- }
- _, err := f.Mkdir("new-directory", 0, 1, 2)
- return err
- })
- })
- }
-}
-
-func TestMkdirInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if _, err := root.Mkdir(invalidName, 0, 0, 0); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-func TestSymlink(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnDirectories(h, t, name, root, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if shouldSucceed {
- backend.EXPECT().Symlink("old-name", "new-name", p9.UID(1), p9.GID(2))
- }
- _, err := f.Symlink("old-name", "new-name", 1, 2)
- return err
- })
- })
- }
-}
-
-func TestSyminkInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- // We need only test for invalid names in the new name,
- // the target can be an arbitrary string and we don't
- // need to sanity check it.
- if _, err := root.Symlink("old-name", invalidName, 0, 0); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-func TestLink(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnDirectories(h, t, name, root, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if shouldSucceed {
- backend.EXPECT().Link(gomock.Any(), "new-link")
- }
- return f.Link(f, "new-link")
- })
- })
- }
-}
-
-func TestLinkInvalid(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- for name := range newTypeMap(nil) {
- for _, invalidName := range allInvalidNames(name) {
- if err := root.Link(root, invalidName); err != syscall.EINVAL {
- t.Errorf("got %v for name %q, want EINVAL", err, invalidName)
- }
- }
- }
-}
-
-func TestMknod(t *testing.T) {
- for name := range newTypeMap(nil) {
- t.Run(name, func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- onlyWorksOnDirectories(h, t, name, root, func(backend *Mock, f p9.File, shouldSucceed bool) error {
- if shouldSucceed {
- backend.EXPECT().Mknod("new-block-device", p9.FileMode(0), uint32(1), uint32(2), p9.UID(3), p9.GID(4)).Times(1)
- }
- _, err := f.Mknod("new-block-device", 0, 1, 2, 3, 4)
- return err
- })
- })
- }
-}
-
-// concurrentFn is a specification of a concurrent operation. This is used to
-// drive the concurrency tests below.
-type concurrentFn struct {
- name string
- match func(p9.FileMode) bool
- op func(h *Harness, backend *Mock, f p9.File, callback func())
-}
-
-func concurrentTest(t *testing.T, name string, fn1, fn2 concurrentFn, sameDir, expectedOkay bool) {
- var (
- names1 []string
- names2 []string
- )
- if sameDir {
- // Use the same file one directory up.
- names1, names2 = []string{"one", name}, []string{"one", name}
- } else {
- // For different directories, just use siblings.
- names1, names2 = []string{"one", name}, []string{"three", name}
- }
-
- t.Run(fmt.Sprintf("%s(%v)+%s(%v)", fn1.name, names1, fn2.name, names2), func(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- // Walk to both files as given.
- _, f1, err := root.Walk(names1)
- if err != nil {
- t.Fatalf("error walking, got %v, want nil", err)
- }
- defer f1.Close()
- b1 := h.Pop(f1)
- _, f2, err := root.Walk(names2)
- if err != nil {
- t.Fatalf("error walking, got %v, want nil", err)
- }
- defer f2.Close()
- b2 := h.Pop(f2)
-
- // Are these a good match for the current test case?
- if !fn1.match(b1.Attr.Mode) {
- t.SkipNow()
- }
- if !fn2.match(b2.Attr.Mode) {
- t.SkipNow()
- }
-
- // Construct our "concurrency creator".
- in1 := make(chan struct{}, 1)
- in2 := make(chan struct{}, 1)
- var top sync.WaitGroup
- var fns sync.WaitGroup
- defer top.Wait()
- top.Add(2) // Accounting for below.
- defer fns.Done()
- fns.Add(1) // See line above; released before top.Wait.
- go func() {
- defer top.Done()
- fn1.op(h, b1, f1, func() {
- in1 <- struct{}{}
- fns.Wait()
- })
- }()
- go func() {
- defer top.Done()
- fn2.op(h, b2, f2, func() {
- in2 <- struct{}{}
- fns.Wait()
- })
- }()
-
- // Compute a reasonable timeout. If we expect the operation to hang,
- // give it 10 milliseconds before we assert that it's fine. After all,
- // there will be a lot of these tests. If we don't expect it to hang,
- // give it a full minute, since the machine could be slow.
- timeout := 10 * time.Millisecond
- if expectedOkay {
- timeout = 1 * time.Minute
- }
-
- // Read the first channel.
- var second chan struct{}
- select {
- case <-in1:
- second = in2
- case <-in2:
- second = in1
- }
-
- // Catch concurrency.
- select {
- case <-second:
- // We finished successful. Is this good? Depends on the
- // expected result.
- if !expectedOkay {
- t.Errorf("%q and %q proceeded concurrently!", fn1.name, fn2.name)
- }
- case <-time.After(timeout):
- // Great, things did not proceed concurrently. Is that what we
- // expected?
- if expectedOkay {
- t.Errorf("%q and %q hung concurrently!", fn1.name, fn2.name)
- }
- }
- })
-}
-
-func randomFileName() string {
- return fmt.Sprintf("%x", rand.Int63())
-}
-
-func TestConcurrency(t *testing.T) {
- readExclusive := []concurrentFn{
- {
- // N.B. We can't explicitly check WalkGetAttr behavior,
- // but we rely on the fact that the internal code paths
- // are the same.
- name: "walk",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- // See the documentation of WalkCallback.
- // Because walk is actually implemented by the
- // mock, we need a special place for this
- // callback.
- //
- // Note that a clone actually locks the parent
- // node. So we walk from this node to test
- // concurrent operations appropriately.
- backend.WalkCallback = func() error {
- callback()
- return nil
- }
- f.Walk([]string{randomFileName()}) // Won't exist.
- },
- },
- {
- name: "fsync",
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Open(gomock.Any())
- backend.EXPECT().FSync().Do(func() {
- callback()
- })
- f.Open(p9.ReadOnly) // Required.
- f.FSync()
- },
- },
- {
- name: "readdir",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Open(gomock.Any())
- backend.EXPECT().Readdir(gomock.Any(), gomock.Any()).Do(func(uint64, uint32) {
- callback()
- })
- f.Open(p9.ReadOnly) // Required.
- f.Readdir(0, 1)
- },
- },
- {
- name: "readlink",
- match: func(mode p9.FileMode) bool { return mode.IsSymlink() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Readlink().Do(func() {
- callback()
- })
- f.Readlink()
- },
- },
- {
- name: "connect",
- match: func(mode p9.FileMode) bool { return mode.IsSocket() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Connect(gomock.Any()).Do(func(p9.ConnectFlags) {
- callback()
- })
- f.Connect(0)
- },
- },
- {
- name: "open",
- match: func(mode p9.FileMode) bool { return p9.CanOpen(mode) },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Open(gomock.Any()).Do(func(p9.OpenFlags) {
- callback()
- })
- f.Open(p9.ReadOnly)
- },
- },
- {
- name: "flush",
- match: func(mode p9.FileMode) bool { return true },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Flush().Do(func() {
- callback()
- })
- f.Flush()
- },
- },
- }
- writeExclusive := []concurrentFn{
- {
- // N.B. We can't really check getattr. But this is an
- // extremely low-risk function, it seems likely that
- // this check is paranoid anyways.
- name: "setattr",
- match: func(mode p9.FileMode) bool { return true },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().SetAttr(gomock.Any(), gomock.Any()).Do(func(p9.SetAttrMask, p9.SetAttr) {
- callback()
- })
- f.SetAttr(p9.SetAttrMask{}, p9.SetAttr{})
- },
- },
- {
- name: "unlinkAt",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().UnlinkAt(gomock.Any(), gomock.Any()).Do(func(string, uint32) {
- callback()
- })
- f.UnlinkAt(randomFileName(), 0)
- },
- },
- {
- name: "mknod",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Mknod(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(func(string, p9.FileMode, uint32, uint32, p9.UID, p9.GID) {
- callback()
- })
- f.Mknod(randomFileName(), 0, 0, 0, 0, 0)
- },
- },
- {
- name: "link",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Link(gomock.Any(), gomock.Any()).Do(func(p9.File, string) {
- callback()
- })
- f.Link(f, randomFileName())
- },
- },
- {
- name: "symlink",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Symlink(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(func(string, string, p9.UID, p9.GID) {
- callback()
- })
- f.Symlink(randomFileName(), randomFileName(), 0, 0)
- },
- },
- {
- name: "mkdir",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().Mkdir(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(func(string, p9.FileMode, p9.UID, p9.GID) {
- callback()
- })
- f.Mkdir(randomFileName(), 0, 0, 0)
- },
- },
- {
- name: "create",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- // Return an error for the creation operation, as this is the simplest.
- backend.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, p9.QID{}, uint32(0), syscall.EINVAL).Do(func(string, p9.OpenFlags, p9.FileMode, p9.UID, p9.GID) {
- callback()
- })
- f.Create(randomFileName(), p9.ReadOnly, 0, 0, 0)
- },
- },
- }
- globalExclusive := []concurrentFn{
- {
- name: "remove",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- // Remove operates on a locked parent. So we
- // add a child, walk to it and call remove.
- // Note that because this operation can operate
- // concurrently with itself, we need to
- // generate a random file name.
- randomFile := randomFileName()
- backend.AddChild(randomFile, h.NewFile())
- defer backend.RemoveChild(randomFile)
- _, file, err := f.Walk([]string{randomFile})
- if err != nil {
- h.t.Fatalf("walk got %v, want nil", err)
- }
-
- // Remove is automatically translated to the parent.
- backend.EXPECT().UnlinkAt(gomock.Any(), gomock.Any()).Do(func(string, uint32) {
- callback()
- })
-
- // Remove is also a close.
- file.(deprecatedRemover).Remove()
- },
- },
- {
- name: "rename",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- // Similarly to remove, because we need to
- // operate on a child, we allow a walk.
- randomFile := randomFileName()
- backend.AddChild(randomFile, h.NewFile())
- defer backend.RemoveChild(randomFile)
- _, file, err := f.Walk([]string{randomFile})
- if err != nil {
- h.t.Fatalf("walk got %v, want nil", err)
- }
- defer file.Close()
- fileBackend := h.Pop(file)
-
- // Rename is automatically translated to the parent.
- backend.EXPECT().RenameAt(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(string, p9.File, string) {
- callback()
- })
-
- // Attempt the rename.
- fileBackend.EXPECT().Renamed(gomock.Any(), gomock.Any())
- file.Rename(f, randomFileName())
- },
- },
- {
- name: "renameAt",
- match: func(mode p9.FileMode) bool { return mode.IsDir() },
- op: func(h *Harness, backend *Mock, f p9.File, callback func()) {
- backend.EXPECT().RenameAt(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(string, p9.File, string) {
- callback()
- })
-
- // Attempt the rename. There are no active fids
- // with this name, so we don't need to expect
- // Renamed hooks on anything.
- f.RenameAt(randomFileName(), f, randomFileName())
- },
- },
- }
-
- for _, fn1 := range readExclusive {
- for _, fn2 := range readExclusive {
- for name := range newTypeMap(nil) {
- // Everything should be able to proceed in parallel.
- concurrentTest(t, name, fn1, fn2, true, true)
- concurrentTest(t, name, fn1, fn2, false, true)
- }
- }
- }
-
- for _, fn1 := range append(readExclusive, writeExclusive...) {
- for _, fn2 := range writeExclusive {
- for name := range newTypeMap(nil) {
- // Only cross-directory functions should proceed in parallel.
- concurrentTest(t, name, fn1, fn2, true, false)
- concurrentTest(t, name, fn1, fn2, false, true)
- }
- }
- }
-
- for _, fn1 := range append(append(readExclusive, writeExclusive...), globalExclusive...) {
- for _, fn2 := range globalExclusive {
- for name := range newTypeMap(nil) {
- // Nothing should be able to run in parallel.
- concurrentTest(t, name, fn1, fn2, true, false)
- concurrentTest(t, name, fn1, fn2, false, false)
- }
- }
- }
-}
-
-func TestReadWriteConcurrent(t *testing.T) {
- h, c := NewHarness(t)
- defer h.Finish()
-
- _, root := newRoot(h, c)
- defer root.Close()
-
- const (
- instances = 10
- iterations = 10000
- dataSize = 1024
- )
- var (
- dataSets [instances][dataSize]byte
- backends [instances]*Mock
- files [instances]p9.File
- )
-
- // Walk to the file normally.
- for i := 0; i < instances; i++ {
- _, backends[i], files[i] = walkHelper(h, "file", root)
- defer files[i].Close()
- }
-
- // Open the files.
- for i := 0; i < instances; i++ {
- backends[i].EXPECT().Open(p9.ReadWrite)
- if _, _, _, err := files[i].Open(p9.ReadWrite); err != nil {
- t.Fatalf("open got %v, wanted nil", err)
- }
- }
-
- // Initialize random data for each instance.
- for i := 0; i < instances; i++ {
- if _, err := rand.Read(dataSets[i][:]); err != nil {
- t.Fatalf("error initializing dataSet#%d, got %v", i, err)
- }
- }
-
- // Define our random read/write mechanism.
- randRead := func(h *Harness, backend *Mock, f p9.File, data, test []byte) {
- // Prepare the backend.
- backend.EXPECT().ReadAt(gomock.Any(), uint64(0)).Do(func(p []byte, offset uint64) {
- if n := copy(p, data); n != len(data) {
- // Note that we have to assert the result here, as the Return statement
- // below cannot be dynamic: it will be bound before this call is made.
- h.t.Errorf("wanted length %d, got %d", len(data), n)
- }
- }).Return(len(data), nil)
-
- // Execute the read.
- if n, err := f.ReadAt(test, 0); n != len(test) || err != nil {
- t.Errorf("failed read: wanted (%d, nil), got (%d, %v)", len(test), n, err)
- return // No sense doing check below.
- }
- if !bytes.Equal(test, data) {
- t.Errorf("data integrity failed during read") // Not as expected.
- }
- }
- randWrite := func(h *Harness, backend *Mock, f p9.File, data []byte) {
- // Prepare the backend.
- backend.EXPECT().WriteAt(gomock.Any(), uint64(0)).Do(func(p []byte, offset uint64) {
- if !bytes.Equal(p, data) {
- h.t.Errorf("data integrity failed during write") // Not as expected.
- }
- }).Return(len(data), nil)
-
- // Execute the write.
- if n, err := f.WriteAt(data, 0); n != len(data) || err != nil {
- t.Errorf("failed read: wanted (%d, nil), got (%d, %v)", len(data), n, err)
- }
- }
- randReadWrite := func(n int, h *Harness, backend *Mock, f p9.File, data []byte) {
- test := make([]byte, len(data))
- for i := 0; i < n; i++ {
- if rand.Intn(2) == 0 {
- randRead(h, backend, f, data, test)
- } else {
- randWrite(h, backend, f, data)
- }
- }
- }
-
- // Start reading and writing.
- var wg sync.WaitGroup
- for i := 0; i < instances; i++ {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- randReadWrite(iterations, h, backends[i], files[i], dataSets[i][:])
- }(i)
- }
- wg.Wait()
-}
diff --git a/pkg/p9/p9test/p9test.go b/pkg/p9/p9test/p9test.go
deleted file mode 100644
index dd8b01b6d..000000000
--- a/pkg/p9/p9test/p9test.go
+++ /dev/null
@@ -1,329 +0,0 @@
-// Copyright 2018 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 p9test provides standard mocks for p9.
-package p9test
-
-import (
- "fmt"
- "sync/atomic"
- "syscall"
- "testing"
-
- "github.com/golang/mock/gomock"
- "gvisor.dev/gvisor/pkg/p9"
- "gvisor.dev/gvisor/pkg/sync"
- "gvisor.dev/gvisor/pkg/unet"
-)
-
-// Harness is an attacher mock.
-type Harness struct {
- t *testing.T
- mockCtrl *gomock.Controller
- Attacher *MockAttacher
- wg sync.WaitGroup
- clientSocket *unet.Socket
- mu sync.Mutex
- created []*Mock
-}
-
-// globalPath is a QID.Path Generator.
-var globalPath uint64
-
-// MakePath returns a globally unique path.
-func MakePath() uint64 {
- return atomic.AddUint64(&globalPath, 1)
-}
-
-// Generator is a function that generates a new file.
-type Generator func(parent *Mock) *Mock
-
-// Mock is a common mock element.
-type Mock struct {
- p9.DefaultWalkGetAttr
- *MockFile
- parent *Mock
- closed bool
- harness *Harness
- QID p9.QID
- Attr p9.Attr
- children map[string]Generator
-
- // WalkCallback is a special function that will be called from within
- // the walk context. This is needed for the concurrent tests within
- // this package.
- WalkCallback func() error
-}
-
-// globalMu protects the children maps in all mocks. Note that this is not a
-// particularly elegant solution, but because the test has walks from the root
-// through to final nodes, we must share maps below, and it's easiest to simply
-// protect against concurrent access globally.
-var globalMu sync.RWMutex
-
-// AddChild adds a new child to the Mock.
-func (m *Mock) AddChild(name string, generator Generator) {
- globalMu.Lock()
- defer globalMu.Unlock()
- m.children[name] = generator
-}
-
-// RemoveChild removes the child with the given name.
-func (m *Mock) RemoveChild(name string) {
- globalMu.Lock()
- defer globalMu.Unlock()
- delete(m.children, name)
-}
-
-// Matches implements gomock.Matcher.Matches.
-func (m *Mock) Matches(x interface{}) bool {
- if om, ok := x.(*Mock); ok {
- return m.QID.Path == om.QID.Path
- }
- return false
-}
-
-// String implements gomock.Matcher.String.
-func (m *Mock) String() string {
- return fmt.Sprintf("Mock{Mode: 0x%x, QID.Path: %d}", m.Attr.Mode, m.QID.Path)
-}
-
-// GetAttr returns the current attributes.
-func (m *Mock) GetAttr(mask p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) {
- return m.QID, p9.AttrMaskAll(), m.Attr, nil
-}
-
-// Walk supports clone and walking in directories.
-func (m *Mock) Walk(names []string) ([]p9.QID, p9.File, error) {
- if m.WalkCallback != nil {
- if err := m.WalkCallback(); err != nil {
- return nil, nil, err
- }
- }
- if len(names) == 0 {
- // Clone the file appropriately.
- nm := m.harness.NewMock(m.parent, m.QID.Path, m.Attr)
- nm.children = m.children // Inherit children.
- return []p9.QID{nm.QID}, nm, nil
- } else if len(names) != 1 {
- m.harness.t.Fail() // Should not happen.
- return nil, nil, syscall.EINVAL
- }
-
- if m.Attr.Mode.IsDir() {
- globalMu.RLock()
- defer globalMu.RUnlock()
- if fn, ok := m.children[names[0]]; ok {
- // Generate the child.
- nm := fn(m)
- return []p9.QID{nm.QID}, nm, nil
- }
- // No child found.
- return nil, nil, syscall.ENOENT
- }
-
- // Call the underlying mock.
- return m.MockFile.Walk(names)
-}
-
-// WalkGetAttr calls the default implementation; this is a client-side optimization.
-func (m *Mock) WalkGetAttr(names []string) ([]p9.QID, p9.File, p9.AttrMask, p9.Attr, error) {
- return m.DefaultWalkGetAttr.WalkGetAttr(names)
-}
-
-// Pop pops off the most recently created Mock and assert that this mock
-// represents the same file passed in. If nil is passed in, no check is
-// performed.
-//
-// Precondition: there must be at least one Mock or this will panic.
-func (h *Harness) Pop(clientFile p9.File) *Mock {
- h.mu.Lock()
- defer h.mu.Unlock()
-
- if clientFile == nil {
- // If no clientFile is provided, then we always return the last
- // created file. The caller can safely use this as long as
- // there is no concurrency.
- m := h.created[len(h.created)-1]
- h.created = h.created[:len(h.created)-1]
- return m
- }
-
- qid, _, _, err := clientFile.GetAttr(p9.AttrMaskAll())
- if err != nil {
- // We do not expect this to happen.
- panic(fmt.Sprintf("err during Pop: %v", err))
- }
-
- // Find the relevant file in our created list. We must scan the last
- // from back to front to ensure that we favor the most recently
- // generated file.
- for i := len(h.created) - 1; i >= 0; i-- {
- m := h.created[i]
- if qid.Path == m.QID.Path {
- // Copy and truncate.
- copy(h.created[i:], h.created[i+1:])
- h.created = h.created[:len(h.created)-1]
- return m
- }
- }
-
- // Unable to find relevant file.
- panic(fmt.Sprintf("unable to locate file with QID %+v", qid.Path))
-}
-
-// NewMock returns a new base file.
-func (h *Harness) NewMock(parent *Mock, path uint64, attr p9.Attr) *Mock {
- m := &Mock{
- MockFile: NewMockFile(h.mockCtrl),
- parent: parent,
- harness: h,
- QID: p9.QID{
- Type: p9.QIDType((attr.Mode & p9.FileModeMask) >> 12),
- Path: path,
- },
- Attr: attr,
- }
-
- // Always ensure Close is after the parent's close. Note that this
- // can't be done via a straight-forward After call, because the parent
- // might change after initial creation. We ensure that this is true at
- // close time.
- m.EXPECT().Close().Return(nil).Times(1).Do(func() {
- if m.parent != nil && m.parent.closed {
- h.t.FailNow()
- }
- // Note that this should not be racy, as this operation should
- // be protected by the Times(1) above first.
- m.closed = true
- })
-
- // Remember what was created.
- h.mu.Lock()
- defer h.mu.Unlock()
- h.created = append(h.created, m)
-
- return m
-}
-
-// NewFile returns a new file mock.
-//
-// Note that ReadAt and WriteAt must be mocked separately.
-func (h *Harness) NewFile() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeRegular})
- }
-}
-
-// NewDirectory returns a new mock directory.
-//
-// Note that Mkdir, Link, Mknod, RenameAt, UnlinkAt and Readdir must be mocked
-// separately. Walk is provided and children may be manipulated via AddChild
-// and RemoveChild. After calling Walk remotely, one can use Pop to find the
-// corresponding backend mock on the server side.
-func (h *Harness) NewDirectory(contents map[string]Generator) Generator {
- return func(parent *Mock) *Mock {
- m := h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeDirectory})
- m.children = contents // Save contents.
- return m
- }
-}
-
-// NewSymlink returns a new mock directory.
-//
-// Note that Readlink must be mocked separately.
-func (h *Harness) NewSymlink() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeSymlink})
- }
-}
-
-// NewBlockDevice returns a new mock block device.
-func (h *Harness) NewBlockDevice() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeBlockDevice})
- }
-}
-
-// NewCharacterDevice returns a new mock character device.
-func (h *Harness) NewCharacterDevice() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeCharacterDevice})
- }
-}
-
-// NewNamedPipe returns a new mock named pipe.
-func (h *Harness) NewNamedPipe() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeNamedPipe})
- }
-}
-
-// NewSocket returns a new mock socket.
-func (h *Harness) NewSocket() Generator {
- return func(parent *Mock) *Mock {
- return h.NewMock(parent, MakePath(), p9.Attr{Mode: p9.ModeSocket})
- }
-}
-
-// Finish completes all checks and shuts down the server.
-func (h *Harness) Finish() {
- h.clientSocket.Shutdown()
- h.wg.Wait()
- h.mockCtrl.Finish()
-}
-
-// NewHarness creates and returns a new test server.
-//
-// It should always be used as:
-//
-// h, c := NewHarness(t)
-// defer h.Finish()
-//
-func NewHarness(t *testing.T) (*Harness, *p9.Client) {
- // Create the mock.
- mockCtrl := gomock.NewController(t)
- h := &Harness{
- t: t,
- mockCtrl: mockCtrl,
- Attacher: NewMockAttacher(mockCtrl),
- }
-
- // Make socket pair.
- serverSocket, clientSocket, err := unet.SocketPair(false)
- if err != nil {
- t.Fatalf("socketpair got err %v wanted nil", err)
- }
-
- // Start the server, synchronized on exit.
- server := p9.NewServer(h.Attacher)
- h.wg.Add(1)
- go func() {
- defer h.wg.Done()
- server.Handle(serverSocket)
- }()
-
- // Create the client.
- client, err := p9.NewClient(clientSocket, p9.DefaultMessageSize, p9.HighestVersionString())
- if err != nil {
- serverSocket.Close()
- clientSocket.Close()
- t.Fatalf("new client got %v, expected nil", err)
- return nil, nil // Never hit.
- }
-
- // Capture the client socket.
- h.clientSocket = clientSocket
- return h, client
-}