diff --git a/boomer.go b/boomer.go index 08644c6e..6e973d83 100644 --- a/boomer.go +++ b/boomer.go @@ -46,15 +46,12 @@ func (b *hrpBoomer) Run(testcases ...ITestCase) { panic(err) } cfg := testcase.Config.ToStruct() - parameters := getParameters(testcase.Config) - if parameters == nil { - parameters = []map[string]interface{}{{}} - } - for _, parameter := range parameters { - cfg.Variables = mergeVariables(parameter, cfg.Variables) - task := b.convertBoomerTask(testcase) - taskSlice = append(taskSlice, task) + err = initParameterIterator(cfg, "boomer") + if err != nil { + panic(err) } + task := b.convertBoomerTask(testcase) + taskSlice = append(taskSlice, task) } b.Boomer.Run(taskSlice...) } @@ -71,6 +68,10 @@ func (b *hrpBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { testcaseSuccess := true // flag whole testcase result var transactionSuccess = true // flag current transaction result + cfg := testcase.Config.ToStruct() + if it := cfg.ParameterIterator; it.HasNext() { + cfg.Variables = mergeVariables(it.Next(), cfg.Variables) + } startTime := time.Now() for index, step := range testcase.TestSteps { stepData, err := runner.runStep(index) diff --git a/examples/parameters_test.json b/examples/parameters_test.json index c519e928..d61fd85f 100644 --- a/examples/parameters_test.json +++ b/examples/parameters_test.json @@ -6,14 +6,11 @@ "iOS/10.1", "iOS/10.2" ], - "username1-password1": [ - ["a", "123"], - ["b", "456"] - ], "username-password": "${parameterize(examples/account.csv)}" }, "parameters_setting": { - "strategy": "random" + "strategy": "random", + "iteration": 1 }, "variables": { "app_version": "f1" diff --git a/examples/parameters_test.yaml b/examples/parameters_test.yaml index 65c307a8..b19ddcf4 100644 --- a/examples/parameters_test.yaml +++ b/examples/parameters_test.yaml @@ -5,6 +5,7 @@ config: username-password: ${parameterize(examples/account.csv)} parameters_setting: strategy: random + iteration: 1 variables: app_version: f1 base_url: "https://postman-echo.com" diff --git a/models.go b/models.go index d587a9c0..9ded19c3 100644 --- a/models.go +++ b/models.go @@ -1,5 +1,11 @@ package hrp +import ( + "math/rand" + "strings" + "time" +) + const ( httpGET string = "GET" httpHEAD string = "HEAD" @@ -18,11 +24,56 @@ type TConfig struct { BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` - ParametersSetting map[string]interface{} `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` + ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` + ParameterIterator *Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"` Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` } +type TParamsConfig struct { + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` + Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"` +} + +type paramsType []map[string]interface{} + +type Iterator struct { + data paramsType + strategy string + iteration int + index int +} + +func (params paramsType) 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.index++ + if len(iter.data) == 0 { + return map[string]interface{}{} + } + if strings.ToLower(iter.strategy) == "random" { + 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)] + } + return value +} + // Request represents HTTP request data structure. // This is used for teststep. type Request struct { diff --git a/parser.go b/parser.go index cc92fde8..7f34539b 100644 --- a/parser.go +++ b/parser.go @@ -3,16 +3,13 @@ package hrp import ( "encoding/json" "fmt" - "math/rand" + "github.com/maja42/goval" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" "net/url" "reflect" "regexp" "strings" - "time" - - "github.com/maja42/goval" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" "github.com/httprunner/hrp/internal/builtin" ) @@ -496,19 +493,6 @@ func findallVariables(raw string) variableSet { return varSet } -func shuffleCartesianProduct(slice []map[string]interface{}) { - if len(slice) == 0 { - return - } - r := rand.New(rand.NewSource(time.Now().Unix())) - for len(slice) > 0 { - n := len(slice) - randIndex := r.Intn(n) - slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1] - slice = slice[:n-1] - } -} - func genCartesianProduct(params [][]map[string]interface{}) []map[string]interface{} { if len(params) == 0 { return nil @@ -527,19 +511,6 @@ func genCartesianProduct(params [][]map[string]interface{}) []map[string]interfa return cartesianProduct } -func getParameters(config IConfig) []map[string]interface{} { - cfg := config.ToStruct() - // parse config parameters - parsedParams, err := parseParameters(cfg.Parameters, cfg.Variables) - if err != nil { - log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed") - } - if cfg.ParametersSetting["strategy"] != nil && strings.ToLower(cfg.ParametersSetting["strategy"].(string)) == "random" { - shuffleCartesianProduct(parsedParams) - } - return parsedParams -} - func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) ([]map[string]interface{}, error) { if len(parameters) == 0 { return nil, nil @@ -562,9 +533,9 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str log.Error().Interface("parameterContent", parsedParameterRawValue).Msg("[parseParameters] parsed parameter content should be Slice, got %v") return nil, errors.New("parsed parameter content should be Slice") } - parameterSlice, err = handleSlice(k, parsedParameterRawValue.Interface()) + parameterSlice, err = parseSlice(k, parsedParameterRawValue.Interface()) case reflect.Slice: - parameterSlice, err = handleSlice(k, rawValue.Interface()) + parameterSlice, err = parseSlice(k, rawValue.Interface()) default: log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be Slice or Text(variables or functions call)") return nil, errors.New("parameter content should be Slice or Text(variables or functions call)") @@ -577,10 +548,13 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str return genCartesianProduct(parsedParametersSlice), nil } -func handleSlice(parameterName string, parameterContent interface{}) ([]map[string]interface{}, error) { +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()) @@ -620,3 +594,31 @@ func handleSlice(parameterName string, parameterContent interface{}) ([]map[stri } return parameterSlice, nil } + +func initParameterIterator(cfg *TConfig, mode string) (err error) { + var parameters paramsType + parameters, err = parseParameters(cfg.Parameters, cfg.Variables) + cfg.ParameterIterator = parameters.Iterator() + if err != nil { + return err + } + // parse config parameters setting + if cfg.ParametersSetting == nil { + cfg.ParametersSetting = &TParamsConfig{} + } + if len(cfg.ParametersSetting.Strategy) == 0 { + cfg.ParametersSetting.Strategy = "sequential" + } else { + cfg.ParametersSetting.Strategy = strings.ToLower(cfg.ParametersSetting.Strategy) + } + cfg.ParameterIterator.strategy = cfg.ParametersSetting.Strategy + if mode == "boomer" { + cfg.ParametersSetting.Iteration = -1 + cfg.ParameterIterator.iteration = cfg.ParametersSetting.Iteration + } else { + if cfg.ParametersSetting.Iteration != 0 { + cfg.ParameterIterator.iteration = cfg.ParametersSetting.Iteration + } + } + return nil +} diff --git a/parser_test.go b/parser_test.go index c69a584d..a8d4bdea 100644 --- a/parser_test.go +++ b/parser_test.go @@ -692,7 +692,7 @@ func TestParseParametersError(t *testing.T) { } } -func TestHandleSlice(t *testing.T) { +func TestParseSlice(t *testing.T) { testData := []struct { rawVar1 string rawVar2 interface{} @@ -724,9 +724,31 @@ func TestHandleSlice(t *testing.T) { }, } for _, data := range testData { - value, _ := handleSlice(data.rawVar1, data.rawVar2) + 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/runner.go b/runner.go index b33d44d3..f568c818 100644 --- a/runner.go +++ b/runner.go @@ -141,13 +141,17 @@ func (r *caseRunner) run() error { if err := r.parseConfig(config); err != nil { return err } + cfg := config.ToStruct() log.Info().Str("testcase", config.Name()).Msg("run testcase start") - r.startTime = time.Now() - for index := range r.TestCase.TestSteps { - _, err := r.runStep(index) - if err != nil { - if r.hrpRunner.failfast { - return errors.Wrap(err, "abort running due to failfast setting") + for it := cfg.ParameterIterator; it.HasNext(); { + cfg.Variables = mergeVariables(it.Next(), cfg.Variables) + r.startTime = time.Now() + for index := range r.TestCase.TestSteps { + _, err := r.runStep(index) + if err != nil { + if r.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") + } } } } @@ -482,6 +486,12 @@ func (r *caseRunner) parseConfig(config IConfig) error { return err } cfg.Variables = parsedVariables + // parse config parameters + err = initParameterIterator(cfg, "runner") + if err != nil { + log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed") + return err + } // parse config name parsedName, err := parseString(cfg.Name, cfg.Variables) if err != nil {