github.com/AndrienkoAleksandr/go@v0.0.19/src/intern/testenv/exec.go (about) 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 func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { 104 if cmd.Env != nil { 105 panic("environment already set") 106 } 107 for _, env := range os.Environ() { 108 // Exclude GODEBUG from the environment to prevent its output 109 // from breaking tests that are trying to parse other command output. 110 if strings.HasPrefix(env, "GODEBUG=") { 111 continue 112 } 113 // Exclude GOTRACEBACK for the same reason. 114 if strings.HasPrefix(env, "GOTRACEBACK=") { 115 continue 116 } 117 cmd.Env = append(cmd.Env, env) 118 } 119 return cmd 120 } 121 122 // CommandContext is like exec.CommandContext, but: 123 // - skips t if the platform does not support os/exec, 124 // - sends SIGQUIT (if supported by the platform) instead of SIGKILL 125 // in its Cancel function 126 // - if the test has a deadline, adds a Context timeout and WaitDelay 127 // for an arbitrary grace period before the test's deadline expires, 128 // - fails the test if the command does not complete before the test's deadline, and 129 // - sets a Cleanup function that verifies that the test did not leak a subprocess. 130 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { 131 t.Helper() 132 MustHaveExec(t) 133 134 var ( 135 cancelCtx context.CancelFunc 136 gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) 137 ) 138 139 if t, ok := t.(interface { 140 testing.TB 141 Deadline() (time.Time, bool) 142 }); ok { 143 if td, ok := t.Deadline(); ok { 144 // Start with a minimum grace period, just long enough to consume the 145 // output of a reasonable program after it terminates. 146 gracePeriod = 100 * time.Millisecond 147 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 148 scale, err := strconv.Atoi(s) 149 if err != nil { 150 t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) 151 } 152 gracePeriod *= time.Duration(scale) 153 } 154 155 // If time allows, increase the termination grace period to 5% of the 156 // test's remaining time. 157 testTimeout := time.Until(td) 158 if gp := testTimeout / 20; gp > gracePeriod { 159 gracePeriod = gp 160 } 161 162 // When we run commands that execute subprocesses, we want to reserve two 163 // grace periods to clean up: one for the delay between the first 164 // termination signal being sent (via the Cancel callback when the Context 165 // expires) and the process being forcibly terminated (via the WaitDelay 166 // field), and a second one for the delay becween the process being 167 // terminated and and the test logging its output for debugging. 168 // 169 // (We want to ensure that the test process itself has enough time to 170 // log the output before it is also terminated.) 171 cmdTimeout := testTimeout - 2*gracePeriod 172 173 if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { 174 // Either ctx doesn't have a deadline, or its deadline would expire 175 // after (or too close before) the test has already timed out. 176 // Add a shorter timeout so that the test will produce useful output. 177 ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) 178 } 179 } 180 } 181 182 cmd := exec.CommandContext(ctx, name, args...) 183 cmd.Cancel = func() error { 184 if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { 185 // The command timed out due to running too close to the test's deadline. 186 // There is no way the test did that intentionally — it's too close to the 187 // wire! — so mark it as a test failure. That way, if the test expects the 188 // command to fail for some other reason, it doesn't have to distinguish 189 // between that reason and a timeout. 190 t.Errorf("test timed out while running command: %v", cmd) 191 } else { 192 // The command is being terminated due to ctx being canceled, but 193 // apparently not due to an explicit test deadline that we added. 194 // Log that information in case it is useful for diagnosing a failure, 195 // but don't actually fail the test because of it. 196 t.Logf("%v: terminating command: %v", ctx.Err(), cmd) 197 } 198 return cmd.Process.Signal(Sigquit) 199 } 200 cmd.WaitDelay = gracePeriod 201 202 t.Cleanup(func() { 203 if cancelCtx != nil { 204 cancelCtx() 205 } 206 if cmd.Process != nil && cmd.ProcessState == nil { 207 t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) 208 } 209 }) 210 211 return cmd 212 } 213 214 // Command is like exec.Command, but applies the same changes as 215 // testenv.CommandContext (with a default Context). 216 func Command(t testing.TB, name string, args ...string) *exec.Cmd { 217 t.Helper() 218 return CommandContext(t, context.Background(), name, args...) 219 }