Skip to content

Commit 4f76fe8

Browse files
cmd/go, testing, os: fail test that calls os.Exit(0)
This catches cases where a test calls code that calls os.Exit(0), thereby skipping all subsequent tests. Fixes #29062 Change-Id: If9478972f40189e27623557e7141469ca4234d89 Reviewed-on: https://go-review.googlesource.com/c/go/+/250977 Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent cdc6355 commit 4f76fe8

File tree

9 files changed

+195
-8
lines changed

9 files changed

+195
-8
lines changed

doc/go1.16.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ <h3 id="go-command">Go command</h3>
5252
TODO: write and link to tutorial or blog post
5353
</p>
5454

55+
<p><!-= golang.org/issue/29062 -->
56+
When using <code>go test</code>, a test that
57+
calls <code>os.Exit(0)</code> during execution of a test function
58+
will now be considered to fail.
59+
This will help catch cases in which a test calls code that calls
60+
os.Exit(0) and thereby stops running all future tests.
61+
If a <code>TestMain</code> function calls <code>os.Exit(0)</code>
62+
that is still considered to be a passing test.
63+
</p>
64+
5565
<p>
5666
TODO
5767
</p>
@@ -101,7 +111,7 @@ <h2 id="library">Core library</h2>
101111

102112
<h3 id="net"><a href="/pkg/net/">net</a></h3>
103113

104-
<p><!-- CL -->
114+
<p><!-- CL 250357 -->
105115
The case of I/O on a closed network connection, or I/O on a network
106116
connection that is closed before any of the I/O completes, can now
107117
be detected using the new <a href="/pkg/net/#ErrClosed">ErrClosed</a> error.

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ func TestPassFlagToTestIncludesAllTestFlags(t *testing.T) {
1616
return
1717
}
1818
name := strings.TrimPrefix(f.Name, "test.")
19-
if name != "testlogfile" && !passFlagToTest[name] {
20-
t.Errorf("passFlagToTest missing entry for %q (flag test.%s)", name, name)
21-
t.Logf("(Run 'go generate cmd/go/internal/test' if it should be added.)")
19+
switch name {
20+
case "testlogfile", "paniconexit0":
21+
// These are internal flags.
22+
default:
23+
if !passFlagToTest[name] {
24+
t.Errorf("passFlagToTest missing entry for %q (flag test.%s)", name, name)
25+
t.Logf("(Run 'go generate cmd/go/internal/test' if it should be added.)")
26+
}
2227
}
2328
})
2429

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ func testFlags() []string {
6262
}
6363
name := strings.TrimPrefix(f.Name, "test.")
6464

65-
if name == "testlogfile" {
66-
// test.testlogfile is “for use only by cmd/go”
67-
} else {
65+
switch name {
66+
case "testlogfile", "paniconexit0":
67+
// These flags are only for use by cmd/go.
68+
default:
6869
names = append(names, name)
6970
}
7071
})

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1164,7 +1164,8 @@ func (c *runCache) builderRunTest(b *work.Builder, ctx context.Context, a *work.
11641164
if !c.disableCache && len(execCmd) == 0 {
11651165
testlogArg = []string{"-test.testlogfile=" + a.Objdir + "testlog.txt"}
11661166
}
1167-
args := str.StringList(execCmd, a.Deps[0].BuiltTarget(), testlogArg, testArgs)
1167+
panicArg := "-test.paniconexit0"
1168+
args := str.StringList(execCmd, a.Deps[0].BuiltTarget(), testlogArg, panicArg, testArgs)
11681169

11691170
if testCoverProfile != "" {
11701171
// Write coverage to temporary profile, for merging later.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Builds and runs test binaries, so skip in short mode.
2+
[short] skip
3+
4+
env GO111MODULE=on
5+
6+
# If a test invoked by 'go test' exits with a zero status code,
7+
# it will panic.
8+
! go test ./zero
9+
! stdout ^ok
10+
! stdout 'exit status'
11+
stdout 'panic'
12+
stdout ^FAIL
13+
14+
# If a test exits with a non-zero status code, 'go test' fails normally.
15+
! go test ./one
16+
! stdout ^ok
17+
stdout 'exit status'
18+
! stdout 'panic'
19+
stdout ^FAIL
20+
21+
# Ensure that other flags still do the right thing.
22+
go test -list=. ./zero
23+
stdout ExitZero
24+
25+
! go test -bench=. ./zero
26+
stdout 'panic'
27+
28+
# 'go test' with no args streams output without buffering. Ensure that it still
29+
# catches a zero exit with missing output.
30+
cd zero
31+
! go test
32+
stdout 'panic'
33+
cd ../normal
34+
go test
35+
stdout ^ok
36+
cd ..
37+
38+
# If a TestMain exits with a zero status code, 'go test' shouldn't
39+
# complain about that. It's a common way to skip testing a package
40+
# entirely.
41+
go test ./main_zero
42+
! stdout 'skipping all tests'
43+
stdout ^ok
44+
45+
# With -v, we'll see the warning from TestMain.
46+
go test -v ./main_zero
47+
stdout 'skipping all tests'
48+
stdout ^ok
49+
50+
# Listing all tests won't actually give a result if TestMain exits. That's okay,
51+
# because this is how TestMain works. If we decide to support -list even when
52+
# TestMain is used to skip entire packages, we can change this test case.
53+
go test -list=. ./main_zero
54+
stdout 'skipping all tests'
55+
! stdout TestNotListed
56+
57+
-- go.mod --
58+
module m
59+
60+
-- ./normal/normal.go --
61+
package normal
62+
-- ./normal/normal_test.go --
63+
package normal
64+
65+
import "testing"
66+
67+
func TestExitZero(t *testing.T) {
68+
}
69+
70+
-- ./zero/zero.go --
71+
package zero
72+
-- ./zero/zero_test.go --
73+
package zero
74+
75+
import (
76+
"os"
77+
"testing"
78+
)
79+
80+
func TestExitZero(t *testing.T) {
81+
os.Exit(0)
82+
}
83+
84+
-- ./one/one.go --
85+
package one
86+
-- ./one/one_test.go --
87+
package one
88+
89+
import (
90+
"os"
91+
"testing"
92+
)
93+
94+
func TestExitOne(t *testing.T) {
95+
os.Exit(1)
96+
}
97+
98+
-- ./main_zero/zero.go --
99+
package zero
100+
-- ./main_zero/zero_test.go --
101+
package zero
102+
103+
import (
104+
"fmt"
105+
"os"
106+
"testing"
107+
)
108+
109+
func TestMain(m *testing.M) {
110+
fmt.Println("skipping all tests")
111+
os.Exit(0)
112+
}
113+
114+
func TestNotListed(t *testing.T) {}

src/internal/testlog/exit.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2020 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 testlog
6+
7+
import "sync"
8+
9+
// PanicOnExit0 reports whether to panic on a call to os.Exit(0).
10+
// This is in the testlog package because, like other definitions in
11+
// package testlog, it is a hook between the testing package and the
12+
// os package. This is used to ensure that an early call to os.Exit(0)
13+
// does not cause a test to pass.
14+
func PanicOnExit0() bool {
15+
panicOnExit0.mu.Lock()
16+
defer panicOnExit0.mu.Unlock()
17+
return panicOnExit0.val
18+
}
19+
20+
// panicOnExit0 is the flag used for PanicOnExit0. This uses a lock
21+
// because the value can be cleared via a timer call that may race
22+
// with calls to os.Exit
23+
var panicOnExit0 struct {
24+
mu sync.Mutex
25+
val bool
26+
}
27+
28+
// SetPanicOnExit0 sets panicOnExit0 to v.
29+
func SetPanicOnExit0(v bool) {
30+
panicOnExit0.mu.Lock()
31+
defer panicOnExit0.mu.Unlock()
32+
panicOnExit0.val = v
33+
}

src/os/proc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package os
88

99
import (
10+
"internal/testlog"
1011
"runtime"
1112
"syscall"
1213
)
@@ -60,6 +61,13 @@ func Getgroups() ([]int, error) {
6061
// For portability, the status code should be in the range [0, 125].
6162
func Exit(code int) {
6263
if code == 0 {
64+
if testlog.PanicOnExit0() {
65+
// We were told to panic on calls to os.Exit(0).
66+
// This is used to fail tests that make an early
67+
// unexpected call to os.Exit(0).
68+
panic("unexpected call to os.Exit(0) during test")
69+
}
70+
6371
// Give race detector a chance to fail the program.
6472
// Racy programs do not have the right to finish successfully.
6573
runtime_beforeExit()

src/testing/internal/testdeps/deps.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,8 @@ func (TestDeps) StopTestLog() error {
121121
log.w = nil
122122
return err
123123
}
124+
125+
// SetPanicOnExit0 tells the os package whether to panic on os.Exit(0).
126+
func (TestDeps) SetPanicOnExit0(v bool) {
127+
testlog.SetPanicOnExit0(v)
128+
}

src/testing/testing.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ func Init() {
294294
blockProfileRate = flag.Int("test.blockprofilerate", 1, "set blocking profile `rate` (see runtime.SetBlockProfileRate)")
295295
mutexProfile = flag.String("test.mutexprofile", "", "write a mutex contention profile to the named file after execution")
296296
mutexProfileFraction = flag.Int("test.mutexprofilefraction", 1, "if >= 0, calls runtime.SetMutexProfileFraction()")
297+
panicOnExit0 = flag.Bool("test.paniconexit0", false, "panic on call to os.Exit(0)")
297298
traceFile = flag.String("test.trace", "", "write an execution trace to `file`")
298299
timeout = flag.Duration("test.timeout", 0, "panic test binary after duration `d` (default 0, timeout disabled)")
299300
cpuListStr = flag.String("test.cpu", "", "comma-separated `list` of cpu counts to run each test with")
@@ -320,6 +321,7 @@ var (
320321
blockProfileRate *int
321322
mutexProfile *string
322323
mutexProfileFraction *int
324+
panicOnExit0 *bool
323325
traceFile *string
324326
timeout *time.Duration
325327
cpuListStr *string
@@ -1261,6 +1263,7 @@ func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return e
12611263
func (f matchStringOnly) ImportPath() string { return "" }
12621264
func (f matchStringOnly) StartTestLog(io.Writer) {}
12631265
func (f matchStringOnly) StopTestLog() error { return errMain }
1266+
func (f matchStringOnly) SetPanicOnExit0(bool) {}
12641267

12651268
// Main is an internal function, part of the implementation of the "go test" command.
12661269
// It was exported because it is cross-package and predates "internal" packages.
@@ -1296,6 +1299,7 @@ type M struct {
12961299
type testDeps interface {
12971300
ImportPath() string
12981301
MatchString(pat, str string) (bool, error)
1302+
SetPanicOnExit0(bool)
12991303
StartCPUProfile(io.Writer) error
13001304
StopCPUProfile()
13011305
StartTestLog(io.Writer)
@@ -1521,11 +1525,17 @@ func (m *M) before() {
15211525
m.deps.StartTestLog(f)
15221526
testlogFile = f
15231527
}
1528+
if *panicOnExit0 {
1529+
m.deps.SetPanicOnExit0(true)
1530+
}
15241531
}
15251532

15261533
// after runs after all testing.
15271534
func (m *M) after() {
15281535
m.afterOnce.Do(func() {
1536+
if *panicOnExit0 {
1537+
m.deps.SetPanicOnExit0(false)
1538+
}
15291539
m.writeProfiles()
15301540
})
15311541
}

0 commit comments

Comments
 (0)