diff --git a/README.md b/README.md index c99095ad..7ddcf78a 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,15 @@ The `Harvester` builder pattern is used to create a `Harvester` instance. The bu The above snippet set's up a `Harvester` instance with consul seed and monitor. +## Notification support + +In order to be able to monitor the changes in the configuration we provide a way to notify when a change is happening via the builder. + +```go + h, err := harvester.New(&cfg).WithNotification(chNotify).Create() + ... +``` + ## Consul Consul has support for versioning (`ModifyIndex`) which allows us to change the value only if the version is higher than the one currently. diff --git a/config/config.go b/config/config.go index dc1fe024..2d633ec4 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,18 @@ type CfgType interface { SetString(string) error } +// ChangeNotification definition for a configuration change. +type ChangeNotification struct { + Name string + Type string + Previous string + Current string +} + +func (n ChangeNotification) String() string { + return fmt.Sprintf("field [%s] of type [%s] changed from [%s] to [%s]", n.Name, n.Type, n.Previous, n.Current) +} + // Field definition of a config value that can change. type Field struct { name string @@ -40,16 +52,18 @@ type Field struct { version uint64 structField CfgType sources map[Source]string + chNotify chan<- ChangeNotification } // newField constructor. -func newField(prefix string, fld reflect.StructField, val reflect.Value) *Field { +func newField(prefix string, fld reflect.StructField, val reflect.Value, chNotify chan<- ChangeNotification) *Field { f := &Field{ name: prefix + fld.Name, tp: fld.Type.Name(), version: 0, structField: val.Addr().Interface().(CfgType), sources: make(map[Source]string), + chNotify: chNotify, } for _, tag := range sourceTags { @@ -94,27 +108,42 @@ func (f *Field) Set(value string, version uint64) error { return nil } + prevValue := f.structField.String() + if err := f.structField.SetString(value); err != nil { return err } f.version = version log.Infof("field %q updated with value %q, version: %d", f.name, f, version) + f.sendNotification(prevValue, value) return nil } +func (f *Field) sendNotification(prev string, current string) { + if f.chNotify == nil { + return + } + f.chNotify <- ChangeNotification{ + Name: f.name, + Type: f.tp, + Previous: prev, + Current: current, + } +} + // Config manages configuration and handles updates on the values. type Config struct { Fields []*Field } // New creates a new monitor. -func New(cfg interface{}) (*Config, error) { +func New(cfg interface{}, chNotify chan<- ChangeNotification) (*Config, error) { if cfg == nil { return nil, errors.New("configuration is nil") } - ff, err := newParser().ParseCfg(cfg) + ff, err := newParser().ParseCfg(cfg, chNotify) if err != nil { return nil, err } diff --git a/config/config_test.go b/config/config_test.go index c33c32ed..d5988782 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,30 +10,29 @@ import ( func TestField_Set(t *testing.T) { c := testConfig{} - cfg, err := New(&c) + cfg, err := New(&c, nil) require.NoError(t, err) cfg.Fields[0].version = 2 type args struct { value string version uint64 } - tests := []struct { - name string + tests := map[string]struct { field Field args args wantErr bool }{ - {name: "success String", field: *cfg.Fields[0], args: args{value: "John Doe", version: 3}, wantErr: false}, - {name: "success Int64", field: *cfg.Fields[1], args: args{value: "18", version: 1}, wantErr: false}, - {name: "success Float64", field: *cfg.Fields[2], args: args{value: "99.9", version: 1}, wantErr: false}, - {name: "success Bool", field: *cfg.Fields[3], args: args{value: "true", version: 1}, wantErr: false}, - {name: "failure Int64", field: *cfg.Fields[1], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "failure Float64", field: *cfg.Fields[2], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "failure Bool", field: *cfg.Fields[3], args: args{value: "XXX", version: 1}, wantErr: true}, - {name: "warn String version older", field: *cfg.Fields[0], args: args{value: "John Doe", version: 2}, wantErr: false}, + "success String": {field: *cfg.Fields[0], args: args{value: "John Doe", version: 3}, wantErr: false}, + "success Int64": {field: *cfg.Fields[1], args: args{value: "18", version: 1}, wantErr: false}, + "success Float64": {field: *cfg.Fields[2], args: args{value: "99.9", version: 1}, wantErr: false}, + "success Bool": {field: *cfg.Fields[3], args: args{value: "true", version: 1}, wantErr: false}, + "failure Int64": {field: *cfg.Fields[1], args: args{value: "XXX", version: 1}, wantErr: true}, + "failure Float64": {field: *cfg.Fields[2], args: args{value: "XXX", version: 1}, wantErr: true}, + "failure Bool": {field: *cfg.Fields[3], args: args{value: "XXX", version: 1}, wantErr: true}, + "warn String version older": {field: *cfg.Fields[0], args: args{value: "John Doe", version: 2}, wantErr: false}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err := tt.field.Set(tt.args.value, tt.args.version) if tt.wantErr { assert.Error(t, err) @@ -48,22 +47,21 @@ func TestNew(t *testing.T) { type args struct { cfg interface{} } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{cfg: &testConfig{}}, wantErr: false}, - {name: "cfg is nil", args: args{cfg: nil}, wantErr: true}, - {name: "cfg is not pointer", args: args{cfg: testConfig{}}, wantErr: true}, - {name: "cfg field not supported", args: args{cfg: &testInvalidTypeConfig{}}, wantErr: true}, - {name: "cfg duplicate consul key", args: args{cfg: &testDuplicateConfig{}}, wantErr: true}, - {name: "cfg tagged struct not supported", args: args{cfg: &testInvalidNestedStructWithTags{}}, wantErr: true}, - {name: "cfg nested duplicate consul key", args: args{cfg: &testDuplicateNestedConsulConfig{}}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}}, wantErr: false}, + "cfg is nil": {args: args{cfg: nil}, wantErr: true}, + "cfg is not pointer": {args: args{cfg: testConfig{}}, wantErr: true}, + "cfg field not supported": {args: args{cfg: &testInvalidTypeConfig{}}, wantErr: true}, + "cfg duplicate consul key": {args: args{cfg: &testDuplicateConfig{}}, wantErr: true}, + "cfg tagged struct not supported": {args: args{cfg: &testInvalidNestedStructWithTags{}}, wantErr: true}, + "cfg nested duplicate consul key": {args: args{cfg: &testDuplicateNestedConsulConfig{}}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := New(tt.args.cfg) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := New(tt.args.cfg, nil) if tt.wantErr { assert.Error(t, err) assert.Nil(t, got) @@ -97,20 +95,33 @@ func assertField(t *testing.T, fld *Field, name, typ string, sources map[Source] func TestConfig_Set(t *testing.T) { c := testConfig{} - cfg, err := New(&c) + chNotify := make(chan ChangeNotification, 1) + cfg, err := New(&c, chNotify) require.NoError(t, err) err = cfg.Fields[0].Set("John Doe", 1) assert.NoError(t, err) + change := <-chNotify + assert.Equal(t, "field [Name] of type [String] changed from [] to [John Doe]", change.String()) err = cfg.Fields[1].Set("18", 1) assert.NoError(t, err) + change = <-chNotify + assert.Equal(t, "field [Age] of type [Int64] changed from [0] to [18]", change.String()) err = cfg.Fields[2].Set("99.9", 1) assert.NoError(t, err) + change = <-chNotify + assert.Equal(t, "field [Balance] of type [Float64] changed from [0.000000] to [99.9]", change.String()) err = cfg.Fields[3].Set("true", 1) assert.NoError(t, err) + change = <-chNotify + assert.Equal(t, "field [HasJob] of type [Bool] changed from [false] to [true]", change.String()) err = cfg.Fields[4].Set("6000", 1) assert.NoError(t, err) + change = <-chNotify + assert.Equal(t, "field [PositionSalary] of type [Int64] changed from [0] to [6000]", change.String()) err = cfg.Fields[5].Set("baz", 1) assert.NoError(t, err) + change = <-chNotify + assert.Equal(t, "field [LevelOneLevelTwoDeepField] of type [String] changed from [] to [baz]", change.String()) assert.Equal(t, "John Doe", c.Name.Get()) assert.Equal(t, int64(18), c.Age.Get()) assert.Equal(t, 99.9, c.Balance.Get()) diff --git a/config/custom_type_test.go b/config/custom_type_test.go index 17d050c5..9f415138 100644 --- a/config/custom_type_test.go +++ b/config/custom_type_test.go @@ -12,7 +12,7 @@ import ( func TestCustomField(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) assert.NoError(t, err) err = cfg.Fields[0].Set("expected", 1) assert.NoError(t, err) @@ -24,7 +24,7 @@ func TestCustomField(t *testing.T) { func TestErrorValidationOnCustomField(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) assert.NoError(t, err) err = cfg.Fields[0].Set("not_expected", 1) assert.Error(t, err) diff --git a/config/parser.go b/config/parser.go index 6eec6a28..813db11d 100644 --- a/config/parser.go +++ b/config/parser.go @@ -22,7 +22,7 @@ func newParser() *parser { return &parser{} } -func (p *parser) ParseCfg(cfg interface{}) ([]*Field, error) { +func (p *parser) ParseCfg(cfg interface{}, chNotify chan<- ChangeNotification) ([]*Field, error) { p.dups = make(map[Source]string) tp := reflect.TypeOf(cfg) @@ -30,10 +30,10 @@ func (p *parser) ParseCfg(cfg interface{}) ([]*Field, error) { return nil, errors.New("configuration should be a pointer type") } - return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem()) + return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem(), chNotify) } -func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([]*Field, error) { +func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value, chNotify chan<- ChangeNotification) ([]*Field, error) { var ff []*Field for i := 0; i < tp.NumField(); i++ { @@ -46,13 +46,13 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([ switch typ { case typeField: - fld, err := p.createField(prefix, f, val.Field(i)) + fld, err := p.createField(prefix, f, val.Field(i), chNotify) if err != nil { return nil, err } ff = append(ff, fld) case typeStruct: - nested, err := p.getFields(prefix+f.Name, f.Type, val.Field(i)) + nested, err := p.getFields(prefix+f.Name, f.Type, val.Field(i), chNotify) if err != nil { return nil, err } @@ -62,8 +62,8 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([ return ff, nil } -func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value) (*Field, error) { - fld := newField(prefix, f, val) +func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value, chNotify chan<- ChangeNotification) (*Field, error) { + fld := newField(prefix, f, val, chNotify) value, ok := fld.Sources()[SourceConsul] if ok { diff --git a/examples/06_notification/main.go b/examples/06_notification/main.go new file mode 100644 index 00000000..cde07a5d --- /dev/null +++ b/examples/06_notification/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "log" + "os" + "sync" + + "github.com/beatlabs/harvester" + "github.com/beatlabs/harvester/config" + harvestersync "github.com/beatlabs/harvester/sync" +) + +type cfg struct { + IndexName harvestersync.String `seed:"customers-v1"` + CacheRetention harvestersync.Int64 `seed:"43200" env:"ENV_CACHE_RETENTION_SECONDS"` + LogLevel harvestersync.String `seed:"DEBUG" flag:"loglevel"` +} + +func main() { + ctx, cnl := context.WithCancel(context.Background()) + defer cnl() + + err := os.Setenv("ENV_CACHE_RETENTION_SECONDS", "86400") + if err != nil { + log.Fatalf("failed to set env var: %v", err) + } + + cfg := cfg{} + chNotify := make(chan config.ChangeNotification) + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + for change := range chNotify { + log.Printf("notification: " + change.String()) + } + wg.Done() + }() + + h, err := harvester.New(&cfg).WithNotification(chNotify).Create() + if err != nil { + log.Fatalf("failed to create harvester: %v", err) + } + + err = h.Harvest(ctx) + if err != nil { + log.Fatalf("failed to harvest configuration: %v", err) + } + + log.Printf("Config : IndexName: %s, CacheRetention: %d, LogLevel: %s\n", cfg.IndexName.Get(), cfg.CacheRetention.Get(), cfg.LogLevel.Get()) + close(chNotify) + wg.Wait() +} diff --git a/go.mod b/go.mod index d841ae45..00e91320 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/beatlabs/harvester -go 1.13 +go 1.15 require ( - github.com/hashicorp/go-hclog v0.15.0 github.com/hashicorp/consul/api v1.8.1 + github.com/hashicorp/go-hclog v0.15.0 github.com/stretchr/testify v1.6.1 ) diff --git a/harvester.go b/harvester.go index 3678698f..3f97ffea 100644 --- a/harvester.go +++ b/harvester.go @@ -2,6 +2,7 @@ package harvester import ( "context" + "errors" "time" "github.com/beatlabs/harvester/config" @@ -45,24 +46,35 @@ func (h *harvester) Harvest(ctx context.Context) error { return h.monitor.Monitor(ctx) } +type consulConfig struct { + addr, dataCenter, token string + timeout time.Duration +} + // Builder of a harvester instance. type Builder struct { - cfg *config.Config - watchers []monitor.Watcher - seedParams []seed.Param - err error + cfg interface{} + seedConsulCfg *consulConfig + monitorConsulCfg *consulConfig + err error + chNotify chan<- config.ChangeNotification } // New constructor. func New(cfg interface{}) *Builder { - b := &Builder{} - c, err := config.New(cfg) - if err != nil { - b.err = err + return &Builder{cfg: cfg} +} + +// WithNotification constructor. +func (b *Builder) WithNotification(chNotify chan<- config.ChangeNotification) *Builder { + if b.err != nil { + return b + } + if chNotify == nil { + b.err = errors.New("notification channel is nil") return b } - b.cfg = c - b.seedParams = []seed.Param{} + b.chNotify = chNotify return b } @@ -71,41 +83,27 @@ func (b *Builder) WithConsulSeed(addr, dataCenter, token string, timeout time.Du if b.err != nil { return b } - getter, err := seedConsul.New(addr, dataCenter, token, timeout) - if err != nil { - b.err = err - return b + b.seedConsulCfg = &consulConfig{ + addr: addr, + dataCenter: dataCenter, + token: token, + timeout: timeout, } - p, err := seed.NewParam(config.SourceConsul, getter) - if err != nil { - b.err = err - return b - } - b.seedParams = append(b.seedParams, *p) return b } // WithConsulMonitor enables support for monitoring key/prefixes on ConsulLogger. It automatically parses the config // and monitors every field found tagged with ConsulLogger. -func (b *Builder) WithConsulMonitor(addr, dc, token string, timeout time.Duration) *Builder { +func (b *Builder) WithConsulMonitor(addr, dataCenter, token string, timeout time.Duration) *Builder { if b.err != nil { return b } - items := make([]consul.Item, 0) - for _, field := range b.cfg.Fields { - consulKey, ok := field.Sources()[config.SourceConsul] - if !ok { - continue - } - log.Infof(`automatically monitoring consul key "%s"`, consulKey) - items = append(items, consul.NewKeyItem(consulKey)) - } - wtc, err := consul.New(addr, dc, token, timeout, items...) - if err != nil { - b.err = err - return b + b.monitorConsulCfg = &consulConfig{ + addr: addr, + dataCenter: dataCenter, + token: token, + timeout: timeout, } - b.watchers = append(b.watchers, wtc) return b } @@ -114,15 +112,64 @@ func (b *Builder) Create() (Harvester, error) { if b.err != nil { return nil, b.err } - sd := seed.New(b.seedParams...) - var mon Monitor - if len(b.watchers) == 0 { - return &harvester{seeder: sd, cfg: b.cfg}, nil + cfg, err := config.New(b.cfg, b.chNotify) + if err != nil { + return nil, err + } + + sd, err := b.setupSeeding() + if err != nil { + return nil, err + } + mon, err := b.setupMonitoring(cfg) + if err != nil { + return nil, err + } + + return &harvester{seeder: sd, monitor: mon, cfg: cfg}, nil +} + +func (b *Builder) setupSeeding() (Seeder, error) { + pp := make([]seed.Param, 0) + if b.seedConsulCfg != nil { + + getter, err := seedConsul.New(b.seedConsulCfg.addr, b.seedConsulCfg.dataCenter, b.seedConsulCfg.token, b.seedConsulCfg.timeout) + if err != nil { + return nil, err + } + + p, err := seed.NewParam(config.SourceConsul, getter) + if err != nil { + return nil, err + } + pp = append(pp, *p) + } + + return seed.New(pp...), nil +} + +func (b *Builder) setupMonitoring(cfg *config.Config) (Monitor, error) { + if b.monitorConsulCfg == nil { + return nil, nil + } + items := make([]consul.Item, 0) + for _, field := range cfg.Fields { + consulKey, ok := field.Sources()[config.SourceConsul] + if !ok { + continue + } + log.Infof(`automatically monitoring consul key "%s"`, consulKey) + items = append(items, consul.NewKeyItem(consulKey)) } - mon, err := monitor.New(b.cfg, b.watchers...) + wtc, err := consul.New(b.monitorConsulCfg.addr, b.monitorConsulCfg.dataCenter, b.monitorConsulCfg.token, b.monitorConsulCfg.timeout, items...) + if err != nil { + return nil, err + } + + mon, err := monitor.New(cfg, wtc) if err != nil { return nil, err } - return &harvester{seeder: sd, monitor: mon, cfg: b.cfg}, nil + return mon, nil } diff --git a/harvester_test.go b/harvester_test.go index d2f6f8ed..2381d252 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/beatlabs/harvester/config" "github.com/beatlabs/harvester/sync" "github.com/stretchr/testify/assert" ) @@ -17,17 +18,16 @@ func TestCreateWithConsul(t *testing.T) { cfg interface{} addr string } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "invalid cfg", args: args{cfg: "test", addr: addr}, wantErr: true}, - {name: "invalid address", args: args{cfg: &testConfig{}, addr: ""}, wantErr: true}, - {name: "success", args: args{cfg: &testConfig{}, addr: addr}, wantErr: false}, + "invalid cfg": {args: args{cfg: "test", addr: addr}, wantErr: true}, + "invalid address": {args: args{cfg: &testConfig{}, addr: ""}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}, addr: addr}, wantErr: false}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.cfg). WithConsulSeed(tt.args.addr, "", "", 0). WithConsulMonitor(tt.args.addr, "", "", 0). @@ -43,6 +43,32 @@ func TestCreateWithConsul(t *testing.T) { } } +func TestWithNotification(t *testing.T) { + type args struct { + cfg interface{} + chNotify chan<- config.ChangeNotification + } + tests := map[string]struct { + args args + wantErr bool + }{ + "nil notify channel": {args: args{cfg: &testConfig{}, chNotify: nil}, wantErr: true}, + "success": {args: args{cfg: &testConfig{}, chNotify: make(chan config.ChangeNotification)}, wantErr: false}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := New(tt.args.cfg).WithNotification(tt.args.chNotify).Create() + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + } + }) + } +} + func TestCreate_NoConsul(t *testing.T) { cfg := &testConfigNoConsul{} got, err := New(cfg).Create() diff --git a/log/log_test.go b/log/log_test.go index c6c79904..51b23834 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -32,19 +32,18 @@ func TestSetupLogging(t *testing.T) { errorf Func debugf Func } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: false}, - {name: "missing info", args: args{infof: nil, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, - {name: "missing warn", args: args{infof: stubLogf, warnf: nil, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, - {name: "missing error", args: args{infof: stubLogf, warnf: stubLogf, errorf: nil, debugf: stubLogf}, wantErr: true}, - {name: "missing debug", args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf}, wantErr: true}, + "success": {args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: false}, + "missing info": {args: args{infof: nil, warnf: stubLogf, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, + "missing warn": {args: args{infof: stubLogf, warnf: nil, errorf: stubLogf, debugf: stubLogf}, wantErr: true}, + "missing error": {args: args{infof: stubLogf, warnf: stubLogf, errorf: nil, debugf: stubLogf}, wantErr: true}, + "missing debug": {args: args{infof: stubLogf, warnf: stubLogf, errorf: stubLogf}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err := Setup(tt.args.infof, tt.args.warnf, tt.args.errorf, tt.args.debugf) if tt.wantErr { assert.Error(t, err) diff --git a/monitor/consul/watcher_test.go b/monitor/consul/watcher_test.go index 6fe3aab8..ff2a1125 100644 --- a/monitor/consul/watcher_test.go +++ b/monitor/consul/watcher_test.go @@ -17,18 +17,17 @@ func TestNew(t *testing.T) { timeout time.Duration ii []Item } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{addr: "xxx", timeout: 1 * time.Second, ii: ii}, wantErr: false}, - {name: "success default timeout", args: args{addr: "xxx", timeout: 0, ii: ii}, wantErr: false}, - {name: "empty address", args: args{addr: "", timeout: 1 * time.Second, ii: ii}, wantErr: true}, - {name: "empty items", args: args{addr: "xxx", timeout: 1 * time.Second, ii: nil}, wantErr: true}, + "success": {args: args{addr: "xxx", timeout: 1 * time.Second, ii: ii}, wantErr: false}, + "success default timeout": {args: args{addr: "xxx", timeout: 0, ii: ii}, wantErr: false}, + "empty address": {args: args{addr: "", timeout: 1 * time.Second, ii: ii}, wantErr: true}, + "empty items": {args: args{addr: "xxx", timeout: 1 * time.Second, ii: nil}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.addr, "dc", "token", tt.args.timeout, tt.args.ii...) if tt.wantErr { assert.Error(t, err) @@ -48,16 +47,15 @@ func TestWatcher_Watch(t *testing.T) { ctx context.Context ch chan<- []*change.Change } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "missing context", args: args{}, wantErr: true}, - {name: "missing chan", args: args{ctx: context.Background()}, wantErr: true}, + "missing context": {args: args{}, wantErr: true}, + "missing chan": {args: args{ctx: context.Background()}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { err = w.Watch(tt.args.ctx, tt.args.ch) if tt.wantErr { assert.Error(t, err) diff --git a/monitor/monitor_test.go b/monitor/monitor_test.go index b1a68b80..e60a7c22 100644 --- a/monitor/monitor_test.go +++ b/monitor/monitor_test.go @@ -14,9 +14,10 @@ import ( ) func TestNew(t *testing.T) { - cfg, err := config.New(&testConfig{}) + cfg, err := config.New(&testConfig{}, nil) + require.NoError(t, err) + errCfg, err := config.New(&testConfig{}, nil) require.NoError(t, err) - errCfg, err := config.New(&testConfig{}) errCfg.Fields[3].Sources()[config.SourceConsul] = "/config/balance" require.NoError(t, err) watchers := []Watcher{&testWatcher{}} @@ -24,18 +25,17 @@ func TestNew(t *testing.T) { cfg *config.Config ww []Watcher } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{cfg: cfg, ww: watchers}, wantErr: false}, - {name: "missing cfg", args: args{cfg: nil, ww: watchers}, wantErr: true}, - {name: "empty watchers", args: args{cfg: cfg, ww: nil}, wantErr: true}, - {name: "error watchers", args: args{cfg: errCfg, ww: watchers}, wantErr: true}, + "success": {args: args{cfg: cfg, ww: watchers}, wantErr: false}, + "missing cfg": {args: args{cfg: nil, ww: watchers}, wantErr: true}, + "empty watchers": {args: args{cfg: cfg, ww: nil}, wantErr: true}, + "error watchers": {args: args{cfg: errCfg, ww: watchers}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.cfg, tt.args.ww...) if tt.wantErr { assert.Error(t, err) @@ -49,7 +49,7 @@ func TestNew(t *testing.T) { } func TestMonitor_Monitor_Error(t *testing.T) { - cfg, err := config.New(&testConfig{}) + cfg, err := config.New(&testConfig{}, nil) require.NoError(t, err) watchers := []Watcher{&testWatcher{}, &testWatcher{err: true}} mon, err := New(cfg, watchers...) @@ -60,7 +60,7 @@ func TestMonitor_Monitor_Error(t *testing.T) { func TestMonitor_Monitor(t *testing.T) { c := &testConfig{} - cfg, err := config.New(c) + cfg, err := config.New(c, nil) require.NoError(t, err) watchers := []Watcher{&testWatcher{}} mon, err := New(cfg, watchers...) @@ -88,7 +88,7 @@ type testWatcher struct { err bool } -func (tw *testWatcher) Watch(ctx context.Context, ch chan<- []*change.Change) error { +func (tw *testWatcher) Watch(_ context.Context, ch chan<- []*change.Change) error { if tw.err { return errors.New("TEST") } diff --git a/seed/consul/getter_integration_test.go b/seed/consul/getter_integration_test.go index 108b7c5b..197d72b5 100644 --- a/seed/consul/getter_integration_test.go +++ b/seed/consul/getter_integration_test.go @@ -45,18 +45,17 @@ func TestGetter_Get(t *testing.T) { key string addr string } - tests := []struct { - name string + tests := map[string]struct { args args want *string wantErr bool }{ - {name: "success", args: args{addr: addr, key: "get_key1"}, want: &one, wantErr: false}, - {name: "missing key", args: args{addr: addr, key: "get_key2"}, want: nil, wantErr: false}, - {name: "wrong address", args: args{addr: "xxx", key: "get_key1"}, want: nil, wantErr: true}, + "success": {args: args{addr: addr, key: "get_key1"}, want: &one, wantErr: false}, + "missing key": {args: args{addr: addr, key: "get_key2"}, want: nil, wantErr: false}, + "wrong address": {args: args{addr: "xxx", key: "get_key1"}, want: nil, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { gtr, err := New(tt.args.addr, "", "", 0) require.NoError(t, err) got, version, err := gtr.Get(tt.args.key) diff --git a/seed/consul/getter_test.go b/seed/consul/getter_test.go index ee057153..a1847252 100644 --- a/seed/consul/getter_test.go +++ b/seed/consul/getter_test.go @@ -12,17 +12,16 @@ func TestNew(t *testing.T) { addr string timeout time.Duration } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{addr: "addr", timeout: 0}, wantErr: false}, - {name: "success explicit timeout", args: args{addr: "addr", timeout: 30 * time.Second}, wantErr: false}, - {name: "missing address", args: args{addr: ""}, wantErr: true}, + "success": {args: args{addr: "addr", timeout: 0}, wantErr: false}, + "success explicit timeout": {args: args{addr: "addr", timeout: 30 * time.Second}, wantErr: false}, + "missing address": {args: args{addr: ""}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := New(tt.args.addr, "dc", "token", 0) if tt.wantErr { assert.Error(t, err) diff --git a/seed/seed_test.go b/seed/seed_test.go index 2c95cc64..dfb2e5ee 100644 --- a/seed/seed_test.go +++ b/seed/seed_test.go @@ -17,16 +17,15 @@ func TestNewParam(t *testing.T) { src config.Source getter Getter } - tests := []struct { - name string + tests := map[string]struct { args args wantErr bool }{ - {name: "success", args: args{src: config.SourceConsul, getter: &testConsulGet{}}, wantErr: false}, - {name: "missing getter", args: args{src: config.SourceConsul, getter: nil}, wantErr: true}, + "success": {args: args{src: config.SourceConsul, getter: &testConsulGet{}}, wantErr: false}, + "missing getter": {args: args{src: config.SourceConsul, getter: nil}, wantErr: true}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for name, tt := range tests { + t.Run(name, func(t *testing.T) { got, err := NewParam(tt.args.src, tt.args.getter) if tt.wantErr { assert.Error(t, err) @@ -123,7 +122,7 @@ func TestSeeder_Seed_Flags(t *testing.T) { os.Args = append(os.Args, tC.extraCliArgs...) seeder := New() - cfg, err := config.New(tC.inputConfig) + cfg, err := config.New(tC.inputConfig, nil) require.NoError(t, err) err = seeder.Seed(cfg) @@ -139,75 +138,111 @@ func TestSeeder_Seed_Flags(t *testing.T) { } func TestSeeder_Seed(t *testing.T) { - require.NoError(t, os.Setenv("ENV_XXX", "XXX")) require.NoError(t, os.Setenv("ENV_AGE", "25")) require.NoError(t, os.Setenv("ENV_WORK_HOURS", "9h")) - c := testConfig{} - goodCfg, err := config.New(&c) - require.NoError(t, err) - prmSuccess, err := NewParam(config.SourceConsul, &testConsulGet{}) - require.NoError(t, err) - invalidIntCfg, err := config.New(&testInvalidInt{}) - require.NoError(t, err) - invalidFloatCfg, err := config.New(&testInvalidFloat{}) - require.NoError(t, err) - invalidBoolCfg, err := config.New(&testInvalidBool{}) - require.NoError(t, err) - missingCfg, err := config.New(&testMissingValue{}) - require.NoError(t, err) prmError, err := NewParam(config.SourceConsul, &testConsulGet{err: true}) require.NoError(t, err) - invalidFileIntCfg, err := config.New(&testInvalidFileInt{}) - require.NoError(t, err) - fileNotExistCfg, err := config.New(&testFileDoesNotExist{}) - require.NoError(t, err) - type fields struct { - consulParam *Param - } - type args struct { - cfg *config.Config - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - {name: "success", fields: fields{consulParam: prmSuccess}, args: args{cfg: goodCfg}}, - {name: "consul get nil", args: args{cfg: goodCfg}, wantErr: true}, - {name: "consul get error, seed successful", fields: fields{consulParam: prmError}, args: args{cfg: goodCfg}}, - {name: "consul missing value", fields: fields{consulParam: prmSuccess}, args: args{cfg: missingCfg}, wantErr: true}, - {name: "invalid int", args: args{cfg: invalidIntCfg}, wantErr: true}, - {name: "invalid float", args: args{cfg: invalidFloatCfg}, wantErr: true}, - {name: "invalid bool", fields: fields{consulParam: prmSuccess}, args: args{cfg: invalidBoolCfg}, wantErr: true}, - {name: "invalid file int", args: args{cfg: invalidFileIntCfg}, wantErr: true}, - {name: "file read error, seed successful", args: args{cfg: fileNotExistCfg}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var s *Seeder - if tt.fields.consulParam == nil { - s = New() - } else { - s = New(*tt.fields.consulParam) - } + t.Run("consul success", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + prmSuccess, err := NewParam(config.SourceConsul, &testConsulGet{}) + require.NoError(t, err) - err := s.Seed(tt.args.cfg) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, "John Doe", c.Name.Get()) - assert.Equal(t, int64(25), c.Age.Get()) - assert.Equal(t, 99.9, c.Balance.Get()) - assert.True(t, c.HasJob.Get()) - assert.Equal(t, "foobar", c.About.Get()) - assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) - } - }) - } + err = New(*prmSuccess).Seed(goodCfg) + + assert.NoError(t, err) + assert.Equal(t, "John Doe", c.Name.Get()) + assert.Equal(t, int64(25), c.Age.Get()) + assert.Equal(t, 99.9, c.Balance.Get()) + assert.True(t, c.HasJob.Get()) + assert.Equal(t, "foobar", c.About.Get()) + assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) + }) + + t.Run("consul error, success", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + + err = New(*prmError).Seed(goodCfg) + + assert.NoError(t, err) + assert.Equal(t, "John Doe", c.Name.Get()) + assert.Equal(t, int64(25), c.Age.Get()) + assert.Equal(t, 99.9, c.Balance.Get()) + assert.True(t, c.HasJob.Get()) + assert.Equal(t, "foobar", c.About.Get()) + assert.Equal(t, 9*time.Hour, c.WorkHours.Get()) + }) + + t.Run("file not exists, success", func(t *testing.T) { + c := &testFileDoesNotExist{} + fileNotExistCfg, err := config.New(c, nil) + require.NoError(t, err) + + err = New(*prmError).Seed(fileNotExistCfg) + + assert.NoError(t, err) + assert.Equal(t, int64(20), c.Age.Get()) + }) + + t.Run("consul nil, failure", func(t *testing.T) { + c := testConfig{} + goodCfg, err := config.New(&c, nil) + require.NoError(t, err) + + err = New().Seed(goodCfg) + + assert.Error(t, err) + }) + + t.Run("consul missing value, failure", func(t *testing.T) { + missingCfg, err := config.New(&testMissingValue{}, nil) + require.NoError(t, err) + + err = New().Seed(missingCfg) + + assert.Error(t, err) + }) + + t.Run("invalid int, failure", func(t *testing.T) { + invalidIntCfg, err := config.New(&testInvalidInt{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidIntCfg) + + assert.Error(t, err) + }) + + t.Run("invalid float, failure", func(t *testing.T) { + invalidFloatCfg, err := config.New(&testInvalidFloat{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidFloatCfg) + + assert.Error(t, err) + }) + + t.Run("invalid bool, failure", func(t *testing.T) { + invalidBoolCfg, err := config.New(&testInvalidBool{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidBoolCfg) + + assert.Error(t, err) + }) + + t.Run("invalid file int, failure", func(t *testing.T) { + invalidFileIntCfg, err := config.New(&testInvalidFileInt{}, nil) + require.NoError(t, err) + + err = New().Seed(invalidFileIntCfg) + + assert.Error(t, err) + }) } type testConfig struct { diff --git a/vendor/modules.txt b/vendor/modules.txt index 0eabd431..5035ac4f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -5,11 +5,13 @@ github.com/davecgh/go-spew/spew # github.com/fatih/color v1.9.0 github.com/fatih/color # github.com/hashicorp/consul/api v1.8.1 +## explicit github.com/hashicorp/consul/api github.com/hashicorp/consul/api/watch # github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cleanhttp # github.com/hashicorp/go-hclog v0.15.0 +## explicit github.com/hashicorp/go-hclog # github.com/hashicorp/go-immutable-radix v1.0.0 github.com/hashicorp/go-immutable-radix @@ -30,6 +32,7 @@ github.com/mitchellh/mapstructure # github.com/pmezard/go-difflib v1.0.0 github.com/pmezard/go-difflib/difflib # github.com/stretchr/testify v1.6.1 +## explicit github.com/stretchr/testify/assert github.com/stretchr/testify/require # golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae