Skip to content

Commit

Permalink
Merge pull request #198 from dnephin/reload-paths
Browse files Browse the repository at this point in the history
watch: add action for reloading watched directory list
  • Loading branch information
dnephin committed May 29, 2021
2 parents 99b2618 + cba7ff6 commit fb92894
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 48 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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`).

Expand Down
8 changes: 4 additions & 4 deletions cmd/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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
Expand Down
34 changes: 22 additions & 12 deletions internal/filewatcher/term_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -76,13 +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 <- Event{resume: chResume, reloadPaths: true}
case '\n':
fmt.Println()
continue
Expand All @@ -98,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()
}
Expand Down
12 changes: 6 additions & 6 deletions internal/filewatcher/term_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
64 changes: 41 additions & 23 deletions internal/filewatcher/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,59 @@ import (

const maxDepth = 7

type RunOptions struct {
PkgPath string
Debug bool
resume chan struct{}
type Event struct {
PkgPath string
Debug bool
resume chan struct{}
reloadPaths bool
}

func Watch(dirs []string, run func(opts RunOptions) error) error {
// 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()

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)
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 {
select {
case <-timer.C:
return fmt.Errorf("exceeded idle timeout while watching files")

case opts := <-redo.Ch():
case event := <-term.Events():
resetTimer(timer)

redo.ResetTerm()
if err := h.runTests(opts); err != nil {
return fmt.Errorf("failed to rerun tests for %v: %v", opts.PkgPath, err)
if event.reloadPaths {
if err := loadPaths(watcher, dirs); err != nil {
return err
}
close(event.resume)
continue
}

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)
Expand Down Expand Up @@ -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{"./..."}
Expand Down Expand Up @@ -200,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
Expand All @@ -218,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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/filewatcher/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit fb92894

Please sign in to comment.