test: Add test case for createNodeOp

Signed-off-by: utam0k <k0ma@utam0k.jp>
This commit is contained in:
utam0k
2025-05-05 21:51:22 +09:00
parent 43bb11b5c1
commit 57d55fbb10
2 changed files with 423 additions and 0 deletions

View File

@@ -0,0 +1,418 @@
/*
Copyright 2025 The Kubernetes 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 benchmark
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes/fake"
testutils "k8s.io/kubernetes/test/utils"
ktesting "k8s.io/kubernetes/test/utils/ktesting"
"k8s.io/utils/ptr"
)
type verifyFunc func(t *testing.T, tCtx ktesting.TContext, op realOp) error
func TestRunOp(t *testing.T) {
tests := []struct {
name string
op realOp
expectedFailure bool
verifyFuncs []verifyFunc
}{
{
name: "Create Single Node",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 1,
},
verifyFuncs: []verifyFunc{
verifyCount(1),
verifyObj(
&v1.Node{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "node-0-",
},
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
Allocatable: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
},
}),
},
},
{
name: "Create Multiple Nodes",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 5,
},
verifyFuncs: []verifyFunc{
verifyCount(5),
verifyObj(
&v1.Node{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "node-0-",
},
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
Allocatable: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
},
}),
},
},
{
name: "Create Nodes with Label Strategy",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 3,
LabelNodePrepareStrategy: testutils.NewLabelNodePrepareStrategy("test-label", "value1", "value2", "value3"),
},
verifyFuncs: []verifyFunc{
verifyCount(3),
verifyLabelValuesAllowed("test-label", sets.New("value1", "value2", "value3")),
},
},
{
name: "Create Nodes with Unique Label Strategy",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 2,
UniqueNodeLabelStrategy: testutils.NewUniqueNodeLabelStrategy("unique-test-label"),
},
verifyFuncs: []verifyFunc{
verifyCount(2),
verifyUniqueLabelValues("unique-test-label"),
},
},
{
name: "Create Nodes with Node Allocatable Strategy",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 2,
NodeAllocatableStrategy: testutils.NewNodeAllocatableStrategy(
map[v1.ResourceName]string{
v1.ResourceCPU: "2",
v1.ResourceMemory: "4Gi",
},
nil, // no CSI node allocatable
nil, // no migrated plugins
),
},
verifyFuncs: []verifyFunc{
verifyCount(2),
verifyObj(
&v1.Node{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "node-0-",
},
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
Allocatable: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("2"),
v1.ResourceMemory: resource.MustParse("4Gi"),
v1.ResourcePods: resource.MustParse("110"),
},
},
}),
},
},
{
name: "Create Nodes with Custom Template",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 2,
NodeTemplatePath: createObjTemplateFile(t,
&v1.Node{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "custom-node-",
},
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourcePods: resource.MustParse("100"),
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("8Gi"),
},
},
},
),
},
verifyFuncs: []verifyFunc{
verifyCount(2),
verifyObj(
&v1.Node{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "custom-node-",
},
Status: v1.NodeStatus{
Capacity: v1.ResourceList{
v1.ResourcePods: resource.MustParse("100"),
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("8Gi"),
},
},
},
),
},
},
{
name: "Invalid Node Template Path",
op: &createNodesOp{
Opcode: createNodesOpcode,
Count: 1,
NodeTemplatePath: ptr.To("non-existent-file.json"),
},
expectedFailure: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, tCtx := ktesting.NewTestContext(t)
client := fake.NewSimpleClientset()
tCtx = ktesting.WithClients(tCtx, nil, nil, client, nil, nil)
exec := &WorkloadExecutor{
tCtx: tCtx,
numPodsScheduledPerNamespace: make(map[string]int),
nextNodeIndex: 0,
}
err := exec.runOp(tt.op, 0)
if tt.expectedFailure {
if err == nil {
t.Fatalf("Expected error but got none")
}
return
}
if err != nil {
t.Fatalf("Failed to run operation: %v", err)
}
if tt.verifyFuncs != nil {
for i, vf := range tt.verifyFuncs {
if err := vf(t, tCtx, tt.op); err != nil {
t.Fatalf("Verification function %d failed for test %q: %v", i, tt.name, err)
}
}
}
})
}
}
// verifyCount returns a verification function that checks if the number of existing objects
// matches the expected count based on the operation type.
func verifyCount(expectedCount int) verifyFunc {
return func(t *testing.T, tCtx ktesting.TContext, op realOp) error {
switch op.(type) {
case *createNodesOp:
nodes, err := tCtx.Client().CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list nodes: %w", err)
}
if got := len(nodes.Items); got != expectedCount {
return fmt.Errorf("unexpected node count: got %d, want %d", got, expectedCount)
}
default:
return fmt.Errorf("verifyCount doesn't support this operation type: %T", op)
}
return nil
}
}
// verifyLabelValuesAllowed returns a verification function that checks if the label values for a given key.
func verifyLabelValuesAllowed(key string, allowValues sets.Set[string]) verifyFunc {
return func(t *testing.T, tCtx ktesting.TContext, op realOp) error {
labelValues, _, err := objLabelValues(t, tCtx, op, key)
if err != nil {
return fmt.Errorf("failed to get label values: %w", err)
}
for labelValue := range labelValues {
if !allowValues.Has(labelValue) {
return fmt.Errorf("Node has unexpected label value %s for key %s", labelValue, key)
}
}
return nil
}
}
// verifyUniqueLabelValues returns a verification function that checks if the label values for a given key are unique.
func verifyUniqueLabelValues(key string) verifyFunc {
return func(t *testing.T, tCtx ktesting.TContext, op realOp) error {
_, duplicatedValues, err := objLabelValues(t, tCtx, op, key)
if err != nil {
return fmt.Errorf("failed to get label values: %w", err)
}
if duplicatedValues.Len() > 0 {
return fmt.Errorf("Node has duplicate label values %v for key %s", duplicatedValues.UnsortedList(), key)
}
return nil
}
}
// objLabelValues is a helper function to extract label values from the listed objects.
// The listed objects are dependent on the operation type.
// It returns two sets: one with deduplicated labelValues and second with duplicated labels.
func objLabelValues(t *testing.T, tCtx ktesting.TContext, op realOp, key string) (sets.Set[string], sets.Set[string], error) {
t.Helper()
labelValues := sets.New[string]()
duplicatedValues := sets.New[string]()
switch op.(type) {
case *createNodesOp:
nodes, err := tCtx.Client().CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to list nodes for label verification: %w", err)
}
for _, node := range nodes.Items {
if labelValue, exists := node.Labels[key]; exists {
if labelValues.Has(labelValue) {
duplicatedValues.Insert(labelValue)
}
labelValues.Insert(labelValue)
} else {
return nil, nil, fmt.Errorf("Node %s is missing expected label %s", node.Name, key)
}
}
default:
return nil, nil, fmt.Errorf("verifyLabel doesn't support this operation type: %T", op)
}
return labelValues, duplicatedValues, nil
}
// verifyObj checks if listed objects match the expected template object using cmp.Diff.
func verifyObj(expectedObj any) verifyFunc {
return func(t *testing.T, tCtx ktesting.TContext, op realOp) error {
var got, want any
var cmpOpts []cmp.Option
switch opDetails := op.(type) {
case *createNodesOp:
nodesList, listErr := tCtx.Client().CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if listErr != nil {
return fmt.Errorf("failed to list nodes: %w", listErr)
}
gotNodes := nodesList.Items
expectedNodeTemplate, ok := expectedObj.(*v1.Node)
if !ok {
return fmt.Errorf("expectedObj must be *v1.Node when op is *createNodesOp, got %T", expectedObj)
}
wantNodes := make([]v1.Node, len(gotNodes))
// we don't need to verify len(), we just need to check if all of them are the same as the expected one.
for i := range gotNodes {
wantNodes[i] = *expectedNodeTemplate
}
cmpOpts = []cmp.Option{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(metav1.ObjectMeta{},
"UID", "ResourceVersion", "Generation", "CreationTimestamp", "ManagedFields", "SelfLink", "Name",
"Labels", // verifyObj doesn't care about labels.
),
cmpopts.IgnoreFields(v1.NodeStatus{}, // This test isn't interested in these fields.
"Conditions",
"Phase",
),
}
got = gotNodes
want = wantNodes
default:
return fmt.Errorf("verifyObj doesn't support this operation type for cmp.Diff: %T", opDetails)
}
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
return fmt.Errorf("unexpected difference (-want +got):\\n%s", diff)
}
return nil
}
}
// createObjTemplateFile creates a temporary file with the given object serialized as JSON.
func createObjTemplateFile(t *testing.T, obj any) *string {
t.Helper()
dir, err := os.MkdirTemp("", "scheduler-perf-test")
if err != nil {
t.Fatalf("Failed to create temp dir for the template: %v", err)
}
t.Cleanup(func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Failed to remove temp dir: %v", err)
}
})
templateFile := filepath.Join(dir, "template.json")
f, err := os.Create(templateFile)
if err != nil {
t.Fatalf("Failed to create the template file %s: %v", templateFile, err)
}
defer func() {
if err := f.Close(); err != nil {
t.Errorf("Failed to close file: %v", err)
}
}()
switch obj := obj.(type) {
case *v1.Node:
if err := json.NewEncoder(f).Encode(obj); err != nil {
t.Fatalf("Failed to encode the template to %s: %v", templateFile, err)
}
default:
t.Fatalf("Unsupported object type for template file: %T", obj)
}
return &templateFile
}

View File

@@ -241,6 +241,11 @@ func makeBaseNode(nodeNamePrefix string) *v1.Node {
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
},
Allocatable: v1.ResourceList{
v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI),
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("32Gi"),
},
Phase: v1.NodeRunning,
Conditions: []v1.NodeCondition{
{Type: v1.NodeReady, Status: v1.ConditionTrue},