dawn_node: Add support for diffing against expectations file
Adds the command line flags:
* `--output <path>` which will write the current test results to the given file
* `--expect <path>` will compare the current run against the given expectations file
Bug: dawn:1123
Change-Id: Ie1bfa4e0c0698a95922e350387f8493b7a6ac68b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/75980
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/src/dawn_node/README.md b/src/dawn_node/README.md
index e967bb0..58c9297 100644
--- a/src/dawn_node/README.md
+++ b/src/dawn_node/README.md
@@ -82,6 +82,10 @@
Note that we pass `--verbose` above so that all test output, including the dumped shader, is written to stdout.
+### Testing against a `run-cts` expectations file
+
+You can write out an expectations file with the `--output <path>` command line flag, and then compare this snapshot to a later run with `--expect <path>`.
+
## Debugging TypeScript with VSCode
Open or create the `.vscode/launch.json` file, and add:
diff --git a/src/dawn_node/tools/src/cmd/run-cts/main.go b/src/dawn_node/tools/src/cmd/run-cts/main.go
index 98ae279..bafb13c 100644
--- a/src/dawn_node/tools/src/cmd/run-cts/main.go
+++ b/src/dawn_node/tools/src/cmd/run-cts/main.go
@@ -32,11 +32,13 @@
"path/filepath"
"regexp"
"runtime"
+ "sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
+ "unicode/utf8"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
@@ -68,6 +70,23 @@
mainCtx context.Context
)
+// ANSI escape sequences
+const (
+ escape = "\u001B["
+ positionLeft = escape + "0G"
+ ansiReset = escape + "0m"
+
+ bold = escape + "1m"
+
+ red = escape + "31m"
+ green = escape + "32m"
+ yellow = escape + "33m"
+ blue = escape + "34m"
+ magenta = escape + "35m"
+ cyan = escape + "36m"
+ white = escape + "37m"
+)
+
type dawnNodeFlags []string
func (f *dawnNodeFlags) String() string {
@@ -75,7 +94,7 @@
}
func (f *dawnNodeFlags) Set(value string) error {
- // Multiple flags must be passed in indivually:
+ // Multiple flags must be passed in individually:
// -flag=a=b -dawn_node_flag=c=d
*f = append(*f, value)
return nil
@@ -110,7 +129,7 @@
backendDefault = "vulkan"
}
- var dawnNode, cts, node, npx, logFilename, backend string
+ var dawnNode, cts, node, npx, resultsPath, expectationsPath, logFilename, backend string
var verbose, isolated, build bool
var numRunners int
var flags dawnNodeFlags
@@ -118,6 +137,8 @@
flag.StringVar(&cts, "cts", "", "root directory of WebGPU CTS")
flag.StringVar(&node, "node", "", "path to node executable")
flag.StringVar(&npx, "npx", "", "path to npx executable")
+ flag.StringVar(&resultsPath, "output", "", "path to write test results file")
+ flag.StringVar(&expectationsPath, "expect", "", "path to expectations file")
flag.BoolVar(&verbose, "verbose", false, "print extra information while testing")
flag.BoolVar(&build, "build", true, "attempt to build the CTS before running")
flag.BoolVar(&isolated, "isolate", false, "run each test in an isolated process")
@@ -195,6 +216,7 @@
dawnNode: dawnNode,
cts: cts,
flags: flags,
+ results: testcaseStatuses{},
evalScript: func(main string) string {
return fmt.Sprintf(`require('./src/common/tools/setup-ts-in-node.js');require('./src/common/runtime/%v.ts');`, main)
},
@@ -245,6 +267,15 @@
}
}
+ // If an expectations file was specified, load it.
+ if expectationsPath != "" {
+ if ex, err := loadExpectations(expectationsPath); err == nil {
+ r.expectations = ex
+ } else {
+ return err
+ }
+ }
+
if numRunners > 0 {
// Find all the test cases that match the given queries.
if err := r.gatherTestCases(query, verbose); err != nil {
@@ -254,15 +285,30 @@
if isolated {
fmt.Println("Running in parallel isolated...")
fmt.Printf("Testing %d test cases...\n", len(r.testcases))
- return r.runParallelIsolated()
+ if err := r.runParallelIsolated(); err != nil {
+ return err
+ }
+ } else {
+ fmt.Println("Running in parallel with server...")
+ fmt.Printf("Testing %d test cases...\n", len(r.testcases))
+ if err := r.runParallelWithServer(); err != nil {
+ return err
+ }
}
- fmt.Println("Running in parallel with server...")
- fmt.Printf("Testing %d test cases...\n", len(r.testcases))
- return r.runParallelWithServer()
+ } else {
+ fmt.Println("Running serially...")
+ if err := r.runSerially(query); err != nil {
+ return err
+ }
}
- fmt.Println("Running serially...")
- return r.runSerially(query)
+ if resultsPath != "" {
+ if err := saveExpectations(resultsPath, r.results); err != nil {
+ return err
+ }
+ }
+
+ return nil
}
type logger struct {
@@ -325,6 +371,8 @@
flags dawnNodeFlags
evalScript func(string) string
testcases []string
+ expectations testcaseStatuses
+ results testcaseStatuses
log logger
}
@@ -655,12 +703,12 @@
}()
// Total number of tests, test counts binned by status
- numTests, numByStatus := len(r.testcases), map[status]int{}
+ numTests, numByExpectedStatus := len(r.testcases), map[expectedStatus]int{}
// Helper function for printing a progress bar.
lastStatusUpdate, animFrame := time.Now(), 0
updateProgress := func() {
- printANSIProgressBar(animFrame, numTests, numByStatus)
+ printANSIProgressBar(animFrame, numTests, numByExpectedStatus)
animFrame++
lastStatusUpdate = time.Now()
}
@@ -676,11 +724,22 @@
for res := range results {
r.log.logResults(res)
-
- numByStatus[res.status] = numByStatus[res.status] + 1
+ r.results[res.testcase] = res.status
+ expected := r.expectations[res.testcase]
+ exStatus := expectedStatus{
+ status: res.status,
+ expected: expected == res.status,
+ }
+ numByExpectedStatus[exStatus] = numByExpectedStatus[exStatus] + 1
name := res.testcase
- if r.verbose || res.error != nil || (res.status != pass && res.status != skip) {
- fmt.Printf("%v - %v: %v\n", name, res.status, res.message)
+ if r.verbose ||
+ res.error != nil ||
+ (exStatus.status != pass && exStatus.status != skip && !exStatus.expected) {
+ fmt.Printf("%v - %v: %v", name, res.status, res.message)
+ if expected != "" {
+ fmt.Printf(" [%v -> %v]", expected, res.status)
+ }
+ fmt.Println()
if res.error != nil {
fmt.Println(res.error)
}
@@ -690,27 +749,59 @@
updateProgress()
}
}
- printANSIProgressBar(animFrame, numTests, numByStatus)
+ printANSIProgressBar(animFrame, numTests, numByExpectedStatus)
// All done. Print final stats.
- fmt.Printf(`
-Completed in %v
+ fmt.Printf("\nCompleted in %v\n", timeTaken)
-pass: %v (%v)
-fail: %v (%v)
-skip: %v (%v)
-timeout: %v (%v)
-`,
- timeTaken,
- numByStatus[pass], percentage(numByStatus[pass], numTests),
- numByStatus[fail], percentage(numByStatus[fail], numTests),
- numByStatus[skip], percentage(numByStatus[skip], numTests),
- numByStatus[timeout], percentage(numByStatus[timeout], numTests),
- )
+ var numExpectedByStatus map[status]int
+ if r.expectations != nil {
+ // The status of each testcase that was run
+ numExpectedByStatus = map[status]int{}
+ for t, s := range r.expectations {
+ if _, wasTested := r.results[t]; wasTested {
+ numExpectedByStatus[s] = numExpectedByStatus[s] + 1
+ }
+ }
+ }
+
+ for _, s := range statuses {
+ // number of tests, just run, that resulted in the given status
+ numByStatus := numByExpectedStatus[expectedStatus{s, true}] +
+ numByExpectedStatus[expectedStatus{s, false}]
+ // difference in number of tests that had the given status from the
+ // expected number (taken from the expectations file)
+ diffFromExpected := 0
+ if numExpectedByStatus != nil {
+ diffFromExpected = numByStatus - numExpectedByStatus[s]
+ }
+ if numByStatus == 0 && diffFromExpected == 0 {
+ continue
+ }
+
+ fmt.Print(bold, statusColor[s])
+ fmt.Print(alignRight(strings.ToUpper(string(s))+": ", 10))
+ fmt.Print(ansiReset)
+ if numByStatus > 0 {
+ fmt.Print(bold)
+ }
+ fmt.Print(alignLeft(numByStatus, 10))
+ fmt.Print(ansiReset)
+ fmt.Print(alignRight("("+percentage(numByStatus, numTests)+")", 6))
+
+ if diffFromExpected != 0 {
+ fmt.Print(bold, " [")
+ fmt.Printf("%+d", diffFromExpected)
+ fmt.Print(ansiReset, "]")
+ }
+ fmt.Println()
+ }
+
}
// runSerially() calls the CTS test runner to run the test query in a single
// process.
+// TODO(bclayton): Support comparing against r.expectations
func (r *runner) runSerially(query string) error {
start := time.Now()
result := r.runTestcase(query)
@@ -735,6 +826,24 @@
timeout status = "timeout"
)
+// All the status types
+var statuses = []status{pass, warn, fail, skip, timeout}
+
+var statusColor = map[status]string{
+ pass: green,
+ warn: yellow,
+ skip: blue,
+ timeout: yellow,
+ fail: red,
+}
+
+// expectedStatus is a test status, along with a boolean to indicate whether the
+// status matches the test expectations
+type expectedStatus struct {
+ status status
+ expected bool
+}
+
// result holds the information about a completed test
type result struct {
index int
@@ -828,24 +937,33 @@
return !s.IsDir()
}
+// alignLeft returns the string of 'val' padded so that it is aligned left in
+// a column of the given width
+func alignLeft(val interface{}, width int) string {
+ s := fmt.Sprint(val)
+ padding := width - utf8.RuneCountInString(s)
+ if padding < 0 {
+ return s
+ }
+ return s + strings.Repeat(" ", padding)
+}
+
+// alignRight returns the string of 'val' padded so that it is aligned right in
+// a column of the given width
+func alignRight(val interface{}, width int) string {
+ s := fmt.Sprint(val)
+ padding := width - utf8.RuneCountInString(s)
+ if padding < 0 {
+ return s
+ }
+ return strings.Repeat(" ", padding) + s
+}
+
// printANSIProgressBar prints a colored progress bar, providing realtime
// information about the status of the CTS run.
// Note: We'll want to skip this if !isatty or if we're running on windows.
-func printANSIProgressBar(animFrame int, numTests int, numByStatus map[status]int) {
- const (
- barWidth = 50
-
- escape = "\u001B["
- positionLeft = escape + "0G"
- red = escape + "31m"
- green = escape + "32m"
- yellow = escape + "33m"
- blue = escape + "34m"
- magenta = escape + "35m"
- cyan = escape + "36m"
- white = escape + "37m"
- reset = escape + "0m"
- )
+func printANSIProgressBar(animFrame int, numTests int, numByExpectedStatus map[expectedStatus]int) {
+ const barWidth = 50
animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'}
blockSymbols := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'}
@@ -857,31 +975,41 @@
numFinished := 0
- for _, ty := range []struct {
- status status
- color string
- }{{pass, green}, {warn, yellow}, {skip, blue}, {timeout, yellow}, {fail, red}} {
- num := numByStatus[ty.status]
- numFinished += num
- statusFrac := float64(num) / float64(numTests)
- fNumBlocks := barWidth * statusFrac
- fmt.Fprint(stdout, ty.color)
- numBlocks := int(math.Ceil(fNumBlocks))
- if numBlocks > 1 {
- fmt.Print(strings.Repeat(string("▉"), numBlocks))
+ for _, status := range statuses {
+ for _, expected := range []bool{true, false} {
+ color := statusColor[status]
+ if expected {
+ color += bold
+ }
+
+ num := numByExpectedStatus[expectedStatus{status, expected}]
+ numFinished += num
+ statusFrac := float64(num) / float64(numTests)
+ fNumBlocks := barWidth * statusFrac
+ fmt.Fprint(stdout, color)
+ numBlocks := int(math.Ceil(fNumBlocks))
+ if expected {
+ if numBlocks > 1 {
+ fmt.Print(strings.Repeat(string("░"), numBlocks))
+ }
+ } else {
+ if numBlocks > 1 {
+ fmt.Print(strings.Repeat(string("▉"), numBlocks))
+ }
+ if numBlocks > 0 {
+ frac := fNumBlocks - math.Floor(fNumBlocks)
+ symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))]
+ fmt.Print(string(symbol))
+ }
+ }
+ numBlocksPrinted += numBlocks
}
- if numBlocks > 0 {
- frac := fNumBlocks - math.Floor(fNumBlocks)
- symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))]
- fmt.Print(string(symbol))
- }
- numBlocksPrinted += numBlocks
}
if barWidth > numBlocksPrinted {
fmt.Print(strings.Repeat(string(" "), barWidth-numBlocksPrinted))
}
- fmt.Fprint(stdout, reset)
+ fmt.Fprint(stdout, ansiReset)
fmt.Print("] ", percentage(numFinished, numTests))
if colors {
@@ -892,3 +1020,56 @@
fmt.Println()
}
}
+
+// testcaseStatus is a pair of testcase name and result status
+// Intended to be serialized for expectations files.
+type testcaseStatus struct {
+ Testcase string
+ Status status
+}
+
+// testcaseStatuses is a map of testcase to test status
+type testcaseStatuses map[string]status
+
+// loadExpectations loads the test expectations from path
+func loadExpectations(path string) (testcaseStatuses, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open expectations file: %w", err)
+ }
+ defer f.Close()
+
+ statuses := []testcaseStatus{}
+ if err := json.NewDecoder(f).Decode(&statuses); err != nil {
+ return nil, fmt.Errorf("failed to read expectations file: %w", err)
+ }
+
+ out := make(testcaseStatuses, len(statuses))
+ for _, s := range statuses {
+ out[s.Testcase] = s.Status
+ }
+ return out, nil
+}
+
+// saveExpectations saves the test results 'ex' as an expectations file to path
+func saveExpectations(path string, ex testcaseStatuses) error {
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("failed to create expectations file: %w", err)
+ }
+ defer f.Close()
+
+ statuses := make([]testcaseStatus, 0, len(ex))
+ for testcase, status := range ex {
+ statuses = append(statuses, testcaseStatus{testcase, status})
+ }
+ sort.Slice(statuses, func(i, j int) bool { return statuses[i].Testcase < statuses[j].Testcase })
+
+ e := json.NewEncoder(f)
+ e.SetIndent("", " ")
+ if err := e.Encode(&statuses); err != nil {
+ return fmt.Errorf("failed to save expectations file: %w", err)
+ }
+
+ return nil
+}