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  }