diff --git a/README.md b/README.md index 6a13020..bff3079 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ GLOBAL OPTIONS: --version, -v print the version ``` +### Examples + +Ignore watch over all test files: + +``` +--ignore './**/*_test.go' +``` + ## Contributing See the [Contributing guide](/CONTRIBUTING.md) for steps on how to contribute to this project. diff --git a/builder.go b/builder.go index c87ab6f..f4cf1ba 100644 --- a/builder.go +++ b/builder.go @@ -12,7 +12,6 @@ import ( type Builder interface { Build() error Binary() string - Errors() string } type builder struct { @@ -25,8 +24,9 @@ type builder struct { // NewBuilder ... func NewBuilder(dir string, bin string, wd string, buildArgs []string) Builder { - if len(bin) == 0 { - bin = "bin" + // resolve bin name by current folder name + if bin == "" { + bin = filepath.Base(wd) } // does not work on Windows without the ".exe" extension @@ -45,11 +45,6 @@ func (b *builder) Binary() string { return b.binary } -// Errors ... -func (b *builder) Errors() string { - return b.errors -} - // Build ... func (b *builder) Build() error { logger.Info("Building program") @@ -64,14 +59,8 @@ func (b *builder) Build() error { return err } - if command.ProcessState.Success() { - b.errors = "" - } else { - b.errors = string(output) - } - - if len(b.errors) > 0 { - return fmt.Errorf(b.errors) + if !command.ProcessState.Success() { + return fmt.Errorf("error building: %s", output) } return nil diff --git a/builder_test.go b/builder_test.go index 8432449..e80727d 100644 --- a/builder_test.go +++ b/builder_test.go @@ -27,3 +27,26 @@ func TestBuilderSuccessBuild(t *testing.T) { } assert.NotNil(t, file, "binary not written properly") } + +func TestBuilderFailureBuild(t *testing.T) { + bArgs := []string{} + bin := "srv" + dir := filepath.Join("testdata", "build-failure") + wd, err := os.Getwd() + if err != nil { + t.Fatalf("couldn't get current working directory: %v", err) + } + + b := NewBuilder(dir, bin, wd, bArgs) + err = b.Build() + assert.NotNil(t, err, "build error") + assert.Equal(t, err.Error(), "exit status 2") +} + +func TestBuilderDefaultBinName(t *testing.T) { + bin := "" + dir := filepath.Join("testdata", "server") + wd := "/src/projects/project-name" + b := NewBuilder(dir, bin, wd, nil) + assert.Equal(t, b.Binary(), "project-name") +} diff --git a/main.go b/main.go index 0f8a3e4..825ab99 100644 --- a/main.go +++ b/main.go @@ -15,10 +15,6 @@ import ( var logger = NewLogger("gaper") -// default values -var defaultExtensions = cli.StringSlice{"go"} -var defaultPoolInterval = 500 - // exit statuses var exitStatusSuccess = 0 var exitStatusError = 1 @@ -94,12 +90,10 @@ func main() { }, cli.IntFlag{ Name: "poll-interval, p", - Value: defaultPoolInterval, Usage: "how often in milliseconds to poll watched files for changes", }, cli.StringSliceFlag{ Name: "extensions, e", - Value: &defaultExtensions, Usage: "a comma-delimited list of file extensions to watch for changes", }, cli.StringFlag{ @@ -135,11 +129,6 @@ func runGaper(cfg *Config) error { return err } - // resolve bin name by current folder name - if cfg.BinName == "" { - cfg.BinName = filepath.Base(wd) - } - if len(cfg.WatchItems) == 0 { cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath) } @@ -182,7 +171,9 @@ func runGaper(cfg *Config) error { case event := <-watcher.Events: logger.Debug("Detected new changed file: ", event) changeRestart = true - restart(builder, runner) + if err := restart(builder, runner); err != nil { + return err + } case err := <-watcher.Errors: return fmt.Errorf("error on watching files: %v", err) case err := <-runner.Errors(): @@ -249,8 +240,7 @@ func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn st return nil } - restart(builder, runner) - return nil + return restart(builder, runner) } func shutdown(runner Runner) { diff --git a/runner.go b/runner.go index 0e1f31c..8dec508 100644 --- a/runner.go +++ b/runner.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "os" @@ -12,6 +13,9 @@ import ( // OSWindows ... const OSWindows = "windows" +// os errors +var errFinished = errors.New("os: process already finished") + // Runner ... type Runner interface { Run() (*exec.Cmd, error) @@ -28,6 +32,7 @@ type runner struct { command *exec.Cmd starttime time.Time errors chan error + end chan bool // used internally by Kill to wait a process die } // NewRunner ... @@ -39,6 +44,7 @@ func NewRunner(wStdout io.Writer, wStderr io.Writer, bin string, args []string) writerStderr: wStderr, starttime: time.Now(), errors: make(chan error), + end: make(chan bool), } } @@ -46,15 +52,14 @@ func NewRunner(wStdout io.Writer, wStderr io.Writer, bin string, args []string) func (r *runner) Run() (*exec.Cmd, error) { logger.Info("Starting program") - if r.command == nil || r.Exited() { - if err := r.runBin(); err != nil { - return nil, fmt.Errorf("error running: %v", err) - } - - time.Sleep(250 * time.Millisecond) + if r.command != nil && !r.Exited() { return r.command, nil } + if err := r.runBin(); err != nil { + return nil, fmt.Errorf("error running: %v", err) + } + return r.command, nil } @@ -66,7 +71,7 @@ func (r *runner) Kill() error { done := make(chan error) go func() { - r.command.Wait() // nolint errcheck + <-r.end close(done) }() @@ -82,7 +87,7 @@ func (r *runner) Kill() error { // Wait for our process to die before we return or hard kill after 3 sec select { case <-time.After(3 * time.Second): - if err := r.command.Process.Kill(); err != nil { + if err := r.command.Process.Kill(); err != nil && err.Error() != errFinished.Error() { return fmt.Errorf("failed to kill: %v", err) } case <-done: @@ -128,6 +133,7 @@ func (r *runner) runBin() error { // wait for exit errors go func() { r.errors <- r.command.Wait() + r.end <- true }() return nil diff --git a/runner_test.go b/runner_test.go index e7aa185..263ff05 100644 --- a/runner_test.go +++ b/runner_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "os" "path/filepath" "runtime" "testing" @@ -33,3 +34,21 @@ func TestRunnerSuccessRun(t *testing.T) { assert.Equal(t, "Gaper\n", stdout.String()) } } + +func TestRunnerSuccessKill(t *testing.T) { + bin := filepath.Join("testdata", "print-gaper") + if runtime.GOOS == OSWindows { + bin += ".bat" + } + + runner := NewRunner(os.Stdout, os.Stderr, bin, nil) + + _, err := runner.Run() + assert.Nil(t, err, "error running binary") + + err = runner.Kill() + assert.Nil(t, err, "error killing program") + + errCmd := <-runner.Errors() + assert.NotNil(t, errCmd, "kill program") +} diff --git a/testdata/build-failure/main.go b/testdata/build-failure/main.go new file mode 100644 index 0000000..bc97689 --- /dev/null +++ b/testdata/build-failure/main.go @@ -0,0 +1,5 @@ +package main + +// nolint +func main() error { +} diff --git a/testdata/hidden-test/.hiden-file b/testdata/hidden-test/.hiden-file new file mode 100644 index 0000000..e69de29 diff --git a/testdata/hidden-test/.hiden-folder/.gitkeep b/testdata/hidden-test/.hiden-folder/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testdata/print-gaper b/testdata/print-gaper index 4dbb017..8447086 100755 --- a/testdata/print-gaper +++ b/testdata/print-gaper @@ -1,2 +1,3 @@ #!/usr/bin/env bash +sleep 2 echo "Gaper" diff --git a/testdata/print-gaper.bat b/testdata/print-gaper.bat index 7cbe875..76de61b 100644 --- a/testdata/print-gaper.bat +++ b/testdata/print-gaper.bat @@ -1 +1,2 @@ +timeout 2 > nul @echo Gaper diff --git a/testdata/server/main_test.go b/testdata/server/main_test.go new file mode 100644 index 0000000..6225fa1 --- /dev/null +++ b/testdata/server/main_test.go @@ -0,0 +1,3 @@ +package main + +// an empty test file just to check during tests we can ignore _test.go files diff --git a/watcher.go b/watcher.go index 44e8351..f7f29e9 100644 --- a/watcher.go +++ b/watcher.go @@ -10,6 +10,12 @@ import ( zglob "github.com/mattn/go-zglob" ) +// DefaultExtensions used by the watcher +var DefaultExtensions = []string{"go"} + +// DefaultPoolInterval used by the watcher +var DefaultPoolInterval = 500 + // Watcher ... type Watcher struct { PollInterval int @@ -22,6 +28,14 @@ type Watcher struct { // NewWatcher ... func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (*Watcher, error) { + if pollInterval == 0 { + pollInterval = DefaultPoolInterval + } + + if len(extensions) == 0 { + extensions = DefaultExtensions + } + allowedExts := make(map[string]bool) for _, ext := range extensions { allowedExts["."+ext] = true @@ -76,8 +90,9 @@ func (w *Watcher) scanChange(watchPath string) (string, error) { var fileChanged string err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error { - if path == ".git" && info.IsDir() { - return filepath.SkipDir + // ignore hidden files and directories + if filepath.Base(path)[0] == '.' { + return nil } for _, x := range w.IgnoreItems { @@ -86,11 +101,6 @@ func (w *Watcher) scanChange(watchPath string) (string, error) { } } - // ignore hidden files - if filepath.Base(path)[0] == '.' { - return nil - } - ext := filepath.Ext(path) if _, ok := w.AllowedExtensions[ext]; ok && info.ModTime().After(startTime) { fileChanged = path diff --git a/watcher_test.go b/watcher_test.go new file mode 100644 index 0000000..2baa39d --- /dev/null +++ b/watcher_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestWatcherDefaultValues(t *testing.T) { + pollInterval := 0 + watchItems := []string{filepath.Join("testdata", "server")} + var ignoreItems []string + var extensions []string + + w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) + + assert.Nil(t, err, "wacher error") + assert.Equal(t, 500, w.PollInterval) + assert.Equal(t, watchItems, w.WatchItems) + assert.Len(t, w.IgnoreItems, 0) + assert.Equal(t, map[string]bool{".go": true}, w.AllowedExtensions) +} + +func TestWatcherGlobPath(t *testing.T) { + pollInterval := 0 + watchItems := []string{filepath.Join("testdata", "server")} + ignoreItems := []string{"./testdata/**/*_test.go"} + var extensions []string + + w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) + file := filepath.Join("testdata", "server", "main_test.go") + assert.Nil(t, err, "wacher error") + assert.Equal(t, []string{file}, w.IgnoreItems) +} + +func TestWatcherWatchChange(t *testing.T) { + srvdir := filepath.Join("testdata", "server") + hiddendir := filepath.Join("testdata", "hidden-test") + + hiddenfile1 := filepath.Join("testdata", ".hidden-file") + hiddenfile2 := filepath.Join("testdata", ".hidden-folder", ".gitkeep") + mainfile := filepath.Join("testdata", "server", "main.go") + testfile := filepath.Join("testdata", "server", "main_test.go") + + pollInterval := 0 + watchItems := []string{srvdir, hiddendir} + ignoreItems := []string{testfile} + extensions := []string{"go"} + + w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) + assert.Nil(t, err, "wacher error") + + go w.Watch() + time.Sleep(time.Millisecond * 500) + + // update hidden files and dirs to check builtin hidden ignore is working + os.Chtimes(hiddenfile1, time.Now(), time.Now()) + os.Chtimes(hiddenfile2, time.Now(), time.Now()) + + // update testfile first to check ignore is working + os.Chtimes(testfile, time.Now(), time.Now()) + + time.Sleep(time.Millisecond * 500) + os.Chtimes(mainfile, time.Now(), time.Now()) + + select { + case event := <-w.Events: + assert.Equal(t, mainfile, event) + case err := <-w.Errors: + assert.Nil(t, err, "wacher event error") + } +}