summaryrefslogtreecommitdiffhomepage
path: root/pkg/p9/p9test/p9test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/p9/p9test/p9test.go')
-rw-r--r--pkg/p9/p9test/p9test.go329
1 files changed, 329 insertions, 0 deletions
diff --git a/pkg/p9/p9test/p9test.go b/pkg/p9/p9test/p9test.go
new file mode 100644
index 000000000..417b55950
--- /dev/null
+++ b/pkg/p9/p9test/p9test.go
@@ -0,0 +1,329 @@
+// Copyright 2018 Google Inc.
+//
+// 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"
+ "sync/atomic"
+ "syscall"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "gvisor.googlesource.com/gvisor/pkg/p9"
+ "gvisor.googlesource.com/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.Close()
+ 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, 1024, 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
+}