Skip to content

Commit

Permalink
Merge pull request #147 from dnephin/signal-handler
Browse files Browse the repository at this point in the history
Add a signal handler to forward SIGINT to 'go test'
  • Loading branch information
dnephin committed Sep 13, 2020
2 parents 028aabe + b2b4724 commit b3209ee
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 1 deletion.
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...)
}
2 changes: 1 addition & 1 deletion internal/text/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func OpRemoveSummaryLineElapsedTime(line string) string {
}

func OpRemoveTestElapsedTime(line string) string {
if i := strings.Index(line, " (0.0"); i > 0 && i+8 == len(line) {
if i := strings.Index(line, " (0."); i > 0 && i+8 == len(line) {
return line[:i]
}
return line
Expand Down
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 b3209ee

Please sign in to comment.