package hrp import ( "math/rand" "reflect" "strings" "sync" "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) type TParamsConfig struct { PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` // overall pick-order strategy Strategies map[string]IteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` } type iteratorPickOrder string const ( pickOrderSequential iteratorPickOrder = "sequential" pickOrderRandom iteratorPickOrder = "random" pickOrderUnique iteratorPickOrder = "unique" ) /* [ {"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}, ] */ type Parameters []map[string]interface{} type IteratorStrategy struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"` } func (p *Parser) InitParametersIterator(cfg *TConfig) (*ParametersIterator, error) { parameters, err := p.LoadParameters(cfg.Parameters, cfg.Variables) if err != nil { return nil, err } return newParametersIterator(parameters, cfg.ParametersSetting), nil } func newParametersIterator(parameters map[string]Parameters, config *TParamsConfig) *ParametersIterator { if config == nil { config = &TParamsConfig{} } iterator := &ParametersIterator{ data: parameters, hasNext: true, sequentialParameters: nil, randomParameterNames: nil, Limit: config.Limit, Index: 0, } if len(parameters) == 0 { iterator.data = map[string]Parameters{} iterator.Limit = 1 return iterator } parametersList := make([]Parameters, 0) for paramName := range parameters { // check parameter individual pick order strategy strategy, ok := config.Strategies[paramName] if !ok || strategy.PickOrder == "" { // default to overall pick order strategy strategy.PickOrder = config.PickOrder } // group parameters by pick order strategy if strategy.PickOrder == pickOrderRandom { iterator.randomParameterNames = append(iterator.randomParameterNames, paramName) } else { parametersList = append(parametersList, parameters[paramName]) } } // generate cartesian product for sequential parameters iterator.sequentialParameters = GenCartesianProduct(parametersList) if iterator.Limit < 0 { log.Warn().Msg("parameters unlimited mode is only supported for load testing") iterator.Limit = 0 } if iterator.Limit == 0 { // limit not set if len(iterator.sequentialParameters) > 0 { // use cartesian product of sequential parameters size as limit iterator.Limit = len(iterator.sequentialParameters) } else { // all parameters are selected by random // only run once iterator.Limit = 1 } } else { // limit > 0 log.Info().Int("limit", iterator.Limit).Msg("set limit for parameters") } return iterator } type ParametersIterator struct { sync.Mutex data map[string]Parameters hasNext bool // cache query result sequentialParameters Parameters // cartesian product for sequential parameters randomParameterNames []string // value is parameter names Limit int // limit count for iteration Index int // current iteration index } // SetUnlimitedMode is used for load testing func (iter *ParametersIterator) SetUnlimitedMode() { log.Info().Msg("set parameters unlimited mode") iter.Limit = -1 } func (iter *ParametersIterator) HasNext() bool { if !iter.hasNext { return false } // unlimited mode if iter.Limit == -1 { return true } // reached limit if iter.Index >= iter.Limit { // cache query result iter.hasNext = false return false } return true } func (iter *ParametersIterator) Next() map[string]interface{} { iter.Lock() defer iter.Unlock() if !iter.hasNext { return nil } var selectedParameters map[string]interface{} if len(iter.sequentialParameters) == 0 { selectedParameters = make(map[string]interface{}) } else if iter.Index < len(iter.sequentialParameters) { selectedParameters = iter.sequentialParameters[iter.Index] } else { // loop back to the first sequential parameter index := iter.Index % len(iter.sequentialParameters) selectedParameters = iter.sequentialParameters[index] } // merge with random parameters for _, paramName := range iter.randomParameterNames { randSource := rand.New(rand.NewSource(time.Now().UnixNano())) randIndex := randSource.Intn(len(iter.data[paramName])) for k, v := range iter.data[paramName][randIndex] { selectedParameters[k] = v } } iter.Index++ if iter.Limit > 0 && iter.Index >= iter.Limit { iter.hasNext = false } return selectedParameters } func (iter *ParametersIterator) Data() map[string]interface{} { res := map[string]interface{}{} for key, params := range iter.data { res[key] = params } return res } func GenCartesianProduct(multiParameters []Parameters) Parameters { if len(multiParameters) == 0 { return nil } cartesianProduct := multiParameters[0] for i := 0; i < len(multiParameters)-1; i++ { var tempProduct Parameters for _, param1 := range cartesianProduct { for _, param2 := range multiParameters[i+1] { tempProduct = append(tempProduct, mergeVariables(param1, param2)) } } cartesianProduct = tempProduct } return cartesianProduct } /* LoadParameters loads parameters from multiple sources. parameter value may be in three types: (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"] (2) call built-in parameterize function, "${parameterize(account.csv)}" (3) call custom function in debugtalk.py, "${gen_app_version()}" configParameters = { "user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], // case 1 "username-password": "${parameterize(account.csv)}", // case 2 "app_version": "${gen_app_version()}", // case 3 } => { "user_agent": [ {"user_agent": "iOS/10.1"}, {"user_agent": "iOS/10.2"}, {"user_agent": "iOS/10.3"}, ], "username-password": [ {"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}, ], "app_version": [ {"app_version": "1.0.0"}, {"app_version": "1.0.1"}, ] } */ func (p *Parser) LoadParameters(configParameters map[string]interface{}, variablesMapping map[string]interface{}) ( map[string]Parameters, error) { if len(configParameters) == 0 { return nil, nil } parsedParameters := make(map[string]Parameters) for k, v := range configParameters { var parametersRawList interface{} rawValue := reflect.ValueOf(v) switch rawValue.Kind() { case reflect.Slice: // case 1 // e.g. user_agent: ["iOS/10.1", "iOS/10.2"] // => ["iOS/10.1", "iOS/10.2"] parametersRawList = rawValue.Interface() case reflect.String: // case 2 or case 3 // e.g. username-password: ${parameterize(examples/hrp/account.csv)} // => [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}] // => [["test1", "111111"], ["test2", "222222"]] // e.g. "app_version": "${gen_app_version()}" // => ["1.0.0", "1.0.1"] parsedParameterContent, err := p.ParseString(rawValue.String(), variablesMapping) if err != nil { log.Error().Err(err). Str("parametersRawContent", rawValue.String()). Msg("parse parameters content failed") return nil, err } parsedParameterRawValue := reflect.ValueOf(parsedParameterContent) if parsedParameterRawValue.Kind() != reflect.Slice { log.Error(). Interface("parsedParameterContent", parsedParameterRawValue). Msg("parsed parameters content is not slice") return nil, errors.New("parsed parameters content should be slice") } parametersRawList = parsedParameterRawValue.Interface() default: log.Error(). Interface("parameters", configParameters). Msg("config parameters raw value should be slice or string (functions call)") return nil, errors.New("config parameters raw value format error") } parameterSlice, err := ConvertParameters(k, parametersRawList) if err != nil { return nil, err } parsedParameters[k] = parameterSlice } return parsedParameters, nil } /* convert parameters to standard format key and parametersRawList may be in three types: case 1: key = "user_agent" parametersRawList = ["iOS/10.1", "iOS/10.2"] case 2: key = "username-password" parametersRawList = [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}] case 3: key = "username-password" parametersRawList = [["test1", "111111"], ["test2", "222222"]] */ func ConvertParameters(key string, parametersRawList interface{}) (parameterSlice []map[string]interface{}, err error) { parametersRawSlice := reflect.ValueOf(parametersRawList) if parametersRawSlice.Kind() != reflect.Slice { return nil, errors.New("parameters raw value is not list") } // ["user_agent"], ["username", "password"], ["app_version"] parameterNames := strings.Split(key, "-") for i := 0; i < parametersRawSlice.Len(); i++ { parametersLine := make(map[string]interface{}) elem := parametersRawSlice.Index(i) // e.g. Type: interface{} | []interface{}, convert interface{} to []interface{} if elem.Kind() == reflect.Interface { elem = elem.Elem() } switch elem.Kind() { case reflect.Slice: // case 3 // e.g. "username-password": ["test1", "111111"] // => {"username": "test1", "password": "111111"} if len(parameterNames) != elem.Len() { log.Error(). Strs("parameterNames", parameterNames). Int("lineIndex", i). Interface("content", elem.Interface()). Msg("parameters line length does not match to names length") return nil, errors.New("parameters line length does not match to names length") } for j := 0; j < elem.Len(); j++ { parametersLine[parameterNames[j]] = elem.Index(j).Interface() } case reflect.Map: // case 2 // e.g. "username-password": {"username": "test1", "password": "111111", "other": "111"} // => {"username": "test1", "password": "passwd1"} for _, name := range parameterNames { lineMap := elem.Interface().(map[string]interface{}) if _, ok := lineMap[name]; ok { parametersLine[name] = elem.MapIndex(reflect.ValueOf(name)).Interface() } else { log.Error(). Strs("parameterNames", parameterNames). Str("name", name). Msg("parameter name not found") return nil, errors.New("parameter name not found") } } default: // case 1 // e.g. "user_agent": "iOS/10.1" // -> {"user_agent": "iOS/10.1"} if len(parameterNames) != 1 { log.Error(). Strs("parameterNames", parameterNames). Int("lineIndex", i). Msg("parameters format error") return nil, errors.New("parameters format error") } parametersLine[parameterNames[0]] = elem.Interface() } parameterSlice = append(parameterSlice, parametersLine) } return parameterSlice, nil }