@@ -22,6 +22,7 @@ import (
2222 "runtime"
2323 "strings"
2424 "sync/atomic"
25+ "syscall"
2526 "testing"
2627 "time"
2728
@@ -177,11 +178,19 @@ type Params struct {
177178 // the face of errors. Once an error has occurred, the script
178179 // will continue as if in verbose mode.
179180 ContinueOnError bool
181+
182+ // Deadline, if not zero, specifies the time at which the test run will have
183+ // exceeded the timeout. It is equivalent to testing.T's Deadline method,
184+ // and Run will set it to the method's return value if this field is zero.
185+ Deadline time.Time
180186}
181187
182188// RunDir runs the tests in the given directory. All files in dir with a ".txt"
183189// or ".txtar" extension are considered to be test files.
184190func Run (t * testing.T , p Params ) {
191+ if deadline , ok := t .Deadline (); ok && p .Deadline .IsZero () {
192+ p .Deadline = deadline
193+ }
185194 RunT (tshim {t }, p )
186195}
187196
@@ -256,6 +265,37 @@ func RunT(t T, p Params) {
256265 if err != nil {
257266 t .Fatal (err )
258267 }
268+
269+ var (
270+ ctx = context .Background ()
271+ gracePeriod = 100 * time .Millisecond
272+ cancel context.CancelFunc
273+ )
274+ if ! p .Deadline .IsZero () {
275+ timeout := time .Until (p .Deadline )
276+
277+ // If time allows, increase the termination grace period to 5% of the
278+ // remaining time.
279+ if gp := timeout / 20 ; gp > gracePeriod {
280+ gracePeriod = gp
281+ }
282+
283+ // When we run commands that execute subprocesses, we want to reserve two
284+ // grace periods to clean up. We will send the first termination signal when
285+ // the context expires, then wait one grace period for the process to
286+ // produce whatever useful output it can (such as a stack trace). After the
287+ // first grace period expires, we'll escalate to os.Kill, leaving the second
288+ // grace period for the test function to record its output before the test
289+ // process itself terminates.
290+ timeout -= 2 * gracePeriod
291+
292+ ctx , cancel = context .WithTimeout (ctx , timeout )
293+ // We don't defer cancel() because RunT returns before the sub-tests,
294+ // and we don't have access to Cleanup due to the T interface. Instead,
295+ // we call it after the refCount goes to zero below.
296+ _ = cancel
297+ }
298+
259299 refCount := int32 (len (files ))
260300 for _ , file := range files {
261301 file := file
@@ -269,7 +309,8 @@ func RunT(t T, p Params) {
269309 name : name ,
270310 file : file ,
271311 params : p ,
272- ctxt : context .Background (),
312+ ctxt : ctx ,
313+ gracePeriod : gracePeriod ,
273314 deferred : func () {},
274315 scriptFiles : make (map [string ]string ),
275316 scriptUpdates : make (map [string ]string ),
@@ -281,8 +322,11 @@ func RunT(t T, p Params) {
281322 removeAll (ts .workdir )
282323 if atomic .AddInt32 (& refCount , - 1 ) == 0 {
283324 // This is the last subtest to finish. Remove the
284- // parent directory too.
325+ // parent directory too, and cancel the context .
285326 os .Remove (testTempDir )
327+ if cancel != nil {
328+ cancel ()
329+ }
286330 }
287331 }()
288332 ts .run ()
@@ -317,7 +361,8 @@ type TestScript struct {
317361 scriptFiles map [string ]string // files stored in the txtar archive (absolute paths -> path in script)
318362 scriptUpdates map [string ]string // updates to testscript files via UpdateScripts.
319363
320- ctxt context.Context // per TestScript context
364+ ctxt context.Context // per TestScript context
365+ gracePeriod time.Duration // time between SIGQUIT and SIGKILL
321366}
322367
323368type backgroundCmd struct {
@@ -346,6 +391,7 @@ func (ts *TestScript) setup() string {
346391 Vars : []string {
347392 "WORK=" + ts .workdir , // must be first for ts.abbrev
348393 "PATH=" + os .Getenv ("PATH" ),
394+ "GOTRACEBACK=system" ,
349395 homeEnvName () + "=/no-home" ,
350396 tempEnvName () + "=" + tmpDir ,
351397 "devnull=" + os .DevNull ,
@@ -729,7 +775,7 @@ func (ts *TestScript) exec(command string, args ...string) (stdout, stderr strin
729775 cmd .Stdout = & stdoutBuf
730776 cmd .Stderr = & stderrBuf
731777 if err = cmd .Start (); err == nil {
732- err = ctxWait (ts .ctxt , cmd )
778+ err = waitOrStop (ts .ctxt , cmd , ts . gracePeriod )
733779 }
734780 ts .stdin = ""
735781 return stdoutBuf .String (), stderrBuf .String (), err
@@ -774,21 +820,68 @@ func (ts *TestScript) BackgroundCmds() []*exec.Cmd {
774820 return cmds
775821}
776822
777- // ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done .
823+ // waitOrStop waits for the already-started command cmd by calling its Wait method .
778824//
779- // This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill.
780- // (See https://golang.org/issue/21135.)
781- func ctxWait (ctx context.Context , cmd * exec.Cmd ) error {
782- errc := make (chan error , 1 )
783- go func () { errc <- cmd .Wait () }()
784-
785- select {
786- case err := <- errc :
787- return err
788- case <- ctx .Done ():
789- interruptProcess (cmd .Process )
790- return <- errc
825+ // If cmd does not return before ctx is done, waitOrStop sends it an interrupt
826+ // signal. If killDelay is positive, waitOrStop waits that additional period for
827+ // Wait to return before sending os.Kill.
828+ func waitOrStop (ctx context.Context , cmd * exec.Cmd , killDelay time.Duration ) error {
829+ if cmd .Process == nil {
830+ panic ("waitOrStop called with a nil cmd.Process — missing Start call?" )
831+ }
832+
833+ errc := make (chan error )
834+ go func () {
835+ select {
836+ case errc <- nil :
837+ return
838+ case <- ctx .Done ():
839+ }
840+
841+ var interrupt os.Signal = syscall .SIGQUIT
842+ if runtime .GOOS == "windows" {
843+ // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
844+ // Windows; using it with os.Process.Signal will return an error.”
845+ // Fall back directly to Kill instead.
846+ interrupt = os .Kill
847+ }
848+
849+ err := cmd .Process .Signal (interrupt )
850+ if err == nil {
851+ err = ctx .Err () // Report ctx.Err() as the reason we interrupted.
852+ } else if err == os .ErrProcessDone {
853+ errc <- nil
854+ return
855+ }
856+
857+ if killDelay > 0 {
858+ timer := time .NewTimer (killDelay )
859+ select {
860+ // Report ctx.Err() as the reason we interrupted the process...
861+ case errc <- ctx .Err ():
862+ timer .Stop ()
863+ return
864+ // ...but after killDelay has elapsed, fall back to a stronger signal.
865+ case <- timer .C :
866+ }
867+
868+ // Wait still hasn't returned.
869+ // Kill the process harder to make sure that it exits.
870+ //
871+ // Ignore any error: if cmd.Process has already terminated, we still
872+ // want to send ctx.Err() (or the error from the Interrupt call)
873+ // to properly attribute the signal that may have terminated it.
874+ _ = cmd .Process .Kill ()
875+ }
876+
877+ errc <- err
878+ }()
879+
880+ waitErr := cmd .Wait ()
881+ if interruptErr := <- errc ; interruptErr != nil {
882+ return interruptErr
791883 }
884+ return waitErr
792885}
793886
794887// interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise.
0 commit comments