You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
436 lines
14 KiB
436 lines
14 KiB
4 months ago
|
// 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-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}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|