Improve test coverage

This commit is contained in:
Max Claus Nunes 2018-07-12 10:27:07 -03:00
parent 55cf00b791
commit f578db31da
11 changed files with 499 additions and 109 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ coverage.out
.DS_Store .DS_Store
testdata/server/server testdata/server/server
dist dist
test-srv

13
Gopkg.lock generated
View File

@ -46,9 +46,18 @@
revision = "792786c7400a136282c1664665ae0a8db921c6c2" revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/objx"
packages = ["."]
revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c"
version = "v0.1.1"
[[projects]] [[projects]]
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = ["assert"] packages = [
"assert",
"mock"
]
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2" version = "v1.2.2"
@ -67,6 +76,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "501c303fff1c8cdb5806d0ebb0c92b671095cbc2049f5b8d2286df401f5efce5" inputs-digest = "8446ff85ebcb6bc802d3f444727c3910444a405f3c073c1095eb5cc8c497138b"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -1,6 +1,6 @@
OS := $(shell uname -s) OS := $(shell uname -s)
TEST_PACKAGES := $(shell go list ./...) TEST_PACKAGES := $(shell go list ./... | grep -v cmd)
COVER_PACKAGES := $(shell go list ./... | paste -sd "," -) COVER_PACKAGES := $(shell go list ./... | grep -v cmd | paste -sd "," -)
LINTER := $(shell command -v gometalinter 2> /dev/null) LINTER := $(shell command -v gometalinter 2> /dev/null)
.PHONY: setup .PHONY: setup

View File

@ -27,7 +27,6 @@ func main() {
PollInterval: c.Int("poll-interval"), PollInterval: c.Int("poll-interval"),
Extensions: c.StringSlice("extensions"), Extensions: c.StringSlice("extensions"),
NoRestartOn: c.String("no-restart-on"), NoRestartOn: c.String("no-restart-on"),
ExitOnSIGINT: true,
} }
} }
@ -38,7 +37,10 @@ func main() {
app.Action = func(c *cli.Context) { app.Action = func(c *cli.Context) {
args := parseArgs(c) args := parseArgs(c)
if err := gaper.Run(args); err != nil { chOSSiginal := make(chan os.Signal, 2)
logger.Verbose(args.Verbose)
if err := gaper.Run(args, chOSSiginal); err != nil {
logger.Error(err) logger.Error(err)
os.Exit(1) os.Exit(1)
} }

134
gaper.go
View File

@ -5,7 +5,6 @@ package gaper
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"syscall" "syscall"
@ -23,6 +22,13 @@ var DefaultExtensions = []string{"go"}
// DefaultPoolInterval is the time in ms used by the watcher to wait between scans // DefaultPoolInterval is the time in ms used by the watcher to wait between scans
var DefaultPoolInterval = 500 var DefaultPoolInterval = 500
// No restart types
var (
NoRestartOnError = "error"
NoRestartOnSuccess = "success"
NoRestartOnExit = "exit"
)
var logger = NewLogger("gaper") var logger = NewLogger("gaper")
// exit statuses // exit statuses
@ -43,71 +49,53 @@ type Config struct {
Extensions []string Extensions []string
NoRestartOn string NoRestartOn string
Verbose bool Verbose bool
ExitOnSIGINT bool WorkingDirectory string
} }
// Run in the gaper high level API // Run starts the whole gaper process watching for file changes or exit codes
// It starts the whole gaper process watching for file changes or exit codes
// and restarting the program // and restarting the program
func Run(cfg *Config) error { // nolint: gocyclo func Run(cfg *Config, chOSSiginal chan os.Signal) error {
var err error
logger.Verbose(cfg.Verbose)
logger.Debug("Starting gaper") logger.Debug("Starting gaper")
if len(cfg.BuildPath) == 0 { if err := setupConfig(cfg); err != nil {
cfg.BuildPath = DefaultBuildPath
}
cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
if err != nil {
return err return err
} }
cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged) builder := NewBuilder(cfg.BuildPath, cfg.BinName, cfg.WorkingDirectory, cfg.BuildArgs)
if err != nil { runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(cfg.WorkingDirectory, builder.Binary()), cfg.ProgramArgs)
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
if len(cfg.WatchItems) == 0 {
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
}
builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs)
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(wd, builder.Binary()), cfg.ProgramArgs)
if err = builder.Build(); err != nil {
return fmt.Errorf("build error: %v", err)
}
shutdown(runner, cfg.ExitOnSIGINT)
if _, err = runner.Run(); err != nil {
return fmt.Errorf("run error: %v", err)
}
watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions) watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
if err != nil { if err != nil {
return fmt.Errorf("watcher error: %v", err) return fmt.Errorf("watcher error: %v", err)
} }
return run(cfg, chOSSiginal, builder, runner, watcher)
}
func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner, watcher Watcher) error { // nolint: gocyclo
if err := builder.Build(); err != nil {
return fmt.Errorf("build error: %v", err)
}
// listen for OS signals
signal.Notify(chOSSiginal, os.Interrupt, syscall.SIGTERM)
if _, err := runner.Run(); err != nil {
return fmt.Errorf("run error: %v", err)
}
// flag to know if an exit was caused by a restart from a file changing // flag to know if an exit was caused by a restart from a file changing
changeRestart := false changeRestart := false
go watcher.Watch() go watcher.Watch()
for { for {
select { select {
case event := <-watcher.Events: case event := <-watcher.Events():
logger.Debug("Detected new changed file: ", event) logger.Debug("Detected new changed file: ", event)
changeRestart = true changeRestart = true
if err := restart(builder, runner); err != nil { if err := restart(builder, runner); err != nil {
return err return err
} }
case err := <-watcher.Errors: case err := <-watcher.Errors():
return fmt.Errorf("error on watching files: %v", err) return fmt.Errorf("error on watching files: %v", err)
case err := <-runner.Errors(): case err := <-runner.Errors():
logger.Debug("Detected program exit: ", err) logger.Debug("Detected program exit: ", err)
@ -121,6 +109,14 @@ func Run(cfg *Config) error { // nolint: gocyclo
if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil { if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil {
return err return err
} }
case signal := <-chOSSiginal:
logger.Debug("Got signal: ", signal)
if err := runner.Kill(); err != nil {
logger.Error("Error killing: ", err)
}
return fmt.Errorf("OS signal: %v", signal)
default: default:
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond) time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
} }
@ -149,49 +145,53 @@ func restart(builder Builder, runner Runner) error {
} }
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error { func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
var exitStatus int exitStatus := runner.ExitStatus(err)
if exiterr, ok := err.(*exec.ExitError); ok {
status, oks := exiterr.Sys().(syscall.WaitStatus)
if !oks {
return fmt.Errorf("couldn't resolve exit status: %v", err)
}
exitStatus = status.ExitStatus()
}
// if "error", an exit code of 0 will still restart. // if "error", an exit code of 0 will still restart.
if noRestartOn == "error" && exitStatus == exitStatusError { if noRestartOn == NoRestartOnError && exitStatus == exitStatusError {
return nil return nil
} }
// if "success", no restart only if exit code is 0. // if "success", no restart only if exit code is 0.
if noRestartOn == "success" && exitStatus == exitStatusSuccess { if noRestartOn == NoRestartOnSuccess && exitStatus == exitStatusSuccess {
return nil return nil
} }
// if "exit", no restart regardless of exit code. // if "exit", no restart regardless of exit code.
if noRestartOn == "exit" { if noRestartOn == NoRestartOnExit {
return nil return nil
} }
return restart(builder, runner) return restart(builder, runner)
} }
func shutdown(runner Runner, exitOnSIGINT bool) { func setupConfig(cfg *Config) error {
c := make(chan os.Signal, 2) var err error
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
s := <-c
logger.Debug("Got signal: ", s)
if err := runner.Kill(); err != nil { if len(cfg.BuildPath) == 0 {
logger.Error("Error killing: ", err) cfg.BuildPath = DefaultBuildPath
} }
if exitOnSIGINT { cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged)
os.Exit(0) if err != nil {
} return err
}() }
cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged)
if err != nil {
return err
}
cfg.WorkingDirectory, err = os.Getwd()
if err != nil {
return err
}
if len(cfg.WatchItems) == 0 {
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
}
return nil
} }
func parseInnerArgs(args []string, argsm string) ([]string, error) { func parseInnerArgs(args []string, argsm string) ([]string, error) {

View File

@ -1,9 +1,241 @@
package gaper package gaper
import ( import (
"errors"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing" "testing"
"time"
"github.com/maxcnunes/gaper/testdata"
"github.com/stretchr/testify/assert"
) )
func TestGaper(t *testing.T) { func TestGaperRunStopOnSGINT(t *testing.T) {
// TODO: add test to gaper high level API args := &Config{
BuildPath: filepath.Join("testdata", "server"),
}
chOSSiginal := make(chan os.Signal, 2)
go func() {
time.Sleep(1 * time.Second)
chOSSiginal <- syscall.SIGINT
}()
err := Run(args, chOSSiginal)
assert.NotNil(t, err, "build error")
assert.Equal(t, "OS signal: interrupt", err.Error())
}
func TestGaperSetupConfigNoParams(t *testing.T) {
cwd, _ := os.Getwd()
args := &Config{}
err := setupConfig(args)
assert.Nil(t, err, "build error")
assert.Equal(t, args.BuildPath, ".")
assert.Equal(t, args.WorkingDirectory, cwd)
assert.Equal(t, args.WatchItems, []string{"."})
}
func TestGaperWatcherError(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(nil)
mockRunner := new(testdata.MockRunner)
cmd := &exec.Cmd{}
runnerErrorsChan := make(chan error)
mockRunner.On("Run").Return(cmd, nil)
mockRunner.On("Errors").Return(runnerErrorsChan)
mockWatcher := new(testdata.MockWacther)
watcherErrorsChan := make(chan error)
watcherEvetnsChan := make(chan string)
mockWatcher.On("Errors").Return(watcherErrorsChan)
mockWatcher.On("Events").Return(watcherEvetnsChan)
dir := filepath.Join("testdata", "server")
cfg := &Config{
BinName: "test-srv",
BuildPath: dir,
}
go func() {
time.Sleep(3 * time.Second)
watcherErrorsChan <- errors.New("watcher-error")
}()
chOSSiginal := make(chan os.Signal, 2)
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
assert.NotNil(t, err, "build error")
assert.Equal(t, "error on watching files: watcher-error", err.Error())
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
mockWatcher.AssertExpectations(t)
}
func TestGaperProgramExit(t *testing.T) {
testCases := []struct {
name string
exitStatus int
noRestartOn string
restart bool
}{
{
name: "no restart on exit error with no-restart-on=error",
exitStatus: exitStatusError,
noRestartOn: NoRestartOnError,
restart: false,
},
{
name: "no restart on exit success with no-restart-on=success",
exitStatus: exitStatusSuccess,
noRestartOn: NoRestartOnSuccess,
restart: false,
},
{
name: "no restart on exit error with no-restart-on=exit",
exitStatus: exitStatusError,
noRestartOn: NoRestartOnExit,
restart: false,
},
{
name: "no restart on exit success with no-restart-on=exit",
exitStatus: exitStatusSuccess,
noRestartOn: NoRestartOnExit,
restart: false,
},
{
name: "restart on exit error with disabled no-restart-on",
exitStatus: exitStatusError,
restart: true,
},
{
name: "restart on exit success with disabled no-restart-on",
exitStatus: exitStatusSuccess,
restart: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(nil)
mockRunner := new(testdata.MockRunner)
cmd := &exec.Cmd{}
runnerErrorsChan := make(chan error)
mockRunner.On("Run").Return(cmd, nil)
mockRunner.On("Kill").Return(nil)
mockRunner.On("Errors").Return(runnerErrorsChan)
mockRunner.On("ExitStatus").Return(tc.exitStatus)
if tc.restart {
mockRunner.On("Exited").Return(true)
}
mockWatcher := new(testdata.MockWacther)
watcherErrorsChan := make(chan error)
watcherEvetnsChan := make(chan string)
mockWatcher.On("Errors").Return(watcherErrorsChan)
mockWatcher.On("Events").Return(watcherEvetnsChan)
dir := filepath.Join("testdata", "server")
cfg := &Config{
BinName: "test-srv",
BuildPath: dir,
NoRestartOn: tc.noRestartOn,
}
chOSSiginal := make(chan os.Signal, 2)
go func() {
time.Sleep(1 * time.Second)
runnerErrorsChan <- errors.New("runner-error")
time.Sleep(1 * time.Second)
chOSSiginal <- syscall.SIGINT
}()
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
assert.NotNil(t, err, "build error")
assert.Equal(t, "OS signal: interrupt", err.Error())
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
mockWatcher.AssertExpectations(t)
})
}
}
func TestGaperRestartExited(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(nil)
mockRunner := new(testdata.MockRunner)
cmd := &exec.Cmd{}
mockRunner.On("Run").Return(cmd, nil)
mockRunner.On("Exited").Return(true)
err := restart(mockBuilder, mockRunner)
assert.Nil(t, err, "restart error")
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
}
func TestGaperRestartNotExited(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(nil)
mockRunner := new(testdata.MockRunner)
cmd := &exec.Cmd{}
mockRunner.On("Run").Return(cmd, nil)
mockRunner.On("Kill").Return(nil)
mockRunner.On("Exited").Return(false)
err := restart(mockBuilder, mockRunner)
assert.Nil(t, err, "restart error")
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
}
func TestGaperRestartNotExitedKillFail(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockRunner := new(testdata.MockRunner)
mockRunner.On("Kill").Return(errors.New("kill-error"))
mockRunner.On("Exited").Return(false)
err := restart(mockBuilder, mockRunner)
assert.NotNil(t, err, "restart error")
assert.Equal(t, "kill error: kill-error", err.Error())
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
}
func TestGaperRestartBuildFail(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(errors.New("build-error"))
mockRunner := new(testdata.MockRunner)
mockRunner.On("Exited").Return(true)
err := restart(mockBuilder, mockRunner)
assert.NotNil(t, err, "restart error")
assert.Equal(t, "build error: build-error", err.Error())
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
}
func TestGaperRestartRunFail(t *testing.T) {
mockBuilder := new(testdata.MockBuilder)
mockBuilder.On("Build").Return(nil)
mockRunner := new(testdata.MockRunner)
cmd := &exec.Cmd{}
mockRunner.On("Run").Return(cmd, errors.New("run-error"))
mockRunner.On("Exited").Return(true)
err := restart(mockBuilder, mockRunner)
assert.NotNil(t, err, "restart error")
assert.Equal(t, "run error: run-error", err.Error())
mockBuilder.AssertExpectations(t)
mockRunner.AssertExpectations(t)
} }

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"syscall"
"time" "time"
) )
@ -22,6 +23,7 @@ type Runner interface {
Kill() error Kill() error
Errors() chan error Errors() chan error
Exited() bool Exited() bool
ExitStatus(err error) int
} }
type runner struct { type runner struct {
@ -111,6 +113,18 @@ func (r *runner) Errors() chan error {
return r.errors return r.errors
} }
// ExitStatus resolves the exit status
func (r *runner) ExitStatus(err error) int {
var exitStatus int
if exiterr, ok := err.(*exec.ExitError); ok {
if status, oks := exiterr.Sys().(syscall.WaitStatus); oks {
exitStatus = status.ExitStatus()
}
}
return exitStatus
}
func (r *runner) runBin() error { func (r *runner) runBin() error {
r.command = exec.Command(r.bin, r.args...) // nolint gas r.command = exec.Command(r.bin, r.args...) // nolint gas
stdout, err := r.command.StdoutPipe() stdout, err := r.command.StdoutPipe()

View File

@ -2,7 +2,9 @@ package gaper
import ( import (
"bytes" "bytes"
"errors"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
@ -48,3 +50,32 @@ func TestRunnerSuccessKill(t *testing.T) {
errCmd := <-runner.Errors() errCmd := <-runner.Errors()
assert.NotNil(t, errCmd, "kill program") assert.NotNil(t, errCmd, "kill program")
} }
func TestRunnerExitedNotStarted(t *testing.T) {
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
assert.Equal(t, runner.Exited(), false)
}
func TestRunnerExitStatusNonExitError(t *testing.T) {
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
err := errors.New("non exec.ExitError")
assert.Equal(t, runner.ExitStatus(err), 0)
}
func testExit() {
os.Exit(1)
}
func TestRunnerExitStatusExitError(t *testing.T) {
if os.Getenv("TEST_EXIT") == "1" {
testExit()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestRunnerExitStatusExitError")
cmd.Env = append(os.Environ(), "TEST_EXIT=1")
err := cmd.Run()
runner := NewRunner(os.Stdout, os.Stderr, "", nil)
assert.Equal(t, runner.ExitStatus(err), 1)
}

80
testdata/mocks.go vendored Normal file
View File

@ -0,0 +1,80 @@
package testdata
import (
"os/exec"
"github.com/stretchr/testify/mock"
)
// MockBuilder ...
type MockBuilder struct {
mock.Mock
}
// Build ...
func (m *MockBuilder) Build() error {
args := m.Called()
return args.Error(0)
}
// Binary ...
func (m *MockBuilder) Binary() string {
args := m.Called()
return args.String(0)
}
// MockRunner ...
type MockRunner struct {
mock.Mock
}
// Run ...
func (m *MockRunner) Run() (*exec.Cmd, error) {
args := m.Called()
return args.Get(0).(*exec.Cmd), args.Error(1)
}
// Kill ...
func (m *MockRunner) Kill() error {
args := m.Called()
return args.Error(0)
}
// Errors ...
func (m *MockRunner) Errors() chan error {
args := m.Called()
return args.Get(0).(chan error)
}
// Exited ...
func (m *MockRunner) Exited() bool {
args := m.Called()
return args.Bool(0)
}
// ExitStatus ...
func (m *MockRunner) ExitStatus(err error) int {
args := m.Called()
return args.Int(0)
}
// MockWacther ...
type MockWacther struct {
mock.Mock
}
// Watch ...
func (m *MockWacther) Watch() {}
// Events ...
func (m *MockWacther) Events() chan string {
args := m.Called()
return args.Get(0).(chan string)
}
// Errors ...
func (m *MockWacther) Errors() chan error {
args := m.Called()
return args.Get(0).(chan error)
}

View File

@ -12,17 +12,24 @@ import (
) )
// Watcher is a interface for the watch process // Watcher is a interface for the watch process
type Watcher struct { type Watcher interface {
PollInterval int Watch()
WatchItems map[string]bool Errors() chan error
IgnoreItems map[string]bool Events() chan string
AllowedExtensions map[string]bool }
Events chan string
Errors chan error // watcher is a interface for the watch process
type watcher struct {
pollInterval int
watchItems map[string]bool
ignoreItems map[string]bool
allowedExtensions map[string]bool
events chan string
errors chan error
} }
// NewWatcher creates a new watcher // NewWatcher creates a new watcher
func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (*Watcher, error) { func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, extensions []string) (Watcher, error) {
if pollInterval == 0 { if pollInterval == 0 {
pollInterval = DefaultPoolInterval pollInterval = DefaultPoolInterval
} }
@ -48,13 +55,13 @@ func NewWatcher(pollInterval int, watchItems []string, ignoreItems []string, ext
logger.Debugf("Resolved watch paths: %v", watchPaths) logger.Debugf("Resolved watch paths: %v", watchPaths)
logger.Debugf("Resolved ignore paths: %v", ignorePaths) logger.Debugf("Resolved ignore paths: %v", ignorePaths)
return &Watcher{ return &watcher{
Events: make(chan string), events: make(chan string),
Errors: make(chan error), errors: make(chan error),
PollInterval: pollInterval, pollInterval: pollInterval,
WatchItems: watchPaths, watchItems: watchPaths,
IgnoreItems: ignorePaths, ignoreItems: ignorePaths,
AllowedExtensions: allowedExts, allowedExtensions: allowedExts,
}, nil }, nil
} }
@ -62,26 +69,37 @@ var startTime = time.Now()
var errDetectedChange = errors.New("done") var errDetectedChange = errors.New("done")
// Watch starts watching for file changes // Watch starts watching for file changes
func (w *Watcher) Watch() { func (w *watcher) Watch() {
for { for {
for watchPath := range w.WatchItems { for watchPath := range w.watchItems {
fileChanged, err := w.scanChange(watchPath) fileChanged, err := w.scanChange(watchPath)
if err != nil { if err != nil {
w.Errors <- err w.errors <- err
return return
} }
if fileChanged != "" { if fileChanged != "" {
w.Events <- fileChanged w.events <- fileChanged
startTime = time.Now() startTime = time.Now()
} }
} }
time.Sleep(time.Duration(w.PollInterval) * time.Millisecond) time.Sleep(time.Duration(w.pollInterval) * time.Millisecond)
} }
} }
func (w *Watcher) scanChange(watchPath string) (string, error) { // Events get events occurred during the watching
// these events are emited only a file changing is detected
func (w *watcher) Events() chan string {
return w.events
}
// Errors get errors occurred during the watching
func (w *watcher) Errors() chan error {
return w.errors
}
func (w *watcher) scanChange(watchPath string) (string, error) {
logger.Debug("Watching ", watchPath) logger.Debug("Watching ", watchPath)
var fileChanged string var fileChanged string
@ -92,12 +110,12 @@ func (w *Watcher) scanChange(watchPath string) (string, error) {
return skipFile(info) return skipFile(info)
} }
if _, ignored := w.IgnoreItems[path]; ignored { if _, ignored := w.ignoreItems[path]; ignored {
return skipFile(info) return skipFile(info)
} }
ext := filepath.Ext(path) ext := filepath.Ext(path)
if _, ok := w.AllowedExtensions[ext]; ok && info.ModTime().After(startTime) { if _, ok := w.allowedExtensions[ext]; ok && info.ModTime().After(startTime) {
fileChanged = path fileChanged = path
return errDetectedChange return errDetectedChange
} }

View File

@ -16,18 +16,19 @@ func TestWatcherDefaultValues(t *testing.T) {
var ignoreItems []string var ignoreItems []string
var extensions []string var extensions []string
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
expectedPath := "testdata/server" expectedPath := "testdata/server"
if runtime.GOOS == OSWindows { if runtime.GOOS == OSWindows {
expectedPath = "testdata\\server" expectedPath = "testdata\\server"
} }
w := wt.(*watcher)
assert.Nil(t, err, "wacher error") assert.Nil(t, err, "wacher error")
assert.Equal(t, 500, w.PollInterval) assert.Equal(t, 500, w.pollInterval)
assert.Equal(t, map[string]bool{expectedPath: true}, w.WatchItems) assert.Equal(t, map[string]bool{expectedPath: true}, w.watchItems)
assert.Len(t, w.IgnoreItems, 0) assert.Len(t, w.ignoreItems, 0)
assert.Equal(t, map[string]bool{".go": true}, w.AllowedExtensions) assert.Equal(t, map[string]bool{".go": true}, w.allowedExtensions)
} }
func TestWatcherGlobPath(t *testing.T) { func TestWatcherGlobPath(t *testing.T) {
@ -36,9 +37,10 @@ func TestWatcherGlobPath(t *testing.T) {
ignoreItems := []string{"./testdata/**/*_test.go"} ignoreItems := []string{"./testdata/**/*_test.go"}
var extensions []string var extensions []string
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
assert.Nil(t, err, "wacher error") assert.Nil(t, err, "wacher error")
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.IgnoreItems) w := wt.(*watcher)
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.ignoreItems)
} }
func TestWatcherRemoveOverlapdPaths(t *testing.T) { func TestWatcherRemoveOverlapdPaths(t *testing.T) {
@ -47,9 +49,10 @@ func TestWatcherRemoveOverlapdPaths(t *testing.T) {
ignoreItems := []string{"./testdata/**/*", "./testdata/server"} ignoreItems := []string{"./testdata/**/*", "./testdata/server"}
var extensions []string var extensions []string
w, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions) wt, err := NewWatcher(pollInterval, watchItems, ignoreItems, extensions)
assert.Nil(t, err, "wacher error") assert.Nil(t, err, "wacher error")
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.IgnoreItems) w := wt.(*watcher)
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.ignoreItems)
} }
func TestWatcherWatchChange(t *testing.T) { func TestWatcherWatchChange(t *testing.T) {
@ -83,9 +86,9 @@ func TestWatcherWatchChange(t *testing.T) {
os.Chtimes(mainfile, time.Now(), time.Now()) os.Chtimes(mainfile, time.Now(), time.Now())
select { select {
case event := <-w.Events: case event := <-w.Events():
assert.Equal(t, mainfile, event) assert.Equal(t, mainfile, event)
case err := <-w.Errors: case err := <-w.Errors():
assert.Nil(t, err, "wacher event error") assert.Nil(t, err, "wacher event error")
} }
} }