From 9f82346b867b5fa793728b3e9a40e730eacd7ae2 Mon Sep 17 00:00:00 2001 From: tantawi Date: Fri, 18 Aug 2023 17:44:56 -0400 Subject: [PATCH] added quota tree unit tests 3 --- .../quota/core/quotatree_test.go | 134 ++++++++++++++ .../quota/core/treecache_test.go | 165 ++++++++++++++++++ .../quota-manager/quota/quotamanager_test.go | 74 ++++++++ 3 files changed, 373 insertions(+) create mode 100644 pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotatree_test.go create mode 100644 pkg/quotaplugins/quota-forest/quota-manager/quota/core/treecache_test.go diff --git a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotatree_test.go b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotatree_test.go new file mode 100644 index 00000000..756c59ec --- /dev/null +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotatree_test.go @@ -0,0 +1,134 @@ +/* +Copyright 2023 The Multi-Cluster App Dispatcher 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 core + +import ( + "testing" + "unsafe" + + tree "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/tree" + "github.com/stretchr/testify/assert" + "k8s.io/utils/strings/slices" +) + +// TestQuotaTreeAllocation : test quota tree allocation and de-allocation +func TestQuotaTreeAllocation(t *testing.T) { + // create a test quota tree + testTreeName := "test-tree" + resourceNames := []string{"CPU"} + testRootNode := createTestRootNode(t) + testQuotaTree := NewQuotaTree(testTreeName, testRootNode, resourceNames) + assert.NotNil(t, testQuotaTree, "Expecting non-nil quota tree") + + // create consumers + consumer1 := NewConsumer("C1", testTreeName, "B", &Allocation{x: []int{2}}, 0, 0, false) + assert.NotNil(t, consumer1, "Expecting non-nil consumer1") + consumer2 := NewConsumer("C2", testTreeName, "B", &Allocation{x: []int{1}}, 0, 0, false) + assert.NotNil(t, consumer2, "Expecting non-nil consumer2") + consumer3 := NewConsumer("C3", testTreeName, "C", &Allocation{x: []int{1}}, 0, 0, false) + assert.NotNil(t, consumer3, "Expecting non-nil consumer3") + consumer4P := NewConsumer("C4P", testTreeName, "B", &Allocation{x: []int{2}}, 1, 0, false) + assert.NotNil(t, consumer4P, "Expecting non-nil consumer4P") + consumerLarge := NewConsumer("CL", testTreeName, "C", &Allocation{x: []int{4}}, 0, 0, false) + assert.NotNil(t, consumerLarge, "Expecting non-nil consumerLarge") + + // allocate consumers + preemptedConsumers := &[]string{} + + // C1 -> A (does not fit on requested node B) + allocated1 := testQuotaTree.Allocate(consumer1, preemptedConsumers) + assert.True(t, allocated1, "Expecting consumer 1 to be allocated") + node1 := consumer1.GetNode().GetID() + assert.Equal(t, "A", node1, "Expecting consumer 1 to be allocated on node A") + + // C2 -> B + allocated2 := testQuotaTree.Allocate(consumer2, preemptedConsumers) + assert.True(t, allocated2, "Expecting consumer 2 to be allocated") + node2 := consumer2.GetNode().GetID() + assert.Equal(t, "B", node2, "Expecting consumer 2 to be allocated on node B") + + // C3 -> C (preempts C1 as C3 fits on its requested node C) + allocated3 := testQuotaTree.Allocate(consumer3, preemptedConsumers) + assert.True(t, allocated3, "Expecting consumer 3 to be allocated") + node3 := consumer3.GetNode().GetID() + assert.Equal(t, "C", node3, "Expecting consumer 3 to be allocated on node C") + consumer1Preempted := slices.Contains(*preemptedConsumers, "C1") + assert.True(t, consumer1Preempted, "Expecting consumer 1 to get preempted") + + // C4P -> A (high priority C4P preempts C2) + allocated4P := testQuotaTree.Allocate(consumer4P, preemptedConsumers) + assert.True(t, allocated4P, "Expecting consumer 4P to be allocated") + node4P := consumer4P.GetNode().GetID() + assert.Equal(t, "A", node4P, "Expecting consumer 4P to be allocated on node A") + consumer2Preempted := slices.Contains(*preemptedConsumers, "C2") + assert.True(t, consumer2Preempted, "Expecting consumer 2 to get preempted") + + // CL large consumer does not fit + allocatedLarge := testQuotaTree.Allocate(consumerLarge, preemptedConsumers) + assert.False(t, allocatedLarge, "Expecting large consumer not to be allocated") + + // CL -> C (large consumer allocated by force on specified node C, no preemptions) + forceAllocatedLarge := testQuotaTree.ForceAllocate(consumerLarge, "C") + assert.True(t, forceAllocatedLarge, "Expecting large consumer to be allocated by force") + nodeLarge := consumerLarge.GetNode().GetID() + assert.Equal(t, "C", nodeLarge, "Expecting large consumer to be force allocated on node C") + + // C3 de-allocated + quotaNode3 := consumer3.GetNode() + deallocated3 := testQuotaTree.DeAllocate(consumer3) + assert.True(t, deallocated3, "Expecting consumer 3 to be de-allocated") + node3x := consumer3.GetNode() + assert.Nil(t, node3x, "Expecting consumer 3 to have a nil node") + consumer3OnNode := consumerInList(consumer3, quotaNode3.GetConsumers()) + assert.False(t, consumer3OnNode, "Expecting consumer 3 to be removed from its allocated node") + + // C3 de-allocated again + deallocated3Again := testQuotaTree.DeAllocate(consumer3) + assert.False(t, deallocated3Again, "Expecting non-existing consumer 3 not to be de-allocated") +} + +// createTestRootNode : create a test quota node as a root for a tree +func createTestRootNode(t *testing.T) *QuotaNode { + + // create three quota nodes A[2], B[1], and C[1] + quotaNodeA, err := NewQuotaNode("A", &Allocation{x: []int{3}}) + assert.NoError(t, err, "No error expected when creating quota node A") + + quotaNodeB, err := NewQuotaNode("B", &Allocation{x: []int{1}}) + assert.NoError(t, err, "No error expected when creating quota node B") + + quotaNodeC, err := NewQuotaNode("C", &Allocation{x: []int{1}}) + assert.NoError(t, err, "No error expected when creating quota node C") + + // connect nodes: A -> ( B C ) + addedB := quotaNodeA.AddChild((*tree.Node)(unsafe.Pointer(quotaNodeB))) + assert.True(t, addedB, "Expecting node B added as child to node A") + addedC := quotaNodeA.AddChild((*tree.Node)(unsafe.Pointer(quotaNodeC))) + assert.True(t, addedC, "Expecting node C added as child to node A") + + return quotaNodeA +} + +func consumerInList(consumer *Consumer, list []*Consumer) bool { + consumerID := consumer.GetID() + for _, c := range list { + if c.GetID() == consumerID { + return true + } + } + return false +} diff --git a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/treecache_test.go b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/treecache_test.go new file mode 100644 index 00000000..b1f7c712 --- /dev/null +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/treecache_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The Multi-Cluster App Dispatcher 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 core + +import ( + "reflect" + "testing" + "unsafe" + + utils "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota/utils" + tree "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/tree" + "github.com/stretchr/testify/assert" +) + +var ( + nodeSpecA string = `{ + "parent": "nil", + "hard": "true", + "quota": { + "cpu": "10", + "memory": "256" + } + }` + + nodeSpecB string = `{ + "parent": "A", + "hard": "false", + "quota": { + "cpu": "2", + "memory": "64" + } + }` + + nodeSpecC string = `{ + "parent": "A", + "quota": { + "cpu": "4", + "memory": "128" + } + }` + + nodeSpecD string = `{ + "parent": "C", + "quota": { + "cpu": "2", + "memory": "64" + } + }` + + treeInfo string = `{ + "name": "TestTree", + "resourceNames": [ + "cpu", + "memory" + ] + }` + + nodesSpec string = `{ ` + + `"A": ` + nodeSpecA + + `, "B": ` + nodeSpecB + + `, "C": ` + nodeSpecC + + `, "D": ` + nodeSpecD + + ` }` +) + +// TestTreeCacheNames : test tree cache operations on tree and resource names +func TestTreeCacheNames(t *testing.T) { + // create a tree cache + treeCache := NewTreeCache() + assert.NotNil(t, treeCache, "Expecting non-nil tree cache") + + // set tree name + treeCache.SetDefaultTreeName() + assert.Equal(t, utils.DefaultTreeName, treeCache.GetTreeName(), "Expecting default tree name") + + treeCache.clearTreeName() + assert.Equal(t, "", treeCache.GetTreeName(), "Expecting tree name to be cleared") + + treeName := "test-tree" + treeCache.SetTreeName(treeName) + assert.Equal(t, treeName, treeCache.GetTreeName(), "Expecting tree name to be set") + + // set resource names + treeCache.AddResourceName("cpu") + treeCache.AddResourceNames([]string{"memory", "storage"}) + treeCache.DeleteResourceName("storage") + finalNames := []string{"cpu", "memory"} + resourceNames := treeCache.GetResourceNames() + assert.ElementsMatch(t, finalNames, resourceNames, "Expecting correct resource names") + numResources := treeCache.GetNumResourceNames() + assert.Equal(t, len(finalNames), numResources, "Expecting matching number of resource names") + + treeCache.clearResourceNames() + resourceNames = treeCache.GetResourceNames() + assert.ElementsMatch(t, []string{}, resourceNames, "Expecting empty resource names") + numResources = treeCache.GetNumResourceNames() + assert.Equal(t, 0, numResources, "Expecting empty resource names") + + treeCache.SetDefaultResourceNames() + resourceNames = treeCache.GetResourceNames() + finalNames = utils.DefaultResourceNames + assert.ElementsMatch(t, finalNames, resourceNames, "Expecting default resource names") + numResources = treeCache.GetNumResourceNames() + assert.Equal(t, len(finalNames), numResources, "Expecting number of default resource names") +} + +// TestTreeCacheNodes : test tree cache operations on nodes +func TestTreeCacheNodes(t *testing.T) { + // create a tree cache + treeCache := NewTreeCache() + assert.NotNil(t, treeCache, "Expecting non-nil tree cache") + + // set tree info + err := treeCache.AddTreeInfoFromString(treeInfo) + assert.NoError(t, err, "Expecting no error parsing tree info string") + wantTreeName := "TestTree" + assert.Equal(t, wantTreeName, treeCache.GetTreeName(), "Expecting tree name to be set") + resourceNames := treeCache.GetResourceNames() + wantResourceNames := []string{"memory", "cpu"} + assert.ElementsMatch(t, wantResourceNames, resourceNames, "Expecting correct resource names") + + // add node specs + err = treeCache.AddNodeSpecsFromString(nodesSpec) + assert.NoError(t, err, "Expecting no error parsing nodes spec string") + quotaTree, response := treeCache.CreateTree() + testTree := createTestTree() + equalTrees := reflect.DeepEqual(quotaTree, testTree) + assert.True(t, equalTrees, + "Expecting created tree to be same as input tree, want %v, got %v", testTree, quotaTree) + assert.True(t, response.IsClean(), "Expecting clean response from tree cache") + assert.ElementsMatch(t, []string{}, response.DanglingNodeNames, "Expecting no dangling nodes") + +} + +// createTestTree : create a test quota tree +func createTestTree() *QuotaTree { + // create quota nodes + quotaNodeA, _ := NewQuotaNodeHard("A", &Allocation{x: []int{10, 256}}, true) + quotaNodeB, _ := NewQuotaNodeHard("B", &Allocation{x: []int{2, 64}}, false) + quotaNodeC, _ := NewQuotaNodeHard("C", &Allocation{x: []int{4, 128}}, false) + quotaNodeD, _ := NewQuotaNodeHard("D", &Allocation{x: []int{2, 64}}, false) + // connect nodes: A -> ( B C ( D ) ) + quotaNodeA.AddChild((*tree.Node)(unsafe.Pointer(quotaNodeB))) + quotaNodeA.AddChild((*tree.Node)(unsafe.Pointer(quotaNodeC))) + quotaNodeC.AddChild((*tree.Node)(unsafe.Pointer(quotaNodeD))) + // make quota tree + treeName := "TestTree" + resourceNames := []string{"cpu", "memory"} + quotaTree := NewQuotaTree(treeName, quotaNodeA, resourceNames) + return quotaTree +} diff --git a/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go b/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go index 5e4813f1..a711b7fa 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go @@ -27,6 +27,7 @@ import ( "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota" "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota/utils" "github.com/stretchr/testify/assert" + "k8s.io/utils/strings/slices" ) // TestNewQuotaManagerConsumerAllocationRelease function emulates multiple threads adding quota consumers and removing them @@ -493,6 +494,7 @@ var ( // TestAddDeleteTrees : test adding, retrieving, deleting trees func TestAddDeleteTrees(t *testing.T) { + // create a test quota manager qmTest := quota.NewManager() assert.NotNil(t, qmTest, "Expecting no error creating a quota manager") modeSet := qmTest.SetMode(quota.Normal) @@ -502,6 +504,7 @@ func TestAddDeleteTrees(t *testing.T) { modeString := qmTest.GetModeString() match := strings.Contains(modeString, "Normal") assert.True(t, match, "Expecting mode set to normal") + // add a few trees by name treeNames := []string{"tree-1", "tree-2", "tree-3"} treeStrings := []string{tree1String, tree2String, tree3String} @@ -510,17 +513,21 @@ func TestAddDeleteTrees(t *testing.T) { assert.NoError(t, err, "No error expected when adding a tree") assert.Equal(t, treeNames[i], name, "Returned name should match") } + // get list of names names := qmTest.GetTreeNames() assert.ElementsMatch(t, treeNames, names, "Expecting retrieved tree names same as added names") + // delete a name deletedTreeName := treeNames[0] remainingTreeNames := treeNames[1:] err := qmTest.DeleteTree(deletedTreeName) assert.NoError(t, err, "No error expected when deleting an existing tree") + // get list of names after deletion names = qmTest.GetTreeNames() assert.ElementsMatch(t, remainingTreeNames, names, "Expecting retrieved tree names to reflect additions/deletions") + // delete a non-existing name err = qmTest.DeleteTree(deletedTreeName) assert.Error(t, err, "Error expected when deleting a non-existing tree") @@ -529,6 +536,8 @@ func TestAddDeleteTrees(t *testing.T) { // TestAddDeleteForests : test adding, retrieving, deleting forests func TestAddDeleteForests(t *testing.T) { var err error + + // create a test quota manager qmTest := quota.NewManager() assert.NotNil(t, qmTest, "Expecting no error creating a quota manager") modeSet := qmTest.SetMode(quota.Normal) @@ -542,6 +551,7 @@ func TestAddDeleteForests(t *testing.T) { assert.NoError(t, err, "No error expected when adding a tree") assert.Equal(t, treeNames[i], name, "Returned name should match") } + // create two forests forestNames := []string{"forest-1", "forest-2"} for _, forestName := range forestNames { @@ -555,9 +565,11 @@ func TestAddDeleteForests(t *testing.T) { assert.NoError(t, err, "No error expected when adding a tree to a forest") err = qmTest.AddTreeToForest("forest-2", "tree-3") assert.NoError(t, err, "No error expected when adding a tree to a forest") + // get list of forest names fNames := qmTest.GetForestNames() assert.ElementsMatch(t, forestNames, fNames, "Expecting retrieved forest names same as added names") + // get forests map forestTreeMap := qmTest.GetForestTreeNames() for _, v := range forestTreeMap { @@ -566,28 +578,90 @@ func TestAddDeleteForests(t *testing.T) { inputForestTreeMap := map[string][]string{"forest-1": {"tree-1"}, "forest-2": {"tree-2", "tree-3"}} assert.True(t, reflect.DeepEqual(forestTreeMap, inputForestTreeMap), "Expecting retrieved forest tree map same as input, got %v, want %v", forestTreeMap, inputForestTreeMap) + // delete a forest deletedForestName := forestNames[0] remainingForestNames := forestNames[1:] err = qmTest.DeleteForest(deletedForestName) assert.NoError(t, err, "No error expected when deleting an existing forest") + // get list of forest names after deletion fNames = qmTest.GetForestNames() assert.ElementsMatch(t, remainingForestNames, fNames, "Expecting retrieved forest names to reflect additions/deletions") + // delete a non-existing forest name err = qmTest.DeleteForest(deletedForestName) assert.Error(t, err, "Error expected when deleting a non-existing forest") + // delete a tree from a forest err = qmTest.DeleteTreeFromForest("forest-2", "tree-2") assert.NoError(t, err, "No error expected when deleting an existing tree from an existing forest") err = qmTest.DeleteTreeFromForest("forest-2", "tree-2") assert.Error(t, err, "Error expected when deleting an non-existing tree from an existing forest") + // check remaining trees after deletions names := qmTest.GetTreeNames() assert.True(t, reflect.DeepEqual(treeNames, names), "Expecting all trees after forest deletions as trees are not deleted, got %v, want %v", names, treeNames) } +var ( + consumerInfoString1 string = `{ + "kind": "Consumer", + "metadata": { + "name": "consumer-info" + }, + "spec": { + "id": "consumer-1", + "trees": [ + { + "treeName": "test-tree", + "groupID": "D", + "request": { + "cpu": 4, + "memory": 16 + } + } + ] + } + }` +) + +// TestAddRemoveConsumers : test adding and removing consumers +func TestAddRemoveConsumers(t *testing.T) { + // create a test quota manager + qmTest := quota.NewManager() + assert.NotNil(t, qmTest, "Expecting no error creating a quota manager") + modeSet := qmTest.SetMode(quota.Normal) + assert.True(t, modeSet, "Expecting no error setting mode to normal") + + // create consumer info + consumerInfo1, err := quota.NewConsumerInfoFromString(consumerInfoString1) + assert.NotNil(t, consumerInfo1, "Expecting a valid consumer info object") + assert.NoError(t, err, "No error expected when creating a consumer info") + consumerID := consumerInfo1.GetID() + assert.Equal(t, "consumer-1", consumerID, "Expecting consumer ID in consumer info to match ID in spec string") + + // add consumer + added, err := qmTest.AddConsumer(consumerInfo1) + assert.True(t, added && err == nil, "Expecting consumer to be added to quota manager") + addedAgain, _ := qmTest.AddConsumer(consumerInfo1) + assert.False(t, addedAgain, "Expecting an existing consumer not to be added to quota manager") + consumerIDs := qmTest.GetAllConsumerIDs() + consumerExists := slices.Contains(consumerIDs, consumerID) + assert.True(t, consumerExists, "Expecting added consumer to be in list") + + // remove consumer + consumerRemoved, err := qmTest.RemoveConsumer(consumerID) + assert.True(t, consumerRemoved && err == nil, "Expecting existing consumer to be removed") + consumerIDsAfterRemoval := qmTest.GetAllConsumerIDs() + consumerExistsAfterRemoval := slices.Contains(consumerIDsAfterRemoval, consumerID) + assert.False(t, consumerExistsAfterRemoval, "Expecting removed consumer not to be in list") + consumerRemovedAgain, err := qmTest.RemoveConsumer(consumerID) + assert.False(t, consumerRemovedAgain, "Expecting non-existing consumer not to be removed") + assert.Error(t, err, "Expecting error when removing a non-existing consumer") +} + type AllocationClassifier struct { }