Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watch: Add u key for updating golden values #249

Merged
merged 3 commits into from
May 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# gotestsum

`gotestsum` runs tests using `go test --json`, prints formatted test output, and a summary of the test run.
`gotestsum` runs tests using `go test -json`, prints formatted test output, and a summary of the test run.
It is designed to work well for both local development, and for automation like CI.
`gotest.tools/gotestsum/testjson` ([godoc](https://pkg.go.dev/gotest.tools/gotestsum/testjson)) is a library
that can be used to read [`test2json`](https://golang.org/cmd/test2json/) output.
Expand Down Expand Up @@ -112,7 +112,7 @@ warning.
When the `--jsonfile` flag or `GOTESTSUM_JSONFILE` environment variable are set
to a file path, `gotestsum` will write a line-delimited JSON file with all the
[test2json](https://golang.org/cmd/test2json/#hdr-Output_Format)
output that was written by `go test --json`. This file can be used to compare test
output that was written by `go test -json`. This file can be used to compare test
runs, or find flaky tests.

```
Expand Down Expand Up @@ -202,7 +202,7 @@ how you specify args to `go test`:

### Custom `go test` command

By default `gotestsum` runs tests using the command `go test --json ./...`. You
By default `gotestsum` runs tests using the command `go test -json ./...`. You
can change the command with positional arguments after a `--`. You can change just the
test directory value (which defaults to `./...`) by setting the `TEST_DIRECTORY`
environment variable.
Expand Down Expand Up @@ -354,6 +354,10 @@ While in watch mode, pressing some keys will perform an action:

* `r` will run tests for the previous event.
Added in version 1.6.1.
* `u` will run tests for the previous event, with the `-update` flag added.
Many [golden](https://gotest.tools/v3/golden) packages use this flag to automatically
update expected values of tests.
Added in version 1.8.1.
* `d` will run tests for the previous event using `dlv test`, allowing you to
debug a test failure using [delve]. A breakpoint will automatically be added at
the first line of any tests which failed in the previous run. Additional
Expand Down
12 changes: 9 additions & 3 deletions cmd/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import (
)

func runWatcher(opts *options) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

w := &watchRuns{opts: *opts}
return filewatcher.Watch(opts.packages, w.run)
return filewatcher.Watch(ctx, opts.packages, w.run)
}

type watchRuns struct {
Expand All @@ -40,8 +43,11 @@ func (w *watchRuns) run(event filewatcher.Event) error {
return nil
}

opts := w.opts
opts.packages = []string{event.PkgPath}
opts := w.opts // shallow copy opts
opts.packages = append([]string{}, opts.packages...)
opts.packages = append(opts.packages, event.PkgPath)
opts.packages = append(opts.packages, event.Args...)

var err error
if w.prevExec, err = runSingle(&opts); !IsExitCoder(err) {
return err
Expand Down
17 changes: 9 additions & 8 deletions internal/filewatcher/term_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bufio"
"context"
"fmt"
"io"
"os"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -62,13 +63,15 @@ func enableNonBlockingRead(fd int) (func(), error) {
return reset, nil
}

var stdin io.Reader = os.Stdin

// Monitor the terminal for key presses. If the key press is associated with an
// action, an event will be sent to channel returned by Events.
func (r *terminal) Monitor(ctx context.Context) {
if r == nil {
return
}
in := bufio.NewReader(os.Stdin)
in := bufio.NewReader(stdin)
for {
char, err := in.ReadByte()
if err != nil {
Expand All @@ -77,20 +80,18 @@ func (r *terminal) Monitor(ctx context.Context) {
}
log.Debugf("received byte %v (%v)", char, string(char))

var chResume chan struct{}
chResume := make(chan struct{})
switch char {
case 'r':
chResume = make(chan struct{})
r.ch <- Event{resume: chResume}
r.ch <- Event{resume: chResume, useLastPath: true}
case 'd':
chResume = make(chan struct{})
r.ch <- Event{Debug: true, resume: chResume}
r.ch <- Event{resume: chResume, useLastPath: true, Debug: true}
case 'a':
chResume = make(chan struct{})
r.ch <- Event{resume: chResume, PkgPath: "./..."}
case 'l':
chResume = make(chan struct{})
r.ch <- Event{resume: chResume, reloadPaths: true}
case 'u':
r.ch <- Event{resume: chResume, useLastPath: true, Args: []string{"-update"}}
case '\n':
fmt.Println()
continue
Expand Down
35 changes: 22 additions & 13 deletions internal/filewatcher/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@ import (
const maxDepth = 7

type Event struct {
PkgPath string
Debug bool
resume chan struct{}
// PkgPath of the package that triggered the event.
PkgPath string
// Args will be appended to the command line args for 'go test'.
Args []string
// Debug runs the tests with delve.
Debug bool
// resume the Watch goroutine when this channel is closed. Used to block
// the Watch goroutine while tests are running.
resume chan struct{}
// reloadPaths will cause the watched path list to be reloaded, to watch
// new directories.
reloadPaths bool
// useLastPath when true will use the PkgPath from the previous run.
useLastPath bool
}

// Watch dirs for filesystem events, and run tests when .go files are saved.
// nolint: gocyclo
func Watch(dirs []string, run func(Event) error) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

func Watch(ctx context.Context, dirs []string, run func(Event) error) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create file watcher: %w", err)
Expand All @@ -48,9 +55,11 @@ func Watch(dirs []string, run func(Event) error) error {
defer term.Reset()
go term.Monitor(ctx)

h := &handler{last: time.Now(), fn: run}
h := &fsEventHandler{last: time.Now(), fn: run}
for {
select {
case <-ctx.Done():
return nil
case <-timer.C:
return fmt.Errorf("exceeded idle timeout while watching files")

Expand Down Expand Up @@ -218,15 +227,15 @@ func handleDirCreated(watcher *fsnotify.Watcher, event fsnotify.Event) (handled
return true
}

type handler struct {
type fsEventHandler struct {
last time.Time
lastPath string
fn func(opts Event) error
}

const floodThreshold = 250 * time.Millisecond
var floodThreshold = 250 * time.Millisecond

func (h *handler) handleEvent(event fsnotify.Event) error {
func (h *fsEventHandler) handleEvent(event fsnotify.Event) error {
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
return nil
}
Expand All @@ -242,8 +251,8 @@ func (h *handler) handleEvent(event fsnotify.Event) error {
return h.runTests(Event{PkgPath: "./" + filepath.Dir(event.Name)})
}

func (h *handler) runTests(opts Event) error {
if opts.PkgPath == "" {
func (h *fsEventHandler) runTests(opts Event) error {
if opts.useLastPath {
opts.PkgPath = h.lastPath
}
fmt.Printf("\nRunning tests in %v\n", opts.PkgPath)
Expand Down
4 changes: 2 additions & 2 deletions internal/filewatcher/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"gotest.tools/v3/fs"
)

func TestHandler_HandleEvent(t *testing.T) {
func TestFSEventHandler_HandleEvent(t *testing.T) {
type testCase struct {
name string
last time.Time
Expand All @@ -27,7 +27,7 @@ func TestHandler_HandleEvent(t *testing.T) {
return nil
}

h := handler{last: tc.last, fn: run}
h := fsEventHandler{last: tc.last, fn: run}
err := h.handleEvent(tc.event)
assert.NilError(t, err)
assert.Equal(t, ran, tc.expectedRun)
Expand Down
110 changes: 110 additions & 0 deletions internal/filewatcher/watch_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build !windows && !aix
// +build !windows,!aix

package filewatcher

import (
"context"
"io"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
)

func TestWatch(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
dir := fs.NewDir(t, t.Name())

r, w := io.Pipe()
patchStdin(t, r)
patchFloodThreshold(t, 0)

chEvents := make(chan Event, 1)
capture := func(event Event) error {
chEvents <- event
return nil
}

go func() {
err := Watch(ctx, []string{dir.Path()}, capture)
assert.Check(t, err)
}()

t.Run("run all tests", func(t *testing.T) {
_, err := w.Write([]byte("a"))
assert.NilError(t, err)

event := <-chEvents
expected := Event{PkgPath: "./..."}
assert.DeepEqual(t, event, expected, cmpEvent)
})

t.Run("run tests on file change", func(t *testing.T) {
fs.Apply(t, dir, fs.WithFile("file.go", ""))

event := <-chEvents
expected := Event{PkgPath: "./" + dir.Path()}
assert.DeepEqual(t, event, expected, cmpEvent)

t.Run("and rerun", func(t *testing.T) {
_, err := w.Write([]byte("r"))
assert.NilError(t, err)

event := <-chEvents
expected := Event{PkgPath: "./" + dir.Path(), useLastPath: true}
assert.DeepEqual(t, event, expected, cmpEvent)
})

t.Run("and debug", func(t *testing.T) {
_, err := w.Write([]byte("d"))
assert.NilError(t, err)

event := <-chEvents
expected := Event{
PkgPath: "./" + dir.Path(),
useLastPath: true,
Debug: true,
}
assert.DeepEqual(t, event, expected, cmpEvent)
})

t.Run("and update", func(t *testing.T) {
_, err := w.Write([]byte("u"))
assert.NilError(t, err)

event := <-chEvents
expected := Event{
PkgPath: "./" + dir.Path(),
Args: []string{"-update"},
useLastPath: true,
}
assert.DeepEqual(t, event, expected, cmpEvent)
})
})
}

var cmpEvent = cmp.Options{
cmp.AllowUnexported(Event{}),
cmpopts.IgnoreTypes(make(chan struct{})),
}

func patchStdin(t *testing.T, in io.Reader) {
orig := stdin
stdin = in
t.Cleanup(func() {
stdin = orig
})
}

func patchFloodThreshold(t *testing.T, d time.Duration) {
orig := floodThreshold
floodThreshold = d
t.Cleanup(func() {
floodThreshold = orig
})
}