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