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 action for reloading watched directory list #198

Merged
merged 2 commits into from
May 29, 2021
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
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