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