Skip to content

Commit

Permalink
testenv: abstract run-with-timeout into testenv
Browse files Browse the repository at this point in the history
This lifts the logic to run a subcommand with a timeout in a test from
the runtime's runTestProg into testenv. The implementation is
unchanged in this CL. We'll improve it in a future CL.

Currently, tests that run subcommands usually just timeout with no
useful output if the subcommand runs for too long. This is a step
toward improving this.

For #37405.

Change-Id: I2298770db516e216379c4c438e05d23cbbdda51d
Reviewed-on: https://go-review.googlesource.com/c/go/+/370701
Trust: Austin Clements <austin@google.com>
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
  • Loading branch information
aclements committed Dec 12, 2021
1 parent 9c6e8f6 commit 6b89773
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 48 deletions.
51 changes: 51 additions & 0 deletions src/internal/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package testenv

import (
"bytes"
"errors"
"flag"
"internal/cfg"
Expand All @@ -22,6 +23,7 @@ import (
"strings"
"sync"
"testing"
"time"
)

// Builder reports the name of the builder running this test
Expand Down Expand Up @@ -306,3 +308,52 @@ func SkipIfShortAndSlow(t testing.TB) {
t.Skipf("skipping test in -short mode on %s", runtime.GOARCH)
}
}

// RunWithTimeout runs cmd and returns its combined output. If the
// subprocess exits with a non-zero status, it will log that status
// and return a non-nil error, but this is not considered fatal.
func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) {
args := cmd.Args
if args == nil {
args = []string{cmd.Path}
}

var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
if err := cmd.Start(); err != nil {
t.Fatalf("starting %s: %v", args, err)
}

// If the process doesn't complete within 1 minute,
// assume it is hanging and kill it to get a stack trace.
p := cmd.Process
done := make(chan bool)
go func() {
scale := 1
// This GOARCH/GOOS test is copied from cmd/dist/test.go.
// TODO(iant): Have cmd/dist update the environment variable.
if runtime.GOARCH == "arm" || runtime.GOOS == "windows" {
scale = 2
}
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
if sc, err := strconv.Atoi(s); err == nil {
scale = sc
}
}

select {
case <-done:
case <-time.After(time.Duration(scale) * time.Minute):
p.Signal(Sigquit)
}
}()

err := cmd.Wait()
if err != nil {
t.Logf("%s exit status: %v", args, err)
}
close(done)

return b.Bytes(), err
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build windows || plan9 || (js && wasm)

package runtime_test
package testenv

import "os"

// sigquit is the signal to send to kill a hanging testdata program.
// Sigquit is the signal to send to kill a hanging subprocess.
// On Unix we send SIGQUIT, but on non-Unix we only have os.Kill.
var sigquit = os.Kill
var Sigquit = os.Kill
13 changes: 13 additions & 0 deletions src/internal/testenv/testenv_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris

package testenv

import "syscall"

// Sigquit is the signal to send to kill a hanging subprocess.
// Send SIGQUIT to get a stack trace.
var Sigquit = syscall.SIGQUIT
41 changes: 2 additions & 39 deletions src/runtime/crash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ import (
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"
)

var toRemove []string
Expand Down Expand Up @@ -71,43 +69,8 @@ func runBuiltTestProg(t *testing.T, exe, name string, env ...string) string {
if testing.Short() {
cmd.Env = append(cmd.Env, "RUNTIME_TEST_SHORT=1")
}
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
if err := cmd.Start(); err != nil {
t.Fatalf("starting %s %s: %v", exe, name, err)
}

// If the process doesn't complete within 1 minute,
// assume it is hanging and kill it to get a stack trace.
p := cmd.Process
done := make(chan bool)
go func() {
scale := 1
// This GOARCH/GOOS test is copied from cmd/dist/test.go.
// TODO(iant): Have cmd/dist update the environment variable.
if runtime.GOARCH == "arm" || runtime.GOOS == "windows" {
scale = 2
}
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
if sc, err := strconv.Atoi(s); err == nil {
scale = sc
}
}

select {
case <-done:
case <-time.After(time.Duration(scale) * time.Minute):
p.Signal(sigquit)
}
}()

if err := cmd.Wait(); err != nil {
t.Logf("%s %s exit status: %v", exe, name, err)
}
close(done)

return b.String()
out, _ := testenv.RunWithTimeout(t, cmd)
return string(out)
}

var serializeBuild = make(chan bool, 2)
Expand Down
6 changes: 1 addition & 5 deletions src/runtime/crash_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,12 @@ import (
"unsafe"
)

// sigquit is the signal to send to kill a hanging testdata program.
// Send SIGQUIT to get a stack trace.
var sigquit = syscall.SIGQUIT

func init() {
if runtime.Sigisblocked(int(syscall.SIGQUIT)) {
// We can't use SIGQUIT to kill subprocesses because
// it's blocked. Use SIGKILL instead. See issue
// #19196 for an example of when this happens.
sigquit = syscall.SIGKILL
testenv.Sigquit = syscall.SIGKILL
}
}

Expand Down

0 comments on commit 6b89773

Please sign in to comment.