gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/runtimes/runner/lib/lib.go (about)

     1  // Copyright 2019 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 lib provides utilities for runner.
    16  package lib
    17  
    18  import (
    19  	"context"
    20  	"encoding/csv"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"reflect"
    25  	"sort"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"gvisor.dev/gvisor/pkg/log"
    31  	"gvisor.dev/gvisor/pkg/test/dockerutil"
    32  	"gvisor.dev/gvisor/pkg/test/testutil"
    33  )
    34  
    35  // ProctorSettings contains settings passed directly to the proctor process.
    36  type ProctorSettings struct {
    37  	// PerTestTimeout is the timeout for each individual test.
    38  	PerTestTimeout time.Duration
    39  	// RunsPerTest is the number of times to run each test.
    40  	// A value of 0 is the same as a value of 1, i.e. "run once".
    41  	RunsPerTest int
    42  	// If FlakyIsError is true, a flaky test will be considered as a failure.
    43  	// If it is false, a flaky test will be considered as passing.
    44  	FlakyIsError bool
    45  	// If FlakyShortCircuit is true, when runnins with RunsPerTest > 1 and a test is detected as
    46  	// flaky, exit immediately rather than running for all RunsPerTest attempts.
    47  	FlakyShortCircuit bool
    48  }
    49  
    50  // ToArgs converts these settings to command-line arguments to pass to the proctor binary.
    51  func (p ProctorSettings) ToArgs() []string {
    52  	return []string{
    53  		fmt.Sprintf("--per_test_timeout=%v", p.PerTestTimeout),
    54  		fmt.Sprintf("--runs_per_test=%d", p.RunsPerTest),
    55  		fmt.Sprintf("--flaky_is_error=%v", p.FlakyIsError),
    56  		fmt.Sprintf("--flaky_short_circuit=%v", p.FlakyShortCircuit),
    57  	}
    58  }
    59  
    60  // Filter is a predicate function for filtering tests.
    61  // It returns true if the given test name should be run.
    62  type Filter func(test string) bool
    63  
    64  // RunTests is a helper that is called by main. It exists so that we can run
    65  // defered functions before exiting. It returns an exit code that should be
    66  // passed to os.Exit.
    67  func RunTests(lang, image string, filter Filter, batchSize int, timeout time.Duration, proctorSettings ProctorSettings) int {
    68  	// Construct the shared docker instance.
    69  	ctx := context.Background()
    70  	d := dockerutil.MakeContainer(ctx, testutil.DefaultLogger(lang))
    71  	defer d.CleanUp(ctx)
    72  
    73  	if err := testutil.TouchShardStatusFile(); err != nil {
    74  		fmt.Fprintf(os.Stderr, "error touching status shard file: %v\n", err)
    75  		return 1
    76  	}
    77  
    78  	timeoutChan := make(chan struct{})
    79  	// Add one minute to let proctor handle timeout.
    80  	timer := time.AfterFunc(timeout+time.Minute, func() { close(timeoutChan) })
    81  	defer timer.Stop()
    82  	// Get a slice of tests to run. This will also start a single Docker
    83  	// container that will be used to run each test. The final test will
    84  	// stop the Docker container.
    85  	tests, err := getTests(ctx, d, lang, image, batchSize, timeoutChan, timeout, filter, proctorSettings)
    86  	if err != nil {
    87  		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
    88  		return 1
    89  	}
    90  	m := mainStart(tests)
    91  	return m.Run()
    92  }
    93  
    94  // getTests executes all tests as table tests.
    95  func getTests(ctx context.Context, d *dockerutil.Container, lang, image string, batchSize int, timeoutChan chan struct{}, timeout time.Duration, filter Filter, proctorSettings ProctorSettings) ([]testing.InternalTest, error) {
    96  	startTime := time.Now()
    97  
    98  	// Start the container.
    99  	opts := dockerutil.RunOpts{
   100  		Image: fmt.Sprintf("runtimes/%s", image),
   101  	}
   102  	d.CopyFiles(&opts, "/proctor", "test/runtimes/proctor/proctor")
   103  	if err := d.Spawn(ctx, opts, "/proctor/proctor", "--pause"); err != nil {
   104  		return nil, fmt.Errorf("docker run failed: %v", err)
   105  	}
   106  
   107  	done := make(chan struct{})
   108  	go func() {
   109  		select {
   110  		case <-done:
   111  			return
   112  		// Make sure that the useful load takes 2/3 of timeout.
   113  		case <-time.After((timeout - time.Since(startTime)) / 3):
   114  		case <-timeoutChan:
   115  		}
   116  		panic("TIMEOUT: Unable to get a list of tests")
   117  	}()
   118  	// Get a list of all tests in the image.
   119  	list, err := d.Exec(ctx, dockerutil.ExecOpts{Privileged: true, User: "0"}, "/proctor/proctor", "--runtime", lang, "--list")
   120  	if err != nil {
   121  		return nil, fmt.Errorf("docker exec failed: %v", err)
   122  	}
   123  	close(done)
   124  
   125  	// Calculate a subset of tests.
   126  	tests := strings.Fields(list)
   127  	sort.Strings(tests)
   128  	indices, err := testutil.TestIndicesForShard(len(tests))
   129  	if err != nil {
   130  		return nil, fmt.Errorf("TestsForShard() failed: %v", err)
   131  	}
   132  	indicesMap := make(map[int]struct{}, len(indices))
   133  	for _, i := range indices {
   134  		indicesMap[i] = struct{}{}
   135  	}
   136  	var testsNotInShard []string
   137  	for i, tc := range tests {
   138  		if _, found := indicesMap[i]; !found {
   139  			testsNotInShard = append(testsNotInShard, tc)
   140  		}
   141  	}
   142  	if len(testsNotInShard) > 0 {
   143  		log.Infof("Tests not in this shard: %s", strings.Join(testsNotInShard, ","))
   144  	}
   145  
   146  	var itests []testing.InternalTest
   147  	for i := 0; i < len(indices); i += batchSize {
   148  		var tcs []string
   149  		end := i + batchSize
   150  		if end > len(indices) {
   151  			end = len(indices)
   152  		}
   153  		for _, tc := range indices[i:end] {
   154  			// Add test if not filtered.
   155  			if filter != nil && !filter(tests[tc]) {
   156  				log.Infof("Skipping test case %s\n", tests[tc])
   157  				continue
   158  			}
   159  			tcs = append(tcs, tests[tc])
   160  		}
   161  		if len(tcs) == 0 {
   162  			// No tests to add to this batch.
   163  			continue
   164  		}
   165  		itests = append(itests, testing.InternalTest{
   166  			Name: strings.Join(tcs, ", "),
   167  			F: func(t *testing.T) {
   168  				var (
   169  					now    = time.Now()
   170  					done   = make(chan struct{})
   171  					output string
   172  					err    error
   173  				)
   174  
   175  				state, err := d.Status(ctx)
   176  				if err != nil {
   177  					t.Fatalf("Could not find container status: %v", err)
   178  				}
   179  				if !state.Running {
   180  					t.Fatalf("container is not running: state = %s", state.Status)
   181  				}
   182  				log.Infof("Running test case batch: %s", strings.Join(tcs, ","))
   183  
   184  				go func() {
   185  					argv := []string{
   186  						"/proctor/proctor", "--runtime", lang,
   187  						"--tests", strings.Join(tcs, ","),
   188  						fmt.Sprintf("--timeout=%s", timeout-time.Since(startTime)),
   189  					}
   190  					argv = append(argv, proctorSettings.ToArgs()...)
   191  					output, err = d.Exec(ctx, dockerutil.ExecOpts{Privileged: true, User: "0"}, argv...)
   192  					close(done)
   193  				}()
   194  
   195  				select {
   196  				case <-done:
   197  					if err == nil {
   198  						fmt.Printf("PASS: (%v) %d tests passed\n", time.Since(now), len(tcs))
   199  						return
   200  					}
   201  					t.Fatalf("FAIL: (%v):\nBatch:\n%s\nOutput:\n%s\n", time.Since(now), strings.Join(tcs, "\n"), output)
   202  				// Add one minute to let proctor handle timeout.
   203  				case <-timeoutChan:
   204  					t.Fatalf("TIMEOUT: (%v):\nBatch:\n%s\nOutput:\n%s\n", time.Since(now), strings.Join(tcs, "\n"), output)
   205  				}
   206  			},
   207  		})
   208  	}
   209  
   210  	return itests, nil
   211  }
   212  
   213  // ExcludeFilter reads the exclude file and returns a filter that excludes the tests listed in
   214  // the given CSV file.
   215  func ExcludeFilter(excludeFile string) (Filter, error) {
   216  	excludes := make(map[string]struct{})
   217  	if excludeFile == "" {
   218  		return nil, nil
   219  	}
   220  	f, err := os.Open(excludeFile)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	defer f.Close()
   225  
   226  	r := csv.NewReader(f)
   227  
   228  	// First line is header. Skip it.
   229  	if _, err := r.Read(); err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	for {
   234  		record, err := r.Read()
   235  		if err == io.EOF {
   236  			break
   237  		}
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		excludes[record[0]] = struct{}{}
   242  	}
   243  	return func(test string) bool {
   244  		_, found := excludes[test]
   245  		return !found
   246  	}, nil
   247  }
   248  
   249  // testDeps implements testing.testDeps (an unexported interface), and is
   250  // required to use testing.MainStart.
   251  type testDeps struct{}
   252  
   253  func (f testDeps) MatchString(a, b string) (bool, error)       { return a == b, nil }
   254  func (f testDeps) StartCPUProfile(io.Writer) error             { return nil }
   255  func (f testDeps) StopCPUProfile()                             {}
   256  func (f testDeps) WriteProfileTo(string, io.Writer, int) error { return nil }
   257  func (f testDeps) ImportPath() string                          { return "" }
   258  func (f testDeps) StartTestLog(io.Writer)                      {}
   259  func (f testDeps) StopTestLog() error                          { return nil }
   260  func (f testDeps) SetPanicOnExit0(bool)                        {}
   261  func (f testDeps) CoordinateFuzzing(time.Duration, int64, time.Duration, int64, int, []corpusEntry, []reflect.Type, string, string) error {
   262  	return nil
   263  }
   264  func (f testDeps) RunFuzzWorker(func(corpusEntry) error) error              { return nil }
   265  func (f testDeps) ReadCorpus(string, []reflect.Type) ([]corpusEntry, error) { return nil, nil }
   266  func (f testDeps) CheckCorpus([]any, []reflect.Type) error                  { return nil }
   267  func (f testDeps) ResetCoverage()                                           {}
   268  func (f testDeps) SnapshotCoverage()                                        {}
   269  
   270  // Copied from testing/fuzz.go.
   271  type corpusEntry = struct {
   272  	Parent     string
   273  	Path       string
   274  	Data       []byte
   275  	Values     []any
   276  	Generation int
   277  	IsSeed     bool
   278  }