diff --git a/pkg/ottl/ottlfuncs/func_flatten_test.go b/pkg/ottl/ottlfuncs/func_flatten_test.go index 51f2ee7c97d5..9e00068c2ccc 100644 --- a/pkg/ottl/ottlfuncs/func_flatten_test.go +++ b/pkg/ottl/ottlfuncs/func_flatten_test.go @@ -5,6 +5,7 @@ package ottlfuncs import ( "context" + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -34,47 +35,43 @@ func Test_flatten(t *testing.T) { }, }, { - name: "conflicting map", + name: "nested map", target: map[string]any{ "address": map[string]any{ - "street": map[string]any{ - "house": int64(1234), - }, - }, - "address.street": map[string]any{ - "house": int64(1235), + "street": "first", + "house": int64(1234), }, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.street.house": int64(1235), + "address.street": "first", + "address.house": int64(1234), }, }, { - name: "conflicting slice", + name: "nested slice", target: map[string]any{ - "address": map[string]any{ - "street": []any{"first"}, - "house": int64(1234), + "occupants": []any{ + "user 1", + "user 2", }, - "address.street": []any{"second"}, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.house": int64(1234), - "address.street.0": "second", + "occupants.0": "user 1", + "occupants.1": "user 2", }, }, { - name: "conflicting map with nested slice", + name: "combination", target: map[string]any{ + "name": "test", "address": map[string]any{ "street": "first", "house": int64(1234), }, - "address.street": "second", "occupants": []any{ "user 1", "user 2", @@ -83,189 +80,130 @@ func Test_flatten(t *testing.T) { prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.street": "second", + "name": "test", + "address.street": "first", "address.house": int64(1234), "occupants.0": "user 1", "occupants.1": "user 2", }, }, { - name: "conflicting map with nested slice in conflicting item", + name: "deep nesting", target: map[string]any{ - "address": map[string]any{ - "street": map[string]any{ - "number": "first", + "1": map[string]any{ + "2": map[string]any{ + "3": map[string]any{ + "4": "5", + }, }, - "house": int64(1234), - }, - "address.street": map[string]any{ - "number": []any{"second", "third"}, - }, - "address.street.number": "fourth", - "occupants": []any{ - "user 1", - "user 2", }, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.street.number": "fourth", - "address.house": int64(1234), - "address.street.number.0": "second", - "address.street.number.1": "third", - "occupants.0": "user 1", - "occupants.1": "user 2", + "1.2.3.4": "5", }, }, { - name: "nested map", + name: "use prefix", target: map[string]any{ + "name": "test", "address": map[string]any{ "street": "first", "house": int64(1234), }, - }, - prefix: ottl.Optional[string]{}, - depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "address.street": "first", - "address.house": int64(1234), - }, - }, - { - name: "nested slice", - target: map[string]any{ "occupants": []any{ "user 1", "user 2", }, }, - prefix: ottl.Optional[string]{}, - depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "occupants.0": "user 1", - "occupants.1": "user 2", - }, - }, - { - name: "simple - conflict on", - target: map[string]any{ - "name": "test", - }, - prefix: ottl.Optional[string]{}, + prefix: ottl.NewTestingOptional[string]("app"), depth: ottl.Optional[int64]{}, expected: map[string]any{ - "name": "test", + "app.name": "test", + "app.address.street": "first", + "app.address.house": int64(1234), + "app.occupants.0": "user 1", + "app.occupants.1": "user 2", }, - conflict: true, }, { - name: "nested map - conflict on", + name: "max depth", target: map[string]any{ - "address": map[string]any{ - "street": "first", - "house": int64(1234), + "0": map[string]any{ + "1": map[string]any{ + "2": map[string]any{ + "3": "value", + }, + }, }, }, prefix: ottl.Optional[string]{}, - depth: ottl.Optional[int64]{}, + depth: ottl.NewTestingOptional[int64](2), expected: map[string]any{ - "address.street": "first", - "address.house": int64(1234), + "0.1.2": map[string]any{ + "3": "value", + }, }, - conflict: true, }, { - name: "conflicting map - conflict on", + name: "max depth with slice", target: map[string]any{ - "address": map[string]any{ - "street": map[string]any{ - "house": int64(1234), + "0": map[string]any{ + "1": map[string]any{ + "2": map[string]any{ + "3": "value", + }, }, }, - "address.street": map[string]any{ - "house": int64(1235), + "1": map[string]any{ + "1": []any{ + map[string]any{ + "1": "value", + }, + }, }, }, prefix: ottl.Optional[string]{}, - depth: ottl.Optional[int64]{}, + depth: ottl.NewTestingOptional[int64](1), expected: map[string]any{ - "address.street.house": int64(1234), - "address.street.house.0": int64(1235), + "0.1": map[string]any{ + "2": map[string]any{ + "3": "value", + }, + }, + "1.1": []any{ + map[string]any{ + "1": "value", + }, + }, }, - conflict: true, }, { - name: "conflicting slice - conflict on", + name: "simple - conflict on", target: map[string]any{ - "address": map[string]any{ - "street": []any{"first"}, - "house": int64(1234), - }, - "address.street": []any{"second"}, + "name": "test", }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.street": "first", - "address.house": int64(1234), - "address.street.0": "second", + "name": "test", }, conflict: true, }, { - name: "conflicting map with nested slice - conflict on", + name: "nested map - conflict on", target: map[string]any{ "address": map[string]any{ "street": "first", "house": int64(1234), }, - "address.street": "second", - "occupants": []any{ - "user 1", - "user 2", - }, - }, - prefix: ottl.Optional[string]{}, - depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "address.street": "first", - "address.house": int64(1234), - "address.street.0": "second", - "occupants": "user 1", - "occupants.0": "user 2", - }, - conflict: true, - }, - { - name: "conflicting map with nested slice in conflicting item - conflict on", - target: map[string]any{ - "address": map[string]any{ - "street": map[string]any{ - "number": "first", - }, - "house": int64(1234), - }, - "address.street": map[string]any{ - "number": []any{"second", "third"}, - }, - "address.street.number": "fourth", - "occupants": []any{ - "user 1", - "user 2", - }, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, expected: map[string]any{ - "address.street.number": "first", - "address.house": int64(1234), - "address.street.number.0": "second", - "address.street.number.1": "third", - "occupants": "user 1", - "occupants.0": "user 2", - "address.street.number.2": "fourth", + "address.street": "first", + "address.house": int64(1234), }, conflict: true, }, @@ -371,120 +309,155 @@ func Test_flatten(t *testing.T) { }, conflict: true, }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := pcommon.NewMap() + err := m.FromRaw(tt.target) + assert.NoError(t, err) + target := ottl.StandardPMapGetter[any]{ + Getter: func(_ context.Context, _ any) (any, error) { + return m, nil + }, + } + + exprFunc, err := flatten[any](target, tt.prefix, tt.depth, ottl.NewTestingOptional[bool](tt.conflict)) + assert.NoError(t, err) + _, err = exprFunc(nil, nil) + assert.NoError(t, err) + + assert.Equal(t, tt.expected, m.AsRaw()) + }) + } +} + +func Test_flatten_undeterministic(t *testing.T) { + tests := []struct { + name string + target map[string]any + prefix ottl.Optional[string] + depth ottl.Optional[int64] + expectedKeys []string + expectedValues []any + conflict bool + }{ { - name: "combination", + name: "conflicting map - conflict on", target: map[string]any{ - "name": "test", "address": map[string]any{ - "street": "first", - "house": int64(1234), + "street": map[string]any{ + "house": int64(1234), + }, }, - "occupants": []any{ - "user 1", - "user 2", + "address.street": map[string]any{ + "house": int64(1235), }, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "name": "test", - "address.street": "first", - "address.house": int64(1234), - "occupants.0": "user 1", - "occupants.1": "user 2", + expectedKeys: []string{ + "address.street.house", + "address.street.house.0", }, + expectedValues: []any{ + int64(1234), + int64(1235), + }, + conflict: true, }, { - name: "deep nesting", + name: "conflicting slice - conflict on", target: map[string]any{ - "1": map[string]any{ - "2": map[string]any{ - "3": map[string]any{ - "4": "5", - }, - }, + "address": map[string]any{ + "street": []any{"first"}, + "house": int64(1234), }, + "address.street": []any{"second"}, }, prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "1.2.3.4": "5", + expectedKeys: []string{ + "address.street", + "address.house", + "address.street.0", + }, + expectedValues: []any{ + int64(1234), + "second", + "first", }, + conflict: true, }, { - name: "use prefix", + name: "conflicting map with nested slice - conflict on", target: map[string]any{ - "name": "test", "address": map[string]any{ "street": "first", "house": int64(1234), }, + "address.street": "second", "occupants": []any{ "user 1", "user 2", }, }, - prefix: ottl.NewTestingOptional[string]("app"), + prefix: ottl.Optional[string]{}, depth: ottl.Optional[int64]{}, - expected: map[string]any{ - "app.name": "test", - "app.address.street": "first", - "app.address.house": int64(1234), - "app.occupants.0": "user 1", - "app.occupants.1": "user 2", + expectedKeys: []string{ + "address.street", + "address.house", + "address.street.0", + "occupants", + "occupants.0", + }, + expectedValues: []any{ + int64(1234), + "second", + "first", + "user 1", + "user 2", }, + conflict: true, }, { - name: "max depth", + name: "conflicting map with nested slice in conflicting item - conflict on", target: map[string]any{ - "0": map[string]any{ - "1": map[string]any{ - "2": map[string]any{ - "3": "value", - }, + "address": map[string]any{ + "street": map[string]any{ + "number": "first", }, + "house": int64(1234), }, - }, - prefix: ottl.Optional[string]{}, - depth: ottl.NewTestingOptional[int64](2), - expected: map[string]any{ - "0.1.2": map[string]any{ - "3": "value", - }, - }, - }, - { - name: "max depth with slice", - target: map[string]any{ - "0": map[string]any{ - "1": map[string]any{ - "2": map[string]any{ - "3": "value", - }, - }, + "address.street": map[string]any{ + "number": []any{"second", "third"}, }, - "1": map[string]any{ - "1": []any{ - map[string]any{ - "1": "value", - }, - }, + "address.street.number": "fourth", + "occupants": []any{ + "user 1", + "user 2", }, }, prefix: ottl.Optional[string]{}, - depth: ottl.NewTestingOptional[int64](1), - expected: map[string]any{ - "0.1": map[string]any{ - "2": map[string]any{ - "3": "value", - }, - }, - "1.1": []any{ - map[string]any{ - "1": "value", - }, - }, + depth: ottl.Optional[int64]{}, + expectedKeys: []string{ + "address.street.number", + "address.house", + "address.street.number.0", + "address.street.number.1", + "occupants", + "occupants.0", + "address.street.number.2", + }, + expectedValues: []any{ + int64(1234), + "second", + "first", + "third", + "fourth", + "user 1", + "user 2", }, + conflict: true, }, } for _, tt := range tests { @@ -503,7 +476,10 @@ func Test_flatten(t *testing.T) { _, err = exprFunc(nil, nil) assert.NoError(t, err) - assert.Equal(t, tt.expected, m.AsRaw()) + keys, val := extractKeysAndValues(m.AsRaw()) + + assert.True(t, compareSlices(keys, tt.expectedKeys)) + assert.True(t, compareSlices(val, tt.expectedValues)) }) } } @@ -547,3 +523,32 @@ func Test_flatten_bad_depth(t *testing.T) { }) } } + +func extractKeysAndValues(m map[string]any) ([]string, []any) { + keys := make([]string, 0, len(m)) + values := make([]any, 0, len(m)) + for key, value := range m { + keys = append(keys, key) + values = append(values, value) + } + return keys, values +} + +func compareSlices[K string | any](a, b []K) bool { + if len(a) != len(b) { + return false + } + + aMap := make(map[any]int) + bMap := make(map[any]int) + + for _, item := range a { + aMap[item]++ + } + + for _, item := range b { + bMap[item]++ + } + + return reflect.DeepEqual(aMap, bMap) +}