pull main

This commit is contained in:
debugtalk
2022-01-05 20:49:02 +08:00
16 changed files with 954 additions and 35 deletions

View File

@@ -3,6 +3,7 @@ package hrp
import ( import (
"time" "time"
"github.com/jinzhu/copier"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/boomer" "github.com/httprunner/hrp/internal/boomer"
@@ -45,6 +46,11 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
cfg := testcase.Config.ToStruct()
err = initParameterIterator(cfg, "boomer")
if err != nil {
panic(err)
}
task := b.convertBoomerTask(testcase) task := b.convertBoomerTask(testcase)
taskSlice = append(taskSlice, task) taskSlice = append(taskSlice, task)
} }
@@ -63,9 +69,21 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
testcaseSuccess := true // flag whole testcase result testcaseSuccess := true // flag whole testcase result
var transactionSuccess = true // flag current transaction result var transactionSuccess = true // flag current transaction result
cfg := testcase.Config.ToStruct()
caseConfig := &TConfig{}
// copy config to avoid data racing
if err := copier.Copy(caseConfig, cfg); err != nil {
log.Error().Err(err).Msg("copy config data failed")
}
// iterate through all parameter iterators and update case variables
for _, it := range caseConfig.ParametersSetting.Iterators {
if it.HasNext() {
caseConfig.Variables = mergeVariables(it.Next(), caseConfig.Variables)
}
}
startTime := time.Now() startTime := time.Now()
for index, step := range testcase.TestSteps { for index, step := range testcase.TestSteps {
stepData, err := runner.runStep(index) stepData, err := runner.runStep(index, caseConfig)
if err != nil { if err != nil {
// step failed // step failed
var elapsed int64 var elapsed int64

44
docs/BUILTIN.md Normal file
View File

@@ -0,0 +1,44 @@
# Builtin
## Assertion Methods
### Usage
In "teststeps" of each json/yaml testcase, the "validate" part contains four fields: "check", "assert", "expect" and
"msg", when using assertion methods, method name should be put in "assert" field. The assertion result of "check"
element will be checked out using the regulation you put in "assert" field and compared with the element in "expect"
field.
### Method List
- equals: assert the element to check equals the expected element.
- equal: alias for equals.
- greater_than: assert the element to check is greater than the expected element.
- less_than: assert the element to check is less than the expected element.
- greater_or_equals: assert the element to check is greater than or equal with the expected element.
- less_or_equals: assert the element to check is less than or equal with the expected element.
- not_equal: assert the element to check is not equal with the expected element.
- contained_by: assert the expected element contains the element to check.
- regex_match: assert the element to check matches the expected element using regex.
- type_match: assert the element to check matches the expected element in type.
- startswith: assert the element to check starts with the expected element.
- endswith: assert the element to check ends with the expected element.
- length_equals: assert the length of the element to check is equal with the expected element.
- length_equal: alias for length_equals.
- contains: assert the element to check contains the expected element.
- string_equals: assert the string is equal with the expected string.
## Common Functions
### Usage
The common functions are useful during the variables configuration, you can use "${FUNCTION_NAME}" to call the specific
function to define variables.
### Function List
- get_timestamp: get the thirteen-digit timestamp of current time. (call without argument)
- sleep: sleep n seconds to simulate the thinking time. (call with one argument n)
- gen_random_string: get the n-digit random string. (call with one argument n)
- max: get the maximum of two numbers m and n. (call with two argument m and n)
- md5: get the MD5 of the input string s. (call with one argument s)

View File

@@ -1,5 +1,10 @@
# Release History # Release History
## v0.4.0 (2022-01-05)
- feat: implement `parameterize` mechanism for data driven
- feat: add multiple builtin assertion methods
## v0.3.1 (2021-12-30) ## v0.3.1 (2021-12-30)
- fix: set ulimit to 10240 before load testing - fix: set ulimit to 10240 before load testing

4
examples/account.csv Normal file
View File

@@ -0,0 +1,4 @@
username,password
test1,111111
test2,222222
test3,333333
1 username password
2 test1 111111
3 test2 222222
4 test3 333333

View File

@@ -19,14 +19,18 @@ func TestCaseCallFunction(t *testing.T) {
TestSteps: []hrp.IStep{ TestSteps: []hrp.IStep{
hrp.NewStep("get with params"). hrp.NewStep("get with params").
GET("/get"). GET("/get").
WithParams(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}). WithParams(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}", "foo3": "Foo3"}).
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
Extract(). Extract().
WithJmesPath("body.args.foo1", "varFoo1"). WithJmesPath("body.args.foo1", "varFoo1").
Validate(). Validate().
AssertEqual("status_code", 200, "check status code"). AssertEqual("status_code", 200, "check status code").
AssertLengthEqual("body.args.foo1", 5, "check args foo1"). AssertLengthEqual("body.args.foo1", 5, "check args foo1").
AssertEqual("body.args.foo2", "12.3", "check args foo2"), // notice: request params value will be converted to string AssertEqual("body.args.foo2", "12.3", "check args foo2").
AssertTypeMatch("body.args.foo3", "str", "check args foo3 is type string").
AssertStringEqual("body.args.foo3", "foo3", "check args foo3 case-insensitivity").
AssertContains("body.args.foo3", "Foo", "check contains ").
AssertContainedBy("body.args.foo3", "this is Foo3 test", "check contained by"), // notice: request params value will be converted to string
hrp.NewStep("post json data with functions"). hrp.NewStep("post json data with functions").
POST("/post"). POST("/post").
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).

View File

@@ -0,0 +1,61 @@
{
"config": {
"name": "request methods testcase: validate with parameters",
"parameters": {
"user_agent": [
"iOS/10.1",
"iOS/10.2"
],
"username-password": "${parameterize(examples/account.csv)}"
},
"parameters_setting": {
"strategy": {
"user_agent": "sequential",
"username-password": "random"
},
"iteration": 6
},
"variables": {
"app_version": "v1",
"user_agent": "iOS/10.3"
},
"base_url": "https://postman-echo.com",
"verify": false
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "$username",
"foo2": "$password",
"foo3": "$user_agent"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"foo3": "$foo3"
},
"headers": {
"User-Agent": "$user_agent,$app_version"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
},
{
"check": "body.args.foo3",
"assert": "not_equal",
"expect": "iOS/10.3",
"msg": "check app version"
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
config:
name: "request methods testcase: validate with parameters"
parameters:
user_agent: [ "iOS/10.1", "iOS/10.2" ]
username-password: ${parameterize(examples/account.csv)}
parameters_setting:
strategy:
user_agent: "sequential"
username-password: "random"
iteration: 6
variables:
app_version: v1
user_agent: iOS/10.3
base_url: "https://postman-echo.com"
verify: False
teststeps:
- name: get with params
variables:
foo1: $username
foo2: $password
foo3: $user_agent
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
foo3: $foo3
headers:
User-Agent: $user_agent,$app_version
validate:
- check: status_code
assert: equals
expect: 200
msg: check status code
- check: body.args.foo3
assert: not_equal
expect: iOS/10.3
msg: check app version

View File

@@ -2,6 +2,7 @@ package builtin
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -15,13 +16,20 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual
"greater_or_equals": assert.GreaterOrEqual, "greater_or_equals": assert.GreaterOrEqual,
"less_or_equals": assert.LessOrEqual, "less_or_equals": assert.LessOrEqual,
"not_equal": assert.NotEqual, "not_equal": assert.NotEqual,
"contains": assert.Contains, "contained_by": assert.Contains,
"regex_match": assert.Regexp, "regex_match": assert.Regexp,
"type_match": assert.IsType,
// custom assertions // custom assertions
"startswith": StartsWith, // check if string starts with substring "startswith": StartsWith, // check if string starts with substring
"endswith": EndsWith, // check if string ends with substring "endswith": EndsWith, // check if string ends with substring
"length_equals": EqualLength, "length_equals": EqualLength,
"length_equal": EqualLength, // alias for length_equals "length_equal": EqualLength, // alias for length_equals
"length_less_than": LessThanLength,
"length_less_or_equals": LessOrEqualsLength,
"length_greater_than": GreaterThanLength,
"length_greater_or_equals": GreaterOrEqualsLength,
"contains": Contains,
"string_equals": EqualString,
} }
func StartsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { func StartsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
@@ -57,6 +65,66 @@ func EqualLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...
return assert.Len(t, actual, length, msgAndArgs...) return assert.Len(t, actual, length, msgAndArgs...)
} }
func GreaterThanLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l <= length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func GreaterOrEqualsLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l < length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func LessThanLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l >= length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func LessOrEqualsLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
ok, l := getLen(actual)
if !ok {
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
}
if l > length {
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
}
return true
}
func convertInt(value interface{}) (int, error) { func convertInt(value interface{}) (int, error) {
switch v := value.(type) { switch v := value.(type) {
case int: case int:
@@ -83,3 +151,32 @@ func convertInt(value interface{}) (int, error) {
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v) return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
} }
} }
// Contains assert whether actual element contains expected element
func Contains(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
return assert.Contains(t, actual, expected, msgAndArgs)
}
func EqualString(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if !assert.IsType(t, "string", actual, msgAndArgs) {
return false
}
if !assert.IsType(t, "string", expected, msgAndArgs) {
return false
}
actualString := actual.(string)
expectedString := expected.(string)
return assert.True(t, strings.EqualFold(actualString, expectedString), msgAndArgs)
}
// getLen try to get length of object.
// return (false, 0) if impossible.
func getLen(x interface{}) (ok bool, length int) {
v := reflect.ValueOf(x)
defer func() {
if e := recover(); e != nil {
ok = false
}
}()
return true, v.Len()
}

View File

@@ -61,3 +61,80 @@ func TestEqualLength(t *testing.T) {
} }
} }
} }
func TestLessThanLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"", 1},
{[]string{}, 1},
{map[string]interface{}{}, 1},
{"a", 2},
{[]string{"a"}, 2},
{map[string]interface{}{"a": 123}, 2},
}
for _, data := range testData {
if !assert.True(t, LessThanLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestLessOrEqualsLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"", 1},
{[]string{}, 1},
{map[string]interface{}{"A": 111}, 1},
{"a", 1},
{[]string{"a"}, 2},
{map[string]interface{}{"a": 123}, 2},
}
for _, data := range testData {
if !assert.True(t, LessOrEqualsLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestGreaterThanLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"abcd", 3},
{[]string{"a", "b", "c"}, 2},
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
}
for _, data := range testData {
if !assert.True(t, GreaterThanLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestGreaterOrEqualsLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"abcd", 3},
{[]string{"w"}, 1},
{map[string]interface{}{"A": 111}, 1},
{"a", 1},
{[]string{"a", "b", "c"}, 2},
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
}
for _, data := range testData {
if !assert.True(t, GreaterOrEqualsLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}

View File

@@ -2,10 +2,16 @@ package builtin
import ( import (
"crypto/md5" "crypto/md5"
"encoding/csv"
"encoding/hex" "encoding/hex"
"io/ioutil"
"math" "math"
"math/rand" "math/rand"
"path/filepath"
"strings"
"time" "time"
"github.com/rs/zerolog/log"
) )
var Functions = map[string]interface{}{ var Functions = map[string]interface{}{
@@ -13,7 +19,9 @@ var Functions = map[string]interface{}{
"sleep": sleep, // call with one argument "sleep": sleep, // call with one argument
"gen_random_string": genRandomString, // call with one argument "gen_random_string": genRandomString, // call with one argument
"max": math.Max, // call with two arguments "max": math.Max, // call with two arguments
"md5": MD5, "md5": MD5, // call with one argument
"parameterize": loadFromCSV,
"P": loadFromCSV,
} }
func init() { func init() {
@@ -44,3 +52,33 @@ func MD5(str string) string {
hasher.Write([]byte(str)) hasher.Write([]byte(str))
return hex.EncodeToString(hasher.Sum(nil)) return hex.EncodeToString(hasher.Sum(nil))
} }
func loadFromCSV(path string) []map[string]interface{} {
path, err := filepath.Abs(path)
if err != nil {
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
panic(err)
}
log.Info().Str("path", path).Msg("load csv file")
file, err := ioutil.ReadFile(path)
if err != nil {
log.Error().Err(err).Msg("load csv file failed")
panic(err)
}
r := csv.NewReader(strings.NewReader(string(file)))
content, err := r.ReadAll()
if err != nil {
log.Error().Err(err).Msg("parse csv file failed")
panic(err)
}
var result []map[string]interface{}
for i := 1; i < len(content); i++ {
row := make(map[string]interface{})
for j := 0; j < len(content[i]); j++ {
row[content[0][j]] = content[i][j]
}
result = append(result, row)
}
return result
}

View File

@@ -1,5 +1,11 @@
package hrp package hrp
import (
"math/rand"
"sync"
"time"
)
const ( const (
httpGET string = "GET" httpGET string = "GET"
httpHEAD string = "HEAD" httpHEAD string = "HEAD"
@@ -13,13 +19,68 @@ const (
// TConfig represents config data structure for testcase. // TConfig represents config data structure for testcase.
// Each testcase should contain one config part. // Each testcase should contain one config part.
type TConfig struct { type TConfig struct {
Name string `json:"name" yaml:"name"` // required Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"` Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Export []string `json:"export,omitempty" yaml:"export,omitempty"` ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
}
type TParamsConfig struct {
Strategy interface{} `json:"strategy,omitempty" yaml:"strategy,omitempty"`
Iteration int `json:"iteration,omitempty" yaml:"iteration,omitempty"`
Iterators []*Iterator `json:"parameterIterator,omitempty" yaml:"parameterIterator,omitempty"` //保存参数的迭代器
}
const (
strategyRandom string = "random"
strategySequential string = "Sequential"
)
type paramsType []map[string]interface{}
type Iterator struct {
sync.Mutex
data paramsType
strategy string // random, sequential
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.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
} }
// Request represents HTTP request data structure. // Request represents HTTP request data structure.

178
parser.go
View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/maja42/goval" "github.com/maja42/goval"
"github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/builtin"
@@ -255,7 +256,6 @@ func callFunc(funcName string, arguments ...interface{}) (interface{}, error) {
// function not found // function not found
return nil, fmt.Errorf("function %s is not found", funcName) return nil, fmt.Errorf("function %s is not found", funcName)
} }
funcValue := reflect.ValueOf(function) funcValue := reflect.ValueOf(function)
if funcValue.Kind() != reflect.Func { if funcValue.Kind() != reflect.Func {
// function not valid // function not valid
@@ -493,3 +493,179 @@ func findallVariables(raw string) variableSet {
return varSet return varSet
} }
func genCartesianProduct(paramsMap map[string]paramsType) paramsType {
if len(paramsMap) == 0 {
return nil
}
var params []paramsType
for _, v := range paramsMap {
params = append(params, v)
}
var cartesianProduct paramsType
cartesianProduct = params[0]
for i := 0; i < len(params)-1; i++ {
var tempProduct paramsType
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]paramsType, error) {
if len(parameters) == 0 {
return nil, nil
}
parsedParametersSlice := make(map[string]paramsType)
var err error
for k, v := range parameters {
var parameterSlice paramsType
rawValue := reflect.ValueOf(v)
switch rawValue.Kind() {
case reflect.String:
// e.g. username-password: ${parameterize(examples/account.csv)} -> [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}]
var parsedParameterContent interface{}
parsedParameterContent, err = 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]paramsType
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, rawValue.MapIndex(reflect.ValueOf(k)).Interface().(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 = strings.ToLower(rawValue.String())
}
cfg.ParametersSetting.Iterators = append(
cfg.ParametersSetting.Iterators,
newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(string), 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.(string), cfg.ParametersSetting.Iteration),
)
}
return nil
}
func newIterator(parameters paramsType, strategy string, 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

@@ -618,3 +618,133 @@ func TestFindallVariables(t *testing.T) {
} }
} }
} }
func TestParseParameters(t *testing.T) {
testData := []struct {
rawVars map[string]interface{}
expectLength int
}{
{
map[string]interface{}{
"username-password": "${parameterize(examples/account.csv)}",
"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": "${parameterize(examples/account.csv)}",
"user_agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{
"username-password": "${parameterize(examples/account.csv)}",
"user-agent": []interface{}{"IOS/10.1", "IOS/10.2"}},
},
{
map[string]interface{}{
"username-password": "${param(examples/account.csv)}",
"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

@@ -98,10 +98,26 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed") log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed")
return err return err
} }
if err := r.newCaseRunner(testcase).run(); err != nil { cfg := testcase.Config.ToStruct()
log.Error().Err(err).Msg("[Run] run testcase failed") // 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 return err
} }
// 在runner模式下指定整体策略cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器
for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); {
// iterate through all parameter iterators and update case variables
for _, it := range cfg.ParametersSetting.Iterators {
if it.HasNext() {
cfg.Variables = mergeVariables(it.Next(), cfg.Variables)
}
}
if err := r.newCaseRunner(testcase).run(); err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
return err
}
}
} }
return nil return nil
} }
@@ -141,16 +157,16 @@ func (r *caseRunner) run() error {
if err := r.parseConfig(config); err != nil { if err := r.parseConfig(config); err != nil {
return err return err
} }
cfg := config.ToStruct()
log.Info().Str("testcase", config.Name()).Msg("run testcase start") log.Info().Str("testcase", config.Name()).Msg("run testcase start")
r.startTime = time.Now() r.startTime = time.Now()
for index := range r.TestCase.TestSteps { for index := range r.TestCase.TestSteps {
_, err := r.runStep(index) _, err := r.runStep(index, cfg)
if err != nil { if err != nil {
if r.hrpRunner.failfast { if r.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting") return errors.Wrap(err, "abort running due to failfast setting")
} }
log.Warn().Err(err).Msg("run step failed, continue next step")
} }
} }
@@ -158,8 +174,7 @@ func (r *caseRunner) run() error {
return nil return nil
} }
func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) { func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
config := r.TestCase.Config
step := r.TestCase.TestSteps[index] step := r.TestCase.TestSteps[index]
// step type priority order: transaction > rendezvous > testcase > request // step type priority order: transaction > rendezvous > testcase > request
@@ -179,23 +194,18 @@ func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
log.Error().Err(err).Msg("copy step data failed") log.Error().Err(err).Msg("copy step data failed")
return nil, err return nil, err
} }
copiedConfig := &TConfig{}
if err = copier.Copy(copiedConfig, config.ToStruct()); err != nil {
log.Error().Err(err).Msg("copy config data failed")
return nil, err
}
stepVariables := copiedStep.Variables stepVariables := copiedStep.Variables
// override variables // override variables
// step variables > session variables (extracted variables from previous steps) // step variables > session variables (extracted variables from previous steps)
stepVariables = mergeVariables(stepVariables, r.sessionVariables) stepVariables = mergeVariables(stepVariables, r.sessionVariables)
// step variables > testcase config variables // step variables > testcase config variables
stepVariables = mergeVariables(stepVariables, copiedConfig.Variables) stepVariables = mergeVariables(stepVariables, caseConfig.Variables)
// parse step variables // parse step variables
parsedVariables, err := parseVariables(stepVariables) parsedVariables, err := parseVariables(stepVariables)
if err != nil { if err != nil {
log.Error().Interface("variables", copiedConfig.Variables).Err(err).Msg("parse step variables failed") log.Error().Interface("variables", caseConfig.Variables).Err(err).Msg("parse step variables failed")
return nil, err return nil, err
} }
copiedStep.Variables = parsedVariables // avoid data racing copiedStep.Variables = parsedVariables // avoid data racing
@@ -212,7 +222,7 @@ func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
} }
} else { } else {
// run request // run request
copiedStep.Request.URL = buildURL(copiedConfig.BaseURL, copiedStep.Request.URL) // avoid data racing copiedStep.Request.URL = buildURL(caseConfig.BaseURL, copiedStep.Request.URL) // avoid data racing
stepResult, err = r.runStepRequest(copiedStep) stepResult, err = r.runStepRequest(copiedStep)
if err != nil { if err != nil {
log.Error().Err(err).Msg("run request step failed") log.Error().Err(err).Msg("run request step failed")
@@ -484,7 +494,6 @@ func (r *caseRunner) parseConfig(config IConfig) error {
return err return err
} }
cfg.Variables = parsedVariables cfg.Variables = parsedVariables
// parse config name // parse config name
parsedName, err := parseString(cfg.Name, cfg.Variables) parsedName, err := parseString(cfg.Name, cfg.Variables)
if err != nil { if err != nil {

View File

@@ -79,10 +79,10 @@ func TestRunRequestRun(t *testing.T) {
TestSteps: []IStep{stepGET, stepPOSTData}, TestSteps: []IStep{stepGET, stepPOSTData},
} }
runner := NewRunner(t).SetDebug(true).newCaseRunner(testcase) runner := NewRunner(t).SetDebug(true).newCaseRunner(testcase)
if _, err := runner.runStep(0); err != nil { if _, err := runner.runStep(0, testcase.Config.ToStruct()); err != nil {
t.Fatalf("tStep.Run() error: %s", err) t.Fatalf("tStep.Run() error: %s", err)
} }
if _, err := runner.runStep(1); err != nil { if _, err := runner.runStep(1, testcase.Config.ToStruct()); err != nil {
t.Fatalf("tStepPOSTData.Run() error: %s", err) t.Fatalf("tStepPOSTData.Run() error: %s", err)
} }
} }

View File

@@ -35,6 +35,94 @@ func (s *StepRequestValidation) AssertEqual(jmesPath string, expected interface{
return s return s
} }
func (s *StepRequestValidation) AssertGreater(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "greater_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLess(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "less_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertGreaterOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "greater_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLessOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "less_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertNotEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "not_equal",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "contains",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "type_match",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertRegexp(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "regex_match",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertStartsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation { func (s *StepRequestValidation) AssertStartsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{ v := Validator{
Check: jmesPath, Check: jmesPath,
@@ -67,3 +155,70 @@ func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected inte
s.step.Validators = append(s.step.Validators, v) s.step.Validators = append(s.step.Validators, v)
return s return s
} }
func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "contained_by",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthLessThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_less_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "string_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthLessOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_less_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthGreaterThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_greater_than",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepRequestValidation) AssertLengthGreaterOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{
Check: jmesPath,
Assert: "length_greater_or_equals",
Expect: expected,
Message: msg,
}
s.step.Validators = append(s.step.Validators, v)
return s
}