blob: 93b559409623ead8d0b3519b16a80f0c67b6797c [file] [log] [blame]
Ben Claytona2a00e22019-02-16 01:05:23 +00001// 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// Regres is a tool that detects test regressions with SwiftShader changes.
16//
17// Regres monitors changes that have been put up for review with Gerrit.
18// Once a new patchset has been found, regres will checkout, build and test the
19// change against the parent changelist. Any differences in results are reported
20// as a review comment on the change.
21//
22// Once a day regres will also test another, larger set of tests, and post the
23// full test results as a Gerrit changelist. The CI test lists can be based from
24// this daily test list, so testing can be limited to tests that were known to
25// pass.
26package main
27
28import (
29 "bytes"
30 "encoding/json"
31 "errors"
32 "flag"
33 "fmt"
34 "io/ioutil"
35 "log"
36 "os"
37 "os/exec"
38 "path/filepath"
39 "regexp"
40 "runtime"
41 "sort"
42 "strings"
43 "sync"
44 "time"
45
46 "./cause"
47 "./git"
48 "./shell"
49
50 gerrit "github.com/andygrunwald/go-gerrit"
51)
52
53const (
54 gitURL = "https://swiftshader.googlesource.com/SwiftShader"
55 gerritURL = "https://swiftshader-review.googlesource.com/"
56 reportHeader = "Regres report:"
57 dataVersion = 1
58 changeUpdateFrequency = time.Minute * 5
59 changeQueryFrequency = time.Minute * 5
60 testTimeout = time.Minute * 5 // timeout for a single test
61 buildTimeout = time.Minute * 10 // timeout for a build
62 dailyUpdateTestListHour = 5 // 5am
63 fullTestList = "tests/regres/full-tests.json"
64 ciTestList = "tests/regres/ci-tests.json"
65)
66
67var (
68 numParallelTests = runtime.NumCPU()
69
70 deqpPath = flag.String("deqp", "", "path to the deqp build directory")
71 cacheDir = flag.String("cache", "cache", "path to the output cache directory")
72 gerritEmail = flag.String("email", "$SS_REGRES_EMAIL", "gerrit email address for posting regres results")
73 gerritUser = flag.String("user", "$SS_REGRES_USER", "gerrit username for posting regres results")
74 gerritPass = flag.String("pass", "$SS_REGRES_PASS", "gerrit password for posting regres results")
75 keepCheckouts = flag.Bool("keep", false, "don't delete checkout directories after use")
76 dryRun = flag.Bool("dry", false, "don't post regres reports to gerrit")
77 maxProcMemory = flag.Uint64("max-proc-mem", shell.MaxProcMemory, "maximum virtual memory per child process")
78)
79
80func main() {
81 if runtime.GOOS != "linux" {
82 log.Fatal("regres only currently runs on linux")
83 }
84
85 flag.ErrHelp = errors.New("regres is a tool to detect regressions between versions of SwiftShader")
86 flag.Parse()
87
88 shell.MaxProcMemory = *maxProcMemory
89
90 r := regres{
91 deqpBuild: *deqpPath,
92 cacheRoot: *cacheDir,
93 gerritEmail: os.ExpandEnv(*gerritEmail),
94 gerritUser: os.ExpandEnv(*gerritUser),
95 gerritPass: os.ExpandEnv(*gerritPass),
96 keepCheckouts: *keepCheckouts,
97 dryRun: *dryRun,
98 }
99
100 if err := r.run(); err != nil {
101 fmt.Fprintln(os.Stderr, err)
102 os.Exit(-1)
103 }
104}
105
106type regres struct {
107 deqpBuild string // path to the build directory of deqp
108 cmake string // path to cmake
109 make string // path to make
110 cacheRoot string // path to the regres cache directory
111 gerritEmail string // gerrit email address used for posting results
112 gerritUser string // gerrit username used for posting results
113 gerritPass string // gerrit password used for posting results
114 keepCheckouts bool // don't delete source & build checkouts after testing
115 dryRun bool // don't post any reviews
116 maxProcMemory uint64 // max virtual memory for child processes
117}
118
119// resolveDirs ensures that the necessary directories used can be found, and
120// expands them to absolute paths.
121func (r *regres) resolveDirs() error {
122 for _, path := range []*string{
123 &r.deqpBuild,
124 &r.cacheRoot,
125 } {
126 abs, err := filepath.Abs(*path)
127 if err != nil {
128 return cause.Wrap(err, "Couldn't find path '%v'", *path)
129 }
130 if _, err := os.Stat(abs); err != nil {
131 return cause.Wrap(err, "Couldn't find path '%v'", abs)
132 }
133 *path = abs
134 }
135 return nil
136}
137
138// resolveExes resolves all external executables used by regres.
139func (r *regres) resolveExes() error {
140 type exe struct {
141 name string
142 path *string
143 }
144 for _, e := range []exe{
145 {"cmake", &r.cmake},
146 {"make", &r.make},
147 } {
148 path, err := exec.LookPath(e.name)
149 if err != nil {
150 return cause.Wrap(err, "Couldn't find path to %s", e.name)
151 }
152 *e.path = path
153 }
154 return nil
155}
156
157// run performs the main processing loop for the regress tool. It:
158// * Scans for open and recently updated changes in gerrit using queryChanges()
159// and changeInfo.update().
160// * Builds the most recent patchset and the commit's parent CL using
161// r.newTest(<hash>).lazyRun().
162// * Compares the results of the tests using compare().
163// * Posts the results of the compare to gerrit as a review.
164// * Repeats the above steps until the process is interrupted.
165func (r *regres) run() error {
166 if err := r.resolveExes(); err != nil {
167 return cause.Wrap(err, "Couldn't resolve all exes")
168 }
169
170 if err := r.resolveDirs(); err != nil {
171 return cause.Wrap(err, "Couldn't resolve all directories")
172 }
173
174 client, err := gerrit.NewClient(gerritURL, nil)
175 if err != nil {
176 return cause.Wrap(err, "Couldn't create gerrit client")
177 }
178 if r.gerritUser != "" {
179 client.Authentication.SetBasicAuth(r.gerritUser, r.gerritPass)
180 }
181
182 changes := map[string]*changeInfo{} // Change ID -> changeInfo
183 lastUpdatedTestLists := toDate(time.Now())
184 lastQueriedChanges := time.Time{}
185
186 for {
187 if now := time.Now(); toDate(now) != lastUpdatedTestLists && now.Hour() >= dailyUpdateTestListHour {
188 lastUpdatedTestLists = toDate(now)
189 if err := r.updateTestLists(client); err != nil {
190 log.Println(err.Error())
191 }
192 }
193
194 // Update list of tracked changes.
195 if time.Since(lastQueriedChanges) > changeQueryFrequency {
196 lastQueriedChanges = time.Now()
197 if err := queryChanges(client, changes); err != nil {
198 log.Println(err.Error())
199 }
200 }
201
202 // Update change info.
203 for _, change := range changes {
204 if time.Since(change.lastUpdated) > changeUpdateFrequency {
205 change.lastUpdated = time.Now()
206 err := change.update(client)
207 if err != nil {
208 log.Println(cause.Wrap(err, "Couldn't update info for change '%s'", change.id))
209 }
210 }
211 }
212
213 // Find the change with the highest priority.
214 var change *changeInfo
215 numPending := 0
216 for _, c := range changes {
217 if c.pending {
218 numPending++
219 if change == nil || c.priority > change.priority {
220 change = c
221 }
222 }
223 }
224
225 if change == nil {
226 // Everything up to date. Take a break.
227 log.Println("Nothing to do. Sleeping")
228 time.Sleep(time.Minute)
229 continue
230 }
231
232 log.Printf("%d changes queued for testing\n", numPending)
233
234 log.Printf("Testing change '%s'\n", change.id)
235
236 // Get the test results for the latest patchset in the change.
237 latest, err := r.newTest(ciTestList, change.latest).lazyRun()
238 if err != nil {
239 log.Println(cause.Wrap(err, "Failed to test changelist '%s'", change.latest))
240 time.Sleep(time.Minute)
241 continue
242 }
243
244 // Get the test results for the changes's parent changelist.
245 parent, err := r.newTest(ciTestList, change.parent).lazyRun()
246 if err != nil {
247 log.Println(cause.Wrap(err, "Failed to test changelist '%s'", change.parent))
248 time.Sleep(time.Minute)
249 continue
250 }
251
252 // Compare the latest patchset to the change's parent commit.
253 msg := compare(parent, latest)
254
255 // Always include the reportHeader in the message.
256 // changeInfo.update() uses this header to detect whether a patchset has
257 // already got a test result.
258 msg = reportHeader + "\n\n" + msg
259
260 if r.dryRun {
261 log.Printf("DRY RUN: add review to change '%v':\n%v\n", change.id, msg)
262 } else {
263 log.Printf("Posting review to '%s'\n", change.id)
264 _, _, err = client.Changes.SetReview(change.id, change.latest.String(), &gerrit.ReviewInput{
265 Message: msg,
266 Tag: "autogenerated:regress",
267 })
268 if err != nil {
269 return cause.Wrap(err, "Failed to post comments on change '%s'", change.id)
270 }
271 }
272 change.pending = false
273 }
274}
275
276func (r *regres) updateTestLists(client *gerrit.Client) error {
277 log.Println("Updating test lists")
278
279 headHash, err := git.FetchRefHash("HEAD", gitURL)
280 if err != nil {
281 return cause.Wrap(err, "Could not get hash of master HEAD")
282 }
283
284 // Get the full test results for latest master.
285 t := r.newTest(fullTestList, headHash)
286
287 // Keep the checked out directory after the test is run. We want this so
288 // we can build a new patchset containing the updated test lists.
289 t.keepCheckouts = true
290 if !r.keepCheckouts {
291 defer os.RemoveAll(t.srcDir)
292 }
293
294 if _, err := t.run(); err != nil {
295 return cause.Wrap(err, "Failed to test changelist '%s'", headHash)
296 }
297
298 // Stage all the updated test files.
299 for _, path := range t.writtenTestLists {
300 log.Println("Staging", path)
301 git.Add(t.srcDir, path)
302 }
303
304 log.Println("Checking for existing test list")
305 results, _, err := client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
306 QueryOptions: gerrit.QueryOptions{
307 Query: []string{fmt.Sprintf(`status:open+owner:"%v"`, r.gerritEmail)},
308 Limit: 1,
309 },
310 })
311 if err != nil {
312 return cause.Wrap(err, "Failed to checking for existing test list")
313 }
314
315 commitMsg := strings.Builder{}
316 commitMsg.WriteString("Regres: Update test lists @ " + headHash.String()[:8])
317 if results != nil && len(*results) > 0 {
318 // Reuse gerrit change ID if there's already a change up for review.
319 id := (*results)[0].ChangeID
320 commitMsg.WriteString("\n\n")
321 commitMsg.WriteString("Change-Id: " + id)
322 }
323
324 if err := git.Commit(t.srcDir, commitMsg.String(), git.CommitFlags{
325 Name: "SwiftShader Regression Bot",
326 Email: r.gerritEmail,
327 }); err != nil {
328 return cause.Wrap(err, "Failed to commit test results")
329 }
330
331 if r.dryRun {
332 log.Printf("DRY RUN: post results for review")
333 } else {
334 log.Println("Pushing test results for review")
335 if err := git.Push(t.srcDir, gitURL, "HEAD", "refs/for/master", git.PushFlags{
336 Username: r.gerritUser,
337 Password: r.gerritPass,
338 }); err != nil {
339 return cause.Wrap(err, "Failed to push test results for review")
340 }
341 log.Println("Test results posted for review")
342 }
343
344 return nil
345}
346
347// changeInfo holds the important information about a single, open change in
348// gerrit.
349type changeInfo struct {
350 id string // Gerrit change ID.
351 pending bool // Is this change waiting a test for the latest patchset?
352 priority int // Calculated priority based on Gerrit labels.
353 latest git.Hash // Git hash of the latest patchset in the change.
354 parent git.Hash // Git hash of the changelist this change is based on.
355 lastUpdated time.Time // Time the change was last fetched.
356}
357
358// queryChanges updates the changes map by querying gerrit for the latest open
359// changes.
360func queryChanges(client *gerrit.Client, changes map[string]*changeInfo) error {
361 log.Println("Checking for latest changes")
362 results, _, err := client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
363 QueryOptions: gerrit.QueryOptions{
364 Query: []string{"status:open+-age:3d"},
365 Limit: 100,
366 },
367 })
368 if err != nil {
369 return cause.Wrap(err, "Failed to get list of changes")
370 }
371
372 ids := map[string]bool{}
373 for _, r := range *results {
374 ids[r.ChangeID] = true
375 }
376
377 // Add new changes
378 for id := range ids {
379 if _, found := changes[id]; !found {
380 log.Printf("Tracking new change '%v'\n", id)
381 changes[id] = &changeInfo{id: id}
382 }
383 }
384
385 // Remove old changes
386 for id := range changes {
387 if found := ids[id]; !found {
388 log.Printf("Untracking change '%v'\n", id)
389 delete(changes, id)
390 }
391 }
392
393 return nil
394}
395
396// update queries gerrit for information about the given change.
397func (c *changeInfo) update(client *gerrit.Client) error {
398 change, _, err := client.Changes.GetChange(c.id, &gerrit.ChangeOptions{
399 AdditionalFields: []string{"CURRENT_REVISION", "CURRENT_COMMIT", "MESSAGES", "LABELS"},
400 })
401 if err != nil {
402 return cause.Wrap(err, "Getting info for change '%s'", c.id)
403 }
404
405 current, ok := change.Revisions[change.CurrentRevision]
406 if !ok {
407 return fmt.Errorf("Couldn't find current revision for change '%s'", c.id)
408 }
409
410 if len(current.Commit.Parents) == 0 {
411 return fmt.Errorf("Couldn't find current commit for change '%s' has no parents(?)", c.id)
412 }
413
414 kokoroPresubmit := change.Labels["Kokoro-Presubmit"].Approved.AccountID != 0
415 codeReviewScore := change.Labels["Code-Review"].Value
416 presubmitReady := change.Labels["Presubmit-Ready"].Approved.AccountID != 0
417
418 c.priority = 0
419 if presubmitReady {
420 c.priority += 10
421 }
422 c.priority += codeReviewScore
423 if kokoroPresubmit {
424 c.priority++
425 }
426
427 // Is the change from a Googler?
428 canTest := strings.HasSuffix(current.Commit.Committer.Email, "@google.com")
429
430 // Has the latest patchset already been tested?
431 if canTest {
432 for _, msg := range change.Messages {
433 if msg.RevisionNumber == current.Number &&
434 strings.Contains(msg.Message, reportHeader) {
435 canTest = false
436 break
437 }
438 }
439 }
440
441 c.pending = canTest
442 c.latest = git.ParseHash(change.CurrentRevision)
443 c.parent = git.ParseHash(current.Commit.Parents[0].Commit)
444
445 return nil
446}
447
448func (r *regres) newTest(testListPath string, commit git.Hash) *test {
449 srcDir := filepath.Join(r.cacheRoot, "src", commit.String())
450 resDir := filepath.Join(r.cacheRoot, "res", commit.String())
451 return &test{
452 r: r,
453 commit: commit,
454 srcDir: srcDir,
455 resDir: resDir,
456 outDir: filepath.Join(srcDir, "out"),
457 buildDir: filepath.Join(srcDir, "build"),
458 testListPath: testListPath,
459 }
460}
461
462type test struct {
463 r *regres
464 commit git.Hash // hash of the commit to test
465 srcDir string // directory for the SwiftShader checkout
466 resDir string // directory for the test results
467 outDir string // directory for SwiftShader output
468 buildDir string // directory for SwiftShader build
469 keepCheckouts bool // don't delete source & build checkouts after testing
470 testListPath string // relative path to the test list .json file
471
472 writtenTestLists []string // paths to test updated lists that have been written
473}
474
475// lazyRun lazily runs the test t.
476// If the test results are not already cached, then test will setup the test
477// environment, and call t.run().
478// The results of the test will be cached into r.cacheRoot.
479func (t *test) lazyRun() (*CommitTestResults, error) {
480 load := func(data []byte) (interface{}, error) {
481 var res CommitTestResults
482 if err := json.NewDecoder(bytes.NewReader(data)).Decode(&res); err != nil {
483 return nil, err
484 }
485 if res.Version != dataVersion {
486 return nil, errors.New("Data is from an old version")
487 }
488 return &res, nil
489 }
490
491 build := func() ([]byte, interface{}, error) {
492 res, err := t.run()
493
494 if err != nil {
495 return nil, nil, err
496 }
497
498 b := bytes.Buffer{}
499 enc := json.NewEncoder(&b)
500 enc.SetIndent("", " ")
501 if err := enc.Encode(res); err != nil {
502 return nil, nil, err
503 }
504
505 return b.Bytes(), res, nil
506 }
507
508 res, err := loadOrBuild(filepath.Join(t.resDir, "results.json"), load, build)
509 if err != nil {
510 return nil, err
511 }
512
513 return res.(*CommitTestResults), nil
514}
515
516// run executes the tests for the test environment t.
517// If the source is not cached, run will fetch the commit to be tested,
518// before building it, and then run the required tests.
519func (t *test) run() (*CommitTestResults, error) {
520 if isDir(t.srcDir) && t.keepCheckouts {
521 log.Printf("Reusing source cache for commit '%s'\n", t.commit)
522 } else {
523 log.Printf("Checking out '%s'\n", t.commit)
524 os.RemoveAll(t.srcDir)
525 if err := git.Checkout(t.srcDir, gitURL, t.commit); err != nil {
526 return nil, cause.Wrap(err, "Checking out commit '%s'", t.commit)
527 }
528 log.Printf("Checked out commit '%s'\n", t.commit)
529 if !t.keepCheckouts {
530 defer os.RemoveAll(t.srcDir)
531 }
532 }
533
534 if err := t.build(); err != nil {
535 log.Printf("Warning: Commit '%s' failed to build. %v", t.commit, err)
536 return &CommitTestResults{Version: dataVersion, Built: false}, nil
537 }
538 log.Printf("Built '%s'\n", t.commit)
539
540 // Load the list of tests that need executing.
541 // Note: this list may vary by each commit.
542 testLists, err := t.loadTestList()
543 if err != nil {
544 return nil, cause.Wrap(err, "Loading test lists")
545 }
546
547 results, err := t.runTests(testLists)
548 if err != nil {
549 return nil, cause.Wrap(err, "Running tests")
550 }
551 log.Printf("Ran tests for '%s'\n", t.commit)
552
553 if t.keepCheckouts {
554 if err := t.writeTestListsByStatus(testLists, results); err != nil {
555 return nil, cause.Wrap(err, "Writing test lists by status")
556 }
557 }
558
559 return results, nil
560}
561
562// loadOrBuild is a helper for building a lazy resolved cache.
563// loadOrBuild attempts to load the file at path. If the file exists and loaded
564// successfully, then load() is called with the file data, and the the result
565// object from load() is returned.
566// If the file does not exist, the file cannot be loaded, or load() returns an
567// error, then build() is called and the byte slice is saved to path, and the
568// object is returned.
569func loadOrBuild(path string,
570 load func([]byte) (interface{}, error),
571 build func() ([]byte, interface{}, error)) (interface{}, error) {
572
573 if data, err := ioutil.ReadFile(path); err == nil {
574 out, err := load(data)
575 if err == nil {
576 return out, nil
577 }
578 log.Printf("Warning: Failed to load '%s': %v", path, err)
579 os.Remove(path) // Delete and rebuild.
580 }
581
582 data, obj, err := build()
583 if err != nil {
584 return nil, err
585 }
586
587 os.MkdirAll(filepath.Dir(path), 0777)
588
589 if err := ioutil.WriteFile(path, data, 0777); err != nil {
590 log.Printf("Warning: Failed to write to '%s': %v", path, err)
591 }
592 return obj, nil
593}
594
595// build builds the SwiftShader source into t.buildDir.
596func (t *test) build() error {
597 log.Printf("Building '%s'\n", t.commit)
598
599 if err := os.MkdirAll(t.buildDir, 0777); err != nil {
600 return cause.Wrap(err, "Failed to create build directory")
601 }
602
603 if err := shell.Shell(buildTimeout, t.r.cmake, t.buildDir,
604 "-DCMAKE_BUILD_TYPE=Release",
605 "-DDCHECK_ALWAYS_ON=1",
606 ".."); err != nil {
607 return err
608 }
609
610 if err := shell.Shell(buildTimeout, t.r.make, t.buildDir, fmt.Sprintf("-j%d", runtime.NumCPU())); err != nil {
611 return err
612 }
613
614 return nil
615}
616
617// runTests runs all the tests.
618func (t *test) runTests(testLists []TestList) (*CommitTestResults, error) {
619 log.Printf("Running tests for '%s'\n", t.commit)
620 start := time.Now()
621
622 // Wait group that completes once all the tests have finished.
623 wg := sync.WaitGroup{}
624 results := make(chan TestResult, 256)
625
626 numTests := 0
627
628 // For each API that we are testing
629 for _, list := range testLists {
630 // Resolve the test runner
631 var exe string
632 switch list.API {
633 case egl:
634 exe = filepath.Join(t.r.deqpBuild, "modules", "egl", "deqp-egl")
635 case gles2:
636 exe = filepath.Join(t.r.deqpBuild, "modules", "gles2", "deqp-gles2")
637 case gles3:
638 exe = filepath.Join(t.r.deqpBuild, "modules", "gles3", "deqp-gles3")
639 case vulkan:
640 exe = filepath.Join(t.r.deqpBuild, "external", "vulkancts", "modules", "vulkan", "deqp-vk")
641 default:
642 return nil, fmt.Errorf("Unknown API '%v'", list.API)
643 }
644 if !isFile(exe) {
645 return nil, fmt.Errorf("Couldn't find dEQP executable at '%s'", exe)
646 }
647
648 // Build a chan for the test names to be run.
649 tests := make(chan string, len(list.Tests))
650
651 // Start a number of go routines to run the tests.
652 wg.Add(numParallelTests)
653 for i := 0; i < numParallelTests; i++ {
654 go func() {
655 t.deqpTestRoutine(exe, tests, results)
656 wg.Done()
657 }()
658 }
659
660 // Hand the tests to the deqpTestRoutines.
661 for _, t := range list.Tests {
662 tests <- t
663 }
664
665 // Close the tests chan to indicate that there are no more tests to run.
666 // The deqpTestRoutine functions will return once all tests have been
667 // run.
668 close(tests)
669
670 numTests += len(list.Tests)
671 }
672
673 out := CommitTestResults{
674 Version: dataVersion,
675 Built: true,
676 Tests: map[string]TestResult{},
677 }
678
679 // Collect the results.
680 finished := make(chan struct{})
681 lastUpdate := time.Now()
682 go func() {
683 start, i := time.Now(), 0
684 for r := range results {
685 i++
686 out.Tests[r.Test] = r
687 if time.Since(lastUpdate) > time.Minute {
688 lastUpdate = time.Now()
689 remaining := numTests - i
690 log.Printf("Ran %d/%d tests (%v%%). Estimated completion in %v.\n",
691 i, numTests, percent(i, numTests),
692 (time.Since(start)/time.Duration(i))*time.Duration(remaining))
693 }
694 }
695 close(finished)
696 }()
697
698 wg.Wait() // Block until all the deqpTestRoutines have finished.
699 close(results) // Signal no more results.
700 <-finished // And wait for the result collecting go-routine to finish.
701
702 out.Duration = time.Since(start)
703
704 return &out, nil
705}
706
707func (t *test) writeTestListsByStatus(testLists []TestList, results *CommitTestResults) error {
708 for _, list := range testLists {
709 files := map[Status]*os.File{}
710 ext := filepath.Ext(list.File)
711 name := list.File[:len(list.File)-len(ext)]
712 for _, status := range Statuses {
713 path := filepath.Join(t.srcDir, name+"-"+string(status)+ext)
714 dir := filepath.Dir(path)
715 os.MkdirAll(dir, 0777)
716 f, err := os.Create(path)
717 if err != nil {
718 return cause.Wrap(err, "Couldn't create file '%v'", path)
719 }
720 defer f.Close()
721 files[status] = f
722
723 t.writtenTestLists = append(t.writtenTestLists, path)
724 }
725
726 for _, testName := range list.Tests {
727 if r, found := results.Tests[testName]; found {
728 fmt.Fprintln(files[r.Status], testName)
729 }
730 }
731 }
732
733 return nil
734}
735
736// Status is an enumerator of test results.
737type Status string
738
739const (
740 // Pass is the status of a successful test.
741 Pass = Status("PASS")
742 // Fail is the status of a failed test.
743 Fail = Status("FAIL")
744 // Timeout is the status of a test that failed to complete in the alloted
745 // time.
746 Timeout = Status("TIMEOUT")
747 // Crash is the status of a test that crashed.
748 Crash = Status("CRASH")
749 // NotSupported is the status of a test feature not supported by the driver.
750 NotSupported = Status("NOT_SUPPORTED")
751 // CompatibilityWarning is the status passing test with a warning.
752 CompatibilityWarning = Status("COMPATIBILITY_WARNING")
753 // QualityWarning is the status passing test with a warning.
754 QualityWarning = Status("QUALITY_WARNING")
755)
756
757// Statuses is the full list of status types
758var Statuses = []Status{Pass, Fail, Timeout, Crash, NotSupported, CompatibilityWarning, QualityWarning}
759
760// Failing returns true if the task status requires fixing.
761func (s Status) Failing() bool {
762 switch s {
763 case Fail, Timeout, Crash:
764 return true
765 default:
766 return false
767 }
768}
769
770// CommitTestResults holds the results the tests across all APIs for a given
771// commit. The CommitTestResults structure may be serialized to cache the
772// results.
773type CommitTestResults struct {
774 Version int
775 Built bool
776 Tests map[string]TestResult
777 Duration time.Duration
778}
779
780// compare returns a string describing all differences between two
781// CommitTestResults. This string is used as the report message posted to the
782// gerrit code review.
783func compare(old, new *CommitTestResults) string {
784 switch {
785 case !old.Built && !new.Built:
786 return "Build continues to be broken."
787 case old.Built && !new.Built:
788 return "Build broken."
789 case !old.Built && !new.Built:
790 return "Build now fixed. Cannot compare against broken parent."
791 }
792
793 oldStatusCounts, newStatusCounts := map[Status]int{}, map[Status]int{}
794 totalTests := 0
795
796 broken, fixed, failing, removed, changed := []string{}, []string{}, []string{}, []string{}, []string{}
797
798 for test, new := range new.Tests {
799 old, found := old.Tests[test]
800 switch {
801 case (!found || old.Status == Pass) && new.Status.Failing():
802 broken = append(broken, test)
803 case (found && old.Status.Failing()) && new.Status == Pass:
804 fixed = append(fixed, test)
805 case found && old.Status != new.Status:
806 changed = append(changed, test)
807 case found && old.Status.Failing() && new.Status.Failing():
808 failing = append(failing, test) // Still broken
809 }
810 totalTests++
811 if found {
812 oldStatusCounts[old.Status] = oldStatusCounts[old.Status] + 1
813 }
814 newStatusCounts[new.Status] = newStatusCounts[new.Status] + 1
815 }
816
817 for test := range old.Tests {
818 if _, found := new.Tests[test]; !found {
819 removed = append(removed, test)
820 }
821 }
822
823 sb := strings.Builder{}
824
825 // list prints the list l to sb, truncating after a limit.
826 list := func(l []string) {
827 const max = 10
828 for i, s := range l {
829 sb.WriteString(" ")
830 if i == max {
831 sb.WriteString(fmt.Sprintf("> %d more\n", len(l)-i))
832 break
833 }
834 sb.WriteString(fmt.Sprintf("> %s", s))
835 if n, ok := new.Tests[s]; ok {
836 if o, ok := old.Tests[s]; ok && n != o {
837 sb.WriteString(fmt.Sprintf(" - [%s -> %s]", o.Status, n.Status))
838 } else {
839 sb.WriteString(fmt.Sprintf(" - [%s]", n.Status))
840 }
841 }
842 sb.WriteString("\n")
843 }
844 }
845
846 sb.WriteString(fmt.Sprintf(" Total tests: %d\n", totalTests))
847 for _, s := range []struct {
848 label string
849 status Status
850 }{
851 {" Pass", Pass},
852 {" Fail", Fail},
853 {" Timeout", Timeout},
854 {" Crash", Crash},
855 {" Not Supported", NotSupported},
856 {"Compatibility Warning", CompatibilityWarning},
857 {" Quality Warning", QualityWarning},
858 } {
859 old, new := oldStatusCounts[s.status], newStatusCounts[s.status]
860 if old == 0 && new == 0 {
861 continue
862 }
863 change := percent64(int64(new-old), int64(old))
864 switch {
865 case old == new:
866 sb.WriteString(fmt.Sprintf("%s: %v\n", s.label, new))
867 case change == 0:
868 sb.WriteString(fmt.Sprintf("%s: %v -> %v\n", s.label, old, new))
869 default:
870 sb.WriteString(fmt.Sprintf("%s: %v -> %v (%+d%%)\n", s.label, old, new, change))
871 }
872 }
873
874 if old, new := old.Duration, new.Duration; old != 0 && new != 0 {
875 label := " Time taken"
876 change := percent64(int64(new-old), int64(old))
877 switch {
878 case old == new:
879 sb.WriteString(fmt.Sprintf("%s: %v\n", label, new))
880 case change == 0:
881 sb.WriteString(fmt.Sprintf("%s: %v -> %v\n", label, old, new))
882 default:
883 sb.WriteString(fmt.Sprintf("%s: %v -> %v (%+d%%)\n", label, old, new, change))
884 }
885 }
886
887 if n := len(broken); n > 0 {
888 sort.Strings(broken)
889 sb.WriteString(fmt.Sprintf("\n--- This change breaks %d tests: ---\n", n))
890 list(broken)
891 }
892 if n := len(fixed); n > 0 {
893 sort.Strings(fixed)
894 sb.WriteString(fmt.Sprintf("\n--- This change fixes %d tests: ---\n", n))
895 list(fixed)
896 }
897 if n := len(removed); n > 0 {
898 sort.Strings(removed)
899 sb.WriteString(fmt.Sprintf("\n--- This change removes %d tests: ---\n", n))
900 list(removed)
901 }
902 if n := len(changed); n > 0 {
903 sort.Strings(changed)
904 sb.WriteString(fmt.Sprintf("\n--- This change alters %d tests: ---\n", n))
905 list(changed)
906 }
907
908 if len(broken) == 0 && len(fixed) == 0 && len(removed) == 0 && len(changed) == 0 {
909 sb.WriteString(fmt.Sprintf("\n--- No change in test results ---\n"))
910 }
911
912 return sb.String()
913}
914
915// TestResult holds the results of a single API test.
916type TestResult struct {
917 Test string
918 Status Status
919 Err string `json:",omitempty"`
920}
921
922func (r TestResult) String() string {
923 if r.Err != "" {
924 return fmt.Sprintf("%s: %s (%s)", r.Test, r.Status, r.Err)
925 }
926 return fmt.Sprintf("%s: %s", r.Test, r.Status)
927}
928
929// Regular expression to parse the output of a dEQP test.
930var parseRE = regexp.MustCompile(`(Fail|Pass|NotSupported|CompatibilityWarning|QualityWarning) \(([\s\S]*)\)`)
931
932// deqpTestRoutine repeatedly runs the dEQP test executable exe with the tests
933// taken from tests. The output of the dEQP test is parsed, and the test result
934// is written to results.
935// deqpTestRoutine only returns once the tests chan has been closed.
936// deqpTestRoutine does not close the results chan.
937func (t *test) deqpTestRoutine(exe string, tests <-chan string, results chan<- TestResult) {
938 for name := range tests {
939 // log.Printf("Running test '%s'\n", name)
940 env := []string{
941 "LD_LIBRARY_PATH=" + t.buildDir + ":" + os.Getenv("LD_LIBRARY_PATH"),
942 "VK_ICD_FILENAMES=" + filepath.Join(t.outDir, "Linux", "vk_swiftshader_icd.json"),
943 "DISPLAY=" + os.Getenv("DISPLAY"),
944 "LIBC_FATAL_STDERR_=1", // Put libc explosions into logs.
945 }
946
947 out, err := shell.Exec(testTimeout, exe, filepath.Dir(exe), env, "--deqp-surface-type=pbuffer", "-n="+name)
948 switch err.(type) {
949 default:
950 results <- TestResult{
951 Test: name,
952 Status: Crash,
953 Err: cause.Wrap(err, string(out)).Error(),
954 }
955 case shell.ErrTimeout:
956 results <- TestResult{
957 Test: name,
958 Status: Timeout,
959 Err: cause.Wrap(err, string(out)).Error(),
960 }
961 case nil:
962 toks := parseRE.FindStringSubmatch(string(out))
963 if len(toks) < 3 {
964 err := fmt.Sprintf("Couldn't parse test '%v' output:\n%s", name, string(out))
965 log.Println("Warning: ", err)
966 results <- TestResult{Test: name, Status: Fail, Err: err}
967 continue
968 }
969 switch toks[1] {
970 case "Pass":
971 results <- TestResult{Test: name, Status: Pass}
972 case "NotSupported":
973 results <- TestResult{Test: name, Status: NotSupported}
974 case "CompatibilityWarning":
975 results <- TestResult{Test: name, Status: CompatibilityWarning}
976 case "QualityWarning":
977 results <- TestResult{Test: name, Status: CompatibilityWarning}
978 case "Fail":
979 var err string
980 if toks[2] != "Fail" {
981 err = toks[2]
982 }
983 results <- TestResult{Test: name, Status: Fail, Err: err}
984 default:
985 err := fmt.Sprintf("Couldn't parse test output:\n%s", string(out))
986 log.Println("Warning: ", err)
987 results <- TestResult{Test: name, Status: Fail, Err: err}
988 }
989 }
990 }
991}
992
993// API is an enumerator of graphics APIs.
994type API string
995
996const (
997 egl = API("egl")
998 gles2 = API("gles2")
999 gles3 = API("gles3")
1000 vulkan = API("vulkan")
1001)
1002
1003// TestList is a list of tests to be run for a given API.
1004type TestList struct {
1005 Name string
1006 File string
1007 API API
1008 Tests []string
1009}
1010
1011// loadTestList loads the test list json file.
1012// The file is first searched at {Commit}/{t.testListPath}
1013// If this cannot be found, then the file is searched at the fallback path
1014// {CWD}/{t.testListPath}
1015// This allows CLs to alter the list of tests to be run, as well as providing
1016// a default set.
1017func (t *test) loadTestList() ([]TestList, error) {
1018 // find the test.json file in {SwiftShader}/tests/regres
1019 root := t.srcDir
1020 if isFile(filepath.Join(root, t.testListPath)) {
1021 log.Println("Using test list from commit")
1022 } else {
1023 // Not found there. Search locally.
1024 root, _ = os.Getwd()
1025 if isFile(filepath.Join(root, t.testListPath)) {
1026 log.Println("Using test list from regres")
1027 } else {
1028 return nil, fmt.Errorf("Could not find test list file '%v'", t.testListPath)
1029 }
1030 }
1031
1032 jsonPath := filepath.Join(root, t.testListPath)
1033 i, err := ioutil.ReadFile(jsonPath)
1034 if err != nil {
1035 return nil, cause.Wrap(err, "Couldn't read test list from '%s'", jsonPath)
1036 }
1037
1038 var groups []struct {
1039 Name string
1040 API string
1041 TestFile string `json:"tests"`
1042 }
1043 if err := json.NewDecoder(bytes.NewReader(i)).Decode(&groups); err != nil {
1044 return nil, cause.Wrap(err, "Couldn't parse '%s'", jsonPath)
1045 }
1046
1047 dir := filepath.Dir(jsonPath)
1048
1049 out := make([]TestList, len(groups))
1050 for i, group := range groups {
1051 path := filepath.Join(dir, group.TestFile)
1052 tests, err := ioutil.ReadFile(path)
1053 if err != nil {
1054 return nil, cause.Wrap(err, "Couldn't read '%s'", tests)
1055 }
1056 relPath, err := filepath.Rel(root, path)
1057 if err != nil {
1058 return nil, cause.Wrap(err, "Couldn't get relative path for '%s'", path)
1059 }
1060 list := TestList{
1061 Name: group.Name,
1062 File: relPath,
1063 API: API(group.API),
1064 }
1065 for _, line := range strings.Split(string(tests), "\n") {
1066 line = strings.TrimSpace(line)
1067 if line != "" && !strings.HasPrefix(line, "#") {
1068 list.Tests = append(list.Tests, line)
1069 }
1070 }
1071 sort.Strings(list.Tests)
1072 out[i] = list
1073 }
1074
1075 return out, nil
1076}
1077
1078// isDir returns true if path is a file.
1079func isFile(path string) bool {
1080 s, err := os.Stat(path)
1081 if err != nil {
1082 return false
1083 }
1084 return !s.IsDir()
1085}
1086
1087// isDir returns true if path is a directory.
1088func isDir(path string) bool {
1089 s, err := os.Stat(path)
1090 if err != nil {
1091 return false
1092 }
1093 return s.IsDir()
1094}
1095
1096// percent returns the percentage completion of i items out of n.
1097func percent(i, n int) int {
1098 return int(percent64(int64(i), int64(n)))
1099}
1100
1101// percent64 returns the percentage completion of i items out of n.
1102func percent64(i, n int64) int64 {
1103 if n == 0 {
1104 return 0
1105 }
1106 return (100 * i) / n
1107}
1108
1109type date struct {
1110 year int
1111 month time.Month
1112 day int
1113}
1114
1115func toDate(t time.Time) date {
1116 d := date{}
1117 d.year, d.month, d.day = t.Date()
1118 return d
1119}