GoPLS Viewer

Home|gopls/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
5package testenv
6
7import (
8    "context"
9    "os"
10    "os/exec"
11    "reflect"
12    "runtime"
13    "strconv"
14    "testing"
15    "time"
16)
17
18// HasExec reports whether the current system can start new processes
19// using os.StartProcess or (more commonly) exec.Command.
20func HasExec() bool {
21    switch runtime.GOOS {
22    case "js""ios":
23        return false
24    }
25    return true
26}
27
28// NeedsExec checks that the current system can start new processes
29// using os.StartProcess or (more commonly) exec.Command.
30// If not, NeedsExec calls t.Skip with an explanation.
31func NeedsExec(t testing.TB) {
32    if !HasExec() {
33        t.Skipf("skipping test: cannot exec subprocess on %s/%s"runtime.GOOSruntime.GOARCH)
34    }
35}
36
37// CommandContext is like exec.CommandContext, but:
38//   - skips t if the platform does not support os/exec,
39//   - if supported, sends SIGQUIT instead of SIGKILL in its Cancel function
40//   - if the test has a deadline, adds a Context timeout and (if supported) WaitDelay
41//     for an arbitrary grace period before the test's deadline expires,
42//   - if Cmd has the Cancel field, fails the test if the command is canceled
43//     due to the test's deadline, and
44//   - if supported, sets a Cleanup function that verifies that the test did not
45//     leak a subprocess.
46func CommandContext(t testing.TBctx context.Contextname stringargs ...string) *exec.Cmd {
47    t.Helper()
48    NeedsExec(t)
49
50    var (
51        cancelCtx   context.CancelFunc
52        gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
53    )
54
55    if tdok := Deadline(t); ok {
56        // Start with a minimum grace period, just long enough to consume the
57        // output of a reasonable program after it terminates.
58        gracePeriod = 100 * time.Millisecond
59        if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
60            scaleerr := strconv.Atoi(s)
61            if err != nil {
62                t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v"err)
63            }
64            gracePeriod *= time.Duration(scale)
65        }
66
67        // If time allows, increase the termination grace period to 5% of the
68        // test's remaining time.
69        testTimeout := time.Until(td)
70        if gp := testTimeout / 20gp > gracePeriod {
71            gracePeriod = gp
72        }
73
74        // When we run commands that execute subprocesses, we want to reserve two
75        // grace periods to clean up: one for the delay between the first
76        // termination signal being sent (via the Cancel callback when the Context
77        // expires) and the process being forcibly terminated (via the WaitDelay
78        // field), and a second one for the delay becween the process being
79        // terminated and and the test logging its output for debugging.
80        //
81        // (We want to ensure that the test process itself has enough time to
82        // log the output before it is also terminated.)
83        cmdTimeout := testTimeout - 2*gracePeriod
84
85        if cdok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout {
86            // Either ctx doesn't have a deadline, or its deadline would expire
87            // after (or too close before) the test has already timed out.
88            // Add a shorter timeout so that the test will produce useful output.
89            ctxcancelCtx = context.WithTimeout(ctxcmdTimeout)
90        }
91    }
92
93    cmd := exec.CommandContext(ctxnameargs...)
94
95    // Use reflection to set the Cancel and WaitDelay fields, if present.
96    // TODO(bcmills): When we no longer support Go versions below 1.20,
97    // remove the use of reflect and assume that the fields are always present.
98    rc := reflect.ValueOf(cmd).Elem()
99
100    if rCancel := rc.FieldByName("Cancel"); rCancel.IsValid() {
101        rCancel.Set(reflect.ValueOf(func() error {
102            if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded {
103                // The command timed out due to running too close to the test's deadline
104                // (because we specifically set a shorter Context deadline for that
105                // above). There is no way the test did that intentionally — it's too
106                // close to the wire! — so mark it as a test failure. That way, if the
107                // test expects the command to fail for some other reason, it doesn't
108                // have to distinguish between that reason and a timeout.
109                t.Errorf("test timed out while running command: %v"cmd)
110            } else {
111                // The command is being terminated due to ctx being canceled, but
112                // apparently not due to an explicit test deadline that we added.
113                // Log that information in case it is useful for diagnosing a failure,
114                // but don't actually fail the test because of it.
115                t.Logf("%v: terminating command: %v"ctx.Err(), cmd)
116            }
117            return cmd.Process.Signal(Sigquit)
118        }))
119    }
120
121    if rWaitDelay := rc.FieldByName("WaitDelay"); rWaitDelay.IsValid() {
122        rWaitDelay.Set(reflect.ValueOf(gracePeriod))
123    }
124
125    // t.Cleanup was added in Go 1.14; for earlier Go versions,
126    // we just let the Context leak.
127    type Cleanupper interface {
128        Cleanup(func())
129    }
130    if ctok := t.(Cleanupper); ok {
131        ct.Cleanup(func() {
132            if cancelCtx != nil {
133                cancelCtx()
134            }
135            if cmd.Process != nil && cmd.ProcessState == nil {
136                t.Errorf("command was started, but test did not wait for it to complete: %v"cmd)
137            }
138        })
139    }
140
141    return cmd
142}
143
144// Command is like exec.Command, but applies the same changes as
145// testenv.CommandContext (with a default Context).
146func Command(t testing.TBname stringargs ...string) *exec.Cmd {
147    t.Helper()
148    return CommandContext(tcontext.Background(), nameargs...)
149}
150
MembersX
reflect
HasExec
CommandContext.BlockStmt.cd
context
CommandContext.ctx
CommandContext.ok
CommandContext
exec
time
CommandContext.BlockStmt.BlockStmt.scale
CommandContext.cmd
Command.t
Command.name
os
CommandContext.BlockStmt.testTimeout
Command
CommandContext.name
NeedsExec.t
CommandContext.td
CommandContext.BlockStmt.s
Command.args
strconv
NeedsExec
CommandContext.rCancel
CommandContext.rWaitDelay
runtime
CommandContext.gracePeriod
CommandContext.BlockStmt.BlockStmt.err
CommandContext.BlockStmt.ok
CommandContext.rc
testing
CommandContext.args
CommandContext.cancelCtx
CommandContext.Cleanupper
CommandContext.t
Members
X