// 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" "sync/atomic" "syscall" "testing" "github.com/golang/mock/gomock" "gvisor.dev/gvisor/pkg/p9" "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.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, 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 }