sigs.k8s.io/kueue@v0.6.2/pkg/cache/snapshot_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 cache
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/go-cmp/cmp/cmpopts"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/labels"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/utils/ptr"
    30  
    31  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    32  	"sigs.k8s.io/kueue/pkg/features"
    33  	utiltesting "sigs.k8s.io/kueue/pkg/util/testing"
    34  	"sigs.k8s.io/kueue/pkg/workload"
    35  )
    36  
    37  var snapCmpOpts = []cmp.Option{
    38  	cmpopts.EquateEmpty(),
    39  	cmpopts.IgnoreUnexported(ClusterQueue{}),
    40  	cmpopts.IgnoreFields(ClusterQueue{}, "RGByResource"),
    41  	cmpopts.IgnoreFields(Cohort{}, "Members"), // avoid recursion.
    42  	cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"),
    43  }
    44  
    45  func TestSnapshot(t *testing.T) {
    46  	testCases := map[string]struct {
    47  		cqs                []*kueue.ClusterQueue
    48  		rfs                []*kueue.ResourceFlavor
    49  		wls                []*kueue.Workload
    50  		wantSnapshot       Snapshot
    51  		enableLendingLimit bool
    52  	}{
    53  		"empty": {},
    54  		"independent clusterQueues": {
    55  			cqs: []*kueue.ClusterQueue{
    56  				utiltesting.MakeClusterQueue("a").Obj(),
    57  				utiltesting.MakeClusterQueue("b").Obj(),
    58  			},
    59  			wls: []*kueue.Workload{
    60  				utiltesting.MakeWorkload("alpha", "").
    61  					ReserveQuota(&kueue.Admission{ClusterQueue: "a"}).Obj(),
    62  				utiltesting.MakeWorkload("beta", "").
    63  					ReserveQuota(&kueue.Admission{ClusterQueue: "b"}).Obj(),
    64  			},
    65  			wantSnapshot: Snapshot{
    66  				ClusterQueues: map[string]*ClusterQueue{
    67  					"a": {
    68  						Name:                          "a",
    69  						NamespaceSelector:             labels.Everything(),
    70  						Status:                        active,
    71  						FlavorFungibility:             defaultFlavorFungibility,
    72  						AllocatableResourceGeneration: 1,
    73  						Workloads: map[string]*workload.Info{
    74  							"/alpha": workload.NewInfo(
    75  								utiltesting.MakeWorkload("alpha", "").
    76  									ReserveQuota(&kueue.Admission{ClusterQueue: "a"}).Obj()),
    77  						},
    78  						Preemption: defaultPreemption,
    79  					},
    80  					"b": {
    81  						Name:                          "b",
    82  						NamespaceSelector:             labels.Everything(),
    83  						Status:                        active,
    84  						FlavorFungibility:             defaultFlavorFungibility,
    85  						AllocatableResourceGeneration: 1,
    86  						Workloads: map[string]*workload.Info{
    87  							"/beta": workload.NewInfo(
    88  								utiltesting.MakeWorkload("beta", "").
    89  									ReserveQuota(&kueue.Admission{ClusterQueue: "b"}).Obj()),
    90  						},
    91  						Preemption: defaultPreemption,
    92  					},
    93  				},
    94  			},
    95  		},
    96  		"inactive clusterQueues": {
    97  			cqs: []*kueue.ClusterQueue{
    98  				utiltesting.MakeClusterQueue("flavor-nonexistent-cq").
    99  					ResourceGroup(*utiltesting.MakeFlavorQuotas("nonexistent-flavor").
   100  						Resource(corev1.ResourceCPU, "100").Obj()).
   101  					Obj(),
   102  			},
   103  			wantSnapshot: Snapshot{
   104  				InactiveClusterQueueSets: sets.New("flavor-nonexistent-cq"),
   105  			},
   106  		},
   107  		"resourceFlavors": {
   108  			rfs: []*kueue.ResourceFlavor{
   109  				utiltesting.MakeResourceFlavor("demand").
   110  					Label("a", "b").
   111  					Label("instance", "demand").
   112  					Obj(),
   113  				utiltesting.MakeResourceFlavor("spot").
   114  					Label("c", "d").
   115  					Label("instance", "spot").
   116  					Obj(),
   117  				utiltesting.MakeResourceFlavor("default").Obj(),
   118  			},
   119  			wantSnapshot: Snapshot{
   120  				ClusterQueues: map[string]*ClusterQueue{},
   121  				ResourceFlavors: map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{
   122  					"demand": utiltesting.MakeResourceFlavor("demand").
   123  						Label("a", "b").
   124  						Label("instance", "demand").
   125  						Obj(),
   126  					"spot": utiltesting.MakeResourceFlavor("spot").
   127  						Label("c", "d").
   128  						Label("instance", "spot").
   129  						Obj(),
   130  					"default": utiltesting.MakeResourceFlavor("default").Obj(),
   131  				},
   132  			},
   133  		},
   134  		"cohort": {
   135  			cqs: []*kueue.ClusterQueue{
   136  				utiltesting.MakeClusterQueue("a").
   137  					Cohort("borrowing").
   138  					ResourceGroup(
   139  						*utiltesting.MakeFlavorQuotas("demand").Resource(corev1.ResourceCPU, "100").Obj(),
   140  						*utiltesting.MakeFlavorQuotas("spot").Resource(corev1.ResourceCPU, "200").Obj(),
   141  					).
   142  					Obj(),
   143  				utiltesting.MakeClusterQueue("b").
   144  					Cohort("borrowing").
   145  					ResourceGroup(
   146  						*utiltesting.MakeFlavorQuotas("spot").Resource(corev1.ResourceCPU, "100").Obj(),
   147  					).
   148  					ResourceGroup(
   149  						*utiltesting.MakeFlavorQuotas("default").Resource("example.com/gpu", "50").Obj(),
   150  					).
   151  					Obj(),
   152  				utiltesting.MakeClusterQueue("c").
   153  					ResourceGroup(
   154  						*utiltesting.MakeFlavorQuotas("default").Resource(corev1.ResourceCPU, "100").Obj(),
   155  					).
   156  					Obj(),
   157  			},
   158  			rfs: []*kueue.ResourceFlavor{
   159  				utiltesting.MakeResourceFlavor("demand").Label("instance", "demand").Obj(),
   160  				utiltesting.MakeResourceFlavor("spot").Label("instance", "spot").Obj(),
   161  				utiltesting.MakeResourceFlavor("default").Obj(),
   162  			},
   163  			wls: []*kueue.Workload{
   164  				utiltesting.MakeWorkload("alpha", "").
   165  					PodSets(*utiltesting.MakePodSet("main", 5).
   166  						Request(corev1.ResourceCPU, "2").Obj()).
   167  					ReserveQuota(utiltesting.MakeAdmission("a", "main").Assignment(corev1.ResourceCPU, "demand", "10000m").AssignmentPodCount(5).Obj()).
   168  					Obj(),
   169  				utiltesting.MakeWorkload("beta", "").
   170  					PodSets(*utiltesting.MakePodSet("main", 5).
   171  						Request(corev1.ResourceCPU, "1").
   172  						Request("example.com/gpu", "2").
   173  						Obj(),
   174  					).
   175  					ReserveQuota(utiltesting.MakeAdmission("b", "main").
   176  						Assignment(corev1.ResourceCPU, "spot", "5000m").
   177  						Assignment("example.com/gpu", "default", "10").
   178  						AssignmentPodCount(5).
   179  						Obj()).
   180  					Obj(),
   181  				utiltesting.MakeWorkload("gamma", "").
   182  					PodSets(*utiltesting.MakePodSet("main", 5).
   183  						Request(corev1.ResourceCPU, "1").
   184  						Request("example.com/gpu", "1").
   185  						Obj(),
   186  					).
   187  					ReserveQuota(utiltesting.MakeAdmission("b", "main").
   188  						Assignment(corev1.ResourceCPU, "spot", "5000m").
   189  						Assignment("example.com/gpu", "default", "5").
   190  						AssignmentPodCount(5).
   191  						Obj()).
   192  					Obj(),
   193  				utiltesting.MakeWorkload("sigma", "").
   194  					PodSets(*utiltesting.MakePodSet("main", 5).
   195  						Request(corev1.ResourceCPU, "1").
   196  						Obj(),
   197  					).
   198  					Obj(),
   199  			},
   200  			wantSnapshot: func() Snapshot {
   201  				cohort := &Cohort{
   202  					Name:                          "borrowing",
   203  					AllocatableResourceGeneration: 2,
   204  					RequestableResources: FlavorResourceQuantities{
   205  						"demand": {
   206  							corev1.ResourceCPU: 100_000,
   207  						},
   208  						"spot": {
   209  							corev1.ResourceCPU: 300_000,
   210  						},
   211  						"default": {
   212  							"example.com/gpu": 50,
   213  						},
   214  					},
   215  					Usage: FlavorResourceQuantities{
   216  						"demand": {
   217  							corev1.ResourceCPU: 10_000,
   218  						},
   219  						"spot": {
   220  							corev1.ResourceCPU: 10_000,
   221  						},
   222  						"default": {
   223  							"example.com/gpu": 15,
   224  						},
   225  					},
   226  				}
   227  				return Snapshot{
   228  					ClusterQueues: map[string]*ClusterQueue{
   229  						"a": {
   230  							Name:                          "a",
   231  							Cohort:                        cohort,
   232  							AllocatableResourceGeneration: 1,
   233  							ResourceGroups: []ResourceGroup{
   234  								{
   235  									CoveredResources: sets.New(corev1.ResourceCPU),
   236  									Flavors: []FlavorQuotas{
   237  										{
   238  											Name: "demand",
   239  											Resources: map[corev1.ResourceName]*ResourceQuota{
   240  												corev1.ResourceCPU: {Nominal: 100_000},
   241  											},
   242  										},
   243  										{
   244  											Name: "spot",
   245  											Resources: map[corev1.ResourceName]*ResourceQuota{
   246  												corev1.ResourceCPU: {Nominal: 200_000},
   247  											},
   248  										},
   249  									},
   250  									LabelKeys: sets.New("instance"),
   251  								},
   252  							},
   253  							FlavorFungibility: defaultFlavorFungibility,
   254  							Usage: FlavorResourceQuantities{
   255  								"demand": {corev1.ResourceCPU: 10_000},
   256  								"spot":   {corev1.ResourceCPU: 0},
   257  							},
   258  							Workloads: map[string]*workload.Info{
   259  								"/alpha": workload.NewInfo(utiltesting.MakeWorkload("alpha", "").
   260  									PodSets(*utiltesting.MakePodSet("main", 5).
   261  										Request(corev1.ResourceCPU, "2").Obj()).
   262  									ReserveQuota(utiltesting.MakeAdmission("a", "main").
   263  										Assignment(corev1.ResourceCPU, "demand", "10000m").
   264  										AssignmentPodCount(5).
   265  										Obj()).
   266  									Obj()),
   267  							},
   268  							Preemption:        defaultPreemption,
   269  							NamespaceSelector: labels.Everything(),
   270  							Status:            active,
   271  						},
   272  						"b": {
   273  							Name:                          "b",
   274  							Cohort:                        cohort,
   275  							AllocatableResourceGeneration: 1,
   276  							ResourceGroups: []ResourceGroup{
   277  								{
   278  									CoveredResources: sets.New(corev1.ResourceCPU),
   279  									Flavors: []FlavorQuotas{{
   280  										Name: "spot",
   281  										Resources: map[corev1.ResourceName]*ResourceQuota{
   282  											corev1.ResourceCPU: {Nominal: 100_000},
   283  										},
   284  									}},
   285  									LabelKeys: sets.New("instance"),
   286  								},
   287  								{
   288  									CoveredResources: sets.New[corev1.ResourceName]("example.com/gpu"),
   289  									Flavors: []FlavorQuotas{{
   290  										Name: "default",
   291  										Resources: map[corev1.ResourceName]*ResourceQuota{
   292  											"example.com/gpu": {Nominal: 50},
   293  										},
   294  									}},
   295  								},
   296  							},
   297  							FlavorFungibility: defaultFlavorFungibility,
   298  							Usage: FlavorResourceQuantities{
   299  								"spot": {
   300  									corev1.ResourceCPU: 10_000,
   301  								},
   302  								"default": {
   303  									"example.com/gpu": 15,
   304  								},
   305  							},
   306  							Workloads: map[string]*workload.Info{
   307  								"/beta": workload.NewInfo(utiltesting.MakeWorkload("beta", "").
   308  									PodSets(*utiltesting.MakePodSet("main", 5).
   309  										Request(corev1.ResourceCPU, "1").
   310  										Request("example.com/gpu", "2").
   311  										Obj()).
   312  									ReserveQuota(utiltesting.MakeAdmission("b", "main").
   313  										Assignment(corev1.ResourceCPU, "spot", "5000m").
   314  										Assignment("example.com/gpu", "default", "10").
   315  										AssignmentPodCount(5).
   316  										Obj()).
   317  									Obj()),
   318  								"/gamma": workload.NewInfo(utiltesting.MakeWorkload("gamma", "").
   319  									PodSets(*utiltesting.MakePodSet("main", 5).
   320  										Request(corev1.ResourceCPU, "1").
   321  										Request("example.com/gpu", "1").
   322  										Obj(),
   323  									).
   324  									ReserveQuota(utiltesting.MakeAdmission("b", "main").
   325  										Assignment(corev1.ResourceCPU, "spot", "5000m").
   326  										Assignment("example.com/gpu", "default", "5").
   327  										AssignmentPodCount(5).
   328  										Obj()).
   329  									Obj()),
   330  							},
   331  							Preemption:        defaultPreemption,
   332  							NamespaceSelector: labels.Everything(),
   333  							Status:            active,
   334  						},
   335  						"c": {
   336  							Name:                          "c",
   337  							AllocatableResourceGeneration: 1,
   338  							ResourceGroups: []ResourceGroup{
   339  								{
   340  									CoveredResources: sets.New(corev1.ResourceCPU),
   341  									Flavors: []FlavorQuotas{{
   342  										Name: "default",
   343  										Resources: map[corev1.ResourceName]*ResourceQuota{
   344  											corev1.ResourceCPU: {Nominal: 100_000},
   345  										},
   346  									}},
   347  								},
   348  							},
   349  							FlavorFungibility: defaultFlavorFungibility,
   350  							Usage: FlavorResourceQuantities{
   351  								"default": {
   352  									corev1.ResourceCPU: 0,
   353  								},
   354  							},
   355  							Preemption:        defaultPreemption,
   356  							NamespaceSelector: labels.Everything(),
   357  							Status:            active,
   358  						},
   359  					},
   360  					ResourceFlavors: map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{
   361  						"demand":  utiltesting.MakeResourceFlavor("demand").Label("instance", "demand").Obj(),
   362  						"spot":    utiltesting.MakeResourceFlavor("spot").Label("instance", "spot").Obj(),
   363  						"default": utiltesting.MakeResourceFlavor("default").Obj(),
   364  					},
   365  				}
   366  			}(),
   367  		},
   368  		"clusterQueues with preemption": {
   369  			cqs: []*kueue.ClusterQueue{
   370  				utiltesting.MakeClusterQueue("with-preemption").
   371  					Preemption(kueue.ClusterQueuePreemption{
   372  						ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   373  						WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   374  					}).Obj(),
   375  			},
   376  			wantSnapshot: Snapshot{
   377  				ClusterQueues: map[string]*ClusterQueue{
   378  					"with-preemption": {
   379  						Name:                          "with-preemption",
   380  						NamespaceSelector:             labels.Everything(),
   381  						AllocatableResourceGeneration: 1,
   382  						Status:                        active,
   383  						Workloads:                     map[string]*workload.Info{},
   384  						FlavorFungibility:             defaultFlavorFungibility,
   385  						Preemption: kueue.ClusterQueuePreemption{
   386  							ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   387  							WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   388  						},
   389  					},
   390  				},
   391  			},
   392  		},
   393  		"lendingLimit with 2 clusterQueues and 2 flavors(whenCanBorrow: Borrow)": {
   394  			cqs: []*kueue.ClusterQueue{
   395  				utiltesting.MakeClusterQueue("a").
   396  					Cohort("lending").
   397  					ResourceGroup(
   398  						*utiltesting.MakeFlavorQuotas("arm").Resource(corev1.ResourceCPU, "10", "", "5").Obj(),
   399  						*utiltesting.MakeFlavorQuotas("x86").Resource(corev1.ResourceCPU, "20", "", "10").Obj(),
   400  					).
   401  					FlavorFungibility(defaultFlavorFungibility).
   402  					Obj(),
   403  				utiltesting.MakeClusterQueue("b").
   404  					Cohort("lending").
   405  					ResourceGroup(
   406  						*utiltesting.MakeFlavorQuotas("arm").Resource(corev1.ResourceCPU, "10", "", "5").Obj(),
   407  						*utiltesting.MakeFlavorQuotas("x86").Resource(corev1.ResourceCPU, "20", "", "10").Obj(),
   408  					).
   409  					Obj(),
   410  			},
   411  			rfs: []*kueue.ResourceFlavor{
   412  				utiltesting.MakeResourceFlavor("arm").Label("arch", "arm").Obj(),
   413  				utiltesting.MakeResourceFlavor("x86").Label("arch", "x86").Obj(),
   414  			},
   415  			wls: []*kueue.Workload{
   416  				utiltesting.MakeWorkload("alpha", "").
   417  					PodSets(*utiltesting.MakePodSet("main", 5).
   418  						Request(corev1.ResourceCPU, "2").Obj()).
   419  					ReserveQuota(utiltesting.MakeAdmission("a", "main").
   420  						Assignment(corev1.ResourceCPU, "arm", "10000m").
   421  						AssignmentPodCount(5).Obj()).
   422  					Obj(),
   423  				utiltesting.MakeWorkload("beta", "").
   424  					PodSets(*utiltesting.MakePodSet("main", 5).
   425  						Request(corev1.ResourceCPU, "1").Obj()).
   426  					ReserveQuota(utiltesting.MakeAdmission("a", "main").
   427  						Assignment(corev1.ResourceCPU, "arm", "5000m").
   428  						AssignmentPodCount(5).Obj()).
   429  					Obj(),
   430  				utiltesting.MakeWorkload("gamma", "").
   431  					PodSets(*utiltesting.MakePodSet("main", 5).
   432  						Request(corev1.ResourceCPU, "2").Obj()).
   433  					ReserveQuota(utiltesting.MakeAdmission("a", "main").
   434  						Assignment(corev1.ResourceCPU, "x86", "10000m").
   435  						AssignmentPodCount(5).Obj()).
   436  					Obj(),
   437  			},
   438  			wantSnapshot: func() Snapshot {
   439  				cohort := &Cohort{
   440  					Name:                          "lending",
   441  					AllocatableResourceGeneration: 2,
   442  					RequestableResources: FlavorResourceQuantities{
   443  						"arm": {
   444  							corev1.ResourceCPU: 10_000,
   445  						},
   446  						"x86": {
   447  							corev1.ResourceCPU: 20_000,
   448  						},
   449  					},
   450  					Usage: FlavorResourceQuantities{
   451  						"arm": {
   452  							corev1.ResourceCPU: 10_000,
   453  						},
   454  						"x86": {
   455  							corev1.ResourceCPU: 0,
   456  						},
   457  					},
   458  				}
   459  				return Snapshot{
   460  					ClusterQueues: map[string]*ClusterQueue{
   461  						"a": {
   462  							Name:                          "a",
   463  							Cohort:                        cohort,
   464  							AllocatableResourceGeneration: 1,
   465  							ResourceGroups: []ResourceGroup{
   466  								{
   467  									CoveredResources: sets.New(corev1.ResourceCPU),
   468  									Flavors: []FlavorQuotas{
   469  										{
   470  											Name: "arm",
   471  											Resources: map[corev1.ResourceName]*ResourceQuota{
   472  												corev1.ResourceCPU: {
   473  													Nominal:        10_000,
   474  													BorrowingLimit: nil,
   475  													LendingLimit:   ptr.To[int64](5_000),
   476  												},
   477  											},
   478  										},
   479  										{
   480  											Name: "x86",
   481  											Resources: map[corev1.ResourceName]*ResourceQuota{
   482  												corev1.ResourceCPU: {
   483  													Nominal:        20_000,
   484  													BorrowingLimit: nil,
   485  													LendingLimit:   ptr.To[int64](10_000),
   486  												},
   487  											},
   488  										},
   489  									},
   490  									LabelKeys: sets.New("arch"),
   491  								},
   492  							},
   493  							FlavorFungibility: defaultFlavorFungibility,
   494  							Usage: FlavorResourceQuantities{
   495  								"arm": {corev1.ResourceCPU: 15_000},
   496  								"x86": {corev1.ResourceCPU: 10_000},
   497  							},
   498  							Workloads: map[string]*workload.Info{
   499  								"/alpha": workload.NewInfo(utiltesting.MakeWorkload("alpha", "").
   500  									PodSets(*utiltesting.MakePodSet("main", 5).
   501  										Request(corev1.ResourceCPU, "2").Obj()).
   502  									ReserveQuota(utiltesting.MakeAdmission("a", "main").
   503  										Assignment(corev1.ResourceCPU, "arm", "10000m").
   504  										AssignmentPodCount(5).Obj()).
   505  									Obj()),
   506  								"/beta": workload.NewInfo(utiltesting.MakeWorkload("beta", "").
   507  									PodSets(*utiltesting.MakePodSet("main", 5).
   508  										Request(corev1.ResourceCPU, "1").Obj()).
   509  									ReserveQuota(utiltesting.MakeAdmission("a", "main").
   510  										Assignment(corev1.ResourceCPU, "arm", "5000m").
   511  										AssignmentPodCount(5).Obj()).
   512  									Obj()),
   513  								"/gamma": workload.NewInfo(utiltesting.MakeWorkload("gamma", "").
   514  									PodSets(*utiltesting.MakePodSet("main", 5).
   515  										Request(corev1.ResourceCPU, "2").Obj()).
   516  									ReserveQuota(utiltesting.MakeAdmission("a", "main").
   517  										Assignment(corev1.ResourceCPU, "x86", "10000m").
   518  										AssignmentPodCount(5).Obj()).
   519  									Obj()),
   520  							},
   521  							Preemption:        defaultPreemption,
   522  							NamespaceSelector: labels.Everything(),
   523  							Status:            active,
   524  							GuaranteedQuota: FlavorResourceQuantities{
   525  								"arm": {
   526  									corev1.ResourceCPU: 5_000,
   527  								},
   528  								"x86": {
   529  									corev1.ResourceCPU: 10_000,
   530  								},
   531  							},
   532  						},
   533  						"b": {
   534  							Name:                          "b",
   535  							Cohort:                        cohort,
   536  							AllocatableResourceGeneration: 1,
   537  							ResourceGroups: []ResourceGroup{
   538  								{
   539  									CoveredResources: sets.New(corev1.ResourceCPU),
   540  									Flavors: []FlavorQuotas{
   541  										{
   542  											Name: "arm",
   543  											Resources: map[corev1.ResourceName]*ResourceQuota{
   544  												corev1.ResourceCPU: {
   545  													Nominal:        10_000,
   546  													BorrowingLimit: nil,
   547  													LendingLimit:   ptr.To[int64](5_000),
   548  												},
   549  											},
   550  										},
   551  										{
   552  											Name: "x86",
   553  											Resources: map[corev1.ResourceName]*ResourceQuota{
   554  												corev1.ResourceCPU: {
   555  													Nominal:        20_000,
   556  													BorrowingLimit: nil,
   557  													LendingLimit:   ptr.To[int64](10_000),
   558  												},
   559  											},
   560  										},
   561  									},
   562  									LabelKeys: sets.New("arch"),
   563  								},
   564  							},
   565  							FlavorFungibility: defaultFlavorFungibility,
   566  							Usage: FlavorResourceQuantities{
   567  								"arm": {corev1.ResourceCPU: 0},
   568  								"x86": {corev1.ResourceCPU: 0},
   569  							},
   570  							Preemption:        defaultPreemption,
   571  							NamespaceSelector: labels.Everything(),
   572  							Status:            active,
   573  							GuaranteedQuota: FlavorResourceQuantities{
   574  								"arm": {
   575  									corev1.ResourceCPU: 5_000,
   576  								},
   577  								"x86": {
   578  									corev1.ResourceCPU: 10_000,
   579  								},
   580  							},
   581  						},
   582  					},
   583  					ResourceFlavors: map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{
   584  						"arm": utiltesting.MakeResourceFlavor("arm").Label("arch", "arm").Obj(),
   585  						"x86": utiltesting.MakeResourceFlavor("x86").Label("arch", "x86").Obj(),
   586  					},
   587  				}
   588  			}(),
   589  			enableLendingLimit: true,
   590  		},
   591  	}
   592  
   593  	for name, tc := range testCases {
   594  		t.Run(name, func(t *testing.T) {
   595  			defer features.SetFeatureGateDuringTest(t, features.LendingLimit, tc.enableLendingLimit)()
   596  			cache := New(utiltesting.NewFakeClient())
   597  			for _, cq := range tc.cqs {
   598  				// Purposely do not make a copy of clusterQueues. Clones of necessary fields are
   599  				// done in AddClusterQueue.
   600  				if err := cache.AddClusterQueue(context.Background(), cq); err != nil {
   601  					t.Fatalf("Failed adding ClusterQueue: %v", err)
   602  				}
   603  			}
   604  			for _, rf := range tc.rfs {
   605  				cache.AddOrUpdateResourceFlavor(rf)
   606  			}
   607  			for _, wl := range tc.wls {
   608  				cache.AddOrUpdateWorkload(wl)
   609  			}
   610  			snapshot := cache.Snapshot()
   611  			if diff := cmp.Diff(tc.wantSnapshot, snapshot, snapCmpOpts...); len(diff) != 0 {
   612  				t.Errorf("Unexpected Snapshot (-want,+got):\n%s", diff)
   613  			}
   614  			for _, cq := range snapshot.ClusterQueues {
   615  				for i := range cq.ResourceGroups {
   616  					rg := &cq.ResourceGroups[i]
   617  					for rName := range rg.CoveredResources {
   618  						if cq.RGByResource[rName] != rg {
   619  							t.Errorf("RGByResource[%s] does not point to its resource group", rName)
   620  						}
   621  					}
   622  				}
   623  			}
   624  		})
   625  	}
   626  }
   627  
   628  func TestSnapshotAddRemoveWorkload(t *testing.T) {
   629  	flavors := []*kueue.ResourceFlavor{
   630  		utiltesting.MakeResourceFlavor("default").Obj(),
   631  		utiltesting.MakeResourceFlavor("alpha").Obj(),
   632  		utiltesting.MakeResourceFlavor("beta").Obj(),
   633  	}
   634  	clusterQueues := []*kueue.ClusterQueue{
   635  		utiltesting.MakeClusterQueue("c1").
   636  			Cohort("cohort").
   637  			ResourceGroup(
   638  				*utiltesting.MakeFlavorQuotas("default").Resource(corev1.ResourceCPU, "6").Obj(),
   639  			).
   640  			ResourceGroup(
   641  				*utiltesting.MakeFlavorQuotas("alpha").Resource(corev1.ResourceMemory, "6Gi").Obj(),
   642  				*utiltesting.MakeFlavorQuotas("beta").Resource(corev1.ResourceMemory, "6Gi").Obj(),
   643  			).
   644  			Obj(),
   645  		utiltesting.MakeClusterQueue("c2").
   646  			Cohort("cohort").
   647  			ResourceGroup(
   648  				*utiltesting.MakeFlavorQuotas("default").Resource(corev1.ResourceCPU, "6").Obj(),
   649  			).
   650  			Obj(),
   651  	}
   652  	workloads := []kueue.Workload{
   653  		*utiltesting.MakeWorkload("c1-cpu", "").
   654  			Request(corev1.ResourceCPU, "1").
   655  			ReserveQuota(utiltesting.MakeAdmission("c1").Assignment(corev1.ResourceCPU, "default", "1000m").Obj()).
   656  			Obj(),
   657  		*utiltesting.MakeWorkload("c1-memory-alpha", "").
   658  			Request(corev1.ResourceMemory, "1Gi").
   659  			ReserveQuota(utiltesting.MakeAdmission("c1").Assignment(corev1.ResourceMemory, "alpha", "1Gi").Obj()).
   660  			Obj(),
   661  		*utiltesting.MakeWorkload("c1-memory-beta", "").
   662  			Request(corev1.ResourceMemory, "1Gi").
   663  			ReserveQuota(utiltesting.MakeAdmission("c1").Assignment(corev1.ResourceMemory, "beta", "1Gi").Obj()).
   664  			Obj(),
   665  		*utiltesting.MakeWorkload("c2-cpu-1", "").
   666  			Request(corev1.ResourceCPU, "1").
   667  			ReserveQuota(utiltesting.MakeAdmission("c2").Assignment(corev1.ResourceCPU, "default", "1000m").Obj()).
   668  			Obj(),
   669  		*utiltesting.MakeWorkload("c2-cpu-2", "").
   670  			Request(corev1.ResourceCPU, "1").
   671  			ReserveQuota(utiltesting.MakeAdmission("c2").Assignment(corev1.ResourceCPU, "default", "1000m").Obj()).
   672  			Obj(),
   673  	}
   674  
   675  	ctx := context.Background()
   676  	cl := utiltesting.NewClientBuilder().WithLists(&kueue.WorkloadList{Items: workloads}).Build()
   677  
   678  	cqCache := New(cl)
   679  	for _, flv := range flavors {
   680  		cqCache.AddOrUpdateResourceFlavor(flv)
   681  	}
   682  	for _, cq := range clusterQueues {
   683  		if err := cqCache.AddClusterQueue(ctx, cq); err != nil {
   684  			t.Fatalf("Couldn't add ClusterQueue to cache: %v", err)
   685  		}
   686  	}
   687  	wlInfos := make(map[string]*workload.Info, len(workloads))
   688  	for _, cq := range cqCache.clusterQueues {
   689  		for _, wl := range cq.Workloads {
   690  			wlInfos[workload.Key(wl.Obj)] = wl
   691  		}
   692  	}
   693  	initialSnapshot := cqCache.Snapshot()
   694  	initialCohortResources := initialSnapshot.ClusterQueues["c1"].Cohort.RequestableResources
   695  	cases := map[string]struct {
   696  		remove []string
   697  		add    []string
   698  		want   Snapshot
   699  	}{
   700  		"no-op remove add": {
   701  			remove: []string{"/c1-cpu", "/c2-cpu-1"},
   702  			add:    []string{"/c1-cpu", "/c2-cpu-1"},
   703  			want:   initialSnapshot,
   704  		},
   705  		"remove all": {
   706  			remove: []string{"/c1-cpu", "/c1-memory-alpha", "/c1-memory-beta", "/c2-cpu-1", "/c2-cpu-2"},
   707  			want: func() Snapshot {
   708  				cohort := &Cohort{
   709  					Name:                          "cohort",
   710  					AllocatableResourceGeneration: 2,
   711  					RequestableResources:          initialCohortResources,
   712  					Usage: FlavorResourceQuantities{
   713  						"default": {corev1.ResourceCPU: 0},
   714  						"alpha":   {corev1.ResourceMemory: 0},
   715  						"beta":    {corev1.ResourceMemory: 0},
   716  					},
   717  				}
   718  				return Snapshot{
   719  					ClusterQueues: map[string]*ClusterQueue{
   720  						"c1": {
   721  							Name:                          "c1",
   722  							Cohort:                        cohort,
   723  							Workloads:                     make(map[string]*workload.Info),
   724  							ResourceGroups:                cqCache.clusterQueues["c1"].ResourceGroups,
   725  							FlavorFungibility:             defaultFlavorFungibility,
   726  							AllocatableResourceGeneration: 1,
   727  							Usage: FlavorResourceQuantities{
   728  								"default": {corev1.ResourceCPU: 0},
   729  								"alpha":   {corev1.ResourceMemory: 0},
   730  								"beta":    {corev1.ResourceMemory: 0},
   731  							},
   732  						},
   733  						"c2": {
   734  							Name:                          "c2",
   735  							Cohort:                        cohort,
   736  							Workloads:                     make(map[string]*workload.Info),
   737  							ResourceGroups:                cqCache.clusterQueues["c2"].ResourceGroups,
   738  							FlavorFungibility:             defaultFlavorFungibility,
   739  							AllocatableResourceGeneration: 1,
   740  							Usage: FlavorResourceQuantities{
   741  								"default": {corev1.ResourceCPU: 0},
   742  							},
   743  						},
   744  					},
   745  				}
   746  			}(),
   747  		},
   748  		"remove c1-cpu": {
   749  			remove: []string{"/c1-cpu"},
   750  			want: func() Snapshot {
   751  				cohort := &Cohort{
   752  					Name:                          "cohort",
   753  					AllocatableResourceGeneration: 2,
   754  					RequestableResources:          initialCohortResources,
   755  					Usage: FlavorResourceQuantities{
   756  						"default": {corev1.ResourceCPU: 2_000},
   757  						"alpha":   {corev1.ResourceMemory: utiltesting.Gi},
   758  						"beta":    {corev1.ResourceMemory: utiltesting.Gi},
   759  					},
   760  				}
   761  				return Snapshot{
   762  					ClusterQueues: map[string]*ClusterQueue{
   763  						"c1": {
   764  							Name:   "c1",
   765  							Cohort: cohort,
   766  							Workloads: map[string]*workload.Info{
   767  								"/c1-memory-alpha": nil,
   768  								"/c1-memory-beta":  nil,
   769  							},
   770  							AllocatableResourceGeneration: 1,
   771  							ResourceGroups:                cqCache.clusterQueues["c1"].ResourceGroups,
   772  							FlavorFungibility:             defaultFlavorFungibility,
   773  							Usage: FlavorResourceQuantities{
   774  								"default": {corev1.ResourceCPU: 0},
   775  								"alpha":   {corev1.ResourceMemory: utiltesting.Gi},
   776  								"beta":    {corev1.ResourceMemory: utiltesting.Gi},
   777  							},
   778  						},
   779  						"c2": {
   780  							Name:   "c2",
   781  							Cohort: cohort,
   782  							Workloads: map[string]*workload.Info{
   783  								"/c2-cpu-1": nil,
   784  								"/c2-cpu-2": nil,
   785  							},
   786  							ResourceGroups:                cqCache.clusterQueues["c2"].ResourceGroups,
   787  							FlavorFungibility:             defaultFlavorFungibility,
   788  							AllocatableResourceGeneration: 1,
   789  							Usage: FlavorResourceQuantities{
   790  								"default": {corev1.ResourceCPU: 2_000},
   791  							},
   792  						},
   793  					},
   794  				}
   795  			}(),
   796  		},
   797  		"remove c1-memory-alpha": {
   798  			remove: []string{"/c1-memory-alpha"},
   799  			want: func() Snapshot {
   800  				cohort := &Cohort{
   801  					Name:                          "cohort",
   802  					AllocatableResourceGeneration: 2,
   803  					RequestableResources:          initialCohortResources,
   804  					Usage: FlavorResourceQuantities{
   805  						"default": {corev1.ResourceCPU: 3_000},
   806  						"alpha":   {corev1.ResourceMemory: 0},
   807  						"beta":    {corev1.ResourceMemory: utiltesting.Gi},
   808  					},
   809  				}
   810  				return Snapshot{
   811  					ClusterQueues: map[string]*ClusterQueue{
   812  						"c1": {
   813  							Name:   "c1",
   814  							Cohort: cohort,
   815  							Workloads: map[string]*workload.Info{
   816  								"/c1-memory-alpha": nil,
   817  								"/c1-memory-beta":  nil,
   818  							},
   819  							AllocatableResourceGeneration: 1,
   820  							ResourceGroups:                cqCache.clusterQueues["c1"].ResourceGroups,
   821  							FlavorFungibility:             defaultFlavorFungibility,
   822  							Usage: FlavorResourceQuantities{
   823  								"default": {corev1.ResourceCPU: 1_000},
   824  								"alpha":   {corev1.ResourceMemory: 0},
   825  								"beta":    {corev1.ResourceMemory: utiltesting.Gi},
   826  							},
   827  						},
   828  						"c2": {
   829  							Name:   "c2",
   830  							Cohort: cohort,
   831  							Workloads: map[string]*workload.Info{
   832  								"/c2-cpu-1": nil,
   833  								"/c2-cpu-2": nil,
   834  							},
   835  							AllocatableResourceGeneration: 1,
   836  							ResourceGroups:                cqCache.clusterQueues["c2"].ResourceGroups,
   837  							FlavorFungibility:             defaultFlavorFungibility,
   838  							Usage: FlavorResourceQuantities{
   839  								"default": {corev1.ResourceCPU: 2_000},
   840  							},
   841  						},
   842  					},
   843  				}
   844  			}(),
   845  		},
   846  	}
   847  	cmpOpts := append(snapCmpOpts,
   848  		cmpopts.IgnoreFields(ClusterQueue{}, "NamespaceSelector", "Preemption", "Status"),
   849  		cmpopts.IgnoreFields(Cohort{}),
   850  		cmpopts.IgnoreFields(Snapshot{}, "ResourceFlavors"),
   851  		cmpopts.IgnoreTypes(&workload.Info{}))
   852  	for name, tc := range cases {
   853  		t.Run(name, func(t *testing.T) {
   854  			snap := cqCache.Snapshot()
   855  			for _, name := range tc.remove {
   856  				snap.RemoveWorkload(wlInfos[name])
   857  			}
   858  			for _, name := range tc.add {
   859  				snap.AddWorkload(wlInfos[name])
   860  			}
   861  			if diff := cmp.Diff(tc.want, snap, cmpOpts...); diff != "" {
   862  				t.Errorf("Unexpected snapshot state after operations (-want,+got):\n%s", diff)
   863  			}
   864  		})
   865  	}
   866  }
   867  
   868  func TestSnapshotAddRemoveWorkloadWithLendingLimit(t *testing.T) {
   869  	_ = features.SetEnable(features.LendingLimit, true)
   870  	flavors := []*kueue.ResourceFlavor{
   871  		utiltesting.MakeResourceFlavor("default").Obj(),
   872  	}
   873  	clusterQueues := []*kueue.ClusterQueue{
   874  		utiltesting.MakeClusterQueue("lend-a").
   875  			Cohort("lend").
   876  			ResourceGroup(
   877  				*utiltesting.MakeFlavorQuotas("default").Resource(corev1.ResourceCPU, "10", "", "4").Obj(),
   878  			).
   879  			Preemption(kueue.ClusterQueuePreemption{
   880  				WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   881  				ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority,
   882  			}).
   883  			Obj(),
   884  		utiltesting.MakeClusterQueue("lend-b").
   885  			Cohort("lend").
   886  			ResourceGroup(
   887  				*utiltesting.MakeFlavorQuotas("default").Resource(corev1.ResourceCPU, "10", "", "6").Obj(),
   888  			).
   889  			Preemption(kueue.ClusterQueuePreemption{
   890  				WithinClusterQueue:  kueue.PreemptionPolicyNever,
   891  				ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   892  			}).
   893  			Obj(),
   894  	}
   895  	workloads := []kueue.Workload{
   896  		*utiltesting.MakeWorkload("lend-a-1", "").
   897  			Request(corev1.ResourceCPU, "1").
   898  			ReserveQuota(utiltesting.MakeAdmission("lend-a").Assignment(corev1.ResourceCPU, "default", "1").Obj()).
   899  			Obj(),
   900  		*utiltesting.MakeWorkload("lend-a-2", "").
   901  			Request(corev1.ResourceCPU, "9").
   902  			ReserveQuota(utiltesting.MakeAdmission("lend-a").Assignment(corev1.ResourceCPU, "default", "9").Obj()).
   903  			Obj(),
   904  		*utiltesting.MakeWorkload("lend-a-3", "").
   905  			Request(corev1.ResourceCPU, "6").
   906  			ReserveQuota(utiltesting.MakeAdmission("lend-a").Assignment(corev1.ResourceCPU, "default", "6").Obj()).
   907  			Obj(),
   908  		*utiltesting.MakeWorkload("lend-b-1", "").
   909  			Request(corev1.ResourceCPU, "4").
   910  			ReserveQuota(utiltesting.MakeAdmission("lend-b").Assignment(corev1.ResourceCPU, "default", "4").Obj()).
   911  			Obj(),
   912  	}
   913  
   914  	ctx := context.Background()
   915  	cl := utiltesting.NewClientBuilder().WithLists(&kueue.WorkloadList{Items: workloads}).Build()
   916  
   917  	cqCache := New(cl)
   918  	for _, flv := range flavors {
   919  		cqCache.AddOrUpdateResourceFlavor(flv)
   920  	}
   921  	for _, cq := range clusterQueues {
   922  		if err := cqCache.AddClusterQueue(ctx, cq); err != nil {
   923  			t.Fatalf("Couldn't add ClusterQueue to cache: %v", err)
   924  		}
   925  	}
   926  	wlInfos := make(map[string]*workload.Info, len(workloads))
   927  	for _, cq := range cqCache.clusterQueues {
   928  		for _, wl := range cq.Workloads {
   929  			wlInfos[workload.Key(wl.Obj)] = wl
   930  		}
   931  	}
   932  	initialSnapshot := cqCache.Snapshot()
   933  	initialCohortResources := initialSnapshot.ClusterQueues["lend-a"].Cohort.RequestableResources
   934  	cases := map[string]struct {
   935  		remove []string
   936  		add    []string
   937  		want   Snapshot
   938  	}{
   939  		"remove all then add all": {
   940  			remove: []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
   941  			add:    []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
   942  			want:   initialSnapshot,
   943  		},
   944  		"remove all": {
   945  			remove: []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
   946  			want: func() Snapshot {
   947  				cohort := &Cohort{
   948  					Name:                          "lend",
   949  					AllocatableResourceGeneration: 2,
   950  					RequestableResources:          initialCohortResources,
   951  					Usage: FlavorResourceQuantities{
   952  						"default": {corev1.ResourceCPU: 0},
   953  					},
   954  				}
   955  				return Snapshot{
   956  					ClusterQueues: map[string]*ClusterQueue{
   957  						"lend-a": {
   958  							Name:                          "lend-a",
   959  							Cohort:                        cohort,
   960  							Workloads:                     make(map[string]*workload.Info),
   961  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
   962  							FlavorFungibility:             defaultFlavorFungibility,
   963  							AllocatableResourceGeneration: 1,
   964  							Usage: FlavorResourceQuantities{
   965  								"default": {corev1.ResourceCPU: 0},
   966  							},
   967  							GuaranteedQuota: FlavorResourceQuantities{
   968  								"default": {
   969  									corev1.ResourceCPU: 6_000,
   970  								},
   971  							},
   972  						},
   973  						"lend-b": {
   974  							Name:                          "lend-b",
   975  							Cohort:                        cohort,
   976  							Workloads:                     make(map[string]*workload.Info),
   977  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
   978  							FlavorFungibility:             defaultFlavorFungibility,
   979  							AllocatableResourceGeneration: 1,
   980  							Usage: FlavorResourceQuantities{
   981  								"default": {corev1.ResourceCPU: 0},
   982  							},
   983  							GuaranteedQuota: FlavorResourceQuantities{
   984  								"default": {
   985  									corev1.ResourceCPU: 4_000,
   986  								},
   987  							},
   988  						},
   989  					},
   990  				}
   991  			}(),
   992  		},
   993  		"remove workload, but still using quota over GuaranteedQuota": {
   994  			remove: []string{"/lend-a-2"},
   995  			want: func() Snapshot {
   996  				cohort := &Cohort{
   997  					Name:                          "lend",
   998  					AllocatableResourceGeneration: 2,
   999  					RequestableResources:          initialCohortResources,
  1000  					Usage: FlavorResourceQuantities{
  1001  						"default": {corev1.ResourceCPU: 1_000},
  1002  					},
  1003  				}
  1004  				return Snapshot{
  1005  					ClusterQueues: map[string]*ClusterQueue{
  1006  						"lend-a": {
  1007  							Name:                          "lend-a",
  1008  							Cohort:                        cohort,
  1009  							Workloads:                     make(map[string]*workload.Info),
  1010  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1011  							FlavorFungibility:             defaultFlavorFungibility,
  1012  							AllocatableResourceGeneration: 1,
  1013  							Usage: FlavorResourceQuantities{
  1014  								"default": {corev1.ResourceCPU: 7_000},
  1015  							},
  1016  							GuaranteedQuota: FlavorResourceQuantities{
  1017  								"default": {
  1018  									corev1.ResourceCPU: 6_000,
  1019  								},
  1020  							},
  1021  						},
  1022  						"lend-b": {
  1023  							Name:                          "lend-b",
  1024  							Cohort:                        cohort,
  1025  							Workloads:                     make(map[string]*workload.Info),
  1026  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1027  							FlavorFungibility:             defaultFlavorFungibility,
  1028  							AllocatableResourceGeneration: 1,
  1029  							Usage: FlavorResourceQuantities{
  1030  								"default": {corev1.ResourceCPU: 4_000},
  1031  							},
  1032  							GuaranteedQuota: FlavorResourceQuantities{
  1033  								"default": {
  1034  									corev1.ResourceCPU: 4_000,
  1035  								},
  1036  							},
  1037  						},
  1038  					},
  1039  				}
  1040  			}(),
  1041  		},
  1042  		"remove wokload, using same quota as GuaranteedQuota": {
  1043  			remove: []string{"/lend-a-1", "/lend-a-2"},
  1044  			want: func() Snapshot {
  1045  				cohort := &Cohort{
  1046  					Name:                          "lend",
  1047  					AllocatableResourceGeneration: 2,
  1048  					RequestableResources:          initialCohortResources,
  1049  					Usage: FlavorResourceQuantities{
  1050  						"default": {corev1.ResourceCPU: 0},
  1051  					},
  1052  				}
  1053  				return Snapshot{
  1054  					ClusterQueues: map[string]*ClusterQueue{
  1055  						"lend-a": {
  1056  							Name:                          "lend-a",
  1057  							Cohort:                        cohort,
  1058  							Workloads:                     make(map[string]*workload.Info),
  1059  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1060  							FlavorFungibility:             defaultFlavorFungibility,
  1061  							AllocatableResourceGeneration: 1,
  1062  							Usage: FlavorResourceQuantities{
  1063  								"default": {corev1.ResourceCPU: 6_000},
  1064  							},
  1065  							GuaranteedQuota: FlavorResourceQuantities{
  1066  								"default": {
  1067  									corev1.ResourceCPU: 6_000,
  1068  								},
  1069  							},
  1070  						},
  1071  						"lend-b": {
  1072  							Name:                          "lend-b",
  1073  							Cohort:                        cohort,
  1074  							Workloads:                     make(map[string]*workload.Info),
  1075  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1076  							FlavorFungibility:             defaultFlavorFungibility,
  1077  							AllocatableResourceGeneration: 1,
  1078  							Usage: FlavorResourceQuantities{
  1079  								"default": {corev1.ResourceCPU: 4_000},
  1080  							},
  1081  							GuaranteedQuota: FlavorResourceQuantities{
  1082  								"default": {
  1083  									corev1.ResourceCPU: 4_000,
  1084  								},
  1085  							},
  1086  						},
  1087  					},
  1088  				}
  1089  			}(),
  1090  		},
  1091  		"remove workload, using less quota than GuaranteedQuota": {
  1092  			remove: []string{"/lend-a-2", "/lend-a-3"},
  1093  			want: func() Snapshot {
  1094  				cohort := &Cohort{
  1095  					Name:                          "lend",
  1096  					AllocatableResourceGeneration: 2,
  1097  					RequestableResources:          initialCohortResources,
  1098  					Usage: FlavorResourceQuantities{
  1099  						"default": {corev1.ResourceCPU: 0},
  1100  					},
  1101  				}
  1102  				return Snapshot{
  1103  					ClusterQueues: map[string]*ClusterQueue{
  1104  						"lend-a": {
  1105  							Name:                          "lend-a",
  1106  							Cohort:                        cohort,
  1107  							Workloads:                     make(map[string]*workload.Info),
  1108  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1109  							FlavorFungibility:             defaultFlavorFungibility,
  1110  							AllocatableResourceGeneration: 1,
  1111  							Usage: FlavorResourceQuantities{
  1112  								"default": {corev1.ResourceCPU: 1_000},
  1113  							},
  1114  							GuaranteedQuota: FlavorResourceQuantities{
  1115  								"default": {
  1116  									corev1.ResourceCPU: 6_000,
  1117  								},
  1118  							},
  1119  						},
  1120  						"lend-b": {
  1121  							Name:                          "lend-b",
  1122  							Cohort:                        cohort,
  1123  							Workloads:                     make(map[string]*workload.Info),
  1124  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1125  							FlavorFungibility:             defaultFlavorFungibility,
  1126  							AllocatableResourceGeneration: 1,
  1127  							Usage: FlavorResourceQuantities{
  1128  								"default": {corev1.ResourceCPU: 4_000},
  1129  							},
  1130  							GuaranteedQuota: FlavorResourceQuantities{
  1131  								"default": {
  1132  									corev1.ResourceCPU: 4_000,
  1133  								},
  1134  							},
  1135  						},
  1136  					},
  1137  				}
  1138  			}(),
  1139  		},
  1140  		"remove all then add workload, using less quota than GuaranteedQuota": {
  1141  			remove: []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
  1142  			add:    []string{"/lend-a-1"},
  1143  			want: func() Snapshot {
  1144  				cohort := &Cohort{
  1145  					Name:                          "lend",
  1146  					AllocatableResourceGeneration: 2,
  1147  					RequestableResources:          initialCohortResources,
  1148  					Usage: FlavorResourceQuantities{
  1149  						"default": {corev1.ResourceCPU: 0},
  1150  					},
  1151  				}
  1152  				return Snapshot{
  1153  					ClusterQueues: map[string]*ClusterQueue{
  1154  						"lend-a": {
  1155  							Name:                          "lend-a",
  1156  							Cohort:                        cohort,
  1157  							Workloads:                     make(map[string]*workload.Info),
  1158  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1159  							FlavorFungibility:             defaultFlavorFungibility,
  1160  							AllocatableResourceGeneration: 1,
  1161  							Usage: FlavorResourceQuantities{
  1162  								"default": {corev1.ResourceCPU: 1_000},
  1163  							},
  1164  							GuaranteedQuota: FlavorResourceQuantities{
  1165  								"default": {
  1166  									corev1.ResourceCPU: 6_000,
  1167  								},
  1168  							},
  1169  						},
  1170  						"lend-b": {
  1171  							Name:                          "lend-b",
  1172  							Cohort:                        cohort,
  1173  							Workloads:                     make(map[string]*workload.Info),
  1174  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1175  							FlavorFungibility:             defaultFlavorFungibility,
  1176  							AllocatableResourceGeneration: 1,
  1177  							Usage: FlavorResourceQuantities{
  1178  								"default": {corev1.ResourceCPU: 0},
  1179  							},
  1180  							GuaranteedQuota: FlavorResourceQuantities{
  1181  								"default": {
  1182  									corev1.ResourceCPU: 4_000,
  1183  								},
  1184  							},
  1185  						},
  1186  					},
  1187  				}
  1188  			}(),
  1189  		},
  1190  		"remove all then add workload, using same quota as GuaranteedQuota": {
  1191  			remove: []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
  1192  			add:    []string{"/lend-a-3"},
  1193  			want: func() Snapshot {
  1194  				cohort := &Cohort{
  1195  					Name:                          "lend",
  1196  					AllocatableResourceGeneration: 2,
  1197  					RequestableResources:          initialCohortResources,
  1198  					Usage: FlavorResourceQuantities{
  1199  						"default": {corev1.ResourceCPU: 0},
  1200  					},
  1201  				}
  1202  				return Snapshot{
  1203  					ClusterQueues: map[string]*ClusterQueue{
  1204  						"lend-a": {
  1205  							Name:                          "lend-a",
  1206  							Cohort:                        cohort,
  1207  							Workloads:                     make(map[string]*workload.Info),
  1208  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1209  							FlavorFungibility:             defaultFlavorFungibility,
  1210  							AllocatableResourceGeneration: 1,
  1211  							Usage: FlavorResourceQuantities{
  1212  								"default": {corev1.ResourceCPU: 6_000},
  1213  							},
  1214  							GuaranteedQuota: FlavorResourceQuantities{
  1215  								"default": {
  1216  									corev1.ResourceCPU: 6_000,
  1217  								},
  1218  							},
  1219  						},
  1220  						"lend-b": {
  1221  							Name:                          "lend-b",
  1222  							Cohort:                        cohort,
  1223  							Workloads:                     make(map[string]*workload.Info),
  1224  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1225  							FlavorFungibility:             defaultFlavorFungibility,
  1226  							AllocatableResourceGeneration: 1,
  1227  							Usage: FlavorResourceQuantities{
  1228  								"default": {corev1.ResourceCPU: 0},
  1229  							},
  1230  							GuaranteedQuota: FlavorResourceQuantities{
  1231  								"default": {
  1232  									corev1.ResourceCPU: 4_000,
  1233  								},
  1234  							},
  1235  						},
  1236  					},
  1237  				}
  1238  			}(),
  1239  		},
  1240  		"remove all then add workload, using quota over GuaranteedQuota": {
  1241  			remove: []string{"/lend-a-1", "/lend-a-2", "/lend-a-3", "/lend-b-1"},
  1242  			add:    []string{"/lend-a-2"},
  1243  			want: func() Snapshot {
  1244  				cohort := &Cohort{
  1245  					Name:                          "lend",
  1246  					AllocatableResourceGeneration: 2,
  1247  					RequestableResources:          initialCohortResources,
  1248  					Usage: FlavorResourceQuantities{
  1249  						"default": {corev1.ResourceCPU: 3_000},
  1250  					},
  1251  				}
  1252  				return Snapshot{
  1253  					ClusterQueues: map[string]*ClusterQueue{
  1254  						"lend-a": {
  1255  							Name:                          "lend-a",
  1256  							Cohort:                        cohort,
  1257  							Workloads:                     make(map[string]*workload.Info),
  1258  							ResourceGroups:                cqCache.clusterQueues["lend-a"].ResourceGroups,
  1259  							FlavorFungibility:             defaultFlavorFungibility,
  1260  							AllocatableResourceGeneration: 1,
  1261  							Usage: FlavorResourceQuantities{
  1262  								"default": {corev1.ResourceCPU: 9_000},
  1263  							},
  1264  							GuaranteedQuota: FlavorResourceQuantities{
  1265  								"default": {
  1266  									corev1.ResourceCPU: 6_000,
  1267  								},
  1268  							},
  1269  						},
  1270  						"lend-b": {
  1271  							Name:                          "lend-b",
  1272  							Cohort:                        cohort,
  1273  							Workloads:                     make(map[string]*workload.Info),
  1274  							ResourceGroups:                cqCache.clusterQueues["lend-b"].ResourceGroups,
  1275  							FlavorFungibility:             defaultFlavorFungibility,
  1276  							AllocatableResourceGeneration: 1,
  1277  							Usage: FlavorResourceQuantities{
  1278  								"default": {corev1.ResourceCPU: 0},
  1279  							},
  1280  							GuaranteedQuota: FlavorResourceQuantities{
  1281  								"default": {
  1282  									corev1.ResourceCPU: 4_000,
  1283  								},
  1284  							},
  1285  						},
  1286  					},
  1287  				}
  1288  			}(),
  1289  		},
  1290  	}
  1291  	cmpOpts := append(snapCmpOpts,
  1292  		cmpopts.IgnoreFields(ClusterQueue{}, "NamespaceSelector", "Preemption", "Status"),
  1293  		cmpopts.IgnoreFields(Snapshot{}, "ResourceFlavors"),
  1294  		cmpopts.IgnoreTypes(&workload.Info{}))
  1295  	for name, tc := range cases {
  1296  		t.Run(name, func(t *testing.T) {
  1297  			snap := cqCache.Snapshot()
  1298  			for _, name := range tc.remove {
  1299  				snap.RemoveWorkload(wlInfos[name])
  1300  			}
  1301  			for _, name := range tc.add {
  1302  				snap.AddWorkload(wlInfos[name])
  1303  			}
  1304  			if diff := cmp.Diff(tc.want, snap, cmpOpts...); diff != "" {
  1305  				t.Errorf("Unexpected snapshot state after operations (-want,+got):\n%s", diff)
  1306  			}
  1307  		})
  1308  	}
  1309  }