Add tools/fix-tests
A simple regex based tool for fixing unit tests that fail due to unexpected output
Change-Id: I72c47abaff6d6f4ba8cd497240eadc171af0fec3
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/47629
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/tools/fix-tests/fix-tests.go b/tools/fix-tests/fix-tests.go
new file mode 100644
index 0000000..6b8b860
--- /dev/null
+++ b/tools/fix-tests/fix-tests.go
@@ -0,0 +1,277 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// fix-tests is a tool to update tests with new expected output.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
+
+func showUsage() {
+ fmt.Println(`
+fix-tests is a tool to update tests with new expected output.
+
+Usage:
+ fix-tests <executable>
+
+ executable - the path to the test executable to run.`)
+ os.Exit(1)
+}
+
+func run() error {
+ flag.Parse()
+ args := flag.Args()
+ if len(args) < 1 {
+ showUsage()
+ }
+
+ exe := args[0] // The path to the test executable
+ wd := filepath.Dir(exe) // The directory holding the test exe
+
+ // Create a temporary directory to hold the 'test-results.json' file
+ tmpDir, err := ioutil.TempDir("", "fix-tests")
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(tmpDir, 0666); err != nil {
+ return fmt.Errorf("Failed to create temporary directory: %w", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Full path to the 'test-results.json' in the temporary directory
+ testResultsPath := filepath.Join(tmpDir, "test-results.json")
+
+ // Run the tests
+ switch err := exec.Command(exe, "--gtest_output=json:"+testResultsPath).Run().(type) {
+ default:
+ return err
+ case nil:
+ fmt.Println("All tests passed")
+ case *exec.ExitError:
+ }
+
+ // Read the 'test-results.json' file
+ testResultsFile, err := os.Open(testResultsPath)
+ if err != nil {
+ return err
+ }
+
+ var testResults Results
+ if err := json.NewDecoder(testResultsFile).Decode(&testResults); err != nil {
+ return err
+ }
+
+ // For each failing test...
+ var errs []error
+ numFixes := 0
+ for _, group := range testResults.Groups {
+ for _, suite := range group.Testsuites {
+ for _, failure := range suite.Failures {
+ // .. attempt to fix the problem
+ test := group.Name + "." + suite.Name
+ if err := processFailure(test, wd, failure.Failure); err != nil {
+ errs = append(errs, fmt.Errorf("%v: %w", test, err))
+ } else {
+ numFixes++
+ }
+ }
+ }
+ }
+
+ if numFixes > 0 {
+ fmt.Printf("%v tests fixed\n", numFixes)
+ }
+ if n := len(errs); n > 0 {
+ fmt.Printf("%v tests could not be fixed:\n", n)
+ for _, err := range errs {
+ fmt.Println(err)
+ }
+ }
+ return nil
+}
+
+var (
+ // Regular expression to match a test declaration
+ reTests = regexp.MustCompile(`TEST(?:_[FP])?\((\w+),[ \n]*(\w+)\)`)
+ // Regular expression to match a EXPECT_EQ failure for strings
+ reExpectEq = regexp.MustCompile(`^([./\\a-z_-]*):(\d+).*\nExpected equality of these values:\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?)[^\\]"\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?)[^\\]"`)
+)
+
+func processFailure(test, wd, failure string) error {
+ // Start by un-escaping newlines in the failure message
+ failure = strings.ReplaceAll(failure, "\\n", "\n")
+
+ // Look for a EXPECT_EQ failure pattern
+ var file, a, b string
+ if parts := reExpectEq.FindStringSubmatch(failure); len(parts) == 5 {
+ file, a, b = parts[1], parts[3], parts[4]
+ } else {
+ return fmt.Errorf("Cannot fix this type of failure")
+ }
+
+ // Now un-escape any quotes (the regex is sensitive to these)
+ a = strings.ReplaceAll(a, `\"`, `"`)
+ b = strings.ReplaceAll(b, `\"`, `"`)
+
+ // Get the path to the source file containing the test failure
+ sourcePath := filepath.Join(wd, file)
+
+ // Parse the source file, split into tests
+ sourceFile, err := parseSourceFile(sourcePath)
+ if err != nil {
+ return fmt.Errorf("Couldn't parse tests from file '%v': %w", file, err)
+ }
+
+ // Find the test
+ testIdx, ok := sourceFile.tests[test]
+ if !ok {
+ return fmt.Errorf("Test '%v' not found in '%v'", test, file)
+ }
+
+ // Grab the source for the particular test
+ testSource := sourceFile.parts[testIdx]
+
+ // We don't know if a or b is the expected, so just try flipping the string
+ // to the other form.
+ switch {
+ case strings.Contains(testSource, a):
+ testSource = strings.Replace(testSource, a, b, -1)
+ case strings.Contains(testSource, b):
+ testSource = strings.Replace(testSource, b, a, -1)
+ default:
+ // Try escaping for R"(...)" strings
+ a = strings.ReplaceAll(a, "\n", `\n`)
+ b = strings.ReplaceAll(b, "\n", `\n`)
+ a = strings.ReplaceAll(a, "\"", `\"`)
+ b = strings.ReplaceAll(b, "\"", `\"`)
+ switch {
+ case strings.Contains(testSource, a):
+ testSource = strings.Replace(testSource, a, b, -1)
+ case strings.Contains(testSource, b):
+ testSource = strings.Replace(testSource, b, a, -1)
+ default:
+ return fmt.Errorf("Could not fix test '%v' in '%v'", test, file)
+ }
+ }
+
+ // Replace the part of the source file
+ sourceFile.parts[testIdx] = testSource
+
+ // Write out the source file
+ return writeSourceFile(sourcePath, sourceFile)
+}
+
+// parseSourceFile() reads the file at path, splitting the content into chunks
+// for each TEST.
+func parseSourceFile(path string) (sourceFile, error) {
+ fileBytes, err := ioutil.ReadFile(path)
+ if err != nil {
+ return sourceFile{}, err
+ }
+ fileContent := string(fileBytes)
+
+ out := sourceFile{
+ tests: map[string]int{},
+ }
+
+ pos := 0
+ for _, span := range reTests.FindAllStringIndex(fileContent, -1) {
+ out.parts = append(out.parts, fileContent[pos:span[0]])
+ pos = span[0]
+
+ match := reTests.FindStringSubmatch(fileContent[span[0]:span[1]])
+ group := match[1]
+ suite := match[2]
+ out.tests[group+"."+suite] = len(out.parts)
+ }
+ out.parts = append(out.parts, fileContent[pos:])
+
+ return out, nil
+}
+
+// writeSourceFile() joins the chunks of the file, and writes the content out to
+// path.
+func writeSourceFile(path string, file sourceFile) error {
+ body := strings.Join(file.parts, "")
+ return ioutil.WriteFile(path, []byte(body), 0666)
+}
+
+type sourceFile struct {
+ parts []string
+ tests map[string]int // "X.Y" -> part index
+}
+
+// Results is the root JSON structure of the JSON --gtest_output file .
+type Results struct {
+ Tests int `json:"tests"`
+ Failures int `json:"failures"`
+ Disabled int `json:"disabled"`
+ Errors int `json:"errors"`
+ Timestamp string `json:"timestamp"`
+ Time string `json:"time"`
+ Name string `json:"name"`
+ Groups []TestsuiteGroup `json:"testsuites"`
+}
+
+// TestsuiteGroup is a group of test suites in the JSON --gtest_output file .
+type TestsuiteGroup struct {
+ Name string `json:"name"`
+ Tests int `json:"tests"`
+ Failures int `json:"failures"`
+ Disabled int `json:"disabled"`
+ Errors int `json:"errors"`
+ Timestamp string `json:"timestamp"`
+ Time string `json:"time"`
+ Testsuites []Testsuite `json:"testsuite"`
+}
+
+// Testsuite is a suite of tests in the JSON --gtest_output file.
+type Testsuite struct {
+ Name string `json:"name"`
+ ValueParam string `json:"value_param,omitempty"`
+ Status Status `json:"status"`
+ Result Result `json:"result"`
+ Timestamp string `json:"timestamp"`
+ Time string `json:"time"`
+ Classname string `json:"classname"`
+ Failures []Failure `json:"failures,omitempty"`
+}
+
+// Failure is a reported test failure in the JSON --gtest_output file.
+type Failure struct {
+ Failure string `json:"failure"`
+ Type string `json:"type"`
+}
+
+// Status is a status code in the JSON --gtest_output file.
+type Status string
+
+// Result is a result code in the JSON --gtest_output file.
+type Result string