• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 The Tint Authors.
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// fix-tests is a tool to update tests with new expected output.
16package main
17
18import (
19	"encoding/json"
20	"flag"
21	"fmt"
22	"io/ioutil"
23	"os"
24	"os/exec"
25	"path/filepath"
26	"regexp"
27	"strings"
28
29	"dawn.googlesource.com/tint/tools/src/substr"
30)
31
32func main() {
33	if err := run(); err != nil {
34		fmt.Println(err)
35		os.Exit(1)
36	}
37}
38
39func showUsage() {
40	fmt.Println(`
41fix-tests is a tool to update tests with new expected output.
42
43fix-tests performs string matching and heuristics to fix up expected results of
44tests that use EXPECT_EQ(a, b) and EXPECT_THAT(a, HasSubstr(b))
45
46WARNING: Always thoroughly check the generated output for mistakes.
47This may produce incorrect output
48
49Usage:
50  fix-tests <executable>
51
52  executable         - the path to the test executable to run.`)
53	os.Exit(1)
54}
55
56func run() error {
57	flag.Parse()
58	args := flag.Args()
59	if len(args) < 1 {
60		showUsage()
61	}
62
63	exe := args[0]          // The path to the test executable
64	wd := filepath.Dir(exe) // The directory holding the test exe
65
66	// Create a temporary directory to hold the 'test-results.json' file
67	tmpDir, err := ioutil.TempDir("", "fix-tests")
68	if err != nil {
69		return err
70	}
71	if err := os.MkdirAll(tmpDir, 0666); err != nil {
72		return fmt.Errorf("Failed to create temporary directory: %w", err)
73	}
74	defer os.RemoveAll(tmpDir)
75
76	// Full path to the 'test-results.json' in the temporary directory
77	testResultsPath := filepath.Join(tmpDir, "test-results.json")
78
79	// Run the tests
80	testArgs := []string{"--gtest_output=json:" + testResultsPath}
81	if len(args) > 1 {
82		testArgs = append(testArgs, args[1:]...)
83	}
84	switch err := exec.Command(exe, testArgs...).Run().(type) {
85	default:
86		return err
87	case nil:
88		fmt.Println("All tests passed")
89	case *exec.ExitError:
90	}
91
92	// Read the 'test-results.json' file
93	testResultsFile, err := os.Open(testResultsPath)
94	if err != nil {
95		return err
96	}
97
98	var testResults Results
99	if err := json.NewDecoder(testResultsFile).Decode(&testResults); err != nil {
100		return err
101	}
102
103	// For each failing test...
104	seen := map[string]bool{}
105	numFixed, numFailed := 0, 0
106	for _, group := range testResults.Groups {
107		for _, suite := range group.Testsuites {
108			for _, failure := range suite.Failures {
109				// .. attempt to fix the problem
110				test := testName(group, suite)
111				if seen[test] {
112					continue
113				}
114				seen[test] = true
115
116				if err := processFailure(test, wd, failure.Failure); err != nil {
117					fmt.Println(fmt.Errorf("%v: %w", test, err))
118					numFailed++
119				} else {
120					numFixed++
121				}
122			}
123		}
124	}
125
126	fmt.Println()
127
128	if numFailed > 0 {
129		fmt.Println(numFailed, "tests could not be fixed")
130	}
131	if numFixed > 0 {
132		fmt.Println(numFixed, "tests fixed")
133	}
134	return nil
135}
136
137func testName(group TestsuiteGroup, suite Testsuite) string {
138	groupParts := strings.Split(group.Name, "/")
139	suiteParts := strings.Split(suite.Name, "/")
140	return groupParts[len(groupParts)-1] + "." + suiteParts[0]
141}
142
143var (
144	// Regular expression to match a test declaration
145	reTests = regexp.MustCompile(`TEST(?:_[FP])?\([ \n]*(\w+),[ \n]*(\w+)\)`)
146	// Regular expression to match a `EXPECT_EQ(a, b)` failure for strings
147	reExpectEq = regexp.MustCompile(`([./\\\w_\-:]*):(\d+).*\nExpected equality of these values:\n(?:.|\n)*?(?:Which is: |  )"((?:.|\n)*?[^\\])"\n(?:.|\n)*?(?:Which is: |  )"((?:.|\n)*?[^\\])"`)
148	// Regular expression to match a `EXPECT_THAT(a, HasSubstr(b))` failure for strings
149	reExpectHasSubstr = regexp.MustCompile(`([./\\\w_\-:]*):(\d+).*\nValue of: .*\nExpected: has substring "((?:.|\n)*?[^\\])"\n  Actual: "((?:.|\n)*?[^\\])"`)
150)
151
152func processFailure(test, wd, failure string) error {
153	// Start by un-escaping newlines in the failure message
154	failure = strings.ReplaceAll(failure, "\\n", "\n")
155	// Matched regex strings will also need to be un-escaped, but do this after
156	// the match, as unescaped quotes may upset the regex patterns
157	unescape := func(s string) string {
158		return strings.ReplaceAll(s, `\"`, `"`)
159	}
160	escape := func(s string) string {
161		s = strings.ReplaceAll(s, "\n", `\n`)
162		s = strings.ReplaceAll(s, "\"", `\"`)
163		return s
164	}
165
166	// Look for a EXPECT_EQ failure pattern
167	var file string
168	var fix func(testSource string) (string, error)
169	if parts := reExpectEq.FindStringSubmatch(failure); len(parts) == 5 {
170		// EXPECT_EQ(a, b)
171		a, b := unescape(parts[3]), unescape(parts[4])
172		file = parts[1]
173		fix = func(testSource string) (string, error) {
174			// We don't know if a or b is the expected, so just try flipping the string
175			// to the other form.
176
177			if len(b) > len(a) { // Go with the longer match, in case both are found
178				a, b = b, a
179			}
180			switch {
181			case strings.Contains(testSource, a):
182				testSource = strings.ReplaceAll(testSource, a, b)
183			case strings.Contains(testSource, b):
184				testSource = strings.ReplaceAll(testSource, b, a)
185			default:
186				// Try escaping for R"(...)" strings
187				a, b = escape(a), escape(b)
188				switch {
189				case strings.Contains(testSource, a):
190					testSource = strings.ReplaceAll(testSource, a, b)
191				case strings.Contains(testSource, b):
192					testSource = strings.ReplaceAll(testSource, b, a)
193				default:
194					return "", fmt.Errorf("Could not fix 'EXPECT_EQ' pattern in '%v'", file)
195				}
196			}
197			return testSource, nil
198		}
199	} else if parts := reExpectHasSubstr.FindStringSubmatch(failure); len(parts) == 5 {
200		// EXPECT_THAT(a, HasSubstr(b))
201		a, b := unescape(parts[4]), unescape(parts[3])
202		file = parts[1]
203		fix = func(testSource string) (string, error) {
204			if fix := substr.Fix(a, b); fix != "" {
205				if !strings.Contains(testSource, b) {
206					// Try escaping for R"(...)" strings
207					b, fix = escape(b), escape(fix)
208				}
209				if strings.Contains(testSource, b) {
210					testSource = strings.Replace(testSource, b, fix, -1)
211					return testSource, nil
212				}
213				return "", fmt.Errorf("Could apply fix for 'HasSubstr' pattern in '%v'", file)
214			}
215
216			return "", fmt.Errorf("Could find fix for 'HasSubstr' pattern in '%v'", file)
217		}
218	} else {
219		return fmt.Errorf("Cannot fix this type of failure")
220	}
221
222	// Get the absolute source path
223	sourcePath := file
224	if !filepath.IsAbs(sourcePath) {
225		sourcePath = filepath.Join(wd, file)
226	}
227
228	// Parse the source file, split into tests
229	sourceFile, err := parseSourceFile(sourcePath)
230	if err != nil {
231		return fmt.Errorf("Couldn't parse tests from file '%v': %w", file, err)
232	}
233
234	// Find the test
235	testIdx, ok := sourceFile.tests[test]
236	if !ok {
237		return fmt.Errorf("Test not found in '%v'", file)
238	}
239
240	// Grab the source for the particular test
241	testSource := sourceFile.parts[testIdx]
242
243	if testSource, err = fix(testSource); err != nil {
244		return err
245	}
246
247	// Replace the part of the source file
248	sourceFile.parts[testIdx] = testSource
249
250	// Write out the source file
251	return writeSourceFile(sourcePath, sourceFile)
252}
253
254// parseSourceFile() reads the file at path, splitting the content into chunks
255// for each TEST.
256func parseSourceFile(path string) (sourceFile, error) {
257	fileBytes, err := ioutil.ReadFile(path)
258	if err != nil {
259		return sourceFile{}, err
260	}
261	fileContent := string(fileBytes)
262
263	out := sourceFile{
264		tests: map[string]int{},
265	}
266
267	pos := 0
268	for _, span := range reTests.FindAllStringIndex(fileContent, -1) {
269		out.parts = append(out.parts, fileContent[pos:span[0]])
270		pos = span[0]
271
272		match := reTests.FindStringSubmatch(fileContent[span[0]:span[1]])
273		group := match[1]
274		suite := match[2]
275		out.tests[group+"."+suite] = len(out.parts)
276	}
277	out.parts = append(out.parts, fileContent[pos:])
278
279	return out, nil
280}
281
282// writeSourceFile() joins the chunks of the file, and writes the content out to
283// path.
284func writeSourceFile(path string, file sourceFile) error {
285	body := strings.Join(file.parts, "")
286	return ioutil.WriteFile(path, []byte(body), 0666)
287}
288
289type sourceFile struct {
290	parts []string
291	tests map[string]int // "X.Y" -> part index
292}
293
294// Results is the root JSON structure of the JSON --gtest_output file .
295type Results struct {
296	Tests     int              `json:"tests"`
297	Failures  int              `json:"failures"`
298	Disabled  int              `json:"disabled"`
299	Errors    int              `json:"errors"`
300	Timestamp string           `json:"timestamp"`
301	Time      string           `json:"time"`
302	Name      string           `json:"name"`
303	Groups    []TestsuiteGroup `json:"testsuites"`
304}
305
306// TestsuiteGroup is a group of test suites in the JSON --gtest_output file .
307type TestsuiteGroup struct {
308	Name       string      `json:"name"`
309	Tests      int         `json:"tests"`
310	Failures   int         `json:"failures"`
311	Disabled   int         `json:"disabled"`
312	Errors     int         `json:"errors"`
313	Timestamp  string      `json:"timestamp"`
314	Time       string      `json:"time"`
315	Testsuites []Testsuite `json:"testsuite"`
316}
317
318// Testsuite is a suite of tests in the JSON --gtest_output file.
319type Testsuite struct {
320	Name       string    `json:"name"`
321	ValueParam string    `json:"value_param,omitempty"`
322	Status     Status    `json:"status"`
323	Result     Result    `json:"result"`
324	Timestamp  string    `json:"timestamp"`
325	Time       string    `json:"time"`
326	Classname  string    `json:"classname"`
327	Failures   []Failure `json:"failures,omitempty"`
328}
329
330// Failure is a reported test failure in the JSON --gtest_output file.
331type Failure struct {
332	Failure string `json:"failure"`
333	Type    string `json:"type"`
334}
335
336// Status is a status code in the JSON --gtest_output file.
337type Status string
338
339// Result is a result code in the JSON --gtest_output file.
340type Result string
341