golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/internal/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 "flag" 10 "os" 11 "os/exec" 12 "reflect" 13 "runtime" 14 "strconv" 15 "sync" 16 "testing" 17 "time" 18 ) 19 20 // HasExec reports whether the current system can start new processes 21 // using os.StartProcess or (more commonly) exec.Command. 22 func HasExec() bool { 23 switch runtime.GOOS { 24 case "aix", 25 "android", 26 "darwin", 27 "dragonfly", 28 "freebsd", 29 "illumos", 30 "linux", 31 "netbsd", 32 "openbsd", 33 "plan9", 34 "solaris", 35 "windows": 36 // Known OS that isn't ios or wasm; assume that exec works. 37 return true 38 39 case "ios", "js", "wasip1": 40 // ios has an exec syscall but on real iOS devices it might return a 41 // permission error. In an emulated environment (such as a Corellium host) 42 // it might succeed, so try it and find out. 43 // 44 // As of 2023-04-19 wasip1 and js don't have exec syscalls at all, but we 45 // may as well use the same path so that this branch can be tested without 46 // an ios environment. 47 fallthrough 48 49 default: 50 tryExecOnce.Do(func() { 51 exe, err := os.Executable() 52 if err != nil { 53 return 54 } 55 if flag.Lookup("test.list") == nil { 56 // We found the executable, but we don't know how to run it in a way 57 // that should succeed without side-effects. Just forget it. 58 return 59 } 60 // We know that a test executable exists and can run, because we're 61 // running it now. Use it to check for overall exec support, but be sure 62 // to remove any environment variables that might trigger non-default 63 // behavior in a custom TestMain. 64 cmd := exec.Command(exe, "-test.list=^$") 65 cmd.Env = []string{} 66 if err := cmd.Run(); err == nil { 67 tryExecOk = true 68 } 69 }) 70 return tryExecOk 71 } 72 } 73 74 var ( 75 tryExecOnce sync.Once 76 tryExecOk bool 77 ) 78 79 // NeedsExec checks that the current system can start new processes 80 // using os.StartProcess or (more commonly) exec.Command. 81 // If not, NeedsExec calls t.Skip with an explanation. 82 func NeedsExec(t testing.TB) { 83 if !HasExec() { 84 t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) 85 } 86 } 87 88 // CommandContext is like exec.CommandContext, but: 89 // - skips t if the platform does not support os/exec, 90 // - if supported, sends SIGQUIT instead of SIGKILL in its Cancel function 91 // - if the test has a deadline, adds a Context timeout and (if supported) WaitDelay 92 // for an arbitrary grace period before the test's deadline expires, 93 // - if Cmd has the Cancel field, fails the test if the command is canceled 94 // due to the test's deadline, and 95 // - sets a Cleanup function that verifies that the test did not leak a subprocess. 96 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { 97 t.Helper() 98 NeedsExec(t) 99 100 var ( 101 cancelCtx context.CancelFunc 102 gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) 103 ) 104 105 if td, ok := Deadline(t); ok { 106 // Start with a minimum grace period, just long enough to consume the 107 // output of a reasonable program after it terminates. 108 gracePeriod = 100 * time.Millisecond 109 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 110 scale, err := strconv.Atoi(s) 111 if err != nil { 112 t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) 113 } 114 gracePeriod *= time.Duration(scale) 115 } 116 117 // If time allows, increase the termination grace period to 5% of the 118 // test's remaining time. 119 testTimeout := time.Until(td) 120 if gp := testTimeout / 20; gp > gracePeriod { 121 gracePeriod = gp 122 } 123 124 // When we run commands that execute subprocesses, we want to reserve two 125 // grace periods to clean up: one for the delay between the first 126 // termination signal being sent (via the Cancel callback when the Context 127 // expires) and the process being forcibly terminated (via the WaitDelay 128 // field), and a second one for the delay between the process being 129 // terminated and the test logging its output for debugging. 130 // 131 // (We want to ensure that the test process itself has enough time to 132 // log the output before it is also terminated.) 133 cmdTimeout := testTimeout - 2*gracePeriod 134 135 if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { 136 // Either ctx doesn't have a deadline, or its deadline would expire 137 // after (or too close before) the test has already timed out. 138 // Add a shorter timeout so that the test will produce useful output. 139 ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) 140 } 141 } 142 143 cmd := exec.CommandContext(ctx, name, args...) 144 145 // Use reflection to set the Cancel and WaitDelay fields, if present. 146 // TODO(bcmills): When we no longer support Go versions below 1.20, 147 // remove the use of reflect and assume that the fields are always present. 148 rc := reflect.ValueOf(cmd).Elem() 149 150 if rCancel := rc.FieldByName("Cancel"); rCancel.IsValid() { 151 rCancel.Set(reflect.ValueOf(func() error { 152 if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { 153 // The command timed out due to running too close to the test's deadline 154 // (because we specifically set a shorter Context deadline for that 155 // above). There is no way the test did that intentionally — it's too 156 // close to the wire! — so mark it as a test failure. That way, if the 157 // test expects the command to fail for some other reason, it doesn't 158 // have to distinguish between that reason and a timeout. 159 t.Errorf("test timed out while running command: %v", cmd) 160 } else { 161 // The command is being terminated due to ctx being canceled, but 162 // apparently not due to an explicit test deadline that we added. 163 // Log that information in case it is useful for diagnosing a failure, 164 // but don't actually fail the test because of it. 165 t.Logf("%v: terminating command: %v", ctx.Err(), cmd) 166 } 167 return cmd.Process.Signal(Sigquit) 168 })) 169 } 170 171 if rWaitDelay := rc.FieldByName("WaitDelay"); rWaitDelay.IsValid() { 172 rWaitDelay.Set(reflect.ValueOf(gracePeriod)) 173 } 174 175 t.Cleanup(func() { 176 if cancelCtx != nil { 177 cancelCtx() 178 } 179 if cmd.Process != nil && cmd.ProcessState == nil { 180 t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) 181 } 182 }) 183 184 return cmd 185 } 186 187 // Command is like exec.Command, but applies the same changes as 188 // testenv.CommandContext (with a default Context). 189 func Command(t testing.TB, name string, args ...string) *exec.Cmd { 190 t.Helper() 191 return CommandContext(t, context.Background(), name, args...) 192 }