github.com/JimmyHuang454/JLS-go@v0.0.0-20230831150107-90d536585ba0/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 "os" 10 "os/exec" 11 "runtime" 12 "strconv" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 ) 18 19 // HasExec reports whether the current system can start new processes 20 // using os.StartProcess or (more commonly) exec.Command. 21 func HasExec() bool { 22 switch runtime.GOOS { 23 case "js", "ios": 24 return false 25 } 26 return true 27 } 28 29 // MustHaveExec checks that the current system can start new processes 30 // using os.StartProcess or (more commonly) exec.Command. 31 // If not, MustHaveExec calls t.Skip with an explanation. 32 func MustHaveExec(t testing.TB) { 33 if !HasExec() { 34 t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) 35 } 36 } 37 38 var execPaths sync.Map // path -> error 39 40 // MustHaveExecPath checks that the current system can start the named executable 41 // using os.StartProcess or (more commonly) exec.Command. 42 // If not, MustHaveExecPath calls t.Skip with an explanation. 43 func MustHaveExecPath(t testing.TB, path string) { 44 MustHaveExec(t) 45 46 err, found := execPaths.Load(path) 47 if !found { 48 _, err = exec.LookPath(path) 49 err, _ = execPaths.LoadOrStore(path, err) 50 } 51 if err != nil { 52 t.Skipf("skipping test: %s: %s", path, err) 53 } 54 } 55 56 // CleanCmdEnv will fill cmd.Env with the environment, excluding certain 57 // variables that could modify the behavior of the Go tools such as 58 // GODEBUG and GOTRACEBACK. 59 func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { 60 if cmd.Env != nil { 61 panic("environment already set") 62 } 63 for _, env := range os.Environ() { 64 // Exclude GODEBUG from the environment to prevent its output 65 // from breaking tests that are trying to parse other command output. 66 if strings.HasPrefix(env, "GODEBUG=") { 67 continue 68 } 69 // Exclude GOTRACEBACK for the same reason. 70 if strings.HasPrefix(env, "GOTRACEBACK=") { 71 continue 72 } 73 cmd.Env = append(cmd.Env, env) 74 } 75 return cmd 76 } 77 78 // CommandContext is like exec.CommandContext, but: 79 // - skips t if the platform does not support os/exec, 80 // - sends SIGQUIT (if supported by the platform) instead of SIGKILL 81 // in its Cancel function 82 // - if the test has a deadline, adds a Context timeout and WaitDelay 83 // for an arbitrary grace period before the test's deadline expires, 84 // - fails the test if the command does not complete before the test's deadline, and 85 // - sets a Cleanup function that verifies that the test did not leak a subprocess. 86 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { 87 t.Helper() 88 MustHaveExec(t) 89 90 var ( 91 cancelCtx context.CancelFunc 92 gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) 93 ) 94 95 if t, ok := t.(interface { 96 testing.TB 97 Deadline() (time.Time, bool) 98 }); ok { 99 if td, ok := t.Deadline(); ok { 100 // Start with a minimum grace period, just long enough to consume the 101 // output of a reasonable program after it terminates. 102 gracePeriod = 100 * time.Millisecond 103 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 104 scale, err := strconv.Atoi(s) 105 if err != nil { 106 t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) 107 } 108 gracePeriod *= time.Duration(scale) 109 } 110 111 // If time allows, increase the termination grace period to 5% of the 112 // test's remaining time. 113 testTimeout := time.Until(td) 114 if gp := testTimeout / 20; gp > gracePeriod { 115 gracePeriod = gp 116 } 117 118 // When we run commands that execute subprocesses, we want to reserve two 119 // grace periods to clean up: one for the delay between the first 120 // termination signal being sent (via the Cancel callback when the Context 121 // expires) and the process being forcibly terminated (via the WaitDelay 122 // field), and a second one for the delay becween the process being 123 // terminated and and the test logging its output for debugging. 124 // 125 // (We want to ensure that the test process itself has enough time to 126 // log the output before it is also terminated.) 127 cmdTimeout := testTimeout - 2*gracePeriod 128 129 if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { 130 // Either ctx doesn't have a deadline, or its deadline would expire 131 // after (or too close before) the test has already timed out. 132 // Add a shorter timeout so that the test will produce useful output. 133 ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) 134 } 135 } 136 } 137 138 cmd := exec.CommandContext(ctx, name, args...) 139 cmd.Cancel = func() error { 140 if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { 141 // The command timed out due to running too close to the test's deadline. 142 // There is no way the test did that intentionally — it's too close to the 143 // wire! — so mark it as a test failure. That way, if the test expects the 144 // command to fail for some other reason, it doesn't have to distinguish 145 // between that reason and a timeout. 146 t.Errorf("test timed out while running command: %v", cmd) 147 } else { 148 // The command is being terminated due to ctx being canceled, but 149 // apparently not due to an explicit test deadline that we added. 150 // Log that information in case it is useful for diagnosing a failure, 151 // but don't actually fail the test because of it. 152 t.Logf("%v: terminating command: %v", ctx.Err(), cmd) 153 } 154 return cmd.Process.Signal(Sigquit) 155 } 156 cmd.WaitDelay = gracePeriod 157 158 t.Cleanup(func() { 159 if cancelCtx != nil { 160 cancelCtx() 161 } 162 if cmd.Process != nil && cmd.ProcessState == nil { 163 t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) 164 } 165 }) 166 167 return cmd 168 } 169 170 // Command is like exec.Command, but applies the same changes as 171 // testenv.CommandContext (with a default Context). 172 func Command(t testing.TB, name string, args ...string) *exec.Cmd { 173 t.Helper() 174 return CommandContext(t, context.Background(), name, args...) 175 }