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