mirror of
https://github.com/hanzoai/telemetry.git
synced 2026-10-30 20:01:49 +00:00
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Allow `configoptional.Optional` to wrap scalar values using new interfaces designed specifically to handle similar wrappers around scalar values. Follow up to https://github.com/open-telemetry/opentelemetry-collector/pull/13524. --------- Co-authored-by: Jade Guiton <[email protected]>
1430 lines
34 KiB
Go
1430 lines
34 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package configoptional
|
|
|
|
import (
|
|
"encoding"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.opentelemetry.io/collector/confmap"
|
|
"go.opentelemetry.io/collector/confmap/confmaptest"
|
|
)
|
|
|
|
type Config[T any] struct {
|
|
Sub1 Optional[T] `mapstructure:"sub"`
|
|
}
|
|
|
|
type Sub struct {
|
|
Foo string `mapstructure:"foo"`
|
|
}
|
|
|
|
type WithEnabled struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
}
|
|
|
|
type WithEnabledOmitEmpty struct {
|
|
Enabled bool `mapstructure:"enabled,omitempty"`
|
|
}
|
|
|
|
type OmitEmpty struct {
|
|
Foo int `mapstructure:",omitempty"`
|
|
}
|
|
|
|
type NoMapstructure struct {
|
|
Foo string
|
|
}
|
|
|
|
var _ encoding.TextUnmarshaler = (*textLevel)(nil)
|
|
|
|
type textLevel string
|
|
|
|
const (
|
|
textLevelHigh textLevel = "high"
|
|
textLevelLow textLevel = "low"
|
|
textLevelNone textLevel = "none"
|
|
)
|
|
|
|
func (l *textLevel) UnmarshalText(text []byte) error {
|
|
switch textLevel(text) {
|
|
case textLevelHigh, textLevelLow, textLevelNone:
|
|
*l = textLevel(text)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown textLevel %q", string(text))
|
|
}
|
|
}
|
|
|
|
var _ confmap.Unmarshaler = (*customUnmarshalerStruct)(nil)
|
|
|
|
type customUnmarshalerStruct struct {
|
|
Val string
|
|
}
|
|
|
|
func (c *customUnmarshalerStruct) Unmarshal(conf *confmap.Conf) error {
|
|
m := conf.ToStringMap()
|
|
if v, ok := m["val"]; ok {
|
|
c.Val = fmt.Sprintf("%v", v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var subDefault = Sub{
|
|
Foo: "foobar",
|
|
}
|
|
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
func TestDefaultPanics(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
_ = Default(WithEnabled{})
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
_ = Default(WithEnabledOmitEmpty{})
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
_ = Some(WithEnabled{})
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
_ = None[WithEnabled]()
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
opt := None[int]()
|
|
_ = opt.GetOrInsertDefault()
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
var opt Optional[WithEnabled]
|
|
_ = opt.GetOrInsertDefault()
|
|
})
|
|
|
|
assert.NotPanics(t, func() {
|
|
_ = Default(NoMapstructure{})
|
|
})
|
|
|
|
assert.NotPanics(t, func() {
|
|
_ = Default(OmitEmpty{})
|
|
})
|
|
|
|
assert.NotPanics(t, func() {
|
|
_ = Default(subDefault)
|
|
})
|
|
|
|
assert.NotPanics(t, func() {
|
|
_ = Default(ptr(subDefault))
|
|
})
|
|
}
|
|
|
|
func TestEqualityDefault(t *testing.T) {
|
|
defaultOne := Default(subDefault)
|
|
defaultTwo := Default(subDefault)
|
|
assert.Equal(t, defaultOne, defaultTwo)
|
|
}
|
|
|
|
func TestNoneZeroVal(t *testing.T) {
|
|
var none Optional[Sub]
|
|
require.False(t, none.HasValue())
|
|
require.Nil(t, none.Get())
|
|
|
|
var zeroVal Sub
|
|
ret := none.GetOrInsertDefault()
|
|
require.True(t, none.HasValue())
|
|
assert.Equal(t, &zeroVal, ret)
|
|
}
|
|
|
|
func TestNone(t *testing.T) {
|
|
none := None[Sub]()
|
|
require.False(t, none.HasValue())
|
|
require.Nil(t, none.Get())
|
|
|
|
var zeroVal Sub
|
|
ret := none.GetOrInsertDefault()
|
|
require.True(t, none.HasValue())
|
|
assert.Equal(t, &zeroVal, ret)
|
|
}
|
|
|
|
func ExampleNone() {
|
|
type Person struct {
|
|
Name string
|
|
Age int
|
|
}
|
|
|
|
opt := None[Person]()
|
|
|
|
// A None has no value.
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// GetOrInsertDefault places the zero value
|
|
// and returns it, allowing you to modify it.
|
|
opt.GetOrInsertDefault().Name = "John Doe"
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// Output:
|
|
// false
|
|
// <nil>
|
|
// true
|
|
// &{John Doe 0}
|
|
}
|
|
|
|
func TestSome(t *testing.T) {
|
|
some := Some(Sub{
|
|
Foo: "foobar",
|
|
})
|
|
require.True(t, some.HasValue())
|
|
retGet := some.Get()
|
|
assert.Equal(t, "foobar", retGet.Foo)
|
|
|
|
retGetOrInsertDefault := some.GetOrInsertDefault()
|
|
require.True(t, some.HasValue())
|
|
assert.Equal(t, retGet, retGetOrInsertDefault)
|
|
}
|
|
|
|
func ExampleSome() {
|
|
type Person struct {
|
|
Name string
|
|
Age int
|
|
}
|
|
|
|
opt := Some(Person{
|
|
Name: "John Doe",
|
|
Age: 42,
|
|
})
|
|
|
|
// A Some has a value.
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// GetOrInsertDefault only returns a reference
|
|
// to the inner value without modifying it.
|
|
opt.GetOrInsertDefault().Name = "Jane Doe"
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// Output:
|
|
// true
|
|
// &{John Doe 42}
|
|
// true
|
|
// &{Jane Doe 42}
|
|
}
|
|
|
|
func TestDefault(t *testing.T) {
|
|
defaultSub := Default(subDefault)
|
|
require.False(t, defaultSub.HasValue())
|
|
require.Nil(t, defaultSub.Get())
|
|
|
|
ret := defaultSub.GetOrInsertDefault()
|
|
require.True(t, defaultSub.HasValue())
|
|
assert.Equal(t, &subDefault, ret)
|
|
}
|
|
|
|
func ExampleDefault() {
|
|
type Person struct {
|
|
Name string
|
|
Age int
|
|
}
|
|
|
|
opt := Default(Person{
|
|
Name: "John Doe",
|
|
Age: 42,
|
|
})
|
|
|
|
// A Default has no value.
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// GetOrInsertDefault places the default value
|
|
// and returns it, allowing you to modify it.
|
|
opt.GetOrInsertDefault().Age = 38
|
|
fmt.Println(opt.HasValue())
|
|
fmt.Println(opt.Get())
|
|
|
|
// Output:
|
|
// false
|
|
// <nil>
|
|
// true
|
|
// &{John Doe 38}
|
|
}
|
|
|
|
func TestUnmarshalOptional(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
defaultCfg Config[Sub]
|
|
expectedSub bool
|
|
expectedFoo string
|
|
}{
|
|
{
|
|
name: "none_no_config",
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: None[Sub](),
|
|
},
|
|
expectedSub: false,
|
|
},
|
|
{
|
|
name: "none_with_config",
|
|
config: map[string]any{
|
|
"sub": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: None[Sub](),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "bar", // input overrides default
|
|
},
|
|
{
|
|
// nil is treated as an empty map because of the hooks we use.
|
|
name: "none_with_config_no_foo",
|
|
config: map[string]any{
|
|
"sub": nil,
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: None[Sub](),
|
|
},
|
|
expectedSub: false,
|
|
},
|
|
{
|
|
name: "none_with_config_no_foo_empty_map",
|
|
config: map[string]any{
|
|
"sub": map[string]any{},
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: None[Sub](),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "",
|
|
},
|
|
{
|
|
name: "default_no_config",
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Default(subDefault),
|
|
},
|
|
expectedSub: false,
|
|
},
|
|
{
|
|
name: "default_with_config",
|
|
config: map[string]any{
|
|
"sub": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Default(subDefault),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "bar", // input overrides default
|
|
},
|
|
{
|
|
name: "default_with_config_no_foo",
|
|
config: map[string]any{
|
|
"sub": nil,
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Default(subDefault),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "foobar", // default applies
|
|
},
|
|
{
|
|
name: "default_with_config_no_foo_empty_map",
|
|
config: map[string]any{
|
|
"sub": map[string]any{},
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Default(subDefault),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "foobar", // default applies
|
|
},
|
|
{
|
|
name: "some_no_config",
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Some(Sub{
|
|
Foo: "foobar",
|
|
}),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "foobar", // value is not modified
|
|
},
|
|
{
|
|
name: "some_with_config",
|
|
config: map[string]any{
|
|
"sub": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Some(Sub{
|
|
Foo: "foobar",
|
|
}),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "bar", // input overrides previous value
|
|
},
|
|
{
|
|
name: "some_with_config_no_foo",
|
|
config: map[string]any{
|
|
"sub": nil,
|
|
},
|
|
defaultCfg: Config[Sub]{
|
|
Sub1: Some(Sub{
|
|
Foo: "foobar",
|
|
}),
|
|
},
|
|
expectedSub: true,
|
|
expectedFoo: "foobar", // default applies
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := test.defaultCfg
|
|
conf := confmap.NewFromStringMap(test.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, test.expectedSub, cfg.Sub1.HasValue())
|
|
if test.expectedSub {
|
|
require.Equal(t, test.expectedFoo, cfg.Sub1.Get().Foo)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalOptionalWithoutScalarUnmarshalerOption(t *testing.T) {
|
|
config := map[string]any{
|
|
"sub": map[string]any{"foo": "bar"},
|
|
}
|
|
defaultCfg := Config[Sub]{Sub1: Default(subDefault)}
|
|
expectedFoo := "bar"
|
|
|
|
cfg := defaultCfg
|
|
conf := confmap.NewFromStringMap(config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.True(t, cfg.Sub1.HasValue())
|
|
require.Equal(t, expectedFoo, cfg.Sub1.Get().Foo)
|
|
}
|
|
|
|
func TestUnmarshalErrorEnabledField(t *testing.T) {
|
|
cm := confmap.NewFromStringMap(map[string]any{
|
|
"enabled": true,
|
|
})
|
|
// Use zero value to avoid panic on constructor.
|
|
var none Optional[WithEnabled]
|
|
require.Error(t, cm.Unmarshal(&none))
|
|
}
|
|
|
|
func TestUnmarshalConfigPointer(t *testing.T) {
|
|
cm := confmap.NewFromStringMap(map[string]any{
|
|
"sub": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
})
|
|
|
|
var cfg Config[*Sub]
|
|
err := cm.Unmarshal(&cfg)
|
|
require.NoError(t, err)
|
|
assert.True(t, cfg.Sub1.HasValue())
|
|
assert.Equal(t, "bar", (*cfg.Sub1.Get()).Foo)
|
|
}
|
|
|
|
func TestUnmarshalErr(t *testing.T) {
|
|
cm := confmap.NewFromStringMap(map[string]any{
|
|
"field": "value",
|
|
})
|
|
|
|
cfg := Config[Sub]{
|
|
Sub1: Default(subDefault),
|
|
}
|
|
|
|
assert.False(t, cfg.Sub1.HasValue())
|
|
|
|
err := cm.Unmarshal(&cfg)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "has invalid keys: field")
|
|
assert.False(t, cfg.Sub1.HasValue())
|
|
}
|
|
|
|
type MyIntConfig struct {
|
|
Val int `mapstructure:"my_int"`
|
|
}
|
|
type MyConfig struct {
|
|
Optional[MyIntConfig] `mapstructure:",squash"`
|
|
}
|
|
|
|
type MyListConfig struct {
|
|
Val []string `mapstructure:"my_strs"`
|
|
}
|
|
type TestConfig struct {
|
|
List Optional[MyListConfig] `mapstructure:"list"`
|
|
}
|
|
|
|
var myIntDefault = MyIntConfig{
|
|
Val: 1,
|
|
}
|
|
|
|
func TestSquashedOptional(t *testing.T) {
|
|
cm := confmap.NewFromStringMap(map[string]any{
|
|
"my_int": 42,
|
|
})
|
|
|
|
cfg := MyConfig{
|
|
Default(myIntDefault),
|
|
}
|
|
|
|
err := cm.Unmarshal(&cfg)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, cfg.HasValue())
|
|
assert.Equal(t, 42, cfg.Get().Val)
|
|
}
|
|
|
|
func TestListOptional(t *testing.T) {
|
|
cm := confmap.NewFromStringMap(map[string]any{
|
|
"list": map[string]any{
|
|
"my_strs": []string{"a", "b", "c"},
|
|
},
|
|
})
|
|
|
|
cfg := TestConfig{
|
|
List: Default(MyListConfig{Val: []string{"default"}}),
|
|
}
|
|
|
|
err := cm.Unmarshal(&cfg)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, cfg.List.HasValue())
|
|
require.Equal(t, []string{"a", "b", "c"}, cfg.List.Get().Val)
|
|
}
|
|
|
|
func confFromYAML(t *testing.T, yaml string) *confmap.Conf {
|
|
t.Helper()
|
|
cm, err := confmap.NewRetrievedFromYAML([]byte(yaml))
|
|
require.NoError(t, err)
|
|
conf, err := cm.AsConf()
|
|
require.NoError(t, err)
|
|
return conf
|
|
}
|
|
|
|
func TestComparePointerUnmarshal(t *testing.T) {
|
|
tests := []struct {
|
|
yaml string
|
|
}{
|
|
{yaml: ""},
|
|
{yaml: "sub: "},
|
|
{yaml: "sub: null"},
|
|
{yaml: "sub: {}"},
|
|
{yaml: "sub: {foo: bar}"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.yaml, func(t *testing.T) {
|
|
var optCfg Config[Sub]
|
|
conf := confFromYAML(t, test.yaml)
|
|
optErr := conf.Unmarshal(&optCfg)
|
|
require.NoError(t, optErr)
|
|
|
|
var ptrCfg struct {
|
|
Sub1 *Sub `mapstructure:"sub"`
|
|
}
|
|
ptrErr := conf.Unmarshal(&ptrCfg)
|
|
require.NoError(t, ptrErr)
|
|
|
|
assert.Equal(t, optCfg.Sub1.Get(), ptrCfg.Sub1)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOptionalMarshal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value Config[Sub]
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "none (zero value)",
|
|
value: Config[Sub]{},
|
|
expected: map[string]any{"sub": nil},
|
|
},
|
|
{
|
|
name: "none",
|
|
value: Config[Sub]{Sub1: None[Sub]()},
|
|
expected: map[string]any{"sub": nil},
|
|
},
|
|
{
|
|
name: "default",
|
|
value: Config[Sub]{Sub1: Default(subDefault)},
|
|
expected: map[string]any{"sub": nil},
|
|
},
|
|
{
|
|
name: "some",
|
|
value: Config[Sub]{Sub1: Some(Sub{
|
|
Foo: "bar",
|
|
})},
|
|
expected: map[string]any{
|
|
"sub": map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := confmap.New()
|
|
require.NoError(t, conf.Marshal(test.value))
|
|
assert.Equal(t, test.expected, conf.ToStringMap())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComparePointerMarshal(t *testing.T) {
|
|
type Wrap[T any] struct {
|
|
// Note: passes without requiring "squash".
|
|
Sub1 T `mapstructure:"sub"`
|
|
}
|
|
|
|
type WrapOmitEmpty[T any] struct {
|
|
// Note: passes without requiring "squash", except with Default-flavored Optional values.
|
|
Sub1 T `mapstructure:"sub,omitempty"`
|
|
}
|
|
|
|
tests := []struct {
|
|
pointer *Sub
|
|
optional Optional[Sub]
|
|
skipOmitEmpty bool
|
|
}{
|
|
{pointer: nil, optional: None[Sub]()},
|
|
{pointer: nil, optional: Default(subDefault), skipOmitEmpty: true}, // does not work with omitempty
|
|
{pointer: &Sub{Foo: "bar"}, optional: Some(Sub{Foo: "bar"})},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("%v vs %v", test.pointer, test.optional), func(t *testing.T) {
|
|
wrapPointer := Wrap[*Sub]{Sub1: test.pointer}
|
|
confPointer := confmap.NewFromStringMap(nil)
|
|
require.NoError(t, confPointer.Marshal(wrapPointer))
|
|
|
|
wrapOptional := Wrap[Optional[Sub]]{Sub1: test.optional}
|
|
confOptional := confmap.NewFromStringMap(nil)
|
|
require.NoError(t, confOptional.Marshal(wrapOptional))
|
|
|
|
assert.Equal(t, confPointer.ToStringMap(), confOptional.ToStringMap())
|
|
})
|
|
|
|
if test.skipOmitEmpty {
|
|
continue
|
|
}
|
|
t.Run(fmt.Sprintf("%v vs %v (omitempty)", test.pointer, test.optional), func(t *testing.T) {
|
|
wrapPointer := WrapOmitEmpty[*Sub]{Sub1: test.pointer}
|
|
confPointer := confmap.NewFromStringMap(nil)
|
|
require.NoError(t, confPointer.Marshal(wrapPointer))
|
|
|
|
wrapOptional := WrapOmitEmpty[Optional[Sub]]{Sub1: test.optional}
|
|
confOptional := confmap.NewFromStringMap(nil)
|
|
require.NoError(t, confOptional.Marshal(wrapOptional))
|
|
|
|
assert.Equal(t, confPointer.ToStringMap(), confOptional.ToStringMap())
|
|
})
|
|
}
|
|
}
|
|
|
|
type invalid struct{}
|
|
|
|
func (invalid) Validate() error {
|
|
return errors.New("invalid")
|
|
}
|
|
|
|
var _ confmap.Validator = invalid{}
|
|
|
|
type hasNested struct {
|
|
CouldBe Optional[invalid]
|
|
}
|
|
|
|
func TestOptionalValidate(t *testing.T) {
|
|
require.NoError(t, confmap.Validate(hasNested{
|
|
CouldBe: None[invalid](),
|
|
}))
|
|
require.NoError(t, confmap.Validate(hasNested{
|
|
CouldBe: Default(invalid{}),
|
|
}))
|
|
require.Error(t, confmap.Validate(hasNested{
|
|
CouldBe: Some(invalid{}),
|
|
}))
|
|
}
|
|
|
|
type validatedConfig struct {
|
|
Default Optional[optionalConfig] `mapstructure:"default"`
|
|
Some Optional[someConfig] `mapstructure:"some"`
|
|
}
|
|
|
|
var _ confmap.Validator = (*optionalConfig)(nil)
|
|
|
|
type optionalConfig struct {
|
|
StringVal string `mapstructure:"string_val"`
|
|
}
|
|
|
|
func (n optionalConfig) Validate() error {
|
|
if n.StringVal == "invalid" {
|
|
return errors.New("field `string_val` cannot be set to `invalid`")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type someConfig struct {
|
|
Nested Optional[optionalConfig] `mapstructure:"nested"`
|
|
}
|
|
|
|
func newDefaultValidatedConfig() validatedConfig {
|
|
return validatedConfig{
|
|
Default: Default(optionalConfig{StringVal: "valid"}),
|
|
}
|
|
}
|
|
|
|
func newInvalidDefaultConfig() validatedConfig {
|
|
return validatedConfig{
|
|
Default: Default(optionalConfig{StringVal: "invalid"}),
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalScalar(t *testing.T) {
|
|
type IntConfig struct {
|
|
Val Optional[int] `mapstructure:"val"`
|
|
}
|
|
|
|
t.Run("int", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
initial IntConfig
|
|
expectHasVal bool
|
|
expectVal int
|
|
}{
|
|
// Present scalar value overrides all initial flavors.
|
|
{
|
|
name: "none_with_value",
|
|
config: map[string]any{"val": 42},
|
|
initial: IntConfig{Val: None[int]()},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
{
|
|
name: "default_with_value",
|
|
config: map[string]any{"val": 42},
|
|
initial: IntConfig{Val: Default(5)},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
{
|
|
name: "some_with_value",
|
|
config: map[string]any{"val": 42},
|
|
initial: IntConfig{Val: Some(1)},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
// Absent key leaves the Optional unchanged.
|
|
{
|
|
name: "none_absent_key",
|
|
config: map[string]any{},
|
|
initial: IntConfig{Val: None[int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "default_absent_key",
|
|
config: map[string]any{},
|
|
initial: IntConfig{Val: Default(5)},
|
|
expectHasVal: false,
|
|
},
|
|
// Default.HasValue() == false
|
|
{
|
|
name: "some_absent_key",
|
|
config: map[string]any{},
|
|
initial: IntConfig{Val: Some(3)},
|
|
expectHasVal: true,
|
|
expectVal: 3,
|
|
},
|
|
// Null (as a nil map) explicitly clears to None.
|
|
{
|
|
name: "none_null_map",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: IntConfig{Val: None[int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "default_null_map",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: IntConfig{Val: Default(5)},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some_null_map",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: IntConfig{Val: Some(3)},
|
|
expectHasVal: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
conf := confmap.NewFromStringMap(tc.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("slice_of_ints", func(t *testing.T) {
|
|
type SliceIntConfig struct {
|
|
Val Optional[[]int] `mapstructure:"val"`
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
initial SliceIntConfig
|
|
expectHasVal bool
|
|
expectVal []int
|
|
}{
|
|
{
|
|
name: "none_with_value",
|
|
config: map[string]any{"val": []any{1, 2, 3}},
|
|
initial: SliceIntConfig{Val: None[[]int]()},
|
|
expectHasVal: true,
|
|
expectVal: []int{1, 2, 3},
|
|
},
|
|
{
|
|
name: "default_with_value",
|
|
config: map[string]any{"val": []any{4, 5}},
|
|
initial: SliceIntConfig{Val: Default([]int{1})},
|
|
expectHasVal: true,
|
|
expectVal: []int{4, 5},
|
|
},
|
|
{
|
|
name: "some_with_value",
|
|
config: map[string]any{"val": []any{7}},
|
|
initial: SliceIntConfig{Val: Some([]int{1, 2})},
|
|
expectHasVal: true,
|
|
expectVal: []int{7},
|
|
},
|
|
{
|
|
name: "none_absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceIntConfig{Val: None[[]int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "default_absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceIntConfig{Val: Default([]int{1})},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some_absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceIntConfig{Val: Some([]int{1, 2})},
|
|
expectHasVal: true,
|
|
expectVal: []int{1, 2},
|
|
},
|
|
{
|
|
name: "none_null",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: SliceIntConfig{Val: None[[]int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "default_null",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: SliceIntConfig{Val: Default([]int{1})},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some_null",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: SliceIntConfig{Val: Some([]int{1, 2})},
|
|
expectHasVal: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
conf := confmap.NewFromStringMap(tc.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("slice_of_optional_ints", func(t *testing.T) {
|
|
type SliceOptIntConfig struct {
|
|
Val []Optional[int] `mapstructure:"val"`
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
initial SliceOptIntConfig
|
|
expectVal []Optional[int]
|
|
}{
|
|
{
|
|
name: "with_values",
|
|
config: map[string]any{"val": []any{1, 2, 3}},
|
|
initial: SliceOptIntConfig{},
|
|
expectVal: []Optional[int]{Some(1), Some(2), Some(3)},
|
|
},
|
|
{
|
|
name: "absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceOptIntConfig{},
|
|
expectVal: nil,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
conf := confmap.NewFromStringMap(tc.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectVal, cfg.Val)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("slice_of_text_unmarshalers", func(t *testing.T) {
|
|
type SliceTextConfig struct {
|
|
Val Optional[[]textLevel] `mapstructure:"val"`
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
initial SliceTextConfig
|
|
expectHasVal bool
|
|
expectVal []textLevel
|
|
}{
|
|
{
|
|
name: "none_with_values",
|
|
config: map[string]any{"val": []any{"high", "low"}},
|
|
initial: SliceTextConfig{Val: None[[]textLevel]()},
|
|
expectHasVal: true,
|
|
expectVal: []textLevel{textLevelHigh, textLevelLow},
|
|
},
|
|
{
|
|
name: "default_with_values",
|
|
config: map[string]any{"val": []any{"none"}},
|
|
initial: SliceTextConfig{Val: Default([]textLevel{textLevelHigh})},
|
|
expectHasVal: true,
|
|
expectVal: []textLevel{textLevelNone},
|
|
},
|
|
{
|
|
name: "some_with_values",
|
|
config: map[string]any{"val": []any{"low", "high"}},
|
|
initial: SliceTextConfig{Val: Some([]textLevel{textLevelNone})},
|
|
expectHasVal: true,
|
|
expectVal: []textLevel{textLevelLow, textLevelHigh},
|
|
},
|
|
{
|
|
name: "absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceTextConfig{Val: None[[]textLevel]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "null",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: SliceTextConfig{Val: Some([]textLevel{textLevelHigh})},
|
|
expectHasVal: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
conf := confmap.NewFromStringMap(tc.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("slice_of_confmap_unmarshalers", func(t *testing.T) {
|
|
type SliceUnmarshalerConfig struct {
|
|
Val Optional[[]customUnmarshalerStruct] `mapstructure:"val"`
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
config map[string]any
|
|
initial SliceUnmarshalerConfig
|
|
expectHasVal bool
|
|
expectVal []customUnmarshalerStruct
|
|
}{
|
|
{
|
|
name: "none_with_values",
|
|
config: map[string]any{"val": []any{
|
|
map[string]any{"val": "a"},
|
|
map[string]any{"val": "b"},
|
|
}},
|
|
initial: SliceUnmarshalerConfig{Val: None[[]customUnmarshalerStruct]()},
|
|
expectHasVal: true,
|
|
expectVal: []customUnmarshalerStruct{{Val: "a"}, {Val: "b"}},
|
|
},
|
|
{
|
|
name: "absent_key",
|
|
config: map[string]any{},
|
|
initial: SliceUnmarshalerConfig{Val: None[[]customUnmarshalerStruct]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "null",
|
|
config: map[string]any{"val": map[string]any(nil)},
|
|
initial: SliceUnmarshalerConfig{Val: Some([]customUnmarshalerStruct{{Val: "x"}})},
|
|
expectHasVal: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
conf := confmap.NewFromStringMap(tc.config)
|
|
require.NoError(t, conf.Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestScalarMarshalingRoundTrip(t *testing.T) {
|
|
type strWrapper struct {
|
|
Val Optional[string] `mapstructure:"val"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
initial Optional[string]
|
|
expectAfterHasVal bool
|
|
expectAfterVal string
|
|
}{
|
|
{name: "none", initial: None[string](), expectAfterHasVal: false},
|
|
// Default marshals as nil -> round-trips to None (not Default).
|
|
{name: "default", initial: Default("hello"), expectAfterHasVal: false},
|
|
{name: "some", initial: Some("hello"), expectAfterHasVal: true, expectAfterVal: "hello"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
conf := confmap.New()
|
|
require.NoError(t, conf.Marshal(strWrapper{Val: tc.initial}))
|
|
|
|
var result strWrapper
|
|
require.NoError(t, conf.Unmarshal(&result))
|
|
|
|
require.Equal(t, tc.expectAfterHasVal, result.Val.HasValue())
|
|
if tc.expectAfterHasVal {
|
|
require.Equal(t, tc.expectAfterVal, *result.Val.Get())
|
|
} else {
|
|
require.Nil(t, result.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalFromYAML(t *testing.T) {
|
|
allConf, err := confmaptest.LoadConf("testdata/unmarshal.yaml")
|
|
require.NoError(t, err)
|
|
|
|
sub := func(t *testing.T, key string) *confmap.Conf {
|
|
t.Helper()
|
|
c, err := allConf.Sub(key)
|
|
require.NoError(t, err)
|
|
return c
|
|
}
|
|
|
|
t.Run("struct", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
initial Config[Sub]
|
|
expectHasVal bool
|
|
expectFoo string
|
|
}{
|
|
// None: value present -> Some with provided value.
|
|
{
|
|
name: "none/with_value",
|
|
key: "struct_with_value",
|
|
initial: Config[Sub]{Sub1: None[Sub]()},
|
|
expectHasVal: true,
|
|
expectFoo: "bar",
|
|
},
|
|
// None: null -> stays None.
|
|
{
|
|
name: "none/null",
|
|
key: "struct_null",
|
|
initial: Config[Sub]{Sub1: None[Sub]()},
|
|
expectHasVal: false,
|
|
},
|
|
// None: empty map -> Some with zero value.
|
|
{
|
|
name: "none/empty",
|
|
key: "struct_empty",
|
|
initial: Config[Sub]{Sub1: None[Sub]()},
|
|
expectHasVal: true,
|
|
expectFoo: "",
|
|
},
|
|
// None: absent key -> stays None.
|
|
{
|
|
name: "none/absent",
|
|
key: "struct_absent",
|
|
initial: Config[Sub]{Sub1: None[Sub]()},
|
|
expectHasVal: false,
|
|
},
|
|
// Default: value present -> Some, input value overrides default.
|
|
{
|
|
name: "default/with_value",
|
|
key: "struct_with_value",
|
|
initial: Config[Sub]{Sub1: Default(subDefault)},
|
|
expectHasVal: true,
|
|
expectFoo: "bar",
|
|
},
|
|
// Default: null -> Some, default value applies.
|
|
{
|
|
name: "default/null",
|
|
key: "struct_null",
|
|
initial: Config[Sub]{Sub1: Default(subDefault)},
|
|
expectHasVal: true,
|
|
expectFoo: "foobar",
|
|
},
|
|
// Default: empty map -> Some, default value applies.
|
|
{
|
|
name: "default/empty",
|
|
key: "struct_empty",
|
|
initial: Config[Sub]{Sub1: Default(subDefault)},
|
|
expectHasVal: true,
|
|
expectFoo: "foobar",
|
|
},
|
|
// Default: absent key -> stays None (HasValue false).
|
|
{
|
|
name: "default/absent",
|
|
key: "struct_absent",
|
|
initial: Config[Sub]{Sub1: Default(subDefault)},
|
|
expectHasVal: false,
|
|
},
|
|
// Some: null -> keeps existing value.
|
|
{
|
|
name: "some/null",
|
|
key: "struct_null",
|
|
initial: Config[Sub]{Sub1: Some(Sub{Foo: "foobar"})},
|
|
expectHasVal: true,
|
|
expectFoo: "foobar",
|
|
},
|
|
// Some: value present -> input value overrides existing.
|
|
{
|
|
name: "some/with_value",
|
|
key: "struct_with_value",
|
|
initial: Config[Sub]{Sub1: Some(Sub{Foo: "foobar"})},
|
|
expectHasVal: true,
|
|
expectFoo: "bar",
|
|
},
|
|
// Some: absent key -> unchanged.
|
|
{
|
|
name: "some/absent",
|
|
key: "struct_absent",
|
|
initial: Config[Sub]{Sub1: Some(Sub{Foo: "foobar"})},
|
|
expectHasVal: true,
|
|
expectFoo: "foobar",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
require.NoError(t, sub(t, tc.key).Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Sub1.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectFoo, cfg.Sub1.Get().Foo)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("scalar", func(t *testing.T) {
|
|
type IntConfig struct {
|
|
Val Optional[int] `mapstructure:"val"`
|
|
}
|
|
type StrConfig struct {
|
|
Val Optional[string] `mapstructure:"val"`
|
|
}
|
|
type SliceIntConfig struct {
|
|
Val Optional[[]int] `mapstructure:"val"`
|
|
}
|
|
|
|
t.Run("int", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
initial IntConfig
|
|
expectHasVal bool
|
|
expectVal int
|
|
}{
|
|
// Present value overrides all initial flavors.
|
|
{
|
|
name: "none/with_value",
|
|
key: "int_with_value",
|
|
initial: IntConfig{Val: None[int]()},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
{
|
|
name: "default/with_value",
|
|
key: "int_with_value",
|
|
initial: IntConfig{Val: Default(5)},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
{
|
|
name: "some/with_value",
|
|
key: "int_with_value",
|
|
initial: IntConfig{Val: Some(1)},
|
|
expectHasVal: true,
|
|
expectVal: 42,
|
|
},
|
|
// Null explicitly clears to None.
|
|
{
|
|
name: "some/null",
|
|
key: "int_null",
|
|
initial: IntConfig{Val: Some(3)},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "default/null",
|
|
key: "int_null",
|
|
initial: IntConfig{Val: Default(5)},
|
|
expectHasVal: false,
|
|
},
|
|
// Absent key leaves Optional unchanged.
|
|
{
|
|
name: "none/absent",
|
|
key: "int_absent",
|
|
initial: IntConfig{Val: None[int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some/absent",
|
|
key: "int_absent",
|
|
initial: IntConfig{Val: Some(3)},
|
|
expectHasVal: true,
|
|
expectVal: 3,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
require.NoError(t, sub(t, tc.key).Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("string", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
initial StrConfig
|
|
expectHasVal bool
|
|
expectVal string
|
|
}{
|
|
{
|
|
name: "none/with_value",
|
|
key: "str_with_value",
|
|
initial: StrConfig{Val: None[string]()},
|
|
expectHasVal: true,
|
|
expectVal: "hello",
|
|
},
|
|
{
|
|
name: "default/with_value",
|
|
key: "str_with_value",
|
|
initial: StrConfig{Val: Default("default")},
|
|
expectHasVal: true,
|
|
expectVal: "hello",
|
|
},
|
|
{
|
|
name: "some/with_value",
|
|
key: "str_with_value",
|
|
initial: StrConfig{Val: Some("old")},
|
|
expectHasVal: true,
|
|
expectVal: "hello",
|
|
},
|
|
{
|
|
name: "none/null",
|
|
key: "int_null",
|
|
initial: StrConfig{Val: None[string]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some/null",
|
|
key: "int_null",
|
|
initial: StrConfig{Val: Some("old")},
|
|
expectHasVal: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
require.NoError(t, sub(t, tc.key).Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("slice_of_ints", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
initial SliceIntConfig
|
|
expectHasVal bool
|
|
expectVal []int
|
|
}{
|
|
{
|
|
name: "none/with_values",
|
|
key: "slice_with_values",
|
|
initial: SliceIntConfig{Val: None[[]int]()},
|
|
expectHasVal: true,
|
|
expectVal: []int{1, 2, 3},
|
|
},
|
|
{
|
|
name: "default/with_values",
|
|
key: "slice_with_values",
|
|
initial: SliceIntConfig{Val: Default([]int{9})},
|
|
expectHasVal: true,
|
|
expectVal: []int{1, 2, 3},
|
|
},
|
|
{
|
|
name: "some/null",
|
|
key: "int_null",
|
|
initial: SliceIntConfig{Val: Some([]int{1, 2})},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "none/absent",
|
|
key: "int_absent",
|
|
initial: SliceIntConfig{Val: None[[]int]()},
|
|
expectHasVal: false,
|
|
},
|
|
{
|
|
name: "some/absent",
|
|
key: "int_absent",
|
|
initial: SliceIntConfig{Val: Some([]int{1, 2})},
|
|
expectHasVal: true,
|
|
expectVal: []int{1, 2},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := tc.initial
|
|
require.NoError(t, sub(t, tc.key).Unmarshal(&cfg))
|
|
require.Equal(t, tc.expectHasVal, cfg.Val.HasValue())
|
|
if tc.expectHasVal {
|
|
require.Equal(t, tc.expectVal, *cfg.Val.Get())
|
|
} else {
|
|
require.Nil(t, cfg.Val.Get())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestOptionalFileValidate(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
variant string
|
|
cfg func() validatedConfig
|
|
err error
|
|
}{
|
|
{
|
|
name: "valid default with just key set and no subfields",
|
|
variant: "implicit",
|
|
cfg: newDefaultValidatedConfig,
|
|
},
|
|
{
|
|
name: "valid default with keys set in default",
|
|
variant: "explicit",
|
|
cfg: newDefaultValidatedConfig,
|
|
},
|
|
{
|
|
name: "invalid config",
|
|
variant: "invalid",
|
|
cfg: newDefaultValidatedConfig,
|
|
err: errors.New("default: field `string_val` cannot be set to `invalid`\nsome: nested: field `string_val` cannot be set to `invalid`"),
|
|
},
|
|
{
|
|
name: "invalid default throws an error",
|
|
variant: "implicit",
|
|
cfg: newInvalidDefaultConfig,
|
|
err: errors.New("default: field `string_val` cannot be set to `invalid`"),
|
|
},
|
|
{
|
|
name: "invalid default does not throw an error when key is not set",
|
|
variant: "no_default",
|
|
cfg: newInvalidDefaultConfig,
|
|
},
|
|
{
|
|
name: "invalid default invalid default does not throw an error when the value is overridden",
|
|
variant: "explicit",
|
|
cfg: newInvalidDefaultConfig,
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
conf, err := confmaptest.LoadConf(fmt.Sprintf("testdata/validate_%s.yaml", tt.variant))
|
|
require.NoError(t, err)
|
|
|
|
cfg := tt.cfg()
|
|
|
|
err = conf.Unmarshal(&cfg)
|
|
require.NoError(t, err)
|
|
|
|
err = confmap.Validate(cfg)
|
|
if tt.err == nil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.EqualError(t, err, tt.err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|