diff options
Diffstat (limited to 'pkg/test/testutil')
-rw-r--r-- | pkg/test/testutil/BUILD | 22 | ||||
-rw-r--r-- | pkg/test/testutil/sh.go | 515 | ||||
-rw-r--r-- | pkg/test/testutil/testutil.go | 597 | ||||
-rw-r--r-- | pkg/test/testutil/testutil_runfiles.go | 75 |
4 files changed, 0 insertions, 1209 deletions
diff --git a/pkg/test/testutil/BUILD b/pkg/test/testutil/BUILD deleted file mode 100644 index 00600a2ad..000000000 --- a/pkg/test/testutil/BUILD +++ /dev/null @@ -1,22 +0,0 @@ -load("//tools:defs.bzl", "go_library") - -package(licenses = ["notice"]) - -go_library( - name = "testutil", - testonly = 1, - srcs = [ - "sh.go", - "testutil.go", - "testutil_runfiles.go", - ], - visibility = ["//:sandbox"], - deps = [ - "//pkg/sync", - "//runsc/config", - "//runsc/specutils", - "@com_github_cenkalti_backoff//:go_default_library", - "@com_github_kr_pty//:go_default_library", - "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", - ], -) diff --git a/pkg/test/testutil/sh.go b/pkg/test/testutil/sh.go deleted file mode 100644 index 1c77562be..000000000 --- a/pkg/test/testutil/sh.go +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright 2020 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 - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "os/exec" - "strings" - "syscall" - "time" - - "github.com/kr/pty" -) - -// Prompt is used as shell prompt. -// It is meant to be unique enough to not be seen in command outputs. -const Prompt = "PROMPT> " - -// Simplistic shell string escape. -func shellEscape(s string) string { - // specialChars is used to determine whether s needs quoting at all. - const specialChars = "\\'\"`${[|&;<>()*?! \t\n" - // If s needs quoting, escapedChars is the set of characters that are - // escaped with a backslash. - const escapedChars = "\\\"$`" - if len(s) == 0 { - return "''" - } - if !strings.ContainsAny(s, specialChars) { - return s - } - var b bytes.Buffer - b.WriteString("\"") - for _, c := range s { - if strings.ContainsAny(string(c), escapedChars) { - b.WriteString("\\") - } - b.WriteRune(c) - } - b.WriteString("\"") - return b.String() -} - -type byteOrError struct { - b byte - err error -} - -// Shell manages a /bin/sh invocation with convenience functions to handle I/O. -// The shell is run in its own interactive TTY and should present its prompt. -type Shell struct { - // cmd is a reference to the underlying sh process. - cmd *exec.Cmd - // cmdFinished is closed when cmd exits. - cmdFinished chan struct{} - - // echo is whether the shell will echo input back to us. - // This helps setting expectations of getting feedback of written bytes. - echo bool - // Control characters we expect to see in the shell. - controlCharIntr string - controlCharEOF string - - // ptyMaster and ptyReplica are the TTY pair associated with the shell. - ptyMaster *os.File - ptyReplica *os.File - // readCh is a channel where everything read from ptyMaster is written. - readCh chan byteOrError - - // logger is used for logging. It may be nil. - logger Logger -} - -// cleanup kills the shell process and closes the TTY. -// Users of this library get a reference to this function with NewShell. -func (s *Shell) cleanup() { - s.logf("cleanup", "Shell cleanup started.") - if s.cmd.ProcessState == nil { - if err := s.cmd.Process.Kill(); err != nil { - s.logf("cleanup", "cannot kill shell process: %v", err) - } - // We don't log the error returned by Wait because the monitorExit - // goroutine will already do so. - s.cmd.Wait() - } - s.ptyReplica.Close() - s.ptyMaster.Close() - // Wait for monitorExit goroutine to write exit status to the debug log. - <-s.cmdFinished - // Empty out everything in the readCh, but don't wait too long for it. - var extraBytes bytes.Buffer - unreadTimeout := time.After(100 * time.Millisecond) -unreadLoop: - for { - select { - case r, ok := <-s.readCh: - if !ok { - break unreadLoop - } else if r.err == nil { - extraBytes.WriteByte(r.b) - } - case <-unreadTimeout: - break unreadLoop - } - } - if extraBytes.Len() > 0 { - s.logIO("unread", extraBytes.Bytes(), nil) - } - s.logf("cleanup", "Shell cleanup complete.") -} - -// logIO logs byte I/O to both standard logging and the test log, if provided. -func (s *Shell) logIO(prefix string, b []byte, err error) { - var sb strings.Builder - if len(b) > 0 { - sb.WriteString(fmt.Sprintf("%q", b)) - } else { - sb.WriteString("(nothing)") - } - if err != nil { - sb.WriteString(fmt.Sprintf(" [error: %v]", err)) - } - s.logf(prefix, "%s", sb.String()) -} - -// logf logs something to both standard logging and the test log, if provided. -func (s *Shell) logf(prefix, format string, values ...interface{}) { - if s.logger != nil { - s.logger.Logf("[%s] %s", prefix, fmt.Sprintf(format, values...)) - } -} - -// monitorExit waits for the shell process to exit and logs the exit result. -func (s *Shell) monitorExit() { - if err := s.cmd.Wait(); err != nil { - s.logf("cmd", "shell process terminated: %v", err) - } else { - s.logf("cmd", "shell process terminated successfully") - } - close(s.cmdFinished) -} - -// reader continuously reads the shell output and populates readCh. -func (s *Shell) reader(ctx context.Context) { - b := make([]byte, 4096) - defer close(s.readCh) - for { - select { - case <-s.cmdFinished: - // Shell process terminated; stop trying to read. - return - case <-ctx.Done(): - // Shell process will also have terminated in this case; - // stop trying to read. - // We don't print an error here because doing so would print this in the - // normal case where the context passed to NewShell is canceled at the - // end of a successful test. - return - default: - // Shell still running, try reading. - } - if got, err := s.ptyMaster.Read(b); err != nil { - s.readCh <- byteOrError{err: err} - if err == io.EOF { - return - } - } else { - for i := 0; i < got; i++ { - s.readCh <- byteOrError{b: b[i]} - } - } - } -} - -// readByte reads a single byte, respecting the context. -func (s *Shell) readByte(ctx context.Context) (byte, error) { - select { - case <-ctx.Done(): - return 0, ctx.Err() - case r := <-s.readCh: - return r.b, r.err - } -} - -// readLoop reads as many bytes as possible until the context expires, b is -// full, or a short time passes. It returns how many bytes it has successfully -// read. -func (s *Shell) readLoop(ctx context.Context, b []byte) (int, error) { - soonCtx, soonCancel := context.WithTimeout(ctx, 5*time.Second) - defer soonCancel() - var i int - for i = 0; i < len(b) && soonCtx.Err() == nil; i++ { - next, err := s.readByte(soonCtx) - if err != nil { - if i > 0 { - s.logIO("read", b[:i-1], err) - } else { - s.logIO("read", nil, err) - } - return i, err - } - b[i] = next - } - s.logIO("read", b[:i], soonCtx.Err()) - return i, soonCtx.Err() -} - -// readLine reads a single line. Strips out all \r characters for convenience. -// Upon error, it will still return what it has read so far. -// It will also exit quickly if the line content it has read so far (without a -// line break) matches `prompt`. -func (s *Shell) readLine(ctx context.Context, prompt string) ([]byte, error) { - soonCtx, soonCancel := context.WithTimeout(ctx, 5*time.Second) - defer soonCancel() - var lineData bytes.Buffer - var b byte - var err error - for soonCtx.Err() == nil && b != '\n' { - b, err = s.readByte(soonCtx) - if err != nil { - data := lineData.Bytes() - s.logIO("read", data, err) - return data, err - } - if b != '\r' { - lineData.WriteByte(b) - } - if bytes.Equal(lineData.Bytes(), []byte(prompt)) { - // Assume that there will not be any further output if we get the prompt. - // This avoids waiting for the read deadline just to read the prompt. - break - } - } - data := lineData.Bytes() - s.logIO("read", data, soonCtx.Err()) - return data, soonCtx.Err() -} - -// Expect verifies that the next `len(want)` bytes we read match `want`. -func (s *Shell) Expect(ctx context.Context, want []byte) error { - errPrefix := fmt.Sprintf("want(%q)", want) - b := make([]byte, len(want)) - got, err := s.readLoop(ctx, b) - if err != nil { - if ctx.Err() != nil { - return fmt.Errorf("%s: context done (%w), got: %q", errPrefix, err, b[:got]) - } - return fmt.Errorf("%s: %w", errPrefix, err) - } - if got < len(want) { - return fmt.Errorf("%s: short read (read %d bytes, expected %d): %q", errPrefix, got, len(want), b[:got]) - } - if !bytes.Equal(b, want) { - return fmt.Errorf("got %q want %q", b, want) - } - return nil -} - -// ExpectString verifies that the next `len(want)` bytes we read match `want`. -func (s *Shell) ExpectString(ctx context.Context, want string) error { - return s.Expect(ctx, []byte(want)) -} - -// ExpectPrompt verifies that the next few bytes we read are the shell prompt. -func (s *Shell) ExpectPrompt(ctx context.Context) error { - return s.ExpectString(ctx, Prompt) -} - -// ExpectEmptyLine verifies that the next few bytes we read are an empty line, -// as defined by any number of carriage or line break characters. -func (s *Shell) ExpectEmptyLine(ctx context.Context) error { - line, err := s.readLine(ctx, Prompt) - if err != nil { - return fmt.Errorf("cannot read line: %w", err) - } - if strings.Trim(string(line), "\r\n") != "" { - return fmt.Errorf("line was not empty: %q", line) - } - return nil -} - -// ExpectLine verifies that the next `len(want)` bytes we read match `want`, -// followed by carriage returns or newline characters. -func (s *Shell) ExpectLine(ctx context.Context, want string) error { - if err := s.ExpectString(ctx, want); err != nil { - return err - } - if err := s.ExpectEmptyLine(ctx); err != nil { - return fmt.Errorf("ExpectLine(%q): no line break: %w", want, err) - } - return nil -} - -// Write writes `b` to the shell and verifies that all of them get written. -func (s *Shell) Write(b []byte) error { - written, err := s.ptyMaster.Write(b) - s.logIO("write", b[:written], err) - if err != nil { - return fmt.Errorf("write(%q): %w", b, err) - } - if written != len(b) { - return fmt.Errorf("write(%q): wrote %d of %d bytes (%q)", b, written, len(b), b[:written]) - } - return nil -} - -// WriteLine writes `line` (to which \n will be appended) to the shell. -// If the shell is in `echo` mode, it will also check that we got these bytes -// back to read. -func (s *Shell) WriteLine(ctx context.Context, line string) error { - if err := s.Write([]byte(line + "\n")); err != nil { - return err - } - if s.echo { - // We expect to see everything we've typed. - if err := s.ExpectLine(ctx, line); err != nil { - return fmt.Errorf("echo: %w", err) - } - } - return nil -} - -// StartCommand is a convenience wrapper for WriteLine that mimics entering a -// command line and pressing Enter. It does some basic shell argument escaping. -func (s *Shell) StartCommand(ctx context.Context, cmd ...string) error { - escaped := make([]string, len(cmd)) - for i, arg := range cmd { - escaped[i] = shellEscape(arg) - } - return s.WriteLine(ctx, strings.Join(escaped, " ")) -} - -// GetCommandOutput gets all following bytes until the prompt is encountered. -// This is useful for matching the output of a command. -// All \r are removed for ease of matching. -func (s *Shell) GetCommandOutput(ctx context.Context) ([]byte, error) { - return s.ReadUntil(ctx, Prompt) -} - -// ReadUntil gets all following bytes until a certain line is encountered. -// This final line is not returned as part of the output, but everything before -// it (including the \n) is included. -// This is useful for matching the output of a command. -// All \r are removed for ease of matching. -func (s *Shell) ReadUntil(ctx context.Context, finalLine string) ([]byte, error) { - var output bytes.Buffer - for ctx.Err() == nil { - line, err := s.readLine(ctx, finalLine) - if err != nil { - return nil, err - } - if bytes.Equal(line, []byte(finalLine)) { - break - } - // readLine ensures that `line` either matches `finalLine` or contains \n. - // Thus we can be confident that `line` has a \n here. - output.Write(line) - } - return output.Bytes(), ctx.Err() -} - -// RunCommand is a convenience wrapper for StartCommand + GetCommandOutput. -func (s *Shell) RunCommand(ctx context.Context, cmd ...string) ([]byte, error) { - if err := s.StartCommand(ctx, cmd...); err != nil { - return nil, err - } - return s.GetCommandOutput(ctx) -} - -// RefreshSTTY interprets output from `stty -a` to check whether we are in echo -// mode and other settings. -// It will assume that any line matching `expectPrompt` means the end of -// the `stty -a` output. -// Why do this rather than using `tcgets`? Because this function can be used in -// conjunction with sub-shell processes that can allocate their own TTYs. -func (s *Shell) RefreshSTTY(ctx context.Context, expectPrompt string) error { - // Temporarily assume we will not get any output. - // If echo is actually on, we'll get the "stty -a" line as if it was command - // output. This is OK because we parse the output generously. - s.echo = false - if err := s.WriteLine(ctx, "stty -a"); err != nil { - return fmt.Errorf("could not run `stty -a`: %w", err) - } - sttyOutput, err := s.ReadUntil(ctx, expectPrompt) - if err != nil { - return fmt.Errorf("cannot get `stty -a` output: %w", err) - } - - // Set default control characters in case we can't see them in the output. - s.controlCharIntr = "^C" - s.controlCharEOF = "^D" - // stty output has two general notations: - // `a = b;` (for control characters), and `option` vs `-option` (for boolean - // options). We parse both kinds here. - // For `a = b;`, `controlChar` contains `a`, and `previousToken` is used to - // set `controlChar` to `previousToken` when we see an "=" token. - var previousToken, controlChar string - for _, token := range strings.Fields(string(sttyOutput)) { - if controlChar != "" { - value := strings.TrimSuffix(token, ";") - switch controlChar { - case "intr": - s.controlCharIntr = value - case "eof": - s.controlCharEOF = value - } - controlChar = "" - } else { - switch token { - case "=": - controlChar = previousToken - case "-echo": - s.echo = false - case "echo": - s.echo = true - } - } - previousToken = token - } - s.logf("stty", "refreshed settings: echo=%v, intr=%q, eof=%q", s.echo, s.controlCharIntr, s.controlCharEOF) - return nil -} - -// sendControlCode sends `code` to the shell and expects to see `repr`. -// If `expectLinebreak` is true, it also expects to see a linebreak. -func (s *Shell) sendControlCode(ctx context.Context, code byte, repr string, expectLinebreak bool) error { - if err := s.Write([]byte{code}); err != nil { - return fmt.Errorf("cannot send %q: %w", code, err) - } - if err := s.ExpectString(ctx, repr); err != nil { - return fmt.Errorf("did not see %s: %w", repr, err) - } - if expectLinebreak { - if err := s.ExpectEmptyLine(ctx); err != nil { - return fmt.Errorf("linebreak after %s: %v", repr, err) - } - } - return nil -} - -// SendInterrupt sends the \x03 (Ctrl+C) control character to the shell. -func (s *Shell) SendInterrupt(ctx context.Context, expectLinebreak bool) error { - return s.sendControlCode(ctx, 0x03, s.controlCharIntr, expectLinebreak) -} - -// SendEOF sends the \x04 (Ctrl+D) control character to the shell. -func (s *Shell) SendEOF(ctx context.Context, expectLinebreak bool) error { - return s.sendControlCode(ctx, 0x04, s.controlCharEOF, expectLinebreak) -} - -// NewShell returns a new managed sh process along with a cleanup function. -// The caller is expected to call this function once it no longer needs the -// shell. -// The optional passed-in logger will be used for logging. -func NewShell(ctx context.Context, logger Logger) (*Shell, func(), error) { - ptyMaster, ptyReplica, err := pty.Open() - if err != nil { - return nil, nil, fmt.Errorf("cannot create PTY: %w", err) - } - cmd := exec.CommandContext(ctx, "/bin/sh", "--noprofile", "--norc", "-i") - cmd.Stdin = ptyReplica - cmd.Stdout = ptyReplica - cmd.Stderr = ptyReplica - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - Setctty: true, - Ctty: 0, - } - cmd.Env = append(cmd.Env, fmt.Sprintf("PS1=%s", Prompt)) - if err := cmd.Start(); err != nil { - return nil, nil, fmt.Errorf("cannot start shell: %w", err) - } - s := &Shell{ - cmd: cmd, - cmdFinished: make(chan struct{}), - ptyMaster: ptyMaster, - ptyReplica: ptyReplica, - readCh: make(chan byteOrError, 1<<20), - logger: logger, - } - s.logf("creation", "Shell spawned.") - go s.monitorExit() - go s.reader(ctx) - setupCtx, setupCancel := context.WithTimeout(ctx, 5*time.Second) - defer setupCancel() - // We expect to see the prompt immediately on startup, - // since the shell is started in interactive mode. - if err := s.ExpectPrompt(setupCtx); err != nil { - s.cleanup() - return nil, nil, fmt.Errorf("did not get initial prompt: %w", err) - } - s.logf("creation", "Initial prompt observed.") - // Get initial TTY settings. - if err := s.RefreshSTTY(setupCtx, Prompt); err != nil { - s.cleanup() - return nil, nil, fmt.Errorf("cannot get initial STTY settings: %w", err) - } - return s, s.cleanup, nil -} diff --git a/pkg/test/testutil/testutil.go b/pkg/test/testutil/testutil.go deleted file mode 100644 index a35c7ffa6..000000000 --- a/pkg/test/testutil/testutil.go +++ /dev/null @@ -1,597 +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 testutil contains utility functions for runsc tests. -package testutil - -import ( - "bufio" - "context" - "debug/elf" - "encoding/base32" - "encoding/json" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "math" - "math/rand" - "net/http" - "os" - "os/exec" - "os/signal" - "path" - "path/filepath" - "strconv" - "strings" - "syscall" - "testing" - "time" - - "github.com/cenkalti/backoff" - specs "github.com/opencontainers/runtime-spec/specs-go" - "gvisor.dev/gvisor/pkg/sync" - "gvisor.dev/gvisor/runsc/config" - "gvisor.dev/gvisor/runsc/specutils" -) - -var ( - checkpoint = flag.Bool("checkpoint", true, "control checkpoint/restore support") - partition = flag.Int("partition", 1, "partition number, this is 1-indexed") - totalPartitions = flag.Int("total_partitions", 1, "total number of partitions") - isRunningWithHostNet = flag.Bool("hostnet", false, "whether test is running with hostnet") -) - -// IsCheckpointSupported returns the relevant command line flag. -func IsCheckpointSupported() bool { - return *checkpoint -} - -// IsRunningWithHostNet returns the relevant command line flag. -func IsRunningWithHostNet() bool { - return *isRunningWithHostNet -} - -// ImageByName mangles the image name used locally. This depends on the image -// build infrastructure in images/ and tools/vm. -func ImageByName(name string) string { - return fmt.Sprintf("gvisor.dev/images/%s", name) -} - -// ConfigureExePath configures the executable for runsc in the test environment. -func ConfigureExePath() error { - path, err := FindFile("runsc/runsc") - if err != nil { - return err - } - specutils.ExePath = path - return nil -} - -// TmpDir returns the absolute path to a writable directory that can be used as -// scratch by the test. -func TmpDir() string { - if dir, ok := os.LookupEnv("TEST_TMPDIR"); ok { - return dir - } - return "/tmp" -} - -// Logger is a simple logging wrapper. -// -// This is designed to be implemented by *testing.T. -type Logger interface { - Name() string - Logf(fmt string, args ...interface{}) -} - -// DefaultLogger logs using the log package. -type DefaultLogger string - -// Name implements Logger.Name. -func (d DefaultLogger) Name() string { - return string(d) -} - -// Logf implements Logger.Logf. -func (d DefaultLogger) Logf(fmt string, args ...interface{}) { - log.Printf(fmt, args...) -} - -// multiLogger logs to multiple Loggers. -type multiLogger []Logger - -// Name implements Logger.Name. -func (m multiLogger) Name() string { - names := make([]string, len(m)) - for i, l := range m { - names[i] = l.Name() - } - return strings.Join(names, "+") -} - -// Logf implements Logger.Logf. -func (m multiLogger) Logf(fmt string, args ...interface{}) { - for _, l := range m { - l.Logf(fmt, args...) - } -} - -// NewMultiLogger returns a new Logger that logs on multiple Loggers. -func NewMultiLogger(loggers ...Logger) Logger { - return multiLogger(loggers) -} - -// Cmd is a simple wrapper. -type Cmd struct { - logger Logger - *exec.Cmd -} - -// CombinedOutput returns the output and logs. -func (c *Cmd) CombinedOutput() ([]byte, error) { - out, err := c.Cmd.CombinedOutput() - if len(out) > 0 { - c.logger.Logf("output: %s", string(out)) - } - if err != nil { - c.logger.Logf("error: %v", err) - } - return out, err -} - -// Command is a simple wrapper around exec.Command, that logs. -func Command(logger Logger, args ...string) *Cmd { - logger.Logf("command: %s", strings.Join(args, " ")) - return &Cmd{ - logger: logger, - Cmd: exec.Command(args[0], args[1:]...), - } -} - -// TestConfig returns the default configuration to use in tests. Note that -// 'RootDir' must be set by caller if required. -func TestConfig(t *testing.T) *config.Config { - logDir := os.TempDir() - if dir, ok := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR"); ok { - logDir = dir + "/" - } - - // Only register flags if config is being used. Otherwise anyone that uses - // testutil will get flags registered and they may conflict. - config.RegisterFlags() - - conf, err := config.NewFromFlags() - if err != nil { - panic(err) - } - // Change test defaults. - conf.Debug = true - conf.DebugLog = path.Join(logDir, "runsc.log."+t.Name()+".%TIMESTAMP%.%COMMAND%") - conf.LogPackets = true - conf.Network = config.NetworkNone - conf.Strace = true - conf.TestOnlyAllowRunAsCurrentUserWithoutChroot = true - return conf -} - -// NewSpecWithArgs creates a simple spec with the given args suitable for use -// in tests. -func NewSpecWithArgs(args ...string) *specs.Spec { - return &specs.Spec{ - // The host filesystem root is the container root. - Root: &specs.Root{ - Path: "/", - Readonly: true, - }, - Process: &specs.Process{ - Args: args, - Env: []string{ - "PATH=" + os.Getenv("PATH"), - }, - Capabilities: specutils.AllCapabilities(), - }, - Mounts: []specs.Mount{ - // Hide the host /etc to avoid any side-effects. - // For example, bash reads /etc/passwd and if it is - // very big, tests can fail by timeout. - { - Type: "tmpfs", - Destination: "/etc", - }, - // Root is readonly, but many tests want to write to tmpdir. - // This creates a writable mount inside the root. Also, when tmpdir points - // to "/tmp", it makes the the actual /tmp to be mounted and not a tmpfs - // inside the sentry. - { - Type: "bind", - Destination: TmpDir(), - Source: TmpDir(), - }, - }, - Hostname: "runsc-test-hostname", - } -} - -// SetupRootDir creates a root directory for containers. -func SetupRootDir() (string, func(), error) { - rootDir, err := ioutil.TempDir(TmpDir(), "containers") - if err != nil { - return "", nil, fmt.Errorf("error creating root dir: %v", err) - } - return rootDir, func() { os.RemoveAll(rootDir) }, nil -} - -// SetupContainer creates a bundle and root dir for the container, generates a -// test config, and writes the spec to config.json in the bundle dir. -func SetupContainer(spec *specs.Spec, conf *config.Config) (rootDir, bundleDir string, cleanup func(), err error) { - rootDir, rootCleanup, err := SetupRootDir() - if err != nil { - return "", "", nil, err - } - conf.RootDir = rootDir - bundleDir, bundleCleanup, err := SetupBundleDir(spec) - if err != nil { - rootCleanup() - return "", "", nil, err - } - return rootDir, bundleDir, func() { - bundleCleanup() - rootCleanup() - }, err -} - -// SetupBundleDir creates a bundle dir and writes the spec to config.json. -func SetupBundleDir(spec *specs.Spec) (string, func(), error) { - bundleDir, err := ioutil.TempDir(TmpDir(), "bundle") - if err != nil { - return "", nil, fmt.Errorf("error creating bundle dir: %v", err) - } - cleanup := func() { os.RemoveAll(bundleDir) } - if err := writeSpec(bundleDir, spec); err != nil { - cleanup() - return "", nil, fmt.Errorf("error writing spec: %v", err) - } - return bundleDir, cleanup, nil -} - -// writeSpec writes the spec to disk in the given directory. -func writeSpec(dir string, spec *specs.Spec) error { - b, err := json.Marshal(spec) - if err != nil { - return err - } - return ioutil.WriteFile(filepath.Join(dir, "config.json"), b, 0755) -} - -// idRandomSrc is a pseudo random generator used to in RandomID. -var idRandomSrc = rand.New(rand.NewSource(time.Now().UnixNano())) - -// idRandomSrcMtx is the mutex protecting idRandomSrc.Read from being used -// concurrently in differnt goroutines. -var idRandomSrcMtx sync.Mutex - -// RandomID returns 20 random bytes following the given prefix. -func RandomID(prefix string) string { - // Read 20 random bytes. - b := make([]byte, 20) - // Rand.Read is not safe for concurrent use. Packetimpact tests can be run in - // parallel now, so we have to protect the Read with a mutex. Otherwise we'll - // run into name conflicts. - // https://golang.org/pkg/math/rand/#Rand.Read - idRandomSrcMtx.Lock() - // "[Read] always returns len(p) and a nil error." --godoc - if _, err := idRandomSrc.Read(b); err != nil { - idRandomSrcMtx.Unlock() - panic("rand.Read failed: " + err.Error()) - } - idRandomSrcMtx.Unlock() - if prefix != "" { - prefix = prefix + "-" - } - return fmt.Sprintf("%s%s", prefix, base32.StdEncoding.EncodeToString(b)) -} - -// RandomContainerID generates a random container id for each test. -// -// The container id is used to create an abstract unix domain socket, which -// must be unique. While the container forbids creating two containers with the -// same name, sometimes between test runs the socket does not get cleaned up -// quickly enough, causing container creation to fail. -func RandomContainerID() string { - return RandomID("test-container") -} - -// Copy copies file from src to dst. -func Copy(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - st, err := in.Stat() - if err != nil { - return err - } - - out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, st.Mode().Perm()) - if err != nil { - return err - } - defer out.Close() - - // Mirror the local user's permissions across all users. This is - // because as we inject things into the container, the UID/GID will - // change. Also, the build system may generate artifacts with different - // modes. At the top-level (volume mapping) we have a big read-only - // knob that can be applied to prevent modifications. - // - // Note that this must be done via a separate Chmod call, otherwise the - // current process's umask will get in the way. - var mode os.FileMode - if st.Mode()&0100 != 0 { - mode |= 0111 - } - if st.Mode()&0200 != 0 { - mode |= 0222 - } - if st.Mode()&0400 != 0 { - mode |= 0444 - } - if err := os.Chmod(dst, mode); err != nil { - return err - } - - _, err = io.Copy(out, in) - return err -} - -// Poll is a shorthand function to poll for something with given timeout. -func Poll(cb func() error, timeout time.Duration) error { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - return PollContext(ctx, cb) -} - -// PollContext is like Poll, but takes a context instead of a timeout. -func PollContext(ctx context.Context, cb func() error) error { - b := backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx) - return backoff.Retry(cb, b) -} - -// WaitForHTTP tries GET requests on a port until the call succeeds or timeout. -func WaitForHTTP(ip string, port int, timeout time.Duration) error { - cb := func() error { - c := &http.Client{ - // Calculate timeout to be able to do minimum 5 attempts. - Timeout: timeout / 5, - } - url := fmt.Sprintf("http://%s:%d/", ip, port) - resp, err := c.Get(url) - if err != nil { - log.Printf("Waiting %s: %v", url, err) - return err - } - resp.Body.Close() - return nil - } - return Poll(cb, timeout) -} - -// Reaper reaps child processes. -type Reaper struct { - // mu protects ch, which will be nil if the reaper is not running. - mu sync.Mutex - ch chan os.Signal -} - -// Start starts reaping child processes. -func (r *Reaper) Start() { - r.mu.Lock() - defer r.mu.Unlock() - - if r.ch != nil { - panic("reaper.Start called on a running reaper") - } - - r.ch = make(chan os.Signal, 1) - signal.Notify(r.ch, syscall.SIGCHLD) - - go func() { - for { - r.mu.Lock() - ch := r.ch - r.mu.Unlock() - if ch == nil { - return - } - - _, ok := <-ch - if !ok { - // Channel closed. - return - } - for { - cpid, _ := syscall.Wait4(-1, nil, syscall.WNOHANG, nil) - if cpid < 1 { - break - } - } - } - }() -} - -// Stop stops reaping child processes. -func (r *Reaper) Stop() { - r.mu.Lock() - defer r.mu.Unlock() - - if r.ch == nil { - panic("reaper.Stop called on a stopped reaper") - } - - signal.Stop(r.ch) - close(r.ch) - r.ch = nil -} - -// StartReaper is a helper that starts a new Reaper and returns a function to -// stop it. -func StartReaper() func() { - r := &Reaper{} - r.Start() - return r.Stop -} - -// WaitUntilRead reads from the given reader until the wanted string is found -// or until timeout. -func WaitUntilRead(r io.Reader, want string, timeout time.Duration) error { - sc := bufio.NewScanner(r) - // done must be accessed atomically. A value greater than 0 indicates - // that the read loop can exit. - doneCh := make(chan bool) - defer close(doneCh) - go func() { - for sc.Scan() { - t := sc.Text() - if strings.Contains(t, want) { - doneCh <- true - return - } - select { - case <-doneCh: - return - default: - } - } - doneCh <- false - }() - - select { - case <-time.After(timeout): - return fmt.Errorf("timeout waiting to read %q", want) - case res := <-doneCh: - if !res { - return fmt.Errorf("reader closed while waiting to read %q", want) - } - return nil - } -} - -// KillCommand kills the process running cmd unless it hasn't been started. It -// returns an error if it cannot kill the process unless the reason is that the -// process has already exited. -// -// KillCommand will also reap the process. -func KillCommand(cmd *exec.Cmd) error { - if cmd.Process == nil { - return nil - } - if err := cmd.Process.Kill(); err != nil { - if !strings.Contains(err.Error(), "process already finished") { - return fmt.Errorf("failed to kill process %v: %v", cmd, err) - } - } - return cmd.Wait() -} - -// WriteTmpFile writes text to a temporary file, closes the file, and returns -// the name of the file. A cleanup function is also returned. -func WriteTmpFile(pattern, text string) (string, func(), error) { - file, err := ioutil.TempFile(TmpDir(), pattern) - if err != nil { - return "", nil, err - } - defer file.Close() - if _, err := file.Write([]byte(text)); err != nil { - return "", nil, err - } - return file.Name(), func() { os.RemoveAll(file.Name()) }, nil -} - -// IsStatic returns true iff the given file is a static binary. -func IsStatic(filename string) (bool, error) { - f, err := elf.Open(filename) - if err != nil { - return false, err - } - for _, prog := range f.Progs { - if prog.Type == elf.PT_INTERP { - return false, nil // Has interpreter. - } - } - return true, nil -} - -// TouchShardStatusFile indicates to Bazel that the test runner supports -// sharding by creating or updating the last modified date of the file -// specified by TEST_SHARD_STATUS_FILE. -// -// See https://docs.bazel.build/versions/master/test-encyclopedia.html#role-of-the-test-runner. -func TouchShardStatusFile() error { - if statusFile, ok := os.LookupEnv("TEST_SHARD_STATUS_FILE"); ok { - cmd := exec.Command("touch", statusFile) - if b, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("touch %q failed:\n output: %s\n error: %s", statusFile, string(b), err.Error()) - } - } - return nil -} - -// TestIndicesForShard returns indices for this test shard based on the -// TEST_SHARD_INDEX and TEST_TOTAL_SHARDS environment vars, as well as -// the passed partition flags. -// -// If either of the env vars are not present, then the function will return all -// tests. If there are more shards than there are tests, then the returned list -// may be empty. -func TestIndicesForShard(numTests int) ([]int, error) { - var ( - shardIndex = 0 - shardTotal = 1 - ) - - indexStr, indexOk := os.LookupEnv("TEST_SHARD_INDEX") - totalStr, totalOk := os.LookupEnv("TEST_TOTAL_SHARDS") - if indexOk && totalOk { - // Parse index and total to ints. - var err error - shardIndex, err = strconv.Atoi(indexStr) - if err != nil { - return nil, fmt.Errorf("invalid TEST_SHARD_INDEX %q: %v", indexStr, err) - } - shardTotal, err = strconv.Atoi(totalStr) - if err != nil { - return nil, fmt.Errorf("invalid TEST_TOTAL_SHARDS %q: %v", totalStr, err) - } - } - - // Combine with the partitions. - partitionSize := shardTotal - shardTotal = (*totalPartitions) * shardTotal - shardIndex = partitionSize*(*partition-1) + shardIndex - - // Calculate! - var indices []int - numBlocks := int(math.Ceil(float64(numTests) / float64(shardTotal))) - for i := 0; i < numBlocks; i++ { - pick := i*shardTotal + shardIndex - if pick < numTests { - indices = append(indices, pick) - } - } - return indices, nil -} diff --git a/pkg/test/testutil/testutil_runfiles.go b/pkg/test/testutil/testutil_runfiles.go deleted file mode 100644 index ece9ea9a1..000000000 --- a/pkg/test/testutil/testutil_runfiles.go +++ /dev/null @@ -1,75 +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 testutil - -import ( - "fmt" - "os" - "path/filepath" -) - -// FindFile searchs for a file inside the test run environment. It returns the -// full path to the file. It fails if none or more than one file is found. -func FindFile(path string) (string, error) { - wd, err := os.Getwd() - if err != nil { - return "", err - } - - // The test root is demarcated by a path element called "__main__". Search for - // it backwards from the working directory. - root := wd - for { - dir, name := filepath.Split(root) - if name == "__main__" { - break - } - if len(dir) == 0 { - return "", fmt.Errorf("directory __main__ not found in %q", wd) - } - // Remove ending slash to loop around. - root = dir[:len(dir)-1] - } - - // Annoyingly, bazel adds the build type to the directory path for go - // binaries, but not for c++ binaries. We use two different patterns to - // to find our file. - patterns := []string{ - // Try the obvious path first. - filepath.Join(root, path), - // If it was a go binary, use a wildcard to match the build - // type. The pattern is: /test-path/__main__/directories/*/file. - filepath.Join(root, filepath.Dir(path), "*", filepath.Base(path)), - } - - for _, p := range patterns { - matches, err := filepath.Glob(p) - if err != nil { - // "The only possible returned error is ErrBadPattern, - // when pattern is malformed." -godoc - return "", fmt.Errorf("error globbing %q: %v", p, err) - } - switch len(matches) { - case 0: - // Try the next pattern. - case 1: - // We found it. - return matches[0], nil - default: - return "", fmt.Errorf("more than one match found for %q: %s", path, matches) - } - } - return "", fmt.Errorf("file %q not found", path) -} |