• 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-flush=disable",
333			"--deqp-log-filename="+logPath,
334			"-n="+name)
335		duration := time.Since(start)
336		out := string(outRaw)
337		out = strings.ReplaceAll(out, exe, "<dEQP>")
338		for k, v := range c.LogReplacements {
339			out = strings.ReplaceAll(out, k, v)
340		}
341
342		var coverage *cov.Coverage
343		if c.CoverageEnv != nil && supportsCoverage {
344			coverage, err = c.CoverageEnv.Import(coverageFile)
345			if err != nil {
346				log.Printf("Warning: Failed to process test coverage for test '%v'. %v", name, err)
347			}
348			os.Remove(coverageFile)
349		}
350
351		for _, test := range []struct {
352			re *regexp.Regexp
353			s  testlist.Status
354		}{
355			{unimplementedRE, testlist.Unimplemented},
356			{unsupportedRE, testlist.Unsupported},
357			{unreachableRE, testlist.Unreachable},
358			{assertRE, testlist.Assert},
359			{abortRE, testlist.Abort},
360		} {
361			if s := test.re.FindString(out); s != "" {
362				results <- TestResult{
363					Test:      name,
364					Status:    test.s,
365					TimeTaken: duration,
366					Err:       s,
367					Coverage:  coverage,
368				}
369				continue nextTest
370			}
371		}
372
373		// Don't treat non-zero error codes as crashes.
374		var exitErr *exec.ExitError
375		if errors.As(err, &exitErr) {
376			if exitErr.ExitCode() != 255 {
377				out += fmt.Sprintf("\nProcess terminated with code %d", exitErr.ExitCode())
378				err = nil
379			}
380		}
381
382		switch err.(type) {
383		default:
384			results <- TestResult{
385				Test:      name,
386				Status:    testlist.Crash,
387				TimeTaken: duration,
388				Err:       out,
389				Coverage:  coverage,
390			}
391		case shell.ErrTimeout:
392			log.Printf("Timeout for test '%v'\n", name)
393			results <- TestResult{
394				Test:      name,
395				Status:    testlist.Timeout,
396				TimeTaken: duration,
397				Coverage:  coverage,
398			}
399		case nil:
400			toks := deqpRE.FindStringSubmatch(out)
401			if len(toks) < 3 {
402				err := fmt.Sprintf("Couldn't parse test '%v' output:\n%s", name, out)
403				log.Println("Warning: ", err)
404				results <- TestResult{Test: name, Status: testlist.Fail, Err: err, Coverage: coverage}
405				continue
406			}
407			switch toks[1] {
408			case "Pass":
409				results <- TestResult{Test: name, Status: testlist.Pass, TimeTaken: duration, Coverage: coverage}
410			case "NotSupported":
411				results <- TestResult{Test: name, Status: testlist.NotSupported, TimeTaken: duration, Coverage: coverage}
412			case "CompatibilityWarning":
413				results <- TestResult{Test: name, Status: testlist.CompatibilityWarning, TimeTaken: duration, Coverage: coverage}
414			case "QualityWarning":
415				results <- TestResult{Test: name, Status: testlist.QualityWarning, TimeTaken: duration, Coverage: coverage}
416			case "Fail":
417				var err string
418				if toks[2] != "Fail" {
419					err = toks[2]
420				}
421				results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage}
422			case "InternalError":
423				var err string
424				if toks[2] != "InternalError" {
425					err = toks[2]
426				}
427				results <- TestResult{Test: name, Status: testlist.InternalError, Err: err, TimeTaken: duration, Coverage: coverage}
428			default:
429				err := fmt.Sprintf("Couldn't parse test output:\n%s", out)
430				log.Println("Warning: ", err)
431				results <- TestResult{Test: name, Status: testlist.Fail, Err: err, TimeTaken: duration, Coverage: coverage}
432			}
433		}
434	}
435}
436