diff --git a/.gitignore b/.gitignore index daf913b..2890fba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +vendor/*/ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6eece66 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.7 +script: + - go test -coverprofile=coverage.txt -covermode=atomic -coverpkg . + +before_install: + - go get -u github.com/kardianos/govendor + - govendor sync + +after_success: +- bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 3557f22..ead0a01 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# reloader \ No newline at end of file +# Reloader +[![Build Status](https://travis-ci.org/aandryashin/reloader.svg?branch=master)](https://travis-ci.org/aandryashin/reloader) +[![Coverage](https://codecov.io/github/aandryashin/reloader/reloader.svg)](https://codecov.io/gh/aandryashin/reloader) +[![Release](https://img.shields.io/github/release/aandryashin/reloader.svg)](https://github.com/aandryashin/reloader/releases/latest) + +Library for automatic reloading configuration files based on file system events. diff --git a/reloader.go b/reloader.go new file mode 100644 index 0000000..bdaec6e --- /dev/null +++ b/reloader.go @@ -0,0 +1,44 @@ +package reloader + +import ( + "fmt" + "time" + + "github.com/fsnotify/fsnotify" +) + +func Watch(dir string, load func(), delay time.Duration) error { + return watch(fsnotify.NewWatcher, dir, load, delay) +} + +func watch(fn func() (*fsnotify.Watcher, error), dir string, load func(), delay time.Duration) error { + watcher, err := fn() + if err != nil { + return fmt.Errorf("unable to initialize file system notifications: %v", err) + } + if err := watcher.Add(dir); err != nil { + return fmt.Errorf("unable to watch directory: %v", err) + } + go func() { + var cancel chan struct{} + for { + select { + case e := <-watcher.Events: + if e.Op != 0 { + if cancel != nil { + close(cancel) + } + cancel = make(chan struct{}) + go func(cancel chan struct{}) { + select { + case <-time.After(delay): + load() + case <-cancel: + } + }(cancel) + } + } + } + }() + return nil +} diff --git a/reloader_test.go b/reloader_test.go new file mode 100644 index 0000000..00c5f06 --- /dev/null +++ b/reloader_test.go @@ -0,0 +1,141 @@ +package reloader + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/fsnotify/fsnotify" +) + +func TestWatch(t *testing.T) { + dir, err := ioutil.TempDir("", "config") + if err != nil { + t.Fatal(err) + } + defer os.Remove(dir) + err = Watch(dir, func() {}, 1*time.Second) + if err != nil { + t.Errorf("uexpected error: %v\n", err) + } +} + +func TestWatchUexistentDir(t *testing.T) { + dir, err := ioutil.TempDir("", "config") + if err != nil { + t.Fatal(err) + } + os.Remove(dir) + err = Watch(dir, func() {}, 1*time.Second) + if err == nil { + t.Error("uexpected success") + } +} + +func TestNewWatcherError(t *testing.T) { + fn := func() (*fsnotify.Watcher, error) { + return nil, fmt.Errorf("error") + } + err := watch(fn, "", func() {}, 1*time.Second) + if err == nil { + t.Error("uexpected success") + } +} + +func TestTimerShouldNotTrigger(t *testing.T) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + t.Fatal(err) + } + defer watcher.Close() + fn := func() (*fsnotify.Watcher, error) { + return watcher, nil + } + dir, err := ioutil.TempDir("", "config") + if err != nil { + t.Fatal(err) + } + defer os.Remove(dir) + ch := make(chan bool) + watch(fn, dir, func() { ch <- true }, 200*time.Millisecond) + call := false + watcher.Events <- fsnotify.Event{Op: fsnotify.Create} + select { + case <-time.After(100 * time.Millisecond): + case call = <-ch: + } + if call { + t.Error("timer should not be triggered after 100ms") + } +} + +func TestTimerShouldTrigger(t *testing.T) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + t.Fatal(err) + } + defer watcher.Close() + fn := func() (*fsnotify.Watcher, error) { + return watcher, nil + } + dir, err := ioutil.TempDir("", "config") + if err != nil { + t.Fatal(err) + } + defer os.Remove(dir) + ch := make(chan bool) + watch(fn, dir, func() { ch <- true }, 100*time.Millisecond) + call := false + watcher.Events <- fsnotify.Event{Op: fsnotify.Create} + select { + case <-time.After(200 * time.Millisecond): + case call = <-ch: + } + if !call { + t.Error("timer should be triggered after 200ms") + } +} + +func TestTimerShouldTriggerOnce(t *testing.T) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + t.Fatal(err) + } + defer watcher.Close() + fn := func() (*fsnotify.Watcher, error) { + return watcher, nil + } + dir, err := ioutil.TempDir("", "config") + if err != nil { + t.Fatal(err) + } + defer os.Remove(dir) + ch := make(chan bool) + watch(fn, dir, func() { ch <- true }, 300*time.Millisecond) + call := false + watcher.Events <- fsnotify.Event{Op: fsnotify.Create} + select { + case <-time.After(200 * time.Millisecond): + case call = <-ch: + } + if call { + t.Error("timer should not be triggered after 200ms") + } + watcher.Events <- fsnotify.Event{Op: fsnotify.Create} + select { + case <-time.After(200 * time.Millisecond): + case call = <-ch: + } + if call { + t.Error("timer still should not be triggered after 400ms") + } + select { + case <-time.After(200 * time.Millisecond): + case call = <-ch: + } + if !call { + t.Error("timer should be triggered after 600ms") + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 0000000..3daee42 --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,19 @@ +{ + "comment": "", + "ignore": "", + "package": [ + { + "checksumSHA1": "74zwCNDSsh7NdJcEhgItnfU6d/E=", + "path": "github.com/fsnotify/fsnotify", + "revision": "fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197", + "revisionTime": "2016-10-26T20:31:22Z" + }, + { + "checksumSHA1": "yAfJ8xBj7cQuDtznpPwPt3UmJiE=", + "path": "golang.org/x/sys/unix", + "revision": "30237cf4eefd639b184d1f2cb77a581ea0be8947", + "revisionTime": "2016-11-19T15:29:01Z" + } + ], + "rootPath": "github.com/aandryashin/reloader" +}