From 45b7a7c231d2fca030c690189e4575bc279c1726 Mon Sep 17 00:00:00 2001 From: Alper Rifat Ulucinar Date: Thu, 25 Jan 2024 22:04:01 +0300 Subject: [PATCH] Add unit tests for the conversion package Signed-off-by: Alper Rifat Ulucinar --- pkg/config/conversion/conversions.go | 32 +++++ pkg/config/conversion/conversions_test.go | 149 ++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 pkg/config/conversion/conversions_test.go diff --git a/pkg/config/conversion/conversions.go b/pkg/config/conversion/conversions.go index f1b03edc..2b22ea09 100644 --- a/pkg/config/conversion/conversions.go +++ b/pkg/config/conversion/conversions.go @@ -13,13 +13,34 @@ import ( ) const ( + // AllVersions denotes that a Conversion is applicable for all versions + // of an API with which the Conversion is registered. It can be used for + // both the conversion source or target API versions. AllVersions = "*" ) +// Conversion is the interface for the API version converters. +// Conversion implementations registered for a source, target +// pair are called in chain so Conversion implementations can be modular, e.g., +// a Conversion implementation registered for a specific source and target +// versions does not have to contain all the needed API conversions between +// these two versions. type Conversion interface { + // Applicable should return true if this Conversion is applicable while + // converting the API of the `src` object to the API of the `dst` object. Applicable(src, dst runtime.Object) bool } +// PavedConversion is an optimized Conversion between two fieldpath.Paved +// objects. PavedConversion implementations for a specific source and target +// version pair are chained together and the source and the destination objects +// are paved once at the beginning of the chained PavedConversion.ConvertPaved +// calls. The target fieldpath.Paved object is then converted into the original +// resource.Terraformed object at the end of the chained calls. This prevents +// the intermediate conversions between fieldpath.Paved and +// the resource.Terraformed representations of the same object, and the +// fieldpath.Paved representation is convenient for writing generic +// Conversion implementations not bound to a specific type. type PavedConversion interface { Conversion // ConvertPaved converts from the `src` paved object to the `dst` @@ -28,6 +49,12 @@ type PavedConversion interface { ConvertPaved(src, target *fieldpath.Paved) (bool, error) } +// TerraformedConversion defines a Conversion from a specific source +// resource.Terraformed type to a target one. Generic Conversion +// implementations may prefer to implement the PavedConversion interface. +// Implementations of TerraformedConversion can do type assertions to +// specific source and target types and so they are expected to be +// strongly typed. type TerraformedConversion interface { Conversion // ConvertTerraformed converts from the `src` managed resource to the `dst` @@ -77,6 +104,11 @@ func (f *fieldCopy) ConvertPaved(src, target *fieldpath.Paved) (bool, error) { return true, errors.Wrapf(target.SetValue(f.targetField, v), "failed to set the field %q of the conversion target object", f.targetField) } +// NewFieldRenameConversion returns a new Conversion that implements a +// field renaming conversion from the specified `sourceVersion` to the specified +// `targetVersion` of an API. The field's name in the `sourceVersion` is given +// with the `sourceField` parameter and its name in the `targetVersion` is +// given with `targetField` parameter. func NewFieldRenameConversion(sourceVersion, sourceField, targetVersion, targetField string) Conversion { return &fieldCopy{ baseConversion: newBaseConversion(sourceVersion, targetVersion), diff --git a/pkg/config/conversion/conversions_test.go b/pkg/config/conversion/conversions_test.go new file mode 100644 index 00000000..33f09a70 --- /dev/null +++ b/pkg/config/conversion/conversions_test.go @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2023 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package conversion + +import ( + "fmt" + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" + "k8s.io/utils/ptr" + + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" +) + +const ( + sourceVersion = "v1beta1" + sourceField = "testSourceField" + targetVersion = "v1beta2" + targetField = "testTargetField" +) + +func TestConvertPaved(t *testing.T) { + type args struct { + sourceVersion string + sourceField string + targetVersion string + targetField string + sourceObj *fieldpath.Paved + targetObj *fieldpath.Paved + } + type want struct { + converted bool + err error + targetObj *fieldpath.Paved + } + tests := map[string]struct { + reason string + args args + want want + }{ + "SuccessfulConversion": { + reason: "Source field in source version is successfully converted to the target field in target version.", + args: args{ + sourceVersion: sourceVersion, + sourceField: sourceField, + targetVersion: targetVersion, + targetField: targetField, + sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), + targetObj: getPaved(targetVersion, targetField, nil), + }, + want: want{ + converted: true, + targetObj: getPaved(targetVersion, targetField, ptr.To("testValue")), + }, + }, + "SuccessfulConversionAllVersions": { + reason: "Source field in source version is successfully converted to the target field in target version when the conversion specifies wildcard version for both of the source and the target.", + args: args{ + sourceVersion: AllVersions, + sourceField: sourceField, + targetVersion: AllVersions, + targetField: targetField, + sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), + targetObj: getPaved(targetVersion, targetField, nil), + }, + want: want{ + converted: true, + targetObj: getPaved(targetVersion, targetField, ptr.To("testValue")), + }, + }, + "SourceVersionMismatch": { + reason: "Conversion is not done if the source version of the object does not match the conversion's source version.", + args: args{ + sourceVersion: "mismatch", + sourceField: sourceField, + targetVersion: AllVersions, + targetField: targetField, + sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), + targetObj: getPaved(targetVersion, targetField, nil), + }, + want: want{ + converted: false, + targetObj: getPaved(targetVersion, targetField, nil), + }, + }, + "TargetVersionMismatch": { + reason: "Conversion is not done if the target version of the object does not match the conversion's target version.", + args: args{ + sourceVersion: AllVersions, + sourceField: sourceField, + targetVersion: "mismatch", + targetField: targetField, + sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), + targetObj: getPaved(targetVersion, targetField, nil), + }, + want: want{ + converted: false, + targetObj: getPaved(targetVersion, targetField, nil), + }, + }, + "SourceFieldNotFound": { + reason: "Conversion is not done if the source field is not found in the source object.", + args: args{ + sourceVersion: sourceVersion, + sourceField: sourceField, + targetVersion: targetVersion, + targetField: targetField, + sourceObj: getPaved(sourceVersion, sourceField, nil), + targetObj: getPaved(targetVersion, targetField, ptr.To("test")), + }, + want: want{ + converted: false, + targetObj: getPaved(targetVersion, targetField, ptr.To("test")), + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + c := NewFieldRenameConversion(tc.args.sourceVersion, tc.args.sourceField, tc.args.targetVersion, tc.args.targetField) + converted, err := c.(*fieldCopy).ConvertPaved(tc.args.sourceObj, tc.args.targetObj) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantErr, +gotErr:\n%s", tc.reason, diff) + } + if tc.want.err != nil { + return + } + if diff := cmp.Diff(tc.want.converted, converted); diff != "" { + t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantConverted, +gotConverted:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.targetObj.UnstructuredContent(), tc.args.targetObj.UnstructuredContent()); diff != "" { + t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantTargetObj, +gotTargetObj:\n%s", tc.reason, diff) + } + }) + } +} + +func getPaved(version, field string, value *string) *fieldpath.Paved { + m := map[string]any{ + "apiVersion": fmt.Sprintf("mockgroup/%s", version), + "kind": "mockkind", + } + if value != nil { + m[field] = *value + } + return fieldpath.Pave(m) +}