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