mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 06:59:45 +08:00
refactor: ParametersIterator
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
345
hrp/parameters.go
Normal 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
299
hrp/parameters_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
177
hrp/parser.go
177
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user