blob: 0afe2777b57b87325c47a72cd765f0afe365a826 [file] [log] [blame]
// Copyright 2019 The SwiftShader Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package deqp provides functions for running dEQP, as well as loading and storing the results.
package deqp
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"../cause"
"../cov"
"../shell"
"../testlist"
"../util"
)
const dataVersion = 1
var (
// Regular expression to parse the output of a dEQP test.
deqpRE = regexp.MustCompile(`(Fail|Pass|NotSupported|CompatibilityWarning|QualityWarning|InternalError) \(([^\)]*)\)`)
// Regular expression to parse a test that failed due to UNIMPLEMENTED()
unimplementedRE = regexp.MustCompile(`[^\n]*UNIMPLEMENTED:[^\n]*`)
// Regular expression to parse a test that failed due to UNSUPPORTED()
unsupportedRE = regexp.MustCompile(`[^\n]*UNSUPPORTED:[^\n]*`)
// Regular expression to parse a test that failed due to UNREACHABLE()
unreachableRE = regexp.MustCompile(`[^\n]*UNREACHABLE:[^\n]*`)
// Regular expression to parse a test that failed due to ASSERT()
assertRE = regexp.MustCompile(`[^\n]*ASSERT\([^\)]*\)[^\n]*`)
// Regular expression to parse a test that failed due to ABORT()
abortRE = regexp.MustCompile(`[^\n]*ABORT:[^\n]*`)
)
// Config contains the inputs required for running dEQP on a group of test lists.
type Config struct {
ExeEgl string
ExeGles2 string
ExeGles3 string
ExeVulkan string
TempDir string // Directory for temporary log files, coverage output.
TestLists testlist.Lists
Env []string
LogReplacements map[string]string
NumParallelTests int
CoverageEnv *cov.Env
TestTimeout time.Duration
ValidationLayer bool
}
// Results holds the results of tests across all APIs.
// The Results structure may be serialized to cache results.
type Results struct {
Version int
Error string
Tests map[string]TestResult
Coverage *cov.Tree
Duration time.Duration
}
// TestResult holds the results of a single dEQP test.
type TestResult struct {
Test string
Status testlist.Status
TimeTaken time.Duration
Err string `json:",omitempty"`
Coverage *cov.Coverage
}
func (r TestResult) String() string {
if r.Err != "" {
return fmt.Sprintf("%s: %s (%s)", r.Test, r.Status, r.Err)
}
return fmt.Sprintf("%s: %s", r.Test, r.Status)
}
// LoadResults loads cached test results from disk.
func LoadResults(path string) (*Results, error) {
f, err := os.Open(path)
if err != nil {
return nil, cause.Wrap(err, "Couldn't open '%s' for loading test results", path)
}
defer f.Close()
var out Results
if err := json.NewDecoder(f).Decode(&out); err != nil {
return nil, err
}
if out.Version != dataVersion {
return nil, errors.New("Data is from an old version")
}
return &out, nil
}
// Save saves (caches) test results to disk.
func (r *Results) Save(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return cause.Wrap(err, "couldn't make '%s' for saving test results", filepath.Dir(path))
}
f, err := os.Create(path)
if err != nil {
return cause.Wrap(err, "Couldn't open '%s' for saving test results", path)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(r); err != nil {
return cause.Wrap(err, "Couldn't encode test results")
}
return nil
}
// Run runs all the tests.
func (c *Config) Run() (*Results, error) {
start := time.Now()
if c.TempDir == "" {
dir, err := ioutil.TempDir("", "deqp")
if err != nil {
return nil, cause.Wrap(err, "Could not generate temporary directory")
}
c.TempDir = dir
}
// Wait group that completes once all the tests have finished.
wg := sync.WaitGroup{}
results := make(chan TestResult, 256)
numTests := 0
goroutineIndex := 0
// For each API that we are testing
for _, list := range c.TestLists {
// Resolve the test runner
exe, supportsCoverage := "", false
switch list.API {
case testlist.EGL:
exe = c.ExeEgl
case testlist.GLES2:
exe = c.ExeGles2
case testlist.GLES3:
exe = c.ExeGles3
case testlist.Vulkan:
exe, supportsCoverage = c.ExeVulkan, true
default:
return nil, fmt.Errorf("Unknown API '%v'", list.API)
}
if !util.IsFile(exe) {
return nil, fmt.Errorf("Couldn't find dEQP executable at '%s'", exe)
}
// Build a chan for the test names to be run.
tests := make(chan string, len(list.Tests))
numParallelTests := c.NumParallelTests
if list.API != testlist.Vulkan {
// OpenGL tests attempt to open lots of X11 display connections,
// which may cause us to run out of handles. This maximum was
// determined experimentally on a 72-core system.
maxParallelGLTests := 16
if numParallelTests > maxParallelGLTests {
numParallelTests = maxParallelGLTests
}
}
// Start a number of go routines to run the tests.
wg.Add(numParallelTests)
for i := 0; i < numParallelTests; i++ {
go func(index int) {
c.TestRoutine(exe, tests, results, index, supportsCoverage)
wg.Done()
}(goroutineIndex)
goroutineIndex++
}
// Shuffle the test list.
// This attempts to mix heavy-load tests with lighter ones.
shuffled := make([]string, len(list.Tests))
for i, j := range rand.New(rand.NewSource(42)).Perm(len(list.Tests)) {
shuffled[i] = list.Tests[j]
}
// Hand the tests to the TestRoutines.
for _, t := range shuffled {
tests <- t
}
// Close the tests chan to indicate that there are no more tests to run.
// The TestRoutine functions will return once all tests have been
// run.
close(tests)
numTests += len(list.Tests)
}
out := Results{
Version: dataVersion,
Tests: map[string]TestResult{},
}
if c.CoverageEnv != nil {
out.Coverage = &cov.Tree{}
out.Coverage.Add(cov.Path{}, c.CoverageEnv.AllSourceFiles())
}
// Collect the results.
finished := make(chan struct{})
lastUpdate := time.Now()
go func() {
start, i := time.Now(), 0
for r := range results {
i++
if time.Since(lastUpdate) > time.Minute {
lastUpdate = time.Now()
remaining := numTests - i
log.Printf("Ran %d/%d tests (%v%%). Estimated completion in %v.\n",
i, numTests, util.Percent(i, numTests),
(time.Since(start)/time.Duration(i))*time.Duration(remaining))
}
out.Tests[r.Test] = r
if r.Coverage != nil {
path := strings.Split(r.Test, ".")
out.Coverage.Add(cov.Path(path), r.Coverage)
r.Coverage = nil // Free memory
}
}
close(finished)
}()
wg.Wait() // Block until all the deqpTestRoutines have finished.
close(results) // Signal no more results.
<-finished // And wait for the result collecting go-routine to finish.
out.Duration = time.Since(start)
return &out, nil
}
// TestRoutine repeatedly runs the dEQP test executable exe with the tests
// taken from tests. The output of the dEQP test is parsed, and the test result
// is written to results.
// TestRoutine only returns once the tests chan has been closed.
// TestRoutine does not close the results chan.
func (c *Config) TestRoutine(exe string, tests <-chan string, results chan<- TestResult, goroutineIndex int, supportsCoverage bool) {
// Context for the GCOV_PREFIX environment variable:
// If you compile SwiftShader with gcc and the --coverage flag, the build will contain coverage instrumentation.
// We can use this to get the code coverage of SwiftShader from running dEQP.
// The coverage instrumentation reads the existing coverage files on start-up (at a hardcoded path alongside the
// SwiftShader build), updates coverage info as the programs runs, then (over)writes the coverage files on exit.
// Thus, multiple parallel processes will race when updating coverage information. The GCOV_PREFIX environment
// variable adds a prefix to the hardcoded paths.
// E.g. Given GCOV_PREFIX=/tmp/coverage, the hardcoded path /ss/build/a.gcno becomes /tmp/coverage/ss/build/a.gcno.
// This is mainly intended for running the target program on a different machine where the hardcoded paths don't
// make sense. It can also be used to avoid races. It would be trivial to avoid races if the GCOV_PREFIX variable
// supported macro variables like the Clang code coverage "%p" variable that expands to the process ID; in this
// case, we could use GCOV_PREFIX=/tmp/coverage/%p to avoid races. Unfortunately, gcc does not support this.
// Furthermore, processing coverage information from many directories can be slow; we start a lot of dEQP child
// processes, each of which will likely get a unique process ID. In practice, we only need one directory per go
// routine.
// If GCOV_PREFIX is in Env, replace occurrences of "PROC_ID" in GCOV_PREFIX with goroutineIndex.
// This avoids races between parallel child processes reading and writing coverage output files.
// For example, GCOV_PREFIX="/tmp/gcov_output/PROC_ID" becomes GCOV_PREFIX="/tmp/gcov_output/1" in the first go routine.
// You might expect PROC_ID to be the process ID of some process, but the only real requirement is that
// it is a unique ID between the *parallel* child processes.
env := make([]string, 0, len(c.Env))
for _, v := range c.Env {
if strings.HasPrefix(v, "GCOV_PREFIX=") {
v = strings.ReplaceAll(v, "PROC_ID", strconv.Itoa(goroutineIndex))
}
env = append(env, v)
}
coverageFile := filepath.Join(c.TempDir, fmt.Sprintf("%v.profraw", goroutineIndex))
if supportsCoverage {
if c.CoverageEnv != nil {
env = cov.AppendRuntimeEnv(env, coverageFile)
}
}
logPath := "/dev/null" // TODO(bclayton): Try "nul" on windows.
if !util.IsFile(logPath) {
logPath = filepath.Join(c.TempDir, fmt.Sprintf("%v.log", goroutineIndex))
}
nextTest:
for name := range tests {
// log.Printf("Running test '%s'\n", name)
start := time.Now()
// Set validation layer according to flag.
validation := "disable"
if c.ValidationLayer {
validation = "enable"
}
outRaw, err := shell.Exec(c.TestTimeout, exe, filepath.Dir(exe), env,
"--deqp-validation="+validation,
"--deqp-surface-type=pbuffer",
"--deqp-shadercache=disable",
"--deqp-log-images=disable",
"--deqp-log-shader-sources=disable",
"--deqp-log-decompiled-spirv=disable",
"--deqp-log-empty-loginfo=disable",
"--deqp-log-flush=disable",
"--deqp-log-filename="+logPath,
"-n="+name)
duration := time.Since(start)
out := string(outRaw)
out = strings.ReplaceAll(out, exe, "<dEQP>")
for k, v := range c.LogReplacements {
out = strings.ReplaceAll(out, k, v)
}
var coverage *cov.Coverage
if c.CoverageEnv != nil && supportsCoverage {
coverage, err = c.CoverageEnv.Import(coverageFile)
if err != nil {
log.Printf("Warning: Failed to process test coverage for test '%v'. %v", name, err)
}
os.Remove(coverageFile)
}
for _, test := range []struct {
re *regexp.Regexp
s testlist.Status
}{
{unimplementedRE, testlist.Unimplemented},
{unsupportedRE, testlist.Unsupported},
{unreachableRE, testlist.Unreachable},
{assertRE, testlist.Assert},
{abortRE, testlist.Abort},
} {
if s := test.re.FindString(out); s != "" {
results <- TestResult{
Test: name,
Status: test.s,
TimeTaken: duration,
Err: s,
Coverage: coverage,
}
continue nextTest
}
}
// Don't treat non-zero error codes as crashes.
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitCode() != 255 {
out += fmt.Sprintf("\nProcess terminated with code %d", exitErr.ExitCode())
err = nil
}
}
switch err.(type) {
default:
results <- TestResult{
Test: name,
Status: testlist.Crash,
TimeTaken: duration,
Err: out,
Coverage: coverage,
}
case shell.ErrTimeout:
log.Printf("Timeout for test '%v'\n", name)
results <- TestResult{
Test: name,
Status: testlist.Timeout,
TimeTaken: duration,
Coverage: coverage,
}
case nil:
toks := deqpRE.FindStringSubmatch(out)
if len(toks) < 3 {
err := fmt.Sprintf("Couldn't parse test '%v' output:\n%s", name, out)
log.Println("Warning: ", err)
results <- TestResult{Test: name, Status: testlist.Fail, Err: err, Coverage: coverage}
continue
}
switch toks[1] {
case "Pass":
results <- TestResult{Test: name, Status: testlist.Pass, TimeTaken: duration, Coverage: coverage}
case "NotSupported":
results <- TestResult{Test: name, Status: testlist.NotSupported, TimeTaken: duration, Coverage: coverage}
case "CompatibilityWarning":
results <- TestResult{Test: name, Status: testlist.CompatibilityWarning, TimeTaken: duration, Coverage: coverage}
case "QualityWarning":
results <- TestResult{Test: name, Status: testlist.QualityWarning, TimeTaken: duration, Coverage: coverage}
case "Fail":
var err string
if toks[2] != "Fail" {
err = toks[2]
}
results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage}
case "InternalError":
var err string
if toks[2] != "InternalError" {
err = toks[2]
}
results <- TestResult{Test: name, Status: testlist.InternalError, Err: err, TimeTaken: duration, Coverage: coverage}
default:
err := fmt.Sprintf("Couldn't parse test output:\n%s", out)
log.Println("Warning: ", err)
results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage}
}
}
}
}