// Copyright 2019 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package main import ( "bytes" "encoding/json" "flag" "io" "io/ioutil" "log" "os" "path/filepath" "regexp" "strings" ) var updateGoldenFiles = flag.Bool("updategolden", false, "update golden files") var filterGoldenTests = flag.String("rungolden", "", "regex filter for golden tests to run") type goldenRecordSection struct { Name string `json:"name"` ignoreOldWrapper bool Records []goldenRecord `json:"records"` } type goldenRecord struct { Wd string `json:"wd"` Env []string `json:"env,omitempty"` // runGoldenRecords will read cmd and fill // stdout, stderr, exitCode. WrapperCmd commandResult `json:"wrapper"` // runGoldenRecords will read stdout, stderr, err // and fill cmd Cmds []commandResult `json:"cmds"` } func newGoldenCmd(path string, args ...string) commandResult { return commandResult{ Cmd: &command{ Path: path, Args: args, }, } } var okResult = commandResult{} var okResults = []commandResult{okResult} var errorResult = commandResult{ ExitCode: 1, Stderr: "someerror", Stdout: "somemessage", } var errorResults = []commandResult{errorResult} func runGoldenRecords(ctx *testContext, goldenFile string, sections []goldenRecordSection) { if filterPattern := *filterGoldenTests; filterPattern != "" { sections = filterGoldenRecords(filterPattern, sections) } if len(sections) == 0 { ctx.t.Errorf("No goldenrecords given.") return } sections = fillGoldenResults(ctx, sections) if *updateGoldenFiles { log.Printf("updating golden file under %s", goldenFile) if err := os.MkdirAll(filepath.Dir(goldenFile), 0777); err != nil { ctx.t.Fatal(err) } goldenFile, err := os.Create(goldenFile) if err != nil { ctx.t.Fatal(err) } defer goldenFile.Close() writeGoldenRecords(ctx, goldenFile, sections) } else { compareBuffer := &bytes.Buffer{} writeGoldenRecords(ctx, compareBuffer, sections) goldenFileData, err := ioutil.ReadFile(goldenFile) if err != nil { ctx.t.Fatal(err) } if !bytes.Equal(compareBuffer.Bytes(), goldenFileData) { ctx.t.Fatalf("Commands don't match the golden file under %s. Please regenerate via -updategolden to check the differences.", goldenFile) } } } func filterGoldenRecords(pattern string, sections []goldenRecordSection) []goldenRecordSection { matcher := regexp.MustCompile(pattern) newSections := []goldenRecordSection{} for _, section := range sections { newRecords := []goldenRecord{} for _, record := range section.Records { cmd := record.WrapperCmd.Cmd str := strings.Join(append(append(record.Env, cmd.Path), cmd.Args...), " ") if matcher.MatchString(str) { newRecords = append(newRecords, record) } } section.Records = newRecords newSections = append(newSections, section) } return newSections } func fillGoldenResults(ctx *testContext, sections []goldenRecordSection) []goldenRecordSection { oldWrapperPath := ctx.cfg.oldWrapperPath newSections := []goldenRecordSection{} for _, section := range sections { ctx.cfg.oldWrapperPath = oldWrapperPath if section.ignoreOldWrapper { ctx.cfg.oldWrapperPath = "" } newRecords := []goldenRecord{} for _, record := range section.Records { newCmds := []commandResult{} ctx.cmdMock = func(cmd *command, stdout io.Writer, stderr io.Writer) error { if len(newCmds) >= len(record.Cmds) { ctx.t.Errorf("Not enough commands specified for wrapperCmd %#v and env %#v. Expected: %#v", record.WrapperCmd.Cmd, record.Env, record.Cmds) return nil } cmdResult := record.Cmds[len(newCmds)] cmdResult.Cmd = cmd newCmds = append(newCmds, cmdResult) io.WriteString(stdout, cmdResult.Stdout) io.WriteString(stderr, cmdResult.Stderr) if cmdResult.ExitCode != 0 { return newExitCodeError(cmdResult.ExitCode) } return nil } ctx.stdoutBuffer.Reset() ctx.stderrBuffer.Reset() ctx.env = record.Env if record.Wd == "" { record.Wd = ctx.tempDir } ctx.wd = record.Wd // Create an empty wrapper at the given path. // Needed as we are resolving symlinks which stats the wrapper file. ctx.writeFile(record.WrapperCmd.Cmd.Path, "") record.WrapperCmd.ExitCode = callCompiler(ctx, ctx.cfg, record.WrapperCmd.Cmd) if hasInternalError(ctx.stderrString()) { ctx.t.Errorf("found an internal error for wrapperCmd %#v and env #%v. Got: %s", record.WrapperCmd.Cmd, record.Env, ctx.stderrString()) } if len(newCmds) < len(record.Cmds) { ctx.t.Errorf("Too many commands specified for wrapperCmd %#v and env %#v. Expected: %#v", record.WrapperCmd.Cmd, record.Env, record.Cmds) } record.Cmds = newCmds record.WrapperCmd.Stdout = ctx.stdoutString() record.WrapperCmd.Stderr = ctx.stderrString() newRecords = append(newRecords, record) } section.Records = newRecords newSections = append(newSections, section) } return newSections } func writeGoldenRecords(ctx *testContext, writer io.Writer, sections []goldenRecordSection) { // Replace the temp dir with a stable path so that the goldens stay stable. stableTempDir := filepath.Join(filepath.Dir(ctx.tempDir), "stable") writer = &replacingWriter{ Writer: writer, old: []byte(ctx.tempDir), new: []byte(stableTempDir), } enc := json.NewEncoder(writer) enc.SetIndent("", " ") if err := enc.Encode(sections); err != nil { ctx.t.Fatal(err) } } type replacingWriter struct { io.Writer old []byte new []byte } func (writer *replacingWriter) Write(p []byte) (n int, err error) { p = bytes.ReplaceAll(p, writer.old, writer.new) return writer.Writer.Write(p) }