Source File
exec.go
Belonging Package
internal/testenv
// 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 (
)
// 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.
//
// On some platforms MustHaveExec checks for exec support by re-executing the
// current executable, which must be a binary built by 'go test'.
// We intentionally do not provide a HasExec function because of the risk of
// inappropriate recursion in TestMain functions.
//
// To check for exec support outside of a test, just try to exec the command.
// If exec is not supported, testenv.SyscallIsNotSupported will return true
// for the resulting error.
func ( testing.TB) {
tryExecOnce.Do(func() {
tryExecErr = tryExec()
})
if tryExecErr != nil {
.Skipf("skipping test: cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, tryExecErr)
}
}
var (
tryExecOnce sync.Once
tryExecErr error
)
func tryExec() error {
switch runtime.GOOS {
case "wasip1", "js", "ios":
default:
// Assume that exec always works on non-mobile platforms and Android.
return nil
}
// ios has an exec syscall but on real iOS devices it might return a
// permission error. In an emulated environment (such as a Corellium host)
// it might succeed, so if we need to exec we'll just have to try it and
// find out.
//
// As of 2023-04-19 wasip1 and js don't have exec syscalls at all, but we
// may as well use the same path so that this branch can be tested without
// an ios environment.
if !testing.Testing() {
// This isn't a standard 'go test' binary, so we don't know how to
// self-exec in a way that should succeed without side effects.
// Just forget it.
return errors.New("can't probe for exec support with a non-test executable")
}
// We know that this is a test executable. We should be able to run it with a
// no-op flag to check for overall exec support.
, := os.Executable()
if != nil {
return fmt.Errorf("can't probe for exec support: %w", )
}
:= exec.Command(, "-test.list=^$")
.Env = origEnv
return .Run()
}
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 ( testing.TB, string) {
MustHaveExec()
, := execPaths.Load()
if ! {
_, = exec.LookPath()
, _ = execPaths.LoadOrStore(, )
}
if != nil {
.Skipf("skipping test: %s: %s", , )
}
}
// 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.
//
// If the caller wants to set cmd.Dir, set it before calling this function,
// so PWD will be set correctly in the environment.
func ( *exec.Cmd) *exec.Cmd {
if .Env != nil {
panic("environment already set")
}
for , := range .Environ() {
// Exclude GODEBUG from the environment to prevent its output
// from breaking tests that are trying to parse other command output.
if strings.HasPrefix(, "GODEBUG=") {
continue
}
// Exclude GOTRACEBACK for the same reason.
if strings.HasPrefix(, "GOTRACEBACK=") {
continue
}
.Env = append(.Env, )
}
return
}
// 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
// - if the test has a deadline, adds a Context timeout and WaitDelay
// for an arbitrary grace period before the test's deadline expires,
// - 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 ( testing.TB, context.Context, string, ...string) *exec.Cmd {
.Helper()
MustHaveExec()
var (
context.CancelFunc
time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
)
if , := .(interface {
testing.TB
() (time.Time, bool)
}); {
if , := .(); {
// Start with a minimum grace period, just long enough to consume the
// output of a reasonable program after it terminates.
= 100 * time.Millisecond
if := os.Getenv("GO_TEST_TIMEOUT_SCALE"); != "" {
, := strconv.Atoi()
if != nil {
.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", )
}
*= time.Duration()
}
// If time allows, increase the termination grace period to 5% of the
// test's remaining time.
:= time.Until()
if := / 20; > {
=
}
// When we run commands that execute subprocesses, we want to reserve two
// grace periods to clean up: one for the delay between the first
// termination signal being sent (via the Cancel callback when the Context
// expires) and the process being forcibly terminated (via the WaitDelay
// field), and a second one for the delay between the process being
// terminated and the test logging its output for debugging.
//
// (We want to ensure that the test process itself has enough time to
// log the output before it is also terminated.)
:= - 2*
if , := .Deadline(); ! || time.Until() > {
// Either ctx doesn't have a deadline, or its deadline would expire
// after (or too close before) the test has already timed out.
// Add a shorter timeout so that the test will produce useful output.
, = context.WithTimeout(, )
}
}
}
:= exec.CommandContext(, , ...)
.Cancel = func() error {
if != nil && .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.
.Errorf("test timed out while running command: %v", )
} 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.
.Logf("%v: terminating command: %v", .Err(), )
}
return .Process.Signal(Sigquit)
}
.WaitDelay =
.Cleanup(func() {
if != nil {
()
}
if .Process != nil && .ProcessState == nil {
.Errorf("command was started, but test did not wait for it to complete: %v", )
}
})
return
}
// Command is like exec.Command, but applies the same changes as
// testenv.CommandContext (with a default Context).
func ( testing.TB, string, ...string) *exec.Cmd {
.Helper()
return CommandContext(, context.Background(), , ...)
}
The pages are generated with Golds v0.7.0-preview. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |