// Copyright 2017 The Go 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 test2json import ( "bufio" "bytes" "cmd/internal/script" "cmd/internal/script/scripttest" "context" "encoding/json" "errors" "flag" "fmt" "internal/txtar" "io" "io/fs" "os" "path/filepath" "reflect" "regexp" "strings" "testing" "unicode/utf8" ) var update = flag.Bool("update", false, "rewrite testdata/*.json files") func TestGolden(t *testing.T) { ctx := scripttest.ScriptTestContext(t, context.Background()) engine, env := scripttest.NewEngine(t, nil) files, err := filepath.Glob("testdata/*.test") if err != nil { t.Fatal(err) } for _, file := range files { name := strings.TrimSuffix(filepath.Base(file), ".test") t.Run(name, func(t *testing.T) { orig, err := os.ReadFile(file) if err != nil { t.Fatal(err) } // If there's a corresponding *.src script, execute it srcFile := strings.TrimSuffix(file, ".test") + ".src" if st, err := os.Stat(srcFile); err != nil { if !errors.Is(err, fs.ErrNotExist) { t.Fatal(err) } } else if !st.IsDir() { t.Run("go test", func(t *testing.T) { stdout := runTest(t, ctx, engine, env, srcFile) if *update { t.Logf("rewriting %s", file) if err := os.WriteFile(file, []byte(stdout), 0666); err != nil { t.Fatal(err) } orig = []byte(stdout) return } diffRaw(t, []byte(stdout), orig) }) } // Test one line written to c at a time. // Assume that's the most likely to be handled correctly. var buf bytes.Buffer c := NewConverter(&buf, "", 0) in := append([]byte{}, orig...) for _, line := range bytes.SplitAfter(in, []byte("\n")) { writeAndKill(c, line) } c.Close() if *update { js := strings.TrimSuffix(file, ".test") + ".json" t.Logf("rewriting %s", js) if err := os.WriteFile(js, buf.Bytes(), 0666); err != nil { t.Fatal(err) } return } want, err := os.ReadFile(strings.TrimSuffix(file, ".test") + ".json") if err != nil { t.Fatal(err) } diffJSON(t, buf.Bytes(), want) if t.Failed() { // If the line-at-a-time conversion fails, no point testing boundary conditions. return } // Write entire input in bulk. t.Run("bulk", func(t *testing.T) { buf.Reset() c = NewConverter(&buf, "", 0) in = append([]byte{}, orig...) writeAndKill(c, in) c.Close() diffJSON(t, buf.Bytes(), want) }) // In bulk again with \r\n. t.Run("crlf", func(t *testing.T) { buf.Reset() c = NewConverter(&buf, "", 0) in = bytes.ReplaceAll(orig, []byte("\n"), []byte("\r\n")) writeAndKill(c, in) c.Close() diffJSON(t, bytes.ReplaceAll(buf.Bytes(), []byte(`\r\n`), []byte(`\n`)), want) }) // Write 2 bytes at a time on even boundaries. t.Run("even2", func(t *testing.T) { buf.Reset() c = NewConverter(&buf, "", 0) in = append([]byte{}, orig...) for i := 0; i < len(in); i += 2 { if i+2 <= len(in) { writeAndKill(c, in[i:i+2]) } else { writeAndKill(c, in[i:]) } } c.Close() diffJSON(t, buf.Bytes(), want) }) // Write 2 bytes at a time on odd boundaries. t.Run("odd2", func(t *testing.T) { buf.Reset() c = NewConverter(&buf, "", 0) in = append([]byte{}, orig...) if len(in) > 0 { writeAndKill(c, in[:1]) } for i := 1; i < len(in); i += 2 { if i+2 <= len(in) { writeAndKill(c, in[i:i+2]) } else { writeAndKill(c, in[i:]) } } c.Close() diffJSON(t, buf.Bytes(), want) }) // Test with very small output buffers, to check that // UTF8 sequences are not broken up. for b := 5; b <= 8; b++ { t.Run(fmt.Sprintf("tiny%d", b), func(t *testing.T) { oldIn := inBuffer oldOut := outBuffer defer func() { inBuffer = oldIn outBuffer = oldOut }() inBuffer = 64 outBuffer = b buf.Reset() c = NewConverter(&buf, "", 0) in = append([]byte{}, orig...) writeAndKill(c, in) c.Close() diffJSON(t, buf.Bytes(), want) }) } }) } } func runTest(t *testing.T, ctx context.Context, engine *script.Engine, env []string, srcFile string) string { workdir := t.TempDir() s, err := script.NewState(ctx, workdir, env) if err != nil { t.Fatal(err) } // Unpack archive. a, err := txtar.ParseFile(srcFile) if err != nil { t.Fatal(err) } scripttest.InitScriptDirs(t, s) if err := s.ExtractFiles(a); err != nil { t.Fatal(err) } err, stdout := func() (err error, stdout string) { log := new(strings.Builder) // Defer writing to the test log in case the script engine panics during execution, // but write the log before we write the final "skip" or "FAIL" line. t.Helper() defer func() { t.Helper() stdout = s.Stdout() if closeErr := s.CloseAndWait(log); err == nil { err = closeErr } if log.Len() > 0 && (testing.Verbose() || err != nil) { t.Log(strings.TrimSuffix(log.String(), "\n")) } }() if testing.Verbose() { // Add the environment to the start of the script log. wait, err := script.Env().Run(s) if err != nil { t.Fatal(err) } if wait != nil { stdout, stderr, err := wait(s) if err != nil { t.Fatalf("env: %v\n%s", err, stderr) } if len(stdout) > 0 { s.Logf("%s\n", stdout) } } } testScript := bytes.NewReader(a.Comment) err = engine.Execute(s, srcFile, bufio.NewReader(testScript), log) return }() if skip := (scripttest.SkipError{}); errors.As(err, &skip) { t.Skipf("SKIP: %v", skip) } else if err != nil { t.Fatalf("FAIL: %v", err) } // Remove the output after "=== NAME" i := strings.LastIndex(stdout, "\n\x16=== NAME") if i >= 0 { stdout = stdout[:i+1] } return stdout } // writeAndKill writes b to w and then fills b with Zs. // The filling makes sure that if w is holding onto b for // future use, that future use will have obviously wrong data. func writeAndKill(w io.Writer, b []byte) { w.Write(b) for i := range b { b[i] = 'Z' } } // diffJSON diffs the stream we have against the stream we want // and fails the test with a useful message if they don't match. func diffJSON(t *testing.T, have, want []byte) { t.Helper() type event map[string]any // Parse into events, one per line. parseEvents := func(b []byte) ([]event, []string) { t.Helper() var events []event var lines []string for _, line := range bytes.SplitAfter(b, []byte("\n")) { if len(line) > 0 { line = bytes.TrimSpace(line) var e event err := json.Unmarshal(line, &e) if err != nil { t.Errorf("unmarshal %s: %v", b, err) continue } events = append(events, e) lines = append(lines, string(line)) } } return events, lines } haveEvents, haveLines := parseEvents(have) wantEvents, wantLines := parseEvents(want) if t.Failed() { return } // Make sure the events we have match the events we want. // At each step we're matching haveEvents[i] against wantEvents[j]. // i and j can move independently due to choices about exactly // how to break up text in "output" events. i := 0 j := 0 // Fail reports a failure at the current i,j and stops the test. // It shows the events around the current positions, // with the current positions marked. fail := func() { var buf bytes.Buffer show := func(i int, lines []string) { for k := -2; k < 5; k++ { marker := "" if k == 0 { marker = "» " } if 0 <= i+k && i+k < len(lines) { fmt.Fprintf(&buf, "\t%s%s\n", marker, lines[i+k]) } } if i >= len(lines) { // show marker after end of input fmt.Fprintf(&buf, "\t» \n") } } fmt.Fprintf(&buf, "have:\n") show(i, haveLines) fmt.Fprintf(&buf, "want:\n") show(j, wantLines) t.Fatal(buf.String()) } var outputTest string // current "Test" key in "output" events var wantOutput, haveOutput string // collected "Output" of those events // getTest returns the "Test" setting, or "" if it is missing. getTest := func(e event) string { s, _ := e["Test"].(string) return s } // checkOutput collects output from the haveEvents for the current outputTest // and then checks that the collected output matches the wanted output. checkOutput := func() { for i < len(haveEvents) && haveEvents[i]["Action"] == "output" && getTest(haveEvents[i]) == outputTest { haveOutput += haveEvents[i]["Output"].(string) i++ } if haveOutput != wantOutput { t.Errorf("output mismatch for Test=%q:\nhave %q\nwant %q", outputTest, haveOutput, wantOutput) fail() } haveOutput = "" wantOutput = "" } // Walk through wantEvents matching against haveEvents. for j = range wantEvents { e := wantEvents[j] if e["Action"] == "output" && getTest(e) == outputTest { wantOutput += e["Output"].(string) continue } checkOutput() if e["Action"] == "output" { outputTest = getTest(e) wantOutput += e["Output"].(string) continue } if i >= len(haveEvents) { t.Errorf("early end of event stream: missing event") fail() } if !reflect.DeepEqual(haveEvents[i], e) { t.Errorf("events out of sync") fail() } i++ } checkOutput() if i < len(haveEvents) { t.Errorf("extra events in stream") fail() } } var reRuntime = regexp.MustCompile(`\d*\.\d*s`) func diffRaw(t *testing.T, have, want []byte) { have = bytes.TrimSpace(have) want = bytes.TrimSpace(want) // Replace durations (e.g. 0.01s) with a placeholder have = reRuntime.ReplaceAll(have, []byte("X.XXs")) want = reRuntime.ReplaceAll(want, []byte("X.XXs")) // Compare if bytes.Equal(have, want) { return } // Escape non-printing characters to make the error more legible have = escapeNonPrinting(have) want = escapeNonPrinting(want) // Find where the output differs and remember the last newline var i, nl int for i < len(have) && i < len(want) && have[i] == want[i] { if have[i] == '\n' { nl = i } } if nl == 0 { t.Fatalf("\nhave:\n%s\nwant:\n%s", have, want) } else { nl++ t.Fatalf("\nhave:\n%s» %s\nwant:\n%s» %s", have[:nl], have[nl:], want[:nl], want[nl:]) } } func escapeNonPrinting(buf []byte) []byte { for i := 0; i < len(buf); i++ { c := buf[i] if 0x20 <= c && c < 0x7F || c > 0x7F || c == '\n' { continue } escaped := fmt.Sprintf(`\x%02x`, c) buf = append(buf[:i+len(escaped)], buf[i+1:]...) for j := 0; j < len(escaped); j++ { buf[i+j] = escaped[j] } } return buf } func TestTrimUTF8(t *testing.T) { s := "hello α ☺ 😂 world" // α is 2-byte, ☺ is 3-byte, 😂 is 4-byte b := []byte(s) for i := 0; i < len(s); i++ { j := trimUTF8(b[:i]) u := string([]rune(s[:j])) + string([]rune(s[j:])) if u != s { t.Errorf("trimUTF8(%q) = %d (-%d), not at boundary (split: %q %q)", s[:i], j, i-j, s[:j], s[j:]) } if utf8.FullRune(b[j:i]) { t.Errorf("trimUTF8(%q) = %d (-%d), too early (missed: %q)", s[:j], j, i-j, s[j:i]) } } }