Skip to content

Commit d76b1cd

Browse files
author
Bryan C. Mills
committed
cmd/go: support background processes in TestScript
This will be used to test fixes for bugs in concurrent 'go' command invocations, such as #26794. See the README changes for a description of the semantics. Updates #26794 Change-Id: I897e7b2d11ff4549a4711002eadd6a54f033ce0b Reviewed-on: https://go-review.googlesource.com/c/141218 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 4c8b09e commit d76b1cd

File tree

4 files changed

+233
-35
lines changed

4 files changed

+233
-35
lines changed

src/cmd/go/go_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package main_test
77
import (
88
"bytes"
99
"cmd/internal/sys"
10+
"context"
1011
"debug/elf"
1112
"debug/macho"
1213
"flag"
@@ -108,6 +109,12 @@ var testGo string
108109
var testTmpDir string
109110
var testBin string
110111

112+
// testCtx is canceled when the test binary is about to time out.
113+
//
114+
// If https://golang.org/issue/28135 is accepted, uses of this variable in test
115+
// functions should be replaced by t.Context().
116+
var testCtx = context.Background()
117+
111118
// The TestMain function creates a go command for testing purposes and
112119
// deletes it after the tests have been run.
113120
func TestMain(m *testing.M) {
@@ -120,6 +127,20 @@ func TestMain(m *testing.M) {
120127
os.Unsetenv("GOROOT_FINAL")
121128

122129
flag.Parse()
130+
131+
timeoutFlag := flag.Lookup("test.timeout")
132+
if timeoutFlag != nil {
133+
// TODO(golang.org/issue/28147): The go command does not pass the
134+
// test.timeout flag unless either -timeout or -test.timeout is explicitly
135+
// set on the command line.
136+
if d := timeoutFlag.Value.(flag.Getter).Get().(time.Duration); d != 0 {
137+
aBitShorter := d * 95 / 100
138+
var cancel context.CancelFunc
139+
testCtx, cancel = context.WithTimeout(testCtx, aBitShorter)
140+
defer cancel()
141+
}
142+
}
143+
123144
if *proxyAddr != "" {
124145
StartProxy()
125146
select {}

src/cmd/go/script_test.go

Lines changed: 172 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package main_test
99

1010
import (
1111
"bytes"
12+
"context"
1213
"fmt"
1314
"internal/testenv"
1415
"io/ioutil"
@@ -55,21 +56,28 @@ func TestScript(t *testing.T) {
5556

5657
// A testScript holds execution state for a single test script.
5758
type testScript struct {
58-
t *testing.T
59-
workdir string // temporary work dir ($WORK)
60-
log bytes.Buffer // test execution log (printed at end of test)
61-
mark int // offset of next log truncation
62-
cd string // current directory during test execution; initially $WORK/gopath/src
63-
name string // short name of test ("foo")
64-
file string // full file name ("testdata/script/foo.txt")
65-
lineno int // line number currently executing
66-
line string // line currently executing
67-
env []string // environment list (for os/exec)
68-
envMap map[string]string // environment mapping (matches env)
69-
stdout string // standard output from last 'go' command; for 'stdout' command
70-
stderr string // standard error from last 'go' command; for 'stderr' command
71-
stopped bool // test wants to stop early
72-
start time.Time // time phase started
59+
t *testing.T
60+
workdir string // temporary work dir ($WORK)
61+
log bytes.Buffer // test execution log (printed at end of test)
62+
mark int // offset of next log truncation
63+
cd string // current directory during test execution; initially $WORK/gopath/src
64+
name string // short name of test ("foo")
65+
file string // full file name ("testdata/script/foo.txt")
66+
lineno int // line number currently executing
67+
line string // line currently executing
68+
env []string // environment list (for os/exec)
69+
envMap map[string]string // environment mapping (matches env)
70+
stdout string // standard output from last 'go' command; for 'stdout' command
71+
stderr string // standard error from last 'go' command; for 'stderr' command
72+
stopped bool // test wants to stop early
73+
start time.Time // time phase started
74+
background []backgroundCmd // backgrounded 'exec' and 'go' commands
75+
}
76+
77+
type backgroundCmd struct {
78+
cmd *exec.Cmd
79+
wait <-chan struct{}
80+
neg bool // if true, cmd should fail
7381
}
7482

7583
var extraEnvKeys = []string{
@@ -146,6 +154,17 @@ func (ts *testScript) run() {
146154
}
147155

148156
defer func() {
157+
// On a normal exit from the test loop, background processes are cleaned up
158+
// before we print PASS. If we return early (e.g., due to a test failure),
159+
// don't print anything about the processes that were still running.
160+
for _, bg := range ts.background {
161+
interruptProcess(bg.cmd.Process)
162+
}
163+
for _, bg := range ts.background {
164+
<-bg.wait
165+
}
166+
ts.background = nil
167+
149168
markTime()
150169
// Flush testScript log to testing.T log.
151170
ts.t.Log("\n" + ts.abbrev(ts.log.String()))
@@ -284,14 +303,23 @@ Script:
284303

285304
// Command can ask script to stop early.
286305
if ts.stopped {
287-
return
306+
// Break instead of returning, so that we check the status of any
307+
// background processes and print PASS.
308+
break
288309
}
289310
}
290311

312+
for _, bg := range ts.background {
313+
interruptProcess(bg.cmd.Process)
314+
}
315+
ts.cmdWait(false, nil)
316+
291317
// Final phase ended.
292318
rewind()
293319
markTime()
294-
fmt.Fprintf(&ts.log, "PASS\n")
320+
if !ts.stopped {
321+
fmt.Fprintf(&ts.log, "PASS\n")
322+
}
295323
}
296324

297325
// scriptCmds are the script command implementations.
@@ -317,6 +345,7 @@ var scriptCmds = map[string]func(*testScript, bool, []string){
317345
"stdout": (*testScript).cmdStdout,
318346
"stop": (*testScript).cmdStop,
319347
"symlink": (*testScript).cmdSymlink,
348+
"wait": (*testScript).cmdWait,
320349
}
321350

322351
// addcrlf adds CRLF line endings to the named files.
@@ -451,26 +480,43 @@ func (ts *testScript) cmdEnv(neg bool, args []string) {
451480

452481
// exec runs the given command.
453482
func (ts *testScript) cmdExec(neg bool, args []string) {
454-
if len(args) < 1 {
455-
ts.fatalf("usage: exec program [args...]")
483+
if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
484+
ts.fatalf("usage: exec program [args...] [&]")
456485
}
486+
457487
var err error
458-
ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
459-
if ts.stdout != "" {
460-
fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
461-
}
462-
if ts.stderr != "" {
463-
fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
488+
if len(args) > 0 && args[len(args)-1] == "&" {
489+
var cmd *exec.Cmd
490+
cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...)
491+
if err == nil {
492+
wait := make(chan struct{})
493+
go func() {
494+
ctxWait(testCtx, cmd)
495+
close(wait)
496+
}()
497+
ts.background = append(ts.background, backgroundCmd{cmd, wait, neg})
498+
}
499+
ts.stdout, ts.stderr = "", ""
500+
} else {
501+
ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
502+
if ts.stdout != "" {
503+
fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
504+
}
505+
if ts.stderr != "" {
506+
fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
507+
}
508+
if err == nil && neg {
509+
ts.fatalf("unexpected command success")
510+
}
464511
}
512+
465513
if err != nil {
466514
fmt.Fprintf(&ts.log, "[%v]\n", err)
467-
if !neg {
515+
if testCtx.Err() != nil {
516+
ts.fatalf("test timed out while running command")
517+
} else if !neg {
468518
ts.fatalf("unexpected command failure")
469519
}
470-
} else {
471-
if neg {
472-
ts.fatalf("unexpected command success")
473-
}
474520
}
475521
}
476522

@@ -545,6 +591,14 @@ func (ts *testScript) cmdSkip(neg bool, args []string) {
545591
if neg {
546592
ts.fatalf("unsupported: ! skip")
547593
}
594+
595+
// Before we mark the test as skipped, shut down any background processes and
596+
// make sure they have returned the correct status.
597+
for _, bg := range ts.background {
598+
interruptProcess(bg.cmd.Process)
599+
}
600+
ts.cmdWait(false, nil)
601+
548602
if len(args) == 1 {
549603
ts.t.Skip(args[0])
550604
}
@@ -687,6 +741,52 @@ func (ts *testScript) cmdSymlink(neg bool, args []string) {
687741
ts.check(os.Symlink(args[2], ts.mkabs(args[0])))
688742
}
689743

744+
// wait waits for background commands to exit, setting stderr and stdout to their result.
745+
func (ts *testScript) cmdWait(neg bool, args []string) {
746+
if neg {
747+
ts.fatalf("unsupported: ! wait")
748+
}
749+
if len(args) > 0 {
750+
ts.fatalf("usage: wait")
751+
}
752+
753+
var stdouts, stderrs []string
754+
for _, bg := range ts.background {
755+
<-bg.wait
756+
757+
args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...)
758+
fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState)
759+
760+
cmdStdout := bg.cmd.Stdout.(*strings.Builder).String()
761+
if cmdStdout != "" {
762+
fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
763+
stdouts = append(stdouts, cmdStdout)
764+
}
765+
766+
cmdStderr := bg.cmd.Stderr.(*strings.Builder).String()
767+
if cmdStderr != "" {
768+
fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
769+
stderrs = append(stderrs, cmdStderr)
770+
}
771+
772+
if bg.cmd.ProcessState.Success() {
773+
if bg.neg {
774+
ts.fatalf("unexpected command success")
775+
}
776+
} else {
777+
if testCtx.Err() != nil {
778+
ts.fatalf("test timed out while running command")
779+
} else if !bg.neg {
780+
ts.fatalf("unexpected command failure")
781+
}
782+
}
783+
}
784+
785+
ts.stdout = strings.Join(stdouts, "")
786+
ts.stderr = strings.Join(stderrs, "")
787+
ts.background = nil
788+
}
789+
690790
// Helpers for command implementations.
691791

692792
// abbrev abbreviates the actual work directory in the string s to the literal string "$WORK".
@@ -716,10 +816,51 @@ func (ts *testScript) exec(command string, args ...string) (stdout, stderr strin
716816
var stdoutBuf, stderrBuf strings.Builder
717817
cmd.Stdout = &stdoutBuf
718818
cmd.Stderr = &stderrBuf
719-
err = cmd.Run()
819+
if err = cmd.Start(); err == nil {
820+
err = ctxWait(testCtx, cmd)
821+
}
720822
return stdoutBuf.String(), stderrBuf.String(), err
721823
}
722824

825+
// execBackground starts the given command line (an actual subprocess, not simulated)
826+
// in ts.cd with environment ts.env.
827+
func (ts *testScript) execBackground(command string, args ...string) (*exec.Cmd, error) {
828+
cmd := exec.Command(command, args...)
829+
cmd.Dir = ts.cd
830+
cmd.Env = append(ts.env, "PWD="+ts.cd)
831+
var stdoutBuf, stderrBuf strings.Builder
832+
cmd.Stdout = &stdoutBuf
833+
cmd.Stderr = &stderrBuf
834+
return cmd, cmd.Start()
835+
}
836+
837+
// ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done.
838+
//
839+
// This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill.
840+
// (See https://golang.org/issue/21135.)
841+
func ctxWait(ctx context.Context, cmd *exec.Cmd) error {
842+
errc := make(chan error, 1)
843+
go func() { errc <- cmd.Wait() }()
844+
845+
select {
846+
case err := <-errc:
847+
return err
848+
case <-ctx.Done():
849+
interruptProcess(cmd.Process)
850+
return <-errc
851+
}
852+
}
853+
854+
// interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise.
855+
func interruptProcess(p *os.Process) {
856+
if err := p.Signal(os.Interrupt); err != nil {
857+
// Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
858+
// Windows; using it with os.Process.Signal will return an error.”
859+
// Fall back to Kill instead.
860+
p.Kill()
861+
}
862+
}
863+
723864
// expand applies environment variable expansion to the string s.
724865
func (ts *testScript) expand(s string) string {
725866
return os.Expand(s, func(key string) string { return ts.envMap[key] })

src/cmd/go/testdata/script/README

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,23 @@ The commands are:
9999
With no arguments, print the environment (useful for debugging).
100100
Otherwise add the listed key=value pairs to the environment.
101101

102-
- [!] exec program [args...]
102+
- [!] exec program [args...] [&]
103103
Run the given executable program with the arguments.
104104
It must (or must not) succeed.
105105
Note that 'exec' does not terminate the script (unlike in Unix shells).
106106

107+
If the last token is '&', the program executes in the background. The standard
108+
output and standard error of the previous command is cleared, but the output
109+
of the background process is buffered — and checking of its exit status is
110+
delayed — until the next call to 'wait', 'skip', or 'stop' or the end of the
111+
test. At the end of the test, any remaining background processes are
112+
terminated using os.Interrupt (if supported) or os.Kill.
113+
107114
- [!] exists [-readonly] file...
108115
Each of the listed files or directories must (or must not) exist.
109116
If -readonly is given, the files or directories must be unwritable.
110117

111-
- [!] go args...
118+
- [!] go args... [&]
112119
Run the (test copy of the) go command with the given arguments.
113120
It must (or must not) succeed.
114121

@@ -131,18 +138,25 @@ The commands are:
131138

132139
- [!] stderr [-count=N] pattern
133140
Apply the grep command (see above) to the standard error
134-
from the most recent exec or go command.
141+
from the most recent exec, go, or wait command.
135142

136143
- [!] stdout [-count=N] pattern
137144
Apply the grep command (see above) to the standard output
138-
from the most recent exec or go command.
145+
from the most recent exec, go, or wait command.
139146

140147
- stop [message]
141148
Stop the test early (marking it as passing), including the message if given.
142149

143150
- symlink file -> target
144151
Create file as a symlink to target. The -> (like in ls -l output) is required.
145152

153+
- wait
154+
Wait for all 'exec' and 'go' commands started in the background (with the '&'
155+
token) to exit, and display success or failure status for them.
156+
After a call to wait, the 'stderr' and 'stdout' commands will apply to the
157+
concatenation of the corresponding streams of the background commands,
158+
in the order in which those commands were started.
159+
146160
When TestScript runs a script and the script fails, by default TestScript shows
147161
the execution of the most recent phase of the script (since the last # comment)
148162
and only shows the # comments for earlier phases. For example, here is a

0 commit comments

Comments
 (0)