Skip to content

Commit e9d2f1e

Browse files
committed
cmd/go: add go test -json flag
This CL finally adds one of our longest-requested cmd/go features: a way for test-running harnesses to access test output in structured form. In fact the structured json output is more informative than the text output, because the output from multiple parallel tests can be interleaved as it becomes available, instead of needing to wait for the previous test to finish before showing any output from the next test. See CL 76872 for the conversion details. Fixes #2981. Change-Id: I749c4fc260190af9fe633437a781ec0cf56b7260 Reviewed-on: https://go-review.googlesource.com/76873 Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1 parent 3e2dc45 commit e9d2f1e

File tree

5 files changed

+96
-4
lines changed

5 files changed

+96
-4
lines changed

src/cmd/go/go_test.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -5061,11 +5061,50 @@ func TestGcflagsPatterns(t *testing.T) {
50615061
tg.grepStderrNot("compile.* -N .*-p fmt", "incorrectly built fmt with -N flag")
50625062
}
50635063

5064-
// Issue 22644
50655064
func TestGoTestMinusN(t *testing.T) {
50665065
// Intent here is to verify that 'go test -n' works without crashing.
50675066
// This reuses flag_test.go, but really any test would do.
50685067
tg := testgo(t)
50695068
defer tg.cleanup()
50705069
tg.run("test", "testdata/flag_test.go", "-n", "-args", "-v=7")
50715070
}
5071+
5072+
func TestGoTestJSON(t *testing.T) {
5073+
tg := testgo(t)
5074+
defer tg.cleanup()
5075+
tg.parallel()
5076+
tg.makeTempdir()
5077+
tg.setenv("GOCACHE", tg.tempdir)
5078+
tg.setenv("GOPATH", filepath.Join(tg.pwd(), "testdata"))
5079+
5080+
// Test that math and fmt output is interlaced.
5081+
if runtime.GOMAXPROCS(-1) < 2 {
5082+
tg.setenv("GOMAXPROCS", "2")
5083+
}
5084+
// This has the potential to be a flaky test.
5085+
// Probably the first try will work, but the second try should have
5086+
// both tests equally cached and should definitely work.
5087+
for try := 0; ; try++ {
5088+
tg.run("test", "-json", "-short", "-v", "sleepy1", "sleepy2")
5089+
state := 0
5090+
for _, line := range strings.Split(tg.getStdout(), "\n") {
5091+
if state == 0 && strings.Contains(line, `"Package":"sleepy1"`) {
5092+
state = 1
5093+
}
5094+
if state == 1 && strings.Contains(line, `"Package":"sleepy2"`) {
5095+
state = 2
5096+
}
5097+
if state == 2 && strings.Contains(line, `"Package":"sleepy1"`) {
5098+
state = 3
5099+
break
5100+
}
5101+
}
5102+
if state != 3 {
5103+
if try < 1 {
5104+
continue
5105+
}
5106+
t.Fatalf("did not find fmt interlaced with math")
5107+
}
5108+
break
5109+
}
5110+
}

src/cmd/go/internal/test/test.go

+31-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"regexp"
2222
"sort"
2323
"strings"
24+
"sync"
2425
"text/template"
2526
"time"
2627
"unicode"
@@ -32,6 +33,7 @@ import (
3233
"cmd/go/internal/load"
3334
"cmd/go/internal/str"
3435
"cmd/go/internal/work"
36+
"cmd/internal/test2json"
3537
)
3638

3739
// Break init loop.
@@ -457,6 +459,7 @@ var (
457459
testO string // -o flag
458460
testProfile bool // some profiling flag
459461
testNeedBinary bool // profile needs to keep binary around
462+
testJSON bool // -json flag
460463
testV bool // -v flag
461464
testTimeout string // -timeout flag
462465
testArgs []string
@@ -1166,6 +1169,21 @@ type runCache struct {
11661169
id2 cache.ActionID
11671170
}
11681171

1172+
// stdoutMu and lockedStdout provide a locked standard output
1173+
// that guarantees never to interlace writes from multiple
1174+
// goroutines, so that we can have multiple JSON streams writing
1175+
// to a lockedStdout simultaneously and know that events will
1176+
// still be intelligible.
1177+
var stdoutMu sync.Mutex
1178+
1179+
type lockedStdout struct{}
1180+
1181+
func (lockedStdout) Write(b []byte) (int, error) {
1182+
stdoutMu.Lock()
1183+
defer stdoutMu.Unlock()
1184+
return os.Stdout.Write(b)
1185+
}
1186+
11691187
// builderRunTest is the action for running a test binary.
11701188
func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
11711189
if c.buf == nil {
@@ -1206,6 +1224,12 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
12061224
cmd.Dir = a.Package.Dir
12071225
cmd.Env = base.EnvForDir(cmd.Dir, cfg.OrigEnv)
12081226
var buf bytes.Buffer
1227+
var stdout io.Writer = os.Stdout
1228+
if testJSON {
1229+
json := test2json.NewConverter(lockedStdout{}, a.Package.ImportPath, test2json.Timestamp)
1230+
defer json.Close()
1231+
stdout = json
1232+
}
12091233
if len(pkgArgs) == 0 || testBench {
12101234
// Stream test output (no buffering) when no package has
12111235
// been given on the command line (implicit current directory)
@@ -1221,10 +1245,15 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
12211245
// subject to change. It would be nice to remove this special case
12221246
// entirely, but it is surely very helpful to see progress being made
12231247
// when tests are run on slow single-CPU ARM systems.
1224-
if testShowPass && (len(pkgs) == 1 || cfg.BuildP == 1) {
1248+
//
1249+
// If we're showing JSON output, then display output as soon as
1250+
// possible even when multiple tests are being run: the JSON output
1251+
// events are attributed to specific package tests, so interlacing them
1252+
// is OK.
1253+
if testShowPass && (len(pkgs) == 1 || cfg.BuildP == 1) || testJSON {
12251254
// Write both to stdout and buf, for possible saving
12261255
// to cache, and for looking for the "no tests to run" message.
1227-
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
1256+
cmd.Stdout = io.MultiWriter(stdout, &buf)
12281257
} else {
12291258
cmd.Stdout = &buf
12301259
}

src/cmd/go/internal/test/testflag.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var testFlagDefn = []*cmdflag.Defn{
3333
{Name: "covermode"},
3434
{Name: "coverpkg"},
3535
{Name: "exec"},
36+
{Name: "json", BoolVar: &testJSON},
3637
{Name: "vet"},
3738

3839
// Passed to 6.out, adding a "test." prefix to the name if necessary: -v becomes -test.v.
@@ -133,8 +134,11 @@ func testFlags(args []string) (packageNames, passToTest []string) {
133134
// Arguably should be handled by f.Value, but aren't.
134135
switch f.Name {
135136
// bool flags.
136-
case "c", "i", "v", "cover":
137+
case "c", "i", "v", "cover", "json":
137138
cmdflag.SetBool(cmd, f.BoolVar, value)
139+
if f.Name == "json" && testJSON {
140+
passToTest = append(passToTest, "-test.v")
141+
}
138142
case "o":
139143
testO = value
140144
testNeedBinary = true
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package p
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func Test1(t *testing.T) {
9+
time.Sleep(200 * time.Millisecond)
10+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package p
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func Test1(t *testing.T) {
9+
time.Sleep(200 * time.Millisecond)
10+
}

0 commit comments

Comments
 (0)