From 14592d791aeb57654912a140bb452e247286364b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 1 May 2022 20:52:36 -0400 Subject: [PATCH 1/3] watch: add u key for updates --- cmd/watch.go | 7 +++++-- internal/filewatcher/term_unix.go | 12 +++++------- internal/filewatcher/watch.go | 26 ++++++++++++++++++-------- internal/filewatcher/watch_test.go | 2 +- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/cmd/watch.go b/cmd/watch.go index b0665a67..8b9737ee 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -40,8 +40,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 diff --git a/internal/filewatcher/term_unix.go b/internal/filewatcher/term_unix.go index b0c41f0a..4b68a4f1 100644 --- a/internal/filewatcher/term_unix.go +++ b/internal/filewatcher/term_unix.go @@ -77,20 +77,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 diff --git a/internal/filewatcher/watch.go b/internal/filewatcher/watch.go index ce8be930..5e4ddc8d 100644 --- a/internal/filewatcher/watch.go +++ b/internal/filewatcher/watch.go @@ -19,10 +19,20 @@ 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. @@ -48,7 +58,7 @@ 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 <-timer.C: @@ -218,7 +228,7 @@ 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 @@ -226,7 +236,7 @@ type handler struct { const 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 } @@ -242,8 +252,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) diff --git a/internal/filewatcher/watch_test.go b/internal/filewatcher/watch_test.go index f02f8a3e..18d918f8 100644 --- a/internal/filewatcher/watch_test.go +++ b/internal/filewatcher/watch_test.go @@ -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) From 57ba68e778401c3279aa15880d1d5dc430b73f4f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 8 May 2022 14:08:45 -0400 Subject: [PATCH 2/3] Add some tests for watch events --- cmd/watch.go | 5 +- internal/filewatcher/term_unix.go | 5 +- internal/filewatcher/watch.go | 9 +- internal/filewatcher/watch_test.go | 2 +- internal/filewatcher/watch_unix_test.go | 110 ++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 internal/filewatcher/watch_unix_test.go diff --git a/cmd/watch.go b/cmd/watch.go index 8b9737ee..a6b6e3a1 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -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 { diff --git a/internal/filewatcher/term_unix.go b/internal/filewatcher/term_unix.go index 4b68a4f1..58109a0a 100644 --- a/internal/filewatcher/term_unix.go +++ b/internal/filewatcher/term_unix.go @@ -7,6 +7,7 @@ import ( "bufio" "context" "fmt" + "io" "os" "golang.org/x/sys/unix" @@ -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 { diff --git a/internal/filewatcher/watch.go b/internal/filewatcher/watch.go index 5e4ddc8d..230183ea 100644 --- a/internal/filewatcher/watch.go +++ b/internal/filewatcher/watch.go @@ -37,10 +37,7 @@ type Event struct { // 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) @@ -61,6 +58,8 @@ func Watch(dirs []string, run func(Event) error) error { 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") @@ -234,7 +233,7 @@ type fsEventHandler struct { fn func(opts Event) error } -const floodThreshold = 250 * time.Millisecond +var floodThreshold = 250 * time.Millisecond func (h *fsEventHandler) handleEvent(event fsnotify.Event) error { if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { diff --git a/internal/filewatcher/watch_test.go b/internal/filewatcher/watch_test.go index 18d918f8..d0e5af00 100644 --- a/internal/filewatcher/watch_test.go +++ b/internal/filewatcher/watch_test.go @@ -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 diff --git a/internal/filewatcher/watch_unix_test.go b/internal/filewatcher/watch_unix_test.go new file mode 100644 index 00000000..8fef2b24 --- /dev/null +++ b/internal/filewatcher/watch_unix_test.go @@ -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 + }) +} From 849289b523de75bb12366e5f8e5734c3fbb7205f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 8 May 2022 14:18:00 -0400 Subject: [PATCH 3/3] Add to readme --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6eb1d5f7..d5e88251 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. ``` @@ -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. @@ -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