• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package main
6
7import (
8	"bytes"
9	"encoding/json"
10	"flag"
11	"io"
12	"io/ioutil"
13	"log"
14	"os"
15	"path/filepath"
16	"regexp"
17	"strings"
18)
19
20var updateGoldenFiles = flag.Bool("updategolden", false, "update golden files")
21var filterGoldenTests = flag.String("rungolden", "", "regex filter for golden tests to run")
22
23type goldenFile struct {
24	Name    string         `json:"name"`
25	Records []goldenRecord `json:"records"`
26}
27
28type goldenRecord struct {
29	Wd  string   `json:"wd"`
30	Env []string `json:"env,omitempty"`
31	// runGoldenRecords will read cmd and fill
32	// stdout, stderr, exitCode.
33	WrapperCmd commandResult `json:"wrapper"`
34	// runGoldenRecords will read stdout, stderr, err
35	// and fill cmd
36	Cmds []commandResult `json:"cmds"`
37}
38
39func newGoldenCmd(path string, args ...string) commandResult {
40	return commandResult{
41		Cmd: &command{
42			Path: path,
43			Args: args,
44		},
45	}
46}
47
48var okResult = commandResult{}
49var okResults = []commandResult{okResult}
50var errorResult = commandResult{
51	ExitCode: 1,
52	Stderr:   "someerror",
53	Stdout:   "somemessage",
54}
55var errorResults = []commandResult{errorResult}
56
57func runGoldenRecords(ctx *testContext, goldenDir string, files []goldenFile) {
58	if filterPattern := *filterGoldenTests; filterPattern != "" {
59		files = filterGoldenRecords(filterPattern, files)
60	}
61	if len(files) == 0 {
62		ctx.t.Errorf("No goldenrecords given.")
63		return
64	}
65	files = fillGoldenResults(ctx, files)
66	if *updateGoldenFiles {
67		log.Printf("updating golden files under %s", goldenDir)
68		if err := os.MkdirAll(goldenDir, 0777); err != nil {
69			ctx.t.Fatal(err)
70		}
71		for _, file := range files {
72			fileHandle, err := os.Create(filepath.Join(goldenDir, file.Name))
73			if err != nil {
74				ctx.t.Fatal(err)
75			}
76			defer fileHandle.Close()
77
78			writeGoldenRecords(ctx, fileHandle, file.Records)
79		}
80	} else {
81		for _, file := range files {
82			compareBuffer := &bytes.Buffer{}
83			writeGoldenRecords(ctx, compareBuffer, file.Records)
84			filePath := filepath.Join(goldenDir, file.Name)
85			goldenFileData, err := ioutil.ReadFile(filePath)
86			if err != nil {
87				ctx.t.Error(err)
88				continue
89			}
90			if !bytes.Equal(compareBuffer.Bytes(), goldenFileData) {
91				ctx.t.Errorf("Commands don't match the golden file under %s. Please regenerate via -updategolden to check the differences.",
92					filePath)
93			}
94		}
95	}
96}
97
98func filterGoldenRecords(pattern string, files []goldenFile) []goldenFile {
99	matcher := regexp.MustCompile(pattern)
100	newFiles := []goldenFile{}
101	for _, file := range files {
102		newRecords := []goldenRecord{}
103		for _, record := range file.Records {
104			cmd := record.WrapperCmd.Cmd
105			str := strings.Join(append(append(record.Env, cmd.Path), cmd.Args...), " ")
106			if matcher.MatchString(str) {
107				newRecords = append(newRecords, record)
108			}
109		}
110		file.Records = newRecords
111		newFiles = append(newFiles, file)
112	}
113	return newFiles
114}
115
116func fillGoldenResults(ctx *testContext, files []goldenFile) []goldenFile {
117	newFiles := []goldenFile{}
118	for _, file := range files {
119		newRecords := []goldenRecord{}
120		for _, record := range file.Records {
121			newCmds := []commandResult{}
122			ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
123				if len(newCmds) >= len(record.Cmds) {
124					ctx.t.Errorf("Not enough commands specified for wrapperCmd %#v and env %#v. Expected: %#v",
125						record.WrapperCmd.Cmd, record.Env, record.Cmds)
126					return nil
127				}
128				cmdResult := record.Cmds[len(newCmds)]
129				cmdResult.Cmd = cmd
130				if numEnvUpdates := len(cmdResult.Cmd.EnvUpdates); numEnvUpdates > 0 {
131					if strings.HasPrefix(cmdResult.Cmd.EnvUpdates[numEnvUpdates-1], "PYTHONPATH") {
132						cmdResult.Cmd.EnvUpdates[numEnvUpdates-1] = "PYTHONPATH=/somepath/test_binary"
133					}
134				}
135				newCmds = append(newCmds, cmdResult)
136				io.WriteString(stdout, cmdResult.Stdout)
137				io.WriteString(stderr, cmdResult.Stderr)
138				if cmdResult.ExitCode != 0 {
139					return newExitCodeError(cmdResult.ExitCode)
140				}
141				return nil
142			}
143			ctx.stdoutBuffer.Reset()
144			ctx.stderrBuffer.Reset()
145			ctx.env = record.Env
146			if record.Wd == "" {
147				record.Wd = ctx.tempDir
148			}
149			ctx.wd = record.Wd
150			// Create an empty wrapper at the given path.
151			// Needed as we are resolving symlinks which stats the wrapper file.
152			ctx.writeFile(record.WrapperCmd.Cmd.Path, "")
153			record.WrapperCmd.ExitCode = callCompiler(ctx, ctx.cfg, record.WrapperCmd.Cmd)
154			if hasInternalError(ctx.stderrString()) {
155				ctx.t.Errorf("found an internal error for wrapperCmd %#v and env #%v. Got: %s",
156					record.WrapperCmd.Cmd, record.Env, ctx.stderrString())
157			}
158			if len(newCmds) < len(record.Cmds) {
159				ctx.t.Errorf("Too many commands specified for wrapperCmd %#v and env %#v. Expected: %#v",
160					record.WrapperCmd.Cmd, record.Env, record.Cmds)
161			}
162			record.Cmds = newCmds
163			record.WrapperCmd.Stdout = ctx.stdoutString()
164			record.WrapperCmd.Stderr = ctx.stderrString()
165			newRecords = append(newRecords, record)
166		}
167		file.Records = newRecords
168		newFiles = append(newFiles, file)
169	}
170	return newFiles
171}
172
173func writeGoldenRecords(ctx *testContext, writer io.Writer, records []goldenRecord) {
174	// Replace the temp dir with a stable path so that the goldens stay stable.
175	stableTempDir := filepath.Join(filepath.Dir(ctx.tempDir), "stable")
176	writer = &replacingWriter{
177		Writer: writer,
178		old:    [][]byte{[]byte(ctx.tempDir)},
179		new:    [][]byte{[]byte(stableTempDir)},
180	}
181	enc := json.NewEncoder(writer)
182	enc.SetIndent("", "  ")
183	if err := enc.Encode(records); err != nil {
184		ctx.t.Fatal(err)
185	}
186}
187
188type replacingWriter struct {
189	io.Writer
190	old [][]byte
191	new [][]byte
192}
193
194func (writer *replacingWriter) Write(p []byte) (n int, err error) {
195	// TODO: Use bytes.ReplaceAll once cros sdk uses golang >= 1.12
196	for i, old := range writer.old {
197		p = bytes.Replace(p, old, writer.new[i], -1)
198	}
199	return writer.Writer.Write(p)
200}
201