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 }