From 810bce0a8801c09c15a26b5193f785b1013eec8c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 16 Apr 2022 00:35:25 +0800 Subject: [PATCH] refactor: ParametersIterator --- hrp/boomer.go | 15 +- hrp/config.go | 59 ------- hrp/parameters.go | 345 +++++++++++++++++++++++++++++++++++++++++ hrp/parameters_test.go | 299 +++++++++++++++++++++++++++++++++++ hrp/parser.go | 177 --------------------- hrp/parser_test.go | 150 ------------------ hrp/runner.go | 12 +- hrp/session.go | 26 ++-- 8 files changed, 670 insertions(+), 413 deletions(-) create mode 100644 hrp/parameters.go create mode 100644 hrp/parameters_test.go diff --git a/hrp/boomer.go b/hrp/boomer.go index 8a59defb..7bba9285 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -83,7 +83,6 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend b.plugins = append(b.plugins, sessionRunner.parser.plugin) b.pluginsMutex.Unlock() } - sessionRunner.resetSession() // broadcast to all rendezvous at once when spawn done go func() { @@ -93,6 +92,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend } }() + // set paramters mode for load testing + parametersIterator := sessionRunner.parametersIterator + parametersIterator.SetUnlimitedMode() + return &boomer.Task{ Name: testcase.Config.Name, Weight: testcase.Config.Weight, @@ -100,15 +103,11 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend testcaseSuccess := true // flag whole testcase result transactionSuccess := true // flag current transaction result - var parameterVariables map[string]interface{} - // iterate through all parameter iterators and update case variables - for _, it := range sessionRunner.parsedConfig.ParametersSetting.Iterators { - if it.HasNext() { - parameterVariables = it.Next() - } + if parametersIterator.HasNext() { + sessionRunner.updateConfigVariables(parametersIterator.Next()) } - sessionRunner.updateConfigVariables(parameterVariables) + sessionRunner.resetSession() startTime := time.Now() for _, step := range testcase.TestSteps { stepResult, err := step.Run(sessionRunner) diff --git a/hrp/config.go b/hrp/config.go index 7a3683fd..574ba2d8 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -1,10 +1,7 @@ package hrp import ( - "math/rand" "reflect" - "sync" - "time" "github.com/httprunner/httprunner/hrp/internal/builtin" ) @@ -158,59 +155,3 @@ const ( var ( thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5} ) - -type TParamsConfig struct { - Strategy interface{} `json:"strategy,omitempty" yaml:"strategy,omitempty"` // map[string]string、string - Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"` - Iterators []*Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"` // 保存参数的迭代器 -} - -type Iterator struct { - sync.Mutex - data iteratorParamsType - strategy iteratorStrategyType // random, sequential - iteration int - index int -} - -type iteratorStrategyType string - -const ( - strategyRandom iteratorStrategyType = "random" - strategySequential iteratorStrategyType = "sequential" -) - -type iteratorParamsType []map[string]interface{} - -func (params iteratorParamsType) Iterator() *Iterator { - return &Iterator{ - data: params, - iteration: len(params), - index: 0, - } -} - -func (iter *Iterator) HasNext() bool { - if iter.iteration == -1 { - return true - } - return iter.index < iter.iteration -} - -func (iter *Iterator) Next() (value map[string]interface{}) { - iter.Lock() - defer iter.Unlock() - if len(iter.data) == 0 { - iter.index++ - return map[string]interface{}{} - } - if iter.strategy == strategyRandom { - randSource := rand.New(rand.NewSource(time.Now().Unix())) - randIndex := randSource.Intn(len(iter.data)) - value = iter.data[randIndex] - } else { - value = iter.data[iter.index%len(iter.data)] - } - iter.index++ - return value -} diff --git a/hrp/parameters.go b/hrp/parameters.go new file mode 100644 index 00000000..a42f87eb --- /dev/null +++ b/hrp/parameters.go @@ -0,0 +1,345 @@ +package hrp + +import ( + "math/rand" + "reflect" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +type TParamsConfig struct { + Strategy iteratorStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // overall strategy + Strategies map[string]iteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // map[string]string、string + Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` +} + +type iteratorStrategy string + +const ( + strategySequential iteratorStrategy = "sequential" + strategyRandom iteratorStrategy = "random" + strategyUnique iteratorStrategy = "unique" +) + +/* +[ + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, +] +*/ +type Parameters []map[string]interface{} + +func initParametersIterator(cfg *TConfig) (*ParametersIterator, error) { + parameters, err := 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 { + iterator := &ParametersIterator{ + data: parameters, + hasNext: true, + sequentialParameters: nil, + randomParameterNames: nil, + limit: config.Limit, + index: 0, + } + + if len(parameters) == 0 { + iterator.hasNext = false + return iterator + } + + parametersList := make([]Parameters, 0) + for paramName := range parameters { + // check parameter individual strategy + strategy, ok := config.Strategies[paramName] + if !ok { + // default to overall strategy + strategy = config.Strategy + } + + // group parameters by strategy + if strategy == strategyRandom { + 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 { + if len(iterator.sequentialParameters) > 0 { + iterator.limit = len(iterator.sequentialParameters) + } else { + iterator.limit = 1 + } + } + + 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 + index int +} + +// SetUnlimitedMode is used for load testing +func (iter *ParametersIterator) SetUnlimitedMode() { + 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 + } + + if len(iter.data) == 0 { + iter.hasNext = false + return nil + } + + selectedParameters := make(map[string]interface{}) + if iter.index < len(iter.sequentialParameters) { + selectedParameters = iter.sequentialParameters[iter.index] + } + + for _, paramName := range iter.randomParameterNames { + randSource := rand.New(rand.NewSource(time.Now().Unix())) + randIndex := randSource.Intn(len(iter.data[paramName])) + selectedParameters[paramName] = iter.data[paramName][randIndex] + } + + iter.index++ + if iter.limit > 0 && iter.index >= iter.limit { + iter.hasNext = false + } + + return selectedParameters +} + +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 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 := newParser().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) + 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 +} diff --git a/hrp/parameters_test.go b/hrp/parameters_test.go new file mode 100644 index 00000000..5f834313 --- /dev/null +++ b/hrp/parameters_test.go @@ -0,0 +1,299 @@ +package hrp + +import ( + "fmt" + "testing" + + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" +) + +func TestLoadParameters(t *testing.T) { + testData := []struct { + configParameters map[string]interface{} + loadedParameters map[string]Parameters + }{ + { + map[string]interface{}{ + "username-password": fmt.Sprintf("${parameterize(%s/$file)}", hrpExamplesDir), + }, + map[string]Parameters{ + "username-password": { + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, + {"username": "test3", "password": "333333"}, + }, + }, + }, + { + map[string]interface{}{ + "username-password": [][]interface{}{ + {"test1", "111111"}, + {"test2", "222222"}, + }, + "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}, + "app_version": []interface{}{4.0}, + }, + map[string]Parameters{ + "username-password": { + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, + }, + "user_agent": { + {"user_agent": "IOS/10.1"}, + {"user_agent": "IOS/10.2"}, + }, + "app_version": { + {"app_version": 4.0}, + }, + }, + }, + { + map[string]interface{}{}, + nil, + }, + { + nil, + nil, + }, + } + + variablesMapping := map[string]interface{}{ + "file": "account.csv", + } + for _, data := range testData { + value, err := loadParameters(data.configParameters, variablesMapping) + if !assert.Nil(t, err) { + t.Fatal() + } + if !assert.Equal(t, data.loadedParameters, value) { + t.Fatal() + } + } +} + +func TestLoadParametersError(t *testing.T) { + testData := []struct { + configParameters map[string]interface{} + }{ + { + map[string]interface{}{ + "username_password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), + "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, + }, + { + map[string]interface{}{ + "username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), + "user-agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, + }, + { + map[string]interface{}{ + "username-password": fmt.Sprintf("${param(%s/account.csv)}", hrpExamplesDir), + "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, + }, + } + for _, data := range testData { + _, err := loadParameters(data.configParameters, map[string]interface{}{}) + if !assert.Error(t, err) { + t.Fatal() + } + } +} + +func TestInitParametersIterator(t *testing.T) { + configParameters := map[string]interface{}{ + "username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), // 3 + "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}, + "app_version": []interface{}{4.0}, + } + testData := []struct { + cfg *TConfig + expectLimit int + }{ + { + &TConfig{ + Parameters: configParameters, + ParametersSetting: &TParamsConfig{}, + }, + 6, + }, + { + &TConfig{ + Parameters: configParameters, + ParametersSetting: &TParamsConfig{ + Strategy: "random", + }, + }, + 1, + }, + { + &TConfig{ + Parameters: configParameters, + ParametersSetting: &TParamsConfig{ + Strategies: map[string]iteratorStrategy{ + "username-password": "random", + }, + }, + }, + 2, + }, + } + for _, data := range testData { + iterator, err := initParametersIterator(data.cfg) + if !assert.Nil(t, err) { + t.Fatal() + } + if !assert.Equal(t, data.expectLimit, iterator.limit) { + t.Fatal() + } + + for i := 0; i < data.expectLimit; i++ { + if !assert.True(t, iterator.HasNext()) { + t.Fatal() + } + log.Info().Interface("next", iterator.Next()).Msg("get next parameters") + } + // should not have next + if !assert.False(t, iterator.HasNext()) { + t.Fatal() + } + } +} + +func TestGenCartesianProduct(t *testing.T) { + testData := []struct { + multiParameters []Parameters + expect Parameters + }{ + { + []Parameters{ + { + {"app_version": 4.0}, + }, + { + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, + }, + { + {"user_agent": "iOS/10.1"}, + {"user_agent": "iOS/10.2"}, + }, + }, + Parameters{ + {"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.1", "username": "test1"}, + {"app_version": 4.0, "password": "111111", "user_agent": "iOS/10.2", "username": "test1"}, + {"app_version": 4.0, "password": "222222", "user_agent": "iOS/10.1", "username": "test2"}, + {"app_version": 4.0, "password": "222222", "user_agent": "iOS/10.2", "username": "test2"}, + }, + }, + { + nil, + nil, + }, + { + []Parameters{}, + nil, + }, + } + + for _, data := range testData { + parameters := genCartesianProduct(data.multiParameters) + if !assert.Equal(t, data.expect, parameters) { + t.Fatal() + } + } +} + +func TestConvertParameters(t *testing.T) { + testData := []struct { + key string + parametersRawList interface{} + expect []map[string]interface{} + }{ + { + "username-password", + []map[string]interface{}{ + {"username": "test1", "password": 111111, "other": "111"}, + {"username": "test2", "password": 222222, "other": "222"}, + }, + []map[string]interface{}{ + {"username": "test1", "password": 111111}, + {"username": "test2", "password": 222222}, + }, + }, + { + "username-password", + [][]string{ + {"test1", "111111"}, + {"test2", "222222"}, + }, + []map[string]interface{}{ + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, + }, + }, + { + "app_version", + []float64{3.1, 3.0}, + []map[string]interface{}{ + {"app_version": 3.1}, + {"app_version": 3.0}, + }, + }, + { + "user_agent", + []string{"iOS/10.1", "iOS/10.2"}, + []map[string]interface{}{ + {"user_agent": "iOS/10.1"}, + {"user_agent": "iOS/10.2"}, + }, + }, + } + + for _, data := range testData { + value, err := convertParameters(data.key, data.parametersRawList) + if !assert.Nil(t, err) { + t.Fatal() + } + if !assert.Equal(t, data.expect, value) { + t.Fatal() + } + } +} + +func TestConvertParametersError(t *testing.T) { + testData := []struct { + key string + parametersRawList interface{} + }{ + { + "app_version", + 123, // not slice + }, + { + "app_version", + "123", // not slice + }, + { + "username-password", + []map[string]interface{}{ // parameter names not match + {"username": "test1", "other": "111"}, + {"username": "test2", "other": "222"}, + }, + }, + { + "username-password", + [][]string{ // parameter names length not match + {"test1"}, + {"test2"}, + }, + }, + } + + for _, data := range testData { + _, err := convertParameters(data.key, data.parametersRawList) + if !assert.Error(t, err) { + t.Fatal() + } + } +} diff --git a/hrp/parser.go b/hrp/parser.go index 46ab0f47..3a0d03bb 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/maja42/goval" - "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/funplugin" @@ -532,179 +531,3 @@ func findallVariables(raw string) variableSet { return varSet } - -func genCartesianProduct(paramsMap map[string]iteratorParamsType) iteratorParamsType { - if len(paramsMap) == 0 { - return nil - } - var params []iteratorParamsType - for _, v := range paramsMap { - params = append(params, v) - } - var cartesianProduct iteratorParamsType - cartesianProduct = params[0] - for i := 0; i < len(params)-1; i++ { - var tempProduct iteratorParamsType - for _, param1 := range cartesianProduct { - for _, param2 := range params[i+1] { - tempProduct = append(tempProduct, mergeVariables(param1, param2)) - } - } - cartesianProduct = tempProduct - } - return cartesianProduct -} - -func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) (map[string]iteratorParamsType, error) { - if len(parameters) == 0 { - return nil, nil - } - parsedParametersSlice := make(map[string]iteratorParamsType) - var err error - for k, v := range parameters { - var parameterSlice iteratorParamsType - rawValue := reflect.ValueOf(v) - switch rawValue.Kind() { - case reflect.String: - // e.g. username-password: ${parameterize(examples/hrp/account.csv)} -> [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}] - var parsedParameterContent interface{} - parsedParameterContent, err = newParser().ParseString(rawValue.String(), variablesMapping) - if err != nil { - log.Error().Interface("parameterContent", rawValue).Msg("[parseParameters] parse parameter content error") - return nil, err - } - parsedParameterRawValue := reflect.ValueOf(parsedParameterContent) - if parsedParameterRawValue.Kind() != reflect.Slice { - log.Error().Interface("parameterContent", parsedParameterRawValue).Msg("[parseParameters] parsed parameter content should be slice") - return nil, errors.New("parsed parameter content should be slice") - } - parameterSlice, err = parseSlice(k, parsedParameterRawValue.Interface()) - case reflect.Slice: - // e.g. user_agent: ["iOS/10.1", "iOS/10.2"] -> [{"user_agent": "iOS/10.1"}, {"user_agent": "iOS/10.2"}] - parameterSlice, err = parseSlice(k, rawValue.Interface()) - default: - log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be slice or text(functions call)") - return nil, errors.New("parameter content should be slice or text(functions call)") - } - if err != nil { - return nil, err - } - parsedParametersSlice[k] = parameterSlice - } - return parsedParametersSlice, nil -} - -func parseSlice(parameterName string, parameterContent interface{}) ([]map[string]interface{}, error) { - parameterNameSlice := strings.Split(parameterName, "-") - var parameterSlice []map[string]interface{} - parameterContentSlice := reflect.ValueOf(parameterContent) - if parameterContentSlice.Kind() != reflect.Slice { - return nil, errors.New("parameterContent should be slice") - } - for i := 0; i < parameterContentSlice.Len(); i++ { - parameterMap := make(map[string]interface{}) - elem := reflect.ValueOf(parameterContentSlice.Index(i).Interface()) - switch elem.Kind() { - case reflect.Map: - // e.g. "username-password": [{"username": "test1", "password": "passwd1", "other": "111"}, {"username": "test2", "password": "passwd2", "other": ""222}] - // -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}] - for _, key := range parameterNameSlice { - if _, ok := elem.Interface().(map[string]interface{})[key]; ok { - parameterMap[key] = elem.MapIndex(reflect.ValueOf(key)).Interface() - } else { - log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name not found") - return nil, errors.New("parameter name not found") - } - } - case reflect.Slice: - // e.g. "username-password": [["test1", "passwd1"], ["test2", "passwd2"]] - // -> [{"username": "test1", "password": "passwd1"}, {"username": "test2", "password": "passwd2"}] - if len(parameterNameSlice) != elem.Len() { - log.Error().Interface("parameterNameSlice", parameterNameSlice).Interface("parameterContent", elem.Interface()).Msg("[parseParameters] parameter name slice and parameter content slice should have the same length") - return nil, errors.New("parameter name slice and parameter content slice should have the same length") - } else { - for j := 0; j < elem.Len(); j++ { - parameterMap[parameterNameSlice[j]] = elem.Index(j).Interface() - } - } - default: - // e.g. "app_version": [3.1, 3.0] - // -> [{"app_version": 3.1}, {"app_version": 3.0}] - if len(parameterNameSlice) != 1 { - log.Error().Interface("parameterNameSlice", parameterNameSlice).Msg("[parseParameters] parameter name slice should have only one element when parameter content is string") - return nil, errors.New("parameter name slice should have only one element when parameter content is string") - } - parameterMap[parameterNameSlice[0]] = elem.Interface() - } - parameterSlice = append(parameterSlice, parameterMap) - } - return parameterSlice, nil -} - -func initParameterIterator(cfg *TConfig, mode string) (err error) { - var parameters map[string]iteratorParamsType - parameters, err = parseParameters(cfg.Parameters, cfg.Variables) - if err != nil { - return err - } - // parse config parameters setting - if cfg.ParametersSetting == nil { - cfg.ParametersSetting = &TParamsConfig{Iterators: []*Iterator{}} - } - // boomer模式下不限制迭代次数 - if mode == "boomer" { - cfg.ParametersSetting.Iteration = -1 - } - rawValue := reflect.ValueOf(cfg.ParametersSetting.Strategy) - switch rawValue.Kind() { - case reflect.Map: - // strategy: {"user_agent": "sequential", "username-password": "random"}, 每个参数对应一个迭代器,每个迭代器随机、顺序选取元素互不影响 - for k, v := range parameters { - if _, ok := rawValue.Interface().(map[string]interface{})[k]; ok { - // use strategy if configured - cfg.ParametersSetting.Iterators = append( - cfg.ParametersSetting.Iterators, - newIterator(v, iteratorStrategyType(rawValue.MapIndex(reflect.ValueOf(k)).String()), cfg.ParametersSetting.Iteration), - ) - } else { - // use sequential strategy by default - cfg.ParametersSetting.Iterators = append( - cfg.ParametersSetting.Iterators, - newIterator(v, strategySequential, cfg.ParametersSetting.Iteration), - ) - } - } - case reflect.String: - // strategy: random, 仅生成一个的迭代器,该迭代器在参数笛卡尔积slice中随机选取元素 - if len(rawValue.String()) == 0 { - cfg.ParametersSetting.Strategy = strategySequential - } else { - cfg.ParametersSetting.Strategy = iteratorStrategyType(strings.ToLower(rawValue.String())) - } - cfg.ParametersSetting.Iterators = append( - cfg.ParametersSetting.Iterators, - newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(iteratorStrategyType), cfg.ParametersSetting.Iteration), - ) - default: - // default strategy: sequential, 仅生成一个的迭代器,该迭代器在参数笛卡尔积slice中顺序选取元素 - cfg.ParametersSetting.Strategy = strategySequential - cfg.ParametersSetting.Iterators = append( - cfg.ParametersSetting.Iterators, - newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(iteratorStrategyType), cfg.ParametersSetting.Iteration), - ) - } - return nil -} - -func newIterator(parameters iteratorParamsType, strategy iteratorStrategyType, iteration int) *Iterator { - iter := parameters.Iterator() - iter.strategy = strategy - if iteration > 0 { - iter.iteration = iteration - } else if iteration < 0 { - iter.iteration = -1 - } else if iter.iteration == 0 { - iter.iteration = 1 - } - return iter -} diff --git a/hrp/parser_test.go b/hrp/parser_test.go index 38fa5cc9..10e843a7 100644 --- a/hrp/parser_test.go +++ b/hrp/parser_test.go @@ -1,7 +1,6 @@ package hrp import ( - "fmt" "sort" "testing" "time" @@ -754,152 +753,3 @@ func TestFindallVariables(t *testing.T) { } } } - -func TestParseParameters(t *testing.T) { - testData := []struct { - rawVars map[string]interface{} - expectLength int - }{ - { - map[string]interface{}{ - "username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), - "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}, - }, - 6, - }, - { - map[string]interface{}{ - "username-password": [][]interface{}{ - {"test1", "111111"}, - {"test2", "222222"}, - {"test3", "333333"}, - }, - "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}, - "app_version": []interface{}{0.3}, - }, - 6, - }, - { - map[string]interface{}{ - "username-password": [][]interface{}{ - {"test1", "111111"}, - {"test2", "222222"}, - {"test3", "333333"}, - }, - "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}, - "app_version": []interface{}{0.3, 0.4, 0.5}, - }, - 18, - }, - { - map[string]interface{}{}, - 0, - }, - { - nil, - 0, - }, - } - for _, data := range testData { - params, _ := parseParameters(data.rawVars, map[string]interface{}{}) - value := genCartesianProduct(params) - if !assert.Len(t, value, data.expectLength) { - t.Fail() - } - } -} - -func TestParseParametersError(t *testing.T) { - testData := []struct { - rawVars map[string]interface{} - }{ - { - map[string]interface{}{ - "username_password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), - "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, - }, - { - map[string]interface{}{ - "username-password": fmt.Sprintf("${parameterize(%s/account.csv)}", hrpExamplesDir), - "user-agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, - }, - { - map[string]interface{}{ - "username-password": fmt.Sprintf("${param(%s/account.csv)}", hrpExamplesDir), - "user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}}, - }, - } - for _, data := range testData { - _, err := parseParameters(data.rawVars, map[string]interface{}{}) - if !assert.Error(t, err) { - t.Fail() - } - } -} - -func TestParseSlice(t *testing.T) { - testData := []struct { - rawVar1 string - rawVar2 interface{} - expect []map[string]interface{} - }{ - { - "username-password", - []map[string]interface{}{ - {"username": "test1", "password": 111111, "other": "111"}, - {"username": "test2", "password": 222222, "other": "222"}, - }, - []map[string]interface{}{ - {"username": "test1", "password": 111111}, - {"username": "test2", "password": 222222}, - }, - }, - { - "username-password", - [][]string{ - {"test1", "111111"}, - {"test2", "222222"}, - }, - []map[string]interface{}{ - {"username": "test1", "password": "111111"}, - {"username": "test2", "password": "222222"}, - }, - }, - { - "app_version", - []float64{3.1, 3.0}, - []map[string]interface{}{ - {"app_version": 3.1}, - {"app_version": 3.0}, - }, - }, - } - for _, data := range testData { - value, _ := parseSlice(data.rawVar1, data.rawVar2) - if !assert.Equal(t, data.expect, value) { - t.Fail() - } - } -} - -func TestParseSliceError(t *testing.T) { - testData := []struct { - rawVar1 string - rawVar2 interface{} - }{ - { - "app_version", - 123, - }, - { - "app_version", - "123", - }, - } - for _, data := range testData { - _, err := parseSlice(data.rawVar1, data.rawVar2) - if !assert.Error(t, err) { - t.Fail() - } - } -} diff --git a/hrp/runner.go b/hrp/runner.go index 2e751fd6..76da1e75 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -163,16 +163,8 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { } }() - // 在runner模式下,指定整体策略,cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器 - for it := sessionRunner.parsedConfig.ParametersSetting.Iterators[0]; it.HasNext(); { - var parameterVariables map[string]interface{} - // iterate through all parameter iterators and update case variables - for _, it := range sessionRunner.parsedConfig.ParametersSetting.Iterators { - if it.HasNext() { - parameterVariables = it.Next() - } - } - if err = sessionRunner.Start(parameterVariables); err != nil { + for it := sessionRunner.parametersIterator; it.HasNext(); { + if err = sessionRunner.Start(it.Next()); err != nil { log.Error().Err(err).Msg("[Run] run testcase failed") return err } diff --git a/hrp/session.go b/hrp/session.go index 478fbe03..fa4dbfa0 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -12,11 +12,12 @@ import ( // SessionRunner is used to run testcase and its steps. // each testcase has its own SessionRunner instance and share session variables. type SessionRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser - parsedConfig *TConfig - sessionVariables map[string]interface{} + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + parsedConfig *TConfig + parametersIterator *ParametersIterator + sessionVariables map[string]interface{} // transactions stores transaction timing info. // key is transaction name, value is map of transaction type and time, e.g. start time and end time. transactions map[string]map[transactionType]time.Time @@ -61,13 +62,16 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { Str("type", string(step.Type())).Msg("run step start") stepResult, err := step.Run(r) - if err != nil && r.hrpRunner.failfast { + if err != nil { log.Error(). Str("step", stepResult.Name). Str("type", string(stepResult.StepType)). Bool("success", false). Msg("run step end") - return errors.Wrap(err, "abort running due to failfast setting") + + if r.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") + } } // update extracted variables @@ -150,11 +154,15 @@ func (r *SessionRunner) parseConfig() error { r.parsedConfig.ThinkTimeSetting.checkThinkTime() // parse testcase config parameters - err = initParameterIterator(r.parsedConfig, "runner") + parametersIterator, err := initParametersIterator(r.parsedConfig) if err != nil { - log.Error().Interface("parameters", r.parsedConfig.Parameters).Err(err).Msg("parse config parameters failed") + log.Error().Err(err). + Interface("parameters", r.parsedConfig.Parameters). + Interface("parametersSetting", r.parsedConfig.ParametersSetting). + Msg("parse config parameters failed") return errors.Wrap(err, "parse testcase config parameters failed") } + r.parametersIterator = parametersIterator return nil }