// Copyright 2019 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 testutil provides common test utilities for kernfs-based // filesystems. package testutil import ( "fmt" "io" "strings" "testing" "github.com/google/go-cmp/cmp" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/fspath" "gvisor.dev/gvisor/pkg/sentry/context" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/usermem" "gvisor.dev/gvisor/pkg/sentry/vfs" "gvisor.dev/gvisor/pkg/sync" ) // System represents the context for a single test. // // Test systems must be explicitly destroyed with System.Destroy. type System struct { t *testing.T Ctx context.Context Creds *auth.Credentials VFS *vfs.VirtualFilesystem Root vfs.VirtualDentry mns *vfs.MountNamespace } // NewSystem constructs a System. // // Precondition: Caller must hold a reference on mns, whose ownership // is transferred to the new System. func NewSystem(ctx context.Context, t *testing.T, v *vfs.VirtualFilesystem, mns *vfs.MountNamespace) *System { s := &System{ t: t, Ctx: ctx, Creds: auth.CredentialsFromContext(ctx), VFS: v, mns: mns, Root: mns.Root(), } return s } // WithSubtest creates a temporary test system with a new test harness, // referencing all other resources from the original system. This is useful when // a system is reused for multiple subtests, and the T needs to change for each // case. Note that this is safe when test cases run in parallel, as all // resources referenced by the system are immutable, or handle interior // mutations in a thread-safe manner. // // The returned system must not outlive the original and should not be destroyed // via System.Destroy. func (s *System) WithSubtest(t *testing.T) *System { return &System{ t: t, Ctx: s.Ctx, Creds: s.Creds, VFS: s.VFS, mns: s.mns, Root: s.Root, } } // WithTemporaryContext constructs a temporary test system with a new context // ctx. The temporary system borrows all resources and references from the // original system. The returned temporary system must not outlive the original // system, and should not be destroyed via System.Destroy. func (s *System) WithTemporaryContext(ctx context.Context) *System { return &System{ t: s.t, Ctx: ctx, Creds: s.Creds, VFS: s.VFS, mns: s.mns, Root: s.Root, } } // Destroy release resources associated with a test system. func (s *System) Destroy() { s.Root.DecRef() s.mns.DecRef(s.VFS) // Reference on mns passed to NewSystem. } // ReadToEnd reads the contents of fd until EOF to a string. func (s *System) ReadToEnd(fd *vfs.FileDescription) (string, error) { buf := make([]byte, usermem.PageSize) bufIOSeq := usermem.BytesIOSequence(buf) opts := vfs.ReadOptions{} var content strings.Builder for { n, err := fd.Read(s.Ctx, bufIOSeq, opts) if n == 0 || err != nil { if err == io.EOF { err = nil } return content.String(), err } content.Write(buf[:n]) } } // PathOpAtRoot constructs a PathOperation with the given path from // the root of the filesystem. func (s *System) PathOpAtRoot(path string) *vfs.PathOperation { return &vfs.PathOperation{ Root: s.Root, Start: s.Root, Path: fspath.Parse(path), } } // GetDentryOrDie attempts to resolve a dentry referred to by the // provided path operation. If unsuccessful, the test fails. func (s *System) GetDentryOrDie(pop *vfs.PathOperation) vfs.VirtualDentry { vd, err := s.VFS.GetDentryAt(s.Ctx, s.Creds, pop, &vfs.GetDentryOptions{}) if err != nil { s.t.Fatalf("GetDentryAt(pop:%+v) failed: %v", pop, err) } return vd } // DirentType is an alias for values for linux_dirent64.d_type. type DirentType = uint8 // ListDirents lists the Dirents for a directory at pop. func (s *System) ListDirents(pop *vfs.PathOperation) *DirentCollector { fd, err := s.VFS.OpenAt(s.Ctx, s.Creds, pop, &vfs.OpenOptions{Flags: linux.O_RDONLY}) if err != nil { s.t.Fatalf("OpenAt for PathOperation %+v failed: %v", pop, err) } defer fd.DecRef() collector := &DirentCollector{} if err := fd.IterDirents(s.Ctx, collector); err != nil { s.t.Fatalf("IterDirent failed: %v", err) } return collector } // AssertAllDirentTypes verifies that the set of dirents in collector contains // exactly the specified set of expected entries. AssertAllDirentTypes respects // collector.skipDots, and implicitly checks for "." and ".." accordingly. func (s *System) AssertAllDirentTypes(collector *DirentCollector, expected map[string]DirentType) { // Also implicitly check for "." and "..", if enabled. if !collector.skipDots { expected["."] = linux.DT_DIR expected[".."] = linux.DT_DIR } dentryTypes := make(map[string]DirentType) collector.mu.Lock() for _, dirent := range collector.dirents { dentryTypes[dirent.Name] = dirent.Type } collector.mu.Unlock() if diff := cmp.Diff(expected, dentryTypes); diff != "" { s.t.Fatalf("IterDirent had unexpected results:\n--- want\n+++ got\n%v", diff) } } // AssertDirentOffsets verifies that collector contains at least the entries // specified in expected, with the given NextOff field. Entries specified in // expected but missing from collector result in failure. Extra entries in // collector are ignored. AssertDirentOffsets respects collector.skipDots, and // implicitly checks for "." and ".." accordingly. func (s *System) AssertDirentOffsets(collector *DirentCollector, expected map[string]int64) { // Also implicitly check for "." and "..", if enabled. if !collector.skipDots { expected["."] = 1 expected[".."] = 2 } dentryNextOffs := make(map[string]int64) collector.mu.Lock() for _, dirent := range collector.dirents { // Ignore extra entries in dentries that are not in expected. if _, ok := expected[dirent.Name]; ok { dentryNextOffs[dirent.Name] = dirent.NextOff } } collector.mu.Unlock() if diff := cmp.Diff(expected, dentryNextOffs); diff != "" { s.t.Fatalf("IterDirent had unexpected results:\n--- want\n+++ got\n%v", diff) } } // DirentCollector provides an implementation for vfs.IterDirentsCallback for // testing. It simply iterates to the end of a given directory FD and collects // all dirents emitted by the callback. type DirentCollector struct { mu sync.Mutex order []*vfs.Dirent dirents map[string]*vfs.Dirent // When the collector is used in various Assert* functions, should "." and // ".." be implicitly checked? skipDots bool } // SkipDotsChecks enables or disables the implicit checks on "." and ".." when // the collector is used in various Assert* functions. Note that "." and ".." // are still collected if passed to d.Handle, so the caller should only disable // the checks when they aren't expected. func (d *DirentCollector) SkipDotsChecks(value bool) { d.skipDots = value } // Handle implements vfs.IterDirentsCallback.Handle. func (d *DirentCollector) Handle(dirent vfs.Dirent) bool { d.mu.Lock() if d.dirents == nil { d.dirents = make(map[string]*vfs.Dirent) } d.order = append(d.order, &dirent) d.dirents[dirent.Name] = &dirent d.mu.Unlock() return true } // Count returns the number of dirents currently in the collector. func (d *DirentCollector) Count() int { d.mu.Lock() defer d.mu.Unlock() return len(d.dirents) } // Contains checks whether the collector has a dirent with the given name and // type. func (d *DirentCollector) Contains(name string, typ uint8) error { d.mu.Lock() defer d.mu.Unlock() dirent, ok := d.dirents[name] if !ok { return fmt.Errorf("No dirent named %q found", name) } if dirent.Type != typ { return fmt.Errorf("Dirent named %q found, but was expecting type %s, got: %+v", name, linux.DirentType.Parse(uint64(typ)), dirent) } return nil } // Dirents returns all dirents discovered by this collector. func (d *DirentCollector) Dirents() map[string]*vfs.Dirent { d.mu.Lock() dirents := make(map[string]*vfs.Dirent) for n, d := range d.dirents { dirents[n] = d } d.mu.Unlock() return dirents } // OrderedDirents returns an ordered list of dirents as discovered by this // collector. func (d *DirentCollector) OrderedDirents() []*vfs.Dirent { d.mu.Lock() dirents := make([]*vfs.Dirent, len(d.order)) copy(dirents, d.order) d.mu.Unlock() return dirents }