github.com/kubeshop/testkube@v1.17.23/cmd/tcl/testworkflow-toolkit/common/combinations.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package common 10 11 import ( 12 "encoding/json" 13 "fmt" 14 "math" 15 "strings" 16 17 "golang.org/x/exp/maps" 18 "k8s.io/apimachinery/pkg/util/intstr" 19 20 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 21 "github.com/kubeshop/testkube/internal/common" 22 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 23 ) 24 25 func CountCombinations(matrix map[string][]interface{}) int64 { 26 combinations := int64(1) 27 for k := range matrix { 28 combinations *= int64(len(matrix[k])) 29 } 30 return combinations 31 } 32 33 func GetMatrixValues(matrix map[string][]interface{}, index int64) map[string]interface{} { 34 // Compute modulo for each matrix parameter 35 keys := maps.Keys(matrix) 36 modulo := map[string]int64{} 37 floor := map[string]int64{} 38 for i, k := range keys { 39 modulo[k] = int64(len(matrix[k])) 40 floor[k] = 1 41 for j := i + 1; j < len(keys); j++ { 42 floor[k] *= int64(len(matrix[keys[j]])) 43 } 44 } 45 46 // Compute values for selected index 47 result := make(map[string]interface{}) 48 for _, k := range keys { 49 kIdx := (index / floor[k]) % modulo[k] 50 result[k] = matrix[k][kIdx] 51 } 52 return result 53 } 54 55 func ReadCount(s intstr.IntOrString, machines ...expressionstcl.Machine) (int64, error) { 56 countExpr, err := expressionstcl.CompileAndResolve(s.String(), machines...) 57 if err != nil { 58 return 0, fmt.Errorf("%s: invalid: %s", s.String(), err) 59 } 60 if countExpr.Static() == nil { 61 return 0, fmt.Errorf("%s: could not resolve: %s", s.String(), err) 62 } 63 countVal, err := countExpr.Static().IntValue() 64 if err != nil { 65 return 0, fmt.Errorf("%s: could not convert to int: %s", s.String(), err) 66 } 67 if countVal < 0 { 68 return 0, fmt.Errorf("%s: should not be lower than zero", s.String()) 69 } 70 return countVal, nil 71 } 72 73 func readParams(base map[string]testworkflowsv1.DynamicList, machines ...expressionstcl.Machine) (map[string][]interface{}, error) { 74 result := make(map[string][]interface{}) 75 for key, items := range base { 76 exprStr := items.Expression 77 if !items.Dynamic { 78 b, err := json.Marshal(items.Static) 79 if err != nil { 80 return nil, fmt.Errorf("%s: could not parse list of values: %s\n", key, err) 81 } 82 exprStr = string(b) 83 } 84 expr, err := expressionstcl.CompileAndResolve(exprStr, machines...) 85 if err != nil { 86 return nil, fmt.Errorf("%s: %s: %s", key, exprStr, err) 87 } 88 if expr.Static() == nil { 89 return nil, fmt.Errorf("%s: %s: could not resolve", key, exprStr) 90 } 91 list, err := expr.Static().SliceValue() 92 if err != nil { 93 return nil, fmt.Errorf("%s: %s: could not parse as list: %s", key, exprStr, err) 94 } 95 result[key] = list 96 } 97 for key := range result { 98 if len(result[key]) == 0 { 99 delete(result, key) 100 } 101 } 102 return result, nil 103 } 104 105 func GetShardValues(values map[string][]interface{}, index int64, count int64) map[string][]interface{} { 106 result := make(map[string][]interface{}) 107 for k := range values { 108 if index > int64(len(values[k])) { 109 result[k] = []interface{}{} 110 continue 111 } 112 shards := int64(len(values[k])) 113 size := int64(math.Floor(float64(shards) / float64(count))) 114 if index >= shards { 115 result[k] = make([]interface{}, 0) 116 continue 117 } 118 sizeMatchPoint := shards - size*count 119 if sizeMatchPoint > index { 120 start := index * (size + 1) 121 end := start + (size + 1) 122 result[k] = values[k][start:end] 123 } else { 124 start := sizeMatchPoint + index*size 125 end := start + size 126 result[k] = values[k][start:end] 127 } 128 } 129 return result 130 } 131 132 type ParamsSpec struct { 133 ShardCount int64 134 MatrixCount int64 135 Count int64 136 Matrix map[string][]interface{} 137 Shards map[string][]interface{} 138 } 139 140 func (p *ParamsSpec) ShardIndexAt(index int64) int64 { 141 return index % p.ShardCount 142 } 143 144 func (p *ParamsSpec) MatrixIndexAt(index int64) int64 { 145 return (index - p.ShardIndexAt(index)) / p.ShardCount 146 } 147 148 func (p *ParamsSpec) ShardsAt(index int64) map[string][]interface{} { 149 return GetShardValues(p.Shards, p.ShardIndexAt(index), p.ShardCount) 150 } 151 152 func (p *ParamsSpec) MatrixAt(index int64) map[string]interface{} { 153 return GetMatrixValues(p.Matrix, p.MatrixIndexAt(index)) 154 } 155 156 func (p *ParamsSpec) MachineAt(index int64) expressionstcl.Machine { 157 // Get basic indices 158 shardIndex := p.ShardIndexAt(index) 159 matrixIndex := p.MatrixIndexAt(index) 160 161 // Compute values for this instance 162 matrixValues := p.MatrixAt(index) 163 shardValues := p.ShardsAt(index) 164 165 return expressionstcl.NewMachine(). 166 Register("index", index). 167 Register("count", p.Count). 168 Register("matrixIndex", matrixIndex). 169 Register("matrixCount", p.MatrixCount). 170 Register("matrix", matrixValues). 171 Register("shardIndex", shardIndex). 172 Register("shardCount", p.ShardCount). 173 Register("shard", shardValues) 174 } 175 176 func (p *ParamsSpec) Humanize() string { 177 // Print information 178 infos := make([]string, 0) 179 if p.MatrixCount > 1 { 180 infos = append(infos, fmt.Sprintf("%d combinations", p.MatrixCount)) 181 } 182 if p.ShardCount > 1 { 183 infos = append(infos, fmt.Sprintf("sharded %d times", p.ShardCount)) 184 } 185 if p.Count == 0 { 186 return "no executions requested" 187 } 188 if p.Count == 1 { 189 return "1 execution requested" 190 } 191 return fmt.Sprintf("%d executions requested: %s", p.Count, strings.Join(infos, ", ")) 192 } 193 194 func GetParamsSpec(origMatrix map[string]testworkflowsv1.DynamicList, origShards map[string]testworkflowsv1.DynamicList, origCount *intstr.IntOrString, origMaxCount *intstr.IntOrString, machines ...expressionstcl.Machine) (*ParamsSpec, error) { 195 // Resolve the shards and matrix 196 shards, err := readParams(origShards, machines...) 197 if err != nil { 198 return nil, fmt.Errorf("shards: %w", err) 199 } 200 matrix, err := readParams(origMatrix, machines...) 201 if err != nil { 202 return nil, fmt.Errorf("matrix: %w", err) 203 } 204 minShards := int64(math.MaxInt64) 205 for key := range shards { 206 if int64(len(shards[key])) < minShards { 207 minShards = int64(len(shards[key])) 208 } 209 } 210 211 // Calculate number of matrix combinations 212 combinations := CountCombinations(matrix) 213 214 // Resolve the count 215 var count, maxCount *int64 216 if origCount != nil { 217 countVal, err := ReadCount(*origCount, machines...) 218 if err != nil { 219 return nil, fmt.Errorf("count: %w", err) 220 } 221 count = &countVal 222 } 223 if origMaxCount != nil { 224 countVal, err := ReadCount(*origMaxCount, machines...) 225 if err != nil { 226 return nil, fmt.Errorf("maxCount: %w", err) 227 } 228 maxCount = &countVal 229 } 230 if count == nil && maxCount == nil { 231 count = common.Ptr(int64(1)) 232 } 233 if count != nil && maxCount != nil && *maxCount < *count { 234 count = maxCount 235 maxCount = nil 236 } 237 if maxCount != nil && *maxCount > minShards { 238 count = &minShards 239 maxCount = nil 240 } else if maxCount != nil { 241 count = maxCount 242 maxCount = nil 243 } 244 245 return &ParamsSpec{ 246 ShardCount: *count, 247 MatrixCount: combinations, 248 Count: *count * combinations, 249 Matrix: matrix, 250 Shards: shards, 251 }, nil 252 }