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