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