mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
pull main
This commit is contained in:
20
boomer.go
20
boomer.go
@@ -3,6 +3,7 @@ package hrp
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/hrp/internal/boomer"
|
||||
@@ -45,6 +46,11 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg := testcase.Config.ToStruct()
|
||||
err = initParameterIterator(cfg, "boomer")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
task := b.convertBoomerTask(testcase)
|
||||
taskSlice = append(taskSlice, task)
|
||||
}
|
||||
@@ -63,9 +69,21 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
|
||||
testcaseSuccess := true // flag whole testcase result
|
||||
var transactionSuccess = true // flag current transaction result
|
||||
|
||||
cfg := testcase.Config.ToStruct()
|
||||
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()
|
||||
for index, step := range testcase.TestSteps {
|
||||
stepData, err := runner.runStep(index)
|
||||
stepData, err := runner.runStep(index, caseConfig)
|
||||
if err != nil {
|
||||
// step failed
|
||||
var elapsed int64
|
||||
|
||||
44
docs/BUILTIN.md
Normal file
44
docs/BUILTIN.md
Normal 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)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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)
|
||||
|
||||
- fix: set ulimit to 10240 before load testing
|
||||
|
||||
4
examples/account.csv
Normal file
4
examples/account.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
username,password
|
||||
test1,111111
|
||||
test2,222222
|
||||
test3,333333
|
||||
|
@@ -19,14 +19,18 @@ func TestCaseCallFunction(t *testing.T) {
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
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"}).
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
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").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
|
||||
61
examples/parameters_test.json
Normal file
61
examples/parameters_test.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
40
examples/parameters_test.yaml
Normal file
40
examples/parameters_test.yaml
Normal 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
|
||||
@@ -2,6 +2,7 @@ package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"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,
|
||||
"less_or_equals": assert.LessOrEqual,
|
||||
"not_equal": assert.NotEqual,
|
||||
"contains": assert.Contains,
|
||||
"contained_by": assert.Contains,
|
||||
"regex_match": assert.Regexp,
|
||||
"type_match": assert.IsType,
|
||||
// custom assertions
|
||||
"startswith": StartsWith, // check if string starts with substring
|
||||
"endswith": EndsWith, // check if string ends with substring
|
||||
"length_equals": EqualLength,
|
||||
"length_equal": EqualLength, // alias for length_equals
|
||||
"startswith": StartsWith, // check if string starts with substring
|
||||
"endswith": EndsWith, // check if string ends with substring
|
||||
"length_equals": EqualLength,
|
||||
"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 {
|
||||
@@ -57,6 +65,66 @@ func EqualLength(t assert.TestingT, expected, actual interface{}, 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) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
@@ -83,3 +151,32 @@ func convertInt(value interface{}) (int, error) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package builtin
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Functions = map[string]interface{}{
|
||||
@@ -13,7 +19,9 @@ var Functions = map[string]interface{}{
|
||||
"sleep": sleep, // call with one argument
|
||||
"gen_random_string": genRandomString, // call with one argument
|
||||
"max": math.Max, // call with two arguments
|
||||
"md5": MD5,
|
||||
"md5": MD5, // call with one argument
|
||||
"parameterize": loadFromCSV,
|
||||
"P": loadFromCSV,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -44,3 +52,33 @@ func MD5(str string) string {
|
||||
hasher.Write([]byte(str))
|
||||
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
|
||||
}
|
||||
|
||||
75
models.go
75
models.go
@@ -1,5 +1,11 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
httpGET string = "GET"
|
||||
httpHEAD string = "HEAD"
|
||||
@@ -13,13 +19,68 @@ const (
|
||||
// TConfig represents config data structure for testcase.
|
||||
// Each testcase should contain one config part.
|
||||
type TConfig struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,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.
|
||||
|
||||
178
parser.go
178
parser.go
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/maja42/goval"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/hrp/internal/builtin"
|
||||
@@ -255,7 +256,6 @@ func callFunc(funcName string, arguments ...interface{}) (interface{}, error) {
|
||||
// function not found
|
||||
return nil, fmt.Errorf("function %s is not found", funcName)
|
||||
}
|
||||
|
||||
funcValue := reflect.ValueOf(function)
|
||||
if funcValue.Kind() != reflect.Func {
|
||||
// function not valid
|
||||
@@ -493,3 +493,179 @@ func findallVariables(raw string) variableSet {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
130
parser_test.go
130
parser_test.go
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
runner.go
41
runner.go
@@ -98,10 +98,26 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
|
||||
log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed")
|
||||
return err
|
||||
}
|
||||
if err := r.newCaseRunner(testcase).run(); err != nil {
|
||||
log.Error().Err(err).Msg("[Run] run testcase failed")
|
||||
cfg := testcase.Config.ToStruct()
|
||||
// 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
|
||||
}
|
||||
// 在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
|
||||
}
|
||||
@@ -141,16 +157,16 @@ func (r *caseRunner) run() error {
|
||||
if err := r.parseConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := config.ToStruct()
|
||||
log.Info().Str("testcase", config.Name()).Msg("run testcase start")
|
||||
|
||||
r.startTime = time.Now()
|
||||
for index := range r.TestCase.TestSteps {
|
||||
_, err := r.runStep(index)
|
||||
_, err := r.runStep(index, cfg)
|
||||
if err != nil {
|
||||
if r.hrpRunner.failfast {
|
||||
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
|
||||
}
|
||||
|
||||
func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
|
||||
config := r.TestCase.Config
|
||||
func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
|
||||
step := r.TestCase.TestSteps[index]
|
||||
|
||||
// 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")
|
||||
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
|
||||
// override variables
|
||||
// step variables > session variables (extracted variables from previous steps)
|
||||
stepVariables = mergeVariables(stepVariables, r.sessionVariables)
|
||||
// step variables > testcase config variables
|
||||
stepVariables = mergeVariables(stepVariables, copiedConfig.Variables)
|
||||
stepVariables = mergeVariables(stepVariables, caseConfig.Variables)
|
||||
|
||||
// parse step variables
|
||||
parsedVariables, err := parseVariables(stepVariables)
|
||||
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
|
||||
}
|
||||
copiedStep.Variables = parsedVariables // avoid data racing
|
||||
@@ -212,7 +222,7 @@ func (r *caseRunner) runStep(index int) (stepResult *stepData, err error) {
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("run request step failed")
|
||||
@@ -484,7 +494,6 @@ func (r *caseRunner) parseConfig(config IConfig) error {
|
||||
return err
|
||||
}
|
||||
cfg.Variables = parsedVariables
|
||||
|
||||
// parse config name
|
||||
parsedName, err := parseString(cfg.Name, cfg.Variables)
|
||||
if err != nil {
|
||||
|
||||
@@ -79,10 +79,10 @@ func TestRunRequestRun(t *testing.T) {
|
||||
TestSteps: []IStep{stepGET, stepPOSTData},
|
||||
}
|
||||
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)
|
||||
}
|
||||
if _, err := runner.runStep(1); err != nil {
|
||||
if _, err := runner.runStep(1, testcase.Config.ToStruct()); err != nil {
|
||||
t.Fatalf("tStepPOSTData.Run() error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
155
validate.go
155
validate.go
@@ -35,6 +35,94 @@ func (s *StepRequestValidation) AssertEqual(jmesPath string, expected interface{
|
||||
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 {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
@@ -67,3 +155,70 @@ func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected inte
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user