k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/util/runtimeobjects/runtimeobjects_test.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes 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 runtimeobjects_test
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  
    25  	apps "k8s.io/api/apps/v1"
    26  	batch "k8s.io/api/batch/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	v1 "k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/api/resource"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/client-go/kubernetes/fake"
    34  	"k8s.io/client-go/kubernetes/scheme"
    35  	"k8s.io/perf-tests/clusterloader2/pkg/measurement/util/runtimeobjects"
    36  )
    37  
    38  var (
    39  	controllerName         = "foobar"
    40  	testNamespace          = "test-namespace"
    41  	defaultResourceVersion = "1"
    42  	defaultReplicas        = int32(10)
    43  	daemonsetReplicas      = int32(1)
    44  
    45  	daemonsetReplicasNoAffinity            = int32(4)
    46  	daemonsetReplicasNoAffinityTolerateAll = int32(5)
    47  )
    48  
    49  var (
    50  	simpleLabel   = map[string]string{"foo": "bar"}
    51  	affinityLabel = map[string]string{"foo": "bar", "affinity": "true"}
    52  	image         = "gcr.io/some-project/some-image"
    53  )
    54  
    55  var node1 = corev1.Node{
    56  	ObjectMeta: metav1.ObjectMeta{
    57  		Name:   "node1",
    58  		Labels: simpleLabel,
    59  	},
    60  }
    61  
    62  var node2 = corev1.Node{
    63  	ObjectMeta: metav1.ObjectMeta{
    64  		Name:   "node2",
    65  		Labels: affinityLabel,
    66  	},
    67  }
    68  
    69  var node3 = corev1.Node{
    70  	ObjectMeta: metav1.ObjectMeta{
    71  		Name:   "node3",
    72  		Labels: simpleLabel,
    73  	},
    74  	Spec: v1.NodeSpec{
    75  		Unschedulable: true,
    76  		Taints: []v1.Taint{
    77  			{
    78  				Key:    v1.TaintNodeUnschedulable,
    79  				Effect: v1.TaintEffectNoSchedule,
    80  			},
    81  		},
    82  	},
    83  }
    84  
    85  var node4 = corev1.Node{
    86  	ObjectMeta: metav1.ObjectMeta{
    87  		Name:   "node4",
    88  		Labels: simpleLabel,
    89  	},
    90  	Spec: v1.NodeSpec{
    91  		Taints: []v1.Taint{
    92  			{
    93  				Key:    "something1",
    94  				Effect: v1.TaintEffectNoSchedule,
    95  			},
    96  		},
    97  	},
    98  }
    99  
   100  var node5 = corev1.Node{
   101  	ObjectMeta: metav1.ObjectMeta{
   102  		Name:   "node5",
   103  		Labels: simpleLabel,
   104  	},
   105  	Spec: v1.NodeSpec{
   106  		Taints: []v1.Taint{
   107  			{
   108  				Key:    "something2",
   109  				Effect: v1.TaintEffectPreferNoSchedule,
   110  			},
   111  		},
   112  	},
   113  }
   114  
   115  var affinity = &corev1.Affinity{
   116  	NodeAffinity: &corev1.NodeAffinity{
   117  		RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
   118  			NodeSelectorTerms: []corev1.NodeSelectorTerm{
   119  				{
   120  					MatchExpressions: []corev1.NodeSelectorRequirement{
   121  						{
   122  							Key:      "affinity",
   123  							Operator: v1.NodeSelectorOpIn,
   124  							Values:   []string{"ok", "true"},
   125  						},
   126  					},
   127  				},
   128  			},
   129  		},
   130  	},
   131  }
   132  
   133  var replicationcontroller = &corev1.ReplicationController{
   134  	ObjectMeta: metav1.ObjectMeta{
   135  		Name:            controllerName,
   136  		Namespace:       testNamespace,
   137  		ResourceVersion: defaultResourceVersion,
   138  	},
   139  	Spec: corev1.ReplicationControllerSpec{
   140  		Replicas: &defaultReplicas,
   141  		Selector: simpleLabel,
   142  		Template: &v1.PodTemplateSpec{
   143  			ObjectMeta: metav1.ObjectMeta{
   144  				Labels: simpleLabel,
   145  			},
   146  			Spec: resourcePodSpec("", "50M", "0.5", nil, nil),
   147  		},
   148  	},
   149  }
   150  
   151  var replicaset = &apps.ReplicaSet{
   152  	ObjectMeta: metav1.ObjectMeta{
   153  		Name:            controllerName,
   154  		Namespace:       testNamespace,
   155  		ResourceVersion: defaultResourceVersion,
   156  	},
   157  	Spec: apps.ReplicaSetSpec{
   158  		Replicas: &defaultReplicas,
   159  		Selector: &metav1.LabelSelector{
   160  			MatchLabels: simpleLabel,
   161  		},
   162  		Template: v1.PodTemplateSpec{
   163  			ObjectMeta: metav1.ObjectMeta{
   164  				Labels: simpleLabel,
   165  			},
   166  			Spec: resourcePodSpec("", "50M", "0.5", nil, nil),
   167  		},
   168  	},
   169  }
   170  
   171  var deployment = &apps.Deployment{
   172  	ObjectMeta: metav1.ObjectMeta{
   173  		Name:            controllerName,
   174  		Namespace:       testNamespace,
   175  		ResourceVersion: defaultResourceVersion,
   176  	},
   177  	Spec: apps.DeploymentSpec{
   178  		Replicas: &defaultReplicas,
   179  		Selector: &metav1.LabelSelector{
   180  			MatchLabels: simpleLabel,
   181  		},
   182  		Template: v1.PodTemplateSpec{
   183  			ObjectMeta: metav1.ObjectMeta{
   184  				Labels: simpleLabel,
   185  			},
   186  			Spec: resourcePodSpec("", "50M", "0.5", nil, nil),
   187  		},
   188  	},
   189  }
   190  
   191  var daemonset = &apps.DaemonSet{
   192  	ObjectMeta: metav1.ObjectMeta{
   193  		Name:            controllerName,
   194  		Namespace:       testNamespace,
   195  		ResourceVersion: defaultResourceVersion,
   196  	},
   197  	Spec: apps.DaemonSetSpec{
   198  		Selector: &metav1.LabelSelector{
   199  			MatchLabels: simpleLabel,
   200  		},
   201  		Template: v1.PodTemplateSpec{
   202  			ObjectMeta: metav1.ObjectMeta{
   203  				Labels: simpleLabel,
   204  			},
   205  			Spec: resourcePodSpec("", "50M", "0.5", simpleLabel, affinity),
   206  		},
   207  	},
   208  }
   209  
   210  var daemonsetNoAffinity = &apps.DaemonSet{
   211  	ObjectMeta: metav1.ObjectMeta{
   212  		Name:            controllerName,
   213  		Namespace:       testNamespace,
   214  		ResourceVersion: defaultResourceVersion,
   215  	},
   216  	Spec: apps.DaemonSetSpec{
   217  		Selector: &metav1.LabelSelector{
   218  			MatchLabels: simpleLabel,
   219  		},
   220  		Template: v1.PodTemplateSpec{
   221  			ObjectMeta: metav1.ObjectMeta{
   222  				Labels: simpleLabel,
   223  			},
   224  			Spec: resourcePodSpec("", "50M", "0.5", simpleLabel, nil),
   225  		},
   226  	},
   227  }
   228  
   229  var daemonsetNoAffinityTolerateAll = &apps.DaemonSet{
   230  	ObjectMeta: metav1.ObjectMeta{
   231  		Name:            controllerName,
   232  		Namespace:       testNamespace,
   233  		ResourceVersion: defaultResourceVersion,
   234  	},
   235  	Spec: apps.DaemonSetSpec{
   236  		Selector: &metav1.LabelSelector{
   237  			MatchLabels: simpleLabel,
   238  		},
   239  		Template: v1.PodTemplateSpec{
   240  			ObjectMeta: metav1.ObjectMeta{
   241  				Labels: simpleLabel,
   242  			},
   243  			Spec: func() v1.PodSpec {
   244  				podSpec := resourcePodSpec("", "50M", "0.5", simpleLabel, nil)
   245  				podSpec.Tolerations = []v1.Toleration{
   246  					{
   247  						Effect: v1.TaintEffectNoSchedule,
   248  					},
   249  				}
   250  				return podSpec
   251  			}(),
   252  		},
   253  	},
   254  }
   255  
   256  var job = &batch.Job{
   257  	TypeMeta: metav1.TypeMeta{Kind: "Job"},
   258  	ObjectMeta: metav1.ObjectMeta{
   259  		Name:            controllerName,
   260  		Namespace:       testNamespace,
   261  		ResourceVersion: defaultResourceVersion,
   262  	},
   263  	Spec: batch.JobSpec{
   264  		Parallelism: &defaultReplicas,
   265  		Selector: &metav1.LabelSelector{
   266  			MatchLabels: simpleLabel,
   267  		},
   268  		Template: v1.PodTemplateSpec{
   269  			ObjectMeta: metav1.ObjectMeta{
   270  				Labels: simpleLabel,
   271  			},
   272  			Spec: resourcePodSpec("", "50M", "0.5", nil, nil),
   273  		},
   274  	},
   275  }
   276  
   277  // pod is a sample pod that can be created for replicationcontroller,
   278  // replicaset, deployment, job (NOT daemonset).
   279  var pod = &corev1.Pod{
   280  	ObjectMeta: metav1.ObjectMeta{
   281  		Name:            controllerName + "-abcd",
   282  		Namespace:       testNamespace,
   283  		ResourceVersion: defaultResourceVersion,
   284  	},
   285  	Spec: alterPodSpec(resourcePodSpec("", "50M", "0.5", nil, nil)),
   286  }
   287  
   288  var daemonsetPod = &corev1.Pod{
   289  	ObjectMeta: metav1.ObjectMeta{
   290  		Name:            controllerName + "-abcd",
   291  		Namespace:       testNamespace,
   292  		ResourceVersion: defaultResourceVersion,
   293  	},
   294  	Spec: alterPodSpec(resourcePodSpec("", "50M", "0.5", simpleLabel, affinity)),
   295  }
   296  
   297  func resourcePodSpec(nodeName, memory, cpu string, nodeSelector map[string]string, affinity *v1.Affinity) v1.PodSpec {
   298  	return v1.PodSpec{
   299  		NodeName: nodeName,
   300  		Containers: []v1.Container{{
   301  			Resources: v1.ResourceRequirements{
   302  				Requests: allocatableResources(memory, cpu),
   303  			},
   304  			Image: image,
   305  			Env: []v1.EnvVar{
   306  				{
   307  					Name:  "env1",
   308  					Value: "val1",
   309  				},
   310  			},
   311  		}},
   312  		NodeSelector: nodeSelector,
   313  		Affinity:     affinity,
   314  		Tolerations: []v1.Toleration{
   315  			{
   316  				Key:    "default-toleration",
   317  				Value:  "default-value",
   318  				Effect: v1.TaintEffectNoSchedule,
   319  			},
   320  		},
   321  	}
   322  }
   323  
   324  // alterPodSpec changees podSpec to simulate possible differences between template and final pod.
   325  func alterPodSpec(in v1.PodSpec) v1.PodSpec {
   326  	out := in.DeepCopy()
   327  	// append some tolerations
   328  	out.Tolerations = append(out.Tolerations, v1.Toleration{
   329  		Key:    "test",
   330  		Value:  "value",
   331  		Effect: v1.TaintEffectNoExecute,
   332  	})
   333  	// set some defaults
   334  	i := int64(30)
   335  	out.TerminationGracePeriodSeconds = &i
   336  	out.ActiveDeadlineSeconds = &i
   337  
   338  	// Simulate schedule
   339  	if out.NodeName == "" {
   340  		out.NodeName = node1.Name
   341  	}
   342  
   343  	// Copy resources
   344  	for i := range out.Containers {
   345  		c := &out.Containers[i]
   346  		if c.Resources.Requests == nil {
   347  			c.Resources.Requests = c.Resources.Limits.DeepCopy()
   348  		}
   349  	}
   350  	return *out
   351  }
   352  
   353  func allocatableResources(memory, cpu string) v1.ResourceList {
   354  	return v1.ResourceList{
   355  		v1.ResourceMemory: resource.MustParse(memory),
   356  		v1.ResourceCPU:    resource.MustParse(cpu),
   357  		v1.ResourcePods:   resource.MustParse("100"),
   358  	}
   359  }
   360  
   361  func TestGetResourceVersionFromRuntimeObject(t *testing.T) {
   362  	objects := []runtime.Object{
   363  		replicationcontroller,
   364  		replicaset,
   365  		deployment,
   366  		job,
   367  		daemonset,
   368  	}
   369  	for _, obj := range objects {
   370  		unstructured := &unstructured.Unstructured{}
   371  		if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil {
   372  			t.Fatalf("error converting controller to unstructured: %v", err)
   373  		}
   374  		rv, err := runtimeobjects.GetResourceVersionFromRuntimeObject(unstructured)
   375  		if err != nil {
   376  			t.Fatalf("get resource version from runtime object failed: %v", err)
   377  		}
   378  
   379  		if defaultResourceVersion != fmt.Sprint(rv) {
   380  			t.Fatalf("Unexpected resource version from runtime object, expected: %s, actual: %v", defaultResourceVersion, rv)
   381  		}
   382  	}
   383  }
   384  
   385  func TestGetSpecFromRuntimeObject(t *testing.T) {
   386  	objects := []runtime.Object{
   387  		replicationcontroller,
   388  		replicaset,
   389  		deployment,
   390  		job,
   391  		daemonset,
   392  	}
   393  	expected := []interface{}{
   394  		replicationcontroller.Spec,
   395  		replicaset.Spec,
   396  		deployment.Spec,
   397  		job.Spec,
   398  		daemonset.Spec,
   399  	}
   400  	for i, obj := range objects {
   401  		unstructured := &unstructured.Unstructured{}
   402  		if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil {
   403  			t.Fatalf("error converting controller to unstructured: %v", err)
   404  		}
   405  		spec, err := runtimeobjects.GetSpecFromRuntimeObject(unstructured)
   406  		if err != nil {
   407  			t.Fatalf("get spec from runtime object failed: %v", err)
   408  		}
   409  		target, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&expected[i])
   410  		if err != nil {
   411  			t.Fatalf("error converting target spec to unstructured: %v", err)
   412  		}
   413  
   414  		if !reflect.DeepEqual(target, spec) {
   415  			t.Fatalf("Unexpected spec from runtime object, expected: %v, actual: %v", expected[i], spec)
   416  		}
   417  	}
   418  }
   419  
   420  func changeImage(in *v1.Pod) *v1.Pod {
   421  	out := in.DeepCopy()
   422  
   423  	for i := range out.Spec.Containers {
   424  		c := &out.Spec.Containers[i]
   425  		c.Image = c.Image + "-diff"
   426  	}
   427  
   428  	return out
   429  }
   430  
   431  func changeEnv(in *v1.Pod) *v1.Pod {
   432  	out := in.DeepCopy()
   433  
   434  	for i := range out.Spec.Containers {
   435  		c := &out.Spec.Containers[i]
   436  		for j := range c.Env {
   437  			e := &c.Env[j]
   438  			e.Value = e.Value + "-diff"
   439  		}
   440  	}
   441  
   442  	return out
   443  }
   444  
   445  func noWhitespace(s string) string {
   446  	s = strings.ReplaceAll(s, " ", "")
   447  	s = strings.ReplaceAll(s, "\n", "")
   448  	s = strings.ReplaceAll(s, "\r", "")
   449  	s = strings.ReplaceAll(s, " ", "") //non-breaking space
   450  	return strings.ReplaceAll(s, "\t", "")
   451  }
   452  
   453  const fullCompareErrorDifferentEnv = `Not matching templates, diff:   v1.PodSpec{
   454      Volumes:        nil,
   455      InitContainers: nil,
   456      Containers: []v1.Container{
   457              {
   458                      ... // 5 identical fields
   459                      Ports:   nil,
   460                      EnvFrom: nil,
   461                      Env: []v1.EnvVar{
   462                              {
   463                                      Name:      "env1",
   464  -                                   Value:     "val1",
   465  +                                   Value:     "val1-diff",
   466                                      ValueFrom: nil,
   467                              },
   468                      },
   469                      Resources:    {Requests: {s"cpu": {i: {...}, Format: "DecimalSI"}, s"memory": {i: {...}, s: "50M", Format: "DecimalSI"}, s"pods": {i: {...}, s: "100", Format: "DecimalSI"}}},
   470                      VolumeMounts: nil,
   471                      ... // 12 identical fields
   472              },
   473      },
   474      EphemeralContainers:           nil,
   475      RestartPolicy:                 "",
   476  -   TerminationGracePeriodSeconds: nil,
   477  +   TerminationGracePeriodSeconds: &30,
   478  -   ActiveDeadlineSeconds:         nil,
   479  +   ActiveDeadlineSeconds:         &30,
   480      DNSPolicy:                     "",
   481      NodeSelector:                  nil,
   482      ServiceAccountName:            "",
   483      DeprecatedServiceAccount:      "",
   484      AutomountServiceAccountToken:  nil,
   485  -   NodeName:                      "",
   486  +   NodeName:                      "node1",
   487      HostNetwork:                   false,
   488      HostPID:                       false,
   489      ... // 6 identical fields
   490      Affinity:      nil,
   491      SchedulerName: "",
   492      Tolerations: []v1.Toleration{
   493              {Key: "default-toleration", Value: "default-value", Effect: "NoSchedule"},
   494  +           {Key: "test", Value: "value", Effect: "NoExecute"},
   495      },
   496      HostAliases:       nil,
   497      PriorityClassName: "",
   498      ... // 9 identical fields
   499    }
   500  `
   501  
   502  const fullCompareErrorDifferentImage = `Not matching templates, diff:   v1.PodSpec{
   503      Volumes:        nil,
   504      InitContainers: nil,
   505      Containers: []v1.Container{
   506              {
   507                      Name:    "",
   508  -                   Image:   "gcr.io/some-project/some-image",
   509  +                   Image:   "gcr.io/some-project/some-image-diff",
   510                      Command: nil,
   511                      Args:    nil,
   512                      ... // 18 identical fields
   513              },
   514      },
   515      EphemeralContainers:           nil,
   516      RestartPolicy:                 "",
   517  -   TerminationGracePeriodSeconds: nil,
   518  +   TerminationGracePeriodSeconds: &30,
   519  -   ActiveDeadlineSeconds:         nil,
   520  +   ActiveDeadlineSeconds:         &30,
   521      DNSPolicy:                     "",
   522      NodeSelector:                  nil,
   523      ServiceAccountName:            "",
   524      DeprecatedServiceAccount:      "",
   525      AutomountServiceAccountToken:  nil,
   526  -   NodeName:                      "",
   527  +   NodeName:                      "node1",
   528      HostNetwork:                   false,
   529      HostPID:                       false,
   530      ... // 6 identical fields
   531      Affinity:      nil,
   532      SchedulerName: "",
   533      Tolerations: []v1.Toleration{
   534              {Key: "default-toleration", Value: "default-value", Effect: "NoSchedule"},
   535  +           {Key: "test", Value: "value", Effect: "NoExecute"},
   536      },
   537      HostAliases:       nil,
   538      PriorityClassName: "",
   539      ... // 9 identical fields
   540    }`
   541  
   542  func TestGetIsPodUpdatedPredicateFromRuntimeObject(t *testing.T) {
   543  	testCases := []struct {
   544  		name                 string
   545  		obj                  runtime.Object
   546  		pod                  *corev1.Pod
   547  		wantErr              bool
   548  		wantfullCompareError string
   549  	}{
   550  		{
   551  			name: "deployment, positive",
   552  			obj:  deployment,
   553  			pod:  pod,
   554  		},
   555  		{
   556  			name:                 "deployment, different env",
   557  			obj:                  deployment,
   558  			pod:                  changeEnv(pod),
   559  			wantfullCompareError: fullCompareErrorDifferentEnv,
   560  		},
   561  		{
   562  			name:                 "deployment, different image",
   563  			obj:                  deployment,
   564  			pod:                  changeImage(pod),
   565  			wantfullCompareError: fullCompareErrorDifferentImage,
   566  		},
   567  		{
   568  			name: "replicaset, positive",
   569  			obj:  replicaset,
   570  			pod:  pod,
   571  		},
   572  		{
   573  			name: "replicationcontroller, positive",
   574  			obj:  replicationcontroller,
   575  			pod:  pod,
   576  		},
   577  		{
   578  			name: "daemonset, positive",
   579  			obj:  daemonset,
   580  			pod:  daemonsetPod,
   581  		},
   582  		{
   583  			name: "job, positive",
   584  			obj:  job,
   585  			pod:  pod,
   586  		},
   587  		{
   588  			name:    "no spec.template",
   589  			obj:     pod, // pod has no spec.template field.
   590  			pod:     pod,
   591  			wantErr: true,
   592  		},
   593  	}
   594  
   595  	for _, tc := range testCases {
   596  		t.Run(tc.name, func(t *testing.T) {
   597  			unstructured := &unstructured.Unstructured{}
   598  			if err := scheme.Scheme.Convert(tc.obj, unstructured, nil); err != nil {
   599  				t.Fatalf("error converting controller to unstructured: %v", err)
   600  			}
   601  			pred, err := runtimeobjects.GetIsPodUpdatedPredicateFromRuntimeObject(unstructured)
   602  			if (err != nil) != tc.wantErr {
   603  				t.Errorf("unexpected error; want: %v; got %v", tc.wantErr, err)
   604  			}
   605  			if err != nil {
   606  				return
   607  			}
   608  			gotErr := pred(tc.pod)
   609  			var gotErrString string
   610  			if gotErr != nil {
   611  				gotErrString = gotErr.Error()
   612  			}
   613  			if noWhitespace(gotErrString) != noWhitespace(tc.wantfullCompareError) {
   614  				t.Errorf("pred(tc.pod) = %v; want %v", noWhitespace(gotErrString), noWhitespace(tc.wantfullCompareError))
   615  			}
   616  		})
   617  	}
   618  }
   619  
   620  func TestGetReplicasFromRuntimeObject(t *testing.T) {
   621  	objects := []runtime.Object{
   622  		replicationcontroller,
   623  		replicaset,
   624  		deployment,
   625  		job,
   626  		daemonset,
   627  		daemonsetNoAffinity,
   628  		daemonsetNoAffinityTolerateAll,
   629  	}
   630  	expected := []int32{
   631  		defaultReplicas,
   632  		defaultReplicas,
   633  		defaultReplicas,
   634  		defaultReplicas,
   635  		daemonsetReplicas,
   636  		daemonsetReplicasNoAffinity,
   637  		daemonsetReplicasNoAffinityTolerateAll,
   638  	}
   639  
   640  	fakeClient := fake.NewSimpleClientset(&node1, &node2, &node3, &node4, &node5)
   641  	for i, obj := range objects {
   642  		unstructured := &unstructured.Unstructured{}
   643  		if err := scheme.Scheme.Convert(obj, unstructured, nil); err != nil {
   644  			t.Fatalf("error converting controller to unstructured: %v", err)
   645  		}
   646  		replicasWatcher, err := runtimeobjects.GetReplicasFromRuntimeObject(fakeClient, unstructured)
   647  		if err != nil {
   648  			t.Fatalf("get replicas from runtime object failed: %v", err)
   649  		}
   650  
   651  		replicas, err := runtimeobjects.GetReplicasOnce(replicasWatcher)
   652  		if err != nil {
   653  			t.Fatalf("got unexpected error while getting number of replicas: %v", err)
   654  		}
   655  
   656  		if int(expected[i]) != replicas {
   657  			t.Fatalf("unexpected replicas from runtime object, expected: %d, actual: %d", expected[i], replicas)
   658  		}
   659  	}
   660  }