diff options
author | Ian Lance Taylor <iant@golang.org> | 2020-07-27 22:27:54 -0700 |
---|---|---|
committer | Ian Lance Taylor <iant@golang.org> | 2020-08-01 11:21:40 -0700 |
commit | f75af8c1464e948b5e166cf5ab09ebf0d82fc253 (patch) | |
tree | 3ba3299859b504bdeb477727471216bd094a0191 /libgo/go/cmd/go/script_test.go | |
parent | 75a23e59031fe673fc3b2e60fd1fe5f4c70ecb85 (diff) | |
download | gcc-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.go | 483 |
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") |