knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/testing/roundtrip/roundtrip.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package roundtrip
    18  
    19  import (
    20  	"context"
    21  	"math/rand"
    22  	"net/url"
    23  	"reflect"
    24  	"regexp"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"github.com/google/go-cmp/cmp/cmpopts"
    29  	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
    30  	"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
    31  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    32  	apimeta "k8s.io/apimachinery/pkg/api/meta"
    33  	metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/runtime/schema"
    37  	"k8s.io/apimachinery/pkg/runtime/serializer"
    38  	"k8s.io/apimachinery/pkg/util/sets"
    39  	"sigs.k8s.io/randfill"
    40  
    41  	"knative.dev/pkg/apis"
    42  )
    43  
    44  type convertibleObject interface {
    45  	runtime.Object
    46  	apis.Convertible
    47  }
    48  
    49  // globalNonRoundTrippableTypes are kinds that are effectively reserved
    50  // across all GroupVersions. They don't roundtrip
    51  //
    52  // This list comes from k8s.io/apimachinery. We can drop this constant when
    53  // the PR (https://github.com/kubernetes/kubernetes/pull/86959) merges and
    54  // we bump to a version that has the change
    55  var globalNonRoundTrippableTypes = sets.NewString(
    56  	"ExportOptions",
    57  	"GetOptions",
    58  	// WatchEvent does not include kind and version and can only be deserialized
    59  	// implicitly (if the caller expects the specific object). The watch call defines
    60  	// the schema by content type, rather than via kind/version included in each
    61  	// object.
    62  	"WatchEvent",
    63  	// ListOptions is now part of the meta group
    64  	"ListOptions",
    65  	// Delete options is only read in metav1
    66  	"DeleteOptions",
    67  )
    68  
    69  var (
    70  	metaV1Types    map[reflect.Type]struct{}
    71  	metaV1ListType = reflect.TypeOf((*metav1.ListMetaAccessor)(nil)).Elem()
    72  )
    73  
    74  func init() {
    75  	gv := schema.GroupVersion{Group: "roundtrip.group", Version: "v1"}
    76  
    77  	scheme := runtime.NewScheme()
    78  
    79  	metav1.AddToGroupVersion(scheme, gv)
    80  	metaV1Types = make(map[reflect.Type]struct{})
    81  
    82  	// Build up a list of types to ignore
    83  	for _, t := range scheme.KnownTypes(gv) {
    84  		metaV1Types[t] = struct{}{}
    85  	}
    86  }
    87  
    88  // ExternalTypesViaJSON applies the round-trip test to all external round-trippable Kinds
    89  // in the scheme. This is effectively testing the scenario:
    90  //
    91  //	external -> json -> external
    92  func ExternalTypesViaJSON(t *testing.T, scheme *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) {
    93  	codecFactory := serializer.NewCodecFactory(scheme)
    94  
    95  	f := fuzzer.FuzzerFor(
    96  		fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs),
    97  		rand.NewSource(rand.Int63()),
    98  		codecFactory,
    99  	)
   100  
   101  	f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration"))
   102  
   103  	kinds := scheme.AllKnownTypes()
   104  	for gvk := range kinds {
   105  		if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
   106  			continue
   107  		}
   108  		t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) {
   109  			roundtrip.RoundTripSpecificKindWithoutProtobuf(t, gvk, scheme, codecFactory, f, nil)
   110  		})
   111  	}
   112  }
   113  
   114  // ExternalTypesViaHub applies the round-trip test to all external round-trippable Kinds
   115  // in the scheme. This is effectively testing the scenario:
   116  //
   117  //	external version -> hub version -> external version
   118  func ExternalTypesViaHub(t *testing.T, scheme, hubs *runtime.Scheme, fuzzerFuncs fuzzer.FuzzerFuncs) {
   119  	f := fuzzer.FuzzerFor(
   120  		fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, fuzzerFuncs),
   121  		rand.NewSource(rand.Int63()),
   122  		// This seems to be used for protobuf not json
   123  		serializer.NewCodecFactory(scheme),
   124  	)
   125  
   126  	f.SkipFieldsWithPattern(regexp.MustCompile("DeprecatedGeneration"))
   127  
   128  	for gvk, objType := range scheme.AllKnownTypes() {
   129  		if gvk.Version == runtime.APIVersionInternal ||
   130  			gvk.Group == "" || // K8s group
   131  			globalNonRoundTrippableTypes.Has(gvk.Kind) {
   132  			continue
   133  		}
   134  
   135  		if _, ok := metaV1Types[objType]; ok {
   136  			continue
   137  		}
   138  
   139  		if reflect.PointerTo(objType).AssignableTo(metaV1ListType) {
   140  			continue
   141  		}
   142  
   143  		if hubs.Recognizes(gvk) {
   144  			continue
   145  		}
   146  
   147  		t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) {
   148  			for range *roundtrip.FuzzIters {
   149  				roundTripViaHub(t, gvk, scheme, hubs, f)
   150  
   151  				if t.Failed() {
   152  					break
   153  				}
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func roundTripViaHub(t *testing.T, gvk schema.GroupVersionKind, scheme, hubs *runtime.Scheme, f *randfill.Filler) {
   160  	ctx := context.Background()
   161  
   162  	hub, hubGVK := hubInstanceForGK(t, hubs, gvk.GroupKind())
   163  	obj := objForGVK(t, gvk, scheme)
   164  
   165  	fuzzObject(t, f, gvk, obj)
   166  
   167  	original := obj
   168  	obj = obj.DeepCopyObject().(convertibleObject)
   169  
   170  	if !apiequality.Semantic.DeepEqual(original, obj) {
   171  		t.Error("DeepCopy modified the original object (DeepCopy should not have side-effects), diff:", diff(original, obj))
   172  		return
   173  	}
   174  
   175  	if err := hub.ConvertFrom(ctx, obj); err != nil {
   176  		t.Errorf("Conversion to hub (%s) failed: %s", hubGVK, err)
   177  	}
   178  
   179  	if !apiequality.Semantic.DeepEqual(original, obj) {
   180  		t.Errorf("Conversion to hub (%s) modified the original object (ConvertFrom should not have side-effects), diff: %v", hubGVK, diff(original, obj))
   181  		return
   182  	}
   183  
   184  	newObj := objForGVK(t, gvk, scheme)
   185  	if err := hub.ConvertTo(ctx, newObj); err != nil {
   186  		t.Errorf("Conversion from hub (%s) failed: %s", hubGVK, err)
   187  		t.Errorf("object: %#v", obj)
   188  		return
   189  	}
   190  
   191  	if !apiequality.Semantic.DeepEqual(obj, newObj) {
   192  		t.Errorf("round trip through hub (%s) produced a diff: %s", hubGVK, diff(original, newObj))
   193  		return
   194  	}
   195  }
   196  
   197  func diff(obj1, obj2 interface{}) string {
   198  	// knative.dev/pkg/apis.URL is an alias to net.URL which embeds a
   199  	// url.Userinfo that has an unexported field
   200  	return cmp.Diff(obj1, obj2, cmpopts.IgnoreUnexported(url.Userinfo{}))
   201  }
   202  
   203  func objForGVK(t *testing.T,
   204  	gvk schema.GroupVersionKind,
   205  	scheme *runtime.Scheme,
   206  ) convertibleObject {
   207  	t.Helper()
   208  
   209  	obj, err := scheme.New(gvk)
   210  	if err != nil {
   211  		t.Fatal("unable to create object instance for type", err)
   212  	}
   213  
   214  	objType, err := apimeta.TypeAccessor(obj)
   215  	if err != nil {
   216  		t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err)
   217  	}
   218  	objType.SetKind(gvk.Kind)
   219  	objType.SetAPIVersion(gvk.GroupVersion().String())
   220  	return obj.(convertibleObject)
   221  }
   222  
   223  func fuzzObject(t *testing.T, fuzzer *randfill.Filler, gvk schema.GroupVersionKind, obj interface{}) {
   224  	fuzzer.Fill(obj)
   225  
   226  	objType, err := apimeta.TypeAccessor(obj)
   227  	if err != nil {
   228  		t.Fatalf("%q is not a TypeMeta and cannot be tested: %v", gvk, err)
   229  	}
   230  	objType.SetKind(gvk.Kind)
   231  	objType.SetAPIVersion(gvk.GroupVersion().String())
   232  }
   233  
   234  func hubInstanceForGK(t *testing.T,
   235  	hubs *runtime.Scheme,
   236  	gk schema.GroupKind,
   237  ) (apis.Convertible, schema.GroupVersionKind) {
   238  	t.Helper()
   239  
   240  	for hubGVK := range hubs.AllKnownTypes() {
   241  		if hubGVK.GroupKind() == gk {
   242  			obj, err := hubs.New(hubGVK)
   243  			if err != nil {
   244  				t.Fatal("error creating objects", err)
   245  			}
   246  
   247  			return obj.(apis.Convertible), hubGVK
   248  		}
   249  	}
   250  
   251  	t.Fatalf("hub type not found")
   252  	return nil, schema.GroupVersionKind{}
   253  }