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  }