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
5package testenv
6
7import (
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.
33func 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
42var (
43	tryExecOnce sync.Once
44	tryExecErr  error
45)
46
47func 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
82var 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.
87func 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.
106func 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.
133func 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).
219func Command(t testing.TB, name string, args ...string) *exec.Cmd {
220	t.Helper()
221	return CommandContext(t, context.Background(), name, args...)
222}
223