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  }