From cdd0de15cffeb2b32b8768194fed50235a345698 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 28 May 2021 15:06:35 -0400 Subject: [PATCH 1/2] Add 'l' action for watch mode --- README.md | 7 ++++-- internal/filewatcher/term_unix.go | 3 +++ internal/filewatcher/watch.go | 36 +++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cc52fe3c..f7ffe4c4 100644 --- a/README.md +++ b/README.md @@ -309,8 +309,8 @@ When the `--watch` flag is set, `gotestsum` will watch directories using [file system notifications](https://pkg.go.dev/github.com/fsnotify/fsnotify). When a Go file in one of those directories is modified, `gotestsum` will run the tests for the package which contains the changed file. By default all -directories under the current directory will be watched. Use the `--packages` flag -to specify a different list. +directories with at least one file with a `.go` extension, under the current +directory will be watched. Use the `--packages` flag to specify a different list. While in watch mode, pressing some keys will perform an action: @@ -321,6 +321,9 @@ While in watch mode, pressing some keys will perform an action: breakpoints can be added with [`runtime.Breakpoint`](https://golang.org/pkg/runtime/#Breakpoint) or by using the delve command prompt. * `a` will run tests for all packages, by using `./...` as the package selector. +* `l` will scan the directory list again, and if there are any new directories + which contain a file with a `.go` extension, they will be added to the watch + list. Note that [delve] must be installed in order to use debug (`d`). diff --git a/internal/filewatcher/term_unix.go b/internal/filewatcher/term_unix.go index 303d6bcc..c9e2e76f 100644 --- a/internal/filewatcher/term_unix.go +++ b/internal/filewatcher/term_unix.go @@ -83,6 +83,9 @@ func (r *redoHandler) Run(ctx context.Context) { case 'a': chResume = make(chan struct{}) r.ch <- RunOptions{resume: chResume, PkgPath: "./..."} + case 'l': + chResume = make(chan struct{}) + r.ch <- RunOptions{resume: chResume, reloadPaths: true} case '\n': fmt.Println() continue diff --git a/internal/filewatcher/watch.go b/internal/filewatcher/watch.go index e80c4e5d..9db8b118 100644 --- a/internal/filewatcher/watch.go +++ b/internal/filewatcher/watch.go @@ -16,27 +16,26 @@ import ( const maxDepth = 7 type RunOptions struct { - PkgPath string - Debug bool - resume chan struct{} + PkgPath string + Debug bool + resume chan struct{} + reloadPaths bool } +// Watch dirs for filesystem events, and run tests when .go files are saved. +// nolint: gocyclo func Watch(dirs []string, run func(opts RunOptions) error) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - toWatch := findAllDirs(dirs, maxDepth) watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("failed to create file watcher: %w", err) } defer watcher.Close() // nolint: errcheck // always returns nil error - fmt.Printf("Watching %v directories. Use Ctrl-c to to stop a run or exit.\n", len(toWatch)) - for _, dir := range toWatch { - if err = watcher.Add(dir); err != nil { - return fmt.Errorf("failed to watch directory %v: %w", dir, err) - } + if err := loadPaths(watcher, dirs); err != nil { + return err } timer := time.NewTimer(maxIdleTime) @@ -55,6 +54,14 @@ func Watch(dirs []string, run func(opts RunOptions) error) error { case opts := <-redo.Ch(): resetTimer(timer) + if opts.reloadPaths { + if err := loadPaths(watcher, dirs); err != nil { + return err + } + close(opts.resume) + continue + } + redo.ResetTerm() if err := h.runTests(opts); err != nil { return fmt.Errorf("failed to rerun tests for %v: %v", opts.PkgPath, err) @@ -89,6 +96,17 @@ func resetTimer(timer *time.Timer) { timer.Reset(maxIdleTime) } +func loadPaths(watcher *fsnotify.Watcher, dirs []string) error { + toWatch := findAllDirs(dirs, maxDepth) + fmt.Printf("Watching %v directories. Use Ctrl-c to to stop a run or exit.\n", len(toWatch)) + for _, dir := range toWatch { + if err := watcher.Add(dir); err != nil { + return fmt.Errorf("failed to watch directory %v: %w", dir, err) + } + } + return nil +} + func findAllDirs(dirs []string, maxDepth int) []string { if len(dirs) == 0 { dirs = []string{"./..."} From cba7ff6c247b0bb9a0f2e16e79034a2443fe3d70 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 28 May 2021 15:24:46 -0400 Subject: [PATCH 2/2] internal/filewatcher: rename some types and methods The new names should hopefully make this code easier to read and understand. --- cmd/watch.go | 8 +++---- internal/filewatcher/term_unix.go | 33 +++++++++++++++++----------- internal/filewatcher/term_windows.go | 12 +++++----- internal/filewatcher/watch.go | 32 +++++++++++++-------------- internal/filewatcher/watch_test.go | 2 +- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/cmd/watch.go b/cmd/watch.go index 79733646..0864ec5e 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -22,15 +22,15 @@ type watchRuns struct { prevExec *testjson.Execution } -func (w *watchRuns) run(runOpts filewatcher.RunOptions) error { - if runOpts.Debug { +func (w *watchRuns) run(event filewatcher.Event) error { + if event.Debug { path, cleanup, err := delveInitFile(w.prevExec) if err != nil { return fmt.Errorf("failed to write delve init file: %w", err) } defer cleanup() o := delveOpts{ - pkgPath: runOpts.PkgPath, + pkgPath: event.PkgPath, args: w.opts.args, initFilePath: path, } @@ -41,7 +41,7 @@ func (w *watchRuns) run(runOpts filewatcher.RunOptions) error { } opts := w.opts - opts.packages = []string{runOpts.PkgPath} + opts.packages = []string{event.PkgPath} 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 c9e2e76f..267b8c93 100644 --- a/internal/filewatcher/term_unix.go +++ b/internal/filewatcher/term_unix.go @@ -12,18 +12,20 @@ import ( "gotest.tools/gotestsum/log" ) -type redoHandler struct { - ch chan RunOptions +type terminal struct { + ch chan Event reset func() } -func newRedoHandler() *redoHandler { - h := &redoHandler{ch: make(chan RunOptions)} - h.SetupTerm() +func newTerminal() *terminal { + h := &terminal{ch: make(chan Event)} + h.Start() return h } -func (r *redoHandler) SetupTerm() { +// Start the terminal is non-blocking read mode. The terminal can be reset to +// normal mode by calling Reset. +func (r *terminal) Start() { if r == nil { return } @@ -59,7 +61,9 @@ func enableNonBlockingRead(fd int) (func(), error) { return reset, nil } -func (r *redoHandler) Run(ctx context.Context) { +// 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 } @@ -76,16 +80,16 @@ func (r *redoHandler) Run(ctx context.Context) { switch char { case 'r': chResume = make(chan struct{}) - r.ch <- RunOptions{resume: chResume} + r.ch <- Event{resume: chResume} case 'd': chResume = make(chan struct{}) - r.ch <- RunOptions{Debug: true, resume: chResume} + r.ch <- Event{Debug: true, resume: chResume} case 'a': chResume = make(chan struct{}) - r.ch <- RunOptions{resume: chResume, PkgPath: "./..."} + r.ch <- Event{resume: chResume, PkgPath: "./..."} case 'l': chResume = make(chan struct{}) - r.ch <- RunOptions{resume: chResume, reloadPaths: true} + r.ch <- Event{resume: chResume, reloadPaths: true} case '\n': fmt.Println() continue @@ -101,14 +105,17 @@ func (r *redoHandler) Run(ctx context.Context) { } } -func (r *redoHandler) Ch() <-chan RunOptions { +// Events returns a channel which will receive events when keys are pressed. +// When an event is received, the caller must close the resume channel to +// resume monitoring for events. +func (r *terminal) Events() <-chan Event { if r == nil { return nil } return r.ch } -func (r *redoHandler) ResetTerm() { +func (r *terminal) Reset() { if r != nil && r.reset != nil { r.reset() } diff --git a/internal/filewatcher/term_windows.go b/internal/filewatcher/term_windows.go index 5929d1e8..2c22c1ac 100644 --- a/internal/filewatcher/term_windows.go +++ b/internal/filewatcher/term_windows.go @@ -2,18 +2,18 @@ package filewatcher import "context" -type redoHandler struct{} +type terminal struct{} -func newRedoHandler() *redoHandler { +func newTerminal() *terminal { return nil } -func (r *redoHandler) Run(_ context.Context) {} +func (r *terminal) Monitor(context.Context) {} -func (r *redoHandler) Ch() <-chan RunOptions { +func (r *terminal) Events() <-chan Event { return nil } -func (r *redoHandler) SetupTerm() {} +func (r *terminal) Start() {} -func (r *redoHandler) ResetTerm() {} +func (r *terminal) Reset() {} diff --git a/internal/filewatcher/watch.go b/internal/filewatcher/watch.go index 9db8b118..13a8e23a 100644 --- a/internal/filewatcher/watch.go +++ b/internal/filewatcher/watch.go @@ -15,7 +15,7 @@ import ( const maxDepth = 7 -type RunOptions struct { +type Event struct { PkgPath string Debug bool resume chan struct{} @@ -24,7 +24,7 @@ type RunOptions struct { // Watch dirs for filesystem events, and run tests when .go files are saved. // nolint: gocyclo -func Watch(dirs []string, run func(opts RunOptions) error) error { +func Watch(dirs []string, run func(Event) error) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -41,9 +41,9 @@ func Watch(dirs []string, run func(opts RunOptions) error) error { timer := time.NewTimer(maxIdleTime) defer timer.Stop() - redo := newRedoHandler() - defer redo.ResetTerm() - go redo.Run(ctx) + term := newTerminal() + defer term.Reset() + go term.Monitor(ctx) h := &handler{last: time.Now(), fn: run} for { @@ -51,23 +51,23 @@ func Watch(dirs []string, run func(opts RunOptions) error) error { case <-timer.C: return fmt.Errorf("exceeded idle timeout while watching files") - case opts := <-redo.Ch(): + case event := <-term.Events(): resetTimer(timer) - if opts.reloadPaths { + if event.reloadPaths { if err := loadPaths(watcher, dirs); err != nil { return err } - close(opts.resume) + close(event.resume) continue } - redo.ResetTerm() - if err := h.runTests(opts); err != nil { - return fmt.Errorf("failed to rerun tests for %v: %v", opts.PkgPath, err) + term.Reset() + if err := h.runTests(event); err != nil { + return fmt.Errorf("failed to rerun tests for %v: %v", event.PkgPath, err) } - redo.SetupTerm() - close(opts.resume) + term.Start() + close(event.resume) case event := <-watcher.Events: resetTimer(timer) @@ -218,7 +218,7 @@ func handleDirCreated(watcher *fsnotify.Watcher, event fsnotify.Event) (handled type handler struct { last time.Time lastPath string - fn func(opts RunOptions) error + fn func(opts Event) error } const floodThreshold = 250 * time.Millisecond @@ -236,10 +236,10 @@ func (h *handler) handleEvent(event fsnotify.Event) error { log.Debugf("skipping event received less than %v after the previous", floodThreshold) return nil } - return h.runTests(RunOptions{PkgPath: "./" + filepath.Dir(event.Name)}) + return h.runTests(Event{PkgPath: "./" + filepath.Dir(event.Name)}) } -func (h *handler) runTests(opts RunOptions) error { +func (h *handler) runTests(opts Event) error { if opts.PkgPath == "" { opts.PkgPath = h.lastPath } diff --git a/internal/filewatcher/watch_test.go b/internal/filewatcher/watch_test.go index 15f350f6..f02f8a3e 100644 --- a/internal/filewatcher/watch_test.go +++ b/internal/filewatcher/watch_test.go @@ -22,7 +22,7 @@ func TestHandler_HandleEvent(t *testing.T) { fn := func(t *testing.T, tc testCase) { var ran bool - run := func(opts RunOptions) error { + run := func(opts Event) error { ran = true return nil }