Skip to content

Commit 1fba10c

Browse files
committed
cmd/go: fail if a test binary exits with no output
For example, if a test calls os.Exit(0), that could trick a 'go test' run into not running some of the other tests, and thinking that they all succeeded. This can easily go unnoticed and cause developers headaches. Add a simple sanity check as part of 'go test': if the test binary succeeds and doesn't print anything, we should error, as something clearly went very wrong. This is done by inspecting each of the stdout writes from the spawned process, since we don't want to read the entirety of the output into a buffer. We need to introduce a "buffered" bool var, as there's now an io.Writer layer between cmd.Stdout and &buf. A few TestMain funcs in the standard library needed fixing, as they returned without printing anything as a means to skip testing the entire package. For that purpose add testenv.MainMust, which prints a warning and prints SKIP, similar to when -run matches no tests. Finally, add tests for both os.Exit(0) and os.Exit(1), both as part of TestMain and as part of a single test, and test that the various stdout modes still do the right thing. Fixes #29062. Change-Id: Ic6f8ef3387dfc64e4cd3e8f903d7ca5f5f38d397 Reviewed-on: https://go-review.googlesource.com/c/go/+/184457 Run-TryBot: Daniel Martí <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 38c4a73 commit 1fba10c

File tree

7 files changed

+200
-12
lines changed

7 files changed

+200
-12
lines changed

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,18 @@ func (lockedStdout) Write(b []byte) (int, error) {
10481048
return os.Stdout.Write(b)
10491049
}
10501050

1051+
type outputChecker struct {
1052+
w io.Writer
1053+
anyOutput bool
1054+
}
1055+
1056+
func (o *outputChecker) Write(p []byte) (int, error) {
1057+
if !o.anyOutput && len(bytes.TrimSpace(p)) > 0 {
1058+
o.anyOutput = true
1059+
}
1060+
return o.w.Write(p)
1061+
}
1062+
10511063
// builderRunTest is the action for running a test binary.
10521064
func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
10531065
if a.Failed {
@@ -1067,6 +1079,7 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
10671079
}
10681080

10691081
var buf bytes.Buffer
1082+
buffered := false
10701083
if len(pkgArgs) == 0 || testBench {
10711084
// Stream test output (no buffering) when no package has
10721085
// been given on the command line (implicit current directory)
@@ -1093,9 +1106,16 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
10931106
stdout = io.MultiWriter(stdout, &buf)
10941107
} else {
10951108
stdout = &buf
1109+
buffered = true
10961110
}
10971111
}
10981112

1113+
// Keep track of whether we've seen any output at all. This is useful
1114+
// later, to avoid succeeding if the test binary did nothing or didn't
1115+
// reach the end of testing.M.Run.
1116+
outCheck := outputChecker{w: stdout}
1117+
stdout = &outCheck
1118+
10991119
if c.buf == nil {
11001120
// We did not find a cached result using the link step action ID,
11011121
// so we ran the link step. Try again now with the link output
@@ -1109,7 +1129,7 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
11091129
c.tryCacheWithID(b, a, a.Deps[0].BuildContentID())
11101130
}
11111131
if c.buf != nil {
1112-
if stdout != &buf {
1132+
if !buffered {
11131133
stdout.Write(c.buf.Bytes())
11141134
c.buf.Reset()
11151135
}
@@ -1207,6 +1227,19 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
12071227

12081228
mergeCoverProfile(cmd.Stdout, a.Objdir+"_cover_.out")
12091229

1230+
if err == nil && !testList && !outCheck.anyOutput {
1231+
// If a test does os.Exit(0) by accident, 'go test' may succeed
1232+
// and it can take a while for a human to notice the package's
1233+
// tests didn't actually pass.
1234+
//
1235+
// If a test binary ran without error, it should have at least
1236+
// printed something, such as a PASS line.
1237+
//
1238+
// The only exceptions are when no tests have run, and the
1239+
// -test.list flag, which just prints the names of tests
1240+
// matching a pattern.
1241+
err = fmt.Errorf("test binary succeeded but did not print anything")
1242+
}
12101243
if err == nil {
12111244
norun := ""
12121245
if !testShowPass && !testJSON {
@@ -1227,7 +1260,7 @@ func (c *runCache) builderRunTest(b *work.Builder, a *work.Action) error {
12271260
fmt.Fprintf(cmd.Stdout, "FAIL\t%s\t%s\n", a.Package.ImportPath, t)
12281261
}
12291262

1230-
if cmd.Stdout != &buf {
1263+
if !buffered {
12311264
buf.Reset() // cmd.Stdout was going to os.Stdout already
12321265
}
12331266
return nil
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
env GO111MODULE=on
2+
3+
# If a test exits with a zero status code, 'go test' prints its own error
4+
# message and fails.
5+
! go test ./zero
6+
! stdout ^ok
7+
! stdout 'exit status'
8+
stdout 'did not print anything'
9+
stdout ^FAIL
10+
11+
# If a test exits with a non-zero status code, 'go test' fails normally.
12+
! go test ./one
13+
! stdout ^ok
14+
stdout 'exit status'
15+
! stdout 'did not print anything'
16+
stdout ^FAIL
17+
18+
# Ensure that other flags still do the right thing.
19+
go test -list=. ./zero
20+
stdout ExitZero
21+
22+
! go test -bench=. ./zero
23+
stdout 'did not print anything'
24+
25+
# 'go test' with no args streams output without buffering. Ensure that it still
26+
# catches a zero exit with missing output.
27+
cd zero
28+
! go test
29+
stdout 'did not print anything'
30+
cd ../normal
31+
go test
32+
stdout ^ok
33+
cd ..
34+
35+
# If a TestMain prints something and exits with a zero status code, 'go test'
36+
# shouldn't complain about that. It's a common way to skip testing a package
37+
# entirely.
38+
go test ./main_zero_warning
39+
! stdout 'skipping all tests'
40+
stdout ^ok
41+
42+
# With -v, we'll see the warning from TestMain.
43+
go test -v ./main_zero_warning
44+
stdout 'skipping all tests'
45+
stdout ^ok
46+
47+
# Listing all tests won't actually give a result if TestMain exits. That's okay,
48+
# because this is how TestMain works. If we decide to support -list even when
49+
# TestMain is used to skip entire packages, we can change this test case.
50+
go test -list=. ./main_zero_warning
51+
stdout 'skipping all tests'
52+
! stdout TestNotListed
53+
54+
# If a TestMain prints nothing and exits with a zero status code, 'go test'
55+
# should fail.
56+
! go test ./main_zero_nowarning
57+
stdout 'did not print anything'
58+
59+
# A test that simply prints "PASS" and exits with a zero status code shouldn't
60+
# be OK, but we don't catch that at the moment. It's hard to tell if any test
61+
# started but didn't finish without using -test.v.
62+
go test ./fake_pass
63+
stdout ^ok
64+
65+
-- go.mod --
66+
module m
67+
68+
-- ./normal/normal.go --
69+
package normal
70+
-- ./normal/normal_test.go --
71+
package normal
72+
73+
import "testing"
74+
75+
func TestExitZero(t *testing.T) {
76+
}
77+
78+
-- ./zero/zero.go --
79+
package zero
80+
-- ./zero/zero_test.go --
81+
package zero
82+
83+
import (
84+
"os"
85+
"testing"
86+
)
87+
88+
func TestExitZero(t *testing.T) {
89+
os.Exit(0)
90+
}
91+
92+
-- ./one/one.go --
93+
package one
94+
-- ./one/one_test.go --
95+
package one
96+
97+
import (
98+
"os"
99+
"testing"
100+
)
101+
102+
func TestExitOne(t *testing.T) {
103+
os.Exit(1)
104+
}
105+
106+
-- ./main_zero_warning/zero.go --
107+
package zero
108+
-- ./main_zero_warning/zero_test.go --
109+
package zero
110+
111+
import (
112+
"fmt"
113+
"os"
114+
"testing"
115+
)
116+
117+
func TestMain(m *testing.M) {
118+
fmt.Println("skipping all tests")
119+
os.Exit(0)
120+
}
121+
122+
func TestNotListed(t *testing.T) {}
123+
124+
-- ./main_zero_nowarning/zero.go --
125+
package zero
126+
-- ./main_zero_nowarning/zero_test.go --
127+
package zero
128+
129+
import (
130+
"os"
131+
"testing"
132+
)
133+
134+
func TestMain(m *testing.M) {
135+
os.Exit(0)
136+
}
137+
138+
-- ./fake_pass/fake.go --
139+
package fake
140+
-- ./fake_pass/fake_test.go --
141+
package fake
142+
143+
import (
144+
"fmt"
145+
"os"
146+
"testing"
147+
)
148+
149+
func TestFakePass(t *testing.T) {
150+
fmt.Println("PASS")
151+
os.Exit(0)
152+
}

src/cmd/internal/goobj/goobj_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ var (
2929
)
3030

3131
func TestMain(m *testing.M) {
32-
if !testenv.HasGoBuild() {
33-
return
34-
}
32+
testenv.MainMust(testenv.HasGoBuild)
3533

3634
if err := buildGoobj(); err != nil {
3735
fmt.Println(err)

src/cmd/nm/nm_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ func TestMain(m *testing.M) {
2626
}
2727

2828
func testMain(m *testing.M) int {
29-
if !testenv.HasGoBuild() {
30-
return 0
31-
}
29+
testenv.MainMust(testenv.HasGoBuild)
3230

3331
tmpDir, err := ioutil.TempDir("", "TestNM")
3432
if err != nil {

src/cmd/objdump/objdump_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ import (
2222
var tmp, exe string // populated by buildObjdump
2323

2424
func TestMain(m *testing.M) {
25-
if !testenv.HasGoBuild() {
26-
return
27-
}
25+
testenv.MainMust(testenv.HasGoBuild)
2826

2927
var exitcode int
3028
if err := buildObjdump(); err == nil {

src/go/build/deps_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ var pkgDeps = map[string][]string{
202202
"testing": {"L2", "flag", "fmt", "internal/race", "os", "runtime/debug", "runtime/pprof", "runtime/trace", "time"},
203203
"testing/iotest": {"L2", "log"},
204204
"testing/quick": {"L2", "flag", "fmt", "reflect", "time"},
205-
"internal/testenv": {"L2", "OS", "flag", "testing", "syscall", "internal/cfg"},
205+
"internal/testenv": {"L2", "OS", "flag", "fmt", "testing", "syscall", "internal/cfg"},
206206
"internal/lazyregexp": {"L2", "OS", "regexp"},
207207
"internal/lazytemplate": {"L2", "OS", "text/template"},
208208

src/internal/testenv/testenv.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package testenv
1313
import (
1414
"errors"
1515
"flag"
16+
"fmt"
1617
"internal/cfg"
1718
"os"
1819
"os/exec"
@@ -32,6 +33,14 @@ func Builder() string {
3233
return os.Getenv("GO_BUILDER_NAME")
3334
}
3435

36+
func MainMust(cond func() bool) {
37+
if !cond() {
38+
fmt.Println("testenv: warning: can't run any tests")
39+
fmt.Println("SKIP")
40+
os.Exit(0)
41+
}
42+
}
43+
3544
// HasGoBuild reports whether the current system can build programs with ``go build''
3645
// and then run them with os.StartProcess or exec.Command.
3746
func HasGoBuild() bool {

0 commit comments

Comments
 (0)