summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cmd/dist/test.go96
-rw-r--r--src/cmd/dist/testjson.go176
-rw-r--r--src/cmd/dist/testjson_test.go86
3 files changed, 344 insertions, 14 deletions
diff --git a/src/cmd/dist/test.go b/src/cmd/dist/test.go
index 4b33933459..04dfd22f88 100644
--- a/src/cmd/dist/test.go
+++ b/src/cmd/dist/test.go
@@ -6,6 +6,7 @@ package main
import (
"bytes"
+ "encoding/json"
"flag"
"fmt"
"io"
@@ -41,6 +42,7 @@ func cmdtest() {
"Special exception: if the string begins with '!', the match is inverted.")
flag.BoolVar(&t.msan, "msan", false, "run in memory sanitizer builder mode")
flag.BoolVar(&t.asan, "asan", false, "run in address sanitizer builder mode")
+ flag.BoolVar(&t.json, "json", false, "report test results in JSON")
xflagparse(-1) // any number of args
if noRebuild {
@@ -70,6 +72,7 @@ type tester struct {
short bool
cgoEnabled bool
partial bool
+ json bool
tests []distTest // use addTest to extend
testNames map[string]bool
@@ -212,12 +215,14 @@ func (t *tester) run() {
}
}
- if err := t.maybeLogMetadata(); err != nil {
- t.failed = true
- if t.keepGoing {
- log.Printf("Failed logging metadata: %v", err)
- } else {
- fatalf("Failed logging metadata: %v", err)
+ if !t.json {
+ if err := t.maybeLogMetadata(); err != nil {
+ t.failed = true
+ if t.keepGoing {
+ log.Printf("Failed logging metadata: %v", err)
+ } else {
+ fatalf("Failed logging metadata: %v", err)
+ }
}
}
@@ -240,13 +245,17 @@ func (t *tester) run() {
t.runPending(nil)
timelog("end", "dist test")
+ if !t.json {
+ if t.failed {
+ fmt.Println("\nFAILED")
+ } else if t.partial {
+ fmt.Println("\nALL TESTS PASSED (some were excluded)")
+ } else {
+ fmt.Println("\nALL TESTS PASSED")
+ }
+ }
if t.failed {
- fmt.Println("\nFAILED")
xexit(1)
- } else if t.partial {
- fmt.Println("\nALL TESTS PASSED (some were excluded)")
- } else {
- fmt.Println("\nALL TESTS PASSED")
}
}
@@ -302,7 +311,8 @@ type goTest struct {
runOnHost bool // When cross-compiling, run this test on the host instead of guest
// variant, if non-empty, is a name used to distinguish different
- // configurations of the same test package(s).
+ // configurations of the same test package(s). If set and sharded is false,
+ // the Package field in test2json output is rewritten to pkg:variant.
variant string
// sharded indicates that variant is used solely for sharding and that
// the set of test names run by each variant of a package is non-overlapping.
@@ -335,7 +345,31 @@ func (opts *goTest) bgCommand(t *tester, stdout, stderr io.Writer) *exec.Cmd {
cmd := exec.Command(goCmd, args...)
setupCmd(cmd)
- cmd.Stdout = stdout
+ if t.json && opts.variant != "" && !opts.sharded {
+ // Rewrite Package in the JSON output to be pkg:variant. For sharded
+ // variants, pkg.TestName is already unambiguous, so we don't need to
+ // rewrite the Package field.
+ if len(opts.pkgs) != 0 {
+ panic("cannot combine multiple packages with variants")
+ }
+ // We only want to process JSON on the child's stdout. Ideally if
+ // stdout==stderr, we would also use the same testJSONFilter for
+ // cmd.Stdout and cmd.Stderr in order to keep the underlying
+ // interleaving of writes, but then it would see even partial writes
+ // interleaved, which would corrupt the JSON. So, we only process
+ // cmd.Stdout. This has another consequence though: if stdout==stderr,
+ // we have to serialize Writes in case the Writer is not concurrent
+ // safe. If we were just passing stdout/stderr through to exec, it would
+ // do this for us, but since we're wrapping stdout, we have to do it
+ // ourselves.
+ if stdout == stderr {
+ stdout = &lockedWriter{w: stdout}
+ stderr = stdout
+ }
+ cmd.Stdout = &testJSONFilter{w: stdout, variant: opts.variant}
+ } else {
+ cmd.Stdout = stdout
+ }
cmd.Stderr = stderr
return cmd
@@ -403,6 +437,9 @@ func (opts *goTest) buildArgs(t *tester) (goCmd string, build, run, pkgs, testFl
if opts.cpu != "" {
run = append(run, "-cpu="+opts.cpu)
}
+ if t.json {
+ run = append(run, "-json")
+ }
if opts.gcflags != "" {
build = append(build, "-gcflags=all="+opts.gcflags)
@@ -948,7 +985,7 @@ func (t *tester) registerTest(name, heading string, test *goTest, opts ...regist
if skipFunc != nil {
msg, skip := skipFunc(dt)
if skip {
- fmt.Println(msg)
+ t.printSkip(test, msg)
return nil
}
}
@@ -959,6 +996,34 @@ func (t *tester) registerTest(name, heading string, test *goTest, opts ...regist
})
}
+func (t *tester) printSkip(test *goTest, msg string) {
+ if !t.json {
+ fmt.Println(msg)
+ return
+ }
+ type event struct {
+ Time time.Time
+ Action string
+ Package string
+ Output string `json:",omitempty"`
+ }
+ out := json.NewEncoder(os.Stdout)
+ for _, pkg := range test.packages() {
+ variantName := pkg
+ if test.variant != "" {
+ variantName += ":" + test.variant
+ }
+ ev := event{Time: time.Now(), Package: variantName, Action: "start"}
+ out.Encode(ev)
+ ev.Action = "output"
+ ev.Output = msg
+ out.Encode(ev)
+ ev.Action = "skip"
+ ev.Output = ""
+ out.Encode(ev)
+ }
+}
+
// dirCmd constructs a Cmd intended to be run in the foreground.
// The command will be run in dir, and Stdout and Stderr will go to os.Stdout
// and os.Stderr.
@@ -1005,6 +1070,9 @@ func (t *tester) iOS() bool {
}
func (t *tester) out(v string) {
+ if t.json {
+ return
+ }
if t.banner == "" {
return
}
diff --git a/src/cmd/dist/testjson.go b/src/cmd/dist/testjson.go
new file mode 100644
index 0000000000..261b9584ce
--- /dev/null
+++ b/src/cmd/dist/testjson.go
@@ -0,0 +1,176 @@
+// Copyright 2023 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 main
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+)
+
+// lockedWriter serializes Write calls to an underlying Writer.
+type lockedWriter struct {
+ lock sync.Mutex
+ w io.Writer
+}
+
+func (w *lockedWriter) Write(b []byte) (int, error) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+ return w.w.Write(b)
+}
+
+// testJSONFilter is an io.Writer filter that replaces the Package field in
+// test2json output.
+type testJSONFilter struct {
+ w io.Writer // Underlying writer
+ variant string // Add ":variant" to Package field
+
+ lineBuf bytes.Buffer // Buffer for incomplete lines
+}
+
+func (f *testJSONFilter) Write(b []byte) (int, error) {
+ bn := len(b)
+
+ // Process complete lines, and buffer any incomplete lines.
+ for len(b) > 0 {
+ nl := bytes.IndexByte(b, '\n')
+ if nl < 0 {
+ f.lineBuf.Write(b)
+ break
+ }
+ var line []byte
+ if f.lineBuf.Len() > 0 {
+ // We have buffered data. Add the rest of the line from b and
+ // process the complete line.
+ f.lineBuf.Write(b[:nl+1])
+ line = f.lineBuf.Bytes()
+ } else {
+ // Process a complete line from b.
+ line = b[:nl+1]
+ }
+ b = b[nl+1:]
+ f.process(line)
+ f.lineBuf.Reset()
+ }
+
+ return bn, nil
+}
+
+func (f *testJSONFilter) process(line []byte) {
+ if len(line) > 0 && line[0] == '{' {
+ // Plausible test2json output. Parse it generically.
+ //
+ // We go to some effort here to preserve key order while doing this
+ // generically. This will stay robust to changes in the test2json
+ // struct, or other additions outside of it. If humans are ever looking
+ // at the output, it's really nice to keep field order because it
+ // preserves a lot of regularity in the output.
+ dec := json.NewDecoder(bytes.NewBuffer(line))
+ dec.UseNumber()
+ val, err := decodeJSONValue(dec)
+ if err == nil && val.atom == json.Delim('{') {
+ // Rewrite the Package field.
+ found := false
+ for i := 0; i < len(val.seq); i += 2 {
+ if val.seq[i].atom == "Package" {
+ if pkg, ok := val.seq[i+1].atom.(string); ok {
+ val.seq[i+1].atom = pkg + ":" + f.variant
+ found = true
+ break
+ }
+ }
+ }
+ if found {
+ data, err := json.Marshal(val)
+ if err != nil {
+ // Should never happen.
+ panic(fmt.Sprintf("failed to round-trip JSON %q: %s", string(line), err))
+ }
+ data = append(data, '\n')
+ f.w.Write(data)
+ return
+ }
+ }
+ }
+
+ // Something went wrong. Just pass the line through.
+ f.w.Write(line)
+}
+
+type jsonValue struct {
+ atom json.Token // If json.Delim, then seq will be filled
+ seq []jsonValue // If atom == json.Delim('{'), alternating pairs
+}
+
+var jsonPop = errors.New("end of JSON sequence")
+
+func decodeJSONValue(dec *json.Decoder) (jsonValue, error) {
+ t, err := dec.Token()
+ if err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ return jsonValue{}, err
+ }
+
+ switch t := t.(type) {
+ case json.Delim:
+ if t == '}' || t == ']' {
+ return jsonValue{}, jsonPop
+ }
+
+ var seq []jsonValue
+ for {
+ val, err := decodeJSONValue(dec)
+ if err == jsonPop {
+ break
+ } else if err != nil {
+ return jsonValue{}, err
+ }
+ seq = append(seq, val)
+ }
+ return jsonValue{t, seq}, nil
+ default:
+ return jsonValue{t, nil}, nil
+ }
+}
+
+func (v jsonValue) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ var marshal1 func(v jsonValue) error
+ marshal1 = func(v jsonValue) error {
+ if t, ok := v.atom.(json.Delim); ok {
+ buf.WriteRune(rune(t))
+ for i, v2 := range v.seq {
+ if t == '{' && i%2 == 1 {
+ buf.WriteByte(':')
+ } else if i > 0 {
+ buf.WriteByte(',')
+ }
+ if err := marshal1(v2); err != nil {
+ return err
+ }
+ }
+ if t == '{' {
+ buf.WriteByte('}')
+ } else {
+ buf.WriteByte(']')
+ }
+ return nil
+ }
+ bytes, err := json.Marshal(v.atom)
+ if err != nil {
+ return err
+ }
+ buf.Write(bytes)
+ return nil
+ }
+ err := marshal1(v)
+ return buf.Bytes(), err
+}
diff --git a/src/cmd/dist/testjson_test.go b/src/cmd/dist/testjson_test.go
new file mode 100644
index 0000000000..2ff7bf61f5
--- /dev/null
+++ b/src/cmd/dist/testjson_test.go
@@ -0,0 +1,86 @@
+// Copyright 2023 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 main
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestJSONFilterRewritePackage(t *testing.T) {
+ const in = `{"Package":"abc"}
+{"Field1":"1","Package":"abc","Field3":"3"}
+{"Package":123}
+{}
+{"Package":"abc","Unexpected":[null,true,false,99999999999999999999]}
+`
+ want := strings.ReplaceAll(in, `"Package":"abc"`, `"Package":"abc:variant"`)
+
+ checkJSONFilter(t, in, want)
+}
+
+func TestJSONFilterMalformed(t *testing.T) {
+ const in = `unexpected text
+{"Package":"abc"}
+more text
+{"Package":"abc"}trailing text
+{not json}
+`
+ const want = `unexpected text
+{"Package":"abc:variant"}
+more text
+{"Package":"abc:variant"}
+{not json}
+`
+ // Note that currently we won't round-trip trailing text after a valid JSON
+ // line. That might be a mistake.
+ checkJSONFilter(t, in, want)
+}
+
+func TestJSONFilterBoundaries(t *testing.T) {
+ const in = `{"Package":"abc"}
+{"Package":"def"}
+{"Package":"ghi"}
+`
+ want := strings.ReplaceAll(in, `"}`, `:variant"}`)
+
+ // Write one bytes at a time.
+ t.Run("bytes", func(t *testing.T) {
+ checkJSONFilterWith(t, want, func(f *testJSONFilter) {
+ for i := 0; i < len(in); i++ {
+ f.Write([]byte{in[i]})
+ }
+ })
+ })
+ // Write a block containing a whole line bordered by two partial lines.
+ t.Run("bytes", func(t *testing.T) {
+ checkJSONFilterWith(t, want, func(f *testJSONFilter) {
+ const b1 = 5
+ const b2 = len(in) - 5
+ f.Write([]byte(in[:b1]))
+ f.Write([]byte(in[b1:b2]))
+ f.Write([]byte(in[b2:]))
+ })
+ })
+}
+
+func checkJSONFilter(t *testing.T, in, want string) {
+ t.Helper()
+ checkJSONFilterWith(t, want, func(f *testJSONFilter) {
+ f.Write([]byte(in))
+ })
+}
+
+func checkJSONFilterWith(t *testing.T, want string, write func(*testJSONFilter)) {
+ t.Helper()
+
+ out := new(strings.Builder)
+ f := &testJSONFilter{w: out, variant: "variant"}
+ write(f)
+ got := out.String()
+ if want != got {
+ t.Errorf("want:\n%s\ngot:\n%s", want, got)
+ }
+}