refactor: ParametersIterator

This commit is contained in:
debugtalk
2022-04-16 00:35:25 +08:00
parent 3ec7cb4f10
commit 810bce0a88
8 changed files with 670 additions and 413 deletions

View File

@@ -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)

View File

@@ -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
}

345
hrp/parameters.go Normal file
View File

@@ -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
}

299
hrp/parameters_test.go Normal file
View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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
}