replica_calculator_test.go 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package podautoscaler
  14. import (
  15. "fmt"
  16. "math"
  17. "testing"
  18. "time"
  19. autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
  20. v1 "k8s.io/api/core/v1"
  21. "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
  22. "k8s.io/apimachinery/pkg/api/resource"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/labels"
  25. "k8s.io/apimachinery/pkg/runtime"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/apimachinery/pkg/util/sets"
  28. "k8s.io/client-go/informers"
  29. "k8s.io/client-go/kubernetes/fake"
  30. core "k8s.io/client-go/testing"
  31. "k8s.io/client-go/tools/cache"
  32. "k8s.io/kubernetes/pkg/api/legacyscheme"
  33. "k8s.io/kubernetes/pkg/controller"
  34. metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
  35. cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
  36. emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
  37. metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
  38. metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
  39. cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake"
  40. emfake "k8s.io/metrics/pkg/client/external_metrics/fake"
  41. "github.com/stretchr/testify/assert"
  42. "github.com/stretchr/testify/require"
  43. )
  44. type resourceInfo struct {
  45. name v1.ResourceName
  46. requests []resource.Quantity
  47. levels []int64
  48. // only applies to pod names returned from "heapster"
  49. podNames []string
  50. targetUtilization int32
  51. expectedUtilization int32
  52. expectedValue int64
  53. }
  54. type metricType int
  55. const (
  56. objectMetric metricType = iota
  57. objectPerPodMetric
  58. externalMetric
  59. externalPerPodMetric
  60. podMetric
  61. )
  62. type metricInfo struct {
  63. name string
  64. levels []int64
  65. singleObject *autoscalingv2.CrossVersionObjectReference
  66. selector *metav1.LabelSelector
  67. metricType metricType
  68. targetUtilization int64
  69. perPodTargetUtilization int64
  70. expectedUtilization int64
  71. }
  72. type replicaCalcTestCase struct {
  73. currentReplicas int32
  74. expectedReplicas int32
  75. expectedError error
  76. timestamp time.Time
  77. resource *resourceInfo
  78. metric *metricInfo
  79. metricLabelSelector labels.Selector
  80. podReadiness []v1.ConditionStatus
  81. podStartTime []metav1.Time
  82. podPhase []v1.PodPhase
  83. podDeletionTimestamp []bool
  84. }
  85. const (
  86. testNamespace = "test-namespace"
  87. podNamePrefix = "test-pod"
  88. numContainersPerPod = 2
  89. )
  90. func (tc *replicaCalcTestCase) prepareTestClientSet() *fake.Clientset {
  91. fakeClient := &fake.Clientset{}
  92. fakeClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  93. obj := &v1.PodList{}
  94. podsCount := int(tc.currentReplicas)
  95. // Failed pods are not included in tc.currentReplicas
  96. if tc.podPhase != nil && len(tc.podPhase) > podsCount {
  97. podsCount = len(tc.podPhase)
  98. }
  99. for i := 0; i < podsCount; i++ {
  100. podReadiness := v1.ConditionTrue
  101. if tc.podReadiness != nil && i < len(tc.podReadiness) {
  102. podReadiness = tc.podReadiness[i]
  103. }
  104. var podStartTime metav1.Time
  105. if tc.podStartTime != nil {
  106. podStartTime = tc.podStartTime[i]
  107. }
  108. podPhase := v1.PodRunning
  109. if tc.podPhase != nil {
  110. podPhase = tc.podPhase[i]
  111. }
  112. podDeletionTimestamp := false
  113. if tc.podDeletionTimestamp != nil {
  114. podDeletionTimestamp = tc.podDeletionTimestamp[i]
  115. }
  116. podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
  117. pod := v1.Pod{
  118. Status: v1.PodStatus{
  119. Phase: podPhase,
  120. StartTime: &podStartTime,
  121. Conditions: []v1.PodCondition{
  122. {
  123. Type: v1.PodReady,
  124. Status: podReadiness,
  125. },
  126. },
  127. },
  128. ObjectMeta: metav1.ObjectMeta{
  129. Name: podName,
  130. Namespace: testNamespace,
  131. Labels: map[string]string{
  132. "name": podNamePrefix,
  133. },
  134. },
  135. Spec: v1.PodSpec{
  136. Containers: []v1.Container{{}, {}},
  137. },
  138. }
  139. if podDeletionTimestamp {
  140. pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
  141. }
  142. if tc.resource != nil && i < len(tc.resource.requests) {
  143. pod.Spec.Containers[0].Resources = v1.ResourceRequirements{
  144. Requests: v1.ResourceList{
  145. tc.resource.name: tc.resource.requests[i],
  146. },
  147. }
  148. pod.Spec.Containers[1].Resources = v1.ResourceRequirements{
  149. Requests: v1.ResourceList{
  150. tc.resource.name: tc.resource.requests[i],
  151. },
  152. }
  153. }
  154. obj.Items = append(obj.Items, pod)
  155. }
  156. return true, obj, nil
  157. })
  158. return fakeClient
  159. }
  160. func (tc *replicaCalcTestCase) prepareTestMetricsClient() *metricsfake.Clientset {
  161. fakeMetricsClient := &metricsfake.Clientset{}
  162. // NB: we have to sound like Gollum due to gengo's inability to handle already-plural resource names
  163. fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  164. if tc.resource != nil {
  165. metrics := &metricsapi.PodMetricsList{}
  166. for i, resValue := range tc.resource.levels {
  167. podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
  168. if len(tc.resource.podNames) > i {
  169. podName = tc.resource.podNames[i]
  170. }
  171. // NB: the list reactor actually does label selector filtering for us,
  172. // so we have to make sure our results match the label selector
  173. podMetric := metricsapi.PodMetrics{
  174. ObjectMeta: metav1.ObjectMeta{
  175. Name: podName,
  176. Namespace: testNamespace,
  177. Labels: map[string]string{"name": podNamePrefix},
  178. },
  179. Timestamp: metav1.Time{Time: tc.timestamp},
  180. Window: metav1.Duration{Duration: time.Minute},
  181. Containers: make([]metricsapi.ContainerMetrics, numContainersPerPod),
  182. }
  183. for i := 0; i < numContainersPerPod; i++ {
  184. podMetric.Containers[i] = metricsapi.ContainerMetrics{
  185. Name: fmt.Sprintf("container%v", i),
  186. Usage: v1.ResourceList{
  187. v1.ResourceName(tc.resource.name): *resource.NewMilliQuantity(
  188. int64(resValue),
  189. resource.DecimalSI),
  190. },
  191. }
  192. }
  193. metrics.Items = append(metrics.Items, podMetric)
  194. }
  195. return true, metrics, nil
  196. }
  197. return true, nil, fmt.Errorf("no pod resource metrics specified in test client")
  198. })
  199. return fakeMetricsClient
  200. }
  201. func (tc *replicaCalcTestCase) prepareTestCMClient(t *testing.T) *cmfake.FakeCustomMetricsClient {
  202. fakeCMClient := &cmfake.FakeCustomMetricsClient{}
  203. fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  204. getForAction, wasGetFor := action.(cmfake.GetForAction)
  205. if !wasGetFor {
  206. return true, nil, fmt.Errorf("expected a get-for action, got %v instead", action)
  207. }
  208. if tc.metric == nil {
  209. return true, nil, fmt.Errorf("no custom metrics specified in test client")
  210. }
  211. assert.Equal(t, tc.metric.name, getForAction.GetMetricName(), "the metric requested should have matched the one specified")
  212. if getForAction.GetName() == "*" {
  213. metrics := cmapi.MetricValueList{}
  214. // multiple objects
  215. assert.Equal(t, "pods", getForAction.GetResource().Resource, "the type of object that we requested multiple metrics for should have been pods")
  216. for i, level := range tc.metric.levels {
  217. podMetric := cmapi.MetricValue{
  218. DescribedObject: v1.ObjectReference{
  219. Kind: "Pod",
  220. Name: fmt.Sprintf("%s-%d", podNamePrefix, i),
  221. Namespace: testNamespace,
  222. },
  223. Timestamp: metav1.Time{Time: tc.timestamp},
  224. Metric: cmapi.MetricIdentifier{
  225. Name: tc.metric.name,
  226. },
  227. Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
  228. }
  229. metrics.Items = append(metrics.Items, podMetric)
  230. }
  231. return true, &metrics, nil
  232. }
  233. name := getForAction.GetName()
  234. mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)
  235. metrics := &cmapi.MetricValueList{}
  236. assert.NotNil(t, tc.metric.singleObject, "should have only requested a single-object metric when calling GetObjectMetricReplicas")
  237. gk := schema.FromAPIVersionAndKind(tc.metric.singleObject.APIVersion, tc.metric.singleObject.Kind).GroupKind()
  238. mapping, err := mapper.RESTMapping(gk)
  239. if err != nil {
  240. return true, nil, fmt.Errorf("unable to get mapping for %s: %v", gk.String(), err)
  241. }
  242. groupResource := mapping.Resource.GroupResource()
  243. assert.Equal(t, groupResource.String(), getForAction.GetResource().Resource, "should have requested metrics for the resource matching the GroupKind passed in")
  244. assert.Equal(t, tc.metric.singleObject.Name, name, "should have requested metrics for the object matching the name passed in")
  245. metrics.Items = []cmapi.MetricValue{
  246. {
  247. DescribedObject: v1.ObjectReference{
  248. Kind: tc.metric.singleObject.Kind,
  249. APIVersion: tc.metric.singleObject.APIVersion,
  250. Name: name,
  251. },
  252. Timestamp: metav1.Time{Time: tc.timestamp},
  253. Metric: cmapi.MetricIdentifier{
  254. Name: tc.metric.name,
  255. },
  256. Value: *resource.NewMilliQuantity(int64(tc.metric.levels[0]), resource.DecimalSI),
  257. },
  258. }
  259. return true, metrics, nil
  260. })
  261. return fakeCMClient
  262. }
  263. func (tc *replicaCalcTestCase) prepareTestEMClient(t *testing.T) *emfake.FakeExternalMetricsClient {
  264. fakeEMClient := &emfake.FakeExternalMetricsClient{}
  265. fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  266. listAction, wasList := action.(core.ListAction)
  267. if !wasList {
  268. return true, nil, fmt.Errorf("expected a list-for action, got %v instead", action)
  269. }
  270. if tc.metric == nil {
  271. return true, nil, fmt.Errorf("no external metrics specified in test client")
  272. }
  273. assert.Equal(t, tc.metric.name, listAction.GetResource().Resource, "the metric requested should have matched the one specified")
  274. selector, err := metav1.LabelSelectorAsSelector(tc.metric.selector)
  275. if err != nil {
  276. return true, nil, fmt.Errorf("failed to convert label selector specified in test client")
  277. }
  278. assert.Equal(t, selector, listAction.GetListRestrictions().Labels, "the metric selector should have matched the one specified")
  279. metrics := emapi.ExternalMetricValueList{}
  280. for _, level := range tc.metric.levels {
  281. metric := emapi.ExternalMetricValue{
  282. Timestamp: metav1.Time{Time: tc.timestamp},
  283. MetricName: tc.metric.name,
  284. Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
  285. }
  286. metrics.Items = append(metrics.Items, metric)
  287. }
  288. return true, &metrics, nil
  289. })
  290. return fakeEMClient
  291. }
  292. func (tc *replicaCalcTestCase) prepareTestClient(t *testing.T) (*fake.Clientset, *metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient) {
  293. fakeClient := tc.prepareTestClientSet()
  294. fakeMetricsClient := tc.prepareTestMetricsClient()
  295. fakeCMClient := tc.prepareTestCMClient(t)
  296. fakeEMClient := tc.prepareTestEMClient(t)
  297. return fakeClient, fakeMetricsClient, fakeCMClient, fakeEMClient
  298. }
  299. func (tc *replicaCalcTestCase) runTest(t *testing.T) {
  300. testClient, testMetricsClient, testCMClient, testEMClient := tc.prepareTestClient(t)
  301. metricsClient := metricsclient.NewRESTMetricsClient(testMetricsClient.MetricsV1beta1(), testCMClient, testEMClient)
  302. informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc())
  303. informer := informerFactory.Core().V1().Pods()
  304. replicaCalc := NewReplicaCalculator(metricsClient, informer.Lister(), defaultTestingTolerance, defaultTestingCpuInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
  305. stop := make(chan struct{})
  306. defer close(stop)
  307. informerFactory.Start(stop)
  308. if !cache.WaitForNamedCacheSync("HPA", stop, informer.Informer().HasSynced) {
  309. return
  310. }
  311. selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
  312. MatchLabels: map[string]string{"name": podNamePrefix},
  313. })
  314. if err != nil {
  315. require.Nil(t, err, "something went horribly wrong...")
  316. }
  317. if tc.resource != nil {
  318. outReplicas, outUtilization, outRawValue, outTimestamp, err := replicaCalc.GetResourceReplicas(tc.currentReplicas, tc.resource.targetUtilization, tc.resource.name, testNamespace, selector)
  319. if tc.expectedError != nil {
  320. require.Error(t, err, "there should be an error calculating the replica count")
  321. assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
  322. return
  323. }
  324. require.NoError(t, err, "there should not have been an error calculating the replica count")
  325. assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
  326. assert.Equal(t, tc.resource.expectedUtilization, outUtilization, "utilization should be as expected")
  327. assert.Equal(t, tc.resource.expectedValue, outRawValue, "raw value should be as expected")
  328. assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
  329. return
  330. }
  331. var outReplicas int32
  332. var outUtilization int64
  333. var outTimestamp time.Time
  334. switch tc.metric.metricType {
  335. case objectMetric:
  336. if tc.metric.singleObject == nil {
  337. t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
  338. }
  339. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetObjectMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, tc.metric.singleObject, selector, nil)
  340. case objectPerPodMetric:
  341. if tc.metric.singleObject == nil {
  342. t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
  343. }
  344. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetObjectPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUtilization, tc.metric.name, testNamespace, tc.metric.singleObject, nil)
  345. case externalMetric:
  346. if tc.metric.selector == nil {
  347. t.Fatal("Metric specified as externalMetric but metric.selector is nil.")
  348. }
  349. if tc.metric.targetUtilization <= 0 {
  350. t.Fatalf("Metric specified as externalMetric but metric.targetUtilization is %d which is <=0.", tc.metric.targetUtilization)
  351. }
  352. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetExternalMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, tc.metric.selector, selector)
  353. case externalPerPodMetric:
  354. if tc.metric.selector == nil {
  355. t.Fatal("Metric specified as externalPerPodMetric but metric.selector is nil.")
  356. }
  357. if tc.metric.perPodTargetUtilization <= 0 {
  358. t.Fatalf("Metric specified as externalPerPodMetric but metric.perPodTargetUtilization is %d which is <=0.", tc.metric.perPodTargetUtilization)
  359. }
  360. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetExternalPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUtilization, tc.metric.name, testNamespace, tc.metric.selector)
  361. case podMetric:
  362. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, selector, nil)
  363. default:
  364. t.Fatalf("Unknown metric type: %d", tc.metric.metricType)
  365. }
  366. if tc.expectedError != nil {
  367. require.Error(t, err, "there should be an error calculating the replica count")
  368. assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
  369. return
  370. }
  371. require.NoError(t, err, "there should not have been an error calculating the replica count")
  372. assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
  373. assert.Equal(t, tc.metric.expectedUtilization, outUtilization, "utilization should be as expected")
  374. assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
  375. }
  376. func TestReplicaCalcDisjointResourcesMetrics(t *testing.T) {
  377. tc := replicaCalcTestCase{
  378. currentReplicas: 1,
  379. expectedError: fmt.Errorf("no metrics returned matched known pods"),
  380. resource: &resourceInfo{
  381. name: v1.ResourceCPU,
  382. requests: []resource.Quantity{resource.MustParse("1.0")},
  383. levels: []int64{100},
  384. podNames: []string{"an-older-pod-name"},
  385. targetUtilization: 100,
  386. },
  387. }
  388. tc.runTest(t)
  389. }
  390. func TestReplicaCalcScaleUp(t *testing.T) {
  391. tc := replicaCalcTestCase{
  392. currentReplicas: 3,
  393. expectedReplicas: 5,
  394. resource: &resourceInfo{
  395. name: v1.ResourceCPU,
  396. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  397. levels: []int64{300, 500, 700},
  398. targetUtilization: 30,
  399. expectedUtilization: 50,
  400. expectedValue: numContainersPerPod * 500,
  401. },
  402. }
  403. tc.runTest(t)
  404. }
  405. func TestReplicaCalcScaleUpUnreadyLessScale(t *testing.T) {
  406. tc := replicaCalcTestCase{
  407. currentReplicas: 3,
  408. expectedReplicas: 4,
  409. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  410. resource: &resourceInfo{
  411. name: v1.ResourceCPU,
  412. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  413. levels: []int64{300, 500, 700},
  414. targetUtilization: 30,
  415. expectedUtilization: 60,
  416. expectedValue: numContainersPerPod * 600,
  417. },
  418. }
  419. tc.runTest(t)
  420. }
  421. func TestReplicaCalcScaleUpHotCpuLessScale(t *testing.T) {
  422. tc := replicaCalcTestCase{
  423. currentReplicas: 3,
  424. expectedReplicas: 4,
  425. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  426. resource: &resourceInfo{
  427. name: v1.ResourceCPU,
  428. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  429. levels: []int64{300, 500, 700},
  430. targetUtilization: 30,
  431. expectedUtilization: 60,
  432. expectedValue: numContainersPerPod * 600,
  433. },
  434. }
  435. tc.runTest(t)
  436. }
  437. func TestReplicaCalcScaleUpUnreadyNoScale(t *testing.T) {
  438. tc := replicaCalcTestCase{
  439. currentReplicas: 3,
  440. expectedReplicas: 3,
  441. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  442. resource: &resourceInfo{
  443. name: v1.ResourceCPU,
  444. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  445. levels: []int64{400, 500, 700},
  446. targetUtilization: 30,
  447. expectedUtilization: 40,
  448. expectedValue: numContainersPerPod * 400,
  449. },
  450. }
  451. tc.runTest(t)
  452. }
  453. func TestReplicaCalcScaleHotCpuNoScale(t *testing.T) {
  454. tc := replicaCalcTestCase{
  455. currentReplicas: 3,
  456. expectedReplicas: 3,
  457. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  458. podStartTime: []metav1.Time{coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
  459. resource: &resourceInfo{
  460. name: v1.ResourceCPU,
  461. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  462. levels: []int64{400, 500, 700},
  463. targetUtilization: 30,
  464. expectedUtilization: 40,
  465. expectedValue: numContainersPerPod * 400,
  466. },
  467. }
  468. tc.runTest(t)
  469. }
  470. func TestReplicaCalcScaleUpIgnoresFailedPods(t *testing.T) {
  471. tc := replicaCalcTestCase{
  472. currentReplicas: 2,
  473. expectedReplicas: 4,
  474. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  475. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
  476. resource: &resourceInfo{
  477. name: v1.ResourceCPU,
  478. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  479. levels: []int64{500, 700},
  480. targetUtilization: 30,
  481. expectedUtilization: 60,
  482. expectedValue: numContainersPerPod * 600,
  483. },
  484. }
  485. tc.runTest(t)
  486. }
  487. func TestReplicaCalcScaleUpIgnoresDeletionPods(t *testing.T) {
  488. tc := replicaCalcTestCase{
  489. currentReplicas: 2,
  490. expectedReplicas: 4,
  491. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  492. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
  493. podDeletionTimestamp: []bool{false, false, true, true},
  494. resource: &resourceInfo{
  495. name: v1.ResourceCPU,
  496. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  497. levels: []int64{500, 700},
  498. targetUtilization: 30,
  499. expectedUtilization: 60,
  500. expectedValue: numContainersPerPod * 600,
  501. },
  502. }
  503. tc.runTest(t)
  504. }
  505. func TestReplicaCalcScaleUpCM(t *testing.T) {
  506. tc := replicaCalcTestCase{
  507. currentReplicas: 3,
  508. expectedReplicas: 4,
  509. metric: &metricInfo{
  510. name: "qps",
  511. levels: []int64{20000, 10000, 30000},
  512. targetUtilization: 15000,
  513. expectedUtilization: 20000,
  514. metricType: podMetric,
  515. },
  516. }
  517. tc.runTest(t)
  518. }
  519. func TestReplicaCalcScaleUpCMUnreadyHotCpuNoLessScale(t *testing.T) {
  520. tc := replicaCalcTestCase{
  521. currentReplicas: 3,
  522. expectedReplicas: 6,
  523. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse},
  524. podStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
  525. metric: &metricInfo{
  526. name: "qps",
  527. levels: []int64{50000, 10000, 30000},
  528. targetUtilization: 15000,
  529. expectedUtilization: 30000,
  530. metricType: podMetric,
  531. },
  532. }
  533. tc.runTest(t)
  534. }
  535. func TestReplicaCalcScaleUpCMUnreadyHotCpuScaleWouldScaleDown(t *testing.T) {
  536. tc := replicaCalcTestCase{
  537. currentReplicas: 3,
  538. expectedReplicas: 7,
  539. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  540. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
  541. metric: &metricInfo{
  542. name: "qps",
  543. levels: []int64{50000, 15000, 30000},
  544. targetUtilization: 15000,
  545. expectedUtilization: 31666,
  546. metricType: podMetric,
  547. },
  548. }
  549. tc.runTest(t)
  550. }
  551. func TestReplicaCalcScaleUpCMObject(t *testing.T) {
  552. tc := replicaCalcTestCase{
  553. currentReplicas: 3,
  554. expectedReplicas: 4,
  555. metric: &metricInfo{
  556. name: "qps",
  557. levels: []int64{20000},
  558. targetUtilization: 15000,
  559. expectedUtilization: 20000,
  560. singleObject: &autoscalingv2.CrossVersionObjectReference{
  561. Kind: "Deployment",
  562. APIVersion: "apps/v1",
  563. Name: "some-deployment",
  564. },
  565. },
  566. }
  567. tc.runTest(t)
  568. }
  569. func TestReplicaCalcScaleUpCMPerPodObject(t *testing.T) {
  570. tc := replicaCalcTestCase{
  571. currentReplicas: 3,
  572. expectedReplicas: 4,
  573. metric: &metricInfo{
  574. metricType: objectPerPodMetric,
  575. name: "qps",
  576. levels: []int64{20000},
  577. perPodTargetUtilization: 5000,
  578. expectedUtilization: 6667,
  579. singleObject: &autoscalingv2.CrossVersionObjectReference{
  580. Kind: "Deployment",
  581. APIVersion: "apps/v1",
  582. Name: "some-deployment",
  583. },
  584. },
  585. }
  586. tc.runTest(t)
  587. }
  588. func TestReplicaCalcScaleUpCMObjectIgnoresUnreadyPods(t *testing.T) {
  589. tc := replicaCalcTestCase{
  590. currentReplicas: 3,
  591. expectedReplicas: 5, // If we did not ignore unready pods, we'd expect 15 replicas.
  592. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  593. metric: &metricInfo{
  594. name: "qps",
  595. levels: []int64{50000},
  596. targetUtilization: 10000,
  597. expectedUtilization: 50000,
  598. singleObject: &autoscalingv2.CrossVersionObjectReference{
  599. Kind: "Deployment",
  600. APIVersion: "apps/v1",
  601. Name: "some-deployment",
  602. },
  603. },
  604. }
  605. tc.runTest(t)
  606. }
  607. func TestReplicaCalcScaleUpCMExternal(t *testing.T) {
  608. tc := replicaCalcTestCase{
  609. currentReplicas: 1,
  610. expectedReplicas: 2,
  611. metric: &metricInfo{
  612. name: "qps",
  613. levels: []int64{8600},
  614. targetUtilization: 4400,
  615. expectedUtilization: 8600,
  616. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  617. metricType: podMetric,
  618. },
  619. }
  620. tc.runTest(t)
  621. }
  622. func TestReplicaCalcScaleUpCMExternalIgnoresUnreadyPods(t *testing.T) {
  623. tc := replicaCalcTestCase{
  624. currentReplicas: 3,
  625. expectedReplicas: 2, // Would expect 6 if we didn't ignore unready pods
  626. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  627. metric: &metricInfo{
  628. name: "qps",
  629. levels: []int64{8600},
  630. targetUtilization: 4400,
  631. expectedUtilization: 8600,
  632. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  633. metricType: externalMetric,
  634. },
  635. }
  636. tc.runTest(t)
  637. }
  638. func TestReplicaCalcScaleUpCMExternalNoLabels(t *testing.T) {
  639. tc := replicaCalcTestCase{
  640. currentReplicas: 1,
  641. expectedReplicas: 2,
  642. metric: &metricInfo{
  643. name: "qps",
  644. levels: []int64{8600},
  645. targetUtilization: 4400,
  646. expectedUtilization: 8600,
  647. metricType: podMetric,
  648. },
  649. }
  650. tc.runTest(t)
  651. }
  652. func TestReplicaCalcScaleUpPerPodCMExternal(t *testing.T) {
  653. tc := replicaCalcTestCase{
  654. currentReplicas: 3,
  655. expectedReplicas: 4,
  656. metric: &metricInfo{
  657. name: "qps",
  658. levels: []int64{8600},
  659. perPodTargetUtilization: 2150,
  660. expectedUtilization: 2867,
  661. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  662. metricType: externalPerPodMetric,
  663. },
  664. }
  665. tc.runTest(t)
  666. }
  667. func TestReplicaCalcScaleDown(t *testing.T) {
  668. tc := replicaCalcTestCase{
  669. currentReplicas: 5,
  670. expectedReplicas: 3,
  671. resource: &resourceInfo{
  672. name: v1.ResourceCPU,
  673. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  674. levels: []int64{100, 300, 500, 250, 250},
  675. targetUtilization: 50,
  676. expectedUtilization: 28,
  677. expectedValue: numContainersPerPod * 280,
  678. },
  679. }
  680. tc.runTest(t)
  681. }
  682. func TestReplicaCalcScaleDownCM(t *testing.T) {
  683. tc := replicaCalcTestCase{
  684. currentReplicas: 5,
  685. expectedReplicas: 3,
  686. metric: &metricInfo{
  687. name: "qps",
  688. levels: []int64{12000, 12000, 12000, 12000, 12000},
  689. targetUtilization: 20000,
  690. expectedUtilization: 12000,
  691. metricType: podMetric,
  692. },
  693. }
  694. tc.runTest(t)
  695. }
  696. func TestReplicaCalcScaleDownPerPodCMObject(t *testing.T) {
  697. tc := replicaCalcTestCase{
  698. currentReplicas: 5,
  699. expectedReplicas: 3,
  700. metric: &metricInfo{
  701. name: "qps",
  702. levels: []int64{6000},
  703. perPodTargetUtilization: 2000,
  704. expectedUtilization: 1200,
  705. singleObject: &autoscalingv2.CrossVersionObjectReference{
  706. Kind: "Deployment",
  707. APIVersion: "apps/v1",
  708. Name: "some-deployment",
  709. },
  710. metricType: objectPerPodMetric,
  711. },
  712. }
  713. tc.runTest(t)
  714. }
  715. func TestReplicaCalcScaleDownCMObject(t *testing.T) {
  716. tc := replicaCalcTestCase{
  717. currentReplicas: 5,
  718. expectedReplicas: 3,
  719. metric: &metricInfo{
  720. name: "qps",
  721. levels: []int64{12000},
  722. targetUtilization: 20000,
  723. expectedUtilization: 12000,
  724. singleObject: &autoscalingv2.CrossVersionObjectReference{
  725. Kind: "Deployment",
  726. APIVersion: "apps/v1",
  727. Name: "some-deployment",
  728. },
  729. },
  730. }
  731. tc.runTest(t)
  732. }
  733. func TestReplicaCalcScaleDownCMExternal(t *testing.T) {
  734. tc := replicaCalcTestCase{
  735. currentReplicas: 5,
  736. expectedReplicas: 3,
  737. metric: &metricInfo{
  738. name: "qps",
  739. levels: []int64{8600},
  740. targetUtilization: 14334,
  741. expectedUtilization: 8600,
  742. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  743. metricType: externalMetric,
  744. },
  745. }
  746. tc.runTest(t)
  747. }
  748. func TestReplicaCalcScaleDownPerPodCMExternal(t *testing.T) {
  749. tc := replicaCalcTestCase{
  750. currentReplicas: 5,
  751. expectedReplicas: 3,
  752. metric: &metricInfo{
  753. name: "qps",
  754. levels: []int64{8600},
  755. perPodTargetUtilization: 2867,
  756. expectedUtilization: 1720,
  757. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  758. metricType: externalPerPodMetric,
  759. },
  760. }
  761. tc.runTest(t)
  762. }
  763. func TestReplicaCalcScaleDownIncludeUnreadyPods(t *testing.T) {
  764. tc := replicaCalcTestCase{
  765. currentReplicas: 5,
  766. expectedReplicas: 2,
  767. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  768. resource: &resourceInfo{
  769. name: v1.ResourceCPU,
  770. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  771. levels: []int64{100, 300, 500, 250, 250},
  772. targetUtilization: 50,
  773. expectedUtilization: 30,
  774. expectedValue: numContainersPerPod * 300,
  775. },
  776. }
  777. tc.runTest(t)
  778. }
  779. func TestReplicaCalcScaleDownExcludeUnscheduledPods(t *testing.T) {
  780. tc := replicaCalcTestCase{
  781. currentReplicas: 5,
  782. expectedReplicas: 1,
  783. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse},
  784. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodPending, v1.PodPending, v1.PodPending, v1.PodPending},
  785. resource: &resourceInfo{
  786. name: v1.ResourceCPU,
  787. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  788. levels: []int64{100},
  789. targetUtilization: 50,
  790. expectedUtilization: 10,
  791. expectedValue: numContainersPerPod * 100,
  792. },
  793. }
  794. tc.runTest(t)
  795. }
  796. func TestReplicaCalcScaleDownIgnoreHotCpuPods(t *testing.T) {
  797. tc := replicaCalcTestCase{
  798. currentReplicas: 5,
  799. expectedReplicas: 2,
  800. podStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
  801. resource: &resourceInfo{
  802. name: v1.ResourceCPU,
  803. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  804. levels: []int64{100, 300, 500, 250, 250},
  805. targetUtilization: 50,
  806. expectedUtilization: 30,
  807. expectedValue: numContainersPerPod * 300,
  808. },
  809. }
  810. tc.runTest(t)
  811. }
  812. func TestReplicaCalcScaleDownIgnoresFailedPods(t *testing.T) {
  813. tc := replicaCalcTestCase{
  814. currentReplicas: 5,
  815. expectedReplicas: 3,
  816. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  817. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
  818. resource: &resourceInfo{
  819. name: v1.ResourceCPU,
  820. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  821. levels: []int64{100, 300, 500, 250, 250},
  822. targetUtilization: 50,
  823. expectedUtilization: 28,
  824. expectedValue: numContainersPerPod * 280,
  825. },
  826. }
  827. tc.runTest(t)
  828. }
  829. func TestReplicaCalcScaleDownIgnoresDeletionPods(t *testing.T) {
  830. tc := replicaCalcTestCase{
  831. currentReplicas: 5,
  832. expectedReplicas: 3,
  833. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  834. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
  835. podDeletionTimestamp: []bool{false, false, false, false, false, true, true},
  836. resource: &resourceInfo{
  837. name: v1.ResourceCPU,
  838. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  839. levels: []int64{100, 300, 500, 250, 250},
  840. targetUtilization: 50,
  841. expectedUtilization: 28,
  842. expectedValue: numContainersPerPod * 280,
  843. },
  844. }
  845. tc.runTest(t)
  846. }
  847. func TestReplicaCalcTolerance(t *testing.T) {
  848. tc := replicaCalcTestCase{
  849. currentReplicas: 3,
  850. expectedReplicas: 3,
  851. resource: &resourceInfo{
  852. name: v1.ResourceCPU,
  853. requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
  854. levels: []int64{1010, 1030, 1020},
  855. targetUtilization: 100,
  856. expectedUtilization: 102,
  857. expectedValue: numContainersPerPod * 1020,
  858. },
  859. }
  860. tc.runTest(t)
  861. }
  862. func TestReplicaCalcToleranceCM(t *testing.T) {
  863. tc := replicaCalcTestCase{
  864. currentReplicas: 3,
  865. expectedReplicas: 3,
  866. metric: &metricInfo{
  867. name: "qps",
  868. levels: []int64{20000, 21000, 21000},
  869. targetUtilization: 20000,
  870. expectedUtilization: 20666,
  871. metricType: podMetric,
  872. },
  873. }
  874. tc.runTest(t)
  875. }
  876. func TestReplicaCalcToleranceCMObject(t *testing.T) {
  877. tc := replicaCalcTestCase{
  878. currentReplicas: 3,
  879. expectedReplicas: 3,
  880. metric: &metricInfo{
  881. name: "qps",
  882. levels: []int64{20666},
  883. targetUtilization: 20000,
  884. expectedUtilization: 20666,
  885. singleObject: &autoscalingv2.CrossVersionObjectReference{
  886. Kind: "Deployment",
  887. APIVersion: "apps/v1",
  888. Name: "some-deployment",
  889. },
  890. },
  891. }
  892. tc.runTest(t)
  893. }
  894. func TestReplicaCalcTolerancePerPodCMObject(t *testing.T) {
  895. tc := replicaCalcTestCase{
  896. currentReplicas: 4,
  897. expectedReplicas: 4,
  898. metric: &metricInfo{
  899. metricType: objectPerPodMetric,
  900. name: "qps",
  901. levels: []int64{20166},
  902. perPodTargetUtilization: 5000,
  903. expectedUtilization: 5042,
  904. singleObject: &autoscalingv2.CrossVersionObjectReference{
  905. Kind: "Deployment",
  906. APIVersion: "apps/v1",
  907. Name: "some-deployment",
  908. },
  909. },
  910. }
  911. tc.runTest(t)
  912. }
  913. func TestReplicaCalcToleranceCMExternal(t *testing.T) {
  914. tc := replicaCalcTestCase{
  915. currentReplicas: 3,
  916. expectedReplicas: 3,
  917. metric: &metricInfo{
  918. name: "qps",
  919. levels: []int64{8600},
  920. targetUtilization: 8888,
  921. expectedUtilization: 8600,
  922. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  923. metricType: externalMetric,
  924. },
  925. }
  926. tc.runTest(t)
  927. }
  928. func TestReplicaCalcTolerancePerPodCMExternal(t *testing.T) {
  929. tc := replicaCalcTestCase{
  930. currentReplicas: 3,
  931. expectedReplicas: 3,
  932. metric: &metricInfo{
  933. name: "qps",
  934. levels: []int64{8600},
  935. perPodTargetUtilization: 2900,
  936. expectedUtilization: 2867,
  937. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  938. metricType: externalPerPodMetric,
  939. },
  940. }
  941. tc.runTest(t)
  942. }
  943. func TestReplicaCalcSuperfluousMetrics(t *testing.T) {
  944. tc := replicaCalcTestCase{
  945. currentReplicas: 4,
  946. expectedReplicas: 24,
  947. resource: &resourceInfo{
  948. name: v1.ResourceCPU,
  949. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  950. levels: []int64{4000, 9500, 3000, 7000, 3200, 2000},
  951. targetUtilization: 100,
  952. expectedUtilization: 587,
  953. expectedValue: numContainersPerPod * 5875,
  954. },
  955. }
  956. tc.runTest(t)
  957. }
  958. func TestReplicaCalcMissingMetrics(t *testing.T) {
  959. tc := replicaCalcTestCase{
  960. currentReplicas: 4,
  961. expectedReplicas: 3,
  962. resource: &resourceInfo{
  963. name: v1.ResourceCPU,
  964. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  965. levels: []int64{400, 95},
  966. targetUtilization: 100,
  967. expectedUtilization: 24,
  968. expectedValue: 495, // numContainersPerPod * 247, for sufficiently large values of 247
  969. },
  970. }
  971. tc.runTest(t)
  972. }
  973. func TestReplicaCalcEmptyMetrics(t *testing.T) {
  974. tc := replicaCalcTestCase{
  975. currentReplicas: 4,
  976. expectedError: fmt.Errorf("unable to get metrics for resource cpu: no metrics returned from resource metrics API"),
  977. resource: &resourceInfo{
  978. name: v1.ResourceCPU,
  979. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  980. levels: []int64{},
  981. targetUtilization: 100,
  982. },
  983. }
  984. tc.runTest(t)
  985. }
  986. func TestReplicaCalcEmptyCPURequest(t *testing.T) {
  987. tc := replicaCalcTestCase{
  988. currentReplicas: 1,
  989. expectedError: fmt.Errorf("missing request for"),
  990. resource: &resourceInfo{
  991. name: v1.ResourceCPU,
  992. requests: []resource.Quantity{},
  993. levels: []int64{200},
  994. targetUtilization: 100,
  995. },
  996. }
  997. tc.runTest(t)
  998. }
  999. func TestReplicaCalcMissingMetricsNoChangeEq(t *testing.T) {
  1000. tc := replicaCalcTestCase{
  1001. currentReplicas: 2,
  1002. expectedReplicas: 2,
  1003. resource: &resourceInfo{
  1004. name: v1.ResourceCPU,
  1005. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  1006. levels: []int64{1000},
  1007. targetUtilization: 100,
  1008. expectedUtilization: 100,
  1009. expectedValue: numContainersPerPod * 1000,
  1010. },
  1011. }
  1012. tc.runTest(t)
  1013. }
  1014. func TestReplicaCalcMissingMetricsNoChangeGt(t *testing.T) {
  1015. tc := replicaCalcTestCase{
  1016. currentReplicas: 2,
  1017. expectedReplicas: 2,
  1018. resource: &resourceInfo{
  1019. name: v1.ResourceCPU,
  1020. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  1021. levels: []int64{1900},
  1022. targetUtilization: 100,
  1023. expectedUtilization: 190,
  1024. expectedValue: numContainersPerPod * 1900,
  1025. },
  1026. }
  1027. tc.runTest(t)
  1028. }
  1029. func TestReplicaCalcMissingMetricsNoChangeLt(t *testing.T) {
  1030. tc := replicaCalcTestCase{
  1031. currentReplicas: 2,
  1032. expectedReplicas: 2,
  1033. resource: &resourceInfo{
  1034. name: v1.ResourceCPU,
  1035. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  1036. levels: []int64{600},
  1037. targetUtilization: 100,
  1038. expectedUtilization: 60,
  1039. expectedValue: numContainersPerPod * 600,
  1040. },
  1041. }
  1042. tc.runTest(t)
  1043. }
  1044. func TestReplicaCalcMissingMetricsUnreadyChange(t *testing.T) {
  1045. tc := replicaCalcTestCase{
  1046. currentReplicas: 3,
  1047. expectedReplicas: 3,
  1048. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1049. resource: &resourceInfo{
  1050. name: v1.ResourceCPU,
  1051. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1052. levels: []int64{100, 450},
  1053. targetUtilization: 50,
  1054. expectedUtilization: 45,
  1055. expectedValue: numContainersPerPod * 450,
  1056. },
  1057. }
  1058. tc.runTest(t)
  1059. }
  1060. func TestReplicaCalcMissingMetricsHotCpuNoChange(t *testing.T) {
  1061. tc := replicaCalcTestCase{
  1062. currentReplicas: 3,
  1063. expectedReplicas: 3,
  1064. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  1065. resource: &resourceInfo{
  1066. name: v1.ResourceCPU,
  1067. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1068. levels: []int64{100, 450},
  1069. targetUtilization: 50,
  1070. expectedUtilization: 45,
  1071. expectedValue: numContainersPerPod * 450,
  1072. },
  1073. }
  1074. tc.runTest(t)
  1075. }
  1076. func TestReplicaCalcMissingMetricsUnreadyScaleUp(t *testing.T) {
  1077. tc := replicaCalcTestCase{
  1078. currentReplicas: 3,
  1079. expectedReplicas: 4,
  1080. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1081. resource: &resourceInfo{
  1082. name: v1.ResourceCPU,
  1083. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1084. levels: []int64{100, 2000},
  1085. targetUtilization: 50,
  1086. expectedUtilization: 200,
  1087. expectedValue: numContainersPerPod * 2000,
  1088. },
  1089. }
  1090. tc.runTest(t)
  1091. }
  1092. func TestReplicaCalcMissingMetricsHotCpuScaleUp(t *testing.T) {
  1093. tc := replicaCalcTestCase{
  1094. currentReplicas: 3,
  1095. expectedReplicas: 4,
  1096. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1097. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  1098. resource: &resourceInfo{
  1099. name: v1.ResourceCPU,
  1100. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1101. levels: []int64{100, 2000},
  1102. targetUtilization: 50,
  1103. expectedUtilization: 200,
  1104. expectedValue: numContainersPerPod * 2000,
  1105. },
  1106. }
  1107. tc.runTest(t)
  1108. }
  1109. func TestReplicaCalcMissingMetricsUnreadyScaleDown(t *testing.T) {
  1110. tc := replicaCalcTestCase{
  1111. currentReplicas: 4,
  1112. expectedReplicas: 3,
  1113. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue},
  1114. resource: &resourceInfo{
  1115. name: v1.ResourceCPU,
  1116. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1117. levels: []int64{100, 100, 100},
  1118. targetUtilization: 50,
  1119. expectedUtilization: 10,
  1120. expectedValue: numContainersPerPod * 100,
  1121. },
  1122. }
  1123. tc.runTest(t)
  1124. }
  1125. // TestComputedToleranceAlgImplementation is a regression test which
  1126. // back-calculates a minimal percentage for downscaling based on a small percentage
  1127. // increase in pod utilization which is calibrated against the tolerance value.
  1128. func TestReplicaCalcComputedToleranceAlgImplementation(t *testing.T) {
  1129. startPods := int32(10)
  1130. // 150 mCPU per pod.
  1131. totalUsedCPUOfAllPods := int64(startPods * 150)
  1132. // Each pod starts out asking for 2X what is really needed.
  1133. // This means we will have a 50% ratio of used/requested
  1134. totalRequestedCPUOfAllPods := int32(2 * totalUsedCPUOfAllPods)
  1135. requestedToUsed := float64(totalRequestedCPUOfAllPods / int32(totalUsedCPUOfAllPods))
  1136. // Spread the amount we ask over 10 pods. We can add some jitter later in reportedLevels.
  1137. perPodRequested := totalRequestedCPUOfAllPods / startPods
  1138. // Force a minimal scaling event by satisfying (tolerance < 1 - resourcesUsedRatio).
  1139. target := math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .01
  1140. finalCPUPercentTarget := int32(target * 100)
  1141. resourcesUsedRatio := float64(totalUsedCPUOfAllPods) / float64(float64(totalRequestedCPUOfAllPods)*target)
  1142. // i.e. .60 * 20 -> scaled down expectation.
  1143. finalPods := int32(math.Ceil(resourcesUsedRatio * float64(startPods)))
  1144. // To breach tolerance we will create a utilization ratio difference of tolerance to usageRatioToleranceValue)
  1145. tc := replicaCalcTestCase{
  1146. currentReplicas: startPods,
  1147. expectedReplicas: finalPods,
  1148. resource: &resourceInfo{
  1149. name: v1.ResourceCPU,
  1150. levels: []int64{
  1151. totalUsedCPUOfAllPods / 10,
  1152. totalUsedCPUOfAllPods / 10,
  1153. totalUsedCPUOfAllPods / 10,
  1154. totalUsedCPUOfAllPods / 10,
  1155. totalUsedCPUOfAllPods / 10,
  1156. totalUsedCPUOfAllPods / 10,
  1157. totalUsedCPUOfAllPods / 10,
  1158. totalUsedCPUOfAllPods / 10,
  1159. totalUsedCPUOfAllPods / 10,
  1160. totalUsedCPUOfAllPods / 10,
  1161. },
  1162. requests: []resource.Quantity{
  1163. resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"),
  1164. resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"),
  1165. resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"),
  1166. resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"),
  1167. resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"),
  1168. resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"),
  1169. resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"),
  1170. resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"),
  1171. resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
  1172. resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
  1173. },
  1174. targetUtilization: finalCPUPercentTarget,
  1175. expectedUtilization: int32(totalUsedCPUOfAllPods*100) / totalRequestedCPUOfAllPods,
  1176. expectedValue: numContainersPerPod * totalUsedCPUOfAllPods / 10,
  1177. },
  1178. }
  1179. tc.runTest(t)
  1180. // Reuse the data structure above, now testing "unscaling".
  1181. // Now, we test that no scaling happens if we are in a very close margin to the tolerance
  1182. target = math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .004
  1183. finalCPUPercentTarget = int32(target * 100)
  1184. tc.resource.targetUtilization = finalCPUPercentTarget
  1185. tc.currentReplicas = startPods
  1186. tc.expectedReplicas = startPods
  1187. tc.runTest(t)
  1188. }
  1189. func TestGroupPods(t *testing.T) {
  1190. tests := []struct {
  1191. name string
  1192. pods []*v1.Pod
  1193. metrics metricsclient.PodMetricsInfo
  1194. resource v1.ResourceName
  1195. expectReadyPodCount int
  1196. expectIgnoredPods sets.String
  1197. expectMissingPods sets.String
  1198. }{
  1199. {
  1200. "void",
  1201. []*v1.Pod{},
  1202. metricsclient.PodMetricsInfo{},
  1203. v1.ResourceCPU,
  1204. 0,
  1205. sets.NewString(),
  1206. sets.NewString(),
  1207. },
  1208. {
  1209. "count in a ready pod - memory",
  1210. []*v1.Pod{
  1211. {
  1212. ObjectMeta: metav1.ObjectMeta{
  1213. Name: "bentham",
  1214. },
  1215. Status: v1.PodStatus{
  1216. Phase: v1.PodSucceeded,
  1217. },
  1218. },
  1219. },
  1220. metricsclient.PodMetricsInfo{
  1221. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: time.Minute},
  1222. },
  1223. v1.ResourceMemory,
  1224. 1,
  1225. sets.NewString(),
  1226. sets.NewString(),
  1227. },
  1228. {
  1229. "ignore a pod without ready condition - CPU",
  1230. []*v1.Pod{
  1231. {
  1232. ObjectMeta: metav1.ObjectMeta{
  1233. Name: "lucretius",
  1234. },
  1235. Status: v1.PodStatus{
  1236. Phase: v1.PodSucceeded,
  1237. StartTime: &metav1.Time{
  1238. Time: time.Now(),
  1239. },
  1240. },
  1241. },
  1242. },
  1243. metricsclient.PodMetricsInfo{
  1244. "lucretius": metricsclient.PodMetric{Value: 1},
  1245. },
  1246. v1.ResourceCPU,
  1247. 0,
  1248. sets.NewString("lucretius"),
  1249. sets.NewString(),
  1250. },
  1251. {
  1252. "count in a ready pod with fresh metrics during initialization period - CPU",
  1253. []*v1.Pod{
  1254. {
  1255. ObjectMeta: metav1.ObjectMeta{
  1256. Name: "bentham",
  1257. },
  1258. Status: v1.PodStatus{
  1259. Phase: v1.PodSucceeded,
  1260. StartTime: &metav1.Time{
  1261. Time: time.Now().Add(-1 * time.Minute),
  1262. },
  1263. Conditions: []v1.PodCondition{
  1264. {
  1265. Type: v1.PodReady,
  1266. LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
  1267. Status: v1.ConditionTrue,
  1268. },
  1269. },
  1270. },
  1271. },
  1272. },
  1273. metricsclient.PodMetricsInfo{
  1274. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 30 * time.Second},
  1275. },
  1276. v1.ResourceCPU,
  1277. 1,
  1278. sets.NewString(),
  1279. sets.NewString(),
  1280. },
  1281. {
  1282. "ignore a ready pod without fresh metrics during initialization period - CPU",
  1283. []*v1.Pod{
  1284. {
  1285. ObjectMeta: metav1.ObjectMeta{
  1286. Name: "bentham",
  1287. },
  1288. Status: v1.PodStatus{
  1289. Phase: v1.PodSucceeded,
  1290. StartTime: &metav1.Time{
  1291. Time: time.Now().Add(-1 * time.Minute),
  1292. },
  1293. Conditions: []v1.PodCondition{
  1294. {
  1295. Type: v1.PodReady,
  1296. LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
  1297. Status: v1.ConditionTrue,
  1298. },
  1299. },
  1300. },
  1301. },
  1302. },
  1303. metricsclient.PodMetricsInfo{
  1304. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 60 * time.Second},
  1305. },
  1306. v1.ResourceCPU,
  1307. 0,
  1308. sets.NewString("bentham"),
  1309. sets.NewString(),
  1310. },
  1311. {
  1312. "ignore an unready pod during initialization period - CPU",
  1313. []*v1.Pod{
  1314. {
  1315. ObjectMeta: metav1.ObjectMeta{
  1316. Name: "lucretius",
  1317. },
  1318. Status: v1.PodStatus{
  1319. Phase: v1.PodSucceeded,
  1320. StartTime: &metav1.Time{
  1321. Time: time.Now().Add(-10 * time.Minute),
  1322. },
  1323. Conditions: []v1.PodCondition{
  1324. {
  1325. Type: v1.PodReady,
  1326. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 54*time.Second)},
  1327. Status: v1.ConditionFalse,
  1328. },
  1329. },
  1330. },
  1331. },
  1332. },
  1333. metricsclient.PodMetricsInfo{
  1334. "lucretius": metricsclient.PodMetric{Value: 1},
  1335. },
  1336. v1.ResourceCPU,
  1337. 0,
  1338. sets.NewString("lucretius"),
  1339. sets.NewString(),
  1340. },
  1341. {
  1342. "count in a ready pod without fresh metrics after initialization period - CPU",
  1343. []*v1.Pod{
  1344. {
  1345. ObjectMeta: metav1.ObjectMeta{
  1346. Name: "bentham",
  1347. },
  1348. Status: v1.PodStatus{
  1349. Phase: v1.PodSucceeded,
  1350. StartTime: &metav1.Time{
  1351. Time: time.Now().Add(-3 * time.Minute),
  1352. },
  1353. Conditions: []v1.PodCondition{
  1354. {
  1355. Type: v1.PodReady,
  1356. LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
  1357. Status: v1.ConditionTrue,
  1358. },
  1359. },
  1360. },
  1361. },
  1362. },
  1363. metricsclient.PodMetricsInfo{
  1364. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now().Add(-2 * time.Minute), Window: time.Minute},
  1365. },
  1366. v1.ResourceCPU,
  1367. 1,
  1368. sets.NewString(),
  1369. sets.NewString(),
  1370. },
  1371. {
  1372. "count in an unready pod that was ready after initialization period - CPU",
  1373. []*v1.Pod{
  1374. {
  1375. ObjectMeta: metav1.ObjectMeta{
  1376. Name: "lucretius",
  1377. },
  1378. Status: v1.PodStatus{
  1379. Phase: v1.PodSucceeded,
  1380. StartTime: &metav1.Time{
  1381. Time: time.Now().Add(-10 * time.Minute),
  1382. },
  1383. Conditions: []v1.PodCondition{
  1384. {
  1385. Type: v1.PodReady,
  1386. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9 * time.Minute)},
  1387. Status: v1.ConditionFalse,
  1388. },
  1389. },
  1390. },
  1391. },
  1392. },
  1393. metricsclient.PodMetricsInfo{
  1394. "lucretius": metricsclient.PodMetric{Value: 1},
  1395. },
  1396. v1.ResourceCPU,
  1397. 1,
  1398. sets.NewString(),
  1399. sets.NewString(),
  1400. },
  1401. {
  1402. "ignore pod that has never been ready after initialization period - CPU",
  1403. []*v1.Pod{
  1404. {
  1405. ObjectMeta: metav1.ObjectMeta{
  1406. Name: "lucretius",
  1407. },
  1408. Status: v1.PodStatus{
  1409. Phase: v1.PodSucceeded,
  1410. StartTime: &metav1.Time{
  1411. Time: time.Now().Add(-10 * time.Minute),
  1412. },
  1413. Conditions: []v1.PodCondition{
  1414. {
  1415. Type: v1.PodReady,
  1416. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 50*time.Second)},
  1417. Status: v1.ConditionFalse,
  1418. },
  1419. },
  1420. },
  1421. },
  1422. },
  1423. metricsclient.PodMetricsInfo{
  1424. "lucretius": metricsclient.PodMetric{Value: 1},
  1425. },
  1426. v1.ResourceCPU,
  1427. 1,
  1428. sets.NewString(),
  1429. sets.NewString(),
  1430. },
  1431. {
  1432. "a missing pod",
  1433. []*v1.Pod{
  1434. {
  1435. ObjectMeta: metav1.ObjectMeta{
  1436. Name: "epicurus",
  1437. },
  1438. Status: v1.PodStatus{
  1439. Phase: v1.PodSucceeded,
  1440. StartTime: &metav1.Time{
  1441. Time: time.Now().Add(-3 * time.Minute),
  1442. },
  1443. },
  1444. },
  1445. },
  1446. metricsclient.PodMetricsInfo{},
  1447. v1.ResourceCPU,
  1448. 0,
  1449. sets.NewString(),
  1450. sets.NewString("epicurus"),
  1451. },
  1452. {
  1453. "several pods",
  1454. []*v1.Pod{
  1455. {
  1456. ObjectMeta: metav1.ObjectMeta{
  1457. Name: "lucretius",
  1458. },
  1459. Status: v1.PodStatus{
  1460. Phase: v1.PodSucceeded,
  1461. StartTime: &metav1.Time{
  1462. Time: time.Now(),
  1463. },
  1464. },
  1465. },
  1466. {
  1467. ObjectMeta: metav1.ObjectMeta{
  1468. Name: "niccolo",
  1469. },
  1470. Status: v1.PodStatus{
  1471. Phase: v1.PodSucceeded,
  1472. StartTime: &metav1.Time{
  1473. Time: time.Now().Add(-3 * time.Minute),
  1474. },
  1475. Conditions: []v1.PodCondition{
  1476. {
  1477. Type: v1.PodReady,
  1478. LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
  1479. Status: v1.ConditionTrue,
  1480. },
  1481. },
  1482. },
  1483. },
  1484. {
  1485. ObjectMeta: metav1.ObjectMeta{
  1486. Name: "epicurus",
  1487. },
  1488. Status: v1.PodStatus{
  1489. Phase: v1.PodSucceeded,
  1490. StartTime: &metav1.Time{
  1491. Time: time.Now().Add(-3 * time.Minute),
  1492. },
  1493. },
  1494. },
  1495. },
  1496. metricsclient.PodMetricsInfo{
  1497. "lucretius": metricsclient.PodMetric{Value: 1},
  1498. "niccolo": metricsclient.PodMetric{Value: 1},
  1499. },
  1500. v1.ResourceCPU,
  1501. 1,
  1502. sets.NewString("lucretius"),
  1503. sets.NewString("epicurus"),
  1504. },
  1505. {
  1506. name: "pending pods are ignored",
  1507. pods: []*v1.Pod{
  1508. {
  1509. ObjectMeta: metav1.ObjectMeta{
  1510. Name: "unscheduled",
  1511. },
  1512. Status: v1.PodStatus{
  1513. Phase: v1.PodPending,
  1514. },
  1515. },
  1516. },
  1517. metrics: metricsclient.PodMetricsInfo{},
  1518. resource: v1.ResourceCPU,
  1519. expectReadyPodCount: 0,
  1520. expectIgnoredPods: sets.NewString("unscheduled"),
  1521. expectMissingPods: sets.NewString(),
  1522. },
  1523. }
  1524. for _, tc := range tests {
  1525. t.Run(tc.name, func(t *testing.T) {
  1526. readyPodCount, ignoredPods, missingPods := groupPods(tc.pods, tc.metrics, tc.resource, defaultTestingCpuInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
  1527. if readyPodCount != tc.expectReadyPodCount {
  1528. t.Errorf("%s got readyPodCount %d, expected %d", tc.name, readyPodCount, tc.expectReadyPodCount)
  1529. }
  1530. if !ignoredPods.Equal(tc.expectIgnoredPods) {
  1531. t.Errorf("%s got unreadyPods %v, expected %v", tc.name, ignoredPods, tc.expectIgnoredPods)
  1532. }
  1533. if !missingPods.Equal(tc.expectMissingPods) {
  1534. t.Errorf("%s got missingPods %v, expected %v", tc.name, missingPods, tc.expectMissingPods)
  1535. }
  1536. })
  1537. }
  1538. }
  1539. // TODO: add more tests