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