Skip to content

Commit 93477f0

Browse files
committed
cmd/dist: add -json flag
This enables JSON output for all tests run by dist. Most the complexity here is that, in order to disambiguate JSON records from different package variants, we have to rewrite the JSON stream on the fly to include variant information. We do this by rewriting the Package field to be pkg:variant so existing CI systems will naturally pick up the disambiguated test name. Fixes #37486. Change-Id: I0094e5e27b3a02ffc108534b8258c699ed8c3b87 Reviewed-on: https://go-review.googlesource.com/c/go/+/494958 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent df9f043 commit 93477f0

File tree

3 files changed

+344
-14
lines changed

3 files changed

+344
-14
lines changed

src/cmd/dist/test.go

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66

77
import (
88
"bytes"
9+
"encoding/json"
910
"flag"
1011
"fmt"
1112
"io"
@@ -41,6 +42,7 @@ func cmdtest() {
4142
"Special exception: if the string begins with '!', the match is inverted.")
4243
flag.BoolVar(&t.msan, "msan", false, "run in memory sanitizer builder mode")
4344
flag.BoolVar(&t.asan, "asan", false, "run in address sanitizer builder mode")
45+
flag.BoolVar(&t.json, "json", false, "report test results in JSON")
4446

4547
xflagparse(-1) // any number of args
4648
if noRebuild {
@@ -70,6 +72,7 @@ type tester struct {
7072
short bool
7173
cgoEnabled bool
7274
partial bool
75+
json bool
7376

7477
tests []distTest // use addTest to extend
7578
testNames map[string]bool
@@ -212,12 +215,14 @@ func (t *tester) run() {
212215
}
213216
}
214217

215-
if err := t.maybeLogMetadata(); err != nil {
216-
t.failed = true
217-
if t.keepGoing {
218-
log.Printf("Failed logging metadata: %v", err)
219-
} else {
220-
fatalf("Failed logging metadata: %v", err)
218+
if !t.json {
219+
if err := t.maybeLogMetadata(); err != nil {
220+
t.failed = true
221+
if t.keepGoing {
222+
log.Printf("Failed logging metadata: %v", err)
223+
} else {
224+
fatalf("Failed logging metadata: %v", err)
225+
}
221226
}
222227
}
223228

@@ -240,13 +245,17 @@ func (t *tester) run() {
240245
t.runPending(nil)
241246
timelog("end", "dist test")
242247

248+
if !t.json {
249+
if t.failed {
250+
fmt.Println("\nFAILED")
251+
} else if t.partial {
252+
fmt.Println("\nALL TESTS PASSED (some were excluded)")
253+
} else {
254+
fmt.Println("\nALL TESTS PASSED")
255+
}
256+
}
243257
if t.failed {
244-
fmt.Println("\nFAILED")
245258
xexit(1)
246-
} else if t.partial {
247-
fmt.Println("\nALL TESTS PASSED (some were excluded)")
248-
} else {
249-
fmt.Println("\nALL TESTS PASSED")
250259
}
251260
}
252261

@@ -302,7 +311,8 @@ type goTest struct {
302311
runOnHost bool // When cross-compiling, run this test on the host instead of guest
303312

304313
// variant, if non-empty, is a name used to distinguish different
305-
// configurations of the same test package(s).
314+
// configurations of the same test package(s). If set and sharded is false,
315+
// the Package field in test2json output is rewritten to pkg:variant.
306316
variant string
307317
// sharded indicates that variant is used solely for sharding and that
308318
// 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 {
335345

336346
cmd := exec.Command(goCmd, args...)
337347
setupCmd(cmd)
338-
cmd.Stdout = stdout
348+
if t.json && opts.variant != "" && !opts.sharded {
349+
// Rewrite Package in the JSON output to be pkg:variant. For sharded
350+
// variants, pkg.TestName is already unambiguous, so we don't need to
351+
// rewrite the Package field.
352+
if len(opts.pkgs) != 0 {
353+
panic("cannot combine multiple packages with variants")
354+
}
355+
// We only want to process JSON on the child's stdout. Ideally if
356+
// stdout==stderr, we would also use the same testJSONFilter for
357+
// cmd.Stdout and cmd.Stderr in order to keep the underlying
358+
// interleaving of writes, but then it would see even partial writes
359+
// interleaved, which would corrupt the JSON. So, we only process
360+
// cmd.Stdout. This has another consequence though: if stdout==stderr,
361+
// we have to serialize Writes in case the Writer is not concurrent
362+
// safe. If we were just passing stdout/stderr through to exec, it would
363+
// do this for us, but since we're wrapping stdout, we have to do it
364+
// ourselves.
365+
if stdout == stderr {
366+
stdout = &lockedWriter{w: stdout}
367+
stderr = stdout
368+
}
369+
cmd.Stdout = &testJSONFilter{w: stdout, variant: opts.variant}
370+
} else {
371+
cmd.Stdout = stdout
372+
}
339373
cmd.Stderr = stderr
340374

341375
return cmd
@@ -403,6 +437,9 @@ func (opts *goTest) buildArgs(t *tester) (goCmd string, build, run, pkgs, testFl
403437
if opts.cpu != "" {
404438
run = append(run, "-cpu="+opts.cpu)
405439
}
440+
if t.json {
441+
run = append(run, "-json")
442+
}
406443

407444
if opts.gcflags != "" {
408445
build = append(build, "-gcflags=all="+opts.gcflags)
@@ -948,7 +985,7 @@ func (t *tester) registerTest(name, heading string, test *goTest, opts ...regist
948985
if skipFunc != nil {
949986
msg, skip := skipFunc(dt)
950987
if skip {
951-
fmt.Println(msg)
988+
t.printSkip(test, msg)
952989
return nil
953990
}
954991
}
@@ -959,6 +996,34 @@ func (t *tester) registerTest(name, heading string, test *goTest, opts ...regist
959996
})
960997
}
961998

999+
func (t *tester) printSkip(test *goTest, msg string) {
1000+
if !t.json {
1001+
fmt.Println(msg)
1002+
return
1003+
}
1004+
type event struct {
1005+
Time time.Time
1006+
Action string
1007+
Package string
1008+
Output string `json:",omitempty"`
1009+
}
1010+
out := json.NewEncoder(os.Stdout)
1011+
for _, pkg := range test.packages() {
1012+
variantName := pkg
1013+
if test.variant != "" {
1014+
variantName += ":" + test.variant
1015+
}
1016+
ev := event{Time: time.Now(), Package: variantName, Action: "start"}
1017+
out.Encode(ev)
1018+
ev.Action = "output"
1019+
ev.Output = msg
1020+
out.Encode(ev)
1021+
ev.Action = "skip"
1022+
ev.Output = ""
1023+
out.Encode(ev)
1024+
}
1025+
}
1026+
9621027
// dirCmd constructs a Cmd intended to be run in the foreground.
9631028
// The command will be run in dir, and Stdout and Stderr will go to os.Stdout
9641029
// and os.Stderr.
@@ -1005,6 +1070,9 @@ func (t *tester) iOS() bool {
10051070
}
10061071

10071072
func (t *tester) out(v string) {
1073+
if t.json {
1074+
return
1075+
}
10081076
if t.banner == "" {
10091077
return
10101078
}

src/cmd/dist/testjson.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"bytes"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"io"
13+
"sync"
14+
)
15+
16+
// lockedWriter serializes Write calls to an underlying Writer.
17+
type lockedWriter struct {
18+
lock sync.Mutex
19+
w io.Writer
20+
}
21+
22+
func (w *lockedWriter) Write(b []byte) (int, error) {
23+
w.lock.Lock()
24+
defer w.lock.Unlock()
25+
return w.w.Write(b)
26+
}
27+
28+
// testJSONFilter is an io.Writer filter that replaces the Package field in
29+
// test2json output.
30+
type testJSONFilter struct {
31+
w io.Writer // Underlying writer
32+
variant string // Add ":variant" to Package field
33+
34+
lineBuf bytes.Buffer // Buffer for incomplete lines
35+
}
36+
37+
func (f *testJSONFilter) Write(b []byte) (int, error) {
38+
bn := len(b)
39+
40+
// Process complete lines, and buffer any incomplete lines.
41+
for len(b) > 0 {
42+
nl := bytes.IndexByte(b, '\n')
43+
if nl < 0 {
44+
f.lineBuf.Write(b)
45+
break
46+
}
47+
var line []byte
48+
if f.lineBuf.Len() > 0 {
49+
// We have buffered data. Add the rest of the line from b and
50+
// process the complete line.
51+
f.lineBuf.Write(b[:nl+1])
52+
line = f.lineBuf.Bytes()
53+
} else {
54+
// Process a complete line from b.
55+
line = b[:nl+1]
56+
}
57+
b = b[nl+1:]
58+
f.process(line)
59+
f.lineBuf.Reset()
60+
}
61+
62+
return bn, nil
63+
}
64+
65+
func (f *testJSONFilter) process(line []byte) {
66+
if len(line) > 0 && line[0] == '{' {
67+
// Plausible test2json output. Parse it generically.
68+
//
69+
// We go to some effort here to preserve key order while doing this
70+
// generically. This will stay robust to changes in the test2json
71+
// struct, or other additions outside of it. If humans are ever looking
72+
// at the output, it's really nice to keep field order because it
73+
// preserves a lot of regularity in the output.
74+
dec := json.NewDecoder(bytes.NewBuffer(line))
75+
dec.UseNumber()
76+
val, err := decodeJSONValue(dec)
77+
if err == nil && val.atom == json.Delim('{') {
78+
// Rewrite the Package field.
79+
found := false
80+
for i := 0; i < len(val.seq); i += 2 {
81+
if val.seq[i].atom == "Package" {
82+
if pkg, ok := val.seq[i+1].atom.(string); ok {
83+
val.seq[i+1].atom = pkg + ":" + f.variant
84+
found = true
85+
break
86+
}
87+
}
88+
}
89+
if found {
90+
data, err := json.Marshal(val)
91+
if err != nil {
92+
// Should never happen.
93+
panic(fmt.Sprintf("failed to round-trip JSON %q: %s", string(line), err))
94+
}
95+
data = append(data, '\n')
96+
f.w.Write(data)
97+
return
98+
}
99+
}
100+
}
101+
102+
// Something went wrong. Just pass the line through.
103+
f.w.Write(line)
104+
}
105+
106+
type jsonValue struct {
107+
atom json.Token // If json.Delim, then seq will be filled
108+
seq []jsonValue // If atom == json.Delim('{'), alternating pairs
109+
}
110+
111+
var jsonPop = errors.New("end of JSON sequence")
112+
113+
func decodeJSONValue(dec *json.Decoder) (jsonValue, error) {
114+
t, err := dec.Token()
115+
if err != nil {
116+
if err == io.EOF {
117+
err = io.ErrUnexpectedEOF
118+
}
119+
return jsonValue{}, err
120+
}
121+
122+
switch t := t.(type) {
123+
case json.Delim:
124+
if t == '}' || t == ']' {
125+
return jsonValue{}, jsonPop
126+
}
127+
128+
var seq []jsonValue
129+
for {
130+
val, err := decodeJSONValue(dec)
131+
if err == jsonPop {
132+
break
133+
} else if err != nil {
134+
return jsonValue{}, err
135+
}
136+
seq = append(seq, val)
137+
}
138+
return jsonValue{t, seq}, nil
139+
default:
140+
return jsonValue{t, nil}, nil
141+
}
142+
}
143+
144+
func (v jsonValue) MarshalJSON() ([]byte, error) {
145+
var buf bytes.Buffer
146+
var marshal1 func(v jsonValue) error
147+
marshal1 = func(v jsonValue) error {
148+
if t, ok := v.atom.(json.Delim); ok {
149+
buf.WriteRune(rune(t))
150+
for i, v2 := range v.seq {
151+
if t == '{' && i%2 == 1 {
152+
buf.WriteByte(':')
153+
} else if i > 0 {
154+
buf.WriteByte(',')
155+
}
156+
if err := marshal1(v2); err != nil {
157+
return err
158+
}
159+
}
160+
if t == '{' {
161+
buf.WriteByte('}')
162+
} else {
163+
buf.WriteByte(']')
164+
}
165+
return nil
166+
}
167+
bytes, err := json.Marshal(v.atom)
168+
if err != nil {
169+
return err
170+
}
171+
buf.Write(bytes)
172+
return nil
173+
}
174+
err := marshal1(v)
175+
return buf.Bytes(), err
176+
}

0 commit comments

Comments
 (0)