1// Copyright 2019 The SwiftShader Authors. All Rights Reserved. 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 deqp provides functions for running dEQP, as well as loading and storing the results. 16package deqp 17 18import ( 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io/ioutil" 23 "log" 24 "math/rand" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "regexp" 29 "strconv" 30 "strings" 31 "sync" 32 "time" 33 34 "swiftshader.googlesource.com/SwiftShader/tests/regres/cov" 35 "swiftshader.googlesource.com/SwiftShader/tests/regres/shell" 36 "swiftshader.googlesource.com/SwiftShader/tests/regres/testlist" 37 "swiftshader.googlesource.com/SwiftShader/tests/regres/util" 38) 39 40const dataVersion = 1 41 42var ( 43 // Regular expression to parse the output of a dEQP test. 44 deqpRE = regexp.MustCompile(`(Fail|Pass|NotSupported|CompatibilityWarning|QualityWarning|InternalError) \(([^\)]*)\)`) 45 // Regular expression to parse a test that failed due to UNIMPLEMENTED() 46 unimplementedRE = regexp.MustCompile(`[^\n]*UNIMPLEMENTED:[^\n]*`) 47 // Regular expression to parse a test that failed due to UNSUPPORTED() 48 unsupportedRE = regexp.MustCompile(`[^\n]*UNSUPPORTED:[^\n]*`) 49 // Regular expression to parse a test that failed due to UNREACHABLE() 50 unreachableRE = regexp.MustCompile(`[^\n]*UNREACHABLE:[^\n]*`) 51 // Regular expression to parse a test that failed due to ASSERT() 52 assertRE = regexp.MustCompile(`[^\n]*ASSERT\([^\)]*\)[^\n]*`) 53 // Regular expression to parse a test that failed due to ABORT() 54 abortRE = regexp.MustCompile(`[^\n]*ABORT:[^\n]*`) 55 // Regular expression to parse individual test names and output 56 caseOutputRE = regexp.MustCompile("Test case '([^']*)'..") 57) 58 59// Config contains the inputs required for running dEQP on a group of test lists. 60type Config struct { 61 ExeEgl string 62 ExeGles2 string 63 ExeGles3 string 64 ExeVulkan string 65 TempDir string // Directory for temporary log files, coverage output. 66 TestLists testlist.Lists 67 Env []string 68 LogReplacements map[string]string 69 NumParallelTests int 70 MaxTestsPerProc int 71 CoverageEnv *cov.Env 72 TestTimeout time.Duration 73 ValidationLayer bool 74} 75 76// Results holds the results of tests across all APIs. 77// The Results structure may be serialized to cache results. 78type Results struct { 79 Version int 80 Error string 81 Tests map[string]TestResult 82 Coverage *cov.Tree 83 Duration time.Duration 84} 85 86// TestResult holds the results of a single dEQP test. 87type TestResult struct { 88 Test string 89 Status testlist.Status 90 TimeTaken time.Duration 91 Err string `json:",omitempty"` 92 Coverage *cov.Coverage 93} 94 95func (r TestResult) String() string { 96 if r.Err != "" { 97 return fmt.Sprintf("%s: %s (%s)", r.Test, r.Status, r.Err) 98 } 99 return fmt.Sprintf("%s: %s", r.Test, r.Status) 100} 101 102// LoadResults loads cached test results from disk. 103func LoadResults(path string) (*Results, error) { 104 f, err := os.Open(path) 105 if err != nil { 106 return nil, fmt.Errorf("failed to open '%s' for loading test results: %w", path, err) 107 } 108 defer f.Close() 109 110 var out Results 111 if err := json.NewDecoder(f).Decode(&out); err != nil { 112 return nil, err 113 } 114 if out.Version != dataVersion { 115 return nil, errors.New("Data is from an old version") 116 } 117 return &out, nil 118} 119 120// Save saves (caches) test results to disk. 121func (r *Results) Save(path string) error { 122 if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { 123 return fmt.Errorf("failed to make '%s' for saving test results: %w", filepath.Dir(path), err) 124 } 125 126 f, err := os.Create(path) 127 if err != nil { 128 return fmt.Errorf("failed to open '%s' for saving test results: %w", path, err) 129 } 130 defer f.Close() 131 132 enc := json.NewEncoder(f) 133 enc.SetIndent("", " ") 134 if err := enc.Encode(r); err != nil { 135 return fmt.Errorf("failed to encode test results: %w", err) 136 } 137 138 return nil 139} 140 141// Run runs all the tests. 142func (c *Config) Run() (*Results, error) { 143 start := time.Now() 144 145 if c.TempDir == "" { 146 dir, err := ioutil.TempDir("", "deqp") 147 if err != nil { 148 return nil, fmt.Errorf("failed to generate temporary directory: %w", err) 149 } 150 c.TempDir = dir 151 } 152 153 // Wait group that completes once all the tests have finished. 154 wg := sync.WaitGroup{} 155 results := make(chan TestResult, 256) 156 157 numTests := 0 158 159 goroutineIndex := 0 160 161 // For each API that we are testing 162 for _, list := range c.TestLists { 163 // Resolve the test runner 164 exe, supportsCoverage := "", false 165 166 switch list.API { 167 case testlist.EGL: 168 exe = c.ExeEgl 169 case testlist.GLES2: 170 exe = c.ExeGles2 171 case testlist.GLES3: 172 exe = c.ExeGles3 173 case testlist.Vulkan: 174 exe, supportsCoverage = c.ExeVulkan, true 175 default: 176 return nil, fmt.Errorf("Unknown API '%v'", list.API) 177 } 178 if !util.IsFile(exe) { 179 return nil, fmt.Errorf("failed to find dEQP executable at '%s'", exe) 180 } 181 182 // Build a chan for the test names to be run. 183 tests := make(chan string, len(list.Tests)) 184 185 numParallelTests := c.NumParallelTests 186 if list.API != testlist.Vulkan { 187 // OpenGL tests attempt to open lots of X11 display connections, 188 // which may cause us to run out of handles. This maximum was 189 // determined experimentally on a 72-core system. 190 maxParallelGLTests := 16 191 192 if numParallelTests > maxParallelGLTests { 193 numParallelTests = maxParallelGLTests 194 } 195 } 196 197 // Start a number of go routines to run the tests. 198 wg.Add(numParallelTests) 199 for i := 0; i < numParallelTests; i++ { 200 go func(index int) { 201 c.TestRoutine(exe, tests, results, index, supportsCoverage) 202 wg.Done() 203 }(goroutineIndex) 204 goroutineIndex++ 205 } 206 207 // Shuffle the test list. 208 // This attempts to mix heavy-load tests with lighter ones. 209 shuffled := make([]string, len(list.Tests)) 210 for i, j := range rand.New(rand.NewSource(42)).Perm(len(list.Tests)) { 211 shuffled[i] = list.Tests[j] 212 } 213 214 // Hand the tests to the TestRoutines. 215 for _, t := range shuffled { 216 tests <- t 217 } 218 219 // Close the tests chan to indicate that there are no more tests to run. 220 // The TestRoutine functions will return once all tests have been 221 // run. 222 close(tests) 223 224 numTests += len(list.Tests) 225 } 226 227 out := Results{ 228 Version: dataVersion, 229 Tests: map[string]TestResult{}, 230 } 231 232 if c.CoverageEnv != nil { 233 out.Coverage = &cov.Tree{} 234 out.Coverage.Add(cov.Path{}, c.CoverageEnv.AllSourceFiles()) 235 } 236 237 // Collect the results. 238 finished := make(chan struct{}) 239 lastUpdate := time.Now() 240 go func() { 241 start, i := time.Now(), 0 242 for r := range results { 243 i++ 244 if time.Since(lastUpdate) > time.Minute { 245 lastUpdate = time.Now() 246 remaining := numTests - i 247 log.Printf("Ran %d/%d tests (%v%%). Estimated completion in %v.\n", 248 i, numTests, util.Percent(i, numTests), 249 (time.Since(start)/time.Duration(i))*time.Duration(remaining)) 250 } 251 out.Tests[r.Test] = r 252 if r.Coverage != nil { 253 path := strings.Split(r.Test, ".") 254 out.Coverage.Add(cov.Path(path), r.Coverage) 255 r.Coverage = nil // Free memory 256 } 257 } 258 close(finished) 259 }() 260 261 wg.Wait() // Block until all the deqpTestRoutines have finished. 262 close(results) // Signal no more results. 263 <-finished // And wait for the result collecting go-routine to finish. 264 265 out.Duration = time.Since(start) 266 267 return &out, nil 268} 269 270// TestRoutine repeatedly runs the dEQP test executable exe with the tests 271// taken from tests. The output of the dEQP test is parsed, and the test result 272// is written to results. 273// TestRoutine only returns once the tests chan has been closed. 274// TestRoutine does not close the results chan. 275func (c *Config) TestRoutine(exe string, tests <-chan string, results chan<- TestResult, goroutineIndex int, supportsCoverage bool) { 276 // Context for the GCOV_PREFIX environment variable: 277 // If you compile SwiftShader with gcc and the --coverage flag, the build will contain coverage instrumentation. 278 // We can use this to get the code coverage of SwiftShader from running dEQP. 279 // The coverage instrumentation reads the existing coverage files on start-up (at a hardcoded path alongside the 280 // SwiftShader build), updates coverage info as the programs runs, then (over)writes the coverage files on exit. 281 // Thus, multiple parallel processes will race when updating coverage information. The GCOV_PREFIX environment 282 // variable adds a prefix to the hardcoded paths. 283 // E.g. Given GCOV_PREFIX=/tmp/coverage, the hardcoded path /ss/build/a.gcno becomes /tmp/coverage/ss/build/a.gcno. 284 // This is mainly intended for running the target program on a different machine where the hardcoded paths don't 285 // make sense. It can also be used to avoid races. It would be trivial to avoid races if the GCOV_PREFIX variable 286 // supported macro variables like the Clang code coverage "%p" variable that expands to the process ID; in this 287 // case, we could use GCOV_PREFIX=/tmp/coverage/%p to avoid races. Unfortunately, gcc does not support this. 288 // Furthermore, processing coverage information from many directories can be slow; we start a lot of dEQP child 289 // processes, each of which will likely get a unique process ID. In practice, we only need one directory per go 290 // routine. 291 292 // If GCOV_PREFIX is in Env, replace occurrences of "PROC_ID" in GCOV_PREFIX with goroutineIndex. 293 // This avoids races between parallel child processes reading and writing coverage output files. 294 // For example, GCOV_PREFIX="/tmp/gcov_output/PROC_ID" becomes GCOV_PREFIX="/tmp/gcov_output/1" in the first go routine. 295 // You might expect PROC_ID to be the process ID of some process, but the only real requirement is that 296 // it is a unique ID between the *parallel* child processes. 297 env := make([]string, 0, len(c.Env)) 298 for _, v := range c.Env { 299 if strings.HasPrefix(v, "GCOV_PREFIX=") { 300 v = strings.ReplaceAll(v, "PROC_ID", strconv.Itoa(goroutineIndex)) 301 } 302 env = append(env, v) 303 } 304 305 coverageFile := filepath.Join(c.TempDir, fmt.Sprintf("%v.profraw", goroutineIndex)) 306 if supportsCoverage { 307 if c.CoverageEnv != nil { 308 env = cov.AppendRuntimeEnv(env, coverageFile) 309 } 310 } 311 312 logPath := "/dev/null" // TODO(bclayton): Try "nul" on windows. 313 if !util.IsFile(logPath) { 314 logPath = filepath.Join(c.TempDir, fmt.Sprintf("%v.log", goroutineIndex)) 315 } 316 317 testNames := []string{} 318 for name := range tests { 319 testNames = append(testNames, name) 320 if len(testNames) >= c.MaxTestsPerProc { 321 c.PerformTests(exe, env, coverageFile, logPath, testNames, supportsCoverage, results) 322 // Clear list of test names 323 testNames = testNames[:0] 324 } 325 } 326 if len(testNames) > 0 { 327 c.PerformTests(exe, env, coverageFile, logPath, testNames, supportsCoverage, results) 328 } 329} 330 331func (c *Config) PerformTests(exe string, env []string, coverageFile string, logPath string, testNames []string, supportsCoverage bool, results chan<- TestResult) { 332 // log.Printf("Running test(s) '%s'\n", testNames) 333 334 start := time.Now() 335 // Set validation layer according to flag. 336 validation := "disable" 337 if c.ValidationLayer { 338 validation = "enable" 339 } 340 341 // The list of test names will be passed to stdin, since the deqp-stdin-caselist option is used 342 stdin := strings.Join(testNames, "\n") + "\n" 343 344 numTests := len(testNames) 345 timeout := c.TestTimeout * time.Duration(numTests) 346 outRaw, deqpErr := shell.Exec(timeout, exe, filepath.Dir(exe), env, stdin, 347 "--deqp-validation="+validation, 348 "--deqp-surface-type=pbuffer", 349 "--deqp-shadercache=disable", 350 "--deqp-log-images=disable", 351 "--deqp-log-shader-sources=disable", 352 "--deqp-log-decompiled-spirv=disable", 353 "--deqp-log-empty-loginfo=disable", 354 "--deqp-log-flush=disable", 355 "--deqp-log-filename="+logPath, 356 "--deqp-stdin-caselist") 357 duration := time.Since(start) 358 out := string(outRaw) 359 out = strings.ReplaceAll(out, exe, "<dEQP>") 360 for k, v := range c.LogReplacements { 361 out = strings.ReplaceAll(out, k, v) 362 } 363 364 var coverage *cov.Coverage 365 if c.CoverageEnv != nil && supportsCoverage { 366 var covErr error 367 coverage, covErr = c.CoverageEnv.Import(coverageFile) 368 if covErr != nil { 369 log.Printf("Warning: Failed to process test coverage for test '%v'. %v", testNames, covErr) 370 } 371 os.Remove(coverageFile) 372 } 373 374 if numTests > 1 { 375 // Separate output per test case 376 caseOutputs := caseOutputRE.Split(out, -1) 377 378 // If the output isn't as expected, a crash may have happened 379 isCrash := (len(caseOutputs) != (numTests + 1)) 380 381 // Verify the exit code to see if a crash has happened 382 var exitErr *exec.ExitError 383 if errors.As(deqpErr, &exitErr) { 384 if exitErr.ExitCode() == 255 { 385 isCrash = true 386 } 387 } 388 389 // If a crash has happened, re-run tests separately 390 if isCrash { 391 // Re-run tests one by one 392 for _, testName := range testNames { 393 singleTest := []string{testName} 394 c.PerformTests(exe, env, coverageFile, logPath, singleTest, supportsCoverage, results) 395 } 396 } else { 397 caseOutputs = caseOutputs[1:] // Ignore text up to first "Test case '...'" 398 caseNameMatches := caseOutputRE.FindAllStringSubmatch(out, -1) 399 caseNames := make([]string, len(caseNameMatches)) 400 for i, m := range caseNameMatches { 401 caseNames[i] = m[1] 402 } 403 404 averageDuration := duration / time.Duration(numTests) 405 for i, caseOutput := range caseOutputs { 406 results <- c.AnalyzeOutput(caseNames[i], caseOutput, averageDuration, coverage, deqpErr) 407 } 408 } 409 } else { 410 results <- c.AnalyzeOutput(testNames[0], out, duration, coverage, deqpErr) 411 } 412} 413 414func (c *Config) AnalyzeOutput(name string, out string, duration time.Duration, coverage *cov.Coverage, err error) TestResult { 415 for _, test := range []struct { 416 re *regexp.Regexp 417 s testlist.Status 418 }{ 419 {unimplementedRE, testlist.Unimplemented}, 420 {unsupportedRE, testlist.Unsupported}, 421 {unreachableRE, testlist.Unreachable}, 422 {assertRE, testlist.Assert}, 423 {abortRE, testlist.Abort}, 424 } { 425 if s := test.re.FindString(out); s != "" { 426 return TestResult{ 427 Test: name, 428 Status: test.s, 429 TimeTaken: duration, 430 Err: s, 431 Coverage: coverage, 432 } 433 } 434 } 435 436 // Don't treat non-zero error codes as crashes. 437 var exitErr *exec.ExitError 438 if errors.As(err, &exitErr) { 439 if exitErr.ExitCode() != 255 { 440 out += fmt.Sprintf("\nProcess terminated with code %d", exitErr.ExitCode()) 441 err = nil 442 } 443 } 444 445 switch err.(type) { 446 default: 447 return TestResult{ 448 Test: name, 449 Status: testlist.Crash, 450 TimeTaken: duration, 451 Err: out, 452 Coverage: coverage, 453 } 454 case shell.ErrTimeout: 455 log.Printf("Timeout for test '%v'\n", name) 456 return TestResult{ 457 Test: name, 458 Status: testlist.Timeout, 459 TimeTaken: duration, 460 Coverage: coverage, 461 } 462 case nil: 463 toks := deqpRE.FindStringSubmatch(out) 464 if len(toks) < 3 { 465 err := fmt.Sprintf("Couldn't parse test '%v' output:\n%s", name, out) 466 log.Println("Warning: ", err) 467 return TestResult{Test: name, Status: testlist.Fail, Err: err, Coverage: coverage} 468 } 469 switch toks[1] { 470 case "Pass": 471 return TestResult{Test: name, Status: testlist.Pass, TimeTaken: duration, Coverage: coverage} 472 case "NotSupported": 473 return TestResult{Test: name, Status: testlist.NotSupported, TimeTaken: duration, Coverage: coverage} 474 case "CompatibilityWarning": 475 return TestResult{Test: name, Status: testlist.CompatibilityWarning, TimeTaken: duration, Coverage: coverage} 476 case "QualityWarning": 477 return TestResult{Test: name, Status: testlist.QualityWarning, TimeTaken: duration, Coverage: coverage} 478 case "Fail": 479 var err string 480 if toks[2] != "Fail" { 481 err = toks[2] 482 } 483 return TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage} 484 case "InternalError": 485 var err string 486 if toks[2] != "InternalError" { 487 err = toks[2] 488 } 489 return TestResult{Test: name, Status: testlist.InternalError, Err: err, TimeTaken: duration, Coverage: coverage} 490 default: 491 err := fmt.Sprintf("Couldn't parse test output:\n%s", out) 492 log.Println("Warning: ", err) 493 return TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage} 494 } 495 } 496} 497