@@ -79,63 +79,65 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
79
79
// - skips t if the platform does not support os/exec,
80
80
// - sends SIGQUIT (if supported by the platform) instead of SIGKILL
81
81
// in its Cancel function
82
- // - adds a timeout (with an arbitrary grace period) before the test's deadline expires,
83
- // - sets a WaitDelay for an arbitrary grace period,
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
84
// - fails the test if the command does not complete before the test's deadline, and
85
85
// - sets a Cleanup function that verifies that the test did not leak a subprocess.
86
86
func CommandContext (t testing.TB , ctx context.Context , name string , args ... string ) * exec.Cmd {
87
87
t .Helper ()
88
88
MustHaveExec (t )
89
89
90
90
var (
91
- gracePeriod = 100 * time . Millisecond
92
- cancel context. CancelFunc
91
+ cancelCtx context. CancelFunc
92
+ gracePeriod time. Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
93
93
)
94
- if s := os .Getenv ("GO_TEST_TIMEOUT_SCALE" ); s != "" {
95
- scale , err := strconv .Atoi (s )
96
- if err != nil {
97
- t .Fatalf ("invalid GO_TEST_TIMEOUT_SCALE: %v" , err )
98
- }
99
- gracePeriod *= time .Duration (scale )
100
- }
101
94
102
95
if t , ok := t .(interface {
103
96
testing.TB
104
97
Deadline () (time.Time , bool )
105
98
}); ok {
106
99
if td , ok := t .Deadline (); ok {
107
- if cd , ok := ctx .Deadline (); ! ok || cd .Sub (td ) > gracePeriod {
108
- // Either ctx doesn't have a deadline, or its deadline would expire
109
- // after (or too close before) the test has already timed out.
110
- // Compute a new timeout that will expire before the test does so that
111
- // we can terminate the subprocess with a more useful signal.
112
-
113
- timeout := time .Until (td )
114
-
115
- // If time allows, increase the termination grace period to 5% of the
116
- // remaining time.
117
- if gp := timeout / 20 ; gp > gracePeriod {
118
- gracePeriod = gp
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 )
119
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
+ }
120
117
121
- // When we run commands that execute subprocesses, we want to reserve two
122
- // grace periods to clean up. We will send the first termination signal when
123
- // the context expires, then wait one grace period for the process to
124
- // produce whatever useful output it can (such as a stack trace). After the
125
- // first grace period expires, we'll escalate to os.Kill, leaving the second
126
- // grace period for the test function to record its output before the test
127
- // process itself terminates.
128
- timeout -= 2 * gracePeriod
129
-
130
- ctx , cancel = context .WithTimeout (ctx , timeout )
131
- t .Cleanup (cancel )
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 )
132
134
}
133
135
}
134
136
}
135
137
136
138
cmd := exec .CommandContext (ctx , name , args ... )
137
139
cmd .Cancel = func () error {
138
- if cancel != nil && ctx .Err () == context .DeadlineExceeded {
140
+ if cancelCtx != nil && ctx .Err () == context .DeadlineExceeded {
139
141
// The command timed out due to running too close to the test's deadline.
140
142
// There is no way the test did that intentionally — it's too close to the
141
143
// wire! — so mark it as a test failure. That way, if the test expects the
@@ -154,8 +156,8 @@ func CommandContext(t testing.TB, ctx context.Context, name string, args ...stri
154
156
cmd .WaitDelay = gracePeriod
155
157
156
158
t .Cleanup (func () {
157
- if cancel != nil {
158
- cancel ()
159
+ if cancelCtx != nil {
160
+ cancelCtx ()
159
161
}
160
162
if cmd .Process != nil && cmd .ProcessState == nil {
161
163
t .Errorf ("command was started, but test did not wait for it to complete: %v" , cmd )
0 commit comments