1 // Copyright 2023 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 "os" 10 "os/exec" 11 "reflect" 12 "strconv" 13 "testing" 14 "time" 15 ) 16 17 // CommandContext is like exec.CommandContext, but: 18 // - skips t if the platform does not support os/exec, 19 // - sends SIGQUIT (if supported by the platform) instead of SIGKILL 20 // in its Cancel function 21 // - if the test has a deadline, adds a Context timeout and WaitDelay 22 // for an arbitrary grace period before the test's deadline expires, 23 // - fails the test if the command does not complete before the test's deadline, and 24 // - sets a Cleanup function that verifies that the test did not leak a subprocess. 25 func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { 26 t.Helper() 27 28 var ( 29 cancelCtx context.CancelFunc 30 gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) 31 ) 32 33 if t, ok := t.(interface { 34 testing.TB 35 Deadline() (time.Time, bool) 36 }); ok { 37 if td, ok := t.Deadline(); ok { 38 // Start with a minimum grace period, just long enough to consume the 39 // output of a reasonable program after it terminates. 40 gracePeriod = 100 * time.Millisecond 41 if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { 42 scale, err := strconv.Atoi(s) 43 if err != nil { 44 t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) 45 } 46 gracePeriod *= time.Duration(scale) 47 } 48 49 // If time allows, increase the termination grace period to 5% of the 50 // test's remaining time. 51 testTimeout := time.Until(td) 52 if gp := testTimeout / 20; gp > gracePeriod { 53 gracePeriod = gp 54 } 55 56 // When we run commands that execute subprocesses, we want to reserve two 57 // grace periods to clean up: one for the delay between the first 58 // termination signal being sent (via the Cancel callback when the Context 59 // expires) and the process being forcibly terminated (via the WaitDelay 60 // field), and a second one for the delay becween the process being 61 // terminated and and the test logging its output for debugging. 62 // 63 // (We want to ensure that the test process itself has enough time to 64 // log the output before it is also terminated.) 65 cmdTimeout := testTimeout - 2*gracePeriod 66 67 if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { 68 // Either ctx doesn't have a deadline, or its deadline would expire 69 // after (or too close before) the test has already timed out. 70 // Add a shorter timeout so that the test will produce useful output. 71 ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) 72 } 73 } 74 } 75 76 cmd := exec.CommandContext(ctx, name, args...) 77 // Set the Cancel and WaitDelay fields only if present (go 1.20 and later). 78 // TODO: When Go 1.19 is no longer supported, remove this use of reflection 79 // and instead set the fields directly. 80 if cmdCancel := reflect.ValueOf(cmd).Elem().FieldByName("Cancel"); cmdCancel.IsValid() { 81 cmdCancel.Set(reflect.ValueOf(func() error { 82 if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { 83 // The command timed out due to running too close to the test's deadline. 84 // There is no way the test did that intentionally — it's too close to the 85 // wire! — so mark it as a test failure. That way, if the test expects the 86 // command to fail for some other reason, it doesn't have to distinguish 87 // between that reason and a timeout. 88 t.Errorf("test timed out while running command: %v", cmd) 89 } else { 90 // The command is being terminated due to ctx being canceled, but 91 // apparently not due to an explicit test deadline that we added. 92 // Log that information in case it is useful for diagnosing a failure, 93 // but don't actually fail the test because of it. 94 t.Logf("%v: terminating command: %v", ctx.Err(), cmd) 95 } 96 return cmd.Process.Signal(Sigquit) 97 })) 98 } 99 if cmdWaitDelay := reflect.ValueOf(cmd).Elem().FieldByName("WaitDelay"); cmdWaitDelay.IsValid() { 100 cmdWaitDelay.Set(reflect.ValueOf(gracePeriod)) 101 } 102 103 t.Cleanup(func() { 104 if cancelCtx != nil { 105 cancelCtx() 106 } 107 if cmd.Process != nil && cmd.ProcessState == nil { 108 t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) 109 } 110 }) 111 112 return cmd 113 } 114 115 // Command is like exec.Command, but applies the same changes as 116 // testenv.CommandContext (with a default Context). 117 func Command(t testing.TB, name string, args ...string) *exec.Cmd { 118 t.Helper() 119 return CommandContext(t, context.Background(), name, args...) 120 } 121