sigs.k8s.io/kueue@v0.6.2/pkg/workload/workload_test.go (about) 1 /* 2 Copyright 2022 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 workload 18 19 import ( 20 "context" 21 "testing" 22 "time" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 corev1 "k8s.io/api/core/v1" 27 "k8s.io/apimachinery/pkg/api/resource" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/utils/ptr" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 32 config "sigs.k8s.io/kueue/apis/config/v1beta1" 33 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 34 utiltesting "sigs.k8s.io/kueue/pkg/util/testing" 35 ) 36 37 func TestNewInfo(t *testing.T) { 38 cases := map[string]struct { 39 workload kueue.Workload 40 wantInfo Info 41 }{ 42 "pending": { 43 workload: *utiltesting.MakeWorkload("", ""). 44 Request(corev1.ResourceCPU, "10m"). 45 Request(corev1.ResourceMemory, "512Ki"). 46 Obj(), 47 wantInfo: Info{ 48 TotalRequests: []PodSetResources{ 49 { 50 Name: "main", 51 Requests: Requests{ 52 corev1.ResourceCPU: 10, 53 corev1.ResourceMemory: 512 * 1024, 54 }, 55 Count: 1, 56 }, 57 }, 58 }, 59 }, 60 "pending with reclaim": { 61 workload: *utiltesting.MakeWorkload("", ""). 62 PodSets( 63 *utiltesting.MakePodSet("main", 5). 64 Request(corev1.ResourceCPU, "10m"). 65 Request(corev1.ResourceMemory, "512Ki"). 66 Obj(), 67 ). 68 ReclaimablePods( 69 kueue.ReclaimablePod{ 70 Name: "main", 71 Count: 2, 72 }, 73 ). 74 Obj(), 75 wantInfo: Info{ 76 TotalRequests: []PodSetResources{ 77 { 78 Name: "main", 79 Requests: Requests{ 80 corev1.ResourceCPU: 3 * 10, 81 corev1.ResourceMemory: 3 * 512 * 1024, 82 }, 83 Count: 3, 84 }, 85 }, 86 }, 87 }, 88 "admitted": { 89 workload: *utiltesting.MakeWorkload("", ""). 90 PodSets( 91 *utiltesting.MakePodSet("driver", 1). 92 Request(corev1.ResourceCPU, "10m"). 93 Request(corev1.ResourceMemory, "512Ki"). 94 Obj(), 95 *utiltesting.MakePodSet("workers", 3). 96 Request(corev1.ResourceCPU, "5m"). 97 Request(corev1.ResourceMemory, "1Mi"). 98 Request("ex.com/gpu", "1"). 99 Obj(), 100 ). 101 ReserveQuota(utiltesting.MakeAdmission("foo"). 102 PodSets( 103 kueue.PodSetAssignment{ 104 Name: "driver", 105 Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{ 106 corev1.ResourceCPU: "on-demand", 107 }, 108 ResourceUsage: corev1.ResourceList{ 109 corev1.ResourceCPU: resource.MustParse("10m"), 110 corev1.ResourceMemory: resource.MustParse("512Ki"), 111 }, 112 Count: ptr.To[int32](1), 113 }, 114 kueue.PodSetAssignment{ 115 Name: "workers", 116 ResourceUsage: corev1.ResourceList{ 117 corev1.ResourceCPU: resource.MustParse("15m"), 118 corev1.ResourceMemory: resource.MustParse("3Mi"), 119 "ex.com/gpu": resource.MustParse("3"), 120 }, 121 Count: ptr.To[int32](3), 122 }, 123 ). 124 Obj()). 125 Obj(), 126 wantInfo: Info{ 127 ClusterQueue: "foo", 128 TotalRequests: []PodSetResources{ 129 { 130 Name: "driver", 131 Requests: Requests{ 132 corev1.ResourceCPU: 10, 133 corev1.ResourceMemory: 512 * 1024, 134 }, 135 Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{ 136 corev1.ResourceCPU: "on-demand", 137 }, 138 Count: 1, 139 }, 140 { 141 Name: "workers", 142 Requests: Requests{ 143 corev1.ResourceCPU: 15, 144 corev1.ResourceMemory: 3 * 1024 * 1024, 145 "ex.com/gpu": 3, 146 }, 147 Count: 3, 148 }, 149 }, 150 }, 151 }, 152 "admitted with reclaim": { 153 workload: *utiltesting.MakeWorkload("", ""). 154 PodSets( 155 *utiltesting.MakePodSet("main", 5). 156 Request(corev1.ResourceCPU, "10m"). 157 Request(corev1.ResourceMemory, "10Ki"). 158 Obj(), 159 ). 160 ReserveQuota( 161 utiltesting.MakeAdmission(""). 162 Assignment(corev1.ResourceCPU, "f1", "30m"). 163 Assignment(corev1.ResourceMemory, "f1", "30Ki"). 164 AssignmentPodCount(3). 165 Obj(), 166 ). 167 ReclaimablePods( 168 kueue.ReclaimablePod{ 169 Name: "main", 170 Count: 2, 171 }, 172 ). 173 Obj(), 174 wantInfo: Info{ 175 TotalRequests: []PodSetResources{ 176 { 177 Name: "main", 178 Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{ 179 corev1.ResourceCPU: "f1", 180 corev1.ResourceMemory: "f1", 181 }, 182 Requests: Requests{ 183 corev1.ResourceCPU: 3 * 10, 184 corev1.ResourceMemory: 3 * 10 * 1024, 185 }, 186 Count: 3, 187 }, 188 }, 189 }, 190 }, 191 } 192 for name, tc := range cases { 193 t.Run(name, func(t *testing.T) { 194 info := NewInfo(&tc.workload) 195 if diff := cmp.Diff(info, &tc.wantInfo, cmpopts.IgnoreFields(Info{}, "Obj")); diff != "" { 196 t.Errorf("NewInfo(_) = (-want,+got):\n%s", diff) 197 } 198 }) 199 } 200 } 201 202 var ignoreConditionTimestamps = cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime") 203 204 func TestUpdateWorkloadStatus(t *testing.T) { 205 cases := map[string]struct { 206 oldStatus kueue.WorkloadStatus 207 condType string 208 condStatus metav1.ConditionStatus 209 reason string 210 message string 211 wantStatus kueue.WorkloadStatus 212 }{ 213 "initial empty": { 214 condType: kueue.WorkloadQuotaReserved, 215 condStatus: metav1.ConditionFalse, 216 reason: "Pending", 217 message: "didn't fit", 218 wantStatus: kueue.WorkloadStatus{ 219 Conditions: []metav1.Condition{ 220 { 221 Type: kueue.WorkloadQuotaReserved, 222 Status: metav1.ConditionFalse, 223 Reason: "Pending", 224 Message: "didn't fit", 225 }, 226 }, 227 }, 228 }, 229 "same condition type": { 230 oldStatus: kueue.WorkloadStatus{ 231 Conditions: []metav1.Condition{ 232 { 233 Type: kueue.WorkloadQuotaReserved, 234 Status: metav1.ConditionFalse, 235 Reason: "Pending", 236 Message: "didn't fit", 237 }, 238 }, 239 }, 240 condType: kueue.WorkloadQuotaReserved, 241 condStatus: metav1.ConditionTrue, 242 reason: "Admitted", 243 wantStatus: kueue.WorkloadStatus{ 244 Conditions: []metav1.Condition{ 245 { 246 Type: kueue.WorkloadQuotaReserved, 247 Status: metav1.ConditionTrue, 248 Reason: "Admitted", 249 }, 250 }, 251 }, 252 }, 253 } 254 for name, tc := range cases { 255 t.Run(name, func(t *testing.T) { 256 workload := utiltesting.MakeWorkload("foo", "bar").Obj() 257 workload.Status = tc.oldStatus 258 cl := utiltesting.NewFakeClient(workload) 259 ctx := context.Background() 260 err := UpdateStatus(ctx, cl, workload, tc.condType, tc.condStatus, tc.reason, tc.message, "manager-prefix") 261 if err != nil { 262 t.Fatalf("Failed updating status: %v", err) 263 } 264 var updatedWl kueue.Workload 265 if err := cl.Get(ctx, client.ObjectKeyFromObject(workload), &updatedWl); err != nil { 266 t.Fatalf("Failed obtaining updated object: %v", err) 267 } 268 if diff := cmp.Diff(tc.wantStatus, updatedWl.Status, ignoreConditionTimestamps); diff != "" { 269 t.Errorf("Unexpected status after updating (-want,+got):\n%s", diff) 270 } 271 }) 272 } 273 } 274 275 func TestGetQueueOrderTimestamp(t *testing.T) { 276 var ( 277 evictionOrdering = Ordering{PodsReadyRequeuingTimestamp: config.EvictionTimestamp} 278 creationOrdering = Ordering{PodsReadyRequeuingTimestamp: config.CreationTimestamp} 279 ) 280 281 creationTime := metav1.Now() 282 conditionTime := metav1.NewTime(time.Now().Add(time.Hour)) 283 284 cases := map[string]struct { 285 wl *kueue.Workload 286 want map[Ordering]metav1.Time 287 }{ 288 "no condition": { 289 wl: utiltesting.MakeWorkload("name", "ns"). 290 Creation(creationTime.Time). 291 Obj(), 292 want: map[Ordering]metav1.Time{ 293 evictionOrdering: creationTime, 294 creationOrdering: creationTime, 295 }, 296 }, 297 "evicted by preemption": { 298 wl: utiltesting.MakeWorkload("name", "ns"). 299 Creation(creationTime.Time). 300 Condition(metav1.Condition{ 301 Type: kueue.WorkloadEvicted, 302 Status: metav1.ConditionTrue, 303 LastTransitionTime: conditionTime, 304 Reason: kueue.WorkloadEvictedByPreemption, 305 }). 306 Obj(), 307 want: map[Ordering]metav1.Time{ 308 evictionOrdering: creationTime, 309 creationOrdering: creationTime, 310 }, 311 }, 312 "evicted by PodsReady timeout": { 313 wl: utiltesting.MakeWorkload("name", "ns"). 314 Creation(creationTime.Time). 315 Condition(metav1.Condition{ 316 Type: kueue.WorkloadEvicted, 317 Status: metav1.ConditionTrue, 318 LastTransitionTime: conditionTime, 319 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 320 }). 321 Obj(), 322 want: map[Ordering]metav1.Time{ 323 evictionOrdering: conditionTime, 324 creationOrdering: creationTime, 325 }, 326 }, 327 "after eviction": { 328 wl: utiltesting.MakeWorkload("name", "ns"). 329 Creation(creationTime.Time). 330 Condition(metav1.Condition{ 331 Type: kueue.WorkloadEvicted, 332 Status: metav1.ConditionFalse, 333 LastTransitionTime: conditionTime, 334 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 335 }). 336 Obj(), 337 want: map[Ordering]metav1.Time{ 338 evictionOrdering: creationTime, 339 creationOrdering: creationTime, 340 }, 341 }, 342 } 343 for name, tc := range cases { 344 t.Run(name, func(t *testing.T) { 345 for ordering, want := range tc.want { 346 gotTime := ordering.GetQueueOrderTimestamp(tc.wl) 347 if diff := cmp.Diff(*gotTime, want); diff != "" { 348 t.Errorf("Unexpected time (-want,+got):\n%s", diff) 349 } 350 } 351 }) 352 } 353 } 354 355 func TestReclaimablePodsAreEqual(t *testing.T) { 356 cases := map[string]struct { 357 a, b []kueue.ReclaimablePod 358 wantResult bool 359 }{ 360 "both empty": { 361 b: []kueue.ReclaimablePod{}, 362 wantResult: true, 363 }, 364 "one empty": { 365 b: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}}, 366 wantResult: false, 367 }, 368 "one value missmatch": { 369 a: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}, {Name: "rp2", Count: 2}}, 370 b: []kueue.ReclaimablePod{{Name: "rp2", Count: 1}, {Name: "rp1", Count: 1}}, 371 wantResult: false, 372 }, 373 "one name missmatch": { 374 a: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}, {Name: "rp2", Count: 2}}, 375 b: []kueue.ReclaimablePod{{Name: "rp3", Count: 3}, {Name: "rp1", Count: 1}}, 376 wantResult: false, 377 }, 378 "length missmatch": { 379 a: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}, {Name: "rp2", Count: 2}}, 380 b: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}}, 381 wantResult: false, 382 }, 383 "equal": { 384 a: []kueue.ReclaimablePod{{Name: "rp1", Count: 1}, {Name: "rp2", Count: 2}}, 385 b: []kueue.ReclaimablePod{{Name: "rp2", Count: 2}, {Name: "rp1", Count: 1}}, 386 wantResult: true, 387 }, 388 } 389 for name, tc := range cases { 390 t.Run(name, func(t *testing.T) { 391 result := ReclaimablePodsAreEqual(tc.a, tc.b) 392 if diff := cmp.Diff(result, tc.wantResult); diff != "" { 393 t.Errorf("Unexpected time (-want,+got):\n%s", diff) 394 } 395 }) 396 } 397 } 398 399 func TestAssignmentClusterQueueState(t *testing.T) { 400 cases := map[string]struct { 401 state *AssigmentClusterQueueState 402 wantPendingFlavors bool 403 }{ 404 "no info": { 405 wantPendingFlavors: false, 406 }, 407 "all done": { 408 state: &AssigmentClusterQueueState{ 409 LastTriedFlavorIdx: []map[corev1.ResourceName]int{ 410 { 411 corev1.ResourceCPU: -1, 412 corev1.ResourceMemory: -1, 413 }, 414 { 415 corev1.ResourceMemory: -1, 416 }, 417 }, 418 }, 419 wantPendingFlavors: false, 420 }, 421 "some pending": { 422 state: &AssigmentClusterQueueState{ 423 LastTriedFlavorIdx: []map[corev1.ResourceName]int{ 424 { 425 corev1.ResourceCPU: 0, 426 corev1.ResourceMemory: -1, 427 }, 428 { 429 corev1.ResourceMemory: 1, 430 }, 431 }, 432 }, 433 wantPendingFlavors: true, 434 }, 435 "all pending": { 436 state: &AssigmentClusterQueueState{ 437 LastTriedFlavorIdx: []map[corev1.ResourceName]int{ 438 { 439 corev1.ResourceCPU: 1, 440 corev1.ResourceMemory: 0, 441 }, 442 { 443 corev1.ResourceMemory: 1, 444 }, 445 }, 446 }, 447 wantPendingFlavors: true, 448 }, 449 } 450 for name, tc := range cases { 451 t.Run(name, func(t *testing.T) { 452 got := tc.state.PendingFlavors() 453 if got != tc.wantPendingFlavors { 454 t.Errorf("state.PendingFlavors() = %t, want %t", got, tc.wantPendingFlavors) 455 } 456 }) 457 } 458 } 459 460 func TestHasRequeueState(t *testing.T) { 461 cases := map[string]struct { 462 workload *kueue.Workload 463 want bool 464 }{ 465 "workload has requeue state": { 466 workload: utiltesting.MakeWorkload("test", "test").RequeueState(ptr.To[int32](5), ptr.To(metav1.Now())).Obj(), 467 want: true, 468 }, 469 "workload doesn't have requeue state": { 470 workload: utiltesting.MakeWorkload("test", "test").RequeueState(nil, nil).Obj(), 471 }, 472 } 473 for name, tc := range cases { 474 t.Run(name, func(t *testing.T) { 475 got := HasRequeueState(tc.workload) 476 if tc.want != got { 477 t.Errorf("Unexpected result from HasRequeuState\nwant:%v\ngot:%v\n", tc.want, got) 478 } 479 }) 480 } 481 } 482 483 func TestIsEvictedByDeactivation(t *testing.T) { 484 cases := map[string]struct { 485 workload *kueue.Workload 486 want bool 487 }{ 488 "evicted condition doesn't exist": { 489 workload: utiltesting.MakeWorkload("test", "test").Obj(), 490 }, 491 "evicted condition with false status": { 492 workload: utiltesting.MakeWorkload("test", "test"). 493 Condition(metav1.Condition{ 494 Type: kueue.WorkloadEvicted, 495 Reason: kueue.WorkloadEvictedByDeactivation, 496 Status: metav1.ConditionFalse, 497 }). 498 Obj(), 499 }, 500 "evicted condition with PodsReadyTimeout reason": { 501 workload: utiltesting.MakeWorkload("test", "test"). 502 Condition(metav1.Condition{ 503 Type: kueue.WorkloadEvicted, 504 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 505 Status: metav1.ConditionTrue, 506 }). 507 Obj(), 508 }, 509 "evicted condition with InactiveWorkload reason": { 510 workload: utiltesting.MakeWorkload("test", "test"). 511 Condition(metav1.Condition{ 512 Type: kueue.WorkloadEvicted, 513 Reason: kueue.WorkloadEvictedByDeactivation, 514 Status: metav1.ConditionTrue, 515 }). 516 Obj(), 517 want: true, 518 }, 519 } 520 for name, tc := range cases { 521 t.Run(name, func(t *testing.T) { 522 got := IsEvictedByDeactivation(tc.workload) 523 if tc.want != got { 524 t.Errorf("Unexpected result from IsEvictedByDeactivation\nwant:%v\ngot:%v\n", tc.want, got) 525 } 526 }) 527 } 528 } 529 530 func TestIsEvictedByPodsReadyTimeout(t *testing.T) { 531 cases := map[string]struct { 532 workload *kueue.Workload 533 wantEvictedByTimeout bool 534 wantCondition *metav1.Condition 535 }{ 536 "evicted condition doesn't exist": { 537 workload: utiltesting.MakeWorkload("test", "test").Obj(), 538 }, 539 "evicted condition with false status": { 540 workload: utiltesting.MakeWorkload("test", "test"). 541 Condition(metav1.Condition{ 542 Type: kueue.WorkloadEvicted, 543 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 544 Status: metav1.ConditionFalse, 545 }). 546 Obj(), 547 }, 548 "evicted condition with Preempted reason": { 549 workload: utiltesting.MakeWorkload("test", "test"). 550 Condition(metav1.Condition{ 551 Type: kueue.WorkloadEvicted, 552 Reason: kueue.WorkloadEvictedByPreemption, 553 Status: metav1.ConditionTrue, 554 }). 555 Obj(), 556 }, 557 "evicted condition with PodsReadyTimeout reason": { 558 workload: utiltesting.MakeWorkload("test", "test"). 559 Condition(metav1.Condition{ 560 Type: kueue.WorkloadEvicted, 561 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 562 Status: metav1.ConditionTrue, 563 }). 564 Obj(), 565 wantEvictedByTimeout: true, 566 wantCondition: &metav1.Condition{ 567 Type: kueue.WorkloadEvicted, 568 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 569 Status: metav1.ConditionTrue, 570 }, 571 }, 572 } 573 for name, tc := range cases { 574 t.Run(name, func(t *testing.T) { 575 gotCondition, gotEvictedByTimeout := IsEvictedByPodsReadyTimeout(tc.workload) 576 if tc.wantEvictedByTimeout != gotEvictedByTimeout { 577 t.Errorf("Unexpected evictedByTimeout from IsEvictedByPodsReadyTimeout\nwant:%v\ngot:%v\n", 578 tc.wantEvictedByTimeout, gotEvictedByTimeout) 579 } 580 if diff := cmp.Diff(tc.wantCondition, gotCondition, 581 cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")); len(diff) != 0 { 582 t.Errorf("Unexpected condition from IsEvictedByPodsReadyTimeout: (-want,+got):\n%s", diff) 583 } 584 }) 585 } 586 }