From 39d3ef3b11ace2eb85d08a9ad7f598ca21cce49a Mon Sep 17 00:00:00 2001 From: Anuj Agrawal Date: Sat, 5 Oct 2024 21:11:32 +0530 Subject: [PATCH] Added tests for metrics and framework package of pkg/scheduler Signed-off-by: Anuj Agrawal --- pkg/scheduler/framework/interface_test.go | 260 ++++++++++++++++++++++ pkg/scheduler/framework/types_test.go | 183 +++++++++++++++ pkg/scheduler/metrics/metrics_test.go | 102 +++++++++ 3 files changed, 545 insertions(+) create mode 100644 pkg/scheduler/framework/interface_test.go create mode 100644 pkg/scheduler/framework/types_test.go create mode 100644 pkg/scheduler/metrics/metrics_test.go diff --git a/pkg/scheduler/framework/interface_test.go b/pkg/scheduler/framework/interface_test.go new file mode 100644 index 000000000000..6c4837568c0e --- /dev/null +++ b/pkg/scheduler/framework/interface_test.go @@ -0,0 +1,260 @@ +/* +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 framework + +import ( + "errors" + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPluginToResult_Merge(t *testing.T) { + tests := []struct { + name string + results PluginToResult + want *Result + }{ + { + name: "empty results", + results: PluginToResult{}, + want: nil, + }, + { + name: "all success results", + results: PluginToResult{ + "plugin1": NewResult(Success), + "plugin2": NewResult(Success), + }, + want: NewResult(Success), + }, + { + name: "mixed results with unschedulable", + results: PluginToResult{ + "plugin1": NewResult(Success), + "plugin2": NewResult(Unschedulable, "reason1"), + "plugin3": NewResult(Success), + }, + want: NewResult(Unschedulable, "reason1"), + }, + { + name: "mixed results with error", + results: PluginToResult{ + "plugin1": NewResult(Success), + "plugin2": NewResult(Unschedulable, "reason1"), + "plugin3": NewResult(Error, "error occurred"), + }, + want: NewResult(Error, "reason1", "error occurred"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.results.Merge() + if tt.want == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.Equal(t, tt.want.code, got.code) + + // Sort the reasons before comparing + sortedWantReasons := make([]string, len(tt.want.reasons)) + copy(sortedWantReasons, tt.want.reasons) + sort.Strings(sortedWantReasons) + + sortedGotReasons := make([]string, len(got.reasons)) + copy(sortedGotReasons, got.reasons) + sort.Strings(sortedGotReasons) + + assert.Equal(t, sortedWantReasons, sortedGotReasons) + + if tt.want.err != nil { + assert.Error(t, got.err) + } else { + assert.NoError(t, got.err) + } + } + }) + } +} + +func TestResult_IsSuccess(t *testing.T) { + tests := []struct { + name string + result *Result + want bool + }{ + { + name: "nil result", + result: nil, + want: true, + }, + { + name: "success result", + result: NewResult(Success), + want: true, + }, + { + name: "unschedulable result", + result: NewResult(Unschedulable), + want: false, + }, + { + name: "error result", + result: NewResult(Error), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.result.IsSuccess()) + }) + } +} + +func TestResult_AsError(t *testing.T) { + tests := []struct { + name string + result *Result + wantErr bool + errorMsg string + }{ + { + name: "success result", + result: NewResult(Success), + wantErr: false, + }, + { + name: "unschedulable result", + result: NewResult(Unschedulable, "reason1", "reason2"), + wantErr: true, + errorMsg: "reason1, reason2", + }, + { + name: "error result", + result: NewResult(Error, "error occurred"), + wantErr: true, + errorMsg: "error occurred", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.result.AsError() + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errorMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestResult_AsResult(t *testing.T) { + tests := []struct { + name string + err error + wantCode Code + wantReasons []string + }{ + { + name: "non-nil error", + err: errors.New("test error"), + wantCode: Error, + wantReasons: []string{"test error"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := AsResult(tt.err) + assert.Equal(t, tt.wantCode, got.code) + assert.Equal(t, tt.wantReasons, got.reasons) + assert.Equal(t, tt.err, got.err) + }) + } +} + +func TestResult_Code(t *testing.T) { + tests := []struct { + name string + result *Result + want Code + }{ + { + name: "nil result", + result: nil, + want: Success, + }, + { + name: "success result", + result: NewResult(Success), + want: Success, + }, + { + name: "unschedulable result", + result: NewResult(Unschedulable), + want: Unschedulable, + }, + { + name: "error result", + result: NewResult(Error), + want: Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.result.Code() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCode_String(t *testing.T) { + tests := []struct { + name string + code Code + want string + }{ + { + name: "Success code", + code: Success, + want: "Success", + }, + { + name: "Unschedulable code", + code: Unschedulable, + want: "Unschedulable", + }, + { + name: "Error code", + code: Error, + want: "Error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.code.String() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/scheduler/framework/types_test.go b/pkg/scheduler/framework/types_test.go new file mode 100644 index 000000000000..10f2ddb4c1b5 --- /dev/null +++ b/pkg/scheduler/framework/types_test.go @@ -0,0 +1,183 @@ +/* +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 framework + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" +) + +func TestNewClusterInfo(t *testing.T) { + testCases := []struct { + name string + cluster *clusterv1alpha1.Cluster + want *ClusterInfo + }{ + { + name: "Create ClusterInfo with valid cluster", + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + }, + want: &ClusterInfo{ + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + }, + }, + }, + { + name: "Create ClusterInfo with nil cluster", + cluster: nil, + want: &ClusterInfo{cluster: nil}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := NewClusterInfo(tc.cluster) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestClusterInfo_Cluster(t *testing.T) { + testCases := []struct { + name string + clusterInfo *ClusterInfo + want *clusterv1alpha1.Cluster + }{ + { + name: "Get cluster from valid ClusterInfo", + clusterInfo: &ClusterInfo{ + cluster: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + }, + }, + want: &clusterv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + }, + }, + { + name: "Get cluster from nil ClusterInfo", + clusterInfo: nil, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.clusterInfo.Cluster() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestFitError_Error(t *testing.T) { + testCases := []struct { + name string + fitError FitError + expectedOutput string + expectedReasons []string + }{ + { + name: "No clusters available", + fitError: FitError{ + NumAllClusters: 0, + Diagnosis: Diagnosis{ClusterToResultMap: ClusterToResultMap{}}, + }, + expectedOutput: "0/0 clusters are available: no cluster exists.", + expectedReasons: []string{}, + }, + { + name: "Multiple reasons for unavailability", + fitError: FitError{ + NumAllClusters: 3, + Diagnosis: Diagnosis{ + ClusterToResultMap: ClusterToResultMap{ + "cluster1": &Result{reasons: []string{"insufficient CPU", "insufficient memory"}}, + "cluster2": &Result{reasons: []string{"insufficient CPU"}}, + "cluster3": &Result{reasons: []string{"taint mismatch"}}, + }, + }, + }, + expectedOutput: "0/3 clusters are available:", + expectedReasons: []string{ + "2 insufficient CPU", + "1 insufficient memory", + "1 taint mismatch", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.fitError.Error() + + // Check if the error message starts with the expected output + assert.True(t, strings.HasPrefix(got, tc.expectedOutput), "Error message should start with expected output") + + if len(tc.expectedReasons) > 0 { + // Check each reason + for _, reason := range tc.expectedReasons { + assert.Contains(t, got, reason, "Error message should contain the reason: %s", reason) + } + + // Check the total number of reasons + gotReasons := strings.Split(strings.TrimPrefix(got, tc.expectedOutput), ",") + assert.Equal(t, len(tc.expectedReasons), len(gotReasons), "Number of reasons should match") + } else { + // If no reasons are expected, the got message should exactly match the expected output + assert.Equal(t, tc.expectedOutput, got, "Error message should exactly match expected output when no reasons are provided") + } + }) + } +} + +func TestUnschedulableError_Error(t *testing.T) { + testCases := []struct { + name string + message string + }{ + { + name: "Unschedulable due to insufficient resources", + message: "Insufficient CPU in all clusters", + }, + { + name: "Unschedulable due to taint mismatch", + message: "No cluster matches required tolerations", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + unschedulableErr := UnschedulableError{Message: tc.message} + assert.Equal(t, tc.message, unschedulableErr.Error()) + }) + } +} diff --git a/pkg/scheduler/metrics/metrics_test.go b/pkg/scheduler/metrics/metrics_test.go new file mode 100644 index 000000000000..2872539b046c --- /dev/null +++ b/pkg/scheduler/metrics/metrics_test.go @@ -0,0 +1,102 @@ +/* +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 metrics + +import ( + "errors" + "testing" + "time" +) + +func TestBindingSchedule(t *testing.T) { + tests := []struct { + name string + scheduleType string + duration float64 + err error + }{ + { + name: "Successful schedule", + scheduleType: "test", + duration: 1.5, + err: nil, + }, + { + name: "Failed schedule", + scheduleType: "test", + duration: 0.5, + err: errors.New("schedule failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + // We can't easily test the metric values directly, so we'll just ensure the function doesn't panic + BindingSchedule(tt.scheduleType, tt.duration, tt.err) + }) + } +} + +func TestScheduleStep(t *testing.T) { + tests := []struct { + name string + action string + duration time.Duration + }{ + { + name: "Filter step", + action: ScheduleStepFilter, + duration: 100 * time.Millisecond, + }, + { + name: "Score step", + action: ScheduleStepScore, + duration: 200 * time.Millisecond, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + startTime := time.Now().Add(-tt.duration) + // Ensure the function doesn't panic + ScheduleStep(tt.action, startTime) + }) + } +} + +func TestCountSchedulerBindings(t *testing.T) { + tests := []struct { + name string + event string + }{ + { + name: "Binding add event", + event: BindingAdd, + }, + { + name: "Binding update event", + event: BindingUpdate, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + // Ensure the function doesn't panic + CountSchedulerBindings(tt.event) + }) + } +}