]> Cypherpunks.ru repositories - gostls13.git/blob - src/internal/testenv/exec.go
50d3b0dc73131ca7c8ae0a2688723efbd5200a69
[gostls13.git] / 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 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 between the process being
167                         // terminated 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 }