sigs.k8s.io/kueue@v0.6.2/pkg/controller/jobs/jobset/jobset_controller_test.go (about) 1 /* 2 Copyright 2023 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 jobset 18 19 import ( 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/google/go-cmp/cmp/cmpopts" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/client-go/tools/record" 27 "sigs.k8s.io/controller-runtime/pkg/client" 28 "sigs.k8s.io/controller-runtime/pkg/reconcile" 29 jobset "sigs.k8s.io/jobset/api/jobset/v1alpha2" 30 31 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 32 "sigs.k8s.io/kueue/pkg/constants" 33 "sigs.k8s.io/kueue/pkg/controller/jobframework" 34 utiltesting "sigs.k8s.io/kueue/pkg/util/testing" 35 testingjobset "sigs.k8s.io/kueue/pkg/util/testingjobs/jobset" 36 ) 37 38 func TestPodsReady(t *testing.T) { 39 testcases := map[string]struct { 40 jobSet jobset.JobSet 41 want bool 42 }{ 43 "all jobs are ready": { 44 jobSet: *testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 45 testingjobset.ReplicatedJobRequirements{ 46 Name: "replicated-job-1", 47 Replicas: 2, 48 Parallelism: 1, 49 Completions: 1, 50 }, 51 testingjobset.ReplicatedJobRequirements{ 52 Name: "replicated-job-2", 53 Replicas: 3, 54 Parallelism: 1, 55 Completions: 1, 56 }, 57 ).JobsStatus( 58 jobset.ReplicatedJobStatus{ 59 Name: "replicated-job-1", 60 Ready: 1, 61 Succeeded: 1, 62 }, 63 jobset.ReplicatedJobStatus{ 64 Name: "replicated-job-2", 65 Ready: 3, 66 Succeeded: 0, 67 }, 68 ).Obj(), 69 want: true, 70 }, 71 "not all jobs are ready": { 72 jobSet: *testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 73 testingjobset.ReplicatedJobRequirements{ 74 Name: "replicated-job-1", 75 Replicas: 2, 76 Parallelism: 1, 77 Completions: 1, 78 }, 79 testingjobset.ReplicatedJobRequirements{ 80 Name: "replicated-job-2", 81 Replicas: 3, 82 Parallelism: 1, 83 Completions: 1, 84 }, 85 ).JobsStatus( 86 jobset.ReplicatedJobStatus{ 87 Name: "replicated-job-1", 88 Ready: 1, 89 Succeeded: 0, 90 }, 91 jobset.ReplicatedJobStatus{ 92 Name: "replicated-job-2", 93 Ready: 1, 94 Succeeded: 2, 95 }, 96 ).Obj(), 97 want: false, 98 }, 99 } 100 101 for name, tc := range testcases { 102 t.Run(name, func(t *testing.T) { 103 jobSet := (JobSet)(tc.jobSet) 104 got := jobSet.PodsReady() 105 if tc.want != got { 106 t.Errorf("Unexpected response (want: %v, got: %v)", tc.want, got) 107 } 108 }) 109 } 110 } 111 112 func TestReclaimablePods(t *testing.T) { 113 baseWrapper := testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 114 testingjobset.ReplicatedJobRequirements{ 115 Name: "replicated-job-1", 116 Replicas: 1, 117 Parallelism: 2, 118 Completions: 2, 119 }, 120 testingjobset.ReplicatedJobRequirements{ 121 Name: "replicated-job-2", 122 Replicas: 2, 123 Parallelism: 3, 124 Completions: 6, 125 }, 126 ) 127 128 testcases := map[string]struct { 129 jobSet *jobset.JobSet 130 want []kueue.ReclaimablePod 131 }{ 132 "no status": { 133 jobSet: baseWrapper.DeepCopy().Obj(), 134 want: nil, 135 }, 136 "empty jobs status": { 137 jobSet: baseWrapper.DeepCopy().JobsStatus().Obj(), 138 want: nil, 139 }, 140 "single job done": { 141 jobSet: baseWrapper.DeepCopy().JobsStatus(jobset.ReplicatedJobStatus{ 142 Name: "replicated-job-1", 143 Succeeded: 1, 144 }).Obj(), 145 want: []kueue.ReclaimablePod{{ 146 Name: "replicated-job-1", 147 Count: 2, 148 }}, 149 }, 150 "single job partial done": { 151 jobSet: baseWrapper.DeepCopy().JobsStatus(jobset.ReplicatedJobStatus{ 152 Name: "replicated-job-2", 153 Succeeded: 1, 154 }).Obj(), 155 want: []kueue.ReclaimablePod{{ 156 Name: "replicated-job-2", 157 Count: 3, 158 }}, 159 }, 160 "all done": { 161 jobSet: baseWrapper.DeepCopy().JobsStatus( 162 jobset.ReplicatedJobStatus{ 163 Name: "replicated-job-1", 164 Succeeded: 1, 165 }, 166 jobset.ReplicatedJobStatus{ 167 Name: "replicated-job-2", 168 Succeeded: 2, 169 }, 170 ).Obj(), 171 want: []kueue.ReclaimablePod{ 172 { 173 Name: "replicated-job-1", 174 Count: 2, 175 }, 176 { 177 Name: "replicated-job-2", 178 Count: 6, 179 }, 180 }, 181 }, 182 } 183 184 for name, tc := range testcases { 185 t.Run(name, func(t *testing.T) { 186 jobSet := (*JobSet)(tc.jobSet) 187 got, err := jobSet.ReclaimablePods() 188 if err != nil { 189 t.Fatalf("Unexpected error: %s", err) 190 } 191 if diff := cmp.Diff(tc.want, got); diff != "" { 192 t.Errorf("Unexpected Reclaimable pods (-want +got):\n%s", diff) 193 } 194 }) 195 } 196 } 197 198 var ( 199 jobCmpOpts = []cmp.Option{ 200 cmpopts.EquateEmpty(), 201 cmpopts.IgnoreFields(jobset.JobSet{}, "TypeMeta", "ObjectMeta"), 202 } 203 workloadCmpOpts = []cmp.Option{ 204 cmpopts.EquateEmpty(), 205 cmpopts.IgnoreFields(kueue.Workload{}, "TypeMeta", "ObjectMeta"), 206 cmpopts.IgnoreFields(kueue.WorkloadSpec{}, "Priority"), 207 cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), 208 cmpopts.IgnoreFields(kueue.PodSet{}, "Template"), 209 } 210 ) 211 212 func TestReconciler(t *testing.T) { 213 baseWPCWrapper := utiltesting.MakeWorkloadPriorityClass("test-wpc"). 214 PriorityValue(100) 215 basePCWrapper := utiltesting.MakePriorityClass("test-pc"). 216 PriorityValue(200) 217 218 cases := map[string]struct { 219 reconcilerOptions []jobframework.Option 220 job *jobset.JobSet 221 priorityClasses []client.Object 222 wantJob *jobset.JobSet 223 wantWorkloads []kueue.Workload 224 wantErr error 225 }{ 226 "workload is created with podsets": { 227 reconcilerOptions: []jobframework.Option{ 228 jobframework.WithManageJobsWithoutQueueName(true), 229 }, 230 job: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 231 testingjobset.ReplicatedJobRequirements{ 232 Name: "replicated-job-1", 233 Replicas: 1, 234 Completions: 1, 235 Parallelism: 1, 236 }, 237 testingjobset.ReplicatedJobRequirements{ 238 Name: "replicated-job-2", 239 Replicas: 2, 240 Completions: 2, 241 Parallelism: 2, 242 }, 243 ).Obj(), 244 wantJob: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 245 testingjobset.ReplicatedJobRequirements{ 246 Name: "replicated-job-1", 247 Replicas: 1, 248 Completions: 1, 249 Parallelism: 1, 250 }, 251 testingjobset.ReplicatedJobRequirements{ 252 Name: "replicated-job-2", 253 Replicas: 2, 254 Completions: 2, 255 Parallelism: 2, 256 }, 257 ).Obj(), 258 wantWorkloads: []kueue.Workload{ 259 *utiltesting.MakeWorkload("jobset", "ns"). 260 PodSets( 261 *utiltesting.MakePodSet("replicated-job-1", 1).Obj(), 262 *utiltesting.MakePodSet("replicated-job-2", 4).Obj(), 263 ). 264 Obj(), 265 }, 266 }, 267 "workload is created with podsets and workloadPriorityClass": { 268 reconcilerOptions: []jobframework.Option{ 269 jobframework.WithManageJobsWithoutQueueName(true), 270 }, 271 job: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 272 testingjobset.ReplicatedJobRequirements{ 273 Name: "replicated-job-1", 274 Replicas: 1, 275 Completions: 1, 276 Parallelism: 1, 277 }, 278 ).WorkloadPriorityClass("test-wpc").Obj(), 279 priorityClasses: []client.Object{ 280 baseWPCWrapper.Obj(), 281 }, 282 wantJob: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 283 testingjobset.ReplicatedJobRequirements{ 284 Name: "replicated-job-1", 285 Replicas: 1, 286 Completions: 1, 287 Parallelism: 1, 288 }, 289 ).WorkloadPriorityClass("test-wpc").Obj(), 290 wantWorkloads: []kueue.Workload{ 291 *utiltesting.MakeWorkload("jobset", "ns"). 292 PriorityClass("test-wpc"). 293 Priority(100). 294 PriorityClassSource(constants.WorkloadPriorityClassSource). 295 PodSets( 296 *utiltesting.MakePodSet("replicated-job-1", 1).Obj(), 297 ). 298 Obj(), 299 }, 300 }, 301 "workload is created with podsets and PriorityClass": { 302 reconcilerOptions: []jobframework.Option{ 303 jobframework.WithManageJobsWithoutQueueName(true), 304 }, 305 job: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 306 testingjobset.ReplicatedJobRequirements{ 307 Name: "replicated-job-1", 308 Replicas: 1, 309 Completions: 1, 310 Parallelism: 1, 311 }, 312 ).PriorityClass("test-pc").Obj(), 313 priorityClasses: []client.Object{ 314 basePCWrapper.Obj(), 315 }, 316 wantJob: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 317 testingjobset.ReplicatedJobRequirements{ 318 Name: "replicated-job-1", 319 Replicas: 1, 320 Completions: 1, 321 Parallelism: 1, 322 }, 323 ).PriorityClass("test-pc").Obj(), 324 wantWorkloads: []kueue.Workload{ 325 *utiltesting.MakeWorkload("jobset", "ns"). 326 PriorityClass("test-pc"). 327 Priority(200). 328 PriorityClassSource(constants.PodPriorityClassSource). 329 PodSets( 330 *utiltesting.MakePodSet("replicated-job-1", 1).Obj(), 331 ). 332 Obj(), 333 }, 334 }, 335 "workload is created with podsets, workloadPriorityClass and PriorityClass": { 336 reconcilerOptions: []jobframework.Option{ 337 jobframework.WithManageJobsWithoutQueueName(true), 338 }, 339 job: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 340 testingjobset.ReplicatedJobRequirements{ 341 Name: "replicated-job-1", 342 Replicas: 1, 343 Completions: 1, 344 Parallelism: 1, 345 }, 346 ).PriorityClass("test-pc").WorkloadPriorityClass("test-wpc").Obj(), 347 priorityClasses: []client.Object{ 348 basePCWrapper.Obj(), baseWPCWrapper.Obj(), 349 }, 350 wantJob: testingjobset.MakeJobSet("jobset", "ns").ReplicatedJobs( 351 testingjobset.ReplicatedJobRequirements{ 352 Name: "replicated-job-1", 353 Replicas: 1, 354 Completions: 1, 355 Parallelism: 1, 356 }, 357 ).PriorityClass("test-pc").WorkloadPriorityClass("test-wpc").Obj(), 358 wantWorkloads: []kueue.Workload{ 359 *utiltesting.MakeWorkload("jobset", "ns"). 360 PriorityClass("test-wpc"). 361 Priority(100). 362 PriorityClassSource(constants.WorkloadPriorityClassSource). 363 PodSets( 364 *utiltesting.MakePodSet("replicated-job-1", 1).Obj(), 365 ). 366 Obj(), 367 }, 368 }, 369 } 370 371 for name, tc := range cases { 372 t.Run(name, func(t *testing.T) { 373 ctx, _ := utiltesting.ContextWithLog(t) 374 clientBuilder := utiltesting.NewClientBuilder(jobset.AddToScheme) 375 if err := SetupIndexes(ctx, utiltesting.AsIndexer(clientBuilder)); err != nil { 376 t.Fatalf("Could not setup indexes: %v", err) 377 } 378 objs := append(tc.priorityClasses, tc.job) 379 kClient := clientBuilder.WithObjects(objs...).Build() 380 recorder := record.NewBroadcaster().NewRecorder(kClient.Scheme(), corev1.EventSource{Component: "test"}) 381 reconciler := NewReconciler(kClient, recorder, tc.reconcilerOptions...) 382 383 jobKey := client.ObjectKeyFromObject(tc.job) 384 _, err := reconciler.Reconcile(ctx, reconcile.Request{ 385 NamespacedName: jobKey, 386 }) 387 if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { 388 t.Errorf("Reconcile returned error (-want,+got):\n%s", diff) 389 } 390 391 var gotJobSet jobset.JobSet 392 if err := kClient.Get(ctx, jobKey, &gotJobSet); err != nil { 393 t.Fatalf("Could not get Job after reconcile: %v", err) 394 } 395 if diff := cmp.Diff(tc.wantJob, &gotJobSet, jobCmpOpts...); diff != "" { 396 t.Errorf("Job after reconcile (-want,+got):\n%s", diff) 397 } 398 var gotWorkloads kueue.WorkloadList 399 if err := kClient.List(ctx, &gotWorkloads); err != nil { 400 t.Fatalf("Could not get Workloads after reconcile: %v", err) 401 } 402 if diff := cmp.Diff(tc.wantWorkloads, gotWorkloads.Items, workloadCmpOpts...); diff != "" { 403 t.Errorf("Workloads after reconcile (-want,+got):\n%s", diff) 404 } 405 }) 406 } 407 408 }