Skip to content

Commit

Permalink
Add unit tests for runtime API conversions
Browse files Browse the repository at this point in the history
Signed-off-by: Alper Rifat Ulucinar <[email protected]>
  • Loading branch information
ulucinar committed May 7, 2024
1 parent b2b515f commit 1543b80
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 7 deletions.
2 changes: 1 addition & 1 deletion pkg/config/conversion/conversions_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

Expand Down
15 changes: 12 additions & 3 deletions pkg/config/conversion/runtime_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package conversion

import (
"reflect"
"slices"
"sort"
"strings"
Expand All @@ -29,6 +30,11 @@ const (
ToSingletonList
)

const (
errFmtMultiItemList = "singleton list, at the field path %s, must have a length of at most 1 but it has a length of %d"
errFmtNonSlice = "value at the field path %s must be []any, not %q"
)

// String returns a string representation of the conversion mode.
func (m Mode) String() string {
switch m {
Expand Down Expand Up @@ -104,9 +110,12 @@ func Convert(params map[string]any, paths []string, mode Mode) (map[string]any,
if v != nil {
newVal = map[string]any{}
s, ok := v.([]any)
if !ok || len(s) > 1 {
// if len(s) is 0, then it's not a slice
return nil, errors.Errorf("singleton list, at the field path %s, must have a length of at most 1 but it has a length of %d", e, len(s))
if !ok {
// then it's not a slice
return nil, errors.Errorf(errFmtNonSlice, e, reflect.TypeOf(v))
}
if len(s) > 1 {
return nil, errors.Errorf(errFmtMultiItemList, e, len(s))
}
if len(s) > 0 {
newVal = s[0]
Expand Down
332 changes: 332 additions & 0 deletions pkg/config/conversion/runtime_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package conversion

import (
"reflect"
"testing"

"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)

func TestConvert(t *testing.T) {
type args struct {
params map[string]any
paths []string
mode Mode
}
type want struct {
err error
params map[string]any
}
tests := map[string]struct {
reason string
args args
want want
}{
"NilParamsAndPaths": {
reason: "Conversion on an nil map should not fail.",
args: args{},
},
"EmptyPaths": {
reason: "Empty conversion on a map should be an identity function.",
args: args{
params: map[string]any{"a": "b"},
},
want: want{
params: map[string]any{"a": "b"},
},
},
"SingletonListToEmbeddedObject": {
reason: "Should successfully convert a singleton list at the root level to an embedded object.",
args: args{
params: map[string]any{
"l": []map[string]any{
{
"k": "v",
},
},
},
paths: []string{"l"},
mode: ToEmbeddedObject,
},
want: want{
params: map[string]any{
"l": map[string]any{
"k": "v",
},
},
},
},
"NestedSingletonListsToEmbeddedObjectsPathsInLexicalOrder": {
reason: "Should successfully convert the parent & nested singleton lists to embedded objects. Paths specified in lexical order.",
args: args{
params: map[string]any{
"parent": []map[string]any{
{
"child": []map[string]any{
{
"k": "v",
},
},
},
},
},
paths: []string{"parent", "parent[*].child"},
mode: ToEmbeddedObject,
},
want: want{
params: map[string]any{
"parent": map[string]any{
"child": map[string]any{
"k": "v",
},
},
},
},
},
"NestedSingletonListsToEmbeddedObjectsPathsInReverseLexicalOrder": {
reason: "Should successfully convert the parent & nested singleton lists to embedded objects. Paths specified in reverse-lexical order.",
args: args{
params: map[string]any{
"parent": []map[string]any{
{
"child": []map[string]any{
{
"k": "v",
},
},
},
},
},
paths: []string{"parent[*].child", "parent"},
mode: ToEmbeddedObject,
},
want: want{
params: map[string]any{
"parent": map[string]any{
"child": map[string]any{
"k": "v",
},
},
},
},
},
"EmbeddedObjectToSingletonList": {
reason: "Should successfully convert an embedded object at the root level to a singleton list.",
args: args{
params: map[string]any{
"l": map[string]any{
"k": "v",
},
},
paths: []string{"l"},
mode: ToSingletonList,
},
want: want{
params: map[string]any{
"l": []map[string]any{
{
"k": "v",
},
},
},
},
},
"NestedEmbeddedObjectsToSingletonListInLexicalOrder": {
reason: "Should successfully convert the parent & nested embedded objects to singleton lists. Paths are specified in lexical order.",
args: args{
params: map[string]any{
"parent": map[string]any{
"child": map[string]any{
"k": "v",
},
},
},
paths: []string{"parent", "parent[*].child"},
mode: ToSingletonList,
},
want: want{
params: map[string]any{
"parent": []map[string]any{
{
"child": []map[string]any{
{
"k": "v",
},
},
},
},
},
},
},
"NestedEmbeddedObjectsToSingletonListInReverseLexicalOrder": {
reason: "Should successfully convert the parent & nested embedded objects to singleton lists. Paths are specified in reverse-lexical order.",
args: args{
params: map[string]any{
"parent": map[string]any{
"child": map[string]any{
"k": "v",
},
},
},
paths: []string{"parent[*].child", "parent"},
mode: ToSingletonList,
},
want: want{
params: map[string]any{
"parent": []map[string]any{
{
"child": []map[string]any{
{
"k": "v",
},
},
},
},
},
},
},
"FailConversionOfAMultiItemList": {
reason: `Conversion of a multi-item list in mode "ToEmbeddedObject" should fail.`,
args: args{
params: map[string]any{
"l": []map[string]any{
{
"k1": "v1",
},
{
"k2": "v2",
},
},
},
paths: []string{"l"},
mode: ToEmbeddedObject,
},
want: want{
err: errors.Errorf(errFmtMultiItemList, "l", 2),
},
},
"FailConversionOfNonSlice": {
reason: `Conversion of a non-slice value in mode "ToEmbeddedObject" should fail.`,
args: args{
params: map[string]any{
"l": map[string]any{
"k": "v",
},
},
paths: []string{"l"},
mode: ToEmbeddedObject,
},
want: want{
err: errors.Errorf(errFmtNonSlice, "l", reflect.TypeOf(map[string]any{})),
},
},
"ToSingletonListWithNonExistentPath": {
reason: `"ToSingletonList" mode conversions specifying only non-existent paths should be identity functions.`,
args: args{
params: map[string]any{
"l": map[string]any{
"k": "v",
},
},
paths: []string{"nonexistent"},
mode: ToSingletonList,
},
want: want{
params: map[string]any{
"l": map[string]any{
"k": "v",
},
},
},
},
"ToEmbeddedObjectWithNonExistentPath": {
reason: `"ToEmbeddedObject" mode conversions specifying only non-existent paths should be identity functions.`,
args: args{
params: map[string]any{
"l": []map[string]any{
{
"k": "v",
},
},
},
paths: []string{"nonexistent"},
mode: ToEmbeddedObject,
},
want: want{
params: map[string]any{
"l": []map[string]any{
{
"k": "v",
},
},
},
},
},
}

for n, tt := range tests {
t.Run(n, func(t *testing.T) {
params, err := roundTrip(tt.args.params)
if err != nil {
t.Fatalf("Failed to preprocess tt.args.params: %v", err)
}
wantParams, err := roundTrip(tt.want.params)
if err != nil {
t.Fatalf("Failed to preprocess tt.want.params: %v", err)
}
got, err := Convert(params, tt.args.paths, tt.args.mode)
if diff := cmp.Diff(tt.want.err, err, test.EquateErrors()); diff != "" {
t.Fatalf("\n%s\nConvert(tt.args.params, tt.args.paths): -wantErr, +gotErr:\n%s", tt.reason, diff)
}
if diff := cmp.Diff(wantParams, got); diff != "" {
t.Errorf("\n%s\nConvert(tt.args.params, tt.args.paths): -wantConverted, +gotConverted:\n%s", tt.reason, diff)
}
})
}
}

func TestModeString(t *testing.T) {
tests := map[string]struct {
m Mode
want string
}{
"ToSingletonList": {
m: ToSingletonList,
want: "toSingletonList",
},
"ToEmbeddedObject": {
m: ToEmbeddedObject,
want: "toEmbeddedObject",
},
"Unknown": {
m: ToSingletonList + 1,
want: "unknown",
},
}
for n, tt := range tests {
t.Run(n, func(t *testing.T) {
if diff := cmp.Diff(tt.want, tt.m.String()); diff != "" {
t.Errorf("String(): -want, +got:\n%s", diff)
}
})
}
}

func roundTrip(m map[string]any) (map[string]any, error) {
if len(m) == 0 {
return m, nil
}
buff, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(m)
if err != nil {
return nil, err
}
var r map[string]any
return r, jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(buff, &r)
}
Loading

0 comments on commit 1543b80

Please sign in to comment.