463 lines
9.6 KiB
Go
463 lines
9.6 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package configoptional
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.opentelemetry.io/collector/confmap"
|
|
)
|
|
|
|
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 subDefault = Sub{
|
|
Foo: "foobar",
|
|
}
|
|
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|
|
|
|
func TestDefaultPanics(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
_ = Default(1)
|
|
})
|
|
|
|
assert.Panics(t, func() {
|
|
_ = Default(ptr(1))
|
|
})
|
|
|
|
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.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())
|
|
}
|
|
|
|
func TestNone(t *testing.T) {
|
|
none := None[Sub]()
|
|
require.False(t, none.HasValue())
|
|
require.Nil(t, none.Get())
|
|
}
|
|
|
|
func TestSome(t *testing.T) {
|
|
some := Some(Sub{
|
|
Foo: "foobar",
|
|
})
|
|
require.True(t, some.HasValue())
|
|
assert.Equal(t, "foobar", some.Get().Foo)
|
|
}
|
|
|
|
func TestDefault(t *testing.T) {
|
|
defaultSub := Default(&subDefault)
|
|
require.False(t, defaultSub.HasValue())
|
|
require.Nil(t, defaultSub.Get())
|
|
}
|
|
|
|
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 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"`
|
|
}
|
|
|
|
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 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())
|
|
})
|
|
}
|
|
}
|