Skip to content

Commit a5dc8ff

Browse files
committed
testscript: phase out func() int in RunMain
We wanted the user's command functions to return an exit code as an int rather than calling os.Exit directly like a main function so that we could collect coverage profiles from subprocesses. This way, Go tests using testscript would still report the full code coverage information even when using nested processes. This all thankfully went away with Go 1.20, which introduced the same feature but built right into the toolchain for both `go test` and `go build`. As such, we were able to drop all of that code, including the bit that we ran before os.Exit. For more information, see: https://go.dev/blog/integration-test-coverage At this point, testscript users continue to use the `func() int` signature, via e.g. `func main1() int` out of inertia, but there's actually no good reason to keep doing that. It causes extra boilerplate and confuses new testscript users. Moreover, avoiding the use of os.Exit was rather tricky, for example see the former use of flag.ContinueOnExit in our tests. Add a new API, Main, which uses a `func()` signature just like `func main()`, meaning that no second function declaration is needed. Deprecate RunMain in favor of Main as well.
1 parent f18544a commit a5dc8ff

File tree

13 files changed

+74
-98
lines changed

13 files changed

+74
-98
lines changed

cmd/testscript/main.go

+11-20
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,28 @@ func (e *envVarsFlag) Set(v string) error {
4141
}
4242

4343
func main() {
44-
os.Exit(main1())
45-
}
46-
47-
func main1() int {
4844
switch err := mainerr(); err {
4945
case nil:
50-
return 0
51-
case flag.ErrHelp:
52-
return 2
5346
default:
5447
fmt.Fprintln(os.Stderr, err)
55-
return 1
48+
os.Exit(1)
5649
}
5750
}
5851

5952
func mainerr() (retErr error) {
60-
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
61-
fs.Usage = func() {
53+
flag.Usage = func() {
6254
mainUsage(os.Stderr)
55+
os.Exit(2)
6356
}
6457
var envVars envVarsFlag
65-
fUpdate := fs.Bool("u", false, "update archive file if a cmp fails")
66-
fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done")
67-
fContinue := fs.Bool("continue", false, "continue running the script if an error occurs")
68-
fVerbose := fs.Bool("v", false, "run tests verbosely")
69-
fs.Var(&envVars, "e", "pass through environment variable to script (can appear multiple times)")
70-
if err := fs.Parse(os.Args[1:]); err != nil {
71-
return err
72-
}
73-
74-
files := fs.Args()
58+
fUpdate := flag.Bool("u", false, "update archive file if a cmp fails")
59+
fWork := flag.Bool("work", false, "print temporary work directory and do not remove when done")
60+
fContinue := flag.Bool("continue", false, "continue running the script if an error occurs")
61+
fVerbose := flag.Bool("v", false, "run tests verbosely")
62+
flag.Var(&envVars, "e", "pass through environment variable to script (can appear multiple times)")
63+
flag.Parse()
64+
65+
files := flag.Args()
7566
if len(files) == 0 {
7667
files = []string{"-"}
7768
}

cmd/testscript/main_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package main
66

77
import (
88
"bytes"
9-
"os"
109
"os/exec"
1110
"path/filepath"
1211
"runtime"
@@ -19,9 +18,9 @@ import (
1918
)
2019

2120
func TestMain(m *testing.M) {
22-
os.Exit(testscript.RunMain(m, map[string]func() int{
23-
"testscript": main1,
24-
}))
21+
testscript.Main(m, map[string]func(){
22+
"testscript": main,
23+
})
2524
}
2625

2726
func TestScripts(t *testing.T) {

cmd/txtar-addmod/addmod.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,9 @@ func fatalf(format string, args ...interface{}) {
6262

6363
const goCmd = "go"
6464

65-
func main() {
66-
os.Exit(main1())
67-
}
68-
6965
var allFiles = flag.Bool("all", false, "include all source files")
7066

71-
func main1() int {
67+
func main() {
7268
flag.Usage = usage
7369
flag.Parse()
7470
if flag.NArg() < 2 {
@@ -211,5 +207,5 @@ func main1() int {
211207
}
212208
}
213209
os.RemoveAll(tmpdir)
214-
return exitCode
210+
os.Exit(exitCode)
215211
}

cmd/txtar-addmod/script_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import (
1717
var proxyURL string
1818

1919
func TestMain(m *testing.M) {
20-
os.Exit(testscript.RunMain(gobinMain{m}, map[string]func() int{
21-
"txtar-addmod": main1,
22-
}))
20+
testscript.Main(gobinMain{m}, map[string]func(){
21+
"txtar-addmod": main,
22+
})
2323
}
2424

2525
type gobinMain struct {

cmd/txtar-c/savedir.go

+3-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ package main
1414

1515
import (
1616
"bytes"
17-
stdflag "flag"
17+
"flag"
1818
"fmt"
1919
"log"
2020
"os"
@@ -25,11 +25,10 @@ import (
2525
"github.com/rogpeppe/go-internal/txtar"
2626
)
2727

28-
var flag = stdflag.NewFlagSet(os.Args[0], stdflag.ContinueOnError)
29-
3028
func usage() {
3129
fmt.Fprintf(os.Stderr, "usage: txtar-c dir >saved.txtar\n")
3230
flag.PrintDefaults()
31+
os.Exit(2)
3332
}
3433

3534
var (
@@ -38,17 +37,10 @@ var (
3837
)
3938

4039
func main() {
41-
os.Exit(main1())
42-
}
43-
44-
func main1() int {
4540
flag.Usage = usage
46-
if flag.Parse(os.Args[1:]) != nil {
47-
return 2
48-
}
41+
flag.Parse()
4942
if flag.NArg() != 1 {
5043
usage()
51-
return 2
5244
}
5345

5446
log.SetPrefix("txtar-c: ")
@@ -111,6 +103,4 @@ func main1() int {
111103

112104
data := txtar.Format(a)
113105
os.Stdout.Write(data)
114-
115-
return 0
116106
}

cmd/txtar-c/script_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
package main
66

77
import (
8-
"os"
98
"testing"
109

1110
"github.com/rogpeppe/go-internal/testscript"
1211
)
1312

1413
func TestMain(m *testing.M) {
15-
os.Exit(testscript.RunMain(m, map[string]func() int{
16-
"txtar-c": main1,
17-
}))
14+
testscript.Main(m, map[string]func(){
15+
"txtar-c": main,
16+
})
1817
}
1918

2019
func TestScripts(t *testing.T) {

cmd/txtar-x/extract.go

+4-9
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,14 @@ var (
2929
func usage() {
3030
fmt.Fprintf(os.Stderr, "usage: txtar-x [flags] [file]\n")
3131
flag.PrintDefaults()
32+
os.Exit(2)
3233
}
3334

3435
func main() {
35-
os.Exit(main1())
36-
}
37-
38-
func main1() int {
3936
flag.Usage = usage
4037
flag.Parse()
4138
if flag.NArg() > 1 {
4239
usage()
43-
return 2
4440
}
4541
log.SetPrefix("txtar-x: ")
4642
log.SetFlags(0)
@@ -50,20 +46,19 @@ func main1() int {
5046
data, err := io.ReadAll(os.Stdin)
5147
if err != nil {
5248
log.Printf("cannot read stdin: %v", err)
53-
return 1
49+
os.Exit(1)
5450
}
5551
a = txtar.Parse(data)
5652
} else {
5753
a1, err := txtar.ParseFile(flag.Arg(0))
5854
if err != nil {
5955
log.Print(err)
60-
return 1
56+
os.Exit(1)
6157
}
6258
a = a1
6359
}
6460
if err := txtar.Write(a, *extractDir); err != nil {
6561
log.Print(err)
66-
return 1
62+
os.Exit(1)
6763
}
68-
return 0
6964
}

cmd/txtar-x/extract_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import (
1313
)
1414

1515
func TestMain(m *testing.M) {
16-
os.Exit(testscript.RunMain(m, map[string]func() int{
17-
"txtar-x": main1,
18-
}))
16+
testscript.Main(m, map[string]func(){
17+
"txtar-x": main,
18+
})
1919
}
2020

2121
func TestScripts(t *testing.T) {

gotooltest/testdata/cover.txt

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import (
5151
)
5252

5353
func TestMain(m *testing.M) {
54+
// Note that here we still use RunMain, which is the older deprecated API,
55+
// to sanity check that it works OK.
5456
os.Exit(testscript.RunMain(m, map[string] func() int{
5557
"foo": foo1,
5658
}))

testscript/doc.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ To run a specific script foo.txtar or foo.txt, run
2424
where TestName is the name of the test that Run is called from.
2525
2626
To define an executable command (or several) that can be run as part of the script,
27-
call RunMain with the functions that implement the command's functionality.
27+
call Main with the functions that implement the command's functionality.
2828
The command functions will be called in a separate process, so are
2929
free to mutate global variables without polluting the top level test binary.
3030
3131
func TestMain(m *testing.M) {
32-
os.Exit(testscript.RunMain(m, map[string] func() int{
32+
testscript.Main(m, map[string] func() {
3333
"testscript": testscriptMain,
34-
}))
34+
})
3535
}
3636
3737
In general script files should have short names: a few words, not whole sentences.

testscript/exe.go

+25-17
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ type TestingM interface {
2323
// Deprecated: this option is no longer used.
2424
func IgnoreMissedCoverage() {}
2525

26-
// RunMain should be called within a TestMain function to allow
26+
// Main should be called within a TestMain function to allow
2727
// subcommands to be run in the testscript context.
28+
// Main always calls [os.Exit], so it does not return back to the caller.
2829
//
2930
// The commands map holds the set of command names, each
30-
// with an associated run function which should return the
31-
// code to pass to os.Exit. It's OK for a command function to
32-
// exit itself, but this may result in loss of coverage information.
31+
// with an associated run function which may call os.Exit.
3332
//
3433
// When Run is called, these commands are installed as regular commands in the shell
3534
// path, so can be invoked with "exec" or via any other command (for example a shell script).
@@ -38,9 +37,7 @@ func IgnoreMissedCoverage() {}
3837
// without "exec" - that is, "foo" will behave like "exec foo".
3938
// This can be disabled with Params.RequireExplicitExec to keep consistency
4039
// across test scripts, and to keep separate process executions explicit.
41-
//
42-
// This function returns an exit code to pass to os.Exit, after calling m.Run.
43-
func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
40+
func Main(m TestingM, commands map[string]func()) {
4441
// Depending on os.Args[0], this is either the top-level execution of
4542
// the test binary by "go test", or the execution of one of the provided
4643
// commands via "foo" or "exec foo".
@@ -57,19 +54,16 @@ func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
5754
// Set up all commands in a directory, added in $PATH.
5855
tmpdir, err := os.MkdirTemp("", "testscript-main")
5956
if err != nil {
60-
log.Printf("could not set up temporary directory: %v", err)
61-
return 2
57+
log.Fatalf("could not set up temporary directory: %v", err)
6258
}
6359
defer func() {
6460
if err := os.RemoveAll(tmpdir); err != nil {
65-
log.Printf("cannot delete temporary directory: %v", err)
66-
exitCode = 2
61+
log.Fatalf("cannot delete temporary directory: %v", err)
6762
}
6863
}()
6964
bindir := filepath.Join(tmpdir, "bin")
7065
if err := os.MkdirAll(bindir, 0o777); err != nil {
71-
log.Printf("could not set up PATH binary directory: %v", err)
72-
return 2
66+
log.Fatalf("could not set up PATH binary directory: %v", err)
7367
}
7468
os.Setenv("PATH", bindir+string(filepath.ListSeparator)+os.Getenv("PATH"))
7569

@@ -85,8 +79,7 @@ func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
8579
err = copyBinary(binpath, binfile)
8680
}
8781
if err != nil {
88-
log.Printf("could not set up %s in $PATH: %v", name, err)
89-
return 2
82+
log.Fatalf("could not set up %s in $PATH: %v", name, err)
9083
}
9184
scriptCmds[name] = func(ts *TestScript, neg bool, args []string) {
9285
if ts.params.RequireExplicitExec {
@@ -95,11 +88,26 @@ func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
9588
ts.cmdExec(neg, append([]string{name}, args...))
9689
}
9790
}
98-
return m.Run()
91+
os.Exit(m.Run())
9992
}
10093
// The command being registered is being invoked, so run it, then exit.
10194
os.Args[0] = cmdName
102-
return mainf()
95+
mainf()
96+
os.Exit(0)
97+
}
98+
99+
// Deprecated: use [Main], as the only reason for returning exit codes
100+
// was to collect full code coverage, which Go does automatically now:
101+
// https://go.dev/blog/integration-test-coverage
102+
func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
103+
commands2 := make(map[string]func(), len(commands))
104+
for name, fn := range commands {
105+
commands2[name] = func() { os.Exit(fn()) }
106+
}
107+
Main(m, commands2)
108+
// Main always calls os.Exit; we assume that all users of RunMain would have simply
109+
// called os.Exit with the returned exitCode as well, following the documentation.
110+
panic("unreachable")
103111
}
104112

105113
// copyBinary makes a copy of a binary to a new location. It is used as part of

testscript/testscript.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ type Params struct {
181181
// script.
182182
UpdateScripts bool
183183

184-
// RequireExplicitExec requires that commands passed to RunMain must be used
184+
// RequireExplicitExec requires that commands passed to [Main] must be used
185185
// in test scripts via `exec cmd` and not simply `cmd`. This can help keep
186186
// consistency across test scripts as well as keep separate process
187187
// executions explicit.

0 commit comments

Comments
 (0)