summaryrefslogtreecommitdiffhomepage
path: root/pkg/test
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/test')
-rw-r--r--pkg/test/criutil/BUILD14
-rw-r--r--pkg/test/criutil/criutil.go306
-rw-r--r--pkg/test/dockerutil/BUILD14
-rw-r--r--pkg/test/dockerutil/dockerutil.go584
-rw-r--r--pkg/test/testutil/BUILD20
-rw-r--r--pkg/test/testutil/testutil.go518
-rw-r--r--pkg/test/testutil/testutil_runfiles.go75
7 files changed, 1531 insertions, 0 deletions
diff --git a/pkg/test/criutil/BUILD b/pkg/test/criutil/BUILD
new file mode 100644
index 000000000..a7b082cee
--- /dev/null
+++ b/pkg/test/criutil/BUILD
@@ -0,0 +1,14 @@
+load("//tools:defs.bzl", "go_library")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "criutil",
+ testonly = 1,
+ srcs = ["criutil.go"],
+ visibility = ["//:sandbox"],
+ deps = [
+ "//pkg/test/dockerutil",
+ "//pkg/test/testutil",
+ ],
+)
diff --git a/pkg/test/criutil/criutil.go b/pkg/test/criutil/criutil.go
new file mode 100644
index 000000000..bebebb48e
--- /dev/null
+++ b/pkg/test/criutil/criutil.go
@@ -0,0 +1,306 @@
+// 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 criutil contains utility functions for interacting with the
+// Container Runtime Interface (CRI), principally via the crictl command line
+// tool. This requires critools to be installed on the local system.
+package criutil
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "gvisor.dev/gvisor/pkg/test/dockerutil"
+ "gvisor.dev/gvisor/pkg/test/testutil"
+)
+
+// Crictl contains information required to run the crictl utility.
+type Crictl struct {
+ logger testutil.Logger
+ endpoint string
+ cleanup []func()
+}
+
+// resolvePath attempts to find binary paths. It may set the path to invalid,
+// which will cause the execution to fail with a sensible error.
+func resolvePath(executable string) string {
+ guess, err := exec.LookPath(executable)
+ if err != nil {
+ guess = fmt.Sprintf("/usr/local/bin/%s", executable)
+ }
+ return guess
+}
+
+// NewCrictl returns a Crictl configured with a timeout and an endpoint over
+// which it will talk to containerd.
+func NewCrictl(logger testutil.Logger, endpoint string) *Crictl {
+ // Attempt to find the executable, but don't bother propagating the
+ // error at this point. The first command executed will return with a
+ // binary not found error.
+ return &Crictl{
+ logger: logger,
+ endpoint: endpoint,
+ }
+}
+
+// CleanUp executes cleanup functions.
+func (cc *Crictl) CleanUp() {
+ for _, c := range cc.cleanup {
+ c()
+ }
+ cc.cleanup = nil
+}
+
+// RunPod creates a sandbox. It corresponds to `crictl runp`.
+func (cc *Crictl) RunPod(sbSpecFile string) (string, error) {
+ podID, err := cc.run("runp", sbSpecFile)
+ if err != nil {
+ return "", fmt.Errorf("runp failed: %v", err)
+ }
+ // Strip the trailing newline from crictl output.
+ return strings.TrimSpace(podID), nil
+}
+
+// Create creates a container within a sandbox. It corresponds to `crictl
+// create`.
+func (cc *Crictl) Create(podID, contSpecFile, sbSpecFile string) (string, error) {
+ podID, err := cc.run("create", podID, contSpecFile, sbSpecFile)
+ if err != nil {
+ return "", fmt.Errorf("create failed: %v", err)
+ }
+ // Strip the trailing newline from crictl output.
+ return strings.TrimSpace(podID), nil
+}
+
+// Start starts a container. It corresponds to `crictl start`.
+func (cc *Crictl) Start(contID string) (string, error) {
+ output, err := cc.run("start", contID)
+ if err != nil {
+ return "", fmt.Errorf("start failed: %v", err)
+ }
+ return output, nil
+}
+
+// Stop stops a container. It corresponds to `crictl stop`.
+func (cc *Crictl) Stop(contID string) error {
+ _, err := cc.run("stop", contID)
+ return err
+}
+
+// Exec execs a program inside a container. It corresponds to `crictl exec`.
+func (cc *Crictl) Exec(contID string, args ...string) (string, error) {
+ a := []string{"exec", contID}
+ a = append(a, args...)
+ output, err := cc.run(a...)
+ if err != nil {
+ return "", fmt.Errorf("exec failed: %v", err)
+ }
+ return output, nil
+}
+
+// Rm removes a container. It corresponds to `crictl rm`.
+func (cc *Crictl) Rm(contID string) error {
+ _, err := cc.run("rm", contID)
+ return err
+}
+
+// StopPod stops a pod. It corresponds to `crictl stopp`.
+func (cc *Crictl) StopPod(podID string) error {
+ _, err := cc.run("stopp", podID)
+ return err
+}
+
+// containsConfig is a minimal copy of
+// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/apis/cri/runtime/v1alpha2/api.proto
+// It only contains fields needed for testing.
+type containerConfig struct {
+ Status containerStatus
+}
+
+type containerStatus struct {
+ Network containerNetwork
+}
+
+type containerNetwork struct {
+ IP string
+}
+
+// PodIP returns a pod's IP address.
+func (cc *Crictl) PodIP(podID string) (string, error) {
+ output, err := cc.run("inspectp", podID)
+ if err != nil {
+ return "", err
+ }
+ conf := &containerConfig{}
+ if err := json.Unmarshal([]byte(output), conf); err != nil {
+ return "", fmt.Errorf("failed to unmarshal JSON: %v, %s", err, output)
+ }
+ if conf.Status.Network.IP == "" {
+ return "", fmt.Errorf("no IP found in config: %s", output)
+ }
+ return conf.Status.Network.IP, nil
+}
+
+// RmPod removes a container. It corresponds to `crictl rmp`.
+func (cc *Crictl) RmPod(podID string) error {
+ _, err := cc.run("rmp", podID)
+ return err
+}
+
+// Import imports the given container from the local Docker instance.
+func (cc *Crictl) Import(image string) error {
+ // Note that we provide a 10 minute timeout after connect because we may
+ // be pushing a lot of bytes in order to import the image. The connect
+ // timeout stays the same and is inherited from the Crictl instance.
+ cmd := testutil.Command(cc.logger,
+ resolvePath("ctr"),
+ fmt.Sprintf("--connect-timeout=%s", 30*time.Second),
+ fmt.Sprintf("--address=%s", cc.endpoint),
+ "-n", "k8s.io", "images", "import", "-")
+ cmd.Stderr = os.Stderr // Pass through errors.
+
+ // Create a pipe and start the program.
+ w, err := cmd.StdinPipe()
+ if err != nil {
+ return err
+ }
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+
+ // Save the image on the other end.
+ if err := dockerutil.Save(cc.logger, image, w); err != nil {
+ cmd.Wait()
+ return err
+ }
+
+ // Close our pipe reference & see if it was loaded.
+ if err := w.Close(); err != nil {
+ return w.Close()
+ }
+
+ return cmd.Wait()
+}
+
+// StartContainer pulls the given image ands starts the container in the
+// sandbox with the given podID.
+//
+// Note that the image will always be imported from the local docker daemon.
+func (cc *Crictl) StartContainer(podID, image, sbSpec, contSpec string) (string, error) {
+ if err := cc.Import(image); err != nil {
+ return "", err
+ }
+
+ // Write the specs to files that can be read by crictl.
+ sbSpecFile, cleanup, err := testutil.WriteTmpFile("sbSpec", sbSpec)
+ if err != nil {
+ return "", fmt.Errorf("failed to write sandbox spec: %v", err)
+ }
+ cc.cleanup = append(cc.cleanup, cleanup)
+ contSpecFile, cleanup, err := testutil.WriteTmpFile("contSpec", contSpec)
+ if err != nil {
+ return "", fmt.Errorf("failed to write container spec: %v", err)
+ }
+ cc.cleanup = append(cc.cleanup, cleanup)
+
+ return cc.startContainer(podID, image, sbSpecFile, contSpecFile)
+}
+
+func (cc *Crictl) startContainer(podID, image, sbSpecFile, contSpecFile string) (string, error) {
+ contID, err := cc.Create(podID, contSpecFile, sbSpecFile)
+ if err != nil {
+ return "", fmt.Errorf("failed to create container in pod %q: %v", podID, err)
+ }
+
+ if _, err := cc.Start(contID); err != nil {
+ return "", fmt.Errorf("failed to start container %q in pod %q: %v", contID, podID, err)
+ }
+
+ return contID, nil
+}
+
+// StopContainer stops and deletes the container with the given container ID.
+func (cc *Crictl) StopContainer(contID string) error {
+ if err := cc.Stop(contID); err != nil {
+ return fmt.Errorf("failed to stop container %q: %v", contID, err)
+ }
+
+ if err := cc.Rm(contID); err != nil {
+ return fmt.Errorf("failed to remove container %q: %v", contID, err)
+ }
+
+ return nil
+}
+
+// StartPodAndContainer starts a sandbox and container in that sandbox. It
+// returns the pod ID and container ID.
+func (cc *Crictl) StartPodAndContainer(image, sbSpec, contSpec string) (string, string, error) {
+ if err := cc.Import(image); err != nil {
+ return "", "", err
+ }
+
+ // Write the specs to files that can be read by crictl.
+ sbSpecFile, cleanup, err := testutil.WriteTmpFile("sbSpec", sbSpec)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to write sandbox spec: %v", err)
+ }
+ cc.cleanup = append(cc.cleanup, cleanup)
+ contSpecFile, cleanup, err := testutil.WriteTmpFile("contSpec", contSpec)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to write container spec: %v", err)
+ }
+ cc.cleanup = append(cc.cleanup, cleanup)
+
+ podID, err := cc.RunPod(sbSpecFile)
+ if err != nil {
+ return "", "", err
+ }
+
+ contID, err := cc.startContainer(podID, image, sbSpecFile, contSpecFile)
+
+ return podID, contID, err
+}
+
+// StopPodAndContainer stops a container and pod.
+func (cc *Crictl) StopPodAndContainer(podID, contID string) error {
+ if err := cc.StopContainer(contID); err != nil {
+ return fmt.Errorf("failed to stop container %q in pod %q: %v", contID, podID, err)
+ }
+
+ if err := cc.StopPod(podID); err != nil {
+ return fmt.Errorf("failed to stop pod %q: %v", podID, err)
+ }
+
+ if err := cc.RmPod(podID); err != nil {
+ return fmt.Errorf("failed to remove pod %q: %v", podID, err)
+ }
+
+ return nil
+}
+
+// run runs crictl with the given args.
+func (cc *Crictl) run(args ...string) (string, error) {
+ defaultArgs := []string{
+ resolvePath("crictl"),
+ "--image-endpoint", fmt.Sprintf("unix://%s", cc.endpoint),
+ "--runtime-endpoint", fmt.Sprintf("unix://%s", cc.endpoint),
+ }
+ fullArgs := append(defaultArgs, args...)
+ out, err := testutil.Command(cc.logger, fullArgs...).CombinedOutput()
+ return string(out), err
+}
diff --git a/pkg/test/dockerutil/BUILD b/pkg/test/dockerutil/BUILD
new file mode 100644
index 000000000..7c8758e35
--- /dev/null
+++ b/pkg/test/dockerutil/BUILD
@@ -0,0 +1,14 @@
+load("//tools:defs.bzl", "go_library")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "dockerutil",
+ testonly = 1,
+ srcs = ["dockerutil.go"],
+ visibility = ["//:sandbox"],
+ deps = [
+ "//pkg/test/testutil",
+ "@com_github_kr_pty//:go_default_library",
+ ],
+)
diff --git a/pkg/test/dockerutil/dockerutil.go b/pkg/test/dockerutil/dockerutil.go
new file mode 100644
index 000000000..5f2af9f3b
--- /dev/null
+++ b/pkg/test/dockerutil/dockerutil.go
@@ -0,0 +1,584 @@
+// 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 dockerutil is a collection of utility functions.
+package dockerutil
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/kr/pty"
+ "gvisor.dev/gvisor/pkg/test/testutil"
+)
+
+var (
+ // runtime is the runtime to use for tests. This will be applied to all
+ // containers. Note that the default here ("runsc") corresponds to the
+ // default used by the installations. This is important, because the
+ // default installer for vm_tests (in tools/installers:head, invoked
+ // via tools/vm:defs.bzl) will install with this name. So without
+ // changing anything, tests should have a runsc runtime available to
+ // them. Otherwise installers should update the existing runtime
+ // instead of installing a new one.
+ runtime = flag.String("runtime", "runsc", "specify which runtime to use")
+
+ // config is the default Docker daemon configuration path.
+ config = flag.String("config_path", "/etc/docker/daemon.json", "configuration file for reading paths")
+)
+
+// EnsureSupportedDockerVersion checks if correct docker is installed.
+//
+// This logs directly to stderr, as it is typically called from a Main wrapper.
+func EnsureSupportedDockerVersion() {
+ cmd := exec.Command("docker", "version")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ log.Fatalf("error running %q: %v", "docker version", err)
+ }
+ re := regexp.MustCompile(`Version:\s+(\d+)\.(\d+)\.\d.*`)
+ matches := re.FindStringSubmatch(string(out))
+ if len(matches) != 3 {
+ log.Fatalf("Invalid docker output: %s", out)
+ }
+ major, _ := strconv.Atoi(matches[1])
+ minor, _ := strconv.Atoi(matches[2])
+ if major < 17 || (major == 17 && minor < 9) {
+ log.Fatalf("Docker version 17.09.0 or greater is required, found: %02d.%02d", major, minor)
+ }
+}
+
+// RuntimePath returns the binary path for the current runtime.
+func RuntimePath() (string, error) {
+ // Read the configuration data; the file must exist.
+ configBytes, err := ioutil.ReadFile(*config)
+ if err != nil {
+ return "", err
+ }
+
+ // Unmarshal the configuration.
+ c := make(map[string]interface{})
+ if err := json.Unmarshal(configBytes, &c); err != nil {
+ return "", err
+ }
+
+ // Decode the expected configuration.
+ r, ok := c["runtimes"]
+ if !ok {
+ return "", fmt.Errorf("no runtimes declared: %v", c)
+ }
+ rs, ok := r.(map[string]interface{})
+ if !ok {
+ // The runtimes are not a map.
+ return "", fmt.Errorf("unexpected format: %v", c)
+ }
+ r, ok = rs[*runtime]
+ if !ok {
+ // The expected runtime is not declared.
+ return "", fmt.Errorf("runtime %q not found: %v", *runtime, c)
+ }
+ rs, ok = r.(map[string]interface{})
+ if !ok {
+ // The runtime is not a map.
+ return "", fmt.Errorf("unexpected format: %v", c)
+ }
+ p, ok := rs["path"].(string)
+ if !ok {
+ // The runtime does not declare a path.
+ return "", fmt.Errorf("unexpected format: %v", c)
+ }
+ return p, nil
+}
+
+// Save exports a container image to the given Writer.
+//
+// Note that the writer should be actively consuming the output, otherwise it
+// is not guaranteed that the Save will make any progress and the call may
+// stall indefinitely.
+//
+// This is called by criutil in order to import imports.
+func Save(logger testutil.Logger, image string, w io.Writer) error {
+ cmd := testutil.Command(logger, "docker", "save", testutil.ImageByName(image))
+ cmd.Stdout = w // Send directly to the writer.
+ return cmd.Run()
+}
+
+// MountMode describes if the mount should be ro or rw.
+type MountMode int
+
+const (
+ // ReadOnly is what the name says.
+ ReadOnly MountMode = iota
+ // ReadWrite is what the name says.
+ ReadWrite
+)
+
+// String returns the mount mode argument for this MountMode.
+func (m MountMode) String() string {
+ switch m {
+ case ReadOnly:
+ return "ro"
+ case ReadWrite:
+ return "rw"
+ }
+ panic(fmt.Sprintf("invalid mode: %d", m))
+}
+
+// Docker contains the name and the runtime of a docker container.
+type Docker struct {
+ logger testutil.Logger
+ Runtime string
+ Name string
+ copyErr error
+ mounts []string
+ cleanups []func()
+}
+
+// MakeDocker sets up the struct for a Docker container.
+//
+// Names of containers will be unique.
+func MakeDocker(logger testutil.Logger) *Docker {
+ return &Docker{
+ logger: logger,
+ Name: testutil.RandomID(logger.Name()),
+ Runtime: *runtime,
+ }
+}
+
+// Mount mounts the given source and makes it available in the container.
+func (d *Docker) Mount(target, source string, mode MountMode) {
+ d.mounts = append(d.mounts, fmt.Sprintf("-v=%s:%s:%v", source, target, mode))
+}
+
+// CopyFiles copies in and mounts the given files. They are always ReadOnly.
+func (d *Docker) CopyFiles(target string, sources ...string) {
+ dir, err := ioutil.TempDir("", d.Name)
+ if err != nil {
+ d.copyErr = fmt.Errorf("ioutil.TempDir failed: %v", err)
+ return
+ }
+ d.cleanups = append(d.cleanups, func() { os.RemoveAll(dir) })
+ if err := os.Chmod(dir, 0755); err != nil {
+ d.copyErr = fmt.Errorf("os.Chmod(%q, 0755) failed: %v", dir, err)
+ return
+ }
+ for _, name := range sources {
+ src, err := testutil.FindFile(name)
+ if err != nil {
+ d.copyErr = fmt.Errorf("testutil.FindFile(%q) failed: %v", name, err)
+ return
+ }
+ dst := path.Join(dir, path.Base(name))
+ if err := testutil.Copy(src, dst); err != nil {
+ d.copyErr = fmt.Errorf("testutil.Copy(%q, %q) failed: %v", src, dst, err)
+ return
+ }
+ d.logger.Logf("copy: %s -> %s", src, dst)
+ }
+ d.Mount(target, dir, ReadOnly)
+}
+
+// Link links the given target.
+func (d *Docker) Link(target string, source *Docker) {
+ d.mounts = append(d.mounts, fmt.Sprintf("--link=%s:%s", source.Name, target))
+}
+
+// RunOpts are options for running a container.
+type RunOpts struct {
+ // Image is the image relative to images/. This will be mangled
+ // appropriately, to ensure that only first-party images are used.
+ Image string
+
+ // Memory is the memory limit in kB.
+ Memory int
+
+ // Ports are the ports to be allocated.
+ Ports []int
+
+ // WorkDir sets the working directory.
+ WorkDir string
+
+ // ReadOnly sets the read-only flag.
+ ReadOnly bool
+
+ // Env are additional environment variables.
+ Env []string
+
+ // User is the user to use.
+ User string
+
+ // Privileged enables privileged mode.
+ Privileged bool
+
+ // CapAdd are the extra set of capabilities to add.
+ CapAdd []string
+
+ // CapDrop are the extra set of capabilities to drop.
+ CapDrop []string
+
+ // Pty indicates that a pty will be allocated. If this is non-nil, then
+ // this will run after start-up with the *exec.Command and Pty file
+ // passed in to the function.
+ Pty func(*exec.Cmd, *os.File)
+
+ // Foreground indicates that the container should be run in the
+ // foreground. If this is true, then the output will be available as a
+ // return value from the Run function.
+ Foreground bool
+
+ // Extra are extra arguments that may be passed.
+ Extra []string
+}
+
+// args returns common arguments.
+//
+// Note that this does not define the complete behavior.
+func (d *Docker) argsFor(r *RunOpts, command string, p []string) (rv []string) {
+ isExec := command == "exec"
+ isRun := command == "run"
+
+ if isRun || isExec {
+ rv = append(rv, "-i")
+ }
+ if r.Pty != nil {
+ rv = append(rv, "-t")
+ }
+ if r.User != "" {
+ rv = append(rv, fmt.Sprintf("--user=%s", r.User))
+ }
+ if r.Privileged {
+ rv = append(rv, "--privileged")
+ }
+ for _, c := range r.CapAdd {
+ rv = append(rv, fmt.Sprintf("--cap-add=%s", c))
+ }
+ for _, c := range r.CapDrop {
+ rv = append(rv, fmt.Sprintf("--cap-drop=%s", c))
+ }
+ for _, e := range r.Env {
+ rv = append(rv, fmt.Sprintf("--env=%s", e))
+ }
+ if r.WorkDir != "" {
+ rv = append(rv, fmt.Sprintf("--workdir=%s", r.WorkDir))
+ }
+ if !isExec {
+ if r.Memory != 0 {
+ rv = append(rv, fmt.Sprintf("--memory=%dk", r.Memory))
+ }
+ for _, p := range r.Ports {
+ rv = append(rv, fmt.Sprintf("--publish=%d", p))
+ }
+ if r.ReadOnly {
+ rv = append(rv, fmt.Sprintf("--read-only"))
+ }
+ if len(p) > 0 {
+ rv = append(rv, "--entrypoint=")
+ }
+ }
+
+ // Always attach the test environment & Extra.
+ rv = append(rv, fmt.Sprintf("--env=RUNSC_TEST_NAME=%s", d.Name))
+ rv = append(rv, r.Extra...)
+
+ // Attach necessary bits.
+ if isExec {
+ rv = append(rv, d.Name)
+ } else {
+ rv = append(rv, d.mounts...)
+ rv = append(rv, fmt.Sprintf("--runtime=%s", d.Runtime))
+ rv = append(rv, fmt.Sprintf("--name=%s", d.Name))
+ rv = append(rv, testutil.ImageByName(r.Image))
+ }
+
+ // Attach other arguments.
+ rv = append(rv, p...)
+ return rv
+}
+
+// run runs a complete command.
+func (d *Docker) run(r RunOpts, command string, p ...string) (string, error) {
+ if d.copyErr != nil {
+ return "", d.copyErr
+ }
+ basicArgs := []string{"docker"}
+ if command == "spawn" {
+ command = "run"
+ basicArgs = append(basicArgs, command)
+ basicArgs = append(basicArgs, "-d")
+ } else {
+ basicArgs = append(basicArgs, command)
+ }
+ customArgs := d.argsFor(&r, command, p)
+ cmd := testutil.Command(d.logger, append(basicArgs, customArgs...)...)
+ if r.Pty != nil {
+ // If allocating a terminal, then we just ignore the output
+ // from the command.
+ ptmx, err := pty.Start(cmd.Cmd)
+ if err != nil {
+ return "", err
+ }
+ defer cmd.Wait() // Best effort.
+ r.Pty(cmd.Cmd, ptmx)
+ } else {
+ // Can't support PTY or streaming.
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+ }
+ return "", nil
+}
+
+// Create calls 'docker create' with the arguments provided.
+func (d *Docker) Create(r RunOpts, args ...string) error {
+ out, err := d.run(r, "create", args...)
+ if strings.Contains(out, "Unable to find image") {
+ return fmt.Errorf("unable to find image, did you remember to `make load-%s`: %w", r.Image, err)
+ }
+ return err
+}
+
+// Start calls 'docker start'.
+func (d *Docker) Start() error {
+ return testutil.Command(d.logger, "docker", "start", d.Name).Run()
+}
+
+// Stop calls 'docker stop'.
+func (d *Docker) Stop() error {
+ return testutil.Command(d.logger, "docker", "stop", d.Name).Run()
+}
+
+// Run calls 'docker run' with the arguments provided.
+func (d *Docker) Run(r RunOpts, args ...string) (string, error) {
+ return d.run(r, "run", args...)
+}
+
+// Spawn starts the container and detaches.
+func (d *Docker) Spawn(r RunOpts, args ...string) error {
+ _, err := d.run(r, "spawn", args...)
+ return err
+}
+
+// Logs calls 'docker logs'.
+func (d *Docker) Logs() (string, error) {
+ // Don't capture the output; since it will swamp the logs.
+ out, err := exec.Command("docker", "logs", d.Name).CombinedOutput()
+ return string(out), err
+}
+
+// Exec calls 'docker exec' with the arguments provided.
+func (d *Docker) Exec(r RunOpts, args ...string) (string, error) {
+ return d.run(r, "exec", args...)
+}
+
+// Pause calls 'docker pause'.
+func (d *Docker) Pause() error {
+ return testutil.Command(d.logger, "docker", "pause", d.Name).Run()
+}
+
+// Unpause calls 'docker pause'.
+func (d *Docker) Unpause() error {
+ return testutil.Command(d.logger, "docker", "unpause", d.Name).Run()
+}
+
+// Checkpoint calls 'docker checkpoint'.
+func (d *Docker) Checkpoint(name string) error {
+ return testutil.Command(d.logger, "docker", "checkpoint", "create", d.Name, name).Run()
+}
+
+// Restore calls 'docker start --checkname [name]'.
+func (d *Docker) Restore(name string) error {
+ return testutil.Command(d.logger, "docker", "start", fmt.Sprintf("--checkpoint=%s", name), d.Name).Run()
+}
+
+// Kill calls 'docker kill'.
+func (d *Docker) Kill() error {
+ // Skip logging this command, it will likely be an error.
+ out, err := exec.Command("docker", "kill", d.Name).CombinedOutput()
+ if err != nil && !strings.Contains(string(out), "is not running") {
+ return err
+ }
+ return nil
+}
+
+// Remove calls 'docker rm'.
+func (d *Docker) Remove() error {
+ return testutil.Command(d.logger, "docker", "rm", d.Name).Run()
+}
+
+// CleanUp kills and deletes the container (best effort).
+func (d *Docker) CleanUp() {
+ // Kill the container.
+ if err := d.Kill(); err != nil {
+ // Just log; can't do anything here.
+ d.logger.Logf("error killing container %q: %v", d.Name, err)
+ }
+ // Remove the image.
+ if err := d.Remove(); err != nil {
+ d.logger.Logf("error removing container %q: %v", d.Name, err)
+ }
+ // Forget all mounts.
+ d.mounts = nil
+ // Execute all cleanups.
+ for _, c := range d.cleanups {
+ c()
+ }
+ d.cleanups = nil
+}
+
+// FindPort returns the host port that is mapped to 'sandboxPort'. This calls
+// docker to allocate a free port in the host and prevent conflicts.
+func (d *Docker) FindPort(sandboxPort int) (int, error) {
+ format := fmt.Sprintf(`{{ (index (index .NetworkSettings.Ports "%d/tcp") 0).HostPort }}`, sandboxPort)
+ out, err := testutil.Command(d.logger, "docker", "inspect", "-f", format, d.Name).CombinedOutput()
+ if err != nil {
+ return -1, fmt.Errorf("error retrieving port: %v", err)
+ }
+ port, err := strconv.Atoi(strings.TrimSuffix(string(out), "\n"))
+ if err != nil {
+ return -1, fmt.Errorf("error parsing port %q: %v", out, err)
+ }
+ return port, nil
+}
+
+// FindIP returns the IP address of the container.
+func (d *Docker) FindIP() (net.IP, error) {
+ const format = `{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}`
+ out, err := testutil.Command(d.logger, "docker", "inspect", "-f", format, d.Name).CombinedOutput()
+ if err != nil {
+ return net.IP{}, fmt.Errorf("error retrieving IP: %v", err)
+ }
+ ip := net.ParseIP(strings.TrimSpace(string(out)))
+ if ip == nil {
+ return net.IP{}, fmt.Errorf("invalid IP: %q", string(out))
+ }
+ return ip, nil
+}
+
+// SandboxPid returns the PID to the sandbox process.
+func (d *Docker) SandboxPid() (int, error) {
+ out, err := testutil.Command(d.logger, "docker", "inspect", "-f={{.State.Pid}}", d.Name).CombinedOutput()
+ if err != nil {
+ return -1, fmt.Errorf("error retrieving pid: %v", err)
+ }
+ pid, err := strconv.Atoi(strings.TrimSuffix(string(out), "\n"))
+ if err != nil {
+ return -1, fmt.Errorf("error parsing pid %q: %v", out, err)
+ }
+ return pid, nil
+}
+
+// ID returns the container ID.
+func (d *Docker) ID() (string, error) {
+ out, err := testutil.Command(d.logger, "docker", "inspect", "-f={{.Id}}", d.Name).CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("error retrieving ID: %v", err)
+ }
+ return strings.TrimSpace(string(out)), nil
+}
+
+// Wait waits for container to exit, up to the given timeout. Returns error if
+// wait fails or timeout is hit. Returns the application return code otherwise.
+// Note that the application may have failed even if err == nil, always check
+// the exit code.
+func (d *Docker) Wait(timeout time.Duration) (syscall.WaitStatus, error) {
+ timeoutChan := time.After(timeout)
+ waitChan := make(chan (syscall.WaitStatus))
+ errChan := make(chan (error))
+
+ go func() {
+ out, err := testutil.Command(d.logger, "docker", "wait", d.Name).CombinedOutput()
+ if err != nil {
+ errChan <- fmt.Errorf("error waiting for container %q: %v", d.Name, err)
+ }
+ exit, err := strconv.Atoi(strings.TrimSuffix(string(out), "\n"))
+ if err != nil {
+ errChan <- fmt.Errorf("error parsing exit code %q: %v", out, err)
+ }
+ waitChan <- syscall.WaitStatus(uint32(exit))
+ }()
+
+ select {
+ case ws := <-waitChan:
+ return ws, nil
+ case err := <-errChan:
+ return syscall.WaitStatus(1), err
+ case <-timeoutChan:
+ return syscall.WaitStatus(1), fmt.Errorf("timeout waiting for container %q", d.Name)
+ }
+}
+
+// WaitForOutput calls 'docker logs' to retrieve containers output and searches
+// for the given pattern.
+func (d *Docker) WaitForOutput(pattern string, timeout time.Duration) (string, error) {
+ matches, err := d.WaitForOutputSubmatch(pattern, timeout)
+ if err != nil {
+ return "", err
+ }
+ if len(matches) == 0 {
+ return "", nil
+ }
+ return matches[0], nil
+}
+
+// WaitForOutputSubmatch calls 'docker logs' to retrieve containers output and
+// searches for the given pattern. It returns any regexp submatches as well.
+func (d *Docker) WaitForOutputSubmatch(pattern string, timeout time.Duration) ([]string, error) {
+ re := regexp.MustCompile(pattern)
+ var (
+ lastOut string
+ stopped bool
+ )
+ for exp := time.Now().Add(timeout); time.Now().Before(exp); {
+ out, err := d.Logs()
+ if err != nil {
+ return nil, err
+ }
+ if out != lastOut {
+ if lastOut == "" {
+ d.logger.Logf("output (start): %s", out)
+ } else if strings.HasPrefix(out, lastOut) {
+ d.logger.Logf("output (contn): %s", out[len(lastOut):])
+ } else {
+ d.logger.Logf("output (trunc): %s", out)
+ }
+ lastOut = out // Save for future.
+ if matches := re.FindStringSubmatch(lastOut); matches != nil {
+ return matches, nil // Success!
+ }
+ } else if stopped {
+ // The sandbox stopped and we looked at the
+ // logs at least once since determining that.
+ return nil, fmt.Errorf("no longer running: %v", err)
+ } else if pid, err := d.SandboxPid(); pid == 0 || err != nil {
+ // The sandbox may have stopped, but it's
+ // possible that it has emitted the terminal
+ // line between the last call to Logs and here.
+ stopped = true
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ return nil, fmt.Errorf("timeout waiting for output %q: %s", re.String(), lastOut)
+}
diff --git a/pkg/test/testutil/BUILD b/pkg/test/testutil/BUILD
new file mode 100644
index 000000000..03b1b4677
--- /dev/null
+++ b/pkg/test/testutil/BUILD
@@ -0,0 +1,20 @@
+load("//tools:defs.bzl", "go_library")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "testutil",
+ testonly = 1,
+ srcs = [
+ "testutil.go",
+ "testutil_runfiles.go",
+ ],
+ visibility = ["//:sandbox"],
+ deps = [
+ "//pkg/sync",
+ "//runsc/boot",
+ "//runsc/specutils",
+ "@com_github_cenkalti_backoff//:go_default_library",
+ "@com_github_opencontainers_runtime-spec//specs-go:go_default_library",
+ ],
+)
diff --git a/pkg/test/testutil/testutil.go b/pkg/test/testutil/testutil.go
new file mode 100644
index 000000000..ee8c78014
--- /dev/null
+++ b/pkg/test/testutil/testutil.go
@@ -0,0 +1,518 @@
+// 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"
+ "sync/atomic"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/cenkalti/backoff"
+ specs "github.com/opencontainers/runtime-spec/specs-go"
+ "gvisor.dev/gvisor/pkg/sync"
+ "gvisor.dev/gvisor/runsc/boot"
+ "gvisor.dev/gvisor/runsc/specutils"
+)
+
+var (
+ checkpoint = flag.Bool("checkpoint", true, "control checkpoint/restore support")
+)
+
+// IsCheckpointSupported returns the relevant command line flag.
+func IsCheckpointSupported() bool {
+ return *checkpoint
+}
+
+// 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 {
+ dir := os.Getenv("TEST_TMPDIR")
+ if dir == "" {
+ dir = "/tmp"
+ }
+ return dir
+}
+
+// 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...)
+}
+
+// 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) *boot.Config {
+ logDir := os.TempDir()
+ if dir, ok := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR"); ok {
+ logDir = dir + "/"
+ }
+ return &boot.Config{
+ Debug: true,
+ DebugLog: path.Join(logDir, "runsc.log."+t.Name()+".%TIMESTAMP%.%COMMAND%"),
+ LogFormat: "text",
+ DebugLogFormat: "text",
+ LogPackets: true,
+ Network: boot.NetworkNone,
+ Strace: true,
+ Platform: "ptrace",
+ FileAccess: boot.FileAccessExclusive,
+ NumNetworkChannels: 1,
+
+ TestOnlyAllowRunAsCurrentUserWithoutChroot: true,
+ }
+}
+
+// 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 *boot.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)
+}
+
+// RandomID returns 20 random bytes following the given prefix.
+func RandomID(prefix string) string {
+ // Read 20 random bytes.
+ b := make([]byte, 20)
+ // "[Read] always returns len(p) and a nil error." --godoc
+ if _, err := rand.Read(b); err != nil {
+ panic("rand.Read failed: " + err.Error())
+ }
+ 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()
+ 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(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://localhost:%d/", 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, split bufio.SplitFunc, timeout time.Duration) error {
+ sc := bufio.NewScanner(r)
+ if split != nil {
+ sc.Split(split)
+ }
+ // done must be accessed atomically. A value greater than 0 indicates
+ // that the read loop can exit.
+ var done uint32
+ doneCh := make(chan struct{})
+ go func() {
+ for sc.Scan() {
+ t := sc.Text()
+ if strings.Contains(t, want) {
+ atomic.StoreUint32(&done, 1)
+ close(doneCh)
+ break
+ }
+ if atomic.LoadUint32(&done) > 0 {
+ break
+ }
+ }
+ }()
+ select {
+ case <-time.After(timeout):
+ atomic.StoreUint32(&done, 1)
+ return fmt.Errorf("timeout waiting to read %q", want)
+ case <-doneCh:
+ 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
+}
+
+// TestIndicesForShard returns indices for this test shard based on the
+// TEST_SHARD_INDEX and TEST_TOTAL_SHARDS environment vars.
+//
+// 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, totalStr := os.Getenv("TEST_SHARD_INDEX"), os.Getenv("TEST_TOTAL_SHARDS")
+ if indexStr != "" && totalStr != "" {
+ // 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)
+ }
+ }
+
+ // 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
new file mode 100644
index 000000000..ece9ea9a1
--- /dev/null
+++ b/pkg/test/testutil/testutil_runfiles.go
@@ -0,0 +1,75 @@
+// 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)
+}