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