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