Regres: refactor for use as a library

Refactor the dEQP runner functions into a package so that others can
re-use them.

Change-Id: Ia9c92ca708c3f17a87687c2627843922a8c27514
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/38812
Kokoro-Presubmit: kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Tested-by: Paul Thomson <paulthomson@google.com>
diff --git a/tests/regres/deqp/deqp.go b/tests/regres/deqp/deqp.go
new file mode 100644
index 0000000..b6b7b38
--- /dev/null
+++ b/tests/regres/deqp/deqp.go
@@ -0,0 +1,326 @@
+// 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"
+	"log"
+	"math/rand"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"../cause"
+	"../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) \(([^\)]*)\)`)
+	// 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
+	TestLists        testlist.Lists
+	Env              []string
+	LogReplacements  map[string]string
+	NumParallelTests int
+	TestTimeout      time.Duration
+}
+
+// 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
+	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"`
+}
+
+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()
+
+	// Wait group that completes once all the tests have finished.
+	wg := sync.WaitGroup{}
+	results := make(chan TestResult, 256)
+
+	numTests := 0
+
+	// For each API that we are testing
+	for _, list := range c.TestLists {
+		// Resolve the test runner
+		var exe string
+		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 = c.ExeVulkan
+		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))
+
+		// Start a number of go routines to run the tests.
+		wg.Add(c.NumParallelTests)
+		for i := 0; i < c.NumParallelTests; i++ {
+			go func() {
+				c.TestRoutine(exe, tests, results)
+				wg.Done()
+			}()
+		}
+
+		// 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{},
+	}
+
+	// Collect the results.
+	finished := make(chan struct{})
+	lastUpdate := time.Now()
+	go func() {
+		start, i := time.Now(), 0
+		for r := range results {
+			i++
+			out.Tests[r.Test] = r
+			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))
+			}
+		}
+		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) {
+nextTest:
+	for name := range tests {
+		// log.Printf("Running test '%s'\n", name)
+
+		start := time.Now()
+		outRaw, err := shell.Exec(c.TestTimeout, exe, filepath.Dir(exe), c.Env,
+			"--deqp-surface-type=pbuffer",
+			"--deqp-shadercache=disable",
+			"--deqp-log-images=disable",
+			"--deqp-log-shader-sources=disable",
+			"--deqp-log-flush=disable",
+			"-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)
+		}
+
+		// Don't treat non-zero error codes as crashes.
+		var exitErr *exec.ExitError
+		if errors.As(err, &exitErr) {
+			if exitErr.ExitCode() != -1 {
+				out += fmt.Sprintf("\nProcess terminated with code %d", exitErr.ExitCode())
+				err = nil
+			}
+		}
+
+		switch err.(type) {
+		default:
+			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,
+					}
+					continue nextTest
+				}
+			}
+			results <- TestResult{
+				Test:      name,
+				Status:    testlist.Crash,
+				TimeTaken: duration,
+				Err:       out,
+			}
+		case shell.ErrTimeout:
+			log.Printf("Timeout for test '%v'\n", name)
+			results <- TestResult{
+				Test:      name,
+				Status:    testlist.Timeout,
+				TimeTaken: duration,
+			}
+		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}
+				continue
+			}
+			switch toks[1] {
+			case "Pass":
+				results <- TestResult{Test: name, Status: testlist.Pass, TimeTaken: duration}
+			case "NotSupported":
+				results <- TestResult{Test: name, Status: testlist.NotSupported, TimeTaken: duration}
+			case "CompatibilityWarning":
+				results <- TestResult{Test: name, Status: testlist.CompatibilityWarning, TimeTaken: duration}
+			case "QualityWarning":
+				results <- TestResult{Test: name, Status: testlist.QualityWarning, TimeTaken: duration}
+			case "Fail":
+				var err string
+				if toks[2] != "Fail" {
+					err = toks[2]
+				}
+				results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration}
+			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}
+			}
+		}
+	}
+}
diff --git a/tests/regres/main.go b/tests/regres/main.go
index f57e305..7b48059 100644
--- a/tests/regres/main.go
+++ b/tests/regres/main.go
@@ -34,7 +34,6 @@
 	"fmt"
 	"log"
 	"math"
-	"math/rand"
 	"os"
 	"os/exec"
 	"path"
@@ -43,15 +42,15 @@
 	"runtime"
 	"sort"
 	"strings"
-	"sync"
 	"time"
 
 	"./cause"
 	"./consts"
+	"./deqp"
 	"./git"
 	"./shell"
 	"./testlist"
-
+	"./util"
 	gerrit "github.com/andygrunwald/go-gerrit"
 )
 
@@ -59,7 +58,6 @@
 	gitURL                  = "https://swiftshader.googlesource.com/SwiftShader"
 	gerritURL               = "https://swiftshader-review.googlesource.com/"
 	reportHeader            = "Regres report:"
-	dataVersion             = 1
 	changeUpdateFrequency   = time.Minute * 5
 	changeQueryFrequency    = time.Minute * 5
 	testTimeout             = time.Minute * 2  // timeout for a single test
@@ -302,19 +300,19 @@
 		return "", cause.Wrap(err, "Failed to checkout '%s'", change.latest)
 	}
 
-	deqp, err := r.getOrBuildDEQP(latest)
+	deqpBuild, err := r.getOrBuildDEQP(latest)
 	if err != nil {
 		return "", cause.Wrap(err, "Failed to build dEQP '%v' for change", change.id)
 	}
 
 	log.Printf("Testing latest patchset for change '%s'\n", change.id)
-	latestResults, testlists, err := r.testLatest(change, latest, deqp)
+	latestResults, testlists, err := r.testLatest(change, latest, deqpBuild)
 	if err != nil {
 		return "", cause.Wrap(err, "Failed to test latest change of '%v'", change.id)
 	}
 
 	log.Printf("Testing parent of change '%s'\n", change.id)
-	parentResults, err := r.testParent(change, testlists, deqp)
+	parentResults, err := r.testParent(change, testlists, deqpBuild)
 	if err != nil {
 		return "", cause.Wrap(err, "Failed to test parent change of '%v'", change.id)
 	}
@@ -325,14 +323,14 @@
 	return msg, nil
 }
 
-type deqp struct {
+type deqpBuild struct {
 	path string // path to deqp directory
 	hash string // hash of the deqp config
 }
 
-func (r *regres) getOrBuildDEQP(test *test) (deqp, error) {
+func (r *regres) getOrBuildDEQP(test *test) (deqpBuild, error) {
 	srcDir := test.srcDir
-	if p := path.Join(srcDir, deqpConfigRelPath); !isFile(p) {
+	if p := path.Join(srcDir, deqpConfigRelPath); !util.IsFile(p) {
 		srcDir, _ = os.Getwd()
 		log.Printf("Couldn't open dEQP config file from change (%v), falling back to internal version\n", p)
 	} else {
@@ -340,7 +338,7 @@
 	}
 	file, err := os.Open(path.Join(srcDir, deqpConfigRelPath))
 	if err != nil {
-		return deqp{}, cause.Wrap(err, "Couldn't open dEQP config file")
+		return deqpBuild{}, cause.Wrap(err, "Couldn't open dEQP config file")
 	}
 	defer file.Close()
 
@@ -351,19 +349,19 @@
 		Patches []string `json:"patches"`
 	}{}
 	if err := json.NewDecoder(file).Decode(&cfg); err != nil {
-		return deqp{}, cause.Wrap(err, "Couldn't parse %s", deqpConfigRelPath)
+		return deqpBuild{}, cause.Wrap(err, "Couldn't parse %s", deqpConfigRelPath)
 	}
 
 	hasher := sha1.New()
 	if err := json.NewEncoder(hasher).Encode(&cfg); err != nil {
-		return deqp{}, cause.Wrap(err, "Couldn't re-encode %s", deqpConfigRelPath)
+		return deqpBuild{}, cause.Wrap(err, "Couldn't re-encode %s", deqpConfigRelPath)
 	}
 	hash := hex.EncodeToString(hasher.Sum(nil))
 	cacheDir := path.Join(r.cacheRoot, "deqp", hash)
 	buildDir := path.Join(cacheDir, "build")
-	if !isDir(cacheDir) {
+	if !util.IsDir(cacheDir) {
 		if err := os.MkdirAll(cacheDir, 0777); err != nil {
-			return deqp{}, cause.Wrap(err, "Couldn't make deqp cache directory '%s'", cacheDir)
+			return deqpBuild{}, cause.Wrap(err, "Couldn't make deqp cache directory '%s'", cacheDir)
 		}
 
 		success := false
@@ -379,52 +377,52 @@
 			// attempting to directly checkout a remote commit.
 			log.Printf("Checking out deqp %v branch %v into %v\n", cfg.Remote, cfg.Branch, cacheDir)
 			if err := git.CheckoutRemoteBranch(cacheDir, cfg.Remote, cfg.Branch); err != nil {
-				return deqp{}, cause.Wrap(err, "Couldn't checkout deqp branch %v @ %v", cfg.Remote, cfg.Branch)
+				return deqpBuild{}, cause.Wrap(err, "Couldn't checkout deqp branch %v @ %v", cfg.Remote, cfg.Branch)
 			}
 			log.Printf("Checking out deqp %v commit %v \n", cfg.Remote, cfg.SHA)
 			if err := git.CheckoutCommit(cacheDir, git.ParseHash(cfg.SHA)); err != nil {
-				return deqp{}, cause.Wrap(err, "Couldn't checkout deqp commit %v @ %v", cfg.Remote, cfg.SHA)
+				return deqpBuild{}, cause.Wrap(err, "Couldn't checkout deqp commit %v @ %v", cfg.Remote, cfg.SHA)
 			}
 		} else {
 			log.Printf("Checking out deqp %v @ %v into %v\n", cfg.Remote, cfg.SHA, cacheDir)
 			if err := git.CheckoutRemoteCommit(cacheDir, cfg.Remote, git.ParseHash(cfg.SHA)); err != nil {
-				return deqp{}, cause.Wrap(err, "Couldn't checkout deqp commit %v @ %v", cfg.Remote, cfg.SHA)
+				return deqpBuild{}, cause.Wrap(err, "Couldn't checkout deqp commit %v @ %v", cfg.Remote, cfg.SHA)
 			}
 		}
 
 		log.Println("Fetching deqp dependencies")
 		if err := shell.Shell(buildTimeout, r.python, cacheDir, "external/fetch_sources.py"); err != nil {
-			return deqp{}, cause.Wrap(err, "Couldn't fetch deqp sources %v @ %v", cfg.Remote, cfg.SHA)
+			return deqpBuild{}, cause.Wrap(err, "Couldn't fetch deqp sources %v @ %v", cfg.Remote, cfg.SHA)
 		}
 
 		log.Println("Applying deqp patches")
 		for _, patch := range cfg.Patches {
 			fullPath := path.Join(srcDir, patch)
 			if err := git.Apply(cacheDir, fullPath); err != nil {
-				return deqp{}, cause.Wrap(err, "Couldn't apply deqp patch %v for %v @ %v", patch, cfg.Remote, cfg.SHA)
+				return deqpBuild{}, cause.Wrap(err, "Couldn't apply deqp patch %v for %v @ %v", patch, cfg.Remote, cfg.SHA)
 			}
 		}
 
 		log.Printf("Building deqp into %v\n", buildDir)
 		if err := os.MkdirAll(buildDir, 0777); err != nil {
-			return deqp{}, cause.Wrap(err, "Couldn't make deqp build directory '%v'", buildDir)
+			return deqpBuild{}, cause.Wrap(err, "Couldn't make deqp build directory '%v'", buildDir)
 		}
 
 		if err := shell.Shell(buildTimeout, r.cmake, buildDir,
 			"-DDEQP_TARGET=x11_egl",
 			"-DCMAKE_BUILD_TYPE=Release",
 			".."); err != nil {
-			return deqp{}, cause.Wrap(err, "Couldn't generate build rules for deqp %v @ %v", cfg.Remote, cfg.SHA)
+			return deqpBuild{}, cause.Wrap(err, "Couldn't generate build rules for deqp %v @ %v", cfg.Remote, cfg.SHA)
 		}
 
 		if err := shell.Shell(buildTimeout, r.make, buildDir, fmt.Sprintf("-j%d", runtime.NumCPU())); err != nil {
-			return deqp{}, cause.Wrap(err, "Couldn't build deqp %v @ %v", cfg.Remote, cfg.SHA)
+			return deqpBuild{}, cause.Wrap(err, "Couldn't build deqp %v @ %v", cfg.Remote, cfg.SHA)
 		}
 
 		success = true
 	}
 
-	return deqp{
+	return deqpBuild{
 		path: cacheDir,
 		hash: hash,
 	}, nil
@@ -432,7 +430,7 @@
 
 var additionalTestsRE = regexp.MustCompile(`\n\s*Test[s]?:\s*([^\s]+)[^\n]*`)
 
-func (r *regres) testLatest(change *changeInfo, test *test, d deqp) (*CommitTestResults, testlist.Lists, error) {
+func (r *regres) testLatest(change *changeInfo, test *test, d deqpBuild) (*deqp.Results, testlist.Lists, error) {
 	// Get the test results for the latest patchset in the change.
 	testlists, err := test.loadTestLists(ciTestListRelPath)
 	if err != nil {
@@ -464,7 +462,7 @@
 
 	cachePath := test.resultsCachePath(testlists, d)
 
-	if results, err := loadCommitTestResults(cachePath); err == nil {
+	if results, err := deqp.LoadResults(cachePath); err == nil {
 		return results, testlists, nil // Use cached results
 	}
 
@@ -472,21 +470,21 @@
 	results := test.buildAndRun(testlists, d)
 
 	// Cache the results for future tests
-	if err := results.save(cachePath); err != nil {
+	if err := results.Save(cachePath); err != nil {
 		log.Printf("Warning: Couldn't save results of test to '%v'\n", cachePath)
 	}
 
 	return results, testlists, nil
 }
 
-func (r *regres) testParent(change *changeInfo, testlists testlist.Lists, d deqp) (*CommitTestResults, error) {
+func (r *regres) testParent(change *changeInfo, testlists testlist.Lists, d deqpBuild) (*deqp.Results, error) {
 	// Get the test results for the changes's parent changelist.
 	test := r.newTest(change.parent)
 	defer test.cleanup()
 
 	cachePath := test.resultsCachePath(testlists, d)
 
-	if results, err := loadCommitTestResults(cachePath); err == nil {
+	if results, err := deqp.LoadResults(cachePath); err == nil {
 		return results, nil // Use cached results
 	}
 
@@ -499,7 +497,7 @@
 	results := test.buildAndRun(testlists, d)
 
 	// Store the results of the parent change to the cache.
-	if err := results.save(cachePath); err != nil {
+	if err := results.Save(cachePath); err != nil {
 		log.Printf("Warning: Couldn't save results of test to '%v'\n", cachePath)
 	}
 
@@ -554,7 +552,9 @@
 	// Stage all the updated test files.
 	for _, path := range filePaths {
 		log.Println("Staging", path)
-		git.Add(test.srcDir, path)
+		if err := git.Add(test.srcDir, path); err != nil {
+			return err
+		}
 	}
 
 	log.Println("Checking for existing test list")
@@ -605,10 +605,10 @@
 
 // postMostCommonFailures posts the most common failure cases as a review
 // comment on the given change.
-func (r *regres) postMostCommonFailures(client *gerrit.Client, change *gerrit.ChangeInfo, results *CommitTestResults) error {
+func (r *regres) postMostCommonFailures(client *gerrit.Client, change *gerrit.ChangeInfo, results *deqp.Results) error {
 	const limit = 25
 
-	failures := results.commonFailures()
+	failures := commonFailures(results)
 	if len(failures) > limit {
 		failures = failures[:limit]
 	}
@@ -810,7 +810,7 @@
 
 // checkout clones the test's source commit into t.src.
 func (t *test) checkout() error {
-	if isDir(t.srcDir) && t.keepCheckouts {
+	if util.IsDir(t.srcDir) && t.keepCheckouts {
 		log.Printf("Reusing source cache for commit '%s'\n", t.commit)
 		return nil
 	}
@@ -824,13 +824,13 @@
 }
 
 // buildAndRun calls t.build() followed by t.run(). Errors are logged and
-// reported in the returned CommitTestResults.Error field.
-func (t *test) buildAndRun(testLists testlist.Lists, d deqp) *CommitTestResults {
+// reported in the returned deqprun.Results.Error field.
+func (t *test) buildAndRun(testLists testlist.Lists, d deqpBuild) *deqp.Results {
 	// Build the parent change.
 	if err := t.build(); err != nil {
 		msg := fmt.Sprintf("Failed to build '%s'", t.commit)
 		log.Println(cause.Wrap(err, msg))
-		return &CommitTestResults{Error: msg}
+		return &deqp.Results{Error: msg}
 	}
 
 	// Run the tests on the parent change.
@@ -838,7 +838,7 @@
 	if err != nil {
 		msg := fmt.Sprintf("Failed to test change '%s'", t.commit)
 		log.Println(cause.Wrap(err, msg))
-		return &CommitTestResults{Error: msg}
+		return &deqp.Results{Error: msg}
 	}
 
 	return results
@@ -868,113 +868,41 @@
 	return nil
 }
 
-// run runs all the tests.
-func (t *test) run(testLists testlist.Lists, d deqp) (*CommitTestResults, error) {
+func (t *test) run(testLists testlist.Lists, d deqpBuild) (*deqp.Results, error) {
 	log.Printf("Running tests for '%s'\n", t.commit)
 
 	outDir := filepath.Join(t.srcDir, "out")
-	if !isDir(outDir) { // https://swiftshader-review.googlesource.com/c/SwiftShader/+/27188
+	if !util.IsDir(outDir) { // https://swiftshader-review.googlesource.com/c/SwiftShader/+/27188
 		outDir = t.buildDir
 	}
-	if !isDir(outDir) {
+	if !util.IsDir(outDir) {
 		return nil, fmt.Errorf("Couldn't find output directory")
 	}
 	log.Println("outDir:", outDir)
 
-	start := time.Now()
-
-	// Wait group that completes once all the tests have finished.
-	wg := sync.WaitGroup{}
-	results := make(chan TestResult, 256)
-
-	numTests := 0
-
-	// For each API that we are testing
-	for _, list := range testLists {
-		// Resolve the test runner
-		var exe string
-		switch list.API {
-		case testlist.EGL:
-			exe = filepath.Join(d.path, "build", "modules", "egl", "deqp-egl")
-		case testlist.GLES2:
-			exe = filepath.Join(d.path, "build", "modules", "gles2", "deqp-gles2")
-		case testlist.GLES3:
-			exe = filepath.Join(d.path, "build", "modules", "gles3", "deqp-gles3")
-		case testlist.Vulkan:
-			exe = filepath.Join(d.path, "build", "external", "vulkancts", "modules", "vulkan", "deqp-vk")
-		default:
-			return nil, fmt.Errorf("Unknown API '%v'", list.API)
-		}
-		if !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))
-
-		// Start a number of go routines to run the tests.
-		wg.Add(numParallelTests)
-		for i := 0; i < numParallelTests; i++ {
-			go func() {
-				t.deqpTestRoutine(exe, outDir, tests, results)
-				wg.Done()
-			}()
-		}
-
-		// 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 deqpTestRoutines.
-		for _, t := range shuffled {
-			tests <- t
-		}
-
-		// Close the tests chan to indicate that there are no more tests to run.
-		// The deqpTestRoutine functions will return once all tests have been
-		// run.
-		close(tests)
-
-		numTests += len(list.Tests)
+	config := deqp.Config{
+		ExeEgl:    filepath.Join(d.path, "build", "modules", "egl", "deqp-egl"),
+		ExeGles2:  filepath.Join(d.path, "build", "modules", "gles2", "deqp-gles2"),
+		ExeGles3:  filepath.Join(d.path, "build", "modules", "gles3", "deqp-gles3"),
+		ExeVulkan: filepath.Join(d.path, "build", "external", "vulkancts", "modules", "vulkan", "deqp-vk"),
+		TestLists: testLists,
+		Env: []string{
+			"LD_LIBRARY_PATH=" + t.buildDir + ":" + os.Getenv("LD_LIBRARY_PATH"),
+			"VK_ICD_FILENAMES=" + filepath.Join(outDir, "Linux", "vk_swiftshader_icd.json"),
+			"DISPLAY=" + os.Getenv("DISPLAY"),
+			"LIBC_FATAL_STDERR_=1", // Put libc explosions into logs.
+		},
+		LogReplacements: map[string]string{
+			t.srcDir: "<SwiftShader>",
+		},
+		NumParallelTests: numParallelTests,
+		TestTimeout:      testTimeout,
 	}
 
-	out := CommitTestResults{
-		Version: dataVersion,
-		Tests:   map[string]TestResult{},
-	}
-
-	// Collect the results.
-	finished := make(chan struct{})
-	lastUpdate := time.Now()
-	go func() {
-		start, i := time.Now(), 0
-		for r := range results {
-			i++
-			out.Tests[r.Test] = r
-			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, percent(i, numTests),
-					(time.Since(start)/time.Duration(i))*time.Duration(remaining))
-			}
-		}
-		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
+	return config.Run()
 }
 
-func (t *test) writeTestListsByStatus(testLists testlist.Lists, results *CommitTestResults) ([]string, error) {
+func (t *test) writeTestListsByStatus(testLists testlist.Lists, results *deqp.Results) ([]string, error) {
 	out := []string{}
 
 	for _, list := range testLists {
@@ -1004,56 +932,11 @@
 }
 
 // resultsCachePath returns the path to the cache results file for the given
-// test, testlists and path to deqp.
-func (t *test) resultsCachePath(testLists testlist.Lists, d deqp) string {
+// test, testlists and deqpBuild.
+func (t *test) resultsCachePath(testLists testlist.Lists, d deqpBuild) string {
 	return filepath.Join(t.resDir, testLists.Hash(), d.hash)
 }
 
-// CommitTestResults holds the results the tests across all APIs for a given
-// commit. The CommitTestResults structure may be serialized to cache the
-// results.
-type CommitTestResults struct {
-	Version  int
-	Error    string
-	Tests    map[string]TestResult
-	Duration time.Duration
-}
-
-func loadCommitTestResults(path string) (*CommitTestResults, 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 CommitTestResults
-	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
-}
-
-func (r *CommitTestResults) save(path string) error {
-	os.MkdirAll(filepath.Dir(path), 0777)
-
-	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
-}
-
 type testStatusAndError struct {
 	status testlist.Status
 	error  string
@@ -1065,10 +948,10 @@
 	exampleTest string
 }
 
-func (r *CommitTestResults) commonFailures() []commonFailure {
+func commonFailures(results *deqp.Results) []commonFailure {
 	failures := map[testStatusAndError]int{}
 	examples := map[testStatusAndError]string{}
-	for name, test := range r.Tests {
+	for name, test := range results.Tests {
 		if !test.Status.Failing() {
 			continue
 		}
@@ -1089,9 +972,9 @@
 }
 
 // compare returns a string describing all differences between two
-// CommitTestResults. This string is used as the report message posted to the
+// deqprun.Results. This string is used as the report message posted to the
 // gerrit code review.
-func compare(old, new *CommitTestResults) string {
+func compare(old, new *deqp.Results) string {
 	if old.Error != "" {
 		return old.Error
 	}
@@ -1185,7 +1068,7 @@
 		if old == 0 && new == 0 {
 			continue
 		}
-		change := percent64(int64(new-old), int64(old))
+		change := util.Percent64(int64(new-old), int64(old))
 		switch {
 		case old == new:
 			sb.WriteString(fmt.Sprintf("%s: %v\n", s.label, new))
@@ -1198,7 +1081,7 @@
 
 	if old, new := old.Duration, new.Duration; old != 0 && new != 0 {
 		label := "           Time taken"
-		change := percent64(int64(new-old), int64(old))
+		change := util.Percent64(int64(new-old), int64(old))
 		switch {
 		case old == new:
 			sb.WriteString(fmt.Sprintf("%s: %v\n", label, new))
@@ -1267,7 +1150,7 @@
 		}
 		sort.Slice(timingDiffs, func(i, j int) bool { return timingDiffs[i].relDelta < timingDiffs[j].relDelta })
 		for _, d := range timingDiffs {
-			percent := percent64(int64(d.new-d.old), int64(d.old))
+			percent := util.Percent64(int64(d.new-d.old), int64(d.old))
 			sb.WriteString(fmt.Sprintf("  > %v: %v -> %v (%+d%%)\n", d.name, d.old, d.new, percent))
 		}
 	}
@@ -1275,141 +1158,6 @@
 	return sb.String()
 }
 
-// TestResult holds the results of a single API test.
-type TestResult struct {
-	Test      string
-	Status    testlist.Status
-	TimeTaken time.Duration
-	Err       string `json:",omitempty"`
-}
-
-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)
-}
-
-var (
-	// Regular expression to parse the output of a dEQP test.
-	deqpRE = regexp.MustCompile(`(Fail|Pass|NotSupported|CompatibilityWarning|QualityWarning) \(([^\)]*)\)`)
-	// 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]*`)
-)
-
-// deqpTestRoutine 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.
-// deqpTestRoutine only returns once the tests chan has been closed.
-// deqpTestRoutine does not close the results chan.
-func (t *test) deqpTestRoutine(exe, outDir string, tests <-chan string, results chan<- TestResult) {
-nextTest:
-	for name := range tests {
-		// log.Printf("Running test '%s'\n", name)
-		env := []string{
-			"LD_LIBRARY_PATH=" + t.buildDir + ":" + os.Getenv("LD_LIBRARY_PATH"),
-			"VK_ICD_FILENAMES=" + filepath.Join(outDir, "Linux", "vk_swiftshader_icd.json"),
-			"DISPLAY=" + os.Getenv("DISPLAY"),
-			"LIBC_FATAL_STDERR_=1", // Put libc explosions into logs.
-		}
-
-		start := time.Now()
-		outRaw, err := shell.Exec(testTimeout, exe, filepath.Dir(exe), env,
-			"--deqp-surface-type=pbuffer",
-			"--deqp-shadercache=disable",
-			"--deqp-log-images=disable",
-			"--deqp-log-shader-sources=disable",
-			"--deqp-log-flush=disable",
-			"-n="+name)
-		duration := time.Since(start)
-		out := string(outRaw)
-		out = strings.ReplaceAll(out, t.srcDir, "<SwiftShader>")
-		out = strings.ReplaceAll(out, exe, "<dEQP>")
-
-		// Don't treat non-zero error codes as crashes.
-		var exitErr *exec.ExitError
-		if errors.As(err, &exitErr) {
-			if exitErr.ExitCode() != -1 {
-				out += fmt.Sprintf("\nProcess terminated with code %d", exitErr.ExitCode())
-				err = nil
-			}
-		}
-
-		switch err.(type) {
-		default:
-			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,
-					}
-					continue nextTest
-				}
-			}
-			results <- TestResult{
-				Test:      name,
-				Status:    testlist.Crash,
-				TimeTaken: duration,
-				Err:       out,
-			}
-		case shell.ErrTimeout:
-			log.Printf("Timeout for test '%v'\n", name)
-			results <- TestResult{
-				Test:      name,
-				Status:    testlist.Timeout,
-				TimeTaken: duration,
-			}
-		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}
-				continue
-			}
-			switch toks[1] {
-			case "Pass":
-				results <- TestResult{Test: name, Status: testlist.Pass, TimeTaken: duration}
-			case "NotSupported":
-				results <- TestResult{Test: name, Status: testlist.NotSupported, TimeTaken: duration}
-			case "CompatibilityWarning":
-				results <- TestResult{Test: name, Status: testlist.CompatibilityWarning, TimeTaken: duration}
-			case "QualityWarning":
-				results <- TestResult{Test: name, Status: testlist.QualityWarning, TimeTaken: duration}
-			case "Fail":
-				var err string
-				if toks[2] != "Fail" {
-					err = toks[2]
-				}
-				results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration}
-			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}
-			}
-		}
-	}
-}
-
 // loadTestLists loads the full test lists from the json file.
 // The file is first searched at {t.srcDir}/{relPath}
 // If this cannot be found, then the file is searched at the fallback path
@@ -1418,7 +1166,7 @@
 // a default set.
 func (t *test) loadTestLists(relPath string) (testlist.Lists, error) {
 	// Seach for the test.json file in the checked out source directory.
-	if path := filepath.Join(t.srcDir, relPath); isFile(path) {
+	if path := filepath.Join(t.srcDir, relPath); util.IsFile(path) {
 		log.Printf("Loading test list '%v' from commit\n", relPath)
 		return testlist.Load(t.srcDir, path)
 	}
@@ -1428,7 +1176,7 @@
 	if err != nil {
 		return testlist.Lists{}, cause.Wrap(err, "Couldn't get current working directory")
 	}
-	if path := filepath.Join(wd, relPath); isFile(path) {
+	if path := filepath.Join(wd, relPath); util.IsFile(path) {
 		log.Printf("Loading test list '%v' from regres\n", relPath)
 		return testlist.Load(wd, relPath)
 	}
@@ -1436,37 +1184,6 @@
 	return nil, errors.New("Couldn't find a test list file")
 }
 
-// isDir returns true if path is a file.
-func isFile(path string) bool {
-	s, err := os.Stat(path)
-	if err != nil {
-		return false
-	}
-	return !s.IsDir()
-}
-
-// isDir returns true if path is a directory.
-func isDir(path string) bool {
-	s, err := os.Stat(path)
-	if err != nil {
-		return false
-	}
-	return s.IsDir()
-}
-
-// percent returns the percentage completion of i items out of n.
-func percent(i, n int) int {
-	return int(percent64(int64(i), int64(n)))
-}
-
-// percent64 returns the percentage completion of i items out of n.
-func percent64(i, n int64) int64 {
-	if n == 0 {
-		return 0
-	}
-	return (100 * i) / n
-}
-
 type date struct {
 	year  int
 	month time.Month
diff --git a/tests/regres/util/util.go b/tests/regres/util/util.go
new file mode 100644
index 0000000..1a80d83
--- /dev/null
+++ b/tests/regres/util/util.go
@@ -0,0 +1,51 @@
+// 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 util provides small utility functions.
+package util
+
+import (
+	"os"
+)
+
+// IsFile returns true if path is a file.
+func IsFile(path string) bool {
+	s, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+	return !s.IsDir()
+}
+
+// IsDir returns true if path is a directory.
+func IsDir(path string) bool {
+	s, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+	return s.IsDir()
+}
+
+// Percent returns the percentage completion of i items out of n.
+func Percent(i, n int) int {
+	return int(Percent64(int64(i), int64(n)))
+}
+
+// Percent64 returns the percentage completion of i items out of n.
+func Percent64(i, n int64) int64 {
+	if n == 0 {
+		return 0
+	}
+	return (100 * i) / n
+}