1332 lines
31 KiB
Go
1332 lines
31 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package confmap
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
yaml "sigs.k8s.io/yaml/goyaml.v3"
|
|
)
|
|
|
|
func TestToStringMapFlatten(t *testing.T) {
|
|
conf := NewFromStringMap(map[string]any{"key::embedded": int64(123)})
|
|
assert.Equal(t, map[string]any{"key": map[string]any{"embedded": int64(123)}}, conf.ToStringMap())
|
|
}
|
|
|
|
func TestToStringMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
fileName string
|
|
stringMap map[string]any
|
|
}{
|
|
{
|
|
name: "Sample Collector configuration",
|
|
fileName: filepath.Join("testdata", "config.yaml"),
|
|
stringMap: map[string]any{
|
|
"receivers": map[string]any{
|
|
"nop": nil,
|
|
"nop/myreceiver": nil,
|
|
},
|
|
|
|
"processors": map[string]any{
|
|
"nop": nil,
|
|
"nop/myprocessor": nil,
|
|
},
|
|
|
|
"exporters": map[string]any{
|
|
"nop": nil,
|
|
"nop/myexporter": nil,
|
|
},
|
|
|
|
"extensions": map[string]any{
|
|
"nop": nil,
|
|
"nop/myextension": nil,
|
|
},
|
|
|
|
"service": map[string]any{
|
|
"extensions": []any{"nop"},
|
|
"pipelines": map[string]any{
|
|
"traces": map[string]any{
|
|
"receivers": []any{"nop"},
|
|
"processors": []any{"nop"},
|
|
"exporters": []any{"nop"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Sample types",
|
|
fileName: filepath.Join("testdata", "basic_types.yaml"),
|
|
stringMap: map[string]any{
|
|
"typed.options": map[string]any{
|
|
"floating.point.example": 3.14,
|
|
"integer.example": 1234,
|
|
"bool.example": false,
|
|
"string.example": "this is a string",
|
|
"nil.example": nil,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Embedded keys",
|
|
fileName: filepath.Join("testdata", "embedded_keys.yaml"),
|
|
stringMap: map[string]any{
|
|
"typed": map[string]any{"options": map[string]any{
|
|
"floating": map[string]any{"point": map[string]any{"example": 3.14}},
|
|
"integer": map[string]any{"example": 1234},
|
|
"bool": map[string]any{"example": false},
|
|
"string": map[string]any{"example": "this is a string"},
|
|
"nil": map[string]any{"example": nil},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert.Equal(t, test.stringMap, newConfFromFile(t, test.fileName))
|
|
})
|
|
}
|
|
}
|
|
|
|
type testConfigAny struct {
|
|
AnyField any `mapstructure:"any_field"`
|
|
}
|
|
|
|
func TestNilToAnyField(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"any_field": nil,
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &testConfigAny{}
|
|
require.NoError(t, conf.Unmarshal(cfg))
|
|
assert.Nil(t, cfg.AnyField)
|
|
}
|
|
|
|
func TestExpandNilStructPointersHookFunc(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"boolean": nil,
|
|
"struct": nil,
|
|
"map_struct": map[string]any{
|
|
"struct": nil,
|
|
},
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &testConfig{}
|
|
assert.Nil(t, cfg.Struct)
|
|
require.NoError(t, conf.Unmarshal(cfg))
|
|
assert.Nil(t, cfg.Boolean)
|
|
// assert.False(t, *cfg.Boolean)
|
|
assert.Nil(t, cfg.Struct)
|
|
assert.NotNil(t, cfg.MapStruct)
|
|
assert.Equal(t, &myStruct{}, cfg.MapStruct["struct"])
|
|
}
|
|
|
|
func TestExpandNilStructPointersHookFuncDefaultNotNilConfigNil(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"boolean": nil,
|
|
"struct": nil,
|
|
"map_struct": map[string]any{
|
|
"struct": nil,
|
|
},
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
varBool := true
|
|
s1 := &myStruct{Name: "s1"}
|
|
s2 := &myStruct{Name: "s2"}
|
|
cfg := &testConfig{
|
|
Boolean: &varBool,
|
|
Struct: s1,
|
|
MapStruct: map[string]*myStruct{"struct": s2},
|
|
}
|
|
require.NoError(t, conf.Unmarshal(cfg))
|
|
assert.NotNil(t, cfg.Boolean)
|
|
assert.True(t, *cfg.Boolean)
|
|
assert.NotNil(t, cfg.Struct)
|
|
assert.Equal(t, s1, cfg.Struct)
|
|
assert.NotNil(t, cfg.MapStruct)
|
|
assert.Equal(t, &myStruct{}, cfg.MapStruct["struct"])
|
|
}
|
|
|
|
func TestUnmarshalWithIgnoreUnused(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"boolean": true,
|
|
"string": "this is a string",
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
require.Error(t, conf.Unmarshal(&testIDConfig{}))
|
|
assert.NoError(t, conf.Unmarshal(&testIDConfig{}, WithIgnoreUnused()))
|
|
}
|
|
|
|
type testConfig struct {
|
|
Boolean *bool `mapstructure:"boolean"`
|
|
Struct *myStruct `mapstructure:"struct"`
|
|
MapStruct map[string]*myStruct `mapstructure:"map_struct"`
|
|
}
|
|
|
|
func (t testConfig) Marshal(conf *Conf) error {
|
|
if t.Boolean != nil && !*t.Boolean {
|
|
return errors.New("unable to marshal")
|
|
}
|
|
if err := conf.Marshal(t); err != nil {
|
|
return err
|
|
}
|
|
return conf.Merge(NewFromStringMap(map[string]any{
|
|
"additional": "field",
|
|
}))
|
|
}
|
|
|
|
type myStruct struct {
|
|
Name string
|
|
}
|
|
|
|
type TestID string
|
|
|
|
func (tID *TestID) UnmarshalText(text []byte) error {
|
|
*tID = TestID(strings.TrimSuffix(string(text), "_"))
|
|
if *tID == "error" {
|
|
return errors.New("parsing error")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (tID TestID) MarshalText() (text []byte, err error) {
|
|
out := string(tID)
|
|
if !strings.HasSuffix(out, "_") {
|
|
out += "_"
|
|
}
|
|
return []byte(out), nil
|
|
}
|
|
|
|
type testIDConfig struct {
|
|
Boolean bool `mapstructure:"bool"`
|
|
Map map[TestID]string `mapstructure:"map"`
|
|
}
|
|
|
|
func TestMapKeyStringToMapKeyTextUnmarshalerHookFunc(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"bool": true,
|
|
"map": map[string]any{
|
|
"string": "this is a string",
|
|
},
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
|
|
cfg := &testIDConfig{}
|
|
require.NoError(t, conf.Unmarshal(cfg))
|
|
assert.True(t, cfg.Boolean)
|
|
assert.Equal(t, map[TestID]string{"string": "this is a string"}, cfg.Map)
|
|
}
|
|
|
|
type uint32Config struct {
|
|
Value uint32 `mapstructure:"value"`
|
|
}
|
|
|
|
func TestUint32UnmarshalerSuccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
testValue uint32
|
|
}{
|
|
{
|
|
name: "Test convert 0 to uint",
|
|
testValue: 0,
|
|
},
|
|
{
|
|
name: "Test positive uint conversion",
|
|
testValue: 1000,
|
|
},
|
|
{
|
|
name: "Test largest uint64 conversion",
|
|
testValue: math.MaxUint32,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"value": int(tt.testValue),
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &uint32Config{}
|
|
err := conf.Unmarshal(cfg)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, cfg.Value, tt.testValue)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUint32UnmarshalerFailure(t *testing.T) {
|
|
testValue := -1000
|
|
stringMap := map[string]any{
|
|
"value": testValue,
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &uint32Config{}
|
|
err := conf.Unmarshal(cfg)
|
|
|
|
assert.ErrorContains(t, err, fmt.Sprintf("decoding failed due to the following error(s):\n\ncannot parse 'value', %d overflows uint", testValue))
|
|
}
|
|
|
|
type uint64Config struct {
|
|
Value uint64 `mapstructure:"value"`
|
|
}
|
|
|
|
func TestUint64Unmarshaler(t *testing.T) {
|
|
// Equivalent to -1000, but converted to uint64
|
|
value := uint64(1000)
|
|
testValue := ^(value - 1)
|
|
|
|
stringMap := map[string]any{
|
|
"value": testValue,
|
|
}
|
|
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &uint64Config{}
|
|
err := conf.Unmarshal(cfg)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, cfg.Value, testValue)
|
|
}
|
|
|
|
func TestUint64UnmarshalerFailure(t *testing.T) {
|
|
testValue := -1000
|
|
stringMap := map[string]any{
|
|
"value": testValue,
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
cfg := &uint64Config{}
|
|
err := conf.Unmarshal(cfg)
|
|
|
|
assert.ErrorContains(t, err, fmt.Sprintf("decoding failed due to the following error(s):\n\ncannot parse 'value', %d overflows uint", testValue))
|
|
}
|
|
|
|
func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncDuplicateID(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"bool": true,
|
|
"map": map[string]any{
|
|
"string": "this is a string",
|
|
"string_": "this is another string",
|
|
},
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
|
|
cfg := &testIDConfig{}
|
|
assert.Error(t, conf.Unmarshal(cfg))
|
|
}
|
|
|
|
func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T) {
|
|
stringMap := map[string]any{
|
|
"bool": true,
|
|
"map": map[string]any{
|
|
"error": "this is a string",
|
|
},
|
|
}
|
|
conf := NewFromStringMap(stringMap)
|
|
|
|
cfg := &testIDConfig{}
|
|
assert.Error(t, conf.Unmarshal(cfg))
|
|
}
|
|
|
|
func TestMarshal(t *testing.T) {
|
|
conf := New()
|
|
cfg := &testIDConfig{
|
|
Boolean: true,
|
|
Map: map[TestID]string{
|
|
"string": "this is a string",
|
|
},
|
|
}
|
|
require.NoError(t, conf.Marshal(cfg))
|
|
assert.Equal(t, true, conf.Get("bool"))
|
|
assert.Equal(t, map[string]any{"string_": "this is a string"}, conf.Get("map"))
|
|
}
|
|
|
|
func TestMarshalDuplicateID(t *testing.T) {
|
|
conf := New()
|
|
cfg := &testIDConfig{
|
|
Boolean: true,
|
|
Map: map[TestID]string{
|
|
"string": "this is a string",
|
|
"string_": "this is another string",
|
|
},
|
|
}
|
|
assert.Error(t, conf.Marshal(cfg))
|
|
}
|
|
|
|
func TestMarshalError(t *testing.T) {
|
|
conf := New()
|
|
assert.Error(t, conf.Marshal(nil))
|
|
}
|
|
|
|
func TestMarshaler(t *testing.T) {
|
|
conf := New()
|
|
cfg := &testConfig{
|
|
Struct: &myStruct{
|
|
Name: "StructName",
|
|
},
|
|
}
|
|
require.NoError(t, conf.Marshal(cfg))
|
|
assert.Equal(t, "field", conf.Get("additional"))
|
|
|
|
conf = New()
|
|
type NestedMarshaler struct {
|
|
TestConfig *testConfig
|
|
}
|
|
nmCfg := &NestedMarshaler{
|
|
TestConfig: cfg,
|
|
}
|
|
require.NoError(t, conf.Marshal(nmCfg))
|
|
sub, err := conf.Sub("testconfig")
|
|
require.NoError(t, err)
|
|
assert.True(t, sub.IsSet("additional"))
|
|
assert.Equal(t, "field", sub.Get("additional"))
|
|
varBool := false
|
|
nmCfg.TestConfig.Boolean = &varBool
|
|
assert.Error(t, conf.Marshal(nmCfg))
|
|
}
|
|
|
|
// newConfFromFile creates a new Conf by reading the given file.
|
|
func newConfFromFile(tb testing.TB, fileName string) map[string]any {
|
|
content, err := os.ReadFile(filepath.Clean(fileName))
|
|
require.NoErrorf(tb, err, "unable to read the file %v", fileName)
|
|
|
|
var data map[string]any
|
|
require.NoError(tb, yaml.Unmarshal(content, &data), "unable to parse yaml")
|
|
|
|
return NewFromStringMap(data).ToStringMap()
|
|
}
|
|
|
|
type testConfig2 struct {
|
|
Next *nextConfig `mapstructure:"next"`
|
|
Another string `mapstructure:"another"`
|
|
EmbeddedConfig `mapstructure:",squash"`
|
|
EmbeddedConfig2 `mapstructure:",squash"`
|
|
}
|
|
|
|
type testConfigWithoutUnmarshaler struct {
|
|
Next *nextConfig `mapstructure:"next"`
|
|
Another string `mapstructure:"another"`
|
|
EmbeddedConfig `mapstructure:",squash"`
|
|
EmbeddedConfig2 `mapstructure:",squash"`
|
|
}
|
|
|
|
type testConfigWithEmbeddedError struct {
|
|
Next *nextConfig `mapstructure:"next"`
|
|
Another string `mapstructure:"another"`
|
|
EmbeddedConfigWithError `mapstructure:",squash"`
|
|
}
|
|
|
|
type testConfigWithMarshalError struct {
|
|
Next *nextConfig `mapstructure:"next"`
|
|
Another string `mapstructure:"another"`
|
|
EmbeddedConfigWithMarshalError `mapstructure:",squash"`
|
|
}
|
|
|
|
func (tc *testConfigWithEmbeddedError) Unmarshal(component *Conf) error {
|
|
if err := component.Unmarshal(tc, WithIgnoreUnused()); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type EmbeddedConfig struct {
|
|
Some string `mapstructure:"some"`
|
|
}
|
|
|
|
func (ec *EmbeddedConfig) Unmarshal(component *Conf) error {
|
|
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
|
|
return err
|
|
}
|
|
ec.Some += " is also called"
|
|
return nil
|
|
}
|
|
|
|
type EmbeddedConfig2 struct {
|
|
Some2 string `mapstructure:"some_2"`
|
|
}
|
|
|
|
func (ec *EmbeddedConfig2) Unmarshal(component *Conf) error {
|
|
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
|
|
return err
|
|
}
|
|
ec.Some2 += " also called2"
|
|
return nil
|
|
}
|
|
|
|
type EmbeddedConfigWithError struct{}
|
|
|
|
func (ecwe *EmbeddedConfigWithError) Unmarshal(_ *Conf) error {
|
|
return errors.New("embedded error")
|
|
}
|
|
|
|
type EmbeddedConfigWithMarshalError struct{}
|
|
|
|
func (ecwe EmbeddedConfigWithMarshalError) Marshal(_ *Conf) error {
|
|
return errors.New("marshaling error")
|
|
}
|
|
|
|
func (ecwe EmbeddedConfigWithMarshalError) Unmarshal(_ *Conf) error {
|
|
return nil
|
|
}
|
|
|
|
func (tc *testConfig2) Unmarshal(component *Conf) error {
|
|
if err := component.Unmarshal(tc); err != nil {
|
|
return err
|
|
}
|
|
tc.Another += " is only called directly"
|
|
return nil
|
|
}
|
|
|
|
type nextConfig struct {
|
|
String string `mapstructure:"string"`
|
|
private string
|
|
}
|
|
|
|
func (nc *nextConfig) Unmarshal(component *Conf) error {
|
|
if err := component.Unmarshal(nc); err != nil {
|
|
return err
|
|
}
|
|
nc.String += " is called"
|
|
return nil
|
|
}
|
|
|
|
func TestUnmarshaler(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
"some": "make sure this",
|
|
"some_2": "this better be",
|
|
})
|
|
|
|
tc := &testConfig2{}
|
|
require.NoError(t, cfgMap.Unmarshal(tc))
|
|
assert.Equal(t, "make sure this is only called directly", tc.Another)
|
|
assert.Equal(t, "make sure this is called", tc.Next.String)
|
|
assert.Equal(t, "make sure this is also called", tc.Some)
|
|
assert.Equal(t, "this better be also called2", tc.Some2)
|
|
}
|
|
|
|
func TestEmbeddedUnmarshaler(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
"some": "make sure this",
|
|
"some_2": "this better be",
|
|
})
|
|
|
|
tc := &testConfigWithoutUnmarshaler{}
|
|
require.NoError(t, cfgMap.Unmarshal(tc))
|
|
assert.Equal(t, "make sure this", tc.Another)
|
|
assert.Equal(t, "make sure this is called", tc.Next.String)
|
|
assert.Equal(t, "make sure this is also called", tc.Some)
|
|
assert.Equal(t, "this better be also called2", tc.Some2)
|
|
}
|
|
|
|
func TestEmbeddedUnmarshalerError(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
"some": "make sure this",
|
|
})
|
|
|
|
tc := &testConfigWithEmbeddedError{}
|
|
assert.EqualError(t, cfgMap.Unmarshal(tc), "embedded error")
|
|
}
|
|
|
|
func TestEmbeddedMarshalerError(t *testing.T) {
|
|
t.Skip("This test fails because the main struct calls the embedded struct Unmarshal method, and doesn't execute the embedded struct hook.")
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
})
|
|
|
|
tc := &testConfigWithMarshalError{}
|
|
assert.EqualError(t, cfgMap.Unmarshal(tc), "error running encode hook: marshaling error")
|
|
}
|
|
|
|
type B struct {
|
|
String string `mapstructure:"string"`
|
|
}
|
|
|
|
func (b *B) Unmarshal(conf *Conf) error {
|
|
return conf.Unmarshal(b)
|
|
}
|
|
|
|
type A struct {
|
|
B `mapstructure:",squash"`
|
|
}
|
|
|
|
func (a *A) Unmarshal(conf *Conf) error {
|
|
return conf.Unmarshal(a)
|
|
}
|
|
|
|
func TestUnmarshalerEmbeddedNilMap(t *testing.T) {
|
|
cfg := A{}
|
|
nilConf := NewFromStringMap(nil)
|
|
require.NoError(t, nilConf.Unmarshal(&cfg))
|
|
}
|
|
|
|
func TestUnmarshalerKeepAlreadyInitialized(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
})
|
|
|
|
tc := &testConfig2{Next: &nextConfig{
|
|
private: "keep already configured members",
|
|
}}
|
|
require.NoError(t, cfgMap.Unmarshal(tc))
|
|
assert.Equal(t, "make sure this is only called directly", tc.Another)
|
|
assert.Equal(t, "make sure this is called", tc.Next.String)
|
|
assert.Equal(t, "keep already configured members", tc.Next.private)
|
|
}
|
|
|
|
func TestDirectUnmarshaler(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"next": map[string]any{
|
|
"string": "make sure this",
|
|
},
|
|
"another": "make sure this",
|
|
})
|
|
|
|
tc := &testConfig2{Next: &nextConfig{
|
|
private: "keep already configured members",
|
|
}}
|
|
require.NoError(t, tc.Unmarshal(cfgMap))
|
|
assert.Equal(t, "make sure this is only called directly is only called directly", tc.Another)
|
|
assert.Equal(t, "make sure this is called", tc.Next.String)
|
|
assert.Equal(t, "keep already configured members", tc.Next.private)
|
|
}
|
|
|
|
type testErrConfig struct {
|
|
Err errConfig `mapstructure:"err"`
|
|
}
|
|
|
|
type errConfig struct {
|
|
Foo string `mapstructure:"foo"`
|
|
}
|
|
|
|
func (tc *errConfig) Unmarshal(*Conf) error {
|
|
return errors.New("never works")
|
|
}
|
|
|
|
func TestUnmarshalerErr(t *testing.T) {
|
|
cfgMap := NewFromStringMap(map[string]any{
|
|
"err": map[string]any{
|
|
"foo": "will not unmarshal due to error",
|
|
},
|
|
})
|
|
|
|
tc := &testErrConfig{}
|
|
require.EqualError(t, cfgMap.Unmarshal(tc), "decoding failed due to the following error(s):\n\nerror decoding 'err': never works")
|
|
assert.Empty(t, tc.Err.Foo)
|
|
}
|
|
|
|
func TestZeroSliceHookFunc(t *testing.T) {
|
|
type structWithSlices struct {
|
|
Strings []string `mapstructure:"strings"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg map[string]any
|
|
provided any
|
|
expected any
|
|
}{
|
|
{
|
|
name: "overridden by slice",
|
|
cfg: map[string]any{
|
|
"strings": []string{"111"},
|
|
},
|
|
provided: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy", "zzzz"},
|
|
},
|
|
expected: &structWithSlices{
|
|
Strings: []string{"111"},
|
|
},
|
|
},
|
|
{
|
|
name: "overridden by a bigger slice",
|
|
cfg: map[string]any{
|
|
"strings": []string{"111", "222", "333"},
|
|
},
|
|
provided: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
expected: &structWithSlices{
|
|
Strings: []string{"111", "222", "333"},
|
|
},
|
|
},
|
|
{
|
|
name: "overridden by an empty slice",
|
|
cfg: map[string]any{
|
|
"strings": []string{},
|
|
},
|
|
provided: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
expected: &structWithSlices{
|
|
Strings: []string{},
|
|
},
|
|
},
|
|
{
|
|
name: "not overridden by nil",
|
|
cfg: map[string]any{
|
|
"strings": nil,
|
|
},
|
|
provided: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
expected: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
},
|
|
{
|
|
name: "not overridden by missing value",
|
|
cfg: map[string]any{},
|
|
provided: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
expected: &structWithSlices{
|
|
Strings: []string{"xxx", "yyyy"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := NewFromStringMap(tt.cfg)
|
|
|
|
err := cfg.Unmarshal(tt.provided)
|
|
if assert.NoError(t, err) {
|
|
assert.Equal(t, tt.expected, tt.provided)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Tests for issue that happened in https://github.com/open-telemetry/opentelemetry-collector/issues/12661.
|
|
func TestStructValuesReplaced(t *testing.T) {
|
|
type S struct {
|
|
A string `mapstructure:"A,omitempty"`
|
|
B string `mapstructure:"B,omitempty"`
|
|
}
|
|
|
|
type structWithSlices struct {
|
|
Structs []S `mapstructure:"structs"`
|
|
}
|
|
|
|
slicesStruct := structWithSlices{
|
|
Structs: []S{
|
|
{A: "A"},
|
|
},
|
|
}
|
|
|
|
bCfg := map[string]any{
|
|
"structs": []any{
|
|
map[string]any{
|
|
"B": "B",
|
|
},
|
|
},
|
|
}
|
|
bConf := NewFromStringMap(bCfg)
|
|
err := bConf.Unmarshal(&slicesStruct)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, []S{{B: "B"}}, slicesStruct.Structs)
|
|
}
|
|
|
|
func TestNilValuesUnchanged(t *testing.T) {
|
|
type structWithSlices struct {
|
|
Strings []string `mapstructure:"strings"`
|
|
}
|
|
|
|
slicesStruct := &structWithSlices{}
|
|
|
|
nilCfg := map[string]any{
|
|
"strings": []any(nil),
|
|
}
|
|
nilConf := NewFromStringMap(nilCfg)
|
|
err := nilConf.Unmarshal(slicesStruct)
|
|
require.NoError(t, err)
|
|
|
|
confFromStruct := New()
|
|
err = confFromStruct.Marshal(slicesStruct)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, nilCfg, nilConf.ToStringMap())
|
|
require.Equal(t, confFromStruct.ToStringMap(), nilConf.ToStringMap())
|
|
}
|
|
|
|
func TestEmptySliceUnchanged(t *testing.T) {
|
|
type structWithSlices struct {
|
|
Strings []string `mapstructure:"strings"`
|
|
}
|
|
|
|
slicesStruct := &structWithSlices{}
|
|
|
|
nilCfg := map[string]any{
|
|
"strings": []any{},
|
|
}
|
|
nilConf := NewFromStringMap(nilCfg)
|
|
err := nilConf.Unmarshal(slicesStruct)
|
|
require.NoError(t, err)
|
|
|
|
confFromStruct := New()
|
|
err = confFromStruct.Marshal(slicesStruct)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, nilCfg, nilConf.ToStringMap())
|
|
require.Equal(t, nilConf.ToStringMap(), confFromStruct.ToStringMap())
|
|
}
|
|
|
|
type c struct {
|
|
Modifiers []string `mapstructure:"modifiers"`
|
|
}
|
|
|
|
func (c *c) Unmarshal(conf *Conf) error {
|
|
if err := conf.Unmarshal(c); err != nil {
|
|
return err
|
|
}
|
|
c.Modifiers = append(c.Modifiers, "c.Unmarshal")
|
|
return nil
|
|
}
|
|
|
|
type b struct {
|
|
Modifiers []string `mapstructure:"modifiers"`
|
|
C c `mapstructure:"c"`
|
|
}
|
|
|
|
func (b *b) Unmarshal(conf *Conf) error {
|
|
if err := conf.Unmarshal(b); err != nil {
|
|
return err
|
|
}
|
|
b.Modifiers = append(b.Modifiers, "B.Unmarshal")
|
|
b.C.Modifiers = append(b.C.Modifiers, "B.Unmarshal")
|
|
return nil
|
|
}
|
|
|
|
type a struct {
|
|
Modifiers []string `mapstructure:"modifiers"`
|
|
B b `mapstructure:"b"`
|
|
}
|
|
|
|
func (a *a) Unmarshal(conf *Conf) error {
|
|
if err := conf.Unmarshal(a); err != nil {
|
|
return err
|
|
}
|
|
a.Modifiers = append(a.Modifiers, "A.Unmarshal")
|
|
a.B.Modifiers = append(a.B.Modifiers, "A.Unmarshal")
|
|
a.B.C.Modifiers = append(a.B.C.Modifiers, "A.Unmarshal")
|
|
return nil
|
|
}
|
|
|
|
type wrapper struct {
|
|
A a `mapstructure:"a"`
|
|
}
|
|
|
|
// Test that calling the Unmarshal method on configuration structs is done from the inside out.
|
|
func TestNestedUnmarshalerImplementations(t *testing.T) {
|
|
conf := NewFromStringMap(map[string]any{"a": map[string]any{
|
|
"modifiers": []string{"conf.Unmarshal"},
|
|
"b": map[string]any{
|
|
"modifiers": []string{"conf.Unmarshal"},
|
|
"c": map[string]any{
|
|
"modifiers": []string{"conf.Unmarshal"},
|
|
},
|
|
},
|
|
}})
|
|
|
|
// Use a wrapper struct until we deprecate component.UnmarshalConfig
|
|
w := &wrapper{}
|
|
require.NoError(t, conf.Unmarshal(w))
|
|
|
|
a := w.A
|
|
assert.Equal(t, []string{"conf.Unmarshal", "A.Unmarshal"}, a.Modifiers)
|
|
assert.Equal(t, []string{"conf.Unmarshal", "B.Unmarshal", "A.Unmarshal"}, a.B.Modifiers)
|
|
assert.Equal(t, []string{"conf.Unmarshal", "c.Unmarshal", "B.Unmarshal", "A.Unmarshal"}, a.B.C.Modifiers)
|
|
}
|
|
|
|
// Test that unmarshaling the same conf twice works.
|
|
func TestUnmarshalDouble(t *testing.T) {
|
|
conf := NewFromStringMap(map[string]any{
|
|
"str": "test",
|
|
})
|
|
|
|
type Struct struct {
|
|
Str string `mapstructure:"str"`
|
|
}
|
|
s := &Struct{}
|
|
require.NoError(t, conf.Unmarshal(s))
|
|
assert.Equal(t, "test", s.Str)
|
|
|
|
type Struct2 struct {
|
|
Str string `mapstructure:"str"`
|
|
}
|
|
s2 := &Struct2{}
|
|
require.NoError(t, conf.Unmarshal(s2))
|
|
assert.Equal(t, "test", s2.Str)
|
|
}
|
|
|
|
type embeddedStructWithUnmarshal struct {
|
|
Foo string `mapstructure:"foo"`
|
|
success string
|
|
}
|
|
|
|
func (e *embeddedStructWithUnmarshal) Unmarshal(c *Conf) error {
|
|
if err := c.Unmarshal(e, WithIgnoreUnused()); err != nil {
|
|
return err
|
|
}
|
|
e.success = "success"
|
|
return nil
|
|
}
|
|
|
|
type configWithUnmarshalFromEmbeddedStruct struct {
|
|
embeddedStructWithUnmarshal
|
|
}
|
|
|
|
type topLevel struct {
|
|
Cfg *configWithUnmarshalFromEmbeddedStruct `mapstructure:"toplevel"`
|
|
}
|
|
|
|
// Test that Unmarshal is called on the embedded struct on the struct.
|
|
func TestUnmarshalThroughEmbeddedStruct(t *testing.T) {
|
|
c := NewFromStringMap(map[string]any{
|
|
"toplevel": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
})
|
|
cfg := &topLevel{}
|
|
err := c.Unmarshal(cfg)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "success", cfg.Cfg.success)
|
|
require.Equal(t, "bar", cfg.Cfg.Foo)
|
|
}
|
|
|
|
type configWithOwnUnmarshalAndEmbeddedSquashedStruct struct {
|
|
embeddedStructWithUnmarshal `mapstructure:",squash"`
|
|
}
|
|
|
|
type topLevelSquashedEmbedded struct {
|
|
Cfg *configWithOwnUnmarshalAndEmbeddedSquashedStruct `mapstructure:"toplevel"`
|
|
}
|
|
|
|
// Test that the Unmarshal method is called on the squashed, embedded struct.
|
|
func TestUnmarshalOwnThroughEmbeddedSquashedStruct(t *testing.T) {
|
|
c := NewFromStringMap(map[string]any{
|
|
"toplevel": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
})
|
|
cfg := &topLevelSquashedEmbedded{}
|
|
err := c.Unmarshal(cfg)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "success", cfg.Cfg.success)
|
|
require.Equal(t, "bar", cfg.Cfg.Foo)
|
|
}
|
|
|
|
type recursive struct {
|
|
Foo string `mapstructure:"foo"`
|
|
}
|
|
|
|
func (r *recursive) Unmarshal(conf *Conf) error {
|
|
newR := &recursive{}
|
|
if err := conf.Unmarshal(newR); err != nil {
|
|
return err
|
|
}
|
|
*r = *newR
|
|
return nil
|
|
}
|
|
|
|
// Tests that a struct can unmarshal itself by creating a new copy of itself, unmarshaling itself, and setting its value.
|
|
func TestRecursiveUnmarshaling(t *testing.T) {
|
|
conf := NewFromStringMap(map[string]any{
|
|
"foo": "something",
|
|
})
|
|
r := &recursive{}
|
|
require.NoError(t, conf.Unmarshal(r))
|
|
require.Equal(t, "something", r.Foo)
|
|
}
|
|
|
|
func TestExpandedValue(t *testing.T) {
|
|
cm := NewFromStringMap(map[string]any{
|
|
"key": expandedValue{
|
|
Value: 0xdeadbeef,
|
|
Original: "original",
|
|
},
|
|
})
|
|
assert.Equal(t, 0xdeadbeef, cm.Get("key"))
|
|
assert.Equal(t, map[string]any{"key": 0xdeadbeef}, cm.ToStringMap())
|
|
|
|
type ConfigStr struct {
|
|
Key string `mapstructure:"key"`
|
|
}
|
|
|
|
cfgStr := ConfigStr{}
|
|
require.NoError(t, cm.Unmarshal(&cfgStr))
|
|
assert.Equal(t, "original", cfgStr.Key)
|
|
|
|
type ConfigInt struct {
|
|
Key int `mapstructure:"key"`
|
|
}
|
|
cfgInt := ConfigInt{}
|
|
require.NoError(t, cm.Unmarshal(&cfgInt))
|
|
assert.Equal(t, 0xdeadbeef, cfgInt.Key)
|
|
|
|
type ConfigBool struct {
|
|
Key bool `mapstructure:"key"`
|
|
}
|
|
cfgBool := ConfigBool{}
|
|
assert.Error(t, cm.Unmarshal(&cfgBool))
|
|
}
|
|
|
|
func TestSubExpandedValue(t *testing.T) {
|
|
cm := NewFromStringMap(map[string]any{
|
|
"key": map[string]any{
|
|
"subkey": expandedValue{
|
|
Value: map[string]any{"subsubkey": "value"},
|
|
Original: "subsubkey: value",
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.Equal(t, map[string]any{"subkey": map[string]any{"subsubkey": "value"}}, cm.Get("key"))
|
|
assert.Equal(t, map[string]any{"key": map[string]any{"subkey": map[string]any{"subsubkey": "value"}}}, cm.ToStringMap())
|
|
assert.Equal(t, map[string]any{"subsubkey": "value"}, cm.Get("key::subkey"))
|
|
|
|
sub, err := cm.Sub("key::subkey")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]any{"subsubkey": "value"}, sub.ToStringMap())
|
|
|
|
// This should return value, but currently `Get` does not support keys within expanded values.
|
|
assert.Nil(t, cm.Get("key::subkey::subsubkey"))
|
|
}
|
|
|
|
func TestStringyTypes(t *testing.T) {
|
|
tests := []struct {
|
|
valueOfType any
|
|
isStringy bool
|
|
}{
|
|
{
|
|
valueOfType: "string",
|
|
isStringy: true,
|
|
},
|
|
{
|
|
valueOfType: 1,
|
|
isStringy: false,
|
|
},
|
|
{
|
|
valueOfType: map[string]any{},
|
|
isStringy: false,
|
|
},
|
|
{
|
|
valueOfType: []any{},
|
|
isStringy: false,
|
|
},
|
|
{
|
|
valueOfType: map[string]string{},
|
|
isStringy: true,
|
|
},
|
|
{
|
|
valueOfType: []string{},
|
|
isStringy: true,
|
|
},
|
|
{
|
|
valueOfType: map[string][]string{},
|
|
isStringy: true,
|
|
},
|
|
{
|
|
valueOfType: map[string]map[string]string{},
|
|
isStringy: true,
|
|
},
|
|
{
|
|
valueOfType: []map[string]any{},
|
|
isStringy: false,
|
|
},
|
|
{
|
|
valueOfType: []map[string]string{},
|
|
isStringy: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
// Create a reflect.Type from the value
|
|
to := reflect.TypeOf(tt.valueOfType)
|
|
assert.Equal(t, tt.isStringy, isStringyStructure(to))
|
|
}
|
|
}
|
|
|
|
func TestConfDelete(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
stringMap map[string]any
|
|
}{
|
|
{
|
|
path: "key",
|
|
stringMap: map[string]any{"key": "value"},
|
|
},
|
|
{
|
|
path: "map::expanded",
|
|
stringMap: map[string]any{"map": map[string]any{
|
|
"expanded": expandedValue{
|
|
Value: 0o1234,
|
|
Original: "01234",
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.path, func(t *testing.T) {
|
|
cm := NewFromStringMap(test.stringMap)
|
|
|
|
assert.True(t, cm.IsSet(test.path))
|
|
assert.True(t, cm.Delete(test.path))
|
|
assert.Nil(t, cm.Get(test.path))
|
|
assert.False(t, cm.IsSet(test.path))
|
|
|
|
assert.False(t, cm.Delete(test.path))
|
|
assert.Nil(t, cm.Get(test.path))
|
|
assert.False(t, cm.IsSet(test.path))
|
|
})
|
|
}
|
|
}
|
|
|
|
type structWithConfigOpaqueMap struct {
|
|
Headers map[string]string `mapstructure:"headers"`
|
|
}
|
|
|
|
func TestMapMerge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initial map[string]string
|
|
added map[string]string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
name: "both nil",
|
|
initial: nil,
|
|
added: nil,
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "nil map",
|
|
initial: map[string]string{},
|
|
added: nil,
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "initialized",
|
|
initial: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
added: nil,
|
|
expected: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
{
|
|
name: "both",
|
|
initial: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
added: map[string]string{
|
|
"foobar": "bar",
|
|
},
|
|
expected: map[string]string{
|
|
"foo": "bar",
|
|
"foobar": "bar",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
s := structWithConfigOpaqueMap{
|
|
Headers: test.initial,
|
|
}
|
|
c := NewFromStringMap(map[string]any{
|
|
"headers": test.added,
|
|
})
|
|
require.NoError(t, c.Unmarshal(&s))
|
|
assert.Equal(t, test.expected, s.Headers)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfIsNil(t *testing.T) {
|
|
const subKey = "foo"
|
|
testCases := []struct {
|
|
name string
|
|
input map[string]any
|
|
expectIsNil bool
|
|
subExpectNil bool
|
|
subExpectErr string
|
|
}{
|
|
{
|
|
name: "nil input",
|
|
input: nil,
|
|
expectIsNil: true,
|
|
subExpectNil: true,
|
|
},
|
|
{
|
|
name: "empty map",
|
|
input: map[string]any{},
|
|
expectIsNil: false,
|
|
subExpectNil: true,
|
|
},
|
|
{
|
|
name: "nil subkey",
|
|
input: map[string]any{subKey: nil},
|
|
expectIsNil: false,
|
|
subExpectNil: true,
|
|
},
|
|
{
|
|
name: "empty subkey",
|
|
input: map[string]any{subKey: map[string]any{}},
|
|
expectIsNil: false,
|
|
subExpectNil: false,
|
|
},
|
|
{
|
|
name: "non-empty map",
|
|
input: map[string]any{subKey: map[string]any{"bar": 42}},
|
|
expectIsNil: false,
|
|
subExpectNil: false,
|
|
},
|
|
{
|
|
name: "non-map subkey",
|
|
input: map[string]any{subKey: 123},
|
|
expectIsNil: false,
|
|
subExpectErr: "unexpected sub-config value kind",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
conf := NewFromStringMap(tc.input)
|
|
if tc.expectIsNil {
|
|
assert.Empty(t, conf.AllKeys())
|
|
assert.Equal(t, map[string]any(nil), conf.ToStringMap())
|
|
} else {
|
|
assert.NotEqual(t, map[string]any(nil), conf.ToStringMap())
|
|
}
|
|
|
|
sub, err := conf.Sub(subKey)
|
|
if tc.subExpectErr != "" {
|
|
assert.ErrorContains(t, err, tc.subExpectErr)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tc.subExpectNil {
|
|
assert.Empty(t, sub.AllKeys())
|
|
assert.Equal(t, map[string]any(nil), sub.ToStringMap())
|
|
} else {
|
|
assert.NotEqual(t, map[string]any(nil), sub.ToStringMap())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfmapNilMerge(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
left map[string]any
|
|
right map[string]any
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "both nil",
|
|
left: nil,
|
|
right: nil,
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "left nil",
|
|
left: nil,
|
|
right: map[string]any{"key": "value"},
|
|
expected: map[string]any{"key": "value"},
|
|
},
|
|
{
|
|
name: "right nil",
|
|
left: map[string]any{"key": "value"},
|
|
right: nil,
|
|
expected: map[string]any{"key": "value"},
|
|
},
|
|
{
|
|
name: "both non-nil",
|
|
left: map[string]any{"key1": "value1"},
|
|
right: map[string]any{"key2": "value2"},
|
|
expected: map[string]any{"key1": "value1", "key2": "value2"},
|
|
},
|
|
{
|
|
name: "left empty, right non-empty",
|
|
left: map[string]any{},
|
|
right: map[string]any{"key": "value"},
|
|
expected: map[string]any{"key": "value"},
|
|
},
|
|
{
|
|
name: "left non-empty, right empty",
|
|
left: map[string]any{"key": "value"},
|
|
right: map[string]any{},
|
|
expected: map[string]any{"key": "value"},
|
|
},
|
|
{
|
|
name: "left nil, right empty",
|
|
left: nil,
|
|
right: map[string]any{},
|
|
expected: map[string]any{},
|
|
},
|
|
{
|
|
name: "left empty, right nil",
|
|
left: map[string]any{},
|
|
right: nil,
|
|
expected: map[string]any{},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
leftConf := NewFromStringMap(test.left)
|
|
assert.Equal(t, test.left, leftConf.ToStringMap())
|
|
rightConf := NewFromStringMap(test.right)
|
|
assert.Equal(t, test.right, rightConf.ToStringMap())
|
|
|
|
err := leftConf.Merge(rightConf)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, test.expected, leftConf.ToStringMap())
|
|
})
|
|
|
|
t.Run(test.name+"merge append", func(t *testing.T) {
|
|
leftConf := NewFromStringMap(test.left)
|
|
assert.Equal(t, test.left, leftConf.ToStringMap())
|
|
rightConf := NewFromStringMap(test.right)
|
|
assert.Equal(t, test.right, rightConf.ToStringMap())
|
|
|
|
err := leftConf.mergeAppend(rightConf)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, test.expected, leftConf.ToStringMap())
|
|
})
|
|
}
|
|
}
|