Get() preserves the concrete type of a map[string]T value loaded via Set() or a provider like structs, but MapKeys() only type-asserted map[string]any, so it silently reported no keys for anything else. Read keys via reflection instead so any map with string keys works.
2164 lines
65 KiB
Go
2164 lines
65 KiB
Go
package koanf_test
|
|
|
|
import (
|
|
encjson "encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"maps"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/knadh/koanf/parsers/dotenv"
|
|
"github.com/knadh/koanf/parsers/hcl"
|
|
"github.com/knadh/koanf/parsers/hjson"
|
|
"github.com/knadh/koanf/parsers/json"
|
|
"github.com/knadh/koanf/parsers/toml"
|
|
"github.com/knadh/koanf/parsers/yaml"
|
|
"github.com/knadh/koanf/providers/basicflag"
|
|
"github.com/knadh/koanf/providers/confmap"
|
|
"github.com/knadh/koanf/providers/env/v2"
|
|
"github.com/knadh/koanf/providers/file"
|
|
"github.com/knadh/koanf/providers/posflag"
|
|
"github.com/knadh/koanf/providers/rawbytes"
|
|
"github.com/knadh/koanf/v2"
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
delim = "."
|
|
|
|
mockDir = "../mock"
|
|
mockJSON = mockDir + "/mock.json"
|
|
mockYAML = mockDir + "/mock.yml"
|
|
mockTOML = mockDir + "/mock.toml"
|
|
mockHCL = mockDir + "/mock.hcl"
|
|
mockProp = mockDir + "/mock.prop"
|
|
mockDotEnv = mockDir + "/mock.env"
|
|
mockHJSON = mockDir + "/mock.hjson"
|
|
)
|
|
|
|
// Ordered list of fields in the 'flat' test confs.
|
|
var flatTestKeys = []string{
|
|
"COMMENT",
|
|
"MORE",
|
|
"MiXeD",
|
|
"UPPER",
|
|
"empty",
|
|
"lower",
|
|
"quotedSpecial",
|
|
}
|
|
|
|
var flatTestKeyMap = map[string][]string{
|
|
"COMMENT": {"COMMENT"},
|
|
"MORE": {"MORE"},
|
|
"MiXeD": {"MiXeD"},
|
|
"UPPER": {"UPPER"},
|
|
"empty": {"empty"},
|
|
"lower": {"lower"},
|
|
"quotedSpecial": {"quotedSpecial"},
|
|
}
|
|
|
|
var flatTestAll = `COMMENT -> AFTER
|
|
MORE -> vars
|
|
MiXeD -> CaSe
|
|
UPPER -> CASE
|
|
empty ->
|
|
lower -> case
|
|
quotedSpecial -> j18120734xn2&*@#*&R#d1j23d*(*)`
|
|
|
|
// Ordered list of fields in the test confs.
|
|
var testKeys = []string{
|
|
"bools",
|
|
"duration",
|
|
"empty",
|
|
"intbools",
|
|
"negative_int",
|
|
"orphan",
|
|
"parent1.boolmap.notok3",
|
|
"parent1.boolmap.ok1",
|
|
"parent1.boolmap.ok2",
|
|
"parent1.child1.empty",
|
|
"parent1.child1.grandchild1.ids",
|
|
"parent1.child1.grandchild1.on",
|
|
"parent1.child1.name",
|
|
"parent1.child1.type",
|
|
"parent1.floatmap.key1",
|
|
"parent1.floatmap.key2",
|
|
"parent1.floatmap.key3",
|
|
"parent1.id",
|
|
"parent1.intmap.key1",
|
|
"parent1.intmap.key2",
|
|
"parent1.intmap.key3",
|
|
"parent1.name",
|
|
"parent1.strmap.key1",
|
|
"parent1.strmap.key2",
|
|
"parent1.strmap.key3",
|
|
"parent1.strsmap.key1",
|
|
"parent1.strsmap.key2",
|
|
"parent2.child2.empty",
|
|
"parent2.child2.grandchild2.ids",
|
|
"parent2.child2.grandchild2.on",
|
|
"parent2.child2.name",
|
|
"parent2.id",
|
|
"parent2.name",
|
|
"strbool",
|
|
"strbools",
|
|
"time",
|
|
"type",
|
|
}
|
|
|
|
var testKeyMap = map[string][]string{
|
|
"bools": {"bools"},
|
|
"duration": {"duration"},
|
|
"empty": {"empty"},
|
|
"intbools": {"intbools"},
|
|
"negative_int": {"negative_int"},
|
|
"orphan": {"orphan"},
|
|
"parent1": {"parent1"},
|
|
"parent1.boolmap": {"parent1", "boolmap"},
|
|
"parent1.boolmap.notok3": {"parent1", "boolmap", "notok3"},
|
|
"parent1.boolmap.ok1": {"parent1", "boolmap", "ok1"},
|
|
"parent1.boolmap.ok2": {"parent1", "boolmap", "ok2"},
|
|
"parent1.child1": {"parent1", "child1"},
|
|
"parent1.child1.empty": {"parent1", "child1", "empty"},
|
|
"parent1.child1.grandchild1": {"parent1", "child1", "grandchild1"},
|
|
"parent1.child1.grandchild1.ids": {"parent1", "child1", "grandchild1", "ids"},
|
|
"parent1.child1.grandchild1.on": {"parent1", "child1", "grandchild1", "on"},
|
|
"parent1.child1.name": {"parent1", "child1", "name"},
|
|
"parent1.child1.type": {"parent1", "child1", "type"},
|
|
"parent1.floatmap": {"parent1", "floatmap"},
|
|
"parent1.floatmap.key1": {"parent1", "floatmap", "key1"},
|
|
"parent1.floatmap.key2": {"parent1", "floatmap", "key2"},
|
|
"parent1.floatmap.key3": {"parent1", "floatmap", "key3"},
|
|
"parent1.id": {"parent1", "id"},
|
|
"parent1.intmap": {"parent1", "intmap"},
|
|
"parent1.intmap.key1": {"parent1", "intmap", "key1"},
|
|
"parent1.intmap.key2": {"parent1", "intmap", "key2"},
|
|
"parent1.intmap.key3": {"parent1", "intmap", "key3"},
|
|
"parent1.name": {"parent1", "name"},
|
|
"parent1.strmap": {"parent1", "strmap"},
|
|
"parent1.strmap.key1": {"parent1", "strmap", "key1"},
|
|
"parent1.strmap.key2": {"parent1", "strmap", "key2"},
|
|
"parent1.strmap.key3": {"parent1", "strmap", "key3"},
|
|
"parent1.strsmap": {"parent1", "strsmap"},
|
|
"parent1.strsmap.key1": {"parent1", "strsmap", "key1"},
|
|
"parent1.strsmap.key2": {"parent1", "strsmap", "key2"},
|
|
"parent2": {"parent2"},
|
|
"parent2.child2": {"parent2", "child2"},
|
|
"parent2.child2.empty": {"parent2", "child2", "empty"},
|
|
"parent2.child2.grandchild2": {"parent2", "child2", "grandchild2"},
|
|
"parent2.child2.grandchild2.ids": {"parent2", "child2", "grandchild2", "ids"},
|
|
"parent2.child2.grandchild2.on": {"parent2", "child2", "grandchild2", "on"},
|
|
"parent2.child2.name": {"parent2", "child2", "name"},
|
|
"parent2.id": {"parent2", "id"},
|
|
"parent2.name": {"parent2", "name"},
|
|
"strbool": {"strbool"},
|
|
"strbools": {"strbools"},
|
|
"time": {"time"},
|
|
"type": {"type"},
|
|
}
|
|
|
|
// `parent1.child1.type` and `type` are excluded as their
|
|
// values vary between files.
|
|
var testAll = `bools -> [true false true]
|
|
duration -> 3s
|
|
empty -> map[]
|
|
intbools -> [1 0 1]
|
|
negative_int -> -1234
|
|
orphan -> [red blue orange]
|
|
parent1.boolmap.notok3 -> false
|
|
parent1.boolmap.ok1 -> true
|
|
parent1.boolmap.ok2 -> true
|
|
parent1.child1.empty -> map[]
|
|
parent1.child1.grandchild1.ids -> [1 2 3]
|
|
parent1.child1.grandchild1.on -> true
|
|
parent1.child1.name -> child1
|
|
parent1.floatmap.key1 -> 1.1
|
|
parent1.floatmap.key2 -> 1.2
|
|
parent1.floatmap.key3 -> 1.3
|
|
parent1.id -> 1234
|
|
parent1.intmap.key1 -> 1
|
|
parent1.intmap.key2 -> 1
|
|
parent1.intmap.key3 -> 1
|
|
parent1.name -> parent1
|
|
parent1.strmap.key1 -> val1
|
|
parent1.strmap.key2 -> val2
|
|
parent1.strmap.key3 -> val3
|
|
parent1.strsmap.key1 -> [val1 val2 val3]
|
|
parent1.strsmap.key2 -> [val4 val5]
|
|
parent2.child2.empty -> map[]
|
|
parent2.child2.grandchild2.ids -> [4 5 6]
|
|
parent2.child2.grandchild2.on -> true
|
|
parent2.child2.name -> child2
|
|
parent2.id -> 5678
|
|
parent2.name -> parent2
|
|
strbool -> 1
|
|
strbools -> [1 t f]
|
|
time -> 2019-01-01`
|
|
|
|
var testParent2 = `child2.empty -> map[]
|
|
child2.grandchild2.ids -> [4 5 6]
|
|
child2.grandchild2.on -> true
|
|
child2.name -> child2
|
|
id -> 5678
|
|
name -> parent2`
|
|
|
|
type parentStruct struct {
|
|
Name string `koanf:"name"`
|
|
ID int `koanf:"id"`
|
|
Child1 childStruct `koanf:"child1"`
|
|
}
|
|
type childStruct struct {
|
|
Name string `koanf:"name"`
|
|
Type string `koanf:"type"`
|
|
Empty map[string]string `koanf:"empty"`
|
|
Grandchild1 grandchildStruct `koanf:"grandchild1"`
|
|
}
|
|
type grandchildStruct struct {
|
|
Ids []int `koanf:"ids"`
|
|
On bool `koanf:"on"`
|
|
}
|
|
type testStruct struct {
|
|
Type string `koanf:"type"`
|
|
Empty map[string]string `koanf:"empty"`
|
|
Parent1 parentStruct `koanf:"parent1"`
|
|
}
|
|
|
|
type testStructFlat struct {
|
|
Type string `koanf:"type"`
|
|
Empty map[string]string `koanf:"empty"`
|
|
Parent1Name string `koanf:"parent1.name"`
|
|
Parent1ID int `koanf:"parent1.id"`
|
|
Parent1Child1Name string `koanf:"parent1.child1.name"`
|
|
Parent1Child1Type string `koanf:"parent1.child1.type"`
|
|
Parent1Child1Empty map[string]string `koanf:"parent1.child1.empty"`
|
|
Parent1Child1Grandchild1IDs []int `koanf:"parent1.child1.grandchild1.ids"`
|
|
Parent1Child1Grandchild1On bool `koanf:"parent1.child1.grandchild1.on"`
|
|
}
|
|
|
|
type customText int
|
|
|
|
func (c *customText) UnmarshalText(text []byte) error {
|
|
s := strings.ToLower(string(text))
|
|
|
|
switch {
|
|
case strings.HasSuffix(s, "mb"):
|
|
s = strings.TrimSuffix(s, "mb")
|
|
default:
|
|
s = "0"
|
|
}
|
|
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*c = customText(v)
|
|
return nil
|
|
}
|
|
|
|
type Case struct {
|
|
koanf *koanf.Koanf
|
|
file string
|
|
parser koanf.Parser
|
|
typeName string
|
|
}
|
|
|
|
// 'Flat' Case instances to be used in multiple tests. These will not be
|
|
// mutated.
|
|
var flatCases = []Case{
|
|
{koanf: koanf.New(delim), file: mockDotEnv, parser: dotenv.Parser(), typeName: "dotenv"},
|
|
}
|
|
|
|
// Case instances to be used in multiple tests. These will not be mutated.
|
|
var cases = []Case{
|
|
{koanf: koanf.New(delim), file: mockJSON, parser: json.Parser(), typeName: "json"},
|
|
{koanf: koanf.New(delim), file: mockYAML, parser: yaml.Parser(), typeName: "yml"},
|
|
{koanf: koanf.New(delim), file: mockTOML, parser: toml.Parser(), typeName: "toml"},
|
|
{koanf: koanf.New(delim), file: mockHCL, parser: hcl.Parser(true), typeName: "hcl"},
|
|
{koanf: koanf.New(delim), file: mockHJSON, parser: hjson.Parser(), typeName: "hjson"},
|
|
}
|
|
|
|
func init() {
|
|
// Preload 4 Koanf instances with their providers and config.
|
|
if err := cases[0].koanf.Load(file.Provider(cases[0].file), json.Parser()); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
if err := cases[1].koanf.Load(file.Provider(cases[1].file), yaml.Parser()); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
if err := cases[2].koanf.Load(file.Provider(cases[2].file), toml.Parser()); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
if err := cases[3].koanf.Load(file.Provider(cases[3].file), hcl.Parser(true)); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
if err := cases[4].koanf.Load(file.Provider(cases[4].file), hjson.Parser()); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
|
|
// Preload 1 'flat' Koanf instances with their providers and config.
|
|
if err := flatCases[0].koanf.Load(file.Provider(flatCases[0].file), dotenv.Parser()); err != nil {
|
|
log.Fatalf("error loading config file: %v", err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLoadFile(b *testing.B) {
|
|
k := koanf.New(delim)
|
|
|
|
// Don't use TOML here because it distorts memory benchmarks due to high memory use
|
|
providers := []*file.File{file.Provider(mockJSON), file.Provider(mockYAML)}
|
|
parsers := []koanf.Parser{json.Parser(), yaml.Parser()}
|
|
|
|
b.ResetTimer()
|
|
for n := 0; n < b.N; n++ {
|
|
if err := k.Load(providers[n%2], parsers[n%2]); err != nil {
|
|
b.Fatalf("Unexpected error: %+v", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadFile(t *testing.T) {
|
|
// Load a non-existent file.
|
|
_, err := file.Provider("does-not-exist").ReadBytes()
|
|
assert.NotNil(t, err, "no error for non-existent file")
|
|
|
|
// Load a valid file.
|
|
_, err = file.Provider(mockJSON).ReadBytes()
|
|
assert.Nil(t, err, "error loading file")
|
|
}
|
|
|
|
func TestLoadFlatFileAllKeys(t *testing.T) {
|
|
assert := assert.New(t)
|
|
re, _ := regexp.Compile("(.+?)?type \\-> (.*)\n")
|
|
for _, c := range flatCases {
|
|
// Check against testKeys.
|
|
assert.Equal(flatTestKeys, c.koanf.Keys(), fmt.Sprintf("loaded keys mismatch: %v", c.typeName))
|
|
|
|
// Check against keyMap.
|
|
assert.EqualValues(flatTestKeyMap, c.koanf.KeyMap(), "keymap doesn't match")
|
|
|
|
// Replace the "type" fields that varies across different files
|
|
// to do a complete key -> value map match with testAll.
|
|
s := strings.TrimSpace(re.ReplaceAllString(c.koanf.Sprint(), ""))
|
|
assert.Equal(flatTestAll, s, fmt.Sprintf("key -> value list mismatch: %v", c.typeName))
|
|
}
|
|
}
|
|
|
|
func TestLoadFileAllKeys(t *testing.T) {
|
|
assert := assert.New(t)
|
|
re, _ := regexp.Compile("(.+?)?type \\-> (.*)\n")
|
|
for _, c := range cases {
|
|
// Check against testKeys.
|
|
assert.Equal(testKeys, c.koanf.Keys(), fmt.Sprintf("loaded keys mismatch: %v", c.typeName))
|
|
|
|
// Check against keyMap.
|
|
assert.EqualValues(testKeyMap, c.koanf.KeyMap(), "keymap doesn't match")
|
|
|
|
// Replace the "type" fields that varies across different files
|
|
// to do a complete key -> value map match with testAll.
|
|
s := strings.TrimSpace(re.ReplaceAllString(c.koanf.Sprint(), ""))
|
|
assert.Equal(testAll, s, fmt.Sprintf("key -> value list mismatch: %v", c.typeName))
|
|
}
|
|
}
|
|
|
|
func TestDelim(t *testing.T) {
|
|
k1 := koanf.New(".")
|
|
assert.Equal(t, k1.Delim(), ".")
|
|
|
|
k2 := koanf.New("/")
|
|
assert.Equal(t, k2.Delim(), "/")
|
|
}
|
|
|
|
func TestLoadMergeYamlJson(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
assert.NoError(k.Load(file.Provider(mockYAML), yaml.Parser()),
|
|
"error loading file")
|
|
// loading json after yaml causes the intbools to be loaded as []float64
|
|
assert.NoError(k.Load(file.Provider(mockJSON), yaml.Parser()),
|
|
"error loading file")
|
|
|
|
// checking that there is no issues with expecting it to be an []int64
|
|
v := k.Int64s("intbools")
|
|
assert.Len(v, 3)
|
|
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
assert.Failf("panic", "received panic: %v", err)
|
|
}
|
|
}()
|
|
|
|
v2 := k.MustInt64s("intbools")
|
|
assert.Len(v2, 3)
|
|
}
|
|
|
|
func TestLoadMergeJsonYaml(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
assert.NoError(k.Load(file.Provider(mockJSON), yaml.Parser()),
|
|
"error loading file")
|
|
// loading yaml after json causes the intbools to be loaded as []int after json loaded it with []float64
|
|
assert.NoError(k.Load(file.Provider(mockYAML), yaml.Parser()),
|
|
"error loading file")
|
|
|
|
// checking that there is no issues with expecting it to be an []float64
|
|
v := k.Float64s("intbools")
|
|
assert.Len(v, 3)
|
|
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
assert.Failf("panic", "received panic: %v", err)
|
|
}
|
|
}()
|
|
|
|
v2 := k.MustFloat64s("intbools")
|
|
assert.Len(v2, 3)
|
|
}
|
|
|
|
func TestWatchFile(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
// Create a tmp config file.
|
|
tmpDir := t.TempDir()
|
|
tmpFile := filepath.Join(tmpDir, "koanf_mock")
|
|
err := os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name1"}}`), 0600)
|
|
require.NoError(t, err, "error creating temp config file: %v", err)
|
|
|
|
// Load the new config and watch it for changes.
|
|
f := file.Provider(tmpFile)
|
|
require.NoError(t, k.Load(f, json.Parser()))
|
|
|
|
// Watch for changes.
|
|
changedC := make(chan error, 1)
|
|
require.NoError(t, f.Watch(func(event any, err error) {
|
|
if err == nil {
|
|
// Reload the config.
|
|
err = k.Load(f, json.Parser())
|
|
}
|
|
|
|
select {
|
|
case changedC <- err:
|
|
default:
|
|
}
|
|
}))
|
|
defer f.Unwatch()
|
|
|
|
// Replace the file atomically so fsnotify cannot report a write while the
|
|
// watched file is truncated but not fully rewritten yet.
|
|
tmpWriteFile := filepath.Join(tmpDir, "koanf_mock.next")
|
|
require.NoError(t, os.WriteFile(tmpWriteFile, []byte(`{"parent": {"name": "name2"}}`), 0600))
|
|
require.NoError(t, os.Rename(tmpWriteFile, tmpFile))
|
|
|
|
select {
|
|
case err := <-changedC:
|
|
require.NoError(t, err)
|
|
assert.Equal("name2", k.String("parent.name"), "file watch reload didn't change config")
|
|
case <-time.After(10 * time.Second):
|
|
assert.Fail("timeout waiting for file watch trigger")
|
|
}
|
|
}
|
|
|
|
func TestWatchFileSymlink(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
tmpDir, _ := os.MkdirTemp("", "koanf_*") // TODO: replace with t.TempDir() as of go v1.15
|
|
|
|
// Create a symlink.
|
|
symPath := filepath.Join(tmpDir, "koanf_test_symlink")
|
|
symPath2 := filepath.Join(tmpDir, "koanf_test_symlink2")
|
|
|
|
wd, err := os.Getwd()
|
|
assert.NoError(err, "error getting working dir")
|
|
|
|
jsonFile := filepath.Join(wd, mockJSON)
|
|
yamlFile := filepath.Join(wd, mockYAML)
|
|
|
|
// Create a symlink to the JSON file which will be swapped out later.
|
|
assert.NoError(os.Symlink(jsonFile, symPath), "error creating symlink")
|
|
assert.NoError(os.Symlink(yamlFile, symPath2), "error creating symlink2")
|
|
|
|
// Load the symlink (to the JSON) file.
|
|
f := file.Provider(symPath)
|
|
k.Load(f, json.Parser())
|
|
|
|
// Watch for changes.
|
|
changedC := make(chan string, 1)
|
|
var wg sync.WaitGroup
|
|
wg.Add(1) // our assurance that cb is called max once
|
|
f.Watch(func(event any, err error) {
|
|
// The File watcher always returns a nil `event`, which can
|
|
// be ignored.
|
|
if err != nil {
|
|
// TODO: make use of Error Wrapping-Scheme and assert.ErrorIs() checks as of go v1.13
|
|
assert.Condition(func() bool {
|
|
return strings.Contains(err.Error(), "no such file or directory")
|
|
}, "received unexpected error. err: %s", err)
|
|
return
|
|
}
|
|
// Reload the config.
|
|
k.Load(f, yaml.Parser())
|
|
changedC <- k.String("type")
|
|
wg.Done()
|
|
})
|
|
|
|
// Wait a second and swap the symlink target from the JSON file to the YAML file.
|
|
// Create a temp symlink to the YAML file and rename the old symlink to the new
|
|
// symlink. We do this to avoid removing the symlink and triggering a REMOVE event.
|
|
time.Sleep(1 * time.Second)
|
|
assert.NoError(os.Rename(symPath2, symPath), "error swapping symlink to another file type")
|
|
if waitTimeout(&wg, time.Second*10) {
|
|
assert.Fail("timeout waiting for file watch trigger")
|
|
} else {
|
|
assert.Condition(func() bool {
|
|
return strings.Compare(<-changedC, "yml") == 0
|
|
}, "symlink watch reload didn't change config")
|
|
}
|
|
}
|
|
|
|
func TestWatchFileDirectorySymlink(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
tmpDir := t.TempDir()
|
|
|
|
baseDir := filepath.Join(tmpDir, "base_dir")
|
|
baseDir2 := filepath.Join(tmpDir, "base_dir2")
|
|
|
|
err := os.Mkdir(baseDir, 0700)
|
|
assert.NoError(err, "error creating base dir")
|
|
|
|
err = os.Mkdir(baseDir2, 0700)
|
|
assert.NoError(err, "error creating base dir 2")
|
|
|
|
wd, err := os.Getwd()
|
|
assert.NoError(err, "error getting working dir")
|
|
|
|
jsonFile := filepath.Join(wd, mockJSON)
|
|
yamlFile := filepath.Join(wd, mockYAML)
|
|
|
|
jsonData, err := os.ReadFile(jsonFile)
|
|
assert.NoError(err, "error reading JSON file")
|
|
|
|
err = os.WriteFile(filepath.Join(baseDir, "config"), jsonData, 0600)
|
|
assert.NoError(err, "error writing JSON file to base dir")
|
|
|
|
yamlData, err := os.ReadFile(yamlFile)
|
|
assert.NoError(err, "error reading YAML file")
|
|
|
|
err = os.WriteFile(filepath.Join(baseDir2, "config"), yamlData, 0600)
|
|
assert.NoError(err, "error writing YAML file to base dir 2")
|
|
|
|
// Create a symlink.
|
|
symDir := filepath.Join(tmpDir, "koanf_test_symlink")
|
|
symDir2 := filepath.Join(tmpDir, "koanf_test_symlink2")
|
|
symPath := filepath.Join(tmpDir, "config")
|
|
|
|
// Create a symlink to the JSON file which will be swapped out later.
|
|
assert.NoError(os.Symlink(baseDir, symDir), "error creating symlink dir")
|
|
assert.NoError(os.Symlink(baseDir2, symDir2), "error creating symlink dir2")
|
|
assert.NoError(os.Symlink(filepath.Join(symDir, "config"), symPath), "error creating symlink")
|
|
|
|
// Load the symlink (to the JSON) file.
|
|
f := file.Provider(symPath)
|
|
k.Load(f, json.Parser())
|
|
|
|
// Watch for changes.
|
|
changedC := make(chan string, 1)
|
|
var wg sync.WaitGroup
|
|
wg.Add(1) // our assurance that cb is called max once
|
|
f.Watch(func(event any, err error) {
|
|
// The File watcher always returns a nil `event`, which can
|
|
// be ignored.
|
|
if err != nil {
|
|
// TODO: make use of Error Wrapping-Scheme and assert.ErrorIs() checks as of go v1.13
|
|
assert.Condition(func() bool {
|
|
return strings.Contains(err.Error(), "no such file or directory")
|
|
}, "received unexpected error. err: %s", err)
|
|
return
|
|
}
|
|
// Reload the config.
|
|
k.Load(f, yaml.Parser())
|
|
changedC <- k.String("type")
|
|
wg.Done()
|
|
})
|
|
|
|
// Wait a second and swap the symlink target from the JSON file to the YAML file.
|
|
// Create a temp symlink to the YAML file and rename the old symlink to the new
|
|
// symlink. We do this to avoid removing the symlink and triggering a REMOVE event.
|
|
time.Sleep(1 * time.Second)
|
|
assert.NoError(os.Rename(symDir2, symDir), "error swapping symlink dir to another symlink dir")
|
|
|
|
if waitTimeout(&wg, time.Second*10) {
|
|
assert.Fail("timeout waiting for file watch trigger")
|
|
} else {
|
|
assert.Condition(func() bool {
|
|
return strings.Compare(<-changedC, "yml") == 0
|
|
}, "symlink watch reload didn't change config")
|
|
}
|
|
}
|
|
|
|
func TestUnwatchFile(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
// Create a tmp config file.
|
|
tmpDir := t.TempDir()
|
|
tmpFile := filepath.Join(tmpDir, "koanf_mock")
|
|
require.NoError(t, os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name1"}}`), 0600))
|
|
|
|
// Load the new config file.
|
|
f := file.Provider(tmpFile)
|
|
k.Load(f, json.Parser())
|
|
|
|
// Watch.
|
|
var reloaded int32
|
|
f.Watch(func(event any, err error) {
|
|
atomic.StoreInt32(&reloaded, 1)
|
|
assert.NoError(err)
|
|
})
|
|
|
|
// Change the file and check whether the watch triggered.
|
|
time.Sleep(100 * time.Millisecond)
|
|
os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name2"}}`), 0600)
|
|
time.Sleep(100 * time.Millisecond)
|
|
assert.True(atomic.LoadInt32(&reloaded) == 1, "watched file didn't reload")
|
|
|
|
// Unwatch the file and verify that the watch didn't trigger.
|
|
assert.NoError(f.Unwatch())
|
|
atomic.StoreInt32(&reloaded, 0)
|
|
time.Sleep(100 * time.Millisecond)
|
|
os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name3"}}`), 0600)
|
|
time.Sleep(100 * time.Millisecond)
|
|
assert.False(atomic.LoadInt32(&reloaded) == 1, "unwatched file reloaded")
|
|
|
|
// Re-watch and check again.
|
|
atomic.StoreInt32(&reloaded, 0)
|
|
f.Watch(func(event any, err error) {
|
|
atomic.StoreInt32(&reloaded, 1)
|
|
assert.NoError(err)
|
|
})
|
|
os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name4"}}`), 0600)
|
|
time.Sleep(100 * time.Millisecond)
|
|
assert.True(atomic.LoadInt32(&reloaded) == 1, "watched file didn't reload")
|
|
|
|
f.Unwatch()
|
|
}
|
|
|
|
func TestLoadMerge(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
// Load several types into a fresh Koanf instance.
|
|
k = koanf.New(delim)
|
|
)
|
|
for _, c := range cases {
|
|
assert.Nil(k.Load(file.Provider(c.file), c.parser),
|
|
fmt.Sprintf("error loading: %v", c.file))
|
|
|
|
// Check against testKeys.
|
|
assert.Equal(testKeys, k.Keys(), fmt.Sprintf("loaded keys don't match in: %v", c.file))
|
|
|
|
// The 'type' fields in different file types have different values.
|
|
// As each subsequent file is loaded, the previous value should be overridden.
|
|
assert.Equal(c.typeName, k.String("type"), "types don't match")
|
|
assert.Equal(c.typeName, k.String("parent1.child1.type"), "types don't match")
|
|
}
|
|
|
|
// Load env provider and override value.
|
|
t.Setenv("PREFIX_PARENT1.CHILD1.TYPE", "env")
|
|
|
|
err := k.Load(env.Provider(".", env.Opt{
|
|
Prefix: "PREFIX_",
|
|
TransformFunc: func(k, v string) (string, any) {
|
|
return strings.ReplaceAll(strings.ToLower(k), "prefix_", ""), v
|
|
},
|
|
}), nil)
|
|
|
|
assert.Nil(err, "error loading env")
|
|
assert.Equal("env", k.String("parent1.child1.type"), "types don't match")
|
|
|
|
// Test the env provider than can mutate the value to upper case
|
|
err = k.Load(env.Provider(".", env.Opt{
|
|
Prefix: "PREFIX_",
|
|
TransformFunc: func(k string, v string) (string, any) {
|
|
return strings.ReplaceAll(strings.ToLower(k), "prefix_", ""), strings.ToUpper(v)
|
|
},
|
|
}), nil)
|
|
|
|
assert.Nil(err, "error loading env with value")
|
|
assert.Equal("ENV", k.String("parent1.child1.type"), "types don't match")
|
|
|
|
// Override with the confmap provider.
|
|
k.Load(confmap.Provider(map[string]any{
|
|
"parent1.child1.type": "confmap",
|
|
"type": "confmap",
|
|
}, "."), nil)
|
|
assert.Equal("confmap", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("confmap", k.String("type"), "types don't match")
|
|
|
|
// Override with the rawbytes provider.
|
|
assert.Nil(k.Load(rawbytes.Provider([]byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`)), json.Parser()),
|
|
"error loading raw bytes")
|
|
assert.Equal("rawbytes", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("rawbytes", k.String("type"), "types don't match")
|
|
}
|
|
|
|
func TestFlags(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
def = koanf.New(delim)
|
|
)
|
|
assert.Nil(def.Load(file.Provider(mockJSON), json.Parser()), "error loading file")
|
|
|
|
// Override with the posflag provider.
|
|
f := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
|
|
|
// Key that exists in the loaded conf. Should overwrite with Set().
|
|
f.String("parent1.child1.type", "flag", "")
|
|
f.Set("parent1.child1.type", "flag")
|
|
f.StringSlice("stringslice", []string{"a", "b", "c"}, "")
|
|
f.IntSlice("intslice", []int{1, 2, 3}, "")
|
|
|
|
// Key that doesn't exist in the loaded file conf. Should merge the default value.
|
|
f.String("flagkey", "flag", "")
|
|
|
|
// Key that exists in the loaded conf but no Set(). Default value shouldn't be merged.
|
|
f.String("parent1.name", "flag", "")
|
|
|
|
// Initialize the provider with the Koanf instance passed where default values
|
|
// will merge if the keys are not present in the conf map.
|
|
{
|
|
k := def.Copy()
|
|
assert.Nil(k.Load(posflag.Provider(f, ".", k), nil), "error loading posflag")
|
|
assert.Equal("flag", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("flag", k.String("flagkey"), "value doesn't match")
|
|
assert.NotEqual("flag", k.String("parent1.name"), "value doesn't match")
|
|
assert.Equal([]string{"a", "b", "c"}, k.Strings("stringslice"), "value doesn't match")
|
|
assert.Equal([]int{1, 2, 3}, k.Ints("intslice"), "value doesn't match")
|
|
}
|
|
|
|
// Test the posflag provider can mutate the value to upper case
|
|
{
|
|
k := def.Copy()
|
|
assert.Nil(k.Load(posflag.ProviderWithValue(f, ".", nil, func(k string, v string) (string, any) {
|
|
return strings.Replace(strings.ToLower(k), "prefix_", "", -1), strings.ToUpper(v)
|
|
}), nil), "error loading posflag")
|
|
assert.Equal("FLAG", k.String("parent1.child1.type"), "types don't match")
|
|
}
|
|
|
|
// Test without passing the Koanf instance where default values will not merge.
|
|
{
|
|
k := def.Copy()
|
|
assert.Nil(k.Load(posflag.Provider(f, ".", nil), nil), "error loading posflag")
|
|
assert.Equal("flag", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("", k.String("flagkey"), "value doesn't match")
|
|
assert.NotEqual("", k.String("parent1.name"), "value doesn't match")
|
|
}
|
|
|
|
// Override with the basicflag provider.
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.type", "flag", "")
|
|
bf.String("parent2.child2.name", "override-default", "")
|
|
bf.Set("parent1.child1.type", "basicflag")
|
|
assert.Nil(k.Load(basicflag.Provider(bf, "."), nil), "error loading basicflag")
|
|
assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("override-default", k.String("parent2.child2.name"), "basicflag default value override failed")
|
|
}
|
|
|
|
// No default-value override behaviour.
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.name", "override-default", "")
|
|
bf.String("parent2.child2.name", "override-default", "")
|
|
bf.Set("parent2.child2.name", "custom")
|
|
assert.Nil(k.Load(basicflag.Provider(bf, ".", &basicflag.Opt{KeyMap: def}), nil), "error loading basicflag")
|
|
assert.Equal("child1", k.String("parent1.child1.name"), "basicflag default overwrote")
|
|
assert.Equal("custom", k.String("parent2.child2.name"), "basicflag set failed")
|
|
}
|
|
|
|
// Override with the basicflag provider.
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.type", "flag", "")
|
|
bf.String("parent2.child2.name", "override-default", "")
|
|
bf.Set("parent1.child1.type", "basicflag")
|
|
assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", nil), nil), "error loading basicflag")
|
|
assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match")
|
|
assert.Equal("override-default", k.String("parent2.child2.name"), "basicflag default value override failed")
|
|
}
|
|
|
|
// No default-value override behaviour.
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.name", "override-default", "")
|
|
bf.String("parent2.child2.name", "override-default", "")
|
|
bf.Set("parent2.child2.name", "custom")
|
|
assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", nil, def), nil), "error loading basicflag")
|
|
assert.Equal("child1", k.String("parent1.child1.name"), "basicflag default overwrote")
|
|
assert.Equal("custom", k.String("parent2.child2.name"), "basicflag set failed")
|
|
}
|
|
|
|
// Override with the basicflag provider.
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.type", "flag", "")
|
|
bf.Set("parent1.child1.type", "basicflag")
|
|
assert.Nil(k.Load(basicflag.Provider(bf, "."), nil), "error loading basicflag")
|
|
assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match")
|
|
}
|
|
|
|
// Test the basicflag provider can mutate the value to upper case
|
|
{
|
|
k := def.Copy()
|
|
bf := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
bf.String("parent1.child1.type", "flag", "")
|
|
bf.Set("parent1.child1.type", "basicflag")
|
|
assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", func(k string, v string) (string, any) {
|
|
return strings.Replace(strings.ToLower(k), "prefix_", "", -1), strings.ToUpper(v)
|
|
}), nil), "error loading basicflag")
|
|
assert.Equal("BASICFLAG", k.String("parent1.child1.type"), "types don't match")
|
|
}
|
|
}
|
|
|
|
func TestConfMapValues(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), "error loading file")
|
|
var (
|
|
c1 = k.All()
|
|
ra1 = k.Raw()
|
|
r1 = k.Cut("parent2").Raw()
|
|
)
|
|
|
|
k = koanf.New(delim)
|
|
assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), "error loading file")
|
|
var (
|
|
c2 = k.All()
|
|
ra2 = k.Raw()
|
|
r2 = k.Cut("parent2").Raw()
|
|
)
|
|
|
|
assert.EqualValues(c1, c2, "conf map mismatch")
|
|
assert.EqualValues(ra1, ra2, "conf map mismatch")
|
|
assert.EqualValues(r1, r2, "conf map mismatch")
|
|
}
|
|
|
|
func TestCutCopy(t *testing.T) {
|
|
// Instance 1.
|
|
var (
|
|
assert = assert.New(t)
|
|
k1 = koanf.New(delim)
|
|
)
|
|
assert.Nil(k1.Load(file.Provider(mockJSON), json.Parser()),
|
|
"error loading file")
|
|
var (
|
|
cp1 = k1.Copy()
|
|
cut1 = k1.Cut("")
|
|
cutp1 = k1.Cut("parent2")
|
|
)
|
|
|
|
// Instance 2.
|
|
k2 := koanf.New(delim)
|
|
assert.Nil(k2.Load(file.Provider(mockJSON), json.Parser()),
|
|
"error loading file")
|
|
var (
|
|
cp2 = k2.Copy()
|
|
cut2 = k2.Cut("")
|
|
cutp2 = k2.Cut("parent2")
|
|
)
|
|
|
|
assert.EqualValues(cp1.All(), cp2.All(), "conf map mismatch")
|
|
assert.EqualValues(cut1.All(), cut2.All(), "conf map mismatch")
|
|
assert.EqualValues(cutp1.All(), cutp2.All(), "conf map mismatch")
|
|
assert.Equal(testParent2, strings.TrimSpace(cutp1.Sprint()), "conf map mismatch")
|
|
assert.Equal(strings.TrimSpace(cutp1.Sprint()), strings.TrimSpace(cutp2.Sprint()), "conf map mismatch")
|
|
|
|
// Cut a single field with no children. Should return empty conf maps.
|
|
assert.Equal(k1.Cut("type").Keys(), k2.Cut("type").Keys(), "single field cut mismatch")
|
|
assert.Equal(k1.Cut("xxxx").Keys(), k2.Cut("xxxx").Keys(), "single field cut mismatch")
|
|
assert.Equal(len(k1.Cut("type").Raw()), 0, "non-map cut returned items")
|
|
}
|
|
|
|
func TestWithMergeFunc(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
assert.NoError(k.Load(rawbytes.Provider([]byte(`{"foo":"bar"}`)), json.Parser()))
|
|
assert.NoError(k.Load(rawbytes.Provider([]byte(`{"baz":"bar"}`)), json.Parser(), koanf.WithMergeFunc(func(a, b map[string]any) error {
|
|
// No merge
|
|
return nil
|
|
})))
|
|
|
|
assert.Equal(map[string]any{
|
|
"foo": "bar",
|
|
}, k.All(), "expects the result of the first load only")
|
|
|
|
err := errors.New("stub")
|
|
assert.ErrorIs(k.Load(rawbytes.Provider([]byte(`{"baz":"bar"}`)), json.Parser(), koanf.WithMergeFunc(func(a, b map[string]any) error {
|
|
return err
|
|
})), err, "expects the error thrown by WithMergeFunc")
|
|
}
|
|
|
|
func TestWithMergeFuncGetNoDeadlock(t *testing.T) {
|
|
k := koanf.New(delim)
|
|
|
|
require.NoError(t, k.Load(rawbytes.Provider([]byte(`{"key":"old"}`)), json.Parser()))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
err := k.Load(rawbytes.Provider([]byte(`{"key":"new"}`)), json.Parser(), koanf.WithMergeFunc(func(src, dest map[string]any) error {
|
|
// Calling a typed getter inside the merge func must not deadlock.
|
|
_ = k.String("key")
|
|
maps.Copy(dest, src)
|
|
return nil
|
|
}))
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("deadlock: merge func calling GetString blocked for 5s")
|
|
}
|
|
|
|
assert.New(t).Equal("new", k.String("key"))
|
|
}
|
|
|
|
func TestMerge(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()),
|
|
"error loading file")
|
|
|
|
// Make two different cuts that'll have different confmaps.
|
|
var (
|
|
cut1 = k.Cut("parent1")
|
|
cut2 = k.Cut("parent2")
|
|
)
|
|
assert.NotEqual(cut1.All(), cut2.All(), "different cuts incorrectly match")
|
|
assert.NotEqual(cut1.Sprint(), cut2.Sprint(), "different cuts incorrectly match")
|
|
|
|
// Create an empty Koanf instance.
|
|
k2 := koanf.New(delim)
|
|
|
|
// Merge cut1 into it and check if they match.
|
|
k2.Merge(cut1)
|
|
assert.Equal(cut1.All(), k2.All(), "conf map mismatch")
|
|
}
|
|
|
|
func TestRaw_YamlTypes(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()),
|
|
"error loading file")
|
|
raw := k.Raw()
|
|
|
|
i, ok := raw["intbools"]
|
|
assert.True(ok, "ints key does not exist in the map")
|
|
|
|
arr, ok := i.([]any)
|
|
assert.True(ok, "arr slice is not array of integers")
|
|
|
|
for _, integer := range arr {
|
|
if _, ok := integer.(int); !ok {
|
|
assert.Failf("failure", "%v not an integer but %T", integer, integer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRaw_JsonTypes(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
|
|
assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()),
|
|
"error loading file")
|
|
raw := k.Raw()
|
|
|
|
i, ok := raw["intbools"]
|
|
assert.True(ok, "ints key does not exist in the map")
|
|
|
|
arr, ok := i.([]any)
|
|
assert.True(ok, "arr slice is not array of integers")
|
|
|
|
for _, integer := range arr {
|
|
if _, ok := integer.(float64); !ok {
|
|
assert.Failf("failure", "%v not an integer but %T", integer, integer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMergeStrictError(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
ks := koanf.NewWithConf(koanf.Conf{
|
|
Delim: delim,
|
|
StrictMerge: true,
|
|
})
|
|
|
|
assert.Nil(ks.Load(confmap.Provider(map[string]any{
|
|
"parent2": map[string]any{
|
|
"child2": map[string]any{
|
|
"grandchild2": map[string]any{
|
|
"ids": 123,
|
|
},
|
|
},
|
|
},
|
|
}, delim), nil))
|
|
|
|
err := ks.Load(file.Provider(mockYAML), yaml.Parser())
|
|
assert.Error(err)
|
|
assert.True(strings.HasPrefix(err.Error(), "incorrect types at key parent2.child2.grandchild2"))
|
|
}
|
|
|
|
func TestMergeAt(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()),
|
|
"error loading file")
|
|
|
|
// Get expected koanf, and root data
|
|
var (
|
|
expected = k.Cut("parent2")
|
|
rootData = map[string]any{
|
|
"name": k.String("parent2.name"),
|
|
"id": k.Int("parent2.id"),
|
|
}
|
|
)
|
|
|
|
// Get nested test data to merge at path
|
|
child2 := koanf.New(delim)
|
|
assert.Nil(child2.Load(confmap.Provider(map[string]any{
|
|
"name": k.String("parent2.child2.name"),
|
|
"empty": k.Get("parent2.child2.empty"),
|
|
}, delim), nil))
|
|
grandChild := k.Cut("parent2.child2.grandchild2")
|
|
|
|
// Create test koanf
|
|
ordered := koanf.New(delim)
|
|
assert.Nil(ordered.Load(confmap.Provider(rootData, delim), nil))
|
|
|
|
// Merge at path in order, first child2, then child2.grandchild2
|
|
ordered.MergeAt(child2, "child2")
|
|
ordered.MergeAt(grandChild, "child2.grandchild2")
|
|
assert.Equal(expected.Get(""), ordered.Get(""), "conf map mismatch")
|
|
|
|
// Create test koanf
|
|
reversed := koanf.New(delim)
|
|
assert.Nil(reversed.Load(confmap.Provider(rootData, delim), nil))
|
|
|
|
// Merge at path in reverse order, first child2.grandchild2, then child2
|
|
reversed.MergeAt(grandChild, "child2.grandchild2")
|
|
reversed.MergeAt(child2, "child2")
|
|
assert.Equal(expected.Get(""), reversed.Get(""), "conf map mismatch")
|
|
}
|
|
|
|
func TestSet(t *testing.T) {
|
|
var (
|
|
assert = assert.New(t)
|
|
k = koanf.New(delim)
|
|
)
|
|
assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()),
|
|
"error loading file")
|
|
|
|
assert.Nil(k.Set("parent1.name", "new"))
|
|
assert.Equal(k.String("parent1.name"), "new")
|
|
|
|
assert.Nil(k.Set("parent1.child1.name", 123))
|
|
assert.Equal(k.Int("parent1.child1.name"), 123)
|
|
|
|
assert.Nil(k.Set("parent1.child1.grandchild1.ids", []int{5}))
|
|
assert.Equal(k.Ints("parent1.child1.grandchild1.ids"), []int{5})
|
|
|
|
assert.Nil(k.Set("parent1.child1.grandchild1", 123))
|
|
assert.Equal(k.Int("parent1.child1.grandchild1"), 123)
|
|
assert.Equal(k.Int("parent1.child1.grandchild1.ids"), 0)
|
|
|
|
assert.Nil(k.Set("parent1.child1.grandchild1", map[string]any{"name": "new"}))
|
|
assert.Equal(k.Get("parent1.child1.grandchild1"), map[string]any{"name": "new"})
|
|
}
|
|
|
|
func TestUnmarshal(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
// Expected unmarshalled structure.
|
|
real := testStruct{
|
|
Type: "json",
|
|
Empty: make(map[string]string),
|
|
Parent1: parentStruct{
|
|
Name: "parent1",
|
|
ID: 1234,
|
|
Child1: childStruct{
|
|
Name: "child1",
|
|
Type: "json",
|
|
Empty: make(map[string]string),
|
|
Grandchild1: grandchildStruct{
|
|
Ids: []int{1, 2, 3},
|
|
On: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Unmarshal and check all parsers.
|
|
for _, c := range cases {
|
|
var (
|
|
k = koanf.New(delim)
|
|
ts testStruct
|
|
)
|
|
assert.Nil(k.Load(file.Provider(c.file), c.parser),
|
|
fmt.Sprintf("error loading: %v", c.file))
|
|
assert.Nil(k.Unmarshal("", &ts), "unmarshal failed")
|
|
real.Type = c.typeName
|
|
real.Parent1.Child1.Type = c.typeName
|
|
assert.Equal(real, ts, "unmarshalled structs don't match")
|
|
|
|
// Unmarshal with config.
|
|
ts = testStruct{}
|
|
assert.Nil(k.UnmarshalWithConf("", &ts, koanf.UnmarshalConf{Tag: "koanf"}), "unmarshal failed")
|
|
real.Type = c.typeName
|
|
real.Parent1.Child1.Type = c.typeName
|
|
assert.Equal(real, ts, "unmarshalled structs don't match")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalFlat(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
// Expected unmarshalled structure.
|
|
real := testStructFlat{
|
|
Type: "json",
|
|
Empty: make(map[string]string),
|
|
Parent1Name: "parent1",
|
|
Parent1ID: 1234,
|
|
Parent1Child1Name: "child1",
|
|
Parent1Child1Type: "json",
|
|
Parent1Child1Empty: make(map[string]string),
|
|
Parent1Child1Grandchild1IDs: []int{1, 2, 3},
|
|
Parent1Child1Grandchild1On: true,
|
|
}
|
|
|
|
// Unmarshal and check all parsers.
|
|
for _, c := range cases {
|
|
k := koanf.New(delim)
|
|
assert.Nil(k.Load(file.Provider(c.file), c.parser),
|
|
fmt.Sprintf("error loading: %v", c.file))
|
|
ts := testStructFlat{}
|
|
assert.Nil(k.UnmarshalWithConf("", &ts, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}), "unmarshal failed")
|
|
real.Type = c.typeName
|
|
real.Parent1Child1Type = c.typeName
|
|
assert.Equal(real, ts, "unmarshalled structs don't match")
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalCustomText(t *testing.T) {
|
|
test := struct {
|
|
V1 customText `koanf:"v1"`
|
|
V2 string `koanf:"v2"`
|
|
V3 customText `koanf:"v3"`
|
|
}{}
|
|
|
|
// Test custom unmarshalling of strings via mapstructure's UnmarshalText()
|
|
// methods. customText is an int type that strips of the `mb` suffix and parses
|
|
// the rest into a number.
|
|
|
|
k := koanf.New(delim)
|
|
err := k.Load(rawbytes.Provider([]byte(`{"v1": "42mb", "v2": "42mb"}`)), json.Parser())
|
|
assert.NoError(t, err)
|
|
|
|
k.Unmarshal("", &test)
|
|
assert.Equal(t, int(test.V1), 42)
|
|
assert.Equal(t, test.V2, "42mb")
|
|
assert.Equal(t, int(test.V3), 0)
|
|
}
|
|
|
|
func TestMarshal(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
for _, c := range cases {
|
|
// HCL does not support marshalling.
|
|
if c.typeName == "hcl" {
|
|
continue
|
|
}
|
|
|
|
// Load config.
|
|
k := koanf.New(delim)
|
|
assert.NoError(k.Load(file.Provider(c.file), c.parser),
|
|
fmt.Sprintf("error loading: %v", c.file))
|
|
|
|
// Serialize / marshal into raw bytes using the parser.
|
|
b, err := k.Marshal(c.parser)
|
|
assert.NoError(err, "error marshalling")
|
|
|
|
// Reload raw serialize bytes into a new koanf instance.
|
|
k = koanf.New(delim)
|
|
assert.NoError(k.Load(rawbytes.Provider(b), c.parser),
|
|
fmt.Sprintf("error loading: %v", c.file))
|
|
|
|
// Check if values are intact.
|
|
assert.Equal(float64(1234), c.koanf.MustFloat64("parent1.id"))
|
|
assert.Equal([]string{"red", "blue", "orange"}, c.koanf.MustStrings("orphan"))
|
|
assert.Equal([]int64{1, 2, 3}, c.koanf.MustInt64s("parent1.child1.grandchild1.ids"))
|
|
}
|
|
}
|
|
|
|
func TestGetExists(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
type exCase struct {
|
|
path string
|
|
exists bool
|
|
}
|
|
exCases := []exCase{
|
|
{"xxxxx", false},
|
|
{"parent1.child2", false},
|
|
{"child1", false},
|
|
{"child2", false},
|
|
{"type", true},
|
|
{"parent1", true},
|
|
{"parent2", true},
|
|
{"parent1.name", true},
|
|
{"parent2.name", true},
|
|
{"parent1.child1", true},
|
|
{"parent2.child2", true},
|
|
{"parent1.child1.grandchild1", true},
|
|
{"parent1.child1.grandchild1.on", true},
|
|
{"parent2.child2.grandchild2.on", true},
|
|
}
|
|
for _, c := range exCases {
|
|
assert.Equal(c.exists, cases[0].koanf.Exists(c.path),
|
|
fmt.Sprintf("path resolution failed: %s", c.path))
|
|
assert.Equal(c.exists, cases[0].koanf.Get(c.path) != nil,
|
|
fmt.Sprintf("path resolution failed: %s", c.path))
|
|
}
|
|
}
|
|
|
|
func TestSlices(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
// Load a slice of confmaps [{}, {}].
|
|
var mp map[string]any
|
|
err := encjson.Unmarshal([]byte(`{
|
|
"parent": [
|
|
{"value": 1, "sub": {"value": "1"}},
|
|
{"value": 2, "sub": {"value": "2"}}
|
|
],
|
|
"another": "123"
|
|
}`), &mp)
|
|
assert.NoError(err, "error marshalling test payload")
|
|
k := koanf.New(delim)
|
|
assert.NoError(k.Load(confmap.Provider(mp, "."), nil))
|
|
|
|
assert.Empty(k.Slices("x"))
|
|
assert.Empty(k.Slices("parent.value"))
|
|
assert.Empty(k.Slices("parent.value.sub"))
|
|
|
|
slices := k.Slices("parent")
|
|
assert.NotNil(slices, "got nil slice of confmap")
|
|
assert.NotEmpty(slices, "got empty confmap slice")
|
|
|
|
for i, s := range slices {
|
|
assert.Equal(s.Int("value"), i+1)
|
|
assert.Equal(s.String("sub.value"), fmt.Sprintf("%d", i+1))
|
|
}
|
|
}
|
|
|
|
func TestGetTypes(t *testing.T) {
|
|
assert := assert.New(t)
|
|
for _, c := range cases {
|
|
assert.Equal(nil, c.koanf.Get("xxx"))
|
|
assert.Equal(make(map[string]any), c.koanf.Get("empty"))
|
|
|
|
// Int.
|
|
assert.Equal(int64(0), c.koanf.Int64("xxxx"))
|
|
assert.Equal(int64(1234), c.koanf.Int64("parent1.id"))
|
|
|
|
assert.Equal(int(0), c.koanf.Int("xxxx"))
|
|
assert.Equal(int(1234), c.koanf.Int("parent1.id"))
|
|
|
|
assert.Equal([]int64{}, c.koanf.Int64s("xxxx"))
|
|
assert.Equal([]int64{1, 2, 3}, c.koanf.Int64s("parent1.child1.grandchild1.ids"))
|
|
|
|
assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.intmap"))
|
|
assert.Equal(map[string]int64{}, c.koanf.Int64Map("parent1.boolmap"))
|
|
assert.Equal(map[string]int64{}, c.koanf.Int64Map("xxxx"))
|
|
assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.floatmap"))
|
|
|
|
assert.Equal([]int{1, 2, 3}, c.koanf.Ints("parent1.child1.grandchild1.ids"))
|
|
assert.Equal([]int{}, c.koanf.Ints("xxxx"))
|
|
|
|
assert.Equal(map[string]int{"key1": 1, "key2": 1, "key3": 1}, c.koanf.IntMap("parent1.intmap"))
|
|
assert.Equal(map[string]int{}, c.koanf.IntMap("parent1.boolmap"))
|
|
assert.Equal(map[string]int{}, c.koanf.IntMap("xxxx"))
|
|
|
|
// Float.
|
|
assert.Equal(float64(0), c.koanf.Float64("xxx"))
|
|
assert.Equal(float64(1234), c.koanf.Float64("parent1.id"))
|
|
|
|
assert.Equal([]float64{}, c.koanf.Float64s("xxxx"))
|
|
assert.Equal([]float64{1, 2, 3}, c.koanf.Float64s("parent1.child1.grandchild1.ids"))
|
|
|
|
assert.Equal(map[string]float64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Float64Map("parent1.intmap"))
|
|
assert.Equal(map[string]float64{"key1": 1.1, "key2": 1.2, "key3": 1.3}, c.koanf.Float64Map("parent1.floatmap"))
|
|
assert.Equal(map[string]float64{}, c.koanf.Float64Map("parent1.boolmap"))
|
|
assert.Equal(map[string]float64{}, c.koanf.Float64Map("xxxx"))
|
|
|
|
// String and bytes.
|
|
assert.Equal([]byte{}, c.koanf.Bytes("xxxx"))
|
|
assert.Equal([]byte("parent1"), c.koanf.Bytes("parent1.name"))
|
|
|
|
assert.Equal("", c.koanf.String("xxxx"))
|
|
assert.Equal("parent1", c.koanf.String("parent1.name"))
|
|
|
|
assert.Equal([]string{}, c.koanf.Strings("xxxx"))
|
|
assert.Equal([]string{"red", "blue", "orange"}, c.koanf.Strings("orphan"))
|
|
|
|
assert.Equal(map[string]string{"key1": "val1", "key2": "val2", "key3": "val3"}, c.koanf.StringMap("parent1.strmap"))
|
|
assert.Equal(map[string][]string{"key1": {"val1", "val2", "val3"}, "key2": {"val4", "val5"}}, c.koanf.StringsMap("parent1.strsmap"))
|
|
assert.Equal(map[string]string{}, c.koanf.StringMap("xxxx"))
|
|
assert.Equal(map[string]string{}, c.koanf.StringMap("parent1.intmap"))
|
|
|
|
// Bools.
|
|
assert.Equal(false, c.koanf.Bool("xxxx"))
|
|
assert.Equal(false, c.koanf.Bool("type"))
|
|
assert.Equal(true, c.koanf.Bool("parent1.child1.grandchild1.on"))
|
|
assert.Equal(true, c.koanf.Bool("strbool"))
|
|
|
|
assert.Equal([]bool{}, c.koanf.Bools("xxxx"))
|
|
assert.Equal([]bool{true, false, true}, c.koanf.Bools("bools"))
|
|
assert.Equal([]bool{true, false, true}, c.koanf.Bools("intbools"))
|
|
assert.Equal([]bool{true, true, false}, c.koanf.Bools("strbools"))
|
|
|
|
assert.Equal(map[string]bool{"ok1": true, "ok2": true, "notok3": false}, c.koanf.BoolMap("parent1.boolmap"))
|
|
assert.Equal(map[string]bool{"key1": true, "key2": true, "key3": true}, c.koanf.BoolMap("parent1.intmap"))
|
|
assert.Equal(map[string]bool{}, c.koanf.BoolMap("xxxx"))
|
|
|
|
// Others.
|
|
assert.Equal(time.Duration(1234), c.koanf.Duration("parent1.id"))
|
|
assert.Equal(time.Duration(0), c.koanf.Duration("xxxx"))
|
|
assert.Equal(time.Second*3, c.koanf.Duration("duration"))
|
|
|
|
assert.Equal(time.Time{}, c.koanf.Time("xxxx", "2006-01-02"))
|
|
assert.Equal(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), c.koanf.Time("time", "2006-01-02"))
|
|
|
|
assert.Equal([]string{}, c.koanf.MapKeys("xxxx"), "map keys mismatch")
|
|
assert.Equal([]string{"bools", "duration", "empty", "intbools", "negative_int", "orphan", "parent1", "parent2", "strbool", "strbools", "time", "type"},
|
|
c.koanf.MapKeys(""), "map keys mismatch")
|
|
assert.Equal([]string{"key1", "key2", "key3"}, c.koanf.MapKeys("parent1.strmap"), "map keys mismatch")
|
|
|
|
// Attempt to parse int=1234 as a Unix timestamp.
|
|
assert.Equal(time.Date(1970, 1, 1, 0, 20, 34, 0, time.UTC), c.koanf.Time("parent1.id", "").UTC())
|
|
}
|
|
}
|
|
|
|
func TestMustGetTypes(t *testing.T) {
|
|
assert := assert.New(t)
|
|
for _, c := range cases {
|
|
// Int.
|
|
assert.Panics(func() { c.koanf.MustInt64("xxxx") })
|
|
assert.Equal(int64(1234), c.koanf.MustInt64("parent1.id"))
|
|
assert.Equal(int64(-1234), c.koanf.MustInt64("negative_int"))
|
|
|
|
assert.Panics(func() { c.koanf.MustInt("xxxx") })
|
|
assert.Equal(int(1234), c.koanf.MustInt("parent1.id"))
|
|
assert.Equal(int(-1234), c.koanf.MustInt("negative_int"))
|
|
|
|
assert.Panics(func() { c.koanf.MustInt64s("xxxx") })
|
|
assert.Equal([]int64{1, 2, 3}, c.koanf.MustInt64s("parent1.child1.grandchild1.ids"))
|
|
|
|
assert.Panics(func() { c.koanf.MustInt64Map("xxxx") })
|
|
assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustInt64Map("parent1.intmap"))
|
|
|
|
assert.Panics(func() { c.koanf.MustInt64Map("parent1.boolmap") })
|
|
assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustInt64Map("parent1.floatmap"))
|
|
|
|
assert.Panics(func() { c.koanf.MustInts("xxxx") })
|
|
assert.Equal([]int{1, 2, 3}, c.koanf.MustInts("parent1.child1.grandchild1.ids"))
|
|
|
|
assert.Panics(func() { c.koanf.MustIntMap("xxxx") })
|
|
assert.Panics(func() { c.koanf.MustIntMap("parent1.boolmap") })
|
|
assert.Equal(map[string]int{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustIntMap("parent1.intmap"))
|
|
|
|
// Float.
|
|
assert.Panics(func() { c.koanf.MustInts("xxxx") })
|
|
assert.Equal(float64(1234), c.koanf.MustFloat64("parent1.id"))
|
|
|
|
assert.Panics(func() { c.koanf.MustFloat64s("xxxx") })
|
|
assert.Equal([]float64{1, 2, 3}, c.koanf.MustFloat64s("parent1.child1.grandchild1.ids"))
|
|
|
|
assert.Panics(func() { c.koanf.MustFloat64Map("xxxx") })
|
|
assert.Panics(func() { c.koanf.MustFloat64Map("parent1.boolmap") })
|
|
assert.Equal(map[string]float64{"key1": 1.1, "key2": 1.2, "key3": 1.3}, c.koanf.MustFloat64Map("parent1.floatmap"))
|
|
assert.Equal(map[string]float64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustFloat64Map("parent1.intmap"))
|
|
|
|
// String and bytes.
|
|
assert.Panics(func() { c.koanf.MustBytes("xxxx") })
|
|
assert.Equal([]byte("parent1"), c.koanf.MustBytes("parent1.name"))
|
|
|
|
assert.Panics(func() { c.koanf.MustString("xxxx") })
|
|
assert.Equal("parent1", c.koanf.MustString("parent1.name"))
|
|
|
|
assert.Panics(func() { c.koanf.MustStrings("xxxx") })
|
|
assert.Equal([]string{"red", "blue", "orange"}, c.koanf.MustStrings("orphan"))
|
|
|
|
assert.Panics(func() { c.koanf.MustStringMap("xxxx") })
|
|
assert.Panics(func() { c.koanf.MustStringMap("parent1.intmap") })
|
|
assert.Equal(map[string]string{"key1": "val1", "key2": "val2", "key3": "val3"}, c.koanf.MustStringMap("parent1.strmap"))
|
|
assert.Equal(map[string][]string{"key1": {"val1", "val2", "val3"}, "key2": {"val4", "val5"}}, c.koanf.MustStringsMap("parent1.strsmap"))
|
|
|
|
// // Bools.
|
|
assert.Panics(func() { c.koanf.MustBools("xxxx") })
|
|
assert.Equal([]bool{true, false, true}, c.koanf.MustBools("bools"))
|
|
assert.Equal([]bool{true, false, true}, c.koanf.MustBools("intbools"))
|
|
assert.Equal([]bool{true, true, false}, c.koanf.MustBools("strbools"))
|
|
|
|
assert.Panics(func() { c.koanf.MustBoolMap("xxxx") })
|
|
assert.Equal(map[string]bool{"ok1": true, "ok2": true, "notok3": false}, c.koanf.MustBoolMap("parent1.boolmap"))
|
|
assert.Equal(map[string]bool{"key1": true, "key2": true, "key3": true}, c.koanf.MustBoolMap("parent1.intmap"))
|
|
|
|
// Others.
|
|
assert.Panics(func() { c.koanf.MustDuration("xxxx") })
|
|
assert.Equal(time.Duration(1234), c.koanf.MustDuration("parent1.id"))
|
|
assert.Equal(time.Second*3, c.koanf.MustDuration("duration"))
|
|
assert.Equal(time.Duration(-1234), c.koanf.MustDuration("negative_int"))
|
|
|
|
assert.Panics(func() { c.koanf.MustTime("xxxx", "2006-01-02") })
|
|
assert.Equal(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), c.koanf.MustTime("time", "2006-01-02"))
|
|
|
|
// // Attempt to parse int=1234 as a Unix timestamp.
|
|
assert.Panics(func() { c.koanf.MustTime("time", "2006") })
|
|
assert.Equal(time.Date(1970, 1, 1, 0, 20, 34, 0, time.UTC), c.koanf.MustTime("parent1.id", "").UTC())
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
assert := assert.New(t)
|
|
for _, c := range cases {
|
|
c.koanf.Delete("parent2.child2.grandchild2")
|
|
assert.Equal(false, c.koanf.Exists("parent2.child2.grandchild2.on"))
|
|
assert.Equal(false, c.koanf.Exists("parent2.child2.grandchild2.ids.5"))
|
|
assert.Equal(true, c.koanf.Exists("parent2.child2.name"))
|
|
|
|
c.koanf.Delete("")
|
|
assert.Equal(false, c.koanf.Exists("duration"))
|
|
assert.Equal(false, c.koanf.Exists("empty"))
|
|
}
|
|
}
|
|
|
|
func TestGetStringsMap(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
k := koanf.New(delim)
|
|
|
|
k.Load(confmap.Provider(map[string]any{
|
|
"str": map[string]string{
|
|
"k1": "value",
|
|
},
|
|
"strs": map[string][]string{
|
|
"k1": {"value"},
|
|
},
|
|
"iface": map[string]any{
|
|
"k2": "value",
|
|
},
|
|
"ifaces": map[string][]any{
|
|
"k2": {"value"},
|
|
},
|
|
"ifaces2": map[string]any{
|
|
"k2": []any{"value"},
|
|
},
|
|
"ifaces3": map[string]any{
|
|
"k2": []string{"value"},
|
|
},
|
|
}, "."), nil)
|
|
assert.Equal(map[string]string{"k1": "value"}, k.StringMap("str"), "types don't match")
|
|
assert.Equal(map[string]string{"k2": "value"}, k.StringMap("iface"), "types don't match")
|
|
assert.Equal(map[string][]string{"k1": {"value"}}, k.StringsMap("strs"), "types don't match")
|
|
assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces"), "types don't match")
|
|
assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces2"), "types don't match")
|
|
assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces3"), "types don't match")
|
|
}
|
|
|
|
func TestBoolsNativeSlice(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
k := koanf.New(delim)
|
|
assert.NoError(k.Load(confmap.Provider(map[string]any{
|
|
"bools": []bool{true, false, true},
|
|
}, "."), nil))
|
|
|
|
assert.Equal([]bool{true, false, true}, k.Bools("bools"))
|
|
}
|
|
|
|
func TestMapKeysNativeMap(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
k := koanf.New(delim)
|
|
assert.NoError(k.Load(confmap.Provider(map[string]any{
|
|
"ints": map[string]int64{"a": 1, "b": 2},
|
|
}, "."), nil))
|
|
|
|
assert.Equal([]string{"a", "b"}, k.MapKeys("ints"), "map keys mismatch for a natively-typed map")
|
|
}
|
|
|
|
// waitTimeout waits for the waitgroup for the specified max timeout.
|
|
// Returns true if waiting timed out.
|
|
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
|
c := make(chan struct{})
|
|
go func() {
|
|
defer close(c)
|
|
wg.Wait()
|
|
}()
|
|
select {
|
|
case <-c:
|
|
return false // completed normally
|
|
case <-time.After(timeout):
|
|
return true // timed out
|
|
}
|
|
}
|
|
|
|
// TestFileWatcherRaceCondition reproduces Issue #305
|
|
// File watcher reloading config while reader goroutine accesses values
|
|
// This test verifies our thread safety fix prevents empty string reads
|
|
func TestFileWatcherRaceCondition(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping race condition test in short mode")
|
|
}
|
|
|
|
// Create temp config file
|
|
tmpDir, err := os.MkdirTemp("", "koanf_race_*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
tmpFile := filepath.Join(tmpDir, "config.yaml")
|
|
writeConfig := func(value string) {
|
|
content := fmt.Sprintf("rpc: %q\n", value)
|
|
os.WriteFile(tmpFile, []byte(content), 0600)
|
|
}
|
|
|
|
// Initial config
|
|
writeConfig("initial")
|
|
|
|
k := koanf.New(".")
|
|
provider := file.Provider(tmpFile)
|
|
err = k.Load(provider, yaml.Parser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Setup file watcher that reloads the SAME koanf instance
|
|
// This tests our internal thread safety, not user pattern issues
|
|
var reloadCount int64
|
|
provider.Watch(func(event any, err error) {
|
|
if err != nil {
|
|
t.Logf("watch error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Reload into the same koanf instance - this tests our thread safety
|
|
err = k.Load(provider, yaml.Parser())
|
|
if err != nil {
|
|
t.Logf("reload error: %v", err)
|
|
}
|
|
// Use atomic to avoid race in test counters
|
|
atomic.AddInt64(&reloadCount, 1)
|
|
})
|
|
|
|
// Start reader goroutine that continuously reads the config
|
|
done := make(chan struct{})
|
|
var emptyCount, totalReads int64
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
value := k.String("rpc")
|
|
reads := atomic.AddInt64(&totalReads, 1)
|
|
if value == "" {
|
|
atomic.AddInt64(&emptyCount, 1)
|
|
// With proper thread safety, this should never happen
|
|
t.Errorf("Got empty string on read #%d", reads)
|
|
}
|
|
time.Sleep(time.Microsecond) // Small delay to allow interleaving
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Trigger multiple file changes to increase chance of race
|
|
for i := 0; i < 5; i++ {
|
|
time.Sleep(20 * time.Millisecond)
|
|
writeConfig(fmt.Sprintf("value-%d", i))
|
|
}
|
|
|
|
// Wait for file watching to settle
|
|
time.Sleep(100 * time.Millisecond)
|
|
close(done)
|
|
|
|
finalReads := atomic.LoadInt64(&totalReads)
|
|
finalEmpties := atomic.LoadInt64(&emptyCount)
|
|
finalReloads := atomic.LoadInt64(&reloadCount)
|
|
|
|
t.Logf("Total reads: %d, Empty reads: %d, Reloads: %d", finalReads, finalEmpties, finalReloads)
|
|
if finalEmpties > 0 {
|
|
t.Errorf("Thread safety issue: got %d empty reads out of %d total reads", finalEmpties, finalReads)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentLoadRaceCondition reproduces Issue #335
|
|
// Multiple goroutines calling k.Load() simultaneously
|
|
// This test should fail with "concurrent map writes" panic
|
|
func TestConcurrentLoadRaceCondition(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping race condition test in short mode")
|
|
}
|
|
|
|
k := koanf.New(".")
|
|
|
|
// Number of concurrent goroutines
|
|
numGoroutines := 10
|
|
numLoadsPerGoroutine := 10
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Channel to collect any panics
|
|
panics := make(chan any, numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
panics <- r
|
|
}
|
|
}()
|
|
|
|
for j := 0; j < numLoadsPerGoroutine; j++ {
|
|
// Create different configs to load
|
|
config := map[string]any{
|
|
fmt.Sprintf("key_%d_%d", id, j): fmt.Sprintf("value_%d_%d", id, j),
|
|
"common": fmt.Sprintf("common_%d_%d", id, j),
|
|
}
|
|
|
|
// This should trigger concurrent map writes
|
|
err := k.Load(confmap.Provider(config, "."), nil)
|
|
if err != nil {
|
|
t.Errorf("Load failed: %v", err)
|
|
}
|
|
|
|
// Small delay to increase chance of race
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(panics)
|
|
|
|
// Check if we got any panics (which indicates the race condition)
|
|
panicCount := 0
|
|
for p := range panics {
|
|
panicCount++
|
|
t.Logf("Concurrent Load panic: %v", p)
|
|
}
|
|
|
|
if panicCount > 0 {
|
|
t.Errorf("Race condition detected: got %d panics from concurrent Load operations", panicCount)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentReadWriteMix tests mixed concurrent reads and writes
|
|
// This should expose various race conditions with inconsistent reads
|
|
func TestConcurrentReadWriteMix(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping race condition test in short mode")
|
|
}
|
|
|
|
k := koanf.New(".")
|
|
|
|
// Initialize with some data
|
|
k.Load(confmap.Provider(map[string]any{
|
|
"test.key": "initial",
|
|
"counter": 0,
|
|
}, "."), nil)
|
|
|
|
done := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
|
|
// Start multiple reader goroutines
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(readerID int) {
|
|
defer wg.Done()
|
|
readCount := 0
|
|
for {
|
|
select {
|
|
case <-done:
|
|
t.Logf("Reader %d: performed %d reads", readerID, readCount)
|
|
return
|
|
default:
|
|
// Mix different types of reads
|
|
_ = k.String("test.key")
|
|
_ = k.Int("counter")
|
|
_ = k.Keys()
|
|
_ = k.All()
|
|
readCount++
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Start multiple writer goroutines
|
|
for i := 0; i < 3; i++ {
|
|
wg.Add(1)
|
|
go func(writerID int) {
|
|
defer wg.Done()
|
|
writeCount := 0
|
|
for {
|
|
select {
|
|
case <-done:
|
|
t.Logf("Writer %d: performed %d writes", writerID, writeCount)
|
|
return
|
|
default:
|
|
// Mix different types of writes
|
|
config := map[string]any{
|
|
"test.key": fmt.Sprintf("writer-%d-count-%d", writerID, writeCount),
|
|
"counter": writeCount,
|
|
fmt.Sprintf("dynamic.key.%d", writeCount): writerID,
|
|
}
|
|
k.Load(confmap.Provider(config, "."), nil)
|
|
writeCount++
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Let the race run for a short time
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(done)
|
|
wg.Wait()
|
|
|
|
t.Log("Concurrent read/write test completed - check for race detector warnings")
|
|
}
|
|
|
|
// TestConcurrentEdgeCases tests concurrent access to edge case methods
|
|
func TestConcurrentEdgeCases(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping race condition test in short mode")
|
|
}
|
|
|
|
k := koanf.New(".")
|
|
k.Load(confmap.Provider(map[string]any{
|
|
"parent": map[string]any{
|
|
"child": "value",
|
|
},
|
|
"list": []any{"a", "b", "c"},
|
|
}, "."), nil)
|
|
|
|
done := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
|
|
// Test concurrent Cut operations
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
_ = k.Cut("parent")
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Test concurrent Copy operations
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
_ = k.Copy()
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Test concurrent MapKeys access
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
_ = k.MapKeys("parent")
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Test concurrent modifications
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
count := 0
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
k.Set(fmt.Sprintf("dynamic.%d", count), count)
|
|
count++
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
time.Sleep(30 * time.Millisecond)
|
|
close(done)
|
|
wg.Wait()
|
|
|
|
t.Log("Concurrent edge cases test completed - check for race detector warnings")
|
|
}
|
|
|
|
// TestNoDeadlock verifies that our locking patterns don't cause deadlocks
|
|
// Tests concurrent reads and writes with various method combinations
|
|
func TestNoDeadlock(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping deadlock test in short mode")
|
|
}
|
|
|
|
k := koanf.New(".")
|
|
k.Load(confmap.Provider(map[string]any{
|
|
"parent": map[string]any{
|
|
"child": "value",
|
|
"count": 42,
|
|
},
|
|
"list": []any{"a", "b", "c"},
|
|
}, "."), nil)
|
|
|
|
// Test duration
|
|
testDuration := 500 * time.Millisecond
|
|
|
|
var wg sync.WaitGroup
|
|
done := make(chan struct{})
|
|
|
|
// Start multiple reader goroutines with different read patterns
|
|
for i := 0; i < 3; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
readCount := 0
|
|
for {
|
|
select {
|
|
case <-done:
|
|
t.Logf("Reader %d completed %d operations", id, readCount)
|
|
return
|
|
default:
|
|
// Mix different read operations
|
|
_ = k.Get("parent.child")
|
|
_ = k.Keys()
|
|
_ = k.All()
|
|
_ = k.Raw()
|
|
_ = k.Exists("parent")
|
|
_ = k.Sprint() // This internally uses RLock and avoids calling Keys()
|
|
_ = k.KeyMap()
|
|
_ = k.MapKeys("parent")
|
|
|
|
// Test methods that call other locked methods
|
|
_ = k.Cut("parent") // Calls Get()
|
|
_ = k.Copy() // Calls Cut() → Get()
|
|
|
|
readCount++
|
|
if readCount%100 == 0 {
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Start writer goroutines with different write patterns
|
|
for i := 0; i < 2; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
writeCount := 0
|
|
for {
|
|
select {
|
|
case <-done:
|
|
t.Logf("Writer %d completed %d operations", id, writeCount)
|
|
return
|
|
default:
|
|
// Mix different write operations
|
|
k.Set(fmt.Sprintf("writer_%d.key", id), writeCount)
|
|
k.Delete("nonexistent") // Should be safe
|
|
|
|
// Load new data
|
|
newData := map[string]any{
|
|
fmt.Sprintf("load_%d", writeCount): id,
|
|
"nested": map[string]any{
|
|
"value": writeCount,
|
|
},
|
|
}
|
|
k.Load(confmap.Provider(newData, "."), nil)
|
|
|
|
// Merge operations
|
|
other := koanf.New(".")
|
|
other.Set("merge_key", writeCount)
|
|
k.Merge(other)
|
|
|
|
writeCount++
|
|
if writeCount%50 == 0 {
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Run the test for specified duration
|
|
time.Sleep(testDuration)
|
|
close(done)
|
|
|
|
// Use a timeout to detect deadlocks
|
|
waitChan := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(waitChan)
|
|
}()
|
|
|
|
select {
|
|
case <-waitChan:
|
|
t.Log("Deadlock test completed successfully - no deadlocks detected")
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("DEADLOCK DETECTED: Goroutines did not complete within timeout")
|
|
}
|
|
}
|
|
|
|
// TestFileProviderConcurrency specifically tests the file provider's synchronization
|
|
// under heavy concurrent load to catch any races in Watch/Unwatch
|
|
func TestFileProviderConcurrency(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping file provider concurrency test in short mode")
|
|
}
|
|
|
|
// Create temp config file
|
|
tmpDir := t.TempDir()
|
|
tmpFile := filepath.Join(tmpDir, "config.json")
|
|
initialData := []byte(`{"test": "value"}`)
|
|
err := os.WriteFile(tmpFile, initialData, 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
done := make(chan struct{})
|
|
|
|
// Stress test: multiple goroutines rapidly watch/unwatch the same file
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
// Create new file provider for each iteration
|
|
f := file.Provider(tmpFile)
|
|
|
|
// Try to watch
|
|
var watchErr error
|
|
watchErr = f.Watch(func(event any, err error) {
|
|
// Simple callback that doesn't do much
|
|
_ = event
|
|
_ = err
|
|
})
|
|
|
|
// Sometimes watch will fail if another goroutine is already watching
|
|
if watchErr == nil {
|
|
// If watch succeeded, unwatch after a short delay
|
|
time.Sleep(time.Millisecond)
|
|
unwatchErr := f.Unwatch()
|
|
if unwatchErr != nil {
|
|
// Log but don't fail - this can happen during cleanup
|
|
t.Logf("Unwatch error (normal during stress test): %v", unwatchErr)
|
|
}
|
|
} else if watchErr.Error() != "file is already being watched" {
|
|
// Unexpected error
|
|
t.Errorf("Unexpected watch error: %v", watchErr)
|
|
}
|
|
|
|
// Small delay to prevent tight loop
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Also test concurrent file modifications while watching
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
counter := 0
|
|
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
// Write new data to trigger file events
|
|
newData := fmt.Sprintf(`{"test": "value", "counter": %d}`, counter)
|
|
os.WriteFile(tmpFile, []byte(newData), 0600)
|
|
counter++
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Let the stress test run for 200ms
|
|
time.Sleep(200 * time.Millisecond)
|
|
close(done)
|
|
|
|
// Wait with timeout to detect deadlocks
|
|
waitChan := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(waitChan)
|
|
}()
|
|
|
|
select {
|
|
case <-waitChan:
|
|
t.Log("File provider concurrency test completed successfully")
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("FILE PROVIDER DEADLOCK: Goroutines did not complete within timeout")
|
|
}
|
|
}
|
|
|
|
// TestGetNilPointer tests Get()'ing nil pointers.
|
|
func TestGetNilPointer(t *testing.T) {
|
|
assert := assert.New(t)
|
|
k := koanf.New(".")
|
|
|
|
type test struct {
|
|
Name string
|
|
}
|
|
var nt *test
|
|
assert.Nil(k.Set("key", nt))
|
|
assert.True(k.Exists("key"))
|
|
got := k.Get("key")
|
|
assert.Nil(got)
|
|
_, ok := got.(*test)
|
|
assert.True(ok, "expected type *test, got %T", got)
|
|
|
|
// Test nil value.
|
|
assert.Nil(k.Set("val", nil))
|
|
assert.Nil(k.Get("val"))
|
|
|
|
// Test slice.
|
|
var s *[]string
|
|
assert.Nil(k.Set("slice", s))
|
|
gotSlice := k.Get("slice")
|
|
assert.Nil(gotSlice)
|
|
_, ok = gotSlice.(*[]string)
|
|
assert.True(ok, "expected type *[]string, got %T", gotSlice)
|
|
}
|
|
|
|
// TesConvertInt64 tests various int64 conversion/overflow scenarios.
|
|
func TesConvertInt64(t *testing.T) {
|
|
assert := assert.New(t)
|
|
k := koanf.New(delim)
|
|
|
|
assert.Nil(k.Load(confmap.Provider(map[string]any{
|
|
// Good values.
|
|
"int64": int64(math.MaxInt64),
|
|
"uint64": uint64(math.MaxInt64),
|
|
"strmaxint": "9223372036854775807",
|
|
"strminint": "-9223372036854775808",
|
|
"str2p53": "9007199254740993",
|
|
"float": 1.9,
|
|
"minfloat": float64(math.MinInt64),
|
|
"strfloat": "3.7",
|
|
|
|
// Overflows.
|
|
"bigfloat": float64(math.MaxInt64),
|
|
"hugefloat": 1e300,
|
|
"neghuge": -1e300,
|
|
"inf": math.Inf(1),
|
|
"neginf": math.Inf(-1),
|
|
"nan": math.NaN(),
|
|
"biguint64": uint64(math.MaxInt64) + 1,
|
|
}, delim), nil))
|
|
|
|
assert.Equal(int64(math.MaxInt64), k.Int64("int64"))
|
|
assert.Equal(int64(math.MaxInt64), k.Int64("uint64"))
|
|
assert.Equal(int64(math.MaxInt64), k.Int64("strmaxint"))
|
|
assert.Equal(int64(math.MinInt64), k.Int64("strminint"))
|
|
assert.Equal(int64(9007199254740993), k.Int64("str2p53"))
|
|
assert.Equal(int64(1), k.Int64("float"))
|
|
assert.Equal(int64(math.MinInt64), k.Int64("minfloat"))
|
|
assert.Equal(int64(3), k.Int64("strfloat"))
|
|
|
|
// Out of range must return 0 and not a wrapped value.
|
|
for _, key := range []string{"bigfloat", "hugefloat", "neghuge", "inf", "neginf", "nan", "biguint64"} {
|
|
assert.Equal(int64(0), k.Int64(key), "Int64(%q) must be 0", key)
|
|
}
|
|
}
|