From 661e931dd120484cc1318792bc93ace00bbde088 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 31 Oct 2022 16:42:30 -0400 Subject: [PATCH] internal/testenv: move helpers related to os/exec to a separate file (Suggested by Austin in CL 445596.) Change-Id: Ic774d551e8085d15435ef2b3c78d0169a365ce41 Reviewed-on: https://go-review.googlesource.com/c/go/+/446642 Auto-Submit: Bryan Mills TryBot-Result: Gopher Robot Run-TryBot: Bryan Mills Reviewed-by: Ian Lance Taylor --- src/internal/testenv/exec.go | 173 ++++++++++++++++++++++++++++++++ src/internal/testenv/testenv.go | 158 ----------------------------- 2 files changed, 173 insertions(+), 158 deletions(-) create mode 100644 src/internal/testenv/exec.go diff --git a/src/internal/testenv/exec.go b/src/internal/testenv/exec.go new file mode 100644 index 0000000000..c78df45382 --- /dev/null +++ b/src/internal/testenv/exec.go @@ -0,0 +1,173 @@ +// Copyright 2015 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. + +package testenv + +import ( + "context" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" +) + +// HasExec reports whether the current system can start new processes +// using os.StartProcess or (more commonly) exec.Command. +func HasExec() bool { + switch runtime.GOOS { + case "js", "ios": + return false + } + return true +} + +// MustHaveExec checks that the current system can start new processes +// using os.StartProcess or (more commonly) exec.Command. +// If not, MustHaveExec calls t.Skip with an explanation. +func MustHaveExec(t testing.TB) { + if !HasExec() { + t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) + } +} + +var execPaths sync.Map // path -> error + +// MustHaveExecPath checks that the current system can start the named executable +// using os.StartProcess or (more commonly) exec.Command. +// If not, MustHaveExecPath calls t.Skip with an explanation. +func MustHaveExecPath(t testing.TB, path string) { + MustHaveExec(t) + + err, found := execPaths.Load(path) + if !found { + _, err = exec.LookPath(path) + err, _ = execPaths.LoadOrStore(path, err) + } + if err != nil { + t.Skipf("skipping test: %s: %s", path, err) + } +} + +// CleanCmdEnv will fill cmd.Env with the environment, excluding certain +// variables that could modify the behavior of the Go tools such as +// GODEBUG and GOTRACEBACK. +func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { + if cmd.Env != nil { + panic("environment already set") + } + for _, env := range os.Environ() { + // Exclude GODEBUG from the environment to prevent its output + // from breaking tests that are trying to parse other command output. + if strings.HasPrefix(env, "GODEBUG=") { + continue + } + // Exclude GOTRACEBACK for the same reason. + if strings.HasPrefix(env, "GOTRACEBACK=") { + continue + } + cmd.Env = append(cmd.Env, env) + } + return cmd +} + +// CommandContext is like exec.CommandContext, but: +// - skips t if the platform does not support os/exec, +// - sends SIGQUIT (if supported by the platform) instead of SIGKILL +// in its Cancel function +// - adds a timeout (with an arbitrary grace period) before the test's deadline expires, +// - sets a WaitDelay for an arbitrary grace period, +// - fails the test if the command does not complete before the test's deadline, and +// - sets a Cleanup function that verifies that the test did not leak a subprocess. +func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { + t.Helper() + MustHaveExec(t) + + var ( + gracePeriod = 100 * time.Millisecond + cancel context.CancelFunc + ) + if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + scale, err := strconv.Atoi(s) + if err != nil { + t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) + } + gracePeriod *= time.Duration(scale) + } + + if t, ok := t.(interface { + testing.TB + Deadline() (time.Time, bool) + }); ok { + if td, ok := t.Deadline(); ok { + if cd, ok := ctx.Deadline(); !ok || cd.Sub(td) > gracePeriod { + // Either ctx doesn't have a deadline, or its deadline would expire + // after (or too close before) the test has already timed out. + // Compute a new timeout that will expire before the test does so that + // we can terminate the subprocess with a more useful signal. + + timeout := time.Until(td) + + // 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 + + ctx, cancel = context.WithTimeout(ctx, timeout) + t.Cleanup(cancel) + } + } + } + + cmd := exec.CommandContext(ctx, name, args...) + cmd.Cancel = func() error { + if cancel != nil && ctx.Err() == context.DeadlineExceeded { + // The command timed out due to running too close to the test's deadline. + // There is no way the test did that intentionally — it's too close to the + // wire! — so mark it as a test failure. That way, if the test expects the + // command to fail for some other reason, it doesn't have to distinguish + // between that reason and a timeout. + t.Errorf("test timed out while running command: %v", cmd) + } else { + // The command is being terminated due to ctx being canceled, but + // apparently not due to an explicit test deadline that we added. + // Log that information in case it is useful for diagnosing a failure, + // but don't actually fail the test because of it. + t.Logf("%v: terminating command: %v", ctx.Err(), cmd) + } + return cmd.Process.Signal(Sigquit) + } + cmd.WaitDelay = gracePeriod + + t.Cleanup(func() { + if cancel != nil { + cancel() + } + if cmd.Process != nil && cmd.ProcessState == nil { + t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) + } + }) + + return cmd +} + +// Command is like exec.Command, but applies the same changes as +// testenv.CommandContext (with a default Context). +func Command(t testing.TB, name string, args ...string) *exec.Cmd { + t.Helper() + return CommandContext(t, context.Background(), name, args...) +} diff --git a/src/internal/testenv/testenv.go b/src/internal/testenv/testenv.go index b61a44c855..6190000d02 100644 --- a/src/internal/testenv/testenv.go +++ b/src/internal/testenv/testenv.go @@ -11,7 +11,6 @@ package testenv import ( - "context" "errors" "flag" "fmt" @@ -26,7 +25,6 @@ import ( "strings" "sync" "testing" - "time" ) // Builder reports the name of the builder running this test @@ -217,16 +215,6 @@ func GoTool() (string, error) { return goBin, nil } -// HasExec reports whether the current system can start new processes -// using os.StartProcess or (more commonly) exec.Command. -func HasExec() bool { - switch runtime.GOOS { - case "js", "ios": - return false - } - return true -} - // HasSrc reports whether the entire source tree is available under GOROOT. func HasSrc() bool { switch runtime.GOOS { @@ -236,33 +224,6 @@ func HasSrc() bool { return true } -// MustHaveExec checks that the current system can start new processes -// using os.StartProcess or (more commonly) exec.Command. -// If not, MustHaveExec calls t.Skip with an explanation. -func MustHaveExec(t testing.TB) { - if !HasExec() { - t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) - } -} - -var execPaths sync.Map // path -> error - -// MustHaveExecPath checks that the current system can start the named executable -// using os.StartProcess or (more commonly) exec.Command. -// If not, MustHaveExecPath calls t.Skip with an explanation. -func MustHaveExecPath(t testing.TB, path string) { - MustHaveExec(t) - - err, found := execPaths.Load(path) - if !found { - _, err = exec.LookPath(path) - err, _ = execPaths.LoadOrStore(path, err) - } - if err != nil { - t.Skipf("skipping test: %s: %s", path, err) - } -} - // HasExternalNetwork reports whether the current system can use // external (non-localhost) networks. func HasExternalNetwork() bool { @@ -357,125 +318,6 @@ func SkipFlakyNet(t testing.TB) { } } -// CleanCmdEnv will fill cmd.Env with the environment, excluding certain -// variables that could modify the behavior of the Go tools such as -// GODEBUG and GOTRACEBACK. -func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { - if cmd.Env != nil { - panic("environment already set") - } - for _, env := range os.Environ() { - // Exclude GODEBUG from the environment to prevent its output - // from breaking tests that are trying to parse other command output. - if strings.HasPrefix(env, "GODEBUG=") { - continue - } - // Exclude GOTRACEBACK for the same reason. - if strings.HasPrefix(env, "GOTRACEBACK=") { - continue - } - cmd.Env = append(cmd.Env, env) - } - return cmd -} - -// CommandContext is like exec.CommandContext, but: -// - skips t if the platform does not support os/exec, -// - sends SIGQUIT (if supported by the platform) instead of SIGKILL -// in its Cancel function -// - adds a timeout (with an arbitrary grace period) before the test's deadline expires, -// - sets a WaitDelay for an arbitrary grace period, -// - fails the test if the command does not complete before the test's deadline, and -// - sets a Cleanup function that verifies that the test did not leak a subprocess. -func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { - t.Helper() - MustHaveExec(t) - - var ( - gracePeriod = 100 * time.Millisecond - cancel context.CancelFunc - ) - if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { - scale, err := strconv.Atoi(s) - if err != nil { - t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) - } - gracePeriod *= time.Duration(scale) - } - - if t, ok := t.(interface { - testing.TB - Deadline() (time.Time, bool) - }); ok { - if td, ok := t.Deadline(); ok { - if cd, ok := ctx.Deadline(); !ok || cd.Sub(td) > gracePeriod { - // Either ctx doesn't have a deadline, or its deadline would expire - // after (or too close before) the test has already timed out. - // Compute a new timeout that will expire before the test does so that - // we can terminate the subprocess with a more useful signal. - - timeout := time.Until(td) - - // 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 - - ctx, cancel = context.WithTimeout(ctx, timeout) - t.Cleanup(cancel) - } - } - } - - cmd := exec.CommandContext(ctx, name, args...) - cmd.Cancel = func() error { - if cancel != nil && ctx.Err() == context.DeadlineExceeded { - // The command timed out due to running too close to the test's deadline. - // There is no way the test did that intentionally — it's too close to the - // wire! — so mark it as a test failure. That way, if the test expects the - // command to fail for some other reason, it doesn't have to distinguish - // between that reason and a timeout. - t.Errorf("test timed out while running command: %v", cmd) - } else { - // The command is being terminated due to ctx being canceled, but - // apparently not due to an explicit test deadline that we added. - // Log that information in case it is useful for diagnosing a failure, - // but don't actually fail the test because of it. - t.Logf("%v: terminating command: %v", ctx.Err(), cmd) - } - return cmd.Process.Signal(Sigquit) - } - cmd.WaitDelay = gracePeriod - - t.Cleanup(func() { - if cancel != nil { - cancel() - } - if cmd.Process != nil && cmd.ProcessState == nil { - t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) - } - }) - - return cmd -} - -// Command is like exec.Command, but applies the same changes as -// testenv.CommandContext (with a default Context). -func Command(t testing.TB, name string, args ...string) *exec.Cmd { - t.Helper() - return CommandContext(t, context.Background(), name, args...) -} - // CPUIsSlow reports whether the CPU running the test is suspected to be slow. func CPUIsSlow() bool { switch runtime.GOARCH { -- 2.48.1