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