gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/testutil/testutil.go (about)

     1  // Copyright 2018 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package testutil contains utility functions for runsc tests.
    16  package testutil
    17  
    18  import (
    19  	"bufio"
    20  	"context"
    21  	"debug/elf"
    22  	"encoding/base32"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"log"
    28  	"math"
    29  	"math/rand"
    30  	"net/http"
    31  	"os"
    32  	"os/exec"
    33  	"os/signal"
    34  	"path"
    35  	"path/filepath"
    36  	"strconv"
    37  	"strings"
    38  	"testing"
    39  	"time"
    40  
    41  	"github.com/cenkalti/backoff"
    42  	specs "github.com/opencontainers/runtime-spec/specs-go"
    43  	"golang.org/x/sys/unix"
    44  	"gvisor.dev/gvisor/pkg/sentry/watchdog"
    45  	"gvisor.dev/gvisor/pkg/sync"
    46  	"gvisor.dev/gvisor/runsc/config"
    47  	"gvisor.dev/gvisor/runsc/flag"
    48  	"gvisor.dev/gvisor/runsc/specutils"
    49  )
    50  
    51  var (
    52  	partition       = flag.Int("partition", IntFromEnv("PARTITION", 1), "partition number, this is 1-indexed")
    53  	totalPartitions = flag.Int("total_partitions", IntFromEnv("TOTAL_PARTITIONS", 1), "total number of partitions")
    54  	runscPath       = flag.String("runsc", os.Getenv("RUNTIME"), "path to runsc binary")
    55  
    56  	// Flags controlling features for sandbox under test, prefixed with
    57  	// "test-" to avoid potential conflicts with runsc flags.
    58  	checkpointSupported  = flag.Bool("test-checkpoint", BoolFromEnv("TEST_CHECKPOINT", true), "control checkpoint/restore support")
    59  	isRunningWithOverlay = flag.Bool("test-overlay", BoolFromEnv("TEST_OVERLAY", false), "whether test is running with --overlay2")
    60  	isRunningWithNetRaw  = flag.Bool("test-net-raw", BoolFromEnv("TEST_NET_RAW", false), "whether test is running with raw socket support")
    61  	isRunningWithHostNet = flag.Bool("test-hostnet", BoolFromEnv("TEST_HOSTNET", false), "whether test is running with hostnet")
    62  
    63  	// TestEnvSupportsNetAdmin indicates whether a test sandbox can perform
    64  	// all net admin tasks. Note that some test environments cannot perform
    65  	// some tasks despite the presence of CAP_NET_ADMIN.
    66  	TestEnvSupportsNetAdmin = true
    67  )
    68  
    69  // StringFromEnv returns the value of the named environment variable, or `def` if unset/empty.
    70  // It is useful for defining flags where the default value can be specified through the environment.
    71  func StringFromEnv(name, def string) string {
    72  	str := os.Getenv(name)
    73  	if str == "" {
    74  		return def
    75  	}
    76  	return str
    77  }
    78  
    79  // IntFromEnv returns the integer value of the named environment variable, or `def` if unset/empty.
    80  // It is useful for defining flags where the default value can be specified through the environment.
    81  func IntFromEnv(name string, def int) int {
    82  	str := os.Getenv(name)
    83  	if str == "" {
    84  		return def
    85  	}
    86  	v, err := strconv.ParseInt(str, 10, 64)
    87  	if err != nil {
    88  		// N.B. This library is testonly, so a panic here is reasonable.
    89  		panic(fmt.Errorf("invalid environment variable %q; got %q expected integer: %w", name, str, err))
    90  	}
    91  	return int(v)
    92  }
    93  
    94  // BoolFromEnv returns the boolean value of the named environment variable, or `def` if unset/empty.
    95  // It is useful for defining flags where the default value can be specified through the environment.
    96  func BoolFromEnv(name string, def bool) bool {
    97  	str := strings.ToLower(os.Getenv(name))
    98  	if str == "" {
    99  		return def
   100  	}
   101  	v, err := strconv.ParseBool(str)
   102  	if err != nil {
   103  		panic(fmt.Errorf("invalid environment variable %q; got %q expected bool: %w", name, str, err))
   104  	}
   105  	return v
   106  }
   107  
   108  // DurationFromEnv returns the duration of the named environment variable, or `def` if unset/empty.
   109  // It is useful for defining flags where the default value can be specified through the environment.
   110  func DurationFromEnv(name string, def time.Duration) time.Duration {
   111  	str := strings.ToLower(os.Getenv(name))
   112  	if str == "" {
   113  		return def
   114  	}
   115  	d, err := time.ParseDuration(str)
   116  	if err != nil {
   117  		panic(fmt.Errorf("invalid environment variable %q; got %q expected duration: %w", name, str, err))
   118  	}
   119  	return d
   120  }
   121  
   122  // IsCheckpointSupported returns the relevant command line flag.
   123  func IsCheckpointSupported() bool {
   124  	return *checkpointSupported
   125  }
   126  
   127  // IsRunningWithHostNet returns the relevant command line flag.
   128  func IsRunningWithHostNet() bool {
   129  	return *isRunningWithHostNet
   130  }
   131  
   132  // IsRunningWithNetRaw returns the relevant command line flag.
   133  func IsRunningWithNetRaw() bool {
   134  	return *isRunningWithNetRaw
   135  }
   136  
   137  // IsRunningWithOverlay returns the relevant command line flag.
   138  func IsRunningWithOverlay() bool {
   139  	return *isRunningWithOverlay
   140  }
   141  
   142  // ImageByName mangles the image name used locally. This depends on the image
   143  // build infrastructure in images/ and tools/vm.
   144  func ImageByName(name string) string {
   145  	return fmt.Sprintf("gvisor.dev/images/%s", name)
   146  }
   147  
   148  // ConfigureExePath configures the executable for runsc in the test environment.
   149  func ConfigureExePath() error {
   150  	if *runscPath == "" {
   151  		path, err := FindFile("runsc/runsc")
   152  		if err != nil {
   153  			return err
   154  		}
   155  		*runscPath = path
   156  	}
   157  	specutils.ExePath = *runscPath
   158  	return nil
   159  }
   160  
   161  // TmpDir returns the absolute path to a writable directory that can be used as
   162  // scratch by the test.
   163  func TmpDir() string {
   164  	if dir, ok := os.LookupEnv("TEST_TMPDIR"); ok {
   165  		return dir
   166  	}
   167  	return "/tmp"
   168  }
   169  
   170  // Logger is a simple logging wrapper.
   171  //
   172  // This is designed to be implemented by *testing.T.
   173  type Logger interface {
   174  	Name() string
   175  	Logf(fmt string, args ...any)
   176  }
   177  
   178  // DefaultLogger logs using the log package.
   179  type DefaultLogger string
   180  
   181  // Name implements Logger.Name.
   182  func (d DefaultLogger) Name() string {
   183  	return string(d)
   184  }
   185  
   186  // Logf implements Logger.Logf.
   187  func (d DefaultLogger) Logf(fmt string, args ...any) {
   188  	log.Printf(fmt, args...)
   189  }
   190  
   191  // multiLogger logs to multiple Loggers.
   192  type multiLogger []Logger
   193  
   194  // Name implements Logger.Name.
   195  func (m multiLogger) Name() string {
   196  	names := make([]string, len(m))
   197  	for i, l := range m {
   198  		names[i] = l.Name()
   199  	}
   200  	return strings.Join(names, "+")
   201  }
   202  
   203  // Logf implements Logger.Logf.
   204  func (m multiLogger) Logf(fmt string, args ...any) {
   205  	for _, l := range m {
   206  		l.Logf(fmt, args...)
   207  	}
   208  }
   209  
   210  // NewMultiLogger returns a new Logger that logs on multiple Loggers.
   211  func NewMultiLogger(loggers ...Logger) Logger {
   212  	return multiLogger(loggers)
   213  }
   214  
   215  // Cmd is a simple wrapper.
   216  type Cmd struct {
   217  	logger Logger
   218  	*exec.Cmd
   219  }
   220  
   221  // CombinedOutput returns the output and logs.
   222  func (c *Cmd) CombinedOutput() ([]byte, error) {
   223  	out, err := c.Cmd.CombinedOutput()
   224  	if len(out) > 0 {
   225  		c.logger.Logf("output: %s", string(out))
   226  	}
   227  	if err != nil {
   228  		c.logger.Logf("error: %v", err)
   229  	}
   230  	return out, err
   231  }
   232  
   233  // Command is a simple wrapper around exec.Command, that logs.
   234  func Command(logger Logger, args ...string) *Cmd {
   235  	logger.Logf("command: %s", strings.Join(args, " "))
   236  	return &Cmd{
   237  		logger: logger,
   238  		Cmd:    exec.Command(args[0], args[1:]...),
   239  	}
   240  }
   241  
   242  // TestConfig returns the default configuration to use in tests. Note that
   243  // 'RootDir' must be set by caller if required.
   244  func TestConfig(t *testing.T) *config.Config {
   245  	logDir := os.TempDir()
   246  	if dir, ok := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR"); ok {
   247  		logDir = dir + "/"
   248  	}
   249  
   250  	testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
   251  	config.RegisterFlags(testFlags)
   252  	conf, err := config.NewFromFlags(testFlags)
   253  	if err != nil {
   254  		t.Fatalf("error loading configuration from flags: %v", err)
   255  	}
   256  	// Change test defaults.
   257  	conf.Debug = true
   258  	conf.DebugLog = path.Join(logDir, "runsc.log."+t.Name()+".%TIMESTAMP%.%COMMAND%.txt")
   259  	conf.LogPackets = true
   260  	conf.Network = config.NetworkNone
   261  	conf.Strace = true
   262  	conf.TestOnlyAllowRunAsCurrentUserWithoutChroot = true
   263  	conf.WatchdogAction = watchdog.Panic
   264  	return conf
   265  }
   266  
   267  // NewSpecWithArgs creates a simple spec with the given args suitable for use
   268  // in tests.
   269  func NewSpecWithArgs(args ...string) *specs.Spec {
   270  	return &specs.Spec{
   271  		// The host filesystem root is the container root.
   272  		Root: &specs.Root{
   273  			Path:     "/",
   274  			Readonly: true,
   275  		},
   276  		Process: &specs.Process{
   277  			Args: args,
   278  			Env: []string{
   279  				"PATH=" + os.Getenv("PATH"),
   280  			},
   281  			Capabilities: specutils.AllCapabilities(),
   282  		},
   283  		Mounts: []specs.Mount{
   284  			// Hide the host /etc to avoid any side-effects.
   285  			// For example, bash reads /etc/passwd and if it is
   286  			// very big, tests can fail by timeout.
   287  			{
   288  				Type:        "tmpfs",
   289  				Destination: "/etc",
   290  			},
   291  			// Root is readonly, but many tests want to write to tmpdir.
   292  			// This creates a writable mount inside the root. Also, when tmpdir points
   293  			// to "/tmp", it makes the actual /tmp to be mounted and not a tmpfs
   294  			// inside the sentry.
   295  			{
   296  				Type:        "bind",
   297  				Destination: TmpDir(),
   298  				Source:      TmpDir(),
   299  			},
   300  		},
   301  		Hostname: "runsc-test-hostname",
   302  	}
   303  }
   304  
   305  // SetupRootDir creates a root directory for containers.
   306  func SetupRootDir() (string, func(), error) {
   307  	rootDir, err := ioutil.TempDir(TmpDir(), "containers")
   308  	if err != nil {
   309  		return "", nil, fmt.Errorf("error creating root dir: %v", err)
   310  	}
   311  	return rootDir, func() { os.RemoveAll(rootDir) }, nil
   312  }
   313  
   314  // SetupContainer creates a bundle and root dir for the container, generates a
   315  // test config, and writes the spec to config.json in the bundle dir.
   316  func SetupContainer(spec *specs.Spec, conf *config.Config) (rootDir, bundleDir string, cleanup func(), err error) {
   317  	rootDir, rootCleanup, err := SetupRootDir()
   318  	if err != nil {
   319  		return "", "", nil, err
   320  	}
   321  	conf.RootDir = rootDir
   322  	bundleDir, bundleCleanup, err := SetupBundleDir(spec)
   323  	if err != nil {
   324  		rootCleanup()
   325  		return "", "", nil, err
   326  	}
   327  	return rootDir, bundleDir, func() {
   328  		bundleCleanup()
   329  		rootCleanup()
   330  	}, err
   331  }
   332  
   333  // SetupBundleDir creates a bundle dir and writes the spec to config.json.
   334  func SetupBundleDir(spec *specs.Spec) (string, func(), error) {
   335  	bundleDir, err := ioutil.TempDir(TmpDir(), "bundle")
   336  	if err != nil {
   337  		return "", nil, fmt.Errorf("error creating bundle dir: %v", err)
   338  	}
   339  	cleanup := func() { os.RemoveAll(bundleDir) }
   340  	if err := writeSpec(bundleDir, spec); err != nil {
   341  		cleanup()
   342  		return "", nil, fmt.Errorf("error writing spec: %v", err)
   343  	}
   344  	return bundleDir, cleanup, nil
   345  }
   346  
   347  // writeSpec writes the spec to disk in the given directory.
   348  func writeSpec(dir string, spec *specs.Spec) error {
   349  	b, err := json.Marshal(spec)
   350  	if err != nil {
   351  		return err
   352  	}
   353  	return ioutil.WriteFile(filepath.Join(dir, "config.json"), b, 0755)
   354  }
   355  
   356  // idRandomSrc is a pseudo random generator used to in RandomID.
   357  var idRandomSrc = rand.New(rand.NewSource(time.Now().UnixNano()))
   358  
   359  // idRandomSrcMtx is the mutex protecting idRandomSrc.Read from being used
   360  // concurrently in different goroutines.
   361  var idRandomSrcMtx sync.Mutex
   362  
   363  // RandomID returns 20 random bytes following the given prefix.
   364  func RandomID(prefix string) string {
   365  	// Read 20 random bytes.
   366  	b := make([]byte, 20)
   367  	// Rand.Read is not safe for concurrent use. Packetimpact tests can be run in
   368  	// parallel now, so we have to protect the Read with a mutex. Otherwise we'll
   369  	// run into name conflicts.
   370  	// https://golang.org/pkg/math/rand/#Rand.Read
   371  	idRandomSrcMtx.Lock()
   372  	// "[Read] always returns len(p) and a nil error." --godoc
   373  	if _, err := idRandomSrc.Read(b); err != nil {
   374  		idRandomSrcMtx.Unlock()
   375  		panic("rand.Read failed: " + err.Error())
   376  	}
   377  	idRandomSrcMtx.Unlock()
   378  	if prefix != "" {
   379  		prefix = prefix + "-"
   380  	}
   381  	return fmt.Sprintf("%s%s", prefix, base32.StdEncoding.EncodeToString(b))
   382  }
   383  
   384  // RandomContainerID generates a random container id for each test.
   385  //
   386  // The container id is used to create an abstract unix domain socket, which
   387  // must be unique. While the container forbids creating two containers with the
   388  // same name, sometimes between test runs the socket does not get cleaned up
   389  // quickly enough, causing container creation to fail.
   390  func RandomContainerID() string {
   391  	return RandomID("test-container")
   392  }
   393  
   394  // Copy copies file from src to dst.
   395  func Copy(src, dst string) error {
   396  	in, err := os.Open(src)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	defer in.Close()
   401  
   402  	st, err := in.Stat()
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, st.Mode().Perm())
   408  	if err != nil {
   409  		return err
   410  	}
   411  	defer out.Close()
   412  
   413  	// Mirror the local user's permissions across all users. This is
   414  	// because as we inject things into the container, the UID/GID will
   415  	// change. Also, the build system may generate artifacts with different
   416  	// modes. At the top-level (volume mapping) we have a big read-only
   417  	// knob that can be applied to prevent modifications.
   418  	//
   419  	// Note that this must be done via a separate Chmod call, otherwise the
   420  	// current process's umask will get in the way.
   421  	var mode os.FileMode
   422  	if st.Mode()&0100 != 0 {
   423  		mode |= 0111
   424  	}
   425  	if st.Mode()&0200 != 0 {
   426  		mode |= 0222
   427  	}
   428  	if st.Mode()&0400 != 0 {
   429  		mode |= 0444
   430  	}
   431  	if err := os.Chmod(dst, mode); err != nil {
   432  		return err
   433  	}
   434  
   435  	_, err = io.Copy(out, in)
   436  	return err
   437  }
   438  
   439  // Poll is a shorthand function to poll for something with given timeout.
   440  func Poll(cb func() error, timeout time.Duration) error {
   441  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   442  	defer cancel()
   443  	return PollContext(ctx, cb)
   444  }
   445  
   446  // PollContext is like Poll, but takes a context instead of a timeout.
   447  func PollContext(ctx context.Context, cb func() error) error {
   448  	b := backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx)
   449  	return backoff.Retry(cb, b)
   450  }
   451  
   452  // WaitForHTTP tries GET requests on a port until the call succeeds or timeout.
   453  func WaitForHTTP(ip string, port int, timeout time.Duration) error {
   454  	cb := func() error {
   455  		c := &http.Client{
   456  			// Calculate timeout to be able to do minimum 5 attempts.
   457  			Timeout: timeout / 5,
   458  		}
   459  		url := fmt.Sprintf("http://%s:%d/", ip, port)
   460  		resp, err := c.Get(url)
   461  		if err != nil {
   462  			log.Printf("Waiting %s: %v", url, err)
   463  			return err
   464  		}
   465  		resp.Body.Close()
   466  		return nil
   467  	}
   468  	return Poll(cb, timeout)
   469  }
   470  
   471  // Reaper reaps child processes.
   472  type Reaper struct {
   473  	// mu protects ch, which will be nil if the reaper is not running.
   474  	mu sync.Mutex
   475  	ch chan os.Signal
   476  }
   477  
   478  // Start starts reaping child processes.
   479  func (r *Reaper) Start() {
   480  	r.mu.Lock()
   481  	defer r.mu.Unlock()
   482  
   483  	if r.ch != nil {
   484  		panic("reaper.Start called on a running reaper")
   485  	}
   486  
   487  	r.ch = make(chan os.Signal, 1)
   488  	signal.Notify(r.ch, unix.SIGCHLD)
   489  
   490  	go func() {
   491  		for {
   492  			r.mu.Lock()
   493  			ch := r.ch
   494  			r.mu.Unlock()
   495  			if ch == nil {
   496  				return
   497  			}
   498  
   499  			_, ok := <-ch
   500  			if !ok {
   501  				// Channel closed.
   502  				return
   503  			}
   504  			for {
   505  				cpid, _ := unix.Wait4(-1, nil, unix.WNOHANG, nil)
   506  				if cpid < 1 {
   507  					break
   508  				}
   509  			}
   510  		}
   511  	}()
   512  }
   513  
   514  // Stop stops reaping child processes.
   515  func (r *Reaper) Stop() {
   516  	r.mu.Lock()
   517  	defer r.mu.Unlock()
   518  
   519  	if r.ch == nil {
   520  		panic("reaper.Stop called on a stopped reaper")
   521  	}
   522  
   523  	signal.Stop(r.ch)
   524  	close(r.ch)
   525  	r.ch = nil
   526  }
   527  
   528  // StartReaper is a helper that starts a new Reaper and returns a function to
   529  // stop it.
   530  func StartReaper() func() {
   531  	r := &Reaper{}
   532  	r.Start()
   533  	return r.Stop
   534  }
   535  
   536  // WaitUntilRead reads from the given reader until the wanted string is found
   537  // or until timeout.
   538  func WaitUntilRead(r io.Reader, want string, timeout time.Duration) error {
   539  	sc := bufio.NewScanner(r)
   540  	// done must be accessed atomically. A value greater than 0 indicates
   541  	// that the read loop can exit.
   542  	doneCh := make(chan bool)
   543  	defer close(doneCh)
   544  	go func() {
   545  		for sc.Scan() {
   546  			t := sc.Text()
   547  			if strings.Contains(t, want) {
   548  				doneCh <- true
   549  				return
   550  			}
   551  			select {
   552  			case <-doneCh:
   553  				return
   554  			default:
   555  			}
   556  		}
   557  		doneCh <- false
   558  	}()
   559  
   560  	select {
   561  	case <-time.After(timeout):
   562  		return fmt.Errorf("timeout waiting to read %q", want)
   563  	case res := <-doneCh:
   564  		if !res {
   565  			return fmt.Errorf("reader closed while waiting to read %q", want)
   566  		}
   567  		return nil
   568  	}
   569  }
   570  
   571  // KillCommand kills the process running cmd unless it hasn't been started. It
   572  // returns an error if it cannot kill the process unless the reason is that the
   573  // process has already exited.
   574  //
   575  // KillCommand will also reap the process.
   576  func KillCommand(cmd *exec.Cmd) error {
   577  	if cmd.Process == nil {
   578  		return nil
   579  	}
   580  	if err := cmd.Process.Kill(); err != nil {
   581  		if !strings.Contains(err.Error(), "process already finished") {
   582  			return fmt.Errorf("failed to kill process %v: %v", cmd, err)
   583  		}
   584  	}
   585  	return cmd.Wait()
   586  }
   587  
   588  // WriteTmpFile writes text to a temporary file, closes the file, and returns
   589  // the name of the file. A cleanup function is also returned.
   590  func WriteTmpFile(pattern, text string) (string, func(), error) {
   591  	file, err := ioutil.TempFile(TmpDir(), pattern)
   592  	if err != nil {
   593  		return "", nil, err
   594  	}
   595  	defer file.Close()
   596  	if _, err := file.Write([]byte(text)); err != nil {
   597  		return "", nil, err
   598  	}
   599  	return file.Name(), func() { os.RemoveAll(file.Name()) }, nil
   600  }
   601  
   602  // IsStatic returns true iff the given file is a static binary.
   603  func IsStatic(filename string) (bool, error) {
   604  	f, err := elf.Open(filename)
   605  	if err != nil {
   606  		return false, err
   607  	}
   608  	for _, prog := range f.Progs {
   609  		if prog.Type == elf.PT_INTERP {
   610  			return false, nil // Has interpreter.
   611  		}
   612  	}
   613  	return true, nil
   614  }
   615  
   616  // TouchShardStatusFile indicates to Bazel that the test runner supports
   617  // sharding by creating or updating the last modified date of the file
   618  // specified by TEST_SHARD_STATUS_FILE.
   619  //
   620  // See https://docs.bazel.build/versions/master/test-encyclopedia.html#role-of-the-test-runner.
   621  func TouchShardStatusFile() error {
   622  	if statusFile, ok := os.LookupEnv("TEST_SHARD_STATUS_FILE"); ok {
   623  		cmd := exec.Command("touch", statusFile)
   624  		if b, err := cmd.CombinedOutput(); err != nil {
   625  			return fmt.Errorf("touch %q failed:\n output: %s\n error: %s", statusFile, string(b), err.Error())
   626  		}
   627  	}
   628  	return nil
   629  }
   630  
   631  // TestIndicesForShard returns indices for this test shard based on the
   632  // TEST_SHARD_INDEX and TEST_TOTAL_SHARDS environment vars, as well as
   633  // the passed partition flags.
   634  //
   635  // If either of the env vars are not present, then the function will return all
   636  // tests. If there are more shards than there are tests, then the returned list
   637  // may be empty.
   638  func TestIndicesForShard(numTests int) ([]int, error) {
   639  	var (
   640  		shardIndex = 0
   641  		shardTotal = 1
   642  	)
   643  
   644  	indexStr, indexOk := os.LookupEnv("TEST_SHARD_INDEX")
   645  	totalStr, totalOk := os.LookupEnv("TEST_TOTAL_SHARDS")
   646  	if indexOk && totalOk {
   647  		// Parse index and total to ints.
   648  		var err error
   649  		shardIndex, err = strconv.Atoi(indexStr)
   650  		if err != nil {
   651  			return nil, fmt.Errorf("invalid TEST_SHARD_INDEX %q: %v", indexStr, err)
   652  		}
   653  		shardTotal, err = strconv.Atoi(totalStr)
   654  		if err != nil {
   655  			return nil, fmt.Errorf("invalid TEST_TOTAL_SHARDS %q: %v", totalStr, err)
   656  		}
   657  	}
   658  
   659  	// Combine with the partitions.
   660  	partitionSize := shardTotal
   661  	shardTotal = (*totalPartitions) * shardTotal
   662  	shardIndex = partitionSize*(*partition-1) + shardIndex
   663  
   664  	// Calculate!
   665  	var indices []int
   666  	numBlocks := int(math.Ceil(float64(numTests) / float64(shardTotal)))
   667  	for i := 0; i < numBlocks; i++ {
   668  		pick := i*shardTotal + shardIndex
   669  		if pick < numTests {
   670  			indices = append(indices, pick)
   671  		}
   672  	}
   673  	return indices, nil
   674  }