xref: /aosp_15_r20/external/toolchain-utils/compiler_wrapper/goldenutil_test.go (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1// Copyright 2019 The ChromiumOS Authors
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	"fmt"
12	"io"
13	"io/ioutil"
14	"log"
15	"os"
16	"path/filepath"
17	"regexp"
18	"strings"
19)
20
21var updateGoldenFiles = flag.Bool("updategolden", false, "update golden files")
22var filterGoldenTests = flag.String("rungolden", "", "regex filter for golden tests to run")
23
24type goldenFile struct {
25	Name    string         `json:"name"`
26	Records []goldenRecord `json:"records"`
27}
28
29type goldenRecord struct {
30	Wd  string   `json:"wd"`
31	Env []string `json:"env,omitempty"`
32	// runGoldenRecords will read cmd and fill
33	// stdout, stderr, exitCode.
34	WrapperCmd commandResult `json:"wrapper"`
35	// runGoldenRecords will read stdout, stderr, err
36	// and fill cmd
37	Cmds []commandResult `json:"cmds"`
38}
39
40func newGoldenCmd(path string, args ...string) commandResult {
41	return commandResult{
42		Cmd: &command{
43			Path: path,
44			Args: args,
45		},
46	}
47}
48
49var okResult = commandResult{}
50var okResults = []commandResult{okResult}
51var errorResult = commandResult{
52	ExitCode: 1,
53	Stderr:   "someerror",
54	Stdout:   "somemessage",
55}
56var errorResults = []commandResult{errorResult}
57
58func runGoldenRecords(ctx *testContext, goldenDir string, files []goldenFile) {
59	if filterPattern := *filterGoldenTests; filterPattern != "" {
60		files = filterGoldenRecords(filterPattern, files)
61	}
62	if len(files) == 0 {
63		ctx.t.Errorf("No goldenrecords given.")
64		return
65	}
66	files = fillGoldenResults(ctx, files)
67	if *updateGoldenFiles {
68		log.Printf("updating golden files under %s", goldenDir)
69		if err := os.MkdirAll(goldenDir, 0777); err != nil {
70			ctx.t.Fatal(err)
71		}
72		for _, file := range files {
73			fileHandle, err := os.Create(filepath.Join(goldenDir, file.Name))
74			if err != nil {
75				ctx.t.Fatal(err)
76			}
77			defer fileHandle.Close()
78
79			writeGoldenRecords(ctx, fileHandle, file.Records)
80		}
81	} else {
82		for _, file := range files {
83			compareBuffer := &bytes.Buffer{}
84			writeGoldenRecords(ctx, compareBuffer, file.Records)
85			filePath := filepath.Join(goldenDir, file.Name)
86			goldenFileData, err := ioutil.ReadFile(filePath)
87			if err != nil {
88				ctx.t.Error(err)
89				continue
90			}
91			if !bytes.Equal(compareBuffer.Bytes(), goldenFileData) {
92				ctx.t.Errorf("Commands don't match the golden file under %s. Please regenerate via -updategolden to check the differences.",
93					filePath)
94			}
95		}
96	}
97}
98
99func filterGoldenRecords(pattern string, files []goldenFile) []goldenFile {
100	matcher := regexp.MustCompile(pattern)
101	newFiles := []goldenFile{}
102	for _, file := range files {
103		newRecords := []goldenRecord{}
104		for _, record := range file.Records {
105			cmd := record.WrapperCmd.Cmd
106			str := strings.Join(append(append(record.Env, cmd.Path), cmd.Args...), " ")
107			if matcher.MatchString(str) {
108				newRecords = append(newRecords, record)
109			}
110		}
111		file.Records = newRecords
112		newFiles = append(newFiles, file)
113	}
114	return newFiles
115}
116
117func fillGoldenResults(ctx *testContext, files []goldenFile) []goldenFile {
118	newFiles := []goldenFile{}
119	for _, file := range files {
120		newRecords := []goldenRecord{}
121		for _, record := range file.Records {
122			newCmds := []commandResult{}
123			ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
124				if len(newCmds) >= len(record.Cmds) {
125					ctx.t.Errorf("Not enough commands specified for wrapperCmd %#v and env %#v. Expected: %#v",
126						record.WrapperCmd.Cmd, record.Env, record.Cmds)
127					return nil
128				}
129				cmdResult := record.Cmds[len(newCmds)]
130				cmdResult.Cmd = cmd
131				if numEnvUpdates := len(cmdResult.Cmd.EnvUpdates); numEnvUpdates > 0 {
132					if strings.HasPrefix(cmdResult.Cmd.EnvUpdates[numEnvUpdates-1], "PYTHONPATH") {
133						cmdResult.Cmd.EnvUpdates[numEnvUpdates-1] = "PYTHONPATH=/somepath/test_binary"
134					}
135				}
136				newCmds = append(newCmds, cmdResult)
137				io.WriteString(stdout, cmdResult.Stdout)
138				io.WriteString(stderr, cmdResult.Stderr)
139				if cmdResult.ExitCode != 0 {
140					return newExitCodeError(cmdResult.ExitCode)
141				}
142				return nil
143			}
144			ctx.stdoutBuffer.Reset()
145			ctx.stderrBuffer.Reset()
146			ctx.env = record.Env
147			if record.Wd == "" {
148				record.Wd = ctx.tempDir
149			}
150			ctx.wd = record.Wd
151			// Create an empty wrapper at the given path.
152			// Needed as we are resolving symlinks which stats the wrapper file.
153			ctx.writeFile(record.WrapperCmd.Cmd.Path, "")
154			record.WrapperCmd.ExitCode = callCompiler(ctx, ctx.cfg, record.WrapperCmd.Cmd)
155			if hasInternalError(ctx.stderrString()) {
156				ctx.t.Errorf("found an internal error for wrapperCmd %#v and env #%v. Got: %s",
157					record.WrapperCmd.Cmd, record.Env, ctx.stderrString())
158			}
159			if len(newCmds) < len(record.Cmds) {
160				ctx.t.Errorf("Too many commands specified for wrapperCmd %#v and env %#v. Expected: %#v",
161					record.WrapperCmd.Cmd, record.Env, record.Cmds)
162			}
163			record.Cmds = newCmds
164			record.WrapperCmd.Stdout = ctx.stdoutString()
165			record.WrapperCmd.Stderr = ctx.stderrString()
166			newRecords = append(newRecords, record)
167		}
168		file.Records = newRecords
169		newFiles = append(newFiles, file)
170	}
171	return newFiles
172}
173
174func writeGoldenRecords(ctx *testContext, writer io.Writer, records []goldenRecord) {
175	// We need to rewrite /tmp/${test_specific_tmpdir} records as /tmp/stable, so it's
176	// deterministic across reruns. Round-trip this through JSON so there's no need to maintain
177	// logic that hunts through `record`s. A side-benefit of round-tripping through a JSON `map`
178	// is that `encoding/json` sorts JSON map keys, and `cros format` complains if keys aren't
179	// sorted.
180	encoded, err := json.Marshal(records)
181	if err != nil {
182		ctx.t.Fatal(err)
183	}
184
185	decoded := interface{}(nil)
186	if err := json.Unmarshal(encoded, &decoded); err != nil {
187		ctx.t.Fatal(err)
188	}
189
190	stableTempDir := filepath.Join(filepath.Dir(ctx.tempDir), "stable")
191	decoded, err = dfsJSONValues(decoded, func(i interface{}) interface{} {
192		asString, ok := i.(string)
193		if !ok {
194			return i
195		}
196		return strings.ReplaceAll(asString, ctx.tempDir, stableTempDir)
197	})
198
199	encoder := json.NewEncoder(writer)
200	encoder.SetIndent("", "  ")
201	if err := encoder.Encode(decoded); err != nil {
202		ctx.t.Fatal(err)
203	}
204}
205
206// Performs a DFS on `decodedJSON`, replacing elements with the result of calling `mapFunc()` on
207// each value. Only returns an error if an element type is unexpected (read: the input JSON should
208// only contain the types listed for unmarshalling as an interface value here
209// https://pkg.go.dev/encoding/json#Unmarshal).
210//
211// Two subtleties:
212//  1. This calls `mapFunc()` on nested values after the transformation of their individual elements.
213//     Moreover, given the JSON `[1, 2]` and a mapFunc that just returns nil, the mapFunc will be
214//     called as `mapFunc(1)`, then `mapFunc(2)`, then `mapFunc({}interface{nil, nil})`.
215//  2. This is not called directly on keys in maps. If you want to transform keys, you may do so when
216//     `mapFunc` is called on a `map[string]interface{}`. This is to make differentiating between
217//     keys and values easier.
218func dfsJSONValues(decodedJSON interface{}, mapFunc func(interface{}) interface{}) (interface{}, error) {
219	if decodedJSON == nil {
220		return mapFunc(nil), nil
221	}
222
223	switch d := decodedJSON.(type) {
224	case bool, float64, string:
225		return mapFunc(decodedJSON), nil
226
227	case []interface{}:
228		newSlice := make([]interface{}, len(d))
229		for i, elem := range d {
230			transformed, err := dfsJSONValues(elem, mapFunc)
231			if err != nil {
232				return nil, err
233			}
234			newSlice[i] = transformed
235		}
236		return mapFunc(newSlice), nil
237
238	case map[string]interface{}:
239		newMap := make(map[string]interface{}, len(d))
240		for k, v := range d {
241			transformed, err := dfsJSONValues(v, mapFunc)
242			if err != nil {
243				return nil, err
244			}
245			newMap[k] = transformed
246		}
247		return mapFunc(newMap), nil
248
249	default:
250		return nil, fmt.Errorf("unexpected type in JSON: %T", decodedJSON)
251	}
252}
253