/* Copyright 2024 The Karmada Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package scheduler import ( "testing" "time" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" ) func TestResourceBindingEventFilter(t *testing.T) { testCases := []struct { name string schedulerName string obj interface{} expectedResult bool }{ { name: "ResourceBinding: Matching scheduler name, no labels", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", nil, nil), expectedResult: false, }, { name: "ResourceBinding: Non-matching scheduler name", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "other-scheduler", nil, nil), expectedResult: false, }, { name: "ResourceBinding: Matching scheduler name, with PropagationPolicyPermanentIDLabel", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", map[string]string{ policyv1alpha1.PropagationPolicyPermanentIDLabel: "test-id", }, nil), expectedResult: true, }, { name: "ResourceBinding: Matching scheduler name, with ClusterPropagationPolicyPermanentIDLabel", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", map[string]string{ policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: "test-id", }, nil), expectedResult: true, }, { name: "ResourceBinding: Matching scheduler name, with BindingManagedByLabel", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", map[string]string{ workv1alpha2.BindingManagedByLabel: "test-manager", }, nil), expectedResult: true, }, { name: "ResourceBinding: Matching scheduler name, with empty PropagationPolicyPermanentIDLabel", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", map[string]string{ policyv1alpha1.PropagationPolicyPermanentIDLabel: "", }, nil), expectedResult: false, }, { name: "ClusterResourceBinding: Matching scheduler name, no labels", schedulerName: "test-scheduler", obj: createClusterResourceBinding("test-crb", "test-scheduler", nil, nil), expectedResult: false, }, { name: "ClusterResourceBinding: Non-matching scheduler name", schedulerName: "test-scheduler", obj: createClusterResourceBinding("test-crb", "other-scheduler", nil, nil), expectedResult: false, }, { name: "ClusterResourceBinding: Matching scheduler name, with ClusterPropagationPolicyPermanentIDLabel", schedulerName: "test-scheduler", obj: createClusterResourceBinding("test-crb", "test-scheduler", map[string]string{ policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: "test-id", }, nil), expectedResult: true, }, { name: "Nil object", schedulerName: "test-scheduler", obj: nil, expectedResult: false, }, { name: "Invalid object type", schedulerName: "test-scheduler", obj: "not-a-valid-object", expectedResult: false, }, { name: "ResourceBinding suspended", schedulerName: "test-scheduler", obj: createResourceBinding("test-rb", "test-scheduler", map[string]string{ workv1alpha2.BindingManagedByLabel: "test-manager", }, &workv1alpha2.Suspension{Scheduling: ptr.To(true)}), expectedResult: false, }, { name: "ClusterResourceBinding suspended", schedulerName: "test-scheduler", obj: createClusterResourceBinding("test-crb", "test-scheduler", map[string]string{ policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel: "test-id", }, &workv1alpha2.Suspension{Scheduling: ptr.To(true)}), expectedResult: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := &Scheduler{ schedulerName: tc.schedulerName, } result := s.resourceBindingEventFilter(tc.obj) assert.Equal(t, tc.expectedResult, result, "Test case: %s", tc.name) }) } } func TestAddCluster(t *testing.T) { tests := []struct { name string enableSchedulerEstimator bool obj interface{} expectedAdded bool expectedClusterName string }{ { name: "valid cluster object with estimator enabled", enableSchedulerEstimator: true, obj: createCluster("test-cluster", 0, nil), expectedAdded: true, expectedClusterName: "test-cluster", }, { name: "valid cluster object with estimator disabled", enableSchedulerEstimator: false, obj: createCluster("test-cluster-2", 0, nil), expectedAdded: false, expectedClusterName: "", }, { name: "invalid object type", enableSchedulerEstimator: true, obj: &corev1.Pod{}, expectedAdded: false, expectedClusterName: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockWorker := &mockAsyncWorker{} s := &Scheduler{ enableSchedulerEstimator: tt.enableSchedulerEstimator, schedulerEstimatorWorker: mockWorker, } s.addCluster(tt.obj) if tt.expectedAdded { assert.Equal(t, 1, mockWorker.addCount, "Worker Add should have been called once") assert.Equal(t, tt.expectedClusterName, mockWorker.lastAdded, "Incorrect cluster name added") } else { assert.Equal(t, 0, mockWorker.addCount, "Worker Add should not have been called") assert.Nil(t, mockWorker.lastAdded, "No cluster name should have been added") } assert.Equal(t, 0, mockWorker.enqueueCount, "Worker Enqueue should not have been called") assert.Nil(t, mockWorker.lastEnqueued, "No item should have been enqueued") }) } } func TestUpdateCluster(t *testing.T) { tests := []struct { name string enableSchedulerEstimator bool oldObj interface{} newObj interface{} expectedEstimatorAdded bool expectedReconcileAdded int }{ { name: "valid cluster update with generation change", enableSchedulerEstimator: true, oldObj: createCluster("test-cluster", 1, nil), newObj: createCluster("test-cluster", 2, nil), expectedEstimatorAdded: true, expectedReconcileAdded: 2, }, { name: "valid cluster update with label change", enableSchedulerEstimator: true, oldObj: createCluster("test-cluster", 0, map[string]string{"old": "label"}), newObj: createCluster("test-cluster", 0, map[string]string{"new": "label"}), expectedEstimatorAdded: true, expectedReconcileAdded: 2, }, { name: "valid cluster update without changes", enableSchedulerEstimator: true, oldObj: createCluster("test-cluster", 0, nil), newObj: createCluster("test-cluster", 0, nil), expectedEstimatorAdded: true, expectedReconcileAdded: 0, }, { name: "invalid old object type", enableSchedulerEstimator: true, oldObj: &corev1.Pod{}, newObj: createCluster("test-cluster", 0, nil), expectedEstimatorAdded: false, expectedReconcileAdded: 0, }, { name: "invalid new object type", enableSchedulerEstimator: true, oldObj: createCluster("test-cluster", 0, nil), newObj: &corev1.Pod{}, expectedEstimatorAdded: false, expectedReconcileAdded: 0, }, { name: "both objects invalid", enableSchedulerEstimator: true, oldObj: &corev1.Pod{}, newObj: &corev1.Pod{}, expectedEstimatorAdded: false, expectedReconcileAdded: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { estimatorWorker := &mockAsyncWorker{} reconcileWorker := &mockAsyncWorker{} s := &Scheduler{ enableSchedulerEstimator: tt.enableSchedulerEstimator, schedulerEstimatorWorker: estimatorWorker, clusterReconcileWorker: reconcileWorker, } s.updateCluster(tt.oldObj, tt.newObj) // Check schedulerEstimatorWorker if tt.expectedEstimatorAdded { assert.Equal(t, 1, estimatorWorker.addCount, "Estimator worker Add should have been called once") if cluster, ok := tt.newObj.(*clusterv1alpha1.Cluster); ok { assert.Equal(t, cluster.Name, estimatorWorker.lastAdded, "Incorrect cluster name added to estimator worker") } else { t.Errorf("Expected newObj to be a Cluster, but it wasn't") } } else { assert.Equal(t, 0, estimatorWorker.addCount, "Estimator worker Add should not have been called") assert.Nil(t, estimatorWorker.lastAdded, "No cluster should have been added to estimator worker") } // Check clusterReconcileWorker assert.Equal(t, tt.expectedReconcileAdded, reconcileWorker.addCount, "Reconcile worker Add called unexpected number of times") if tt.expectedReconcileAdded > 0 { lastAdded, ok := reconcileWorker.lastAdded.(*clusterv1alpha1.Cluster) assert.True(t, ok, "Last added item is not a Cluster object") if ok { newCluster, newOk := tt.newObj.(*clusterv1alpha1.Cluster) assert.True(t, newOk, "newObj is not a Cluster object") if newOk { assert.Equal(t, newCluster.Name, lastAdded.Name, "Incorrect cluster added to reconcile worker") } } } else { assert.Nil(t, reconcileWorker.lastAdded, "No cluster should have been added to reconcile worker") } }) } } func TestDeleteCluster(t *testing.T) { tests := []struct { name string enableSchedulerEstimator bool obj interface{} expectedAdded bool expectedClusterName string }{ { name: "valid cluster object with estimator enabled", enableSchedulerEstimator: true, obj: createCluster("test-cluster", 0, nil), expectedAdded: true, expectedClusterName: "test-cluster", }, { name: "valid cluster object with estimator disabled", enableSchedulerEstimator: false, obj: createCluster("test-cluster", 0, nil), expectedAdded: false, expectedClusterName: "", }, { name: "deleted final state unknown with valid cluster", enableSchedulerEstimator: true, obj: cache.DeletedFinalStateUnknown{ Key: "test-cluster", Obj: createCluster("test-cluster", 0, nil), }, expectedAdded: true, expectedClusterName: "test-cluster", }, { name: "deleted final state unknown with invalid object", enableSchedulerEstimator: true, obj: cache.DeletedFinalStateUnknown{ Key: "test-pod", Obj: &corev1.Pod{}, }, expectedAdded: false, expectedClusterName: "", }, { name: "invalid object type", enableSchedulerEstimator: true, obj: &corev1.Pod{}, expectedAdded: false, expectedClusterName: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { worker := &mockAsyncWorker{} s := &Scheduler{ enableSchedulerEstimator: tt.enableSchedulerEstimator, schedulerEstimatorWorker: worker, } s.deleteCluster(tt.obj) if tt.expectedAdded { assert.Equal(t, 1, worker.addCount, "Worker Add should have been called once") assert.Equal(t, tt.expectedClusterName, worker.lastAdded, "Incorrect cluster name added to worker") } else { assert.Equal(t, 0, worker.addCount, "Worker Add should not have been called") assert.Nil(t, worker.lastAdded, "No cluster name should have been added") } }) } } func TestSchedulerNameFilter(t *testing.T) { tests := []struct { name string schedulerNameFromOptions string schedulerName string expected bool }{ { name: "matching scheduler names", schedulerNameFromOptions: "test-scheduler", schedulerName: "test-scheduler", expected: true, }, { name: "non-matching scheduler names", schedulerNameFromOptions: "test-scheduler", schedulerName: "other-scheduler", expected: false, }, { name: "empty scheduler name defaults to DefaultScheduler", schedulerNameFromOptions: DefaultScheduler, schedulerName: "", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := schedulerNameFilter(tt.schedulerNameFromOptions, tt.schedulerName) assert.Equal(t, tt.expected, result) }) } } // Helper functions func createCluster(name string, generation int64, labels map[string]string) *clusterv1alpha1.Cluster { return &clusterv1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: name, Generation: generation, Labels: labels, }, } } func createResourceBinding(name, schedulerName string, labels map[string]string, suspension *workv1alpha2.Suspension) *workv1alpha2.ResourceBinding { return &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, }, Spec: workv1alpha2.ResourceBindingSpec{ SchedulerName: schedulerName, Suspension: suspension, }, } } func createClusterResourceBinding(name, schedulerName string, labels map[string]string, suspension *workv1alpha2.Suspension) *workv1alpha2.ClusterResourceBinding { return &workv1alpha2.ClusterResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, }, Spec: workv1alpha2.ResourceBindingSpec{ SchedulerName: schedulerName, Suspension: suspension, }, } } // Mock Implementations // mockAsyncWorker is a mock implementation of util.AsyncWorker type mockAsyncWorker struct { addCount int enqueueCount int lastAdded interface{} lastEnqueued interface{} } func (m *mockAsyncWorker) Add(item interface{}) { m.addCount++ m.lastAdded = item } func (m *mockAsyncWorker) Enqueue(item interface{}) { m.enqueueCount++ m.lastEnqueued = item } func (m *mockAsyncWorker) AddAfter(_ interface{}, _ time.Duration) {} func (m *mockAsyncWorker) Run(_ int, _ <-chan struct{}) {}