mirror of
https://github.com/spf13/viper
synced 2025-04-28 16:27:17 +00:00
This patch refactors the IsSet function to examine the keys in order to see if a key is set instead of simply checking if a value is nil. This change is necessary due to the fact that default values via flag bindings will result in the old logic always being true for the IsSet function due to a type's default value such as 0 for an integer or an empty string for a string. While a type's default value may be preferable when getting the value for a key, it results in a false positive when determining if a key is actually set. This change enables users to detect whether a key is set by only returning a flag's value if it has changed.
665 lines
15 KiB
Go
665 lines
15 KiB
Go
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
|
//
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package viper
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
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 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 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()
|
|
SetConfigType("yaml")
|
|
r := bytes.NewReader(yamlExample)
|
|
unmarshalReader(r, v.config)
|
|
|
|
SetConfigType("json")
|
|
r = bytes.NewReader(jsonExample)
|
|
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 initYAML() {
|
|
Reset()
|
|
SetConfigType("yaml")
|
|
r := bytes.NewReader(yamlExample)
|
|
|
|
unmarshalReader(r, v.config)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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")
|
|
assert.Equal(t, "/tmp/config.yaml", v.getConfigFile())
|
|
}
|
|
|
|
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 TestUnmarshalling(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[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, Get("clothing"))
|
|
assert.Equal(t, 35, Get("age"))
|
|
}
|
|
|
|
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 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 TestSetEnvReplacer(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", "name", "beard", "ppu", "batters", "hobbies", "clothing", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name"}
|
|
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[interface{}]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[interface{}]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.batter.type": "Regular", "p_type": "donut"}
|
|
|
|
var allkeys sort.StringSlice
|
|
allkeys = AllKeys()
|
|
allkeys.Sort()
|
|
ks.Sort()
|
|
|
|
assert.Equal(t, ks, allkeys)
|
|
assert.Equal(t, all, AllSettings())
|
|
}
|
|
|
|
func TestCaseInSensitive(t *testing.T) {
|
|
assert.Equal(t, true, Get("hacker"))
|
|
Set("Title", "Checking Case")
|
|
assert.Equal(t, "Checking Case", Get("tItle"))
|
|
}
|
|
|
|
func TestAliasesOfAliases(t *testing.T) {
|
|
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")
|
|
|
|
type config struct {
|
|
Port int
|
|
Name string
|
|
}
|
|
|
|
var C config
|
|
|
|
err := Unmarshal(&C)
|
|
if err != nil {
|
|
t.Fatalf("unable to decode into struct, %v", err)
|
|
}
|
|
|
|
assert.Equal(t, &C, &config{Name: "Steve", Port: 1313})
|
|
|
|
Set("port", 1234)
|
|
err = Unmarshal(&C)
|
|
if err != nil {
|
|
t.Fatalf("unable to decode into struct, %v", err)
|
|
}
|
|
assert.Equal(t, &C, &config{Name: "Steve", Port: 1234})
|
|
}
|
|
|
|
func TestBindPFlags(t *testing.T) {
|
|
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 := 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, Get(name), expected)
|
|
}
|
|
|
|
}
|
|
|
|
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[interface{}]interface{}{
|
|
"jacket": "leather",
|
|
"trousers": "denim",
|
|
"pants": map[interface{}]interface{}{
|
|
"size": "large",
|
|
},
|
|
},
|
|
"clothing.jacket": "leather",
|
|
"clothing.pants.size": "large",
|
|
"clothing.trousers": "denim",
|
|
"owner.dob": dob,
|
|
"beard": true,
|
|
}
|
|
|
|
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[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]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(UnsupportedConfigError("")), 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`))
|
|
}
|