Issue #539 - Add viper.CancelWatchConfig()

This commit is contained in:
Daniel Einspanjer 2018-07-25 09:23:24 -04:00
parent d493c32b69
commit 981de21183
2 changed files with 118 additions and 3 deletions

View file

@ -184,6 +184,7 @@ type Viper struct {
properties *properties.Properties properties *properties.Properties
onConfigChange func(fsnotify.Event) onConfigChange func(fsnotify.Event)
watchChannel chan bool
} }
// New returns an initialized Viper instance. // New returns an initialized Viper instance.
@ -277,7 +278,8 @@ func (v *Viper) WatchConfig() {
configFile := filepath.Clean(filename) configFile := filepath.Clean(filename)
configDir, _ := filepath.Split(configFile) configDir, _ := filepath.Split(configFile)
done := make(chan bool) v.watchChannel = make(chan bool)
go func() { go func() {
for { for {
select { select {
@ -293,16 +295,26 @@ func (v *Viper) WatchConfig() {
} }
} }
case err := <-watcher.Errors: case err := <-watcher.Errors:
if err != nil {
log.Println("error:", err) log.Println("error:", err)
} }
} }
}
}() }()
watcher.Add(configDir) watcher.Add(configDir)
<-done <-v.watchChannel
watcher.Close()
}() }()
} }
func CancelWatchConfig() {
v.CancelWatchConfig()
}
func (v *Viper) CancelWatchConfig() {
v.watchChannel <- true
}
// SetConfigFile explicitly defines the path, name and extension of the config file. // SetConfigFile explicitly defines the path, name and extension of the config file.
// Viper will use this and not check any of the config paths. // Viper will use this and not check any of the config paths.
func SetConfigFile(in string) { v.SetConfigFile(in) } func SetConfigFile(in string) { v.SetConfigFile(in) }

View file

@ -7,6 +7,7 @@ package viper
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -18,6 +19,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cast" "github.com/spf13/cast"
@ -768,6 +770,107 @@ func TestIsSet(t *testing.T) {
assert.True(t, v.IsSet("helloworld")) assert.True(t, v.IsSet("helloworld"))
} }
func TestWatchConfig(t *testing.T) {
before := []byte("key = \"value is before\"\n")
changed := []byte("key = \"value is changed\"\n")
after := []byte("key = \"value is after\"\n")
// This message is used for true asserts just making sure the file changes are happening as expected.
errMsg := "Test threw an unexpected error (test error)"
// Context is used for timeout handling within test.
var (
ctx context.Context
cancel context.CancelFunc
)
// Get a reference to a temporary file that we'll use for the test. Note we can't set a file extension using this API.
tmpfile, err := ioutil.TempFile("", "watch-test")
assert.Nil(t, err, errMsg)
defer os.Remove(tmpfile.Name()) // clean up
t.Log("Writing initial value to test config file: ", tmpfile.Name())
_, err = tmpfile.Write(before)
assert.Nil(t, err, errMsg)
err = tmpfile.Close()
assert.Nil(t, err, errMsg)
// This block just confirms that we properly wrote the desired contents to the file.
data, err := ioutil.ReadFile(tmpfile.Name())
assert.Nil(t, err, errMsg)
assert.Equal(t, data, before, "Contents of test file are unexpected (test error)")
// Set up a viper to test backing it with the tmp config file
v := New()
v.SetDefault(`key`, `value is default`)
v.SetConfigFile(tmpfile.Name())
v.SetConfigType("toml")
t.Log("Initial read of config")
err = v.ReadInConfig()
assert.Nil(t, err, errMsg)
assert.Equal(t, `value is before`, v.GetString(`key`), "viper did not see the correct initial value for the config file.")
// Set up a context with deadline so we won't wait forever if we don't see a change.
ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Turn on watch config.
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
t.Log("OnConfigChange executed.")
// If we get the signal of a change, we can immediately cancel the context.
cancel()
})
// XXX: I'm not sure why, but if we don't sleep, the watcher won't see a change that happens immediately after WatchConfig() is called.
time.Sleep(50 * time.Millisecond)
t.Log("Writing changed value to ", tmpfile.Name())
// Not sure if the TRUNC is necessary, but basically re-open the file so we can change it.
tmpfile, err = os.OpenFile(tmpfile.Name(), os.O_TRUNC|os.O_RDWR, 0644)
assert.Nil(t, err, errMsg)
_, err = tmpfile.Write(changed)
assert.Nil(t, err, errMsg)
err = tmpfile.Close()
assert.Nil(t, err, errMsg)
data, err = ioutil.ReadFile(tmpfile.Name())
assert.Nil(t, err, errMsg)
assert.Equal(t, data, changed, "Contents of test file are unexpected (test error)")
// Wait here for either a timeout or signal that we saw a change.
<-ctx.Done()
assert.NotEqual(t, context.DeadlineExceeded, ctx.Err(), "Timed out waiting for change notification.")
assert.Equal(t, `value is changed`, v.GetString(`key`), "viper did not see the correct changed value for the config file after WatchConfig().")
// Canceling turns off the fsevent Watcher so even if we end up starting a new viper instance (or calling viper.Reset()) we won't pick up spurious change events.
v.CancelWatchConfig()
// XXX: I'm not sure why, but if we don't sleep, the watcher might not fully close down before the next event happens. Doesn't affect this test, but can cause an error on the next one.
time.Sleep(50 * time.Millisecond)
// Now we make one more change to the file to verify the viper config doesn't pick up the change.
tmpfile, err = os.OpenFile(tmpfile.Name(), os.O_TRUNC|os.O_RDWR, 0644)
assert.Nil(t, err, errMsg)
_, err = tmpfile.Write(after)
assert.Nil(t, err, errMsg)
err = tmpfile.Close()
assert.Nil(t, err, errMsg)
data, err = ioutil.ReadFile(tmpfile.Name())
assert.Nil(t, err, errMsg)
assert.Equal(t, data, after, "Contents of test file are unexpected (test error)")
assert.NotEqual(t, `value is after`, v.GetString(`key`), "viper saw the after value in the file even after StopWatchConfig().")
}
func TestDirsSearch(t *testing.T) { func TestDirsSearch(t *testing.T) {
root, config, cleanup := initDirs(t) root, config, cleanup := initDirs(t)