sigs.k8s.io/kueue@v0.6.2/pkg/controller/jobframework/integrationmanager_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 jobframework
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"reflect"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/go-cmp/cmp/cmpopts"
    27  	corev1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/client-go/tools/record"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  )
    34  
    35  func testNewReconciler(client.Client, record.EventRecorder, ...Option) JobReconcilerInterface {
    36  	return nil
    37  }
    38  
    39  func testSetupWebhook(ctrl.Manager, ...Option) error {
    40  	return nil
    41  }
    42  
    43  func testSetupIndexes(context.Context, client.FieldIndexer) error {
    44  	return nil
    45  }
    46  
    47  func testAddToScheme(*runtime.Scheme) error {
    48  	return nil
    49  }
    50  
    51  func testCanSupportIntegration(...Option) (bool, error) {
    52  	return true, nil
    53  }
    54  
    55  var (
    56  	testIntegrationCallbacks = IntegrationCallbacks{
    57  		NewReconciler:         testNewReconciler,
    58  		SetupWebhook:          testSetupWebhook,
    59  		JobType:               &corev1.Pod{},
    60  		SetupIndexes:          testSetupIndexes,
    61  		AddToScheme:           testAddToScheme,
    62  		CanSupportIntegration: testCanSupportIntegration,
    63  	}
    64  )
    65  
    66  func TestRegister(t *testing.T) {
    67  	cases := map[string]struct {
    68  		manager              *integrationManager
    69  		integrationName      string
    70  		integrationCallbacks IntegrationCallbacks
    71  		wantError            error
    72  		wantList             []string
    73  		wantCallbacks        IntegrationCallbacks
    74  	}{
    75  		"successful": {
    76  			manager: &integrationManager{
    77  				names: []string{"oldFramework"},
    78  				integrations: map[string]IntegrationCallbacks{
    79  					"oldFramework": testIntegrationCallbacks,
    80  				},
    81  			},
    82  			integrationName:      "newFramework",
    83  			integrationCallbacks: testIntegrationCallbacks,
    84  			wantError:            nil,
    85  			wantList:             []string{"newFramework", "oldFramework"},
    86  			wantCallbacks:        testIntegrationCallbacks,
    87  		},
    88  		"duplicate name": {
    89  			manager: &integrationManager{
    90  				names: []string{"newFramework"},
    91  				integrations: map[string]IntegrationCallbacks{
    92  					"newFramework": testIntegrationCallbacks,
    93  				},
    94  			},
    95  			integrationName:      "newFramework",
    96  			integrationCallbacks: IntegrationCallbacks{},
    97  			wantError:            errDuplicateFrameworkName,
    98  			wantList:             []string{"newFramework"},
    99  			wantCallbacks:        testIntegrationCallbacks,
   100  		},
   101  		"missing NewReconciler": {
   102  			manager:         &integrationManager{},
   103  			integrationName: "newFramework",
   104  			integrationCallbacks: IntegrationCallbacks{
   105  				SetupWebhook:          testSetupWebhook,
   106  				JobType:               &corev1.Pod{},
   107  				SetupIndexes:          testSetupIndexes,
   108  				AddToScheme:           testAddToScheme,
   109  				CanSupportIntegration: testCanSupportIntegration,
   110  			},
   111  			wantError: errMissingMandatoryField,
   112  			wantList:  []string{},
   113  		},
   114  		"missing SetupWebhook": {
   115  			manager:         &integrationManager{},
   116  			integrationName: "newFramework",
   117  			integrationCallbacks: IntegrationCallbacks{
   118  				NewReconciler:         testNewReconciler,
   119  				JobType:               &corev1.Pod{},
   120  				SetupIndexes:          testSetupIndexes,
   121  				AddToScheme:           testAddToScheme,
   122  				CanSupportIntegration: testCanSupportIntegration,
   123  			},
   124  			wantError: errMissingMandatoryField,
   125  			wantList:  []string{},
   126  		},
   127  		"missing JobType": {
   128  			manager:         &integrationManager{},
   129  			integrationName: "newFramework",
   130  			integrationCallbacks: IntegrationCallbacks{
   131  				NewReconciler:         testNewReconciler,
   132  				SetupWebhook:          testSetupWebhook,
   133  				SetupIndexes:          testSetupIndexes,
   134  				AddToScheme:           testAddToScheme,
   135  				CanSupportIntegration: testCanSupportIntegration,
   136  			},
   137  			wantError: errMissingMandatoryField,
   138  			wantList:  []string{},
   139  		},
   140  		"missing SetupIndexes": {
   141  			manager:         &integrationManager{},
   142  			integrationName: "newFramework",
   143  			integrationCallbacks: IntegrationCallbacks{
   144  				NewReconciler:         testNewReconciler,
   145  				SetupWebhook:          testSetupWebhook,
   146  				JobType:               &corev1.Pod{},
   147  				AddToScheme:           testAddToScheme,
   148  				CanSupportIntegration: testCanSupportIntegration,
   149  			},
   150  			wantError: nil,
   151  			wantList:  []string{"newFramework"},
   152  			wantCallbacks: IntegrationCallbacks{
   153  				NewReconciler: testNewReconciler,
   154  				SetupWebhook:  testSetupWebhook,
   155  				JobType:       &corev1.Pod{},
   156  				AddToScheme:   testAddToScheme,
   157  			},
   158  		},
   159  		"missing AddToScheme": {
   160  			manager:         &integrationManager{},
   161  			integrationName: "newFramework",
   162  			integrationCallbacks: IntegrationCallbacks{
   163  				NewReconciler:         testNewReconciler,
   164  				SetupWebhook:          testSetupWebhook,
   165  				JobType:               &corev1.Pod{},
   166  				SetupIndexes:          testSetupIndexes,
   167  				CanSupportIntegration: testCanSupportIntegration,
   168  			},
   169  			wantError: nil,
   170  			wantList:  []string{"newFramework"},
   171  			wantCallbacks: IntegrationCallbacks{
   172  				NewReconciler: testNewReconciler,
   173  				SetupWebhook:  testSetupWebhook,
   174  				JobType:       &corev1.Pod{},
   175  				SetupIndexes:  testSetupIndexes,
   176  			},
   177  		},
   178  		"missing CanSupportIntegration": {
   179  			manager:         &integrationManager{},
   180  			integrationName: "newFramework",
   181  			integrationCallbacks: IntegrationCallbacks{
   182  				NewReconciler: testNewReconciler,
   183  				AddToScheme:   testAddToScheme,
   184  				SetupWebhook:  testSetupWebhook,
   185  				JobType:       &corev1.Pod{},
   186  				SetupIndexes:  testSetupIndexes,
   187  			},
   188  			wantError: nil,
   189  			wantList:  []string{"newFramework"},
   190  			wantCallbacks: IntegrationCallbacks{
   191  				NewReconciler: testNewReconciler,
   192  				AddToScheme:   testAddToScheme,
   193  				SetupWebhook:  testSetupWebhook,
   194  				JobType:       &corev1.Pod{},
   195  				SetupIndexes:  testSetupIndexes,
   196  			},
   197  		},
   198  	}
   199  
   200  	for tcName, tc := range cases {
   201  		t.Run(tcName, func(t *testing.T) {
   202  			gotError := tc.manager.register(tc.integrationName, tc.integrationCallbacks)
   203  			if diff := cmp.Diff(tc.wantError, gotError, cmpopts.EquateErrors()); diff != "" {
   204  				t.Errorf("Unexpected error (-want +got):\n%s", diff)
   205  			}
   206  			gotList := tc.manager.getList()
   207  			if diff := cmp.Diff(tc.wantList, gotList); diff != "" {
   208  				t.Errorf("Unexpected frameworks list (-want +got):\n%s", diff)
   209  			}
   210  
   211  			if gotCallbacks, found := tc.manager.get(tc.integrationName); found {
   212  				if diff := cmp.Diff(tc.wantCallbacks, gotCallbacks, cmp.FilterValues(func(_, _ interface{}) bool { return true }, cmp.Comparer(compareCallbacks))); diff != "" {
   213  					t.Errorf("Unexpected callbacks (-want +got):\n%s", diff)
   214  				}
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func compareCallbacks(x, y interface{}) bool {
   221  	xcb := x.(IntegrationCallbacks)
   222  	ycb := y.(IntegrationCallbacks)
   223  
   224  	if reflect.ValueOf(xcb.NewReconciler).Pointer() != reflect.ValueOf(ycb.NewReconciler).Pointer() {
   225  		return false
   226  	}
   227  	if reflect.ValueOf(xcb.SetupWebhook).Pointer() != reflect.ValueOf(ycb.SetupWebhook).Pointer() {
   228  		return false
   229  	}
   230  	if reflect.TypeOf(xcb.JobType) != reflect.TypeOf(ycb.JobType) {
   231  		return false
   232  	}
   233  	if reflect.ValueOf(xcb.SetupIndexes).Pointer() != reflect.ValueOf(ycb.SetupIndexes).Pointer() {
   234  		return false
   235  	}
   236  	return reflect.ValueOf(xcb.AddToScheme).Pointer() == reflect.ValueOf(ycb.AddToScheme).Pointer()
   237  }
   238  
   239  func TestForEach(t *testing.T) {
   240  	foeEachError := errors.New("test error")
   241  	cases := map[string]struct {
   242  		registered []string
   243  		errorOn    string
   244  		wantCalls  []string
   245  		wantError  error
   246  	}{
   247  		"all": {
   248  			registered: []string{"a", "b", "c", "d", "e"},
   249  			errorOn:    "",
   250  			wantCalls:  []string{"a", "b", "c", "d", "e"},
   251  			wantError:  nil,
   252  		},
   253  		"partial": {
   254  			registered: []string{"a", "b", "c", "d", "e"},
   255  			errorOn:    "c",
   256  			wantCalls:  []string{"a", "b", "c"},
   257  			wantError:  foeEachError,
   258  		},
   259  	}
   260  
   261  	for tcName, tc := range cases {
   262  		t.Run(tcName, func(t *testing.T) {
   263  			manager := integrationManager{}
   264  			for _, name := range tc.registered {
   265  				if err := manager.register(name, testIntegrationCallbacks); err != nil {
   266  					t.Fatalf("unable to register %s, %s", name, err.Error())
   267  				}
   268  			}
   269  
   270  			gotCalls := []string{}
   271  			gotError := manager.forEach(func(name string, cb IntegrationCallbacks) error {
   272  				gotCalls = append(gotCalls, name)
   273  				if name == tc.errorOn {
   274  					return foeEachError
   275  				}
   276  				return nil
   277  			})
   278  
   279  			if diff := cmp.Diff(tc.wantError, gotError, cmpopts.EquateErrors()); diff != "" {
   280  				t.Errorf("Unexpected error (-want +got):\n%s", diff)
   281  			}
   282  			if diff := cmp.Diff(tc.wantCalls, gotCalls); diff != "" {
   283  				t.Errorf("Unexpected calls list (-want +got):\n%s", diff)
   284  			}
   285  		})
   286  	}
   287  }
   288  
   289  func TestGetCallbacksForOwner(t *testing.T) {
   290  	dontManage := IntegrationCallbacks{
   291  		NewReconciler: func(client.Client, record.EventRecorder, ...Option) JobReconcilerInterface {
   292  			panic("not implemented")
   293  		},
   294  		SetupWebhook: func(ctrl.Manager, ...Option) error { panic("not implemented") },
   295  		JobType:      nil,
   296  	}
   297  	manageK1 := func() IntegrationCallbacks {
   298  		ret := dontManage
   299  		ret.IsManagingObjectsOwner = func(owner *metav1.OwnerReference) bool { return owner.Kind == "K1" }
   300  		return ret
   301  	}()
   302  	manageK2 := func() IntegrationCallbacks {
   303  		ret := dontManage
   304  		ret.IsManagingObjectsOwner = func(owner *metav1.OwnerReference) bool { return owner.Kind == "K2" }
   305  		return ret
   306  	}()
   307  
   308  	mgr := integrationManager{
   309  		names: []string{"manageK1", "dontManage", "manageK2"},
   310  		integrations: map[string]IntegrationCallbacks{
   311  			"dontManage": dontManage,
   312  			"manageK1":   manageK1,
   313  			"manageK2":   manageK2,
   314  		},
   315  	}
   316  
   317  	cases := map[string]struct {
   318  		owner         *metav1.OwnerReference
   319  		wantCallbacks *IntegrationCallbacks
   320  	}{
   321  		"K1": {
   322  			owner:         &metav1.OwnerReference{Kind: "K1"},
   323  			wantCallbacks: &manageK1,
   324  		},
   325  		"K2": {
   326  			owner:         &metav1.OwnerReference{Kind: "K2"},
   327  			wantCallbacks: &manageK2,
   328  		},
   329  		"K3": {
   330  			owner:         &metav1.OwnerReference{Kind: "K3"},
   331  			wantCallbacks: nil,
   332  		},
   333  	}
   334  
   335  	for tcName, tc := range cases {
   336  		t.Run(tcName, func(t *testing.T) {
   337  			gotCallbacks := mgr.getCallbacksForOwner(tc.owner)
   338  			if tc.wantCallbacks == nil {
   339  				if gotCallbacks != nil {
   340  					t.Errorf("This owner should be unmanaged")
   341  				}
   342  			} else {
   343  				if gotCallbacks == nil {
   344  					t.Errorf("This owner should be managed")
   345  				} else {
   346  					if diff := cmp.Diff(*tc.wantCallbacks, *gotCallbacks, cmp.FilterValues(func(_, _ interface{}) bool { return true }, cmp.Comparer(compareCallbacks))); diff != "" {
   347  						t.Errorf("Unexpected callbacks (-want +got):\n%s", diff)
   348  					}
   349  				}
   350  			}
   351  		})
   352  	}
   353  }