// Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package viper import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "os" "os/exec" "path" "reflect" "runtime" "sort" "strings" "sync" "testing" "time" "github.com/fsnotify/fsnotify" "github.com/mitchellh/mapstructure" "github.com/spf13/afero" "github.com/spf13/cast" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var yamlExample = []byte(`Hacker: true name: steve hobbies: - skateboarding - snowboarding - go clothing: jacket: leather trousers: denim pants: size: large age: 35 eyes : brown beard: true `) var yamlExampleWithExtras = []byte(`Existing: true Bogus: true `) type testUnmarshalExtra struct { Existing bool } var tomlExample = []byte(` title = "TOML Example" [owner] organization = "MongoDB" Bio = "MongoDB Chief Developer Advocate & Hacker at Large" dob = 1979-05-27T07:32:00Z # First class dates? Why not?`) var jsonExample = []byte(`{ "id": "0001", "type": "donut", "name": "Cake", "ppu": 0.55, "batters": { "batter": [ { "type": "Regular" }, { "type": "Chocolate" }, { "type": "Blueberry" }, { "type": "Devil's Food" } ] } }`) var hclExample = []byte(` id = "0001" type = "donut" name = "Cake" ppu = 0.55 foos { foo { key = 1 } foo { key = 2 } foo { key = 3 } foo { key = 4 } }`) var propertiesExample = []byte(` p_id: 0001 p_type: donut p_name: Cake p_ppu: 0.55 p_batters.batter.type: Regular `) var remoteExample = []byte(`{ "id":"0002", "type":"cronut", "newkey":"remote" }`) func initConfigs() { Reset() var r io.Reader SetConfigType("yaml") r = bytes.NewReader(yamlExample) unmarshalReader(r, v.config) SetConfigType("json") r = bytes.NewReader(jsonExample) unmarshalReader(r, v.config) SetConfigType("hcl") r = bytes.NewReader(hclExample) unmarshalReader(r, v.config) SetConfigType("properties") r = bytes.NewReader(propertiesExample) unmarshalReader(r, v.config) SetConfigType("toml") r = bytes.NewReader(tomlExample) unmarshalReader(r, v.config) SetConfigType("json") remote := bytes.NewReader(remoteExample) unmarshalReader(remote, v.kvstore) } func initConfig(typ, config string) { Reset() SetConfigType(typ) r := strings.NewReader(config) if err := unmarshalReader(r, v.config); err != nil { panic(err) } } func initYAML() { initConfig("yaml", string(yamlExample)) } func initJSON() { Reset() SetConfigType("json") r := bytes.NewReader(jsonExample) unmarshalReader(r, v.config) } func initProperties() { Reset() SetConfigType("properties") r := bytes.NewReader(propertiesExample) unmarshalReader(r, v.config) } func initTOML() { Reset() SetConfigType("toml") r := bytes.NewReader(tomlExample) unmarshalReader(r, v.config) } func initHcl() { Reset() SetConfigType("hcl") r := bytes.NewReader(hclExample) unmarshalReader(r, v.config) } // make directories for testing func initDirs(t *testing.T) (string, string, func()) { var ( testDirs = []string{`a a`, `b`, `c\c`, `D_`} config = `improbable` ) root, err := ioutil.TempDir("", "") cleanup := true defer func() { if cleanup { os.Chdir("..") os.RemoveAll(root) } }() assert.Nil(t, err) err = os.Chdir(root) assert.Nil(t, err) for _, dir := range testDirs { err = os.Mkdir(dir, 0750) assert.Nil(t, err) err = ioutil.WriteFile( path.Join(dir, config+".toml"), []byte("key = \"value is "+dir+"\"\n"), 0640) assert.Nil(t, err) } cleanup = false return root, config, func() { os.Chdir("..") os.RemoveAll(root) } } //stubs for PFlag Values type stringValue string func newStringValue(val string, p *string) *stringValue { *p = val return (*stringValue)(p) } func (s *stringValue) Set(val string) error { *s = stringValue(val) return nil } func (s *stringValue) Type() string { return "string" } func (s *stringValue) String() string { return fmt.Sprintf("%s", *s) } func TestBasics(t *testing.T) { SetConfigFile("/tmp/config.yaml") filename, err := v.getConfigFile() assert.Equal(t, "/tmp/config.yaml", filename) assert.NoError(t, err) } func TestDefault(t *testing.T) { SetDefault("age", 45) assert.Equal(t, 45, Get("age")) SetDefault("clothing.jacket", "slacks") assert.Equal(t, "slacks", Get("clothing.jacket")) SetConfigType("yaml") err := ReadConfig(bytes.NewBuffer(yamlExample)) assert.NoError(t, err) assert.Equal(t, "leather", Get("clothing.jacket")) } func TestUnmarshaling(t *testing.T) { SetConfigType("yaml") r := bytes.NewReader(yamlExample) unmarshalReader(r, v.config) assert.True(t, InConfig("name")) assert.False(t, InConfig("state")) assert.Equal(t, "steve", Get("name")) assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, Get("hobbies")) assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, Get("clothing")) assert.Equal(t, 35, Get("age")) } func TestUnmarshalExact(t *testing.T) { vip := New() target := &testUnmarshalExtra{} vip.SetConfigType("yaml") r := bytes.NewReader(yamlExampleWithExtras) vip.ReadConfig(r) err := vip.UnmarshalExact(target) if err == nil { t.Fatal("UnmarshalExact should error when populating a struct from a conf that contains unused fields") } } func TestOverrides(t *testing.T) { Set("age", 40) assert.Equal(t, 40, Get("age")) } func TestDefaultPost(t *testing.T) { assert.NotEqual(t, "NYC", Get("state")) SetDefault("state", "NYC") assert.Equal(t, "NYC", Get("state")) } func TestAliases(t *testing.T) { RegisterAlias("years", "age") assert.Equal(t, 40, Get("years")) Set("years", 45) assert.Equal(t, 45, Get("age")) } func TestAliasInConfigFile(t *testing.T) { // the config file specifies "beard". If we make this an alias for // "hasbeard", we still want the old config file to work with beard. RegisterAlias("beard", "hasbeard") assert.Equal(t, true, Get("hasbeard")) Set("hasbeard", false) assert.Equal(t, false, Get("beard")) } func TestYML(t *testing.T) { initYAML() assert.Equal(t, "steve", Get("name")) } func TestJSON(t *testing.T) { initJSON() assert.Equal(t, "0001", Get("id")) } func TestProperties(t *testing.T) { initProperties() assert.Equal(t, "0001", Get("p_id")) } func TestTOML(t *testing.T) { initTOML() assert.Equal(t, "TOML Example", Get("title")) } func TestHCL(t *testing.T) { initHcl() assert.Equal(t, "0001", Get("id")) assert.Equal(t, 0.55, Get("ppu")) assert.Equal(t, "donut", Get("type")) assert.Equal(t, "Cake", Get("name")) Set("id", "0002") assert.Equal(t, "0002", Get("id")) assert.NotEqual(t, "cronut", Get("type")) } func TestRemotePrecedence(t *testing.T) { initJSON() remote := bytes.NewReader(remoteExample) assert.Equal(t, "0001", Get("id")) unmarshalReader(remote, v.kvstore) assert.Equal(t, "0001", Get("id")) assert.NotEqual(t, "cronut", Get("type")) assert.Equal(t, "remote", Get("newkey")) Set("newkey", "newvalue") assert.NotEqual(t, "remote", Get("newkey")) assert.Equal(t, "newvalue", Get("newkey")) Set("newkey", "remote") } func TestEnv(t *testing.T) { initJSON() BindEnv("id") BindEnv("f", "FOOD") os.Setenv("ID", "13") os.Setenv("FOOD", "apple") os.Setenv("NAME", "crunk") assert.Equal(t, "13", Get("id")) assert.Equal(t, "apple", Get("f")) assert.Equal(t, "Cake", Get("name")) AutomaticEnv() assert.Equal(t, "crunk", Get("name")) } func TestEnvPrefix(t *testing.T) { initJSON() SetEnvPrefix("foo") // will be uppercased automatically BindEnv("id") BindEnv("f", "FOOD") // not using prefix os.Setenv("FOO_ID", "13") os.Setenv("FOOD", "apple") os.Setenv("FOO_NAME", "crunk") assert.Equal(t, "13", Get("id")) assert.Equal(t, "apple", Get("f")) assert.Equal(t, "Cake", Get("name")) AutomaticEnv() assert.Equal(t, "crunk", Get("name")) } func TestAutoEnv(t *testing.T) { Reset() AutomaticEnv() os.Setenv("FOO_BAR", "13") assert.Equal(t, "13", Get("foo_bar")) } func TestAutoEnvWithPrefix(t *testing.T) { Reset() AutomaticEnv() SetEnvPrefix("Baz") os.Setenv("BAZ_BAR", "13") assert.Equal(t, "13", Get("bar")) } func TestSetEnvKeyReplacer(t *testing.T) { Reset() AutomaticEnv() os.Setenv("REFRESH_INTERVAL", "30s") replacer := strings.NewReplacer("-", "_") SetEnvKeyReplacer(replacer) assert.Equal(t, "30s", Get("refresh-interval")) } func TestAllKeys(t *testing.T) { initConfigs() ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos"} dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}} var allkeys sort.StringSlice allkeys = AllKeys() allkeys.Sort() ks.Sort() assert.Equal(t, ks, allkeys) assert.Equal(t, all, AllSettings()) } func TestAllKeysWithEnv(t *testing.T) { v := New() // bind and define environment variables (including a nested one) v.BindEnv("id") v.BindEnv("foo.bar") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) os.Setenv("ID", "13") os.Setenv("FOO_BAR", "baz") expectedKeys := sort.StringSlice{"id", "foo.bar"} expectedKeys.Sort() keys := sort.StringSlice(v.AllKeys()) keys.Sort() assert.Equal(t, expectedKeys, keys) } func TestAliasesOfAliases(t *testing.T) { Set("Title", "Checking Case") RegisterAlias("Foo", "Bar") RegisterAlias("Bar", "Title") assert.Equal(t, "Checking Case", Get("FOO")) } func TestRecursiveAliases(t *testing.T) { RegisterAlias("Baz", "Roo") RegisterAlias("Roo", "baz") } func TestUnmarshal(t *testing.T) { SetDefault("port", 1313) Set("name", "Steve") Set("duration", "1s1ms") type config struct { Port int Name string Duration time.Duration } var C config err := Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal(t, &config{Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond}, &C) Set("port", 1234) err = Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal(t, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond}, &C) } func TestUnmarshalWithDecoderOptions(t *testing.T) { Set("credentials", "{\"foo\":\"bar\"}") opt := DecodeHook(mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), // Custom Decode Hook Function func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) { if rf != reflect.String || rt != reflect.Map { return data, nil } m := map[string]string{} raw := data.(string) if raw == "" { return m, nil } return m, json.Unmarshal([]byte(raw), &m) }, )) type config struct { Credentials map[string]string } var C config err := Unmarshal(&C, opt) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal(t, &config{ Credentials: map[string]string{"foo": "bar"}, }, &C) } func TestBindPFlags(t *testing.T) { v := New() // create independent Viper object flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) var testValues = map[string]*string{ "host": nil, "port": nil, "endpoint": nil, } var mutatedTestValues = map[string]string{ "host": "localhost", "port": "6060", "endpoint": "/public", } for name := range testValues { testValues[name] = flagSet.String(name, "", "test") } err := v.BindPFlags(flagSet) if err != nil { t.Fatalf("error binding flag set, %v", err) } flagSet.VisitAll(func(flag *pflag.Flag) { flag.Value.Set(mutatedTestValues[flag.Name]) flag.Changed = true }) for name, expected := range mutatedTestValues { assert.Equal(t, expected, v.Get(name)) } } func TestBindPFlagsStringSlice(t *testing.T) { for _, testValue := range []struct { Expected []string Value string }{ {[]string{}, ""}, {[]string{"jeden"}, "jeden"}, {[]string{"dwa", "trzy"}, "dwa,trzy"}, {[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""}} { for _, changed := range []bool{true, false} { v := New() // create independent Viper object flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.StringSlice("stringslice", testValue.Expected, "test") flagSet.Visit(func(f *pflag.Flag) { if len(testValue.Value) > 0 { f.Value.Set(testValue.Value) f.Changed = changed } }) err := v.BindPFlags(flagSet) if err != nil { t.Fatalf("error binding flag set, %v", err) } type TestStr struct { StringSlice []string } val := &TestStr{} if err := v.Unmarshal(val); err != nil { t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err) } assert.Equal(t, testValue.Expected, val.StringSlice) } } } func TestBindPFlag(t *testing.T) { var testString = "testing" var testValue = newStringValue(testString, &testString) flag := &pflag.Flag{ Name: "testflag", Value: testValue, Changed: false, } BindPFlag("testvalue", flag) assert.Equal(t, testString, Get("testvalue")) flag.Value.Set("testing_mutate") flag.Changed = true //hack for pflag usage assert.Equal(t, "testing_mutate", Get("testvalue")) } func TestBoundCaseSensitivity(t *testing.T) { assert.Equal(t, "brown", Get("eyes")) BindEnv("eYEs", "TURTLE_EYES") os.Setenv("TURTLE_EYES", "blue") assert.Equal(t, "blue", Get("eyes")) var testString = "green" var testValue = newStringValue(testString, &testString) flag := &pflag.Flag{ Name: "eyeballs", Value: testValue, Changed: true, } BindPFlag("eYEs", flag) assert.Equal(t, "green", Get("eyes")) } func TestSizeInBytes(t *testing.T) { input := map[string]uint{ "": 0, "b": 0, "12 bytes": 0, "200000000000gb": 0, "12 b": 12, "43 MB": 43 * (1 << 20), "10mb": 10 * (1 << 20), "1gb": 1 << 30, } for str, expected := range input { assert.Equal(t, expected, parseSizeInBytes(str), str) } } func TestFindsNestedKeys(t *testing.T) { initConfigs() dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") Set("super", map[string]interface{}{ "deep": map[string]interface{}{ "nested": "value", }, }) expected := map[string]interface{}{ "super": map[string]interface{}{ "deep": map[string]interface{}{ "nested": "value", }, }, "super.deep": map[string]interface{}{ "nested": "value", }, "super.deep.nested": "value", "owner.organization": "MongoDB", "batters.batter": []interface{}{ map[string]interface{}{ "type": "Regular", }, map[string]interface{}{ "type": "Chocolate", }, map[string]interface{}{ "type": "Blueberry", }, map[string]interface{}{ "type": "Devil's Food", }, }, "hobbies": []interface{}{ "skateboarding", "snowboarding", "go", }, "title": "TOML Example", "newkey": "remote", "batters": map[string]interface{}{ "batter": []interface{}{ map[string]interface{}{ "type": "Regular", }, map[string]interface{}{ "type": "Chocolate", }, map[string]interface{}{ "type": "Blueberry", }, map[string]interface{}{ "type": "Devil's Food", }, }, }, "eyes": "brown", "age": 35, "owner": map[string]interface{}{ "organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob, }, "owner.bio": "MongoDB Chief Developer Advocate & Hacker at Large", "type": "donut", "id": "0001", "name": "Cake", "hacker": true, "ppu": 0.55, "clothing": map[string]interface{}{ "jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{ "size": "large", }, }, "clothing.jacket": "leather", "clothing.pants.size": "large", "clothing.trousers": "denim", "owner.dob": dob, "beard": true, "foos": []map[string]interface{}{ map[string]interface{}{ "foo": []map[string]interface{}{ map[string]interface{}{ "key": 1, }, map[string]interface{}{ "key": 2, }, map[string]interface{}{ "key": 3, }, map[string]interface{}{ "key": 4, }, }, }, }, } for key, expectedValue := range expected { assert.Equal(t, expectedValue, v.Get(key)) } } func TestReadBufConfig(t *testing.T) { v := New() v.SetConfigType("yaml") v.ReadConfig(bytes.NewBuffer(yamlExample)) t.Log(v.AllKeys()) assert.True(t, v.InConfig("name")) assert.False(t, v.InConfig("state")) assert.Equal(t, "steve", v.Get("name")) assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, v.Get("hobbies")) assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, v.Get("clothing")) assert.Equal(t, 35, v.Get("age")) } func TestIsSet(t *testing.T) { v := New() v.SetConfigType("yaml") v.ReadConfig(bytes.NewBuffer(yamlExample)) assert.True(t, v.IsSet("clothing.jacket")) assert.False(t, v.IsSet("clothing.jackets")) assert.False(t, v.IsSet("helloworld")) v.Set("helloworld", "fubar") assert.True(t, v.IsSet("helloworld")) } func TestDirsSearch(t *testing.T) { root, config, cleanup := initDirs(t) defer cleanup() v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) entries, err := ioutil.ReadDir(root) for _, e := range entries { if e.IsDir() { v.AddConfigPath(e.Name()) } } err = v.ReadInConfig() assert.Nil(t, err) assert.Equal(t, `value is `+path.Base(v.configPaths[0]), v.GetString(`key`)) } func TestWrongDirsSearchNotFound(t *testing.T) { _, config, cleanup := initDirs(t) defer cleanup() v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) v.AddConfigPath(`whattayoutalkingbout`) v.AddConfigPath(`thispathaintthere`) err := v.ReadInConfig() assert.Equal(t, reflect.TypeOf(ConfigFileNotFoundError{"", ""}), reflect.TypeOf(err)) // Even though config did not load and the error might have // been ignored by the client, the default still loads assert.Equal(t, `default`, v.GetString(`key`)) } func TestWrongDirsSearchNotFoundForMerge(t *testing.T) { _, config, cleanup := initDirs(t) defer cleanup() v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) v.AddConfigPath(`whattayoutalkingbout`) v.AddConfigPath(`thispathaintthere`) err := v.MergeInConfig() assert.Equal(t, reflect.TypeOf(ConfigFileNotFoundError{"", ""}), reflect.TypeOf(err)) // Even though config did not load and the error might have // been ignored by the client, the default still loads assert.Equal(t, `default`, v.GetString(`key`)) } func TestSub(t *testing.T) { v := New() v.SetConfigType("yaml") v.ReadConfig(bytes.NewBuffer(yamlExample)) subv := v.Sub("clothing") assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("pants.size")) subv = v.Sub("clothing.pants") assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size")) subv = v.Sub("clothing.pants.size") assert.Equal(t, (*Viper)(nil), subv) subv = v.Sub("missing.key") assert.Equal(t, (*Viper)(nil), subv) } var hclWriteExpected = []byte(`"foos" = { "foo" = { "key" = 1 } "foo" = { "key" = 2 } "foo" = { "key" = 3 } "foo" = { "key" = 4 } } "id" = "0001" "name" = "Cake" "ppu" = 0.55 "type" = "donut"`) func TestWriteConfigHCL(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("hcl") err := v.ReadConfig(bytes.NewBuffer(hclExample)) if err != nil { t.Fatal(err) } if err := v.WriteConfigAs("c.hcl"); err != nil { t.Fatal(err) } read, err := afero.ReadFile(fs, "c.hcl") if err != nil { t.Fatal(err) } assert.Equal(t, hclWriteExpected, read) } var jsonWriteExpected = []byte(`{ "batters": { "batter": [ { "type": "Regular" }, { "type": "Chocolate" }, { "type": "Blueberry" }, { "type": "Devil's Food" } ] }, "id": "0001", "name": "Cake", "ppu": 0.55, "type": "donut" }`) func TestWriteConfigJson(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("json") err := v.ReadConfig(bytes.NewBuffer(jsonExample)) if err != nil { t.Fatal(err) } if err := v.WriteConfigAs("c.json"); err != nil { t.Fatal(err) } read, err := afero.ReadFile(fs, "c.json") if err != nil { t.Fatal(err) } assert.Equal(t, jsonWriteExpected, read) } var propertiesWriteExpected = []byte(`p_id = 0001 p_type = donut p_name = Cake p_ppu = 0.55 p_batters.batter.type = Regular `) func TestWriteConfigProperties(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("properties") err := v.ReadConfig(bytes.NewBuffer(propertiesExample)) if err != nil { t.Fatal(err) } if err := v.WriteConfigAs("c.properties"); err != nil { t.Fatal(err) } read, err := afero.ReadFile(fs, "c.properties") if err != nil { t.Fatal(err) } assert.Equal(t, propertiesWriteExpected, read) } func TestWriteConfigTOML(t *testing.T) { fs := afero.NewMemMapFs() v := New() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("toml") err := v.ReadConfig(bytes.NewBuffer(tomlExample)) if err != nil { t.Fatal(err) } if err := v.WriteConfigAs("c.toml"); err != nil { t.Fatal(err) } // The TOML String method does not order the contents. // Therefore, we must read the generated file and compare the data. v2 := New() v2.SetFs(fs) v2.SetConfigName("c") v2.SetConfigType("toml") v2.SetConfigFile("c.toml") err = v2.ReadInConfig() if err != nil { t.Fatal(err) } assert.Equal(t, v.GetString("title"), v2.GetString("title")) assert.Equal(t, v.GetString("owner.bio"), v2.GetString("owner.bio")) assert.Equal(t, v.GetString("owner.dob"), v2.GetString("owner.dob")) assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization")) } var yamlWriteExpected = []byte(`age: 35 beard: true clothing: jacket: leather pants: size: large trousers: denim eyes: brown hacker: true hobbies: - skateboarding - snowboarding - go name: steve `) func TestWriteConfigYAML(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("yaml") err := v.ReadConfig(bytes.NewBuffer(yamlExample)) if err != nil { t.Fatal(err) } if err := v.WriteConfigAs("c.yaml"); err != nil { t.Fatal(err) } read, err := afero.ReadFile(fs, "c.yaml") if err != nil { t.Fatal(err) } assert.Equal(t, yamlWriteExpected, read) } var yamlMergeExampleTgt = []byte(` hello: pop: 37890 lagrenum: 765432101234567 world: - us - uk - fr - de `) var yamlMergeExampleSrc = []byte(` hello: pop: 45000 lagrenum: 7654321001234567 universe: - mw - ad fu: bar `) func TestMergeConfig(t *testing.T) { v := New() v.SetConfigType("yml") if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)); err != nil { t.Fatal(err) } if pop := v.GetInt("hello.pop"); pop != 37890 { t.Fatalf("pop != 37890, = %d", pop) } if pop := v.GetInt("hello.lagrenum"); pop != 765432101234567 { t.Fatalf("lagrenum != 765432101234567, = %d", pop) } if pop := v.GetInt32("hello.pop"); pop != int32(37890) { t.Fatalf("pop != 37890, = %d", pop) } if pop := v.GetInt64("hello.lagrenum"); pop != int64(765432101234567) { t.Fatalf("int64 lagrenum != 765432101234567, = %d", pop) } if world := v.GetStringSlice("hello.world"); len(world) != 4 { t.Fatalf("len(world) != 4, = %d", len(world)) } if fu := v.GetString("fu"); fu != "" { t.Fatalf("fu != \"\", = %s", fu) } if err := v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc)); err != nil { t.Fatal(err) } if pop := v.GetInt("hello.pop"); pop != 45000 { t.Fatalf("pop != 45000, = %d", pop) } if pop := v.GetInt("hello.lagrenum"); pop != 7654321001234567 { t.Fatalf("lagrenum != 7654321001234567, = %d", pop) } if pop := v.GetInt32("hello.pop"); pop != int32(45000) { t.Fatalf("pop != 45000, = %d", pop) } if pop := v.GetInt64("hello.lagrenum"); pop != int64(7654321001234567) { t.Fatalf("int64 lagrenum != 7654321001234567, = %d", pop) } if world := v.GetStringSlice("hello.world"); len(world) != 4 { t.Fatalf("len(world) != 4, = %d", len(world)) } if universe := v.GetStringSlice("hello.universe"); len(universe) != 2 { t.Fatalf("len(universe) != 2, = %d", len(universe)) } if fu := v.GetString("fu"); fu != "bar" { t.Fatalf("fu != \"bar\", = %s", fu) } } func TestMergeConfigNoMerge(t *testing.T) { v := New() v.SetConfigType("yml") if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)); err != nil { t.Fatal(err) } if pop := v.GetInt("hello.pop"); pop != 37890 { t.Fatalf("pop != 37890, = %d", pop) } if world := v.GetStringSlice("hello.world"); len(world) != 4 { t.Fatalf("len(world) != 4, = %d", len(world)) } if fu := v.GetString("fu"); fu != "" { t.Fatalf("fu != \"\", = %s", fu) } if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc)); err != nil { t.Fatal(err) } if pop := v.GetInt("hello.pop"); pop != 45000 { t.Fatalf("pop != 45000, = %d", pop) } if world := v.GetStringSlice("hello.world"); len(world) != 0 { t.Fatalf("len(world) != 0, = %d", len(world)) } if universe := v.GetStringSlice("hello.universe"); len(universe) != 2 { t.Fatalf("len(universe) != 2, = %d", len(universe)) } if fu := v.GetString("fu"); fu != "bar" { t.Fatalf("fu != \"bar\", = %s", fu) } } func TestUnmarshalingWithAliases(t *testing.T) { v := New() v.SetDefault("ID", 1) v.Set("name", "Steve") v.Set("lastname", "Owen") v.RegisterAlias("UserID", "ID") v.RegisterAlias("Firstname", "name") v.RegisterAlias("Surname", "lastname") type config struct { ID int FirstName string Surname string } var C config err := v.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C) } func TestSetConfigNameClearsFileCache(t *testing.T) { SetConfigFile("/tmp/config.yaml") SetConfigName("default") f, err := v.getConfigFile() if err == nil { t.Fatalf("config file cache should have been cleared") } assert.Empty(t, f) } func TestShadowedNestedValue(t *testing.T) { config := `name: steve clothing: jacket: leather trousers: denim pants: size: large ` initConfig("yaml", config) assert.Equal(t, "steve", GetString("name")) polyester := "polyester" SetDefault("clothing.shirt", polyester) SetDefault("clothing.jacket.price", 100) assert.Equal(t, "leather", GetString("clothing.jacket")) assert.Nil(t, Get("clothing.jacket.price")) assert.Equal(t, polyester, GetString("clothing.shirt")) clothingSettings := AllSettings()["clothing"].(map[string]interface{}) assert.Equal(t, "leather", clothingSettings["jacket"]) assert.Equal(t, polyester, clothingSettings["shirt"]) } func TestDotParameter(t *testing.T) { initJSON() // shoud take precedence over batters defined in jsonExample r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`)) unmarshalReader(r, v.config) actual := Get("batters.batter") expected := []interface{}{map[string]interface{}{"type": "Small"}} assert.Equal(t, expected, actual) } func TestCaseInsensitive(t *testing.T) { for _, config := range []struct { typ string content string }{ {"yaml", ` aBcD: 1 eF: gH: 2 iJk: 3 Lm: nO: 4 P: Q: 5 R: 6 `}, {"json", `{ "aBcD": 1, "eF": { "iJk": 3, "Lm": { "P": { "Q": 5, "R": 6 }, "nO": 4 }, "gH": 2 } }`}, {"toml", `aBcD = 1 [eF] gH = 2 iJk = 3 [eF.Lm] nO = 4 [eF.Lm.P] Q = 5 R = 6 `}, } { doTestCaseInsensitive(t, config.typ, config.content) } } func TestCaseInsensitiveSet(t *testing.T) { Reset() m1 := map[string]interface{}{ "Foo": 32, "Bar": map[interface{}]interface { }{ "ABc": "A", "cDE": "B"}, } m2 := map[string]interface{}{ "Foo": 52, "Bar": map[interface{}]interface { }{ "bCd": "A", "eFG": "B"}, } Set("Given1", m1) Set("Number1", 42) SetDefault("Given2", m2) SetDefault("Number2", 52) // Verify SetDefault if v := Get("number2"); v != 52 { t.Fatalf("Expected 52 got %q", v) } if v := Get("given2.foo"); v != 52 { t.Fatalf("Expected 52 got %q", v) } if v := Get("given2.bar.bcd"); v != "A" { t.Fatalf("Expected A got %q", v) } if _, ok := m2["Foo"]; !ok { t.Fatal("Input map changed") } // Verify Set if v := Get("number1"); v != 42 { t.Fatalf("Expected 42 got %q", v) } if v := Get("given1.foo"); v != 32 { t.Fatalf("Expected 32 got %q", v) } if v := Get("given1.bar.abc"); v != "A" { t.Fatalf("Expected A got %q", v) } if _, ok := m1["Foo"]; !ok { t.Fatal("Input map changed") } } func TestParseNested(t *testing.T) { type duration struct { Delay time.Duration } type item struct { Name string Delay time.Duration Nested duration } config := `[[parent]] delay="100ms" [parent.nested] delay="200ms" ` initConfig("toml", config) var items []item err := v.UnmarshalKey("parent", &items) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal(t, 1, len(items)) assert.Equal(t, 100*time.Millisecond, items[0].Delay) assert.Equal(t, 200*time.Millisecond, items[0].Nested.Delay) } func doTestCaseInsensitive(t *testing.T, typ, config string) { initConfig(typ, config) Set("RfD", true) assert.Equal(t, true, Get("rfd")) assert.Equal(t, true, Get("rFD")) assert.Equal(t, 1, cast.ToInt(Get("abcd"))) assert.Equal(t, 1, cast.ToInt(Get("Abcd"))) assert.Equal(t, 2, cast.ToInt(Get("ef.gh"))) assert.Equal(t, 3, cast.ToInt(Get("ef.ijk"))) assert.Equal(t, 4, cast.ToInt(Get("ef.lm.no"))) assert.Equal(t, 5, cast.ToInt(Get("ef.lm.p.q"))) } func newViperWithConfigFile(t *testing.T) (*Viper, string, func()) { watchDir, err := ioutil.TempDir("", "") require.Nil(t, err) configFile := path.Join(watchDir, "config.yaml") err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0640) require.Nil(t, err) cleanup := func() { os.RemoveAll(watchDir) } v := New() v.SetConfigFile(configFile) err = v.ReadInConfig() require.Nil(t, err) require.Equal(t, "bar", v.Get("foo")) return v, configFile, cleanup } func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string, func()) { watchDir, err := ioutil.TempDir("", "") require.Nil(t, err) dataDir1 := path.Join(watchDir, "data1") err = os.Mkdir(dataDir1, 0777) require.Nil(t, err) realConfigFile := path.Join(dataDir1, "config.yaml") t.Logf("Real config file location: %s\n", realConfigFile) err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0640) require.Nil(t, err) cleanup := func() { os.RemoveAll(watchDir) } // now, symlink the tm `data1` dir to `data` in the baseDir os.Symlink(dataDir1, path.Join(watchDir, "data")) // and link the `/datadir1/config.yaml` to `/config.yaml` configFile := path.Join(watchDir, "config.yaml") os.Symlink(path.Join(watchDir, "data", "config.yaml"), configFile) t.Logf("Config file location: %s\n", path.Join(watchDir, "config.yaml")) // init Viper v := New() v.SetConfigFile(configFile) err = v.ReadInConfig() require.Nil(t, err) require.Equal(t, "bar", v.Get("foo")) return v, watchDir, configFile, cleanup } func TestWatchFile(t *testing.T) { t.Run("file content changed", func(t *testing.T) { // given a `config.yaml` file being watched v, configFile, cleanup := newViperWithConfigFile(t) defer cleanup() _, err := os.Stat(configFile) require.NoError(t, err) t.Logf("test config file: %s\n", configFile) wg := sync.WaitGroup{} wg.Add(1) v.OnConfigChange(func(in fsnotify.Event) { t.Logf("config file changed") wg.Done() }) v.WatchConfig() // when overwriting the file and waiting for the custom change notification handler to be triggered err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640) wg.Wait() // then the config value should have changed require.Nil(t, err) assert.Equal(t, "baz", v.Get("foo")) }) t.Run("link to real file changed (à la Kubernetes)", func(t *testing.T) { // skip if not executed on Linux if runtime.GOOS != "linux" { t.Skipf("Skipping test as symlink replacements don't work on non-linux environment...") } v, watchDir, _, _ := newViperWithSymlinkedConfigFile(t) // defer cleanup() wg := sync.WaitGroup{} v.WatchConfig() v.OnConfigChange(func(in fsnotify.Event) { t.Logf("config file changed") wg.Done() }) wg.Add(1) // when link to another `config.yaml` file dataDir2 := path.Join(watchDir, "data2") err := os.Mkdir(dataDir2, 0777) require.Nil(t, err) configFile2 := path.Join(dataDir2, "config.yaml") err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0640) require.Nil(t, err) // change the symlink using the `ln -sfn` command err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run() require.Nil(t, err) wg.Wait() // then require.Nil(t, err) assert.Equal(t, "baz", v.Get("foo")) }) } func BenchmarkGetBool(b *testing.B) { key := "BenchmarkGetBool" v = New() v.Set(key, true) for i := 0; i < b.N; i++ { if !v.GetBool(key) { b.Fatal("GetBool returned false") } } } func BenchmarkGet(b *testing.B) { key := "BenchmarkGet" v = New() v.Set(key, true) for i := 0; i < b.N; i++ { if !v.Get(key).(bool) { b.Fatal("Get returned false") } } } // This is the "perfect result" for the above. func BenchmarkGetBoolFromMap(b *testing.B) { m := make(map[string]bool) key := "BenchmarkGetBool" m[key] = true for i := 0; i < b.N; i++ { if !m[key] { b.Fatal("Map value was false") } } }