mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
cmd/go: bail out from script tests earlier when a timeout occurs
(I needed this to debug an accidental infinite-recursion I introduced while revising CL 293689.) Fixes #38768 Change-Id: I306122f15b5bbd2fc5e836b32fd4dd5992ea891e Reviewed-on: https://go-review.googlesource.com/c/go/+/302052 Trust: Bryan C. Mills <bcmills@google.com> Reviewed-by: Michael Matloob <matloob@golang.org> Run-TryBot: Bryan C. Mills <bcmills@google.com> TryBot-Result: Go Bot <gobot@golang.org>
This commit is contained in:
parent
1824667259
commit
72d98df88e
1 changed files with 71 additions and 61 deletions
|
|
@ -41,6 +41,33 @@ func TestScript(t *testing.T) {
|
||||||
testenv.MustHaveGoBuild(t)
|
testenv.MustHaveGoBuild(t)
|
||||||
testenv.SkipIfShortAndSlow(t)
|
testenv.SkipIfShortAndSlow(t)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
gracePeriod = 100 * time.Millisecond
|
||||||
|
)
|
||||||
|
if deadline, ok := 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we run commands that execute subprocesses, we want to reserve two
|
||||||
|
// grace periods to clean up. We will send the first termination signal when
|
||||||
|
// the context expires, then wait one grace period for the process to
|
||||||
|
// produce whatever useful output it can (such as a stack trace). After the
|
||||||
|
// first grace period expires, we'll escalate to os.Kill, leaving the second
|
||||||
|
// grace period for the test function to record its output before the test
|
||||||
|
// process itself terminates.
|
||||||
|
timeout -= 2 * gracePeriod
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
}
|
||||||
|
|
||||||
files, err := filepath.Glob("testdata/script/*.txt")
|
files, err := filepath.Glob("testdata/script/*.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -50,12 +77,21 @@ func TestScript(t *testing.T) {
|
||||||
name := strings.TrimSuffix(filepath.Base(file), ".txt")
|
name := strings.TrimSuffix(filepath.Base(file), ".txt")
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ts := &testScript{t: t, name: name, file: file}
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
ts := &testScript{
|
||||||
|
t: t,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
gracePeriod: gracePeriod,
|
||||||
|
name: name,
|
||||||
|
file: file,
|
||||||
|
}
|
||||||
ts.setup()
|
ts.setup()
|
||||||
if !*testWork {
|
if !*testWork {
|
||||||
defer removeAll(ts.workdir)
|
defer removeAll(ts.workdir)
|
||||||
}
|
}
|
||||||
ts.run()
|
ts.run()
|
||||||
|
cancel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +99,9 @@ func TestScript(t *testing.T) {
|
||||||
// A testScript holds execution state for a single test script.
|
// A testScript holds execution state for a single test script.
|
||||||
type testScript struct {
|
type testScript struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
gracePeriod time.Duration
|
||||||
workdir string // temporary work dir ($WORK)
|
workdir string // temporary work dir ($WORK)
|
||||||
log bytes.Buffer // test execution log (printed at end of test)
|
log bytes.Buffer // test execution log (printed at end of test)
|
||||||
mark int // offset of next log truncation
|
mark int // offset of next log truncation
|
||||||
|
|
@ -83,7 +122,6 @@ type testScript struct {
|
||||||
type backgroundCmd struct {
|
type backgroundCmd struct {
|
||||||
want simpleStatus
|
want simpleStatus
|
||||||
args []string
|
args []string
|
||||||
cancel context.CancelFunc
|
|
||||||
done <-chan struct{}
|
done <-chan struct{}
|
||||||
err error
|
err error
|
||||||
stdout, stderr strings.Builder
|
stdout, stderr strings.Builder
|
||||||
|
|
@ -109,6 +147,10 @@ var extraEnvKeys = []string{
|
||||||
|
|
||||||
// setup sets up the test execution temporary directory and environment.
|
// setup sets up the test execution temporary directory and environment.
|
||||||
func (ts *testScript) setup() {
|
func (ts *testScript) setup() {
|
||||||
|
if err := ts.ctx.Err(); err != nil {
|
||||||
|
ts.t.Fatalf("test interrupted during setup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
StartProxy()
|
StartProxy()
|
||||||
ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name)
|
ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name)
|
||||||
ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777))
|
ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777))
|
||||||
|
|
@ -200,9 +242,7 @@ func (ts *testScript) run() {
|
||||||
// On a normal exit from the test loop, background processes are cleaned up
|
// On a normal exit from the test loop, background processes are cleaned up
|
||||||
// before we print PASS. If we return early (e.g., due to a test failure),
|
// 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.
|
// don't print anything about the processes that were still running.
|
||||||
for _, bg := range ts.background {
|
ts.cancel()
|
||||||
bg.cancel()
|
|
||||||
}
|
|
||||||
for _, bg := range ts.background {
|
for _, bg := range ts.background {
|
||||||
<-bg.done
|
<-bg.done
|
||||||
}
|
}
|
||||||
|
|
@ -275,6 +315,10 @@ Script:
|
||||||
fmt.Fprintf(&ts.log, "> %s\n", line)
|
fmt.Fprintf(&ts.log, "> %s\n", line)
|
||||||
|
|
||||||
for _, cond := range parsed.conds {
|
for _, cond := range parsed.conds {
|
||||||
|
if err := ts.ctx.Err(); err != nil {
|
||||||
|
ts.fatalf("test interrupted: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short).
|
// Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short).
|
||||||
//
|
//
|
||||||
// NOTE: If you make changes here, update testdata/script/README too!
|
// NOTE: If you make changes here, update testdata/script/README too!
|
||||||
|
|
@ -356,9 +400,7 @@ Script:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bg := range ts.background {
|
ts.cancel()
|
||||||
bg.cancel()
|
|
||||||
}
|
|
||||||
ts.cmdWait(success, nil)
|
ts.cmdWait(success, nil)
|
||||||
|
|
||||||
// Final phase ended.
|
// Final phase ended.
|
||||||
|
|
@ -798,9 +840,7 @@ func (ts *testScript) cmdSkip(want simpleStatus, args []string) {
|
||||||
|
|
||||||
// Before we mark the test as skipped, shut down any background processes and
|
// Before we mark the test as skipped, shut down any background processes and
|
||||||
// make sure they have returned the correct status.
|
// make sure they have returned the correct status.
|
||||||
for _, bg := range ts.background {
|
ts.cancel()
|
||||||
bg.cancel()
|
|
||||||
}
|
|
||||||
ts.cmdWait(success, nil)
|
ts.cmdWait(success, nil)
|
||||||
|
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
|
|
@ -1068,35 +1108,6 @@ func (ts *testScript) startBackground(want simpleStatus, command string, args ..
|
||||||
want: want,
|
want: want,
|
||||||
args: append([]string{command}, args...),
|
args: append([]string{command}, args...),
|
||||||
done: done,
|
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 := exec.Command(command, args...)
|
||||||
|
|
@ -1105,12 +1116,11 @@ func (ts *testScript) startBackground(want simpleStatus, command string, args ..
|
||||||
cmd.Stdout = &bg.stdout
|
cmd.Stdout = &bg.stdout
|
||||||
cmd.Stderr = &bg.stderr
|
cmd.Stderr = &bg.stderr
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
bg.cancel()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
bg.err = waitOrStop(ctx, cmd, stopSignal(), gracePeriod)
|
bg.err = waitOrStop(ts.ctx, cmd, stopSignal(), ts.gracePeriod)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
return bg, nil
|
return bg, nil
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue