Source file src/internal/testenv/exec.go
1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package testenv 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "os" 12 "os/exec" 13 "runtime" 14 "strconv" 15 "strings" 16 "sync" 17 "testing" 18 "time" 19 ) 20 21 // MustHaveExec checks that the current system can start new processes 22 // using os.StartProcess or (more commonly) exec.Command. 23 // If not, MustHaveExec calls t.Skip with an explanation. 24 // 25 // On some platforms MustHaveExec checks for exec support by re-executing the 26 // current executable, which must be a binary built by 'go test'. 27 // We intentionally do not provide a HasExec function because of the risk of 28 // inappropriate recursion in TestMain functions. 29 // 30 // To check for exec support outside of a test, just try to exec the command. 31 // If exec is not supported, testenv.SyscallIsNotSupported will return true 32 // for the resulting error. 33 func MustHaveExec(t testing.TB) { 34 if err := tryExec(); err != nil { 35 msg := fmt.Sprintf("cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err) 36 if t == nil { 37 panic(msg) 38 } 39 t.Helper() 40 t.Skip("skipping test:", msg) 41 } 42 } 43 44 var tryExec = sync.OnceValue(func() error { 45 switch runtime.GOOS { 46 case "wasip1", "js", "ios": 47 default: 48 // Assume that exec always works on non-mobile platforms and Android. 49 return nil 50 } 51 52 // ios has an exec syscall but on real iOS devices it might return a 53 // permission error. In an emulated environment (such as a Corellium host) 54 // it might succeed, so if we need to exec we'll just have to try it and 55 // find out. 56 // 57 // As of 2023-04-19 wasip1 and js don't have exec syscalls at all, but we 58 // may as well use the same path so that this branch can be tested without 59 // an ios environment. 60 61 if !testing.Testing() { 62 // This isn't a standard 'go test' binary, so we don't know how to 63 // self-exec in a way that should succeed without side effects. 64 // Just forget it. 65 return errors.New("can't probe for exec support with a non-test executable") 66 } 67 68 // We know that this is a test executable. We should be able to run it with a 69 // no-op flag to check for overall exec support. 70 exe, err := exePath() 71 if err != nil { 72 return fmt.Errorf("can't probe for exec support: %w", err) 73 } 74 cmd := exec.Command(exe, "-test.list=^$") 75 cmd.Env = origEnv 76 return cmd.Run() 77 }) 78 79 // Executable is a wrapper around [MustHaveExec] and [os.Executable]. 80 // It returns the path name for the executable that started the current process, 81 // or skips the test if the current system can't start new processes, 82 // or fails the test if the path can not be obtained. 83 func Executable(t testing.TB) string { 84 MustHaveExec(t) 85 86 exe, err := exePath() 87 if err != nil { 88 msg := fmt.Sprintf("os.Executable error: %v", err) 89 if t == nil { 90 panic(msg) 91 } 92 t.Fatal(msg) 93 } 94 return exe 95 } 96 97 var exePath = sync.OnceValues(func() (string, error) { 98 return os.Executable() 99 }) 100 101 var execPaths sync.Map // path -> error 102 103 // MustHaveExecPath checks that the current system can start the named executable 104 // using os.StartProcess or (more commonly) exec.Command. 105 // If not, MustHaveExecPath calls t.Skip with an explanation. 106 func MustHaveExecPath(t testing.TB, path string) { 107 MustHaveExec(t) 108 109 err, found := execPaths.Load(path) 110 if !found { 111 _, err = exec.LookPath(path) 112 err, _ = execPaths.LoadOrStore(path, err) 113 } 114 if err != nil { 115 t.Helper() 116 t.Skipf("skipping test: %s: %s", path, err) 117 } 118 } 119 120 // CleanCmdEnv will fill cmd.Env with the environment, excluding certain 121 // variables that could modify the behavior of the Go tools such as 122 // GODEBUG and GOTRACEBACK. 123 // 124 // If the caller wants to set cmd.Dir, set it before calling this function, 125 // so PWD will be set correctly in the environment. 126 func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { 127 if cmd.Env != nil { 128 panic("environment already set") 129 } 130 for _, env := range cmd.Environ() { 131 // Exclude GODEBUG from the environment to prevent its output 132 // from breaking tests that are trying to parse other command output. 133 if strings.HasPrefix(env, "GODEBUG=") { 134 continue 135 } 136 // Exclude GOTRACEBACK for the same reason. 137 if strings.HasPrefix(env, "GOTRACEBACK=") { 138 continue 139 } 140 cmd.Env = append(cmd.Env, env) 141 } 142 return cmd 143 } 144 145 // CommandContext is like exec.CommandContext, but: 146 // - skips t if the platform does not support os/exec, 147 // - sends SIGQUIT (if supported by the platform) instead of SIGKILL 148 // in its Cancel function 149 // - if the test has a deadline, adds a Context timeout and WaitDelay 150 // for an arbitrary grace period before the test's deadline expires, 151 // - fails the test if the command does not complete before the test's deadline, and 152 // - sets a Cleanup function that verifies that the test did not leak a subprocess. 153 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { 154 t.Helper() 155 MustHaveExec(t) 156 157 var ( 158 cancelCtx context.CancelFunc 159 gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) 160 ) 161 162 if t, ok := t.(interface { 163 testing.TB 164 Deadline() (time.Time, bool) 165 }); ok { 166 if td, ok := t.Deadline(); ok { 167 // Start with a minimum grace period, just long enough to consume the 168 // output of a reasonable program after it terminates. 169 gracePeriod = 100 * time.Millisecond 170 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 171 scale, err := strconv.Atoi(s) 172 if err != nil { 173 t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) 174 } 175 gracePeriod *= time.Duration(scale) 176 } 177 178 // If time allows, increase the termination grace period to 5% of the 179 // test's remaining time. 180 testTimeout := time.Until(td) 181 if gp := testTimeout / 20; gp > gracePeriod { 182 gracePeriod = gp 183 } 184 185 // When we run commands that execute subprocesses, we want to reserve two 186 // grace periods to clean up: one for the delay between the first 187 // termination signal being sent (via the Cancel callback when the Context 188 // expires) and the process being forcibly terminated (via the WaitDelay 189 // field), and a second one for the delay between the process being 190 // terminated and the test logging its output for debugging. 191 // 192 // (We want to ensure that the test process itself has enough time to 193 // log the output before it is also terminated.) 194 cmdTimeout := testTimeout - 2*gracePeriod 195 196 if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { 197 // Either ctx doesn't have a deadline, or its deadline would expire 198 // after (or too close before) the test has already timed out. 199 // Add a shorter timeout so that the test will produce useful output. 200 ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) 201 } 202 } 203 } 204 205 cmd := exec.CommandContext(ctx, name, args...) 206 cmd.Cancel = func() error { 207 if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { 208 // The command timed out due to running too close to the test's deadline. 209 // There is no way the test did that intentionally — it's too close to the 210 // wire! — so mark it as a test failure. That way, if the test expects the 211 // command to fail for some other reason, it doesn't have to distinguish 212 // between that reason and a timeout. 213 t.Errorf("test timed out while running command: %v", cmd) 214 } else { 215 // The command is being terminated due to ctx being canceled, but 216 // apparently not due to an explicit test deadline that we added. 217 // Log that information in case it is useful for diagnosing a failure, 218 // but don't actually fail the test because of it. 219 t.Logf("%v: terminating command: %v", ctx.Err(), cmd) 220 } 221 return cmd.Process.Signal(Sigquit) 222 } 223 cmd.WaitDelay = gracePeriod 224 225 t.Cleanup(func() { 226 if cancelCtx != nil { 227 cancelCtx() 228 } 229 if cmd.Process != nil && cmd.ProcessState == nil { 230 t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) 231 } 232 }) 233 234 return cmd 235 } 236 237 // Command is like exec.Command, but applies the same changes as 238 // testenv.CommandContext (with a default Context). 239 func Command(t testing.TB, name string, args ...string) *exec.Cmd { 240 t.Helper() 241 return CommandContext(t, context.Background(), name, args...) 242 } 243