From 0d6d2d732e3f93843c0e932fc7a3a2febedcf485 Mon Sep 17 00:00:00 2001 From: Anuj Agrawal Date: Sat, 12 Oct 2024 15:08:41 +0530 Subject: [PATCH] Added first part of unit tests for pkg/scheduler Signed-off-by: Anuj Agrawal Added first part of unit tests for pkg/scheduler Signed-off-by: Anuj Agrawal --- pkg/scheduler/scheduler_test.go | 816 ++++++++++++++++++++++++++++++++ 1 file changed, 816 insertions(+) diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index ee97bd594494..b6f94f8ce9f9 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -18,23 +18,780 @@ package scheduler import ( "context" + "errors" "fmt" "reflect" + "strings" "testing" "time" + "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" dynamicfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/record" + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" karmadafake "github.com/karmada-io/karmada/pkg/generated/clientset/versioned/fake" + workv1alpha2lister "github.com/karmada-io/karmada/pkg/generated/listers/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/scheduler/core" + schedulercore "github.com/karmada-io/karmada/pkg/scheduler/core" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/grpcconnection" ) +func TestDoSchedule(t *testing.T) { + tests := []struct { + name string + key string + binding interface{} + expectError bool + }{ + { + name: "invalid key format", + key: "invalid/key/format", + binding: nil, + expectError: true, + }, + { + name: "ResourceBinding scheduling", + key: "default/test-binding", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + }, + }, + expectError: false, + }, + { + name: "ClusterResourceBinding scheduling", + key: "test-cluster-binding", + binding: &workv1alpha2.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-binding", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := karmadafake.NewSimpleClientset() + fakeRecorder := record.NewFakeRecorder(10) + + var bindingLister *fakeBindingLister + var clusterBindingLister *fakeClusterBindingLister + + if rb, ok := tt.binding.(*workv1alpha2.ResourceBinding); ok { + bindingLister = &fakeBindingLister{binding: rb} + _, err := fakeClient.WorkV1alpha2().ResourceBindings(rb.Namespace).Create(context.TODO(), rb, metav1.CreateOptions{}) + assert.NoError(t, err) + } + if crb, ok := tt.binding.(*workv1alpha2.ClusterResourceBinding); ok { + clusterBindingLister = &fakeClusterBindingLister{binding: crb} + _, err := fakeClient.WorkV1alpha2().ClusterResourceBindings().Create(context.TODO(), crb, metav1.CreateOptions{}) + assert.NoError(t, err) + } + + mockAlgo := &mockAlgorithm{ + scheduleFunc: func(_ context.Context, _ *workv1alpha2.ResourceBindingSpec, _ *workv1alpha2.ResourceBindingStatus, _ *schedulercore.ScheduleAlgorithmOption) (schedulercore.ScheduleResult, error) { + return schedulercore.ScheduleResult{ + SuggestedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, nil + }, + } + + s := &Scheduler{ + KarmadaClient: fakeClient, + eventRecorder: fakeRecorder, + bindingLister: bindingLister, + clusterBindingLister: clusterBindingLister, + Algorithm: mockAlgo, + } + + err := s.doSchedule(tt.key) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if !tt.expectError { + if rb, ok := tt.binding.(*workv1alpha2.ResourceBinding); ok { + updated, err := fakeClient.WorkV1alpha2().ResourceBindings(rb.Namespace).Get(context.TODO(), rb.Name, metav1.GetOptions{}) + assert.NoError(t, err) + assert.NotNil(t, updated.Spec.Clusters) + assert.Len(t, updated.Spec.Clusters, 1) + assert.Equal(t, "cluster1", updated.Spec.Clusters[0].Name) + } + if crb, ok := tt.binding.(*workv1alpha2.ClusterResourceBinding); ok { + updated, err := fakeClient.WorkV1alpha2().ClusterResourceBindings().Get(context.TODO(), crb.Name, metav1.GetOptions{}) + assert.NoError(t, err) + assert.NotNil(t, updated.Spec.Clusters) + assert.Len(t, updated.Spec.Clusters, 1) + assert.Equal(t, "cluster1", updated.Spec.Clusters[0].Name) + } + } + }) + } +} + +func TestDoScheduleBinding(t *testing.T) { + tests := []struct { + name string + binding *workv1alpha2.ResourceBinding + expectSchedule bool + expectError bool + expectedClusters []workv1alpha2.TargetCluster + expectedEvent string + }{ + { + name: "binding with changed placement", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-1", + Namespace: "default", + Annotations: map[string]string{ + util.PolicyPlacementAnnotation: `{"clusterAffinity":{"clusterNames":["cluster1"]}}`, + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1", "cluster2"}, + }, + }, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + {Name: "cluster2", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + { + name: "binding with replicas changed", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-2", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Replicas: 2, + Placement: &policyv1alpha1.Placement{ + ReplicaScheduling: &policyv1alpha1.ReplicaSchedulingStrategy{ + ReplicaSchedulingType: policyv1alpha1.ReplicaSchedulingTypeDivided, + }, + }, + }, + Status: workv1alpha2.ResourceBindingStatus{ + SchedulerObservedGeneration: 1, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + {Name: "cluster2", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + { + name: "binding with reschedule triggered", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-3", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + RescheduleTriggeredAt: &metav1.Time{Time: time.Now()}, + Placement: &policyv1alpha1.Placement{}, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := karmadafake.NewSimpleClientset(tt.binding) + fakeRecorder := record.NewFakeRecorder(10) + mockAlgorithm := &mockAlgorithm{ + scheduleFunc: func(context.Context, *workv1alpha2.ResourceBindingSpec, *workv1alpha2.ResourceBindingStatus, *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) { + return core.ScheduleResult{SuggestedClusters: tt.expectedClusters}, nil + }, + } + + s := &Scheduler{ + KarmadaClient: fakeClient, + bindingLister: &fakeBindingLister{binding: tt.binding}, + eventRecorder: fakeRecorder, + Algorithm: mockAlgorithm, + } + + err := s.doScheduleBinding(tt.binding.Namespace, tt.binding.Name) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + updatedBinding, err := fakeClient.WorkV1alpha2().ResourceBindings(tt.binding.Namespace).Get(context.TODO(), tt.binding.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + if tt.expectSchedule { + assert.Equal(t, tt.expectedClusters, updatedBinding.Spec.Clusters) + assert.NotEqual(t, tt.binding.Spec.Clusters, updatedBinding.Spec.Clusters) + } else { + assert.Equal(t, tt.binding.Spec.Clusters, updatedBinding.Spec.Clusters) + } + + // Check for expected events + select { + case event := <-fakeRecorder.Events: + assert.Contains(t, event, tt.expectedEvent) + default: + t.Errorf("Expected an event to be recorded") + } + }) + } +} + +func TestDoScheduleClusterBinding(t *testing.T) { + tests := []struct { + name string + binding *workv1alpha2.ClusterResourceBinding + expectSchedule bool + expectError bool + expectedClusters []workv1alpha2.TargetCluster + expectedEvent string + }{ + { + name: "cluster binding with changed placement", + binding: &workv1alpha2.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-binding-1", + Annotations: map[string]string{ + util.PolicyPlacementAnnotation: `{"clusterAffinity":{"clusterNames":["cluster1"]}}`, + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1", "cluster2"}, + }, + }, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + {Name: "cluster2", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + { + name: "cluster binding with replicas changed", + binding: &workv1alpha2.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-binding-2", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Replicas: 2, + Placement: &policyv1alpha1.Placement{ + ReplicaScheduling: &policyv1alpha1.ReplicaSchedulingStrategy{ + ReplicaSchedulingType: policyv1alpha1.ReplicaSchedulingTypeDivided, + }, + }, + }, + Status: workv1alpha2.ResourceBindingStatus{ + SchedulerObservedGeneration: 1, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + {Name: "cluster2", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + { + name: "cluster binding with reschedule triggered", + binding: &workv1alpha2.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster-binding-3", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + RescheduleTriggeredAt: &metav1.Time{Time: time.Now()}, + Placement: &policyv1alpha1.Placement{}, + }, + }, + expectSchedule: true, + expectError: false, + expectedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + expectedEvent: "Normal ScheduleBindingSucceed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := karmadafake.NewSimpleClientset(tt.binding) + fakeRecorder := record.NewFakeRecorder(10) + mockAlgorithm := &mockAlgorithm{ + scheduleFunc: func(context.Context, *workv1alpha2.ResourceBindingSpec, *workv1alpha2.ResourceBindingStatus, *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) { + return core.ScheduleResult{SuggestedClusters: tt.expectedClusters}, nil + }, + } + + s := &Scheduler{ + KarmadaClient: fakeClient, + clusterBindingLister: &fakeClusterBindingLister{binding: tt.binding}, + eventRecorder: fakeRecorder, + Algorithm: mockAlgorithm, + } + + err := s.doScheduleClusterBinding(tt.binding.Name) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + updatedBinding, err := fakeClient.WorkV1alpha2().ClusterResourceBindings().Get(context.TODO(), tt.binding.Name, metav1.GetOptions{}) + assert.NoError(t, err) + + if tt.expectSchedule { + assert.Equal(t, tt.expectedClusters, updatedBinding.Spec.Clusters) + assert.NotEqual(t, tt.binding.Spec.Clusters, updatedBinding.Spec.Clusters) + } else { + assert.Equal(t, tt.binding.Spec.Clusters, updatedBinding.Spec.Clusters) + } + + // Check for expected events + select { + case event := <-fakeRecorder.Events: + assert.Contains(t, event, tt.expectedEvent) + default: + t.Errorf("Expected an event to be recorded") + } + }) + } +} + +func TestScheduleResourceBindingWithClusterAffinity(t *testing.T) { + tests := []struct { + name string + binding *workv1alpha2.ResourceBinding + scheduleResult core.ScheduleResult + scheduleError error + expectError bool + expectedPatch string + expectedEvent string + }{ + { + name: "successful scheduling", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + }, + }, + scheduleResult: core.ScheduleResult{ + SuggestedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, + expectError: false, + expectedPatch: `{"metadata":{"annotations":{"policy.karmada.io/applied-placement":"{\"clusterAffinity\":{\"clusterNames\":[\"cluster1\"]}}"}},"spec":{"clusters":[{"name":"cluster1","replicas":1}]}}`, + expectedEvent: "Normal ScheduleBindingSucceed Binding has been scheduled successfully.", + }, + { + name: "scheduling error", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-error", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinity: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + }, + }, + scheduleResult: core.ScheduleResult{}, + scheduleError: errors.New("scheduling error"), + expectError: true, + expectedEvent: "Warning ScheduleBindingFailed scheduling error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := karmadafake.NewSimpleClientset(tt.binding) + fakeRecorder := record.NewFakeRecorder(10) + mockAlgorithm := &mockAlgorithm{ + scheduleFunc: func(context.Context, *workv1alpha2.ResourceBindingSpec, *workv1alpha2.ResourceBindingStatus, *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) { + return tt.scheduleResult, tt.scheduleError + }, + } + s := &Scheduler{ + KarmadaClient: fakeClient, + eventRecorder: fakeRecorder, + Algorithm: mockAlgorithm, + } + + err := s.scheduleResourceBindingWithClusterAffinity(tt.binding) + + if (err != nil) != tt.expectError { + t.Errorf("scheduleResourceBindingWithClusterAffinity() error = %v, expectError %v", err, tt.expectError) + } + + actions := fakeClient.Actions() + patchActions := filterPatchActions(actions) + + if tt.expectError { + assert.Empty(t, patchActions, "Expected no patch actions for error case") + } else { + assert.Len(t, patchActions, 1, "Expected one patch action") + if len(patchActions) > 0 { + actualPatch := string(patchActions[0].GetPatch()) + assert.JSONEq(t, tt.expectedPatch, actualPatch, "Patch does not match expected") + } + } + + // Check if an event was recorded + select { + case event := <-fakeRecorder.Events: + assert.Contains(t, event, tt.expectedEvent, "Event does not match expected") + default: + t.Errorf("Expected an event to be recorded") + } + }) + } +} + +func TestScheduleResourceBindingWithClusterAffinities(t *testing.T) { + tests := []struct { + name string + binding *workv1alpha2.ResourceBinding + scheduleResults []core.ScheduleResult + scheduleErrors []error + expectError bool + expectedPatches []string + expectedEvent string + }{ + { + name: "successful scheduling with first affinity", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{ + { + AffinityName: "affinity1", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + { + AffinityName: "affinity2", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster2"}, + }, + }, + }, + }, + }, + }, + scheduleResults: []core.ScheduleResult{ + { + SuggestedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, + }, + scheduleErrors: []error{nil}, + expectError: false, + expectedPatches: []string{ + `{"metadata":{"annotations":{"policy.karmada.io/applied-placement":"{\"clusterAffinities\":[{\"affinityName\":\"affinity1\",\"clusterNames\":[\"cluster1\"]},{\"affinityName\":\"affinity2\",\"clusterNames\":[\"cluster2\"]}]}"}},"spec":{"clusters":[{"name":"cluster1","replicas":1}]}}`, + `{"status":{"schedulerObservingAffinityName":"affinity1"}}`, + }, + expectedEvent: "Normal ScheduleBindingSucceed Binding has been scheduled successfully.", + }, + { + name: "successful scheduling with second affinity", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-2", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{ + { + AffinityName: "affinity1", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + { + AffinityName: "affinity2", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster2"}, + }, + }, + }, + }, + }, + }, + scheduleResults: []core.ScheduleResult{ + {}, + { + SuggestedClusters: []workv1alpha2.TargetCluster{ + {Name: "cluster2", Replicas: 1}, + }, + }, + }, + scheduleErrors: []error{errors.New("first affinity failed"), nil}, + expectError: false, + expectedPatches: []string{ + `{"metadata":{"annotations":{"policy.karmada.io/applied-placement":"{\"clusterAffinities\":[{\"affinityName\":\"affinity1\",\"clusterNames\":[\"cluster1\"]},{\"affinityName\":\"affinity2\",\"clusterNames\":[\"cluster2\"]}]}"}},"spec":{"clusters":[{"name":"cluster2","replicas":1}]}}`, + `{"status":{"schedulerObservingAffinityName":"affinity2"}}`, + }, + expectedEvent: "Warning ScheduleBindingFailed failed to schedule ResourceBinding(default/test-binding-2) with clusterAffiliates index(0): first affinity failed", + }, + { + name: "all affinities fail", + binding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding-fail", + Namespace: "default", + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Placement: &policyv1alpha1.Placement{ + ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{ + { + AffinityName: "affinity1", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + }, + { + AffinityName: "affinity2", + ClusterAffinity: policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster2"}, + }, + }, + }, + }, + }, + }, + scheduleResults: []core.ScheduleResult{{}, {}}, + scheduleErrors: []error{errors.New("first affinity failed"), errors.New("second affinity failed")}, + expectError: true, + expectedPatches: []string{}, + expectedEvent: "Warning ScheduleBindingFailed failed to schedule ResourceBinding", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := karmadafake.NewSimpleClientset(tt.binding) + fakeRecorder := record.NewFakeRecorder(10) + mockAlgorithm := &mockAlgorithm{ + scheduleFunc: func(_ context.Context, spec *workv1alpha2.ResourceBindingSpec, status *workv1alpha2.ResourceBindingStatus, _ *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) { + index := getAffinityIndex(spec.Placement.ClusterAffinities, status.SchedulerObservedAffinityName) + if index < len(tt.scheduleResults) { + return tt.scheduleResults[index], tt.scheduleErrors[index] + } + return core.ScheduleResult{}, errors.New("unexpected call to Schedule") + }, + } + s := &Scheduler{ + KarmadaClient: fakeClient, + eventRecorder: fakeRecorder, + Algorithm: mockAlgorithm, + } + + err := s.scheduleResourceBindingWithClusterAffinities(tt.binding) + + if (err != nil) != tt.expectError { + t.Errorf("scheduleResourceBindingWithClusterAffinities() error = %v, expectError %v", err, tt.expectError) + } + + actions := fakeClient.Actions() + patchActions := filterPatchActions(actions) + + if tt.expectError { + assert.Empty(t, patchActions, "Expected no patch actions for error case") + } else { + assert.Len(t, patchActions, len(tt.expectedPatches), "Expected %d patch actions", len(tt.expectedPatches)) + for i, expectedPatch := range tt.expectedPatches { + actualPatch := string(patchActions[i].GetPatch()) + assert.JSONEq(t, expectedPatch, actualPatch, "Patch %d does not match expected", i+1) + } + } + + // Check if an event was recorded + select { + case event := <-fakeRecorder.Events: + if strings.Contains(event, "ScheduleBindingFailed") { + assert.Contains(t, event, tt.expectedEvent, "Event does not match expected") + } else { + assert.Contains(t, event, "ScheduleBindingSucceed", "Expected ScheduleBindingSucceed event") + } + default: + t.Errorf("Expected an event to be recorded") + } + }) + } +} + +func TestPatchScheduleResultForResourceBinding(t *testing.T) { + tests := []struct { + name string + oldBinding *workv1alpha2.ResourceBinding + placement string + scheduleResult []workv1alpha2.TargetCluster + expectError bool + expectedBinding *workv1alpha2.ResourceBinding + }{ + { + name: "successful patch", + oldBinding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + }, + }, + placement: "test-placement", + scheduleResult: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + expectError: false, + expectedBinding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + Annotations: map[string]string{ + util.PolicyPlacementAnnotation: "test-placement", + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Clusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, + }, + }, + { + name: "no changes", + oldBinding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + Annotations: map[string]string{ + util.PolicyPlacementAnnotation: "test-placement", + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Clusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, + }, + placement: "test-placement", + scheduleResult: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + expectError: false, + expectedBinding: &workv1alpha2.ResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "default", + Annotations: map[string]string{ + util.PolicyPlacementAnnotation: "test-placement", + }, + }, + Spec: workv1alpha2.ResourceBindingSpec{ + Clusters: []workv1alpha2.TargetCluster{ + {Name: "cluster1", Replicas: 1}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Scheduler{ + KarmadaClient: karmadafake.NewSimpleClientset(tt.oldBinding), + } + + err := s.patchScheduleResultForResourceBinding(tt.oldBinding, tt.placement, tt.scheduleResult) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + updatedBinding, err := s.KarmadaClient.WorkV1alpha2().ResourceBindings(tt.oldBinding.Namespace).Get(context.TODO(), tt.oldBinding.Name, metav1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, tt.expectedBinding.Annotations, updatedBinding.Annotations) + assert.Equal(t, tt.expectedBinding.Spec.Clusters, updatedBinding.Spec.Clusters) + } + }) + } +} + func TestCreateScheduler(t *testing.T) { dynamicClient := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) karmadaClient := karmadafake.NewSimpleClientset() @@ -817,3 +1574,62 @@ func Test_targetClustersToString(t *testing.T) { }) } } + +// Helper Functions + +// Helper function to filter patch actions +func filterPatchActions(actions []clienttesting.Action) []clienttesting.PatchAction { + var patchActions []clienttesting.PatchAction + for _, action := range actions { + if patch, ok := action.(clienttesting.PatchAction); ok { + patchActions = append(patchActions, patch) + } + } + return patchActions +} + +// Mock Implementations + +type mockAlgorithm struct { + scheduleFunc func(context.Context, *workv1alpha2.ResourceBindingSpec, *workv1alpha2.ResourceBindingStatus, *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) +} + +func (m *mockAlgorithm) Schedule(ctx context.Context, spec *workv1alpha2.ResourceBindingSpec, status *workv1alpha2.ResourceBindingStatus, option *core.ScheduleAlgorithmOption) (core.ScheduleResult, error) { + return m.scheduleFunc(ctx, spec, status, option) +} + +type fakeBindingLister struct { + binding *workv1alpha2.ResourceBinding +} + +func (f *fakeBindingLister) List(_ labels.Selector) (ret []*workv1alpha2.ResourceBinding, err error) { + return []*workv1alpha2.ResourceBinding{f.binding}, nil +} + +func (f *fakeBindingLister) ResourceBindings(_ string) workv1alpha2lister.ResourceBindingNamespaceLister { + return &fakeBindingNamespaceLister{binding: f.binding} +} + +type fakeBindingNamespaceLister struct { + binding *workv1alpha2.ResourceBinding +} + +func (f *fakeBindingNamespaceLister) List(_ labels.Selector) (ret []*workv1alpha2.ResourceBinding, err error) { + return []*workv1alpha2.ResourceBinding{f.binding}, nil +} + +func (f *fakeBindingNamespaceLister) Get(_ string) (*workv1alpha2.ResourceBinding, error) { + return f.binding, nil +} + +type fakeClusterBindingLister struct { + binding *workv1alpha2.ClusterResourceBinding +} + +func (f *fakeClusterBindingLister) List(_ labels.Selector) (ret []*workv1alpha2.ClusterResourceBinding, err error) { + return []*workv1alpha2.ClusterResourceBinding{f.binding}, nil +} + +func (f *fakeClusterBindingLister) Get(_ string) (*workv1alpha2.ClusterResourceBinding, error) { + return f.binding, nil +}