aboutsummaryrefslogtreecommitdiff
path: root/libgo/go/cmd/go/script_test.go
diff options
context:
space:
mode:
authorIan Lance Taylor <iant@golang.org>2020-07-27 22:27:54 -0700
committerIan Lance Taylor <iant@golang.org>2020-08-01 11:21:40 -0700
commitf75af8c1464e948b5e166cf5ab09ebf0d82fc253 (patch)
tree3ba3299859b504bdeb477727471216bd094a0191 /libgo/go/cmd/go/script_test.go
parent75a23e59031fe673fc3b2e60fd1fe5f4c70ecb85 (diff)
downloadgcc-f75af8c1464e948b5e166cf5ab09ebf0d82fc253.zip
gcc-f75af8c1464e948b5e166cf5ab09ebf0d82fc253.tar.gz
gcc-f75af8c1464e948b5e166cf5ab09ebf0d82fc253.tar.bz2
libgo: update to go1.15rc1
Reviewed-on: https://go-review.googlesource.com/c/gofrontend/+/245157
Diffstat (limited to 'libgo/go/cmd/go/script_test.go')
-rw-r--r--libgo/go/cmd/go/script_test.go483
1 files changed, 316 insertions, 167 deletions
diff --git a/libgo/go/cmd/go/script_test.go b/libgo/go/cmd/go/script_test.go
index ec498bb..2e8f18a 100644
--- a/libgo/go/cmd/go/script_test.go
+++ b/libgo/go/cmd/go/script_test.go
@@ -10,6 +10,7 @@ package main_test
import (
"bytes"
"context"
+ "errors"
"fmt"
"go/build"
"internal/testenv"
@@ -30,6 +31,7 @@ import (
"cmd/go/internal/robustio"
"cmd/go/internal/txtar"
"cmd/go/internal/work"
+ "cmd/internal/objabi"
"cmd/internal/sys"
)
@@ -76,15 +78,26 @@ type testScript struct {
stderr string // standard error from last 'go' command; for 'stderr' command
stopped bool // test wants to stop early
start time.Time // time phase started
- background []backgroundCmd // backgrounded 'exec' and 'go' commands
+ background []*backgroundCmd // backgrounded 'exec' and 'go' commands
}
type backgroundCmd struct {
- cmd *exec.Cmd
- wait <-chan struct{}
- neg bool // if true, cmd should fail
+ want simpleStatus
+ args []string
+ cancel context.CancelFunc
+ done <-chan struct{}
+ err error
+ stdout, stderr strings.Builder
}
+type simpleStatus string
+
+const (
+ success simpleStatus = ""
+ failure simpleStatus = "!"
+ successOrFailure simpleStatus = "?"
+)
+
var extraEnvKeys = []string{
"SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210
"WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
@@ -109,12 +122,16 @@ func (ts *testScript) setup() {
"CCACHE_DISABLE=1", // ccache breaks with non-existent HOME
"GOARCH=" + runtime.GOARCH,
"GOCACHE=" + testGOCACHE,
+ "GODEBUG=" + os.Getenv("GODEBUG"),
"GOEXE=" + cfg.ExeSuffix,
+ "GOEXPSTRING=" + objabi.Expstring()[2:],
"GOOS=" + runtime.GOOS,
"GOPATH=" + filepath.Join(ts.workdir, "gopath"),
"GOPROXY=" + proxyURL,
"GOPRIVATE=",
"GOROOT=" + testGOROOT,
+ "GOROOT_FINAL=" + os.Getenv("GOROOT_FINAL"), // causes spurious rebuilds and breaks the "stale" built-in if not propagated
+ "TESTGO_GOROOT=" + testGOROOT,
"GOSUMDB=" + testSumDBVerifierKey,
"GONOPROXY=",
"GONOSUMDB=",
@@ -181,10 +198,10 @@ func (ts *testScript) run() {
// before we print PASS. If we return early (e.g., due to a test failure),
// don't print anything about the processes that were still running.
for _, bg := range ts.background {
- interruptProcess(bg.cmd.Process)
+ bg.cancel()
}
for _, bg := range ts.background {
- <-bg.wait
+ <-bg.done
}
ts.background = nil
@@ -205,7 +222,7 @@ func (ts *testScript) run() {
// With -v or -testwork, start log with full environment.
if *testWork || testing.Verbose() {
// Display environment.
- ts.cmdEnv(false, nil)
+ ts.cmdEnv(success, nil)
fmt.Fprintf(&ts.log, "\n")
ts.mark = ts.log.Len()
}
@@ -245,7 +262,7 @@ Script:
// Parse input line. Ignore blanks entirely.
parsed := ts.parse(line)
if parsed.name == "" {
- if parsed.neg || len(parsed.conds) > 0 {
+ if parsed.want != "" || len(parsed.conds) > 0 {
ts.fatalf("missing command")
}
continue
@@ -324,7 +341,7 @@ Script:
if cmd == nil {
ts.fatalf("unknown command %q", parsed.name)
}
- cmd(ts, parsed.neg, parsed.args)
+ cmd(ts, parsed.want, parsed.args)
// Command can ask script to stop early.
if ts.stopped {
@@ -335,9 +352,9 @@ Script:
}
for _, bg := range ts.background {
- interruptProcess(bg.cmd.Process)
+ bg.cancel()
}
- ts.cmdWait(false, nil)
+ ts.cmdWait(success, nil)
// Final phase ended.
rewind()
@@ -352,7 +369,7 @@ Script:
//
// NOTE: If you make changes here, update testdata/script/README too!
//
-var scriptCmds = map[string]func(*testScript, bool, []string){
+var scriptCmds = map[string]func(*testScript, simpleStatus, []string){
"addcrlf": (*testScript).cmdAddcrlf,
"cc": (*testScript).cmdCc,
"cd": (*testScript).cmdCd,
@@ -385,7 +402,7 @@ var regexpCmd = map[string]bool{
}
// addcrlf adds CRLF line endings to the named files.
-func (ts *testScript) cmdAddcrlf(neg bool, args []string) {
+func (ts *testScript) cmdAddcrlf(want simpleStatus, args []string) {
if len(args) == 0 {
ts.fatalf("usage: addcrlf file...")
}
@@ -399,21 +416,21 @@ func (ts *testScript) cmdAddcrlf(neg bool, args []string) {
}
// cc runs the C compiler along with platform specific options.
-func (ts *testScript) cmdCc(neg bool, args []string) {
+func (ts *testScript) cmdCc(want simpleStatus, args []string) {
if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
ts.fatalf("usage: cc args... [&]")
}
var b work.Builder
b.Init()
- ts.cmdExec(neg, append(b.GccCmd(".", ""), args...))
+ ts.cmdExec(want, append(b.GccCmd(".", ""), args...))
robustio.RemoveAll(b.WorkDir)
}
// cd changes to a different directory.
-func (ts *testScript) cmdCd(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! cd")
+func (ts *testScript) cmdCd(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v cd", want)
}
if len(args) != 1 {
ts.fatalf("usage: cd dir")
@@ -437,9 +454,9 @@ func (ts *testScript) cmdCd(neg bool, args []string) {
}
// chmod changes permissions for a file or directory.
-func (ts *testScript) cmdChmod(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! chmod")
+func (ts *testScript) cmdChmod(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v chmod", want)
}
if len(args) < 2 {
ts.fatalf("usage: chmod perm paths...")
@@ -459,10 +476,10 @@ func (ts *testScript) cmdChmod(neg bool, args []string) {
}
// cmp compares two files.
-func (ts *testScript) cmdCmp(neg bool, args []string) {
- if neg {
+func (ts *testScript) cmdCmp(want simpleStatus, args []string) {
+ if want != success {
// It would be strange to say "this file can have any content except this precise byte sequence".
- ts.fatalf("unsupported: ! cmp")
+ ts.fatalf("unsupported: %v cmp", want)
}
quiet := false
if len(args) > 0 && args[0] == "-q" {
@@ -476,9 +493,9 @@ func (ts *testScript) cmdCmp(neg bool, args []string) {
}
// cmpenv compares two files with environment variable substitution.
-func (ts *testScript) cmdCmpenv(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! cmpenv")
+func (ts *testScript) cmdCmpenv(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v cmpenv", want)
}
quiet := false
if len(args) > 0 && args[0] == "-q" {
@@ -524,7 +541,7 @@ func (ts *testScript) doCmdCmp(args []string, env, quiet bool) {
}
// cp copies files, maybe eventually directories.
-func (ts *testScript) cmdCp(neg bool, args []string) {
+func (ts *testScript) cmdCp(want simpleStatus, args []string) {
if len(args) < 2 {
ts.fatalf("usage: cp src... dst")
}
@@ -564,20 +581,21 @@ func (ts *testScript) cmdCp(neg bool, args []string) {
targ = filepath.Join(dst, filepath.Base(src))
}
err := ioutil.WriteFile(targ, data, mode)
- if neg {
+ switch want {
+ case failure:
if err == nil {
ts.fatalf("unexpected command success")
}
- } else {
+ case success:
ts.check(err)
}
}
}
// env displays or adds to the environment.
-func (ts *testScript) cmdEnv(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! env")
+func (ts *testScript) cmdEnv(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v env", want)
}
conv := func(s string) string { return s }
@@ -615,86 +633,96 @@ func (ts *testScript) cmdEnv(neg bool, args []string) {
}
// exec runs the given command.
-func (ts *testScript) cmdExec(neg bool, args []string) {
+func (ts *testScript) cmdExec(want simpleStatus, args []string) {
if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
ts.fatalf("usage: exec program [args...] [&]")
}
- var err error
+ background := false
if len(args) > 0 && args[len(args)-1] == "&" {
- var cmd *exec.Cmd
- cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...)
- if err == nil {
- wait := make(chan struct{})
- go func() {
- ctxWait(testCtx, cmd)
- close(wait)
- }()
- ts.background = append(ts.background, backgroundCmd{cmd, wait, neg})
- }
- ts.stdout, ts.stderr = "", ""
- } else {
- ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
- if ts.stdout != "" {
- fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
- }
- if ts.stderr != "" {
- fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
- }
- if err == nil && neg {
- ts.fatalf("unexpected command success")
- }
+ background = true
+ args = args[:len(args)-1]
}
+ bg, err := ts.startBackground(want, args[0], args[1:]...)
if err != nil {
- fmt.Fprintf(&ts.log, "[%v]\n", err)
- if testCtx.Err() != nil {
- ts.fatalf("test timed out while running command")
- } else if !neg {
- ts.fatalf("unexpected command failure")
- }
+ ts.fatalf("unexpected error starting command: %v", err)
+ }
+ if background {
+ ts.stdout, ts.stderr = "", ""
+ ts.background = append(ts.background, bg)
+ return
+ }
+
+ <-bg.done
+ ts.stdout = bg.stdout.String()
+ ts.stderr = bg.stderr.String()
+ if ts.stdout != "" {
+ fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
}
+ if ts.stderr != "" {
+ fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
+ }
+ if bg.err != nil {
+ fmt.Fprintf(&ts.log, "[%v]\n", bg.err)
+ }
+ ts.checkCmd(bg)
}
// exists checks that the list of files exists.
-func (ts *testScript) cmdExists(neg bool, args []string) {
- var readonly bool
- if len(args) > 0 && args[0] == "-readonly" {
- readonly = true
- args = args[1:]
+func (ts *testScript) cmdExists(want simpleStatus, args []string) {
+ if want == successOrFailure {
+ ts.fatalf("unsupported: %v exists", want)
+ }
+ var readonly, exec bool
+loop:
+ for len(args) > 0 {
+ switch args[0] {
+ case "-readonly":
+ readonly = true
+ args = args[1:]
+ case "-exec":
+ exec = true
+ args = args[1:]
+ default:
+ break loop
+ }
}
if len(args) == 0 {
- ts.fatalf("usage: exists [-readonly] file...")
+ ts.fatalf("usage: exists [-readonly] [-exec] file...")
}
for _, file := range args {
file = ts.mkabs(file)
info, err := os.Stat(file)
- if err == nil && neg {
+ if err == nil && want == failure {
what := "file"
if info.IsDir() {
what = "directory"
}
ts.fatalf("%s %s unexpectedly exists", what, file)
}
- if err != nil && !neg {
+ if err != nil && want == success {
ts.fatalf("%s does not exist", file)
}
- if err == nil && !neg && readonly && info.Mode()&0222 != 0 {
+ if err == nil && want == success && readonly && info.Mode()&0222 != 0 {
ts.fatalf("%s exists but is writable", file)
}
+ if err == nil && want == success && exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
+ ts.fatalf("%s exists but is not executable", file)
+ }
}
}
// go runs the go command.
-func (ts *testScript) cmdGo(neg bool, args []string) {
- ts.cmdExec(neg, append([]string{testGo}, args...))
+func (ts *testScript) cmdGo(want simpleStatus, args []string) {
+ ts.cmdExec(want, append([]string{testGo}, args...))
}
// mkdir creates directories.
-func (ts *testScript) cmdMkdir(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! mkdir")
+func (ts *testScript) cmdMkdir(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v mkdir", want)
}
if len(args) < 1 {
ts.fatalf("usage: mkdir dir...")
@@ -705,9 +733,9 @@ func (ts *testScript) cmdMkdir(neg bool, args []string) {
}
// rm removes files or directories.
-func (ts *testScript) cmdRm(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! rm")
+func (ts *testScript) cmdRm(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v rm", want)
}
if len(args) < 1 {
ts.fatalf("usage: rm file...")
@@ -720,20 +748,20 @@ func (ts *testScript) cmdRm(neg bool, args []string) {
}
// skip marks the test skipped.
-func (ts *testScript) cmdSkip(neg bool, args []string) {
+func (ts *testScript) cmdSkip(want simpleStatus, args []string) {
if len(args) > 1 {
ts.fatalf("usage: skip [msg]")
}
- if neg {
- ts.fatalf("unsupported: ! skip")
+ if want != success {
+ ts.fatalf("unsupported: %v skip", want)
}
// Before we mark the test as skipped, shut down any background processes and
// make sure they have returned the correct status.
for _, bg := range ts.background {
- interruptProcess(bg.cmd.Process)
+ bg.cancel()
}
- ts.cmdWait(false, nil)
+ ts.cmdWait(success, nil)
if len(args) == 1 {
ts.t.Skip(args[0])
@@ -742,15 +770,18 @@ func (ts *testScript) cmdSkip(neg bool, args []string) {
}
// stale checks that the named build targets are stale.
-func (ts *testScript) cmdStale(neg bool, args []string) {
+func (ts *testScript) cmdStale(want simpleStatus, args []string) {
if len(args) == 0 {
ts.fatalf("usage: stale target...")
}
- tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{else}}"
- if neg {
+ tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{{else}}"
+ switch want {
+ case failure:
tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale{{end}}"
- } else {
+ case success:
tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}"
+ default:
+ ts.fatalf("unsupported: %v stale", want)
}
tmpl += "{{end}}"
goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...)
@@ -764,26 +795,30 @@ func (ts *testScript) cmdStale(neg bool, args []string) {
}
// stdout checks that the last go command standard output matches a regexp.
-func (ts *testScript) cmdStdout(neg bool, args []string) {
- scriptMatch(ts, neg, args, ts.stdout, "stdout")
+func (ts *testScript) cmdStdout(want simpleStatus, args []string) {
+ scriptMatch(ts, want, args, ts.stdout, "stdout")
}
// stderr checks that the last go command standard output matches a regexp.
-func (ts *testScript) cmdStderr(neg bool, args []string) {
- scriptMatch(ts, neg, args, ts.stderr, "stderr")
+func (ts *testScript) cmdStderr(want simpleStatus, args []string) {
+ scriptMatch(ts, want, args, ts.stderr, "stderr")
}
// grep checks that file content matches a regexp.
// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
-func (ts *testScript) cmdGrep(neg bool, args []string) {
- scriptMatch(ts, neg, args, "", "grep")
+func (ts *testScript) cmdGrep(want simpleStatus, args []string) {
+ scriptMatch(ts, want, args, "", "grep")
}
// scriptMatch implements both stdout and stderr.
-func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
+func scriptMatch(ts *testScript, want simpleStatus, args []string, text, name string) {
+ if want == successOrFailure {
+ ts.fatalf("unsupported: %v %s", want, name)
+ }
+
n := 0
if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
- if neg {
+ if want == failure {
ts.fatalf("cannot use -count= with negated match")
}
var err error
@@ -803,12 +838,12 @@ func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
}
extraUsage := ""
- want := 1
+ wantArgs := 1
if name == "grep" {
extraUsage = " file"
- want = 2
+ wantArgs = 2
}
- if len(args) != want {
+ if len(args) != wantArgs {
ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
}
@@ -829,14 +864,16 @@ func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
// Matching against workdir would be misleading.
text = strings.ReplaceAll(text, ts.workdir, "$WORK")
- if neg {
+ switch want {
+ case failure:
if re.MatchString(text) {
if isGrep && !quiet {
fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
}
ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
}
- } else {
+
+ case success:
if !re.MatchString(text) {
if isGrep && !quiet {
fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
@@ -856,9 +893,9 @@ func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
}
// stop stops execution of the test (marking it passed).
-func (ts *testScript) cmdStop(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! stop")
+func (ts *testScript) cmdStop(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v stop", want)
}
if len(args) > 1 {
ts.fatalf("usage: stop [msg]")
@@ -872,9 +909,9 @@ func (ts *testScript) cmdStop(neg bool, args []string) {
}
// symlink creates a symbolic link.
-func (ts *testScript) cmdSymlink(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! symlink")
+func (ts *testScript) cmdSymlink(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v symlink", want)
}
if len(args) != 3 || args[1] != "->" {
ts.fatalf("usage: symlink file -> target")
@@ -885,9 +922,9 @@ func (ts *testScript) cmdSymlink(neg bool, args []string) {
}
// wait waits for background commands to exit, setting stderr and stdout to their result.
-func (ts *testScript) cmdWait(neg bool, args []string) {
- if neg {
- ts.fatalf("unsupported: ! wait")
+func (ts *testScript) cmdWait(want simpleStatus, args []string) {
+ if want != success {
+ ts.fatalf("unsupported: %v wait", want)
}
if len(args) > 0 {
ts.fatalf("usage: wait")
@@ -895,34 +932,24 @@ func (ts *testScript) cmdWait(neg bool, args []string) {
var stdouts, stderrs []string
for _, bg := range ts.background {
- <-bg.wait
+ <-bg.done
- args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...)
- fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState)
+ args := append([]string{filepath.Base(bg.args[0])}, bg.args[1:]...)
+ fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.err)
- cmdStdout := bg.cmd.Stdout.(*strings.Builder).String()
+ cmdStdout := bg.stdout.String()
if cmdStdout != "" {
fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
stdouts = append(stdouts, cmdStdout)
}
- cmdStderr := bg.cmd.Stderr.(*strings.Builder).String()
+ cmdStderr := bg.stderr.String()
if cmdStderr != "" {
fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
stderrs = append(stderrs, cmdStderr)
}
- if bg.cmd.ProcessState.Success() {
- if bg.neg {
- ts.fatalf("unexpected command success")
- }
- } else {
- if testCtx.Err() != nil {
- ts.fatalf("test timed out while running command")
- } else if !bg.neg {
- ts.fatalf("unexpected command failure")
- }
- }
+ ts.checkCmd(bg)
}
ts.stdout = strings.Join(stdouts, "")
@@ -950,58 +977,176 @@ func (ts *testScript) check(err error) {
}
}
+func (ts *testScript) checkCmd(bg *backgroundCmd) {
+ select {
+ case <-bg.done:
+ default:
+ panic("checkCmd called when not done")
+ }
+
+ if bg.err == nil {
+ if bg.want == failure {
+ ts.fatalf("unexpected command success")
+ }
+ return
+ }
+
+ if errors.Is(bg.err, context.DeadlineExceeded) {
+ ts.fatalf("test timed out while running command")
+ }
+
+ if errors.Is(bg.err, context.Canceled) {
+ // The process was still running at the end of the test.
+ // The test must not depend on its exit status.
+ if bg.want != successOrFailure {
+ ts.fatalf("unexpected background command remaining at test end")
+ }
+ return
+ }
+
+ if bg.want == success {
+ ts.fatalf("unexpected command failure")
+ }
+}
+
// exec runs the given command line (an actual subprocess, not simulated)
// in ts.cd with environment ts.env and then returns collected standard output and standard error.
func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) {
- cmd := exec.Command(command, args...)
- cmd.Dir = ts.cd
- cmd.Env = append(ts.env, "PWD="+ts.cd)
- var stdoutBuf, stderrBuf strings.Builder
- cmd.Stdout = &stdoutBuf
- cmd.Stderr = &stderrBuf
- if err = cmd.Start(); err == nil {
- err = ctxWait(testCtx, cmd)
+ bg, err := ts.startBackground(success, command, args...)
+ if err != nil {
+ return "", "", err
}
- return stdoutBuf.String(), stderrBuf.String(), err
+ <-bg.done
+ return bg.stdout.String(), bg.stderr.String(), bg.err
}
-// execBackground starts the given command line (an actual subprocess, not simulated)
+// startBackground starts the given command line (an actual subprocess, not simulated)
// in ts.cd with environment ts.env.
-func (ts *testScript) execBackground(command string, args ...string) (*exec.Cmd, error) {
+func (ts *testScript) startBackground(want simpleStatus, command string, args ...string) (*backgroundCmd, error) {
+ done := make(chan struct{})
+ bg := &backgroundCmd{
+ want: want,
+ args: append([]string{command}, args...),
+ done: done,
+ cancel: func() {},
+ }
+
+ ctx := context.Background()
+ gracePeriod := 100 * time.Millisecond
+ if deadline, ok := ts.t.Deadline(); ok {
+ timeout := time.Until(deadline)
+ // If time allows, increase the termination grace period to 5% of the
+ // remaining time.
+ if gp := timeout / 20; gp > gracePeriod {
+ gracePeriod = gp
+ }
+
+ // Send the first termination signal with two grace periods remaining.
+ // If it still hasn't finished after the first period has elapsed,
+ // we'll escalate to os.Kill with a second period remaining until the
+ // test deadline..
+ timeout -= 2 * gracePeriod
+
+ if timeout <= 0 {
+ // The test has less than the grace period remaining. There is no point in
+ // even starting the command, because it will be terminated immediately.
+ // Save the expense of starting it in the first place.
+ bg.err = context.DeadlineExceeded
+ close(done)
+ return bg, nil
+ }
+
+ ctx, bg.cancel = context.WithTimeout(ctx, timeout)
+ }
+
cmd := exec.Command(command, args...)
cmd.Dir = ts.cd
cmd.Env = append(ts.env, "PWD="+ts.cd)
- var stdoutBuf, stderrBuf strings.Builder
- cmd.Stdout = &stdoutBuf
- cmd.Stderr = &stderrBuf
- return cmd, cmd.Start()
-}
-
-// ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done.
-//
-// This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill.
-// (See https://golang.org/issue/21135.)
-func ctxWait(ctx context.Context, cmd *exec.Cmd) error {
- errc := make(chan error, 1)
- go func() { errc <- cmd.Wait() }()
-
- select {
- case err := <-errc:
- return err
- case <-ctx.Done():
- interruptProcess(cmd.Process)
- return <-errc
+ cmd.Stdout = &bg.stdout
+ cmd.Stderr = &bg.stderr
+ if err := cmd.Start(); err != nil {
+ bg.cancel()
+ return nil, err
}
+
+ go func() {
+ bg.err = waitOrStop(ctx, cmd, stopSignal(), gracePeriod)
+ close(done)
+ }()
+ return bg, nil
}
-// interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise.
-func interruptProcess(p *os.Process) {
- if err := p.Signal(os.Interrupt); err != nil {
+// stopSignal returns the appropriate signal to use to request that a process
+// stop execution.
+func stopSignal() os.Signal {
+ if runtime.GOOS == "windows" {
// Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
// Windows; using it with os.Process.Signal will return an error.”
// Fall back to Kill instead.
- p.Kill()
+ return os.Kill
}
+ return os.Interrupt
+}
+
+// waitOrStop waits for the already-started command cmd by calling its Wait method.
+//
+// If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal.
+// If killDelay is positive, waitOrStop waits that additional period for Wait to return before sending os.Kill.
+//
+// This function is copied from the one added to x/playground/internal in
+// http://golang.org/cl/228438.
+func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error {
+ if cmd.Process == nil {
+ panic("waitOrStop called with a nil cmd.Process — missing Start call?")
+ }
+ if interrupt == nil {
+ panic("waitOrStop requires a non-nil interrupt signal")
+ }
+
+ errc := make(chan error)
+ go func() {
+ select {
+ case errc <- nil:
+ return
+ case <-ctx.Done():
+ }
+
+ err := cmd.Process.Signal(interrupt)
+ if err == nil {
+ err = ctx.Err() // Report ctx.Err() as the reason we interrupted.
+ } else if err.Error() == "os: process already finished" {
+ errc <- nil
+ return
+ }
+
+ if killDelay > 0 {
+ timer := time.NewTimer(killDelay)
+ select {
+ // Report ctx.Err() as the reason we interrupted the process...
+ case errc <- ctx.Err():
+ timer.Stop()
+ return
+ // ...but after killDelay has elapsed, fall back to a stronger signal.
+ case <-timer.C:
+ }
+
+ // Wait still hasn't returned.
+ // Kill the process harder to make sure that it exits.
+ //
+ // Ignore any error: if cmd.Process has already terminated, we still
+ // want to send ctx.Err() (or the error from the Interrupt call)
+ // to properly attribute the signal that may have terminated it.
+ _ = cmd.Process.Kill()
+ }
+
+ errc <- err
+ }()
+
+ waitErr := cmd.Wait()
+ if interruptErr := <-errc; interruptErr != nil {
+ return interruptErr
+ }
+ return waitErr
}
// expand applies environment variable expansion to the string s.
@@ -1044,7 +1189,7 @@ type condition struct {
// A command is a complete command parsed from a script.
type command struct {
- neg bool // if true, expect the command to fail
+ want simpleStatus
conds []condition // all must be satisfied
name string // the name of the command; must be non-empty
args []string // shell-expanded arguments following name
@@ -1079,11 +1224,13 @@ func (ts *testScript) parse(line string) command {
// Command prefix ! means negate the expectations about this command:
// go command should fail, match should not be found, etc.
- if arg == "!" {
- if cmd.neg {
- ts.fatalf("duplicated '!' token")
+ // Prefix ? means allow either success or failure.
+ switch want := simpleStatus(arg); want {
+ case failure, successOrFailure:
+ if cmd.want != "" {
+ ts.fatalf("duplicated '!' or '?' token")
}
- cmd.neg = true
+ cmd.want = want
return
}
@@ -1234,6 +1381,8 @@ var diffTests = []struct {
}
func TestDiff(t *testing.T) {
+ t.Parallel()
+
for _, tt := range diffTests {
// Turn spaces into \n.
text1 := strings.ReplaceAll(tt.text1, " ", "\n")