Skip to content

Commit

Permalink
Add a signal handler to forward SIGINT to 'go test'
Browse files Browse the repository at this point in the history
Some test binaries may want to handle SIGINT to perform cleanup of
external resources. This signal handler will forward the interrupt
signal to the 'go test' process to give it a chance to cleanup before
shutting down.
  • Loading branch information
dnephin committed Sep 7, 2020
1 parent 4761deb commit b2b4724
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 0 deletions.
48 changes: 48 additions & 0 deletions internal/signalhandlerdriver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)

func main() {
if len(os.Args) < 2 {
log("missing required filename argument")
os.Exit(1)
}

pid := []byte(strconv.Itoa(os.Getpid()))
if err := ioutil.WriteFile(os.Args[1], pid, 0644); err != nil {
log("failed to write file:", err.Error())
os.Exit(1)
}

c := make(chan os.Signal, 1)
signal.Notify(c)

var s os.Signal
select {
case s = <-c:
case <-time.After(time.Minute):
log("timeout waiting for signal")
os.Exit(1)
}

log("Received signal:", s)
switch n := s.(type) {
case syscall.Signal:
os.Exit(100 + int(n))
default:
log("failed to parse signal number")
os.Exit(3)
}
}

func log(v ...interface{}) {
fmt.Fprintln(os.Stderr, v...)
}
42 changes: 42 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"os/exec"
"os/signal"
"strings"

"github.com/fatih/color"
Expand Down Expand Up @@ -370,6 +371,10 @@ func startGoTest(ctx context.Context, args []string) (proc, error) {
return p, errors.Wrapf(err, "failed to run %s", strings.Join(cmd.Args, " "))
}
log.Debugf("go test pid: %d", cmd.Process.Pid)

ctx, cancel := context.WithCancel(ctx)
newSignalHandler(ctx, cmd.Process.Pid)
p.cmd = &cancelWaiter{cancel: cancel, wrapped: p.cmd}
return p, nil
}

Expand All @@ -390,3 +395,40 @@ func exitCodeWithDefault(err error) int {
type exitCoder interface {
ExitCode() int
}

func newSignalHandler(ctx context.Context, pid int) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)

go func() {
defer signal.Stop(c)

select {
case <-ctx.Done():
return
case s := <-c:
proc, err := os.FindProcess(pid)
if err != nil {
log.Errorf("failed to find pid of 'go test': %v", err)
return
}
if err := proc.Signal(s); err != nil {
log.Errorf("failed to interrupt 'go test': %v", err)
return
}
}
}()
}

// cancelWaiter wraps a waiter to cancel the context after the wrapped
// Wait exits.
type cancelWaiter struct {
cancel func()
wrapped waiter
}

func (w *cancelWaiter) Wait() error {
err := w.wrapped.Wait()
w.cancel()
return err
}
87 changes: 87 additions & 0 deletions main_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@ package main

import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"

"gotest.tools/gotestsum/internal/text"
"gotest.tools/v3/assert"
"gotest.tools/v3/env"
"gotest.tools/v3/fs"
"gotest.tools/v3/golden"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
)

func TestMain(m *testing.M) {
code := m.Run()
binaryFixture.Cleanup()
os.Exit(code)
}

func TestE2E_RerunFails(t *testing.T) {
type testCase struct {
name string
Expand Down Expand Up @@ -128,3 +140,78 @@ func isPreGo114(ver string) bool {
}
return false
}

var binaryFixture pkgFixtureFile

type pkgFixtureFile struct {
filename string
once sync.Once
cleanup func()
}

func (p *pkgFixtureFile) Path() string {
return p.filename
}

func (p *pkgFixtureFile) Do(f func() string) {
p.once.Do(func() {
p.filename = f()
p.cleanup = func() {
os.RemoveAll(p.filename) // nolint: errcheck
}
})
}

func (p *pkgFixtureFile) Cleanup() {
if p.cleanup != nil {
p.cleanup()
}
}

// compileBinary once the first time this function is called. Subsequent calls
// will return the path to the compiled binary. The binary is removed when all
// the tests in this package have completed.
func compileBinary(t *testing.T) string {
t.Helper()
if testing.Short() {
t.Skip("too slow for short run")
}

binaryFixture.Do(func() string {
tmpDir, err := ioutil.TempDir("", "gotestsum-binary")
assert.NilError(t, err)

path := filepath.Join(tmpDir, "gotestsum")
result := icmd.RunCommand("go", "build", "-o", path, ".")
result.Assert(t, icmd.Success)
return path
})

if binaryFixture.Path() == "" {
t.Skip("previous attempt to compile the binary failed")
}
return binaryFixture.Path()
}

func TestE2E_SignalHandler(t *testing.T) {
skip.If(t, runtime.GOOS == "windows", "test timeout waiting for pidfile")
bin := compileBinary(t)

tmpDir := fs.NewDir(t, t.Name())
defer tmpDir.Remove()

driver := tmpDir.Join("driver")
target := filepath.FromSlash("./internal/signalhandlerdriver/")
icmd.RunCommand("go", "build", "-o", driver, target).
Assert(t, icmd.Success)

pidFile := tmpDir.Join("pidfile")
args := []string{"--raw-command", "--", driver, pidFile}
result := icmd.StartCmd(icmd.Command(bin, args...))

poll.WaitOn(t, poll.FileExists(pidFile), poll.WithTimeout(time.Second))
assert.NilError(t, result.Cmd.Process.Signal(os.Interrupt))
icmd.WaitOnCmd(2*time.Second, result)

result.Assert(t, icmd.Expected{ExitCode: 102})
}

0 comments on commit b2b4724

Please sign in to comment.