537 lines
10 KiB
Go
537 lines
10 KiB
Go
package yaml
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAppendProcessor_MergeWithAppend(t *testing.T) {
|
|
processor := NewAppendProcessor()
|
|
|
|
tests := []struct {
|
|
name string
|
|
dest map[string]any
|
|
src map[string]any
|
|
expected map[string]any
|
|
}{
|
|
{
|
|
name: "append to existing slice",
|
|
dest: map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
map[string]any{
|
|
"key": "b",
|
|
},
|
|
},
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{
|
|
map[string]any{
|
|
"key": "c",
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
map[string]any{
|
|
"key": "b",
|
|
},
|
|
map[string]any{
|
|
"key": "c",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nested append",
|
|
dest: map[string]any{
|
|
"config": map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
src: map[string]any{
|
|
"config": map[string]any{
|
|
"values+": []any{
|
|
map[string]any{
|
|
"key": "b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"config": map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
map[string]any{
|
|
"key": "b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "create new slice if not exists",
|
|
dest: map[string]any{},
|
|
src: map[string]any{
|
|
"values+": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nested append with non-existent parent",
|
|
dest: map[string]any{},
|
|
src: map[string]any{
|
|
"kube-state-metrics": map[string]any{
|
|
"prometheus": map[string]any{
|
|
"monitor": map[string]any{
|
|
"metricRelabelings+": []any{
|
|
map[string]any{
|
|
"action": "labeldrop",
|
|
"regex": "info_.*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"kube-state-metrics": map[string]any{
|
|
"prometheus": map[string]any{
|
|
"monitor": map[string]any{
|
|
"metricRelabelings": []any{
|
|
map[string]any{
|
|
"action": "labeldrop",
|
|
"regex": "info_.*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "overwrite non-slice with append",
|
|
dest: map[string]any{
|
|
"values": "not a slice",
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{
|
|
map[string]any{
|
|
"key": "a",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "type collision - []string vs []any",
|
|
dest: map[string]any{
|
|
"values": []string{"a", "b"},
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{"c", "d"},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{"c", "d"},
|
|
},
|
|
},
|
|
{
|
|
name: "type collision - []int vs []any",
|
|
dest: map[string]any{
|
|
"values": []int{1, 2},
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{3, 4},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{3, 4},
|
|
},
|
|
},
|
|
{
|
|
name: "append on map - should overwrite",
|
|
dest: map[string]any{
|
|
"config": map[string]any{"key": "value"},
|
|
},
|
|
src: map[string]any{
|
|
"config+": []any{"new", "values"},
|
|
},
|
|
expected: map[string]any{
|
|
"config": []any{"new", "values"},
|
|
},
|
|
},
|
|
{
|
|
name: "append on scalar - should overwrite",
|
|
dest: map[string]any{
|
|
"version": "1.0.0",
|
|
},
|
|
src: map[string]any{
|
|
"version+": []any{"2.0.0", "3.0.0"},
|
|
},
|
|
expected: map[string]any{
|
|
"version": []any{"2.0.0", "3.0.0"},
|
|
},
|
|
},
|
|
{
|
|
name: "nil slice in destination",
|
|
dest: map[string]any{
|
|
"values": nil,
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{"a", "b"},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{"a", "b"},
|
|
},
|
|
},
|
|
{
|
|
name: "empty slice in destination",
|
|
dest: map[string]any{
|
|
"values": []any{},
|
|
},
|
|
src: map[string]any{
|
|
"values+": []any{"a", "b"},
|
|
},
|
|
expected: map[string]any{
|
|
"values": []any{"a", "b"},
|
|
},
|
|
},
|
|
{
|
|
name: "nil slice in source",
|
|
dest: map[string]any{
|
|
"values": []any{"existing"},
|
|
},
|
|
src: map[string]any{
|
|
"values+": nil,
|
|
},
|
|
expected: map[string]any{
|
|
"values": nil,
|
|
},
|
|
},
|
|
{
|
|
name: "mixed types in slices",
|
|
dest: map[string]any{
|
|
"data": []any{"string", 42, true},
|
|
},
|
|
src: map[string]any{
|
|
"data+": []any{"new", 100, false},
|
|
},
|
|
expected: map[string]any{
|
|
"data": []any{"string", 42, true, "new", 100, false},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple append keys",
|
|
dest: map[string]any{
|
|
"list1": []any{"a"},
|
|
"list2": []any{"x"},
|
|
},
|
|
src: map[string]any{
|
|
"list1+": []any{"b"},
|
|
"list2+": []any{"y"},
|
|
},
|
|
expected: map[string]any{
|
|
"list1": []any{"a", "b"},
|
|
"list2": []any{"x", "y"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srcCopy := make(map[string]any)
|
|
for k, v := range tt.src {
|
|
srcCopy[k] = v
|
|
}
|
|
|
|
err := processor.MergeWithAppend(tt.dest, tt.src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, tt.dest)
|
|
|
|
assert.Equal(t, srcCopy, tt.src, "source map should not be mutated")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendProcessor_EdgeCases(t *testing.T) {
|
|
processor := NewAppendProcessor()
|
|
|
|
t.Run("empty maps", func(t *testing.T) {
|
|
dest := make(map[string]any)
|
|
src := make(map[string]any)
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, dest)
|
|
})
|
|
|
|
t.Run("nil maps", func(t *testing.T) {
|
|
var dest map[string]any
|
|
var src map[string]any
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, dest)
|
|
})
|
|
|
|
t.Run("deep nested with append", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"level1": map[string]any{
|
|
"level2": map[string]any{
|
|
"level3": map[string]any{
|
|
"values": []any{"deep"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
src := map[string]any{
|
|
"level1": map[string]any{
|
|
"level2": map[string]any{
|
|
"level3": map[string]any{
|
|
"values+": []any{"nested"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expected := map[string]any{
|
|
"level1": map[string]any{
|
|
"level2": map[string]any{
|
|
"level3": map[string]any{
|
|
"values": []any{"deep", "nested"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, dest)
|
|
})
|
|
|
|
t.Run("complex nested structure", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"config": map[string]any{
|
|
"services": []any{
|
|
map[string]any{"name": "service1"},
|
|
},
|
|
"settings": map[string]any{
|
|
"timeout": 30,
|
|
},
|
|
},
|
|
}
|
|
src := map[string]any{
|
|
"config": map[string]any{
|
|
"services+": []any{
|
|
map[string]any{"name": "service2"},
|
|
},
|
|
"settings": map[string]any{
|
|
"retries": 3,
|
|
},
|
|
},
|
|
}
|
|
expected := map[string]any{
|
|
"config": map[string]any{
|
|
"services": []any{
|
|
map[string]any{"name": "service1"},
|
|
map[string]any{"name": "service2"},
|
|
},
|
|
"settings": map[string]any{
|
|
"timeout": 30,
|
|
"retries": 3,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, dest)
|
|
})
|
|
}
|
|
|
|
func TestAppendProcessor_TypeConversions(t *testing.T) {
|
|
processor := NewAppendProcessor()
|
|
|
|
t.Run("map[any]any to map[string]any conversion", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"values": []any{"a"},
|
|
}
|
|
src := map[any]any{
|
|
"values+": []any{"b"},
|
|
}
|
|
|
|
srcConverted := make(map[string]any)
|
|
for k, v := range src {
|
|
if ks, ok := k.(string); ok {
|
|
srcConverted[ks] = v
|
|
}
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, srcConverted)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []any{"a", "b"}, dest["values"])
|
|
})
|
|
|
|
t.Run("mixed key types", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"values": []any{"a"},
|
|
}
|
|
src := map[any]any{
|
|
"values+": []any{"b"},
|
|
"other": "value",
|
|
42: "number_key",
|
|
}
|
|
|
|
srcConverted := make(map[string]any)
|
|
for k, v := range src {
|
|
if ks, ok := k.(string); ok {
|
|
srcConverted[ks] = v
|
|
}
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, srcConverted)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []any{"a", "b"}, dest["values"])
|
|
assert.Equal(t, "value", dest["other"])
|
|
assert.NotContains(t, dest, "42")
|
|
})
|
|
}
|
|
|
|
func TestAppendProcessor_PropertyBased(t *testing.T) {
|
|
processor := NewAppendProcessor()
|
|
|
|
t.Run("idempotent regular merge", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"key1": "value1",
|
|
"key2": []any{"a", "b"},
|
|
}
|
|
src := map[string]any{
|
|
"key1": "value1",
|
|
"key3": "value3",
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
|
|
err = processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
|
|
expected := map[string]any{
|
|
"key1": "value1",
|
|
"key2": []any{"a", "b"},
|
|
"key3": "value3",
|
|
}
|
|
assert.Equal(t, expected, dest)
|
|
})
|
|
|
|
t.Run("append is not idempotent", func(t *testing.T) {
|
|
dest := map[string]any{
|
|
"values": []any{"a"},
|
|
}
|
|
src := map[string]any{
|
|
"values+": []any{"b"},
|
|
}
|
|
|
|
err := processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []any{"a", "b"}, dest["values"])
|
|
|
|
err = processor.MergeWithAppend(dest, src)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []any{"a", "b", "b"}, dest["values"])
|
|
})
|
|
|
|
t.Run("merge is not commutative", func(t *testing.T) {
|
|
map1 := map[string]any{"a": 1, "b": 2}
|
|
map2 := map[string]any{"b": 3, "c": 4}
|
|
|
|
result1 := make(map[string]any)
|
|
for k, v := range map1 {
|
|
result1[k] = v
|
|
}
|
|
err := processor.MergeWithAppend(result1, map2)
|
|
require.NoError(t, err)
|
|
|
|
result2 := make(map[string]any)
|
|
for k, v := range map2 {
|
|
result2[k] = v
|
|
}
|
|
err = processor.MergeWithAppend(result2, map1)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, result1, result2)
|
|
|
|
expected1 := map[string]any{"a": 1, "b": 3, "c": 4}
|
|
expected2 := map[string]any{"a": 1, "b": 2, "c": 4}
|
|
assert.Equal(t, expected1, result1)
|
|
assert.Equal(t, expected2, result2)
|
|
})
|
|
}
|
|
|
|
func TestIsAppendKey(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
expected bool
|
|
}{
|
|
{"key+", true},
|
|
{"key", false},
|
|
{"key++", true},
|
|
{"+key", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
result := IsAppendKey(tt.key)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetBaseKey(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
expected string
|
|
}{
|
|
{"key+", "key"},
|
|
{"key", "key"},
|
|
{"key++", "key+"},
|
|
{"+key", "+key"},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
result := GetBaseKey(tt.key)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|