| // 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" |
| |
| "dawn.googlesource.com/tint/tools/src/substr" |
| ) |
| |
| 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. |
| |
| fix-tests performs string matching and heuristics to fix up expected results of |
| tests that use EXPECT_EQ(a, b) and EXPECT_THAT(a, HasSubstr(b)) |
| |
| WARNING: Always thoroughly check the generated output for mistakes. |
| This may produce incorrect 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 |
| testArgs := []string{"--gtest_output=json:" + testResultsPath} |
| if len(args) > 1 { |
| testArgs = append(testArgs, args[1:]...) |
| } |
| switch err := exec.Command(exe, testArgs...).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... |
| seen := map[string]bool{} |
| numFixed, numFailed := 0, 0 |
| for _, group := range testResults.Groups { |
| for _, suite := range group.Testsuites { |
| for _, failure := range suite.Failures { |
| // .. attempt to fix the problem |
| test := testName(group, suite) |
| if seen[test] { |
| continue |
| } |
| seen[test] = true |
| |
| if err := processFailure(test, wd, failure.Failure); err != nil { |
| fmt.Println(fmt.Errorf("%v: %w", test, err)) |
| numFailed++ |
| } else { |
| numFixed++ |
| } |
| } |
| } |
| } |
| |
| fmt.Println() |
| |
| if numFailed > 0 { |
| fmt.Println(numFailed, "tests could not be fixed") |
| } |
| if numFixed > 0 { |
| fmt.Println(numFixed, "tests fixed") |
| } |
| return nil |
| } |
| |
| func testName(group TestsuiteGroup, suite Testsuite) string { |
| groupParts := strings.Split(group.Name, "/") |
| suiteParts := strings.Split(suite.Name, "/") |
| return groupParts[len(groupParts)-1] + "." + suiteParts[0] |
| } |
| |
| var ( |
| // Regular expression to match a test declaration |
| reTests = regexp.MustCompile(`TEST(?:_[FP])?\([ \n]*(\w+),[ \n]*(\w+)\)`) |
| // Regular expression to match a `EXPECT_EQ(a, b)` failure for strings |
| reExpectEq = regexp.MustCompile(`([./\\\w_\-:]*):(\d+).*\nExpected equality of these values:\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?[^\\])"\n(?:.|\n)*?(?:Which is: | )"((?:.|\n)*?[^\\])"`) |
| // Regular expression to match a `EXPECT_THAT(a, HasSubstr(b))` failure for strings |
| reExpectHasSubstr = regexp.MustCompile(`([./\\\w_\-:]*):(\d+).*\nValue of: .*\nExpected: has substring "((?:.|\n)*?[^\\])"\n Actual: "((?:.|\n)*?[^\\])"`) |
| ) |
| |
| func processFailure(test, wd, failure string) error { |
| // Start by un-escaping newlines in the failure message |
| failure = strings.ReplaceAll(failure, "\\n", "\n") |
| // Matched regex strings will also need to be un-escaped, but do this after |
| // the match, as unescaped quotes may upset the regex patterns |
| unescape := func(s string) string { |
| return strings.ReplaceAll(s, `\"`, `"`) |
| } |
| escape := func(s string) string { |
| s = strings.ReplaceAll(s, "\n", `\n`) |
| s = strings.ReplaceAll(s, "\"", `\"`) |
| return s |
| } |
| |
| // Look for a EXPECT_EQ failure pattern |
| var file string |
| var fix func(testSource string) (string, error) |
| if parts := reExpectEq.FindStringSubmatch(failure); len(parts) == 5 { |
| // EXPECT_EQ(a, b) |
| a, b := unescape(parts[3]), unescape(parts[4]) |
| file = parts[1] |
| fix = func(testSource string) (string, error) { |
| // We don't know if a or b is the expected, so just try flipping the string |
| // to the other form. |
| |
| if len(b) > len(a) { // Go with the longer match, in case both are found |
| a, b = b, a |
| } |
| switch { |
| case strings.Contains(testSource, a): |
| testSource = strings.ReplaceAll(testSource, a, b) |
| case strings.Contains(testSource, b): |
| testSource = strings.ReplaceAll(testSource, b, a) |
| default: |
| // Try escaping for R"(...)" strings |
| a, b = escape(a), escape(b) |
| switch { |
| case strings.Contains(testSource, a): |
| testSource = strings.ReplaceAll(testSource, a, b) |
| case strings.Contains(testSource, b): |
| testSource = strings.ReplaceAll(testSource, b, a) |
| default: |
| return "", fmt.Errorf("Could not fix 'EXPECT_EQ' pattern in '%v'", file) |
| } |
| } |
| return testSource, nil |
| } |
| } else if parts := reExpectHasSubstr.FindStringSubmatch(failure); len(parts) == 5 { |
| // EXPECT_THAT(a, HasSubstr(b)) |
| a, b := unescape(parts[4]), unescape(parts[3]) |
| file = parts[1] |
| fix = func(testSource string) (string, error) { |
| if fix := substr.Fix(a, b); fix != "" { |
| if !strings.Contains(testSource, b) { |
| // Try escaping for R"(...)" strings |
| b, fix = escape(b), escape(fix) |
| } |
| if strings.Contains(testSource, b) { |
| testSource = strings.Replace(testSource, b, fix, -1) |
| return testSource, nil |
| } |
| return "", fmt.Errorf("Could apply fix for 'HasSubstr' pattern in '%v'", file) |
| } |
| |
| return "", fmt.Errorf("Could find fix for 'HasSubstr' pattern in '%v'", file) |
| } |
| } else { |
| return fmt.Errorf("Cannot fix this type of failure") |
| } |
| |
| // Get the absolute source path |
| sourcePath := file |
| if !filepath.IsAbs(sourcePath) { |
| 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 not found in '%v'", file) |
| } |
| |
| // Grab the source for the particular test |
| testSource := sourceFile.parts[testIdx] |
| |
| if testSource, err = fix(testSource); err != nil { |
| return err |
| } |
| |
| // 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 |