From 709f76373503b76be9c231c166e3e5bc6595f428 Mon Sep 17 00:00:00 2001 From: tantawi Date: Thu, 17 Aug 2023 13:02:41 -0400 Subject: [PATCH 1/4] added quota tree unit tests 1 --- .../quota/core/allocation_test.go | 205 ++- .../quota/core/quotanode_test.go | 1228 ++++++++++++++++- .../quota-manager/tree/node_test.go | 38 + .../quota-manager/tree/tree_test.go | 99 +- 4 files changed, 1497 insertions(+), 73 deletions(-) diff --git a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/allocation_test.go b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/allocation_test.go index 339b59c4f..8ac03269e 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/allocation_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/allocation_test.go @@ -185,9 +185,76 @@ func TestAllocation_SetValue(t *testing.T) { } } +func TestAllocation_Add(t *testing.T) { + type fields struct { + value1 []int + value2 []int + } + type args struct { + other *Allocation + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test1", + fields: fields{ + value1: []int{1, 2}, + value2: []int{4, 6}, + }, + args: args{ + other: &Allocation{ + x: []int{3, 4}, + }, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + value1: []int{1, 2}, + value2: []int{1, 2}, + }, + args: args{ + other: &Allocation{ + x: []int{3, 4, 5}, + }, + }, + want: false, + }, + { + name: "test3", + fields: fields{ + value1: []int{1, 2}, + value2: []int{4, 9}, + }, + args: args{ + other: &Allocation{ + x: []int{3, 4}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Allocation{ + x: tt.fields.value1, + } + if got := a.Add(tt.args.other) && reflect.DeepEqual(a.x, tt.fields.value2); got != tt.want { + t.Errorf("Allocation.Add() = %v, want %v; result = %v, want %v", + got, tt.want, a.x, tt.fields.value2) + } + }) + } +} + func TestAllocation_Fit(t *testing.T) { type fields struct { - x []int + value []int } type args struct { allocated *Allocation @@ -201,7 +268,7 @@ func TestAllocation_Fit(t *testing.T) { }{ {name: "test1", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ allocated: &Allocation{ @@ -215,7 +282,7 @@ func TestAllocation_Fit(t *testing.T) { }, {name: "test2", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ allocated: &Allocation{ @@ -229,7 +296,7 @@ func TestAllocation_Fit(t *testing.T) { }, {name: "test3", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ allocated: &Allocation{ @@ -243,7 +310,7 @@ func TestAllocation_Fit(t *testing.T) { }, {name: "test4", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ allocated: &Allocation{ @@ -257,7 +324,7 @@ func TestAllocation_Fit(t *testing.T) { }, {name: "test5", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ allocated: &Allocation{ @@ -273,7 +340,7 @@ func TestAllocation_Fit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Allocation{ - x: tt.fields.x, + x: tt.fields.value, } if got := a.Fit(tt.args.allocated, tt.args.capacity); got != tt.want { t.Errorf("Allocation.Fit() = %v, want %v", got, tt.want) @@ -282,9 +349,125 @@ func TestAllocation_Fit(t *testing.T) { } } +func TestAllocation_IsZero(t *testing.T) { + type fields struct { + value []int + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "test1", + fields: fields{ + value: []int{0, 0, 0}, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + value: []int{0, 1}, + }, + want: false, + }, + { + name: "test3", + fields: fields{ + value: []int{1, 2, -4}, + }, + want: false, + }, + { + name: "test4", + fields: fields{ + value: []int{}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Allocation{ + x: tt.fields.value, + } + if got := a.IsZero(); got != tt.want { + t.Errorf("Allocation.IsZero() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAllocation_Equal(t *testing.T) { + type fields struct { + value []int + } + type args struct { + other *Allocation + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test1", + fields: fields{ + value: []int{1, 2, 3}, + }, + args: args{ + other: &Allocation{x: []int{1, 2, 3}}, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + value: []int{}, + }, + args: args{ + other: &Allocation{x: []int{}}, + }, + want: true, + }, + { + name: "test3", + fields: fields{ + value: []int{1, 2, 3}, + }, + args: args{ + other: &Allocation{x: []int{4, 5, 6}}, + }, + want: false, + }, + { + name: "test4", + fields: fields{ + value: []int{1, 2}, + }, + args: args{ + other: &Allocation{x: []int{1, 2, 3}}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Allocation{ + x: tt.fields.value, + } + if got := a.Equal(tt.args.other); got != tt.want { + t.Errorf("Allocation.Equal() = %v, want %v", got, tt.want) + } + }) + } +} + func TestAllocation_StringPretty(t *testing.T) { type fields struct { - x []int + value []int } type args struct { resourceNames []string @@ -298,7 +481,7 @@ func TestAllocation_StringPretty(t *testing.T) { { name: "test1", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ resourceNames: []string{"cpu", "memory", "gpu"}, @@ -308,7 +491,7 @@ func TestAllocation_StringPretty(t *testing.T) { { name: "test2", fields: fields{ - x: []int{1, 2, 3}, + value: []int{1, 2, 3}, }, args: args{ resourceNames: []string{"cpu", "memory"}, @@ -320,7 +503,7 @@ func TestAllocation_StringPretty(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &Allocation{ - x: tt.fields.x, + x: tt.fields.value, } if got := a.StringPretty(tt.args.resourceNames); got != tt.want { t.Errorf("Allocation.StringPretty() = %v, want %v", got, tt.want) diff --git a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotanode_test.go b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotanode_test.go index 1dfce6ebd..f32af46f5 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotanode_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/core/quotanode_test.go @@ -5,7 +5,7 @@ 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 + 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, @@ -18,23 +18,57 @@ package core import ( "reflect" "testing" + "unsafe" tree "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/tree" ) var ( - nodeA *tree.Node = tree.NewNode("A") - alloc1 Allocation = Allocation{ - x: []int{5, 10, 20}, + quotaNodeA QuotaNode + quotaNodeB QuotaNode + quotaNodeC QuotaNode + quotaNodeD QuotaNode + + consumer1 *Consumer = &Consumer{ + id: "C1", + treeID: "T1", + groupID: "B", + request: &Allocation{x: []int{1, 2}}, + priority: 0, + cType: 0, + unPreemptable: false, + aNode: nil, } - quotaNodeA QuotaNode = QuotaNode{ - Node: *nodeA, - quota: &alloc1, - isHard: false, - allocated: &Allocation{ - x: make([]int, len(alloc1.x)), - }, - consumers: make([]*Consumer, 0), + + consumer2 *Consumer = &Consumer{ + id: "C2", + treeID: "T1", + groupID: "D", + request: &Allocation{x: []int{3, 4}}, + priority: 0, + cType: 1, + unPreemptable: false, + aNode: nil, + } + + consumer3 *Consumer = &Consumer{ + id: "C3", + treeID: "T1", + groupID: "B", + request: &Allocation{x: []int{5, 6}}, + priority: 0, + cType: 0, + unPreemptable: false, + aNode: nil, + } + + nodeT *tree.Node = tree.NewNode("T") + quotaNodeT QuotaNode = QuotaNode{ + Node: *nodeT, + quota: &Allocation{x: []int{}}, + isHard: false, + allocated: &Allocation{x: []int{}}, + consumers: []*Consumer{}, } ) @@ -43,24 +77,39 @@ func TestNewQuotaNode(t *testing.T) { id string quota *Allocation } + type data struct { + quota *Allocation + alloc *Allocation + } tests := []struct { name string args args + data data want *QuotaNode wantErr bool }{ {name: "testA", args: args{ id: "A", - quota: &alloc1, + quota: &Allocation{x: []int{10, 20}}, + }, + data: data{ + quota: &Allocation{x: []int{10, 20}}, + alloc: &Allocation{x: []int{0, 0}}, + }, + want: &QuotaNode{ + Node: *tree.NewNode("A"), + quota: &Allocation{x: []int{10, 20}}, + isHard: false, + allocated: &Allocation{x: []int{0, 0}}, + consumers: []*Consumer{}, }, - want: "aNodeA, wantErr: false, }, {name: "test empty ID", args: args{ id: "", - quota: &alloc1, + quota: &Allocation{x: []int{10, 20}}, }, want: nil, wantErr: true, @@ -87,35 +136,1162 @@ func TestNewQuotaNode(t *testing.T) { } } -func TestQuotaNode_String(t *testing.T) { +func TestNewQuotaNodeHard(t *testing.T) { + type args struct { + id string + quota *Allocation + isHard bool + } + type data struct { + quota *Allocation + alloc *Allocation + } + tests := []struct { + name string + args args + data data + want *QuotaNode + wantErr bool + }{ + {name: "test1", + args: args{ + id: "A", + quota: &Allocation{x: []int{10, 20}}, + isHard: false, + }, + data: data{ + quota: &Allocation{x: []int{10, 20}}, + alloc: &Allocation{x: []int{0, 0}}, + }, + want: &QuotaNode{ + Node: *tree.NewNode("A"), + quota: &Allocation{x: []int{10, 20}}, + isHard: false, + allocated: &Allocation{x: []int{0, 0}}, + consumers: []*Consumer{}, + }, + wantErr: false, + }, + {name: "test2", + args: args{ + id: "A", + quota: &Allocation{x: []int{10, 20}}, + isHard: true, + }, + data: data{ + quota: &Allocation{x: []int{10, 20}}, + alloc: &Allocation{x: []int{0, 0}}, + }, + want: &QuotaNode{ + Node: *tree.NewNode("A"), + quota: &Allocation{x: []int{10, 20}}, + isHard: true, + allocated: &Allocation{x: []int{0, 0}}, + consumers: []*Consumer{}, + }, + wantErr: false, + }, + {name: "test3", + args: args{ + id: "A", + quota: nil, + isHard: true, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewQuotaNodeHard(tt.args.id, tt.args.quota, tt.args.isHard) + if (err != nil) != tt.wantErr { + t.Errorf("NewQuotaNodeHard() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewQuotaNodeHard() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuotaNode_CanFit(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + type args struct { + c *Consumer + } + type results struct { + alloc *Allocation + } + tests := []struct { + name string + fields fields + args args + want bool + results results + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + quota: &Allocation{x: []int{10, 20}}, + allocated: &Allocation{x: []int{5, 8}}, + }, + args: args{ + c: &Consumer{ + id: "c1", + request: &Allocation{x: []int{3, 2}}, + }, + }, + want: true, + results: results{ + alloc: &Allocation{x: []int{5, 8}}, + }, + }, + { + name: "test2", + fields: fields{ + Node: *nodeT, + quota: &Allocation{x: []int{10, 20}}, + allocated: &Allocation{x: []int{5, 8}}, + }, + args: args{ + c: &Consumer{ + id: "c1", + request: &Allocation{x: []int{10, 10}}, + }, + }, + want: false, + results: results{ + alloc: &Allocation{x: []int{5, 8}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.CanFit(tt.args.c); got != tt.want { + t.Errorf("QuotaNode.CanFit() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(qn.allocated, tt.results.alloc) { + t.Errorf("QuotaNode.CanFit(): quota = %v, want %v", qn.allocated, tt.results.alloc) + } + }) + } +} + +func TestQuotaNode_AddRequest(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + type args struct { + c *Consumer + } + type results struct { + r *Allocation + } + tests := []struct { + name string + fields fields + args args + results results + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + allocated: &Allocation{x: []int{1, 2}}, + }, + args: args{ + c: &Consumer{id: "C1", + request: &Allocation{x: []int{1, 0}}}, + }, + results: results{ + &Allocation{x: []int{2, 2}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + qn.AddRequest(tt.args.c) + if !reflect.DeepEqual(qn.allocated, tt.results.r) { + t.Errorf("QuotaNode.AddRequest(): allocated = %v, want %v", qn.allocated, tt.results.r) + } + }) + } +} + +func TestQuotaNode_SubtractRequest(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + type args struct { + c *Consumer + } + type results struct { + r *Allocation + } + tests := []struct { + name string + fields fields + args args + results results + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + allocated: &Allocation{x: []int{1, 2}}, + }, + args: args{ + c: &Consumer{id: "C1", + request: &Allocation{x: []int{1, 0}}}, + }, + results: results{ + &Allocation{x: []int{0, 2}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + qn.SubtractRequest(tt.args.c) + if !reflect.DeepEqual(qn.allocated, tt.results.r) { + t.Errorf("QuotaNode.SubtractRequest(): allocated = %v, want %v", qn.allocated, tt.results.r) + } + }) + } +} + +func TestQuotaNode_AddConsumer(t *testing.T) { type fields struct { - quotaNode QuotaNode + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer } type args struct { - level int + c *Consumer } tests := []struct { name string fields fields args args - want string + want bool }{ - {name: "testA", + { + name: "test1", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{}, + }, + args: args{c: consumer1}, + want: true, + }, + { + name: "test2", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{consumer1}, + }, + args: args{c: consumer2}, + want: true, + }, + { + name: "test3", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{consumer1}, + }, + args: args{c: consumer1}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.AddConsumer(tt.args.c); got != tt.want { + t.Errorf("QuotaNode.AddConsumer() = %v, want %v", got, tt.want) + } + if !containsConsumer(qn.consumers, tt.args.c) { + t.Errorf("QuotaNode.AddConsumer(): consumer %v not added to consumers = %v", + tt.args.c, qn.consumers) + } + }) + } +} + +func TestQuotaNode_RemoveConsumer(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + type args struct { + c *Consumer + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{}, + }, + args: args{ + c: consumer1, + }, + want: false, + }, + { + name: "test2", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{consumer1, consumer2}, + }, + args: args{ + c: consumer1, + }, + want: true, + }, + { + name: "test3", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{consumer1}, + }, + args: args{ + c: consumer2, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.RemoveConsumer(tt.args.c); got != tt.want { + t.Errorf("QuotaNode.RemoveConsumer() = %v, want %v", got, tt.want) + } + if containsConsumer(qn.consumers, tt.args.c) { + t.Errorf("QuotaNode.removeConsumer(): consumer %v not removed from consumers = %v", + tt.args.c, qn.consumers) + } + }) + } +} + +func TestQuotaNode_Allocate(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + type args struct { + c *Consumer + } + type results struct { + alloc *Allocation + } + tests := []struct { + name string + fields fields + args args + results results + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + quota: &Allocation{x: []int{10, 20}}, + allocated: &Allocation{x: []int{5, 8}}, + consumers: []*Consumer{}, + }, + args: args{ + c: &Consumer{ + id: "c1", + request: &Allocation{x: []int{3, 2}}, + }, + }, + results: results{ + alloc: &Allocation{x: []int{8, 10}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + qn.Allocate(tt.args.c) + if !reflect.DeepEqual(qn.allocated, tt.results.alloc) { + t.Errorf("QuotaNode.Allocate(): allocated = %v, want %v", qn.allocated, tt.results.alloc) + } + if !containsConsumer(qn.consumers, tt.args.c) { + t.Errorf("QuotaNode.Allocate(): consumer %v not added to consumers = %v", + tt.args.c, qn.consumers) + } + if tt.args.c.aNode != qn { + t.Errorf("QuotaNode.Allocate(): consumer aNode %v not set to QuotaNode %v", + tt.args.c.aNode, qn) + } + }) + } +} + +func TestQuotaNode_SlideDown(t *testing.T) { + type fields struct { + qn *QuotaNode + } + type data struct { + quota *Allocation + allocated *Allocation + parent *QuotaNode + parentQuota *Allocation + parentAlloc *Allocation + parentConsumers []*Consumer + } + type results struct { + parentAlloc *Allocation + nodeAlloc *Allocation + } + tests := []struct { + name string + fields fields + data data + results results + }{ + { + name: "test1", + fields: fields{ + qn: "aNodeB, + }, + data: data{ + quota: &Allocation{x: []int{2, 2}}, + allocated: &Allocation{x: []int{0, 0}}, + parent: "aNodeA, + parentQuota: &Allocation{x: []int{10, 20}}, + parentAlloc: &Allocation{x: []int{0, 0}}, + parentConsumers: []*Consumer{consumer1, consumer2, consumer3}, + }, + results: results{ + parentAlloc: &Allocation{x: []int{9, 12}}, + nodeAlloc: &Allocation{x: []int{1, 2}}, + }, + }, + { + name: "test2", + fields: fields{ + qn: "aNodeA, + }, + data: data{ + quota: &Allocation{x: []int{5, 5}}, + allocated: &Allocation{x: []int{1, 2}}, + parent: nil, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{1, 2}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + connectTree() + if tt.data.parent != nil { + p := tt.data.parent + p.quota = tt.data.parentQuota + p.allocated = tt.data.parentAlloc + for _, c := range tt.data.parentConsumers { + p.Allocate(c) + } + } + qn := tt.fields.qn + qn.quota = tt.data.quota + qn.allocated = tt.data.allocated + qn.SlideDown() + if !reflect.DeepEqual(qn.allocated, tt.results.nodeAlloc) { + t.Errorf("QuotaNode.SlideDown(): node allocated = %v, want %v", qn.allocated, tt.results.nodeAlloc) + } + if tt.data.parent != nil { + if !reflect.DeepEqual(tt.data.parent.allocated, tt.results.parentAlloc) { + t.Errorf("QuotaNode.SlideDown(): parent allocated = %v, want %v", tt.data.parent.allocated, tt.results.parentAlloc) + } + } + }) + } +} + +func TestQuotaNode_SlideUp(t *testing.T) { + type fields struct { + qn *QuotaNode + } + type args struct { + c *Consumer + applyPriority bool + allocationRecovery *AllocationRecovery + preemptedConsumers *[]string + } + type data struct { + nodeQuota *Allocation + nodeAlloc *Allocation // before adding consumers to node + nodeConsumers []*Consumer // consumerd to be added to node + parent *QuotaNode + parentQuota *Allocation + parentAlloc *Allocation // before adding consumers to parent node + parentConsumers []*Consumer // consumerd to be added to parent node + } + type results struct { + nodeAlloc *Allocation + nodeConsumers []*Consumer + parentAlloc *Allocation + parentConsumers []*Consumer + preemptedConsumers []string + } + tests := []struct { + name string + fields fields + args args + data data + results results + want bool + }{ + { + name: "test1", + fields: fields{ + qn: "aNodeB, + }, + args: args{ + c: consumer3, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer3, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{5, 6}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: "aNodeA, + parentQuota: &Allocation{x: []int{10, 10}}, + parentAlloc: &Allocation{x: []int{0, 0}}, + parentConsumers: []*Consumer{}, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{}, + parentAlloc: &Allocation{x: []int{1, 2}}, + parentConsumers: []*Consumer{consumer1}, + preemptedConsumers: []string{}, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + qn: "aNodeB, + }, + args: args{ + c: consumer3, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer3, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{4, 4}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: "aNodeA, + parentQuota: &Allocation{x: []int{10, 10}}, + parentAlloc: &Allocation{x: []int{0, 0}}, + parentConsumers: []*Consumer{}, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{1, 2}}, + nodeConsumers: []*Consumer{consumer1}, + parentAlloc: &Allocation{x: []int{1, 2}}, + parentConsumers: []*Consumer{}, + preemptedConsumers: []string{}, + }, + want: false, + }, + { + name: "test3", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: consumer3, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer3, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{5, 6}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: nil, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{}, + preemptedConsumers: []string{consumer1.id}, + }, + want: true, + }, + { + name: "test4", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: consumer3, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer3, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{4, 4}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: nil, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{1, 2}}, + nodeConsumers: []*Consumer{consumer1}, + preemptedConsumers: []string{}, + }, + want: false, + }, + { + name: "test5", + fields: fields{ + qn: "aNodeC, + }, + args: args{ + c: consumer3, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer3, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{5, 6}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: "aNodeA, + parentQuota: &Allocation{x: []int{10, 10}}, + parentAlloc: &Allocation{x: []int{0, 0}}, + parentConsumers: []*Consumer{}, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{1, 2}}, + nodeConsumers: []*Consumer{consumer1}, + parentAlloc: &Allocation{x: []int{1, 2}}, + parentConsumers: []*Consumer{}, + preemptedConsumers: []string{}, + }, + want: false, + }, + { + name: "test6", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: consumer2, + applyPriority: false, + allocationRecovery: &AllocationRecovery{ + consumer: consumer2, + alteredNodes: []*QuotaNode{}, + alteredConsumers: make(map[string]*Consumer), + originalConsumersNode: map[string]*QuotaNode{}, + }, + preemptedConsumers: &[]string{}, + }, + data: data{ + nodeQuota: &Allocation{x: []int{3, 4}}, + nodeAlloc: &Allocation{x: []int{0, 0}}, + nodeConsumers: []*Consumer{consumer1}, + parent: nil, + }, + results: results{ + nodeAlloc: &Allocation{x: []int{1, 2}}, + nodeConsumers: []*Consumer{consumer1}, + preemptedConsumers: []string{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + connectTree() + if tt.data.parent != nil { + p := tt.data.parent + p.quota = tt.data.parentQuota + p.allocated = tt.data.parentAlloc + for _, c := range tt.data.parentConsumers { + p.Allocate(c) + } + for _, c := range tt.data.nodeConsumers { + p.AddRequest(c) + } + } + qn := tt.fields.qn + qn.quota = tt.data.nodeQuota + qn.allocated = tt.data.nodeAlloc + for _, c := range tt.data.nodeConsumers { + qn.Allocate(c) + } + if got := qn.SlideUp(tt.args.c, tt.args.applyPriority, tt.args.allocationRecovery, tt.args.preemptedConsumers); got != tt.want { + t.Errorf("QuotaNode.SlideUp() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(qn.allocated, tt.results.nodeAlloc) { + t.Errorf("QuotaNode.SlideUp(): node allocated = %v, want %v", qn.allocated, tt.results.nodeAlloc) + } + if !containsAllConsumers(qn.consumers, tt.results.nodeConsumers) { + t.Errorf("QuotaNode.SlideUp(): node consumers = %v, want %v", qn.consumers, tt.results.nodeConsumers) + } + if tt.data.parent != nil { + if !reflect.DeepEqual(tt.data.parent.allocated, tt.results.parentAlloc) { + t.Errorf("QuotaNode.SlideUp(): parent allocated = %v, want %v", tt.data.parent.allocated, tt.results.parentAlloc) + } + if !containsAllConsumers(tt.data.parent.consumers, tt.results.parentConsumers) { + t.Errorf("QuotaNode.SlideUp(): parent consumers = %v, want %v", tt.data.parent.consumers, tt.results.parentConsumers) + } + } + if !sameAllStrings(*tt.args.preemptedConsumers, tt.results.preemptedConsumers) { + t.Errorf("QuotaNode.SlideUp(): preemptedConsumers = %v, want %v", *tt.args.preemptedConsumers, tt.results.preemptedConsumers) + } + }) + } +} + +func TestQuotaNode_HasLeaf(t *testing.T) { + type fields struct { + qn *QuotaNode + } + type args struct { + c *Consumer + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test1", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: &Consumer{ + id: "c1", + groupID: "B", + }, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: &Consumer{ + id: "c1", + groupID: "C", + }, + }, + want: false, + }, + { + name: "test1", + fields: fields{ + qn: "aNodeA, + }, + args: args{ + c: &Consumer{ + id: "c1", + groupID: "D", + }, + }, + want: true, + }, + } + connectTree() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := tt.fields.qn + if got := qn.HasLeaf(tt.args.c); got != tt.want { + t.Errorf("QuotaNode.HasLeaf() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuotaNode_IsHard(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + isHard: true, + }, + want: true, + }, + { + name: "test2", + fields: fields{ + Node: *nodeT, + isHard: false, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.IsHard(); got != tt.want { + t.Errorf("QuotaNode.IsHard() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuotaNode_GetQuota(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + tests := []struct { + name string + fields fields + want *Allocation + }{ + { + name: "test1", fields: fields{ - quotaNode: quotaNodeA, + Node: *nodeT, + quota: &Allocation{x: []int{1, 2, 3}}, + }, + want: &Allocation{x: []int{1, 2, 3}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.GetQuota(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("QuotaNode.GetQuota() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuotaNode_SetQuota(t *testing.T) { + type fields struct { + qn *QuotaNode + } + type args struct { + quota *Allocation + } + type results struct { + r *Allocation + } + tests := []struct { + name string + fields fields + args args + results results + }{ + { + name: "test1", + fields: fields{ + qn: "aNodeT, }, args: args{ - level: 1, + quota: &Allocation{x: []int{10, 20}}, + }, + results: results{ + r: &Allocation{x: []int{10, 20}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := tt.fields.qn + qn.SetQuota(tt.args.quota) + if !reflect.DeepEqual(qn.quota, tt.results.r) { + t.Errorf("QuotaNode.SetQuota(): quota = %v, want %v", qn.quota, tt.results.r) + } + }) + } +} + +func TestQuotaNode_GetAllocated(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + tests := []struct { + name string + fields fields + want *Allocation + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + allocated: &Allocation{x: []int{1, 2, 3}}, + }, + want: &Allocation{x: []int{1, 2, 3}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.GetAllocated(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("QuotaNode.GetAllocated() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuotaNode_GetConsumers(t *testing.T) { + type fields struct { + Node tree.Node + quota *Allocation + isHard bool + allocated *Allocation + consumers []*Consumer + } + tests := []struct { + name string + fields fields + want []*Consumer + }{ + { + name: "test1", + fields: fields{ + Node: *nodeT, + consumers: []*Consumer{consumer1, consumer2}, }, - want: "--|A: isHard=false; quota=[5 10 20]; allocated=[0 0 0]; consumers={ }\n", + want: []*Consumer{consumer1, consumer2}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - qn := &tt.fields.quotaNode - if got := qn.String(tt.args.level); got != tt.want { - t.Errorf("QuotaNode.String() = %s, want %s", got, tt.want) + qn := &QuotaNode{ + Node: tt.fields.Node, + quota: tt.fields.quota, + isHard: tt.fields.isHard, + allocated: tt.fields.allocated, + consumers: tt.fields.consumers, + } + if got := qn.GetConsumers(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("QuotaNode.GetConsumers() = %v, want %v", got, tt.want) } }) } } + +func containsConsumer(elems []*Consumer, v *Consumer) bool { + for _, s := range elems { + if v == s { + return true + } + } + return false +} + +func containsAllConsumers(elems []*Consumer, vs []*Consumer) bool { + for _, v := range vs { + if !containsConsumer(elems, v) { + return false + } + } + return true +} + +func containsString(elems []string, v string) bool { + for _, s := range elems { + if v == s { + return true + } + } + return false +} + +func sameAllStrings(elems []string, vs []string) bool { + if len(elems) != len(vs) { + return false + } + for _, v := range vs { + if !containsString(elems, v) { + return false + } + } + return true +} + +func connectTree() { + clearTree() + //A -> ( B C -> ( D ) ) + nodeA := (*tree.Node)(unsafe.Pointer("aNodeA)) + nodeB := (*tree.Node)(unsafe.Pointer("aNodeB)) + nodeC := (*tree.Node)(unsafe.Pointer("aNodeC)) + nodeD := (*tree.Node)(unsafe.Pointer("aNodeD)) + + nodeA.AddChild(nodeB) + nodeA.AddChild(nodeC) + nodeC.AddChild(nodeD) +} + +func clearTree() { + quotaNodeA = QuotaNode{ + Node: *tree.NewNode("A"), + quota: &Allocation{x: []int{}}, + isHard: false, + allocated: &Allocation{x: []int{}}, + consumers: []*Consumer{}, + } + + quotaNodeB = QuotaNode{ + Node: *tree.NewNode("B"), + quota: &Allocation{x: []int{}}, + isHard: false, + allocated: &Allocation{x: []int{}}, + consumers: []*Consumer{}, + } + + quotaNodeC = QuotaNode{ + Node: *tree.NewNode("C"), + quota: &Allocation{x: []int{}}, + isHard: true, + allocated: &Allocation{x: []int{}}, + consumers: []*Consumer{}, + } + + quotaNodeD = QuotaNode{ + Node: *tree.NewNode("D"), + quota: &Allocation{x: []int{}}, + isHard: false, + allocated: &Allocation{x: []int{}}, + consumers: []*Consumer{}, + } +} diff --git a/pkg/quotaplugins/quota-forest/quota-manager/tree/node_test.go b/pkg/quotaplugins/quota-forest/quota-manager/tree/node_test.go index 06c54e3a3..dc831fe1f 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/tree/node_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/tree/node_test.go @@ -188,6 +188,44 @@ func TestNode_GetChildren(t *testing.T) { }) } } + +func TestNode_HasLeaf(t *testing.T) { + type args struct { + leafID string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "leaf-child-B", + args: args{leafID: "B"}, + want: true, + }, + { + name: "leaf-D", + args: args{leafID: "D"}, + want: true, + }, + { + name: "leaf-X", + args: args{leafID: "X"}, + want: false, + }, + } + resetNodes() + connectNodes() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := nodeA + if got := n.HasLeaf(tt.args.leafID); got != tt.want { + t.Errorf("Node.HasLeaf() = %v, want %v", got, tt.want) + } + }) + } +} + func TestNode_AddChild(t *testing.T) { type fields struct { node *Node diff --git a/pkg/quotaplugins/quota-forest/quota-manager/tree/tree_test.go b/pkg/quotaplugins/quota-forest/quota-manager/tree/tree_test.go index e333b4fe8..e5a180a3a 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/tree/tree_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/tree/tree_test.go @@ -70,6 +70,69 @@ func TestNewTree(t *testing.T) { } } +func TestTree_GetHeight(t *testing.T) { + type fields struct { + tree *Tree + } + tests := []struct { + name string + fields fields + want int + }{ + { + name: "good tree", + fields: fields{ + tree: treeA, + }, + want: 2, + }, + { + name: "empty tree", + fields: fields{ + tree: treeNil, + }, + want: 0, + }, + } + resetNodes() + connectNodes() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := tt.fields.tree + if got := tr.GetHeight(); got != tt.want { + t.Errorf("Tree.GetHeight() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTree_GetRoot(t *testing.T) { + type fields struct { + tree *Tree + } + tests := []struct { + name string + fields fields + want *Node + }{ + { + name: "success", + fields: fields{ + tree: treeA, + }, + want: nodeA, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := tt.fields.tree + if got := tr.GetRoot(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Tree.GetRoot() = %v, want %v", got, tt.want) + } + }) + } +} + func TestTree_GetLeafIDs(t *testing.T) { type fields struct { tree *Tree @@ -246,42 +309,6 @@ func TestTree_GetNode(t *testing.T) { } } -func TestTree_GetHeight(t *testing.T) { - type fields struct { - tree *Tree - } - tests := []struct { - name string - fields fields - want int - }{ - { - name: "good tree", - fields: fields{ - tree: treeA, - }, - want: 2, - }, - { - name: "empty tree", - fields: fields{ - tree: treeNil, - }, - want: 0, - }, - } - resetNodes() - connectNodes() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := tt.fields.tree - if got := tr.GetHeight(); got != tt.want { - t.Errorf("Tree.GetHeight() = %v, want %v", got, tt.want) - } - }) - } -} - func TestTree_GetNodeIDs(t *testing.T) { type fields struct { tree *Tree From 42e8cc845a0e7a8b632d6f047ec651df11e2af29 Mon Sep 17 00:00:00 2001 From: tantawi Date: Thu, 17 Aug 2023 17:48:05 -0400 Subject: [PATCH 2/4] added quota tree unit tests 2 --- .../demos/manager/forest/demo.go | 5 +- .../quota-manager/demos/manager/tree/demo.go | 5 +- .../quota-forest/quota-manager/main.go | 5 +- .../quota-manager/quota/quotamanager_test.go | 207 ++++++++++++++++++ 4 files changed, 213 insertions(+), 9 deletions(-) diff --git a/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/forest/demo.go b/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/forest/demo.go index 62ef22d7f..fb68b49e3 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/forest/demo.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/forest/demo.go @@ -5,7 +5,7 @@ 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 + 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, @@ -18,7 +18,6 @@ package main import ( "flag" "fmt" - "io/ioutil" "os" "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota" @@ -52,7 +51,7 @@ func main() { for _, treeName := range treeNames { fName := prefix + treeName + ".json" fmt.Printf("Tree file name: %s\n", fName) - jsonTree, err := ioutil.ReadFile(fName) + jsonTree, err := os.ReadFile(fName) if err != nil { fmt.Printf("error reading quota tree file: %s", fName) return diff --git a/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/tree/demo.go b/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/tree/demo.go index 6d5bf6452..f57793d4d 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/tree/demo.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/demos/manager/tree/demo.go @@ -5,7 +5,7 @@ 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 + 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, @@ -18,7 +18,6 @@ package main import ( "flag" "fmt" - "io/ioutil" "os" "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota" @@ -52,7 +51,7 @@ func main() { fmt.Println(quotaManager.GetModeString()) // add a quota tree from file - jsonTree, err := ioutil.ReadFile(treeFileName) + jsonTree, err := os.ReadFile(treeFileName) if err != nil { fmt.Printf("error reading quota tree file: %s", treeFileName) return diff --git a/pkg/quotaplugins/quota-forest/quota-manager/main.go b/pkg/quotaplugins/quota-forest/quota-manager/main.go index 63ad97066..3523c90ac 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/main.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/main.go @@ -5,7 +5,7 @@ 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 + 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, @@ -18,7 +18,6 @@ package main import ( "flag" "fmt" - "io/ioutil" "os" "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/quotaplugins/quota-forest/quota-manager/quota" @@ -40,7 +39,7 @@ func main() { fmt.Println("==> Creating Quota Manager") fmt.Println("**************************") quotaManager := quota.NewManager() - treeJsonString, err := ioutil.ReadFile(treeFileName) + treeJsonString, err := os.ReadFile(treeFileName) if err != nil { fmt.Printf("error reading quota tree file: %s", treeFileName) return 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 0a22255ce..5e4813f1d 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go @@ -17,6 +17,8 @@ limitations under the License. package quota_test import ( + "reflect" + "sort" "strings" "testing" "time" @@ -381,6 +383,211 @@ func TestQuotaManagerQuotaUsedLongRunningConsumers(t *testing.T) { } +var ( + tree1String string = `{ + "kind": "QuotaTree", + "metadata": { + "name": "tree-1" + }, + "spec": { + "resourceNames": [ + "cpu", + "memory" + ], + "nodes": { + "A": { + "parent": "nil", + "hard": "true", + "quota": { + "cpu": "10", + "memory": "256" + } + }, + "B": { + "parent": "A", + "hard": "true", + "quota": { + "cpu": "2", + "memory": "64" + } + }, + "C": { + "parent": "A", + "quota": { + "cpu": "6", + "memory": "64" + } + } + } + } +}` + + tree2String string = `{ + "kind": "QuotaTree", + "metadata": { + "name": "tree-2" + }, + "spec": { + "resourceNames": [ + "gpu" + ], + "nodes": { + "X": { + "parent": "nil", + "hard": "true", + "quota": { + "gpu": "32" + } + }, + "Y": { + "parent": "X", + "hard": "true", + "quota": { + "gpu": "16" + } + }, + "Z": { + "parent": "X", + "quota": { + "gpu": "8" + } + } + } + } +}` + + tree3String string = `{ + "kind": "QuotaTree", + "metadata": { + "name": "tree-3" + }, + "spec": { + "resourceNames": [ + "count" + ], + "nodes": { + "zone": { + "parent": "nil", + "hard": "true", + "quota": { + "count": "100" + } + }, + "rack": { + "parent": "zone", + "hard": "true", + "quota": { + "count": "100" + } + }, + "server": { + "parent": "rack", + "quota": { + "count": "100" + } + } + } + } +}` +) + +// TestAddDeleteTrees : test adding, retrieving, deleting trees +func TestAddDeleteTrees(t *testing.T) { + 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") + mode := qmTest.GetMode() + assert.True(t, mode == quota.Normal, "Expecting mode set to normal") + 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} + for i, treeString := range treeStrings { + name, err := qmTest.AddTreeFromString(treeString) + 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") +} + +// TestAddDeleteForests : test adding, retrieving, deleting forests +func TestAddDeleteForests(t *testing.T) { + var err error + 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") + + // add a few trees by name + treeNames := []string{"tree-1", "tree-2", "tree-3"} + treeStrings := []string{tree1String, tree2String, tree3String} + for i, treeString := range treeStrings { + name, err := qmTest.AddTreeFromString(treeString) + 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 { + err = qmTest.AddForest(forestName) + assert.NoError(t, err, "No error expected when adding a forest") + } + // assign trees to forests + err = qmTest.AddTreeToForest("forest-1", "tree-1") + assert.NoError(t, err, "No error expected when adding a tree to a forest") + err = qmTest.AddTreeToForest("forest-2", "tree-2") + 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 { + sort.Strings(v) + } + 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) +} + type AllocationClassifier struct { } From f86a03a8f8a9b298e58ee2558a8819dd70e5e64d Mon Sep 17 00:00:00 2001 From: tantawi Date: Fri, 18 Aug 2023 17:44:56 -0400 Subject: [PATCH 3/4] 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 000000000..756c59ecf --- /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 000000000..b1f7c7122 --- /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 5e4813f1d..a711b7fa9 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 { } From 1301530bb50e1b1c91990a41c39b373130315c8f Mon Sep 17 00:00:00 2001 From: tantawi Date: Thu, 7 Sep 2023 10:28:14 -0400 Subject: [PATCH 4/4] added quota tree unit tests 4 --- .../quota-manager/quota/quotamanager_test.go | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) 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 a711b7fa9..a45e4e753 100644 --- a/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go +++ b/pkg/quotaplugins/quota-forest/quota-manager/quota/quotamanager_test.go @@ -25,6 +25,7 @@ import ( "github.com/eapache/go-resiliency/retrier" "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/core" "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" @@ -662,6 +663,158 @@ func TestAddRemoveConsumers(t *testing.T) { assert.Error(t, err, "Expecting error when removing a non-existing consumer") } +// TestPreemptSamePriorityAtRoot : test preemption of same priority borrowing consumer at root +func TestPreemptSamePriorityAtRoot(t *testing.T) { + + var treeString string = `{ + "kind": "QuotaTree", + "metadata": { + "name": "tree" + }, + "spec": { + "resourceNames": [ + "gpu" + ], + "nodes": { + "T": { + "parent": "nil", + "hard": "true", + "quota": { + "gpu": "32" + } + }, + "M": { + "parent": "T", + "quota": { + "gpu": "16" + } + }, + "N": { + "parent": "T", + "quota": { + "gpu": "16" + } + }, + "A": { + "parent": "M", + "quota": { + "gpu": "8" + } + }, + "B": { + "parent": "M", + "quota": { + "gpu": "8" + } + }, + "C": { + "parent": "N", + "quota": { + "gpu": "8" + } + }, + "D": { + "parent": "N", + "quota": { + "gpu": "8" + } + } + } + } + }` + + var job1String string = `{ + "kind": "Consumer", + "metadata": { + "name": "job-1" + }, + "spec": { + "id": "job-1", + "trees": [ + { + "treeName": "tree", + "groupID": "A", + "request": { + "gpu": 24 + } + } + ] + } + }` + + var job2String string = `{ + "kind": "Consumer", + "metadata": { + "name": "job-2" + }, + "spec": { + "id": "job-2", + "trees": [ + { + "treeName": "tree", + "groupID": "C", + "request": { + "gpu": 8 + } + } + ] + } + }` + + var job3String string = `{ + "kind": "Consumer", + "metadata": { + "name": "job-3" + }, + "spec": { + "id": "job-3", + "trees": [ + { + "treeName": "tree", + "groupID": "B", + "request": { + "gpu": 1 + } + } + ] + } + }` + + // 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") + + // add quota tree + treeName, err := qmTest.AddTreeFromString(treeString) + assert.NoError(t, err, "No error expected when adding a tree") + + // create and add consumers + consumerStrings := []string{job1String, job2String, job3String} + numConsumers := len(consumerStrings) + consumerID := make([]string, numConsumers) + var response *core.AllocationResponse + for i, consumerString := range consumerStrings { + consumerInfo, err := quota.NewConsumerInfoFromString(consumerString) + assert.NoError(t, err, "No error expected when creating a consumer info") + consumerID[i] = consumerInfo.GetID() + added, err := qmTest.AddConsumer(consumerInfo) + assert.True(t, added && err == nil, "Expecting consumer to be added to quota manager") + response, err = qmTest.Allocate(treeName, consumerID[i]) + assert.NoError(t, err, "No Error expected when allocating consumer %s to tree, err %v, got %v", + consumerID[i], err, qmTest.GetTreeController(treeName)) + } + + // check expected results after last allocation + assert.NotNil(t, response, "A non nill response is expected") + assert.True(t, response.IsAllocated(), "Allocating consumer %s should succeed", consumerID[numConsumers-1]) + assert.Contains(t, response.GetPreemptedIds(), consumerID[0], + "Expecting consumer %s to be preempted", consumerID[0]) + assert.Contains(t, qmTest.GetTreeController(treeName).GetConsumerIDs(), consumerID[1], + "Expecting consumer %s to remain allocated", consumerID[1]) +} + type AllocationClassifier struct { }