mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 08:59:44 +08:00
Merge pull request #1248 from httprunner/refactor-parameters
refactor parameters iterator
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## hrp
|
||||
|
||||
One-stop solution for HTTP(S) testing.
|
||||
Next-Generation API Testing Solution.
|
||||
|
||||
### Synopsis
|
||||
|
||||
@@ -12,12 +12,13 @@ One-stop solution for HTTP(S) testing.
|
||||
██║ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║██║ ╚████║███████╗██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
||||
|
||||
hrp (HttpRunner+) aims to be a one-stop solution for HTTP(S) testing, covering API testing,
|
||||
load testing and digital experience monitoring (DEM). Enjoy! ✨ 🚀 ✨
|
||||
HttpRunner is an open source API testing tool that supports HTTP(S)/HTTP2/WebSocket/RPC
|
||||
network protocols, covering API testing, performance testing and digital experience
|
||||
monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
|
||||
|
||||
License: Apache-2.0
|
||||
Website: https://httprunner.com
|
||||
Github: https://github.com/httprunner/httprunner/hrp
|
||||
Github: https://github.com/httprunner/httprunner
|
||||
Copyright 2021 debugtalk
|
||||
|
||||
### Options
|
||||
@@ -30,7 +31,8 @@ Copyright 2021 debugtalk
|
||||
|
||||
* [hrp boom](hrp_boom.md) - run load test with boomer
|
||||
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
|
||||
* [hrp run](hrp_run.md) - run API test
|
||||
* [hrp pytest](hrp_pytest.md) - run API test with pytest
|
||||
* [hrp run](hrp_run.md) - run API test with go engine
|
||||
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
||||
|
||||
###### Auto generated by spf13/cobra on 26-Mar-2022
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
|
||||
@@ -39,6 +39,6 @@ hrp boom [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||
|
||||
###### Auto generated by spf13/cobra on 26-Mar-2022
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
|
||||
@@ -22,6 +22,6 @@ hrp har2case $har_path... [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||
|
||||
###### Auto generated by spf13/cobra on 26-Mar-2022
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
|
||||
19
docs/cmd/hrp_pytest.md
Normal file
19
docs/cmd/hrp_pytest.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## hrp pytest
|
||||
|
||||
run API test with pytest
|
||||
|
||||
```
|
||||
hrp pytest $path ... [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for pytest
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
@@ -1,6 +1,6 @@
|
||||
## hrp run
|
||||
|
||||
run API test
|
||||
run API test with go engine
|
||||
|
||||
### Synopsis
|
||||
|
||||
@@ -32,6 +32,6 @@ hrp run $path... [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||
|
||||
###### Auto generated by spf13/cobra on 26-Mar-2022
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
|
||||
@@ -17,6 +17,6 @@ hrp startproject $project_name [flags]
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||
|
||||
###### Auto generated by spf13/cobra on 26-Mar-2022
|
||||
###### Auto generated by spf13/cobra on 17-Apr-2022
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
"iOS/10.1",
|
||||
"iOS/10.2"
|
||||
],
|
||||
"username-password": "${parameterize(examples/hrp/account.csv)}"
|
||||
"username-password": "${parameterize($file)}"
|
||||
},
|
||||
"parameters_setting": {
|
||||
"strategy": {
|
||||
"strategies": {
|
||||
"user_agent": "sequential",
|
||||
"username-password": "random"
|
||||
},
|
||||
"iteration": 6
|
||||
"limit": 6
|
||||
},
|
||||
"variables": {
|
||||
"app_version": "v1",
|
||||
"user_agent": "iOS/10.3"
|
||||
"user_agent": "iOS/10.3",
|
||||
"file": "examples/hrp/account.csv"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": false
|
||||
|
||||
@@ -2,15 +2,16 @@ config:
|
||||
name: "request methods testcase: validate with parameters"
|
||||
parameters:
|
||||
user_agent: [ "iOS/10.1", "iOS/10.2" ]
|
||||
username-password: ${parameterize(examples/hrp/account.csv)}
|
||||
username-password: ${parameterize($file)}
|
||||
parameters_setting:
|
||||
strategy:
|
||||
strategies:
|
||||
user_agent: "sequential"
|
||||
username-password: "random"
|
||||
iteration: 6
|
||||
limit: 6
|
||||
variables:
|
||||
app_version: v1
|
||||
user_agent: iOS/10.3
|
||||
file: examples/hrp/account.csv
|
||||
base_url: "https://postman-echo.com"
|
||||
verify: False
|
||||
|
||||
|
||||
@@ -72,18 +72,18 @@ func (b *HRPBoomer) Quit() {
|
||||
}
|
||||
|
||||
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
|
||||
// init session runner for testcase
|
||||
sessionRunner, err := b.hrpRunner.NewSessionRunner(testcase)
|
||||
// init runner for testcase
|
||||
// this runner is shared by multiple session runners
|
||||
caseRunner, err := b.hrpRunner.newCaseRunner(testcase)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create session runner")
|
||||
log.Error().Err(err).Msg("failed to create runner")
|
||||
os.Exit(1)
|
||||
}
|
||||
if sessionRunner.parser.plugin != nil {
|
||||
if caseRunner.parser.plugin != nil {
|
||||
b.pluginsMutex.Lock()
|
||||
b.plugins = append(b.plugins, sessionRunner.parser.plugin)
|
||||
b.plugins = append(b.plugins, caseRunner.parser.plugin)
|
||||
b.pluginsMutex.Unlock()
|
||||
}
|
||||
sessionRunner.resetSession()
|
||||
|
||||
// broadcast to all rendezvous at once when spawn done
|
||||
go func() {
|
||||
@@ -93,6 +93,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
}
|
||||
}()
|
||||
|
||||
// set paramters mode for load testing
|
||||
parametersIterator := caseRunner.parametersIterator
|
||||
parametersIterator.SetUnlimitedMode()
|
||||
|
||||
return &boomer.Task{
|
||||
Name: testcase.Config.Name,
|
||||
Weight: testcase.Config.Weight,
|
||||
@@ -100,14 +104,12 @@ 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()
|
||||
}
|
||||
// init session runner
|
||||
sessionRunner := caseRunner.newSession()
|
||||
|
||||
if parametersIterator.HasNext() {
|
||||
sessionRunner.updateConfigVariables(parametersIterator.Next())
|
||||
}
|
||||
sessionRunner.updateConfigVariables(parameterVariables)
|
||||
|
||||
startTime := time.Now()
|
||||
for _, step := range testcase.TestSteps {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "hrp",
|
||||
Short: "One-stop solution for API testing.",
|
||||
Short: "Next-Generation API Testing Solution.",
|
||||
Long: `
|
||||
██╗ ██╗████████╗████████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗███╗ ██╗███████╗██████╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║████╗ ██║██╔════╝██╔══██╗
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -109,6 +109,6 @@ func TestLoopCount(t *testing.T) {
|
||||
go runner.start()
|
||||
<-runner.stopChan
|
||||
if !assert.Equal(t, runner.loop.loopCount, atomic.LoadInt64(&runner.loop.finishedCount)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestSerializeStats(t *testing.T) {
|
||||
first := serialized[0]
|
||||
entry, err := deserializeStatsEntry(first)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if entry.Name != "success" {
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestStartsWith(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StartsWith(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestEndsWith(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EndsWith(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestEqualLength(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EqualLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func TestLessThanLength(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessThanLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func TestLessOrEqualsLength(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func TestGreaterThanLength(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterThanLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func TestGreaterOrEqualsLength(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ func TestContainedBy(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, ContainedBy(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func TestStringEqual(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StringEqual(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func TestRegexMatch(t *testing.T) {
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, RegexMatch(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,11 +270,12 @@ func loadFromCSV(path string) []map[string]interface{} {
|
||||
log.Error().Err(err).Msg("parse csv file failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
firstLine := content[0] // parameter names
|
||||
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]
|
||||
row[firstLine[j]] = content[i][j]
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@ var (
|
||||
func TestGenJSON(t *testing.T) {
|
||||
jsonPath, err := NewHAR(harPath).GenJSON()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.NotEmpty(t, jsonPath) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenYAML(t *testing.T) {
|
||||
yamlPath, err := NewHAR(harPath2).GenYAML()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.NotEmpty(t, yamlPath) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ func TestLoadHAR(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
h, err := har.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "GET", h.Log.Entries[0].Request.Method) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "POST", h.Log.Entries[1].Request.Method) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,18 +53,18 @@ func TestLoadHARWithProfile(t *testing.T) {
|
||||
har.SetProfile(profilePath)
|
||||
_, err := har.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t,
|
||||
map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
har.profile["headers"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t,
|
||||
map[string]interface{}{"UserName": "debugtalk"},
|
||||
har.profile["cookies"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,73 +72,73 @@ func TestMakeTestCase(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
tCase, err := har.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request method
|
||||
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request url
|
||||
if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request params
|
||||
if !assert.Equal(t, "HDnY8", tCase.TestSteps[0].Request.Params["foo1"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request cookies
|
||||
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Cookies["sails.sid"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request headers
|
||||
if !assert.Equal(t, "HttpRunnerPlus", tCase.TestSteps[0].Request.Headers["User-Agent"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "postman-echo.com", tCase.TestSteps[0].Request.Headers["Host"]) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make request data
|
||||
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// make validators
|
||||
validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "status_code", validator.Check) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "body.url", validator.Check) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFilenameWithoutExtension(t *testing.T) {
|
||||
filename := getFilenameWithoutExtension(harPath2)
|
||||
if !assert.Equal(t, "postman-echo", filename) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ func TestMakeRequestHeaders(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}, step.Request.Headers) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,13 +177,13 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}, step.Request.Headers) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,14 +200,14 @@ func TestMakeRequestCookies(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"abc": "123",
|
||||
"UserName": "leolee",
|
||||
}, step.Request.Cookies) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,13 +225,13 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]string{
|
||||
"UserName": "debugtalk",
|
||||
}, step.Request.Cookies) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,11 +251,11 @@ func TestMakeRequestDataParams(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, "a=1&b=2", step.Request.Body) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,11 +272,11 @@ func TestMakeRequestDataJSON(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, map[string]interface{}{"a": "1", "b": "2"}, step.Request.Body) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,11 +293,11 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, nil, step.Request.Body) { // TODO
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +320,11 @@ func TestMakeValidate(t *testing.T) {
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
validator, ok := step.Validators[0].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
@@ -332,12 +332,12 @@ func TestMakeValidate(t *testing.T) {
|
||||
Expect: 200,
|
||||
Assert: "equals",
|
||||
Message: "assert response status code"}) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
validator, ok = step.Validators[1].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
@@ -345,12 +345,12 @@ func TestMakeValidate(t *testing.T) {
|
||||
Expect: "application/json; charset=utf-8",
|
||||
Assert: "equals",
|
||||
Message: "assert response header Content-Type"}) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
validator, ok = step.Validators[2].(hrp.Validator)
|
||||
if !ok {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, validator,
|
||||
hrp.Validator{
|
||||
@@ -358,6 +358,6 @@ func TestMakeValidate(t *testing.T) {
|
||||
Expect: float64(200), // TODO
|
||||
Assert: "equals",
|
||||
Message: "assert response body Code"}) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,20 @@ func TestGenDemoExamples(t *testing.T) {
|
||||
os.RemoveAll(dir)
|
||||
err := CreateScaffold(dir, Go)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
dir = "../../../examples/demo-with-py-plugin"
|
||||
os.RemoveAll(dir)
|
||||
err = CreateScaffold(dir, Py)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
dir = "../../../examples/demo-without-plugin"
|
||||
os.RemoveAll(dir)
|
||||
err = CreateScaffold(dir, Ignore)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
6
hrp/internal/scaffold/templates/pytest.ini
Normal file
6
hrp/internal/scaffold/templates/pytest.ini
Normal file
@@ -0,0 +1,6 @@
|
||||
[pytest]
|
||||
addopts = -s
|
||||
# https://docs.pytest.org/en/latest/how-to/output.html
|
||||
junit_logging = all
|
||||
junit_duration_report = total
|
||||
log_cli = False
|
||||
@@ -25,6 +25,6 @@ func TestStructToUrlValues(t *testing.T) {
|
||||
}
|
||||
val := structToUrlValues(event)
|
||||
if val.Encode() != "ea=convert&ec=unittest&el=v0.3.0&ev=123" {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
365
hrp/parameters.go
Normal file
365
hrp/parameters.go
Normal file
@@ -0,0 +1,365 @@
|
||||
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"` // individual strategies for each parameters
|
||||
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 {
|
||||
if config == nil {
|
||||
config = &TParamsConfig{}
|
||||
}
|
||||
iterator := &ParametersIterator{
|
||||
data: parameters,
|
||||
hasNext: true,
|
||||
sequentialParameters: nil,
|
||||
randomParameterNames: nil,
|
||||
limit: config.Limit,
|
||||
index: 0,
|
||||
}
|
||||
|
||||
if len(parameters) == 0 {
|
||||
iterator.data = map[string]Parameters{}
|
||||
iterator.limit = 1
|
||||
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 {
|
||||
log.Warn().Msg("parameters unlimited mode is only supported for load testing")
|
||||
iterator.limit = 0
|
||||
}
|
||||
if iterator.limit == 0 {
|
||||
// limit not set
|
||||
if len(iterator.sequentialParameters) > 0 {
|
||||
// use cartesian product of sequential parameters size as limit
|
||||
iterator.limit = len(iterator.sequentialParameters)
|
||||
} else {
|
||||
// all parameters are selected by random
|
||||
// only run once
|
||||
iterator.limit = 1
|
||||
}
|
||||
} else { // limit > 0
|
||||
log.Info().Int("limit", iterator.limit).Msg("set limit for parameters")
|
||||
}
|
||||
|
||||
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 // limit count for iteration
|
||||
index int // current iteration index
|
||||
}
|
||||
|
||||
// SetUnlimitedMode is used for load testing
|
||||
func (iter *ParametersIterator) SetUnlimitedMode() {
|
||||
log.Info().Msg("set parameters unlimited mode")
|
||||
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
|
||||
}
|
||||
|
||||
var selectedParameters map[string]interface{}
|
||||
if len(iter.sequentialParameters) == 0 {
|
||||
selectedParameters = make(map[string]interface{})
|
||||
} else if iter.index < len(iter.sequentialParameters) {
|
||||
selectedParameters = iter.sequentialParameters[iter.index]
|
||||
} else {
|
||||
// loop back to the first sequential parameter
|
||||
index := iter.index % len(iter.sequentialParameters)
|
||||
selectedParameters = iter.sequentialParameters[index]
|
||||
}
|
||||
|
||||
// merge with random parameters
|
||||
for _, paramName := range iter.randomParameterNames {
|
||||
randSource := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
randIndex := randSource.Intn(len(iter.data[paramName]))
|
||||
for k, v := range iter.data[paramName][randIndex] {
|
||||
selectedParameters[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
516
hrp/parameters_test.go
Normal file
516
hrp/parameters_test.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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 TestInitParametersIteratorCount(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"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
expectLimit int
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
// default equals to set overall parameters strategy to "sequential"
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategy: "sequential",
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
// default equals to set each individual parameters strategy to "sequential"
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": "sequential",
|
||||
"user_agent": "sequential",
|
||||
"app_version": "sequential",
|
||||
},
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"user_agent": "sequential",
|
||||
"app_version": "sequential",
|
||||
},
|
||||
},
|
||||
},
|
||||
6, // 3 * 2 * 1
|
||||
},
|
||||
|
||||
// set overall parameters overall strategy to "random"
|
||||
// each random parameters only select one item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategy: "random",
|
||||
},
|
||||
},
|
||||
1, // 1 * 1 * 1
|
||||
},
|
||||
// set some individual parameters strategy to "random"
|
||||
// this will override overall strategy
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"user_agent": "random",
|
||||
},
|
||||
},
|
||||
},
|
||||
3, // 3 * 1 * 1
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": "random",
|
||||
},
|
||||
},
|
||||
},
|
||||
2, // 1 * 2 * 1
|
||||
},
|
||||
|
||||
// set limit for parameters
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters, // total: 6 = 3 * 2 * 1
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 4, // limit could be less than total
|
||||
},
|
||||
},
|
||||
4,
|
||||
},
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters, // total: 6 = 3 * 2 * 1
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 9, // limit could also be greater than total
|
||||
},
|
||||
},
|
||||
9,
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
1,
|
||||
},
|
||||
}
|
||||
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()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
// should not have next
|
||||
if !assert.False(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitParametersIteratorUnlimitedCount(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"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
ParametersSetting: &TParamsConfig{},
|
||||
},
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, data := range testData {
|
||||
iterator, err := initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
// set unlimited mode
|
||||
iterator.SetUnlimitedMode()
|
||||
if !assert.Equal(t, -1, iterator.limit) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
if !assert.Equal(t, 100, iterator.index) {
|
||||
t.Fatal()
|
||||
}
|
||||
// should also have next
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitParametersIteratorContent(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"}, // 2
|
||||
"app_version": []interface{}{4.0}, // 1
|
||||
}
|
||||
testData := []struct {
|
||||
cfg *TConfig
|
||||
checkIndex int
|
||||
expectParameters map[string]interface{}
|
||||
}{
|
||||
// default, no parameters setting
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: configParameters,
|
||||
},
|
||||
0, // check first item
|
||||
map[string]interface{}{
|
||||
"username": "test1", "password": "111111", "user_agent": "iOS/10.1", "app_version": 4.0,
|
||||
},
|
||||
},
|
||||
|
||||
// set limit for parameters
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: map[string]interface{}{
|
||||
"username-password": []map[string]interface{}{ // 1
|
||||
{"username": "test1", "password": 111111, "other": "111"},
|
||||
},
|
||||
"user_agent": []string{"iOS/10.1", "iOS/10.2"}, // 2
|
||||
},
|
||||
ParametersSetting: &TParamsConfig{
|
||||
Limit: 5, // limit could also be greater than total
|
||||
Strategies: map[string]iteratorStrategy{
|
||||
"username-password": "random",
|
||||
},
|
||||
},
|
||||
},
|
||||
2, // check 3th item, equals to the first item
|
||||
map[string]interface{}{
|
||||
"username": "test1", "password": 111111, "user_agent": "iOS/10.1",
|
||||
},
|
||||
},
|
||||
|
||||
// no parameters
|
||||
// also will generate one empty item
|
||||
{
|
||||
&TConfig{
|
||||
Parameters: nil,
|
||||
ParametersSetting: nil,
|
||||
},
|
||||
0,
|
||||
map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
for _, data := range testData {
|
||||
iterator, err := initParametersIterator(data.cfg)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// get expected parameters item
|
||||
for i := 0; i < data.checkIndex; i++ {
|
||||
if !assert.True(t, iterator.HasNext()) {
|
||||
t.Fatal()
|
||||
}
|
||||
iterator.Next() // consume next parameters
|
||||
}
|
||||
parametersItem := iterator.Next()
|
||||
|
||||
if !assert.Equal(t, data.expectParameters, parametersItem) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
179
hrp/parser.go
179
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"
|
||||
@@ -455,7 +454,7 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in
|
||||
return parsedVariables, nil
|
||||
}
|
||||
|
||||
type variableSet map[string]struct{}
|
||||
type variableSet map[string]struct{} // TODO
|
||||
|
||||
func extractVariables(raw interface{}) variableSet {
|
||||
rawValue := reflect.ValueOf(raw)
|
||||
@@ -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"
|
||||
@@ -14,41 +13,41 @@ func TestBuildURL(t *testing.T) {
|
||||
|
||||
url = buildURL("https://postman-echo.com", "/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com", "get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com/", "/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
url = buildURL("https://postman-echo.com/abc", "get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// omit query string in base url
|
||||
url = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
url = buildURL("", "https://postman-echo.com/get")
|
||||
if !assert.Equal(t, url, "https://postman-echo.com/get") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// notice: step request url > config base url
|
||||
url = buildURL("https://postman-echo.com", "https://httpbin.org/get")
|
||||
if !assert.Equal(t, url, "https://httpbin.org/get") {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ func TestRegexCompileVariable(t *testing.T) {
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 3) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +81,7 @@ func TestRegexCompileAbnormalVariable(t *testing.T) {
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileVariable.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 0) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +99,7 @@ func TestRegexCompileFunction(t *testing.T) {
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileFunction.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 3) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +121,7 @@ func TestRegexCompileAbnormalFunction(t *testing.T) {
|
||||
for _, expr := range testData {
|
||||
varMatched := regexCompileFunction.FindStringSubmatch(expr)
|
||||
if !assert.Len(t, varMatched, 0) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,10 +183,10 @@ func TestParseDataStringWithVariables(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,10 +208,10 @@ func TestParseDataStringWithUndefinedVariables(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,10 +253,10 @@ func TestParseDataStringWithVariablesAbnormal(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,10 +284,10 @@ func TestParseDataMapWithVariables(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
parsedData, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, parsedData) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,10 +318,10 @@ func TestParseHeaders(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
parsedHeaders, err := parser.ParseHeaders(data.rawHeaders, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectHeaders, parsedHeaders) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +347,7 @@ func TestMergeVariables(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
mergedVariables := mergeVariables(data.stepVariables, data.configVariables)
|
||||
if !assert.Equal(t, data.expectVariables, mergedVariables) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +383,7 @@ func TestMergeMap(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
mergedMap := mergeMap(data.m, data.overriddenMap)
|
||||
if !assert.Equal(t, data.expectMap, mergedMap) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,7 +414,7 @@ func TestMergeSlices(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
mergedSlice := mergeSlices(data.slice, data.overriddenSlice)
|
||||
if !assert.Equal(t, data.expectSlice, mergedSlice) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +453,7 @@ func TestMergeValidators(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
mergedValidators := mergeValidators(data.validators, data.overriddenValidators)
|
||||
if !assert.Equal(t, data.expectValidators, mergedValidators) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,35 +464,35 @@ func TestCallBuiltinFunction(t *testing.T) {
|
||||
// call function without arguments
|
||||
_, err := parser.CallFunc("get_timestamp")
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with one argument
|
||||
timeStart := time.Now()
|
||||
_, err = parser.CallFunc("sleep", 1)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Greater(t, time.Since(timeStart), time.Duration(1)*time.Second) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with one argument
|
||||
result, err := parser.CallFunc("gen_random_string", 10)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 10, len(result.(string))) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// call function with two argument
|
||||
result, err = parser.CallFunc("max", float64(10), 9.99)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, float64(10), result.(float64)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,10 +517,10 @@ func TestLiteralEval(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
value, err := literalEval(data.expr)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,10 +547,10 @@ func TestParseFunctionArguments(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
value, err := parseFunctionArguments(data.expr)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,10 +576,10 @@ func TestParseDataStringWithFunctions(t *testing.T) {
|
||||
for _, data := range testData1 {
|
||||
value, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, len(value.(string))) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,10 +595,10 @@ func TestParseDataStringWithFunctions(t *testing.T) {
|
||||
for _, data := range testData2 {
|
||||
value, err := parser.Parse(data.expr, variablesMapping)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,7 +619,7 @@ func TestConvertString(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
value := convertString(data.raw)
|
||||
if !assert.Equal(t, data.expect, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,10 +643,10 @@ func TestParseVariables(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
value, err := parser.ParseVariables(data.rawVars)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectVars, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -675,10 +674,10 @@ func TestParseVariablesAbnormal(t *testing.T) {
|
||||
for _, data := range testData {
|
||||
value, err := parser.ParseVariables(data.rawVars)
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, data.expectVars, value) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,7 +710,7 @@ func TestExtractVariables(t *testing.T) {
|
||||
}
|
||||
sort.Strings(varList)
|
||||
if !assert.Equal(t, data.expectVars, varList) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -750,156 +749,7 @@ func TestFindallVariables(t *testing.T) {
|
||||
}
|
||||
sort.Strings(varList)
|
||||
if !assert.Equal(t, data.expectVars, varList) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,17 @@ const (
|
||||
hashicorpPyPluginFile = "debugtalk.py" // used for hashicorp python plugin
|
||||
)
|
||||
|
||||
func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, err error) {
|
||||
func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, pluginDir string, err error) {
|
||||
// plugin file not found
|
||||
if path == "" {
|
||||
return nil, nil
|
||||
return nil, "", nil
|
||||
}
|
||||
pluginPath, err := locatePlugin(path)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, "", nil
|
||||
}
|
||||
// TODO: move pluginDir to funplugin
|
||||
pluginDir = filepath.Dir(pluginPath)
|
||||
|
||||
// found plugin file
|
||||
plugin, err = funplugin.Init(pluginPath, funplugin.WithLogOn(logOn))
|
||||
|
||||
@@ -10,36 +10,36 @@ func TestLocateFile(t *testing.T) {
|
||||
// specify target file path
|
||||
_, err := locateFile(templatesDir+"plugin/debugtalk.go", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// specify path with the same dir
|
||||
_, err = locateFile(templatesDir+"plugin/debugtalk.py", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// specify target file path dir
|
||||
_, err = locateFile(templatesDir+"plugin/", "debugtalk.go")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// specify wrong path
|
||||
_, err = locateFile(".", "debugtalk.go")
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
_, err = locateFile("/abc", "debugtalk.go")
|
||||
if !assert.Error(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocatePythonPlugin(t *testing.T) {
|
||||
_, err := locatePlugin(templatesDir + "plugin/debugtalk.py")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,6 @@ func TestLocateGoPlugin(t *testing.T) {
|
||||
|
||||
_, err := locatePlugin(templatesDir + "debugtalk.bin")
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ func TestSearchJmespath(t *testing.T) {
|
||||
resp.Body = io.NopCloser(strings.NewReader(testText))
|
||||
respObj, err := newResponseObject(t, newParser(), &resp)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
for _, data := range testData {
|
||||
if !assert.Equal(t, data.expected, respObj.searchJmespath(data.raw)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,11 +49,11 @@ func TestSearchRegexp(t *testing.T) {
|
||||
resp.Body = io.NopCloser(strings.NewReader(testText))
|
||||
respObj, err := newResponseObject(t, newParser(), &resp)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
for _, data := range testData {
|
||||
if !assert.Equal(t, data.expected, respObj.searchRegexp(data.raw)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
hrp/runner.go
123
hrp/runner.go
@@ -2,19 +2,17 @@ package hrp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
@@ -163,16 +161,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
|
||||
}
|
||||
@@ -182,22 +172,9 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
|
||||
}
|
||||
s.Time.Duration = time.Since(s.Time.StartAt).Seconds()
|
||||
|
||||
// update the report output path
|
||||
pluginPath, err := locatePlugin(testcases[0].GetPath())
|
||||
if err == nil {
|
||||
outputPath, _ := filepath.Split(pluginPath)
|
||||
summaryPath = filepath.Join(outputPath, summaryPath)
|
||||
reportPath = filepath.Join(outputPath, reportPath)
|
||||
}
|
||||
|
||||
// save summary
|
||||
if r.saveTests {
|
||||
dir, _ := filepath.Split(summaryPath)
|
||||
err := builtin.EnsureFolderExists(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = builtin.Dump2JSON(s, fmt.Sprintf(summaryPath, s.Time.StartAt.Unix()))
|
||||
err := s.genSummary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,24 +193,106 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
|
||||
// NewSessionRunner creates a new session runner for testcase.
|
||||
// each testcase has its own session runner
|
||||
func (r *HRPRunner) NewSessionRunner(testcase *TestCase) (*SessionRunner, error) {
|
||||
runner, err := r.newCaseRunner(testcase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionRunner := &SessionRunner{
|
||||
testCaseRunner: runner,
|
||||
}
|
||||
sessionRunner.resetSession()
|
||||
return sessionRunner, nil
|
||||
}
|
||||
|
||||
func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) {
|
||||
runner := &testCaseRunner{
|
||||
testCase: testcase,
|
||||
hrpRunner: r,
|
||||
parser: newParser(),
|
||||
summary: newSummary(),
|
||||
}
|
||||
|
||||
// init parser plugin
|
||||
plugin, err := initPlugin(testcase.Config.Path, r.pluginLogOn)
|
||||
plugin, pluginDir, err := initPlugin(testcase.Config.Path, r.pluginLogOn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init plugin failed")
|
||||
}
|
||||
sessionRunner.parser.plugin = plugin
|
||||
runner.parser.plugin = plugin
|
||||
runner.rootDir = pluginDir
|
||||
|
||||
// parse testcase config
|
||||
if err := sessionRunner.parseConfig(); err != nil {
|
||||
if err := runner.parseConfig(); err != nil {
|
||||
return nil, errors.Wrap(err, "parse testcase config failed")
|
||||
}
|
||||
|
||||
return sessionRunner, nil
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
type testCaseRunner struct {
|
||||
testCase *TestCase
|
||||
hrpRunner *HRPRunner
|
||||
parser *Parser
|
||||
parsedConfig *TConfig
|
||||
parametersIterator *ParametersIterator
|
||||
rootDir string // project root dir
|
||||
}
|
||||
|
||||
// parseConfig parses testcase config, stores to parsedConfig.
|
||||
func (r *testCaseRunner) parseConfig() error {
|
||||
cfg := r.testCase.Config
|
||||
|
||||
r.parsedConfig = &TConfig{}
|
||||
// deep copy config to avoid data racing
|
||||
if err := copier.Copy(r.parsedConfig, cfg); err != nil {
|
||||
log.Error().Err(err).Msg("copy testcase config failed")
|
||||
return err
|
||||
}
|
||||
|
||||
// parse config variables
|
||||
parsedVariables, err := r.parser.ParseVariables(cfg.Variables)
|
||||
if err != nil {
|
||||
log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed")
|
||||
return err
|
||||
}
|
||||
r.parsedConfig.Variables = parsedVariables
|
||||
|
||||
// parse config name
|
||||
parsedName, err := r.parser.ParseString(cfg.Name, parsedVariables)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse config name failed")
|
||||
}
|
||||
r.parsedConfig.Name = convertString(parsedName)
|
||||
|
||||
// parse config base url
|
||||
parsedBaseURL, err := r.parser.ParseString(cfg.BaseURL, parsedVariables)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse config base url failed")
|
||||
}
|
||||
r.parsedConfig.BaseURL = convertString(parsedBaseURL)
|
||||
|
||||
// ensure correction of think time config
|
||||
r.parsedConfig.ThinkTimeSetting.checkThinkTime()
|
||||
|
||||
// parse testcase config parameters
|
||||
parametersIterator, err := initParametersIterator(r.parsedConfig)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// each boomer task initiates a new session
|
||||
// in order to avoid data racing
|
||||
func (r *testCaseRunner) newSession() *SessionRunner {
|
||||
sessionRunner := &SessionRunner{
|
||||
testCaseRunner: r,
|
||||
}
|
||||
sessionRunner.resetSession()
|
||||
return sessionRunner
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func TestRunCaseWithPluginJSON(t *testing.T) {
|
||||
|
||||
err := NewRunner(nil).Run(&demoTestCaseWithPluginJSONPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestRunCaseWithPluginYAML(t *testing.T) {
|
||||
|
||||
err := NewRunner(nil).Run(&demoTestCaseWithPluginYAMLPath) // hrp.Run(testCase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestRunCaseWithRefAPI(t *testing.T) {
|
||||
|
||||
err := NewRunner(nil).Run(&demoTestCaseWithRefAPIPath)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
testcase := &TestCase{
|
||||
@@ -195,7 +195,7 @@ func TestRunCaseWithRefAPI(t *testing.T) {
|
||||
r := NewRunner(t)
|
||||
err = r.Run(testcase)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,30 +204,30 @@ func TestLoadTestCases(t *testing.T) {
|
||||
tc := TestCasePath("../examples/demo-with-py-plugin/testcases/")
|
||||
testCases, err := loadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, len(testCases), 3) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from folder path, including sub folders
|
||||
tc = TestCasePath("../examples/demo-with-py-plugin/")
|
||||
testCases, err = loadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, len(testCases), 3) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from single file path
|
||||
tc = demoTestCaseWithPluginJSONPath
|
||||
testCases, err = loadTestCases(&tc)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, 1, len(testCases)) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// load test cases from TestCase instance
|
||||
@@ -236,9 +236,9 @@ func TestLoadTestCases(t *testing.T) {
|
||||
}
|
||||
testCases, err = loadTestCases(testcase)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, len(testCases), 1) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
_ "embed"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -12,10 +11,7 @@ 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
|
||||
*testCaseRunner
|
||||
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.
|
||||
@@ -29,6 +25,7 @@ func (r *SessionRunner) resetSession() {
|
||||
r.sessionVariables = make(map[string]interface{})
|
||||
r.transactions = make(map[string]map[transactionType]time.Time)
|
||||
r.startTime = time.Now()
|
||||
r.summary = newSummary()
|
||||
}
|
||||
|
||||
func (r *SessionRunner) GetParser() *Parser {
|
||||
@@ -61,13 +58,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
|
||||
@@ -107,58 +107,17 @@ func (r *SessionRunner) MergeStepVariables(vars map[string]interface{}) (map[str
|
||||
|
||||
// updateConfigVariables updates config variables with given variables.
|
||||
// this is used for data driven
|
||||
func (r *SessionRunner) updateConfigVariables(givenVars map[string]interface{}) {
|
||||
for k, v := range givenVars {
|
||||
func (r *SessionRunner) updateConfigVariables(parameters map[string]interface{}) {
|
||||
if len(parameters) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Interface("parameters", parameters).Msg("update config variables")
|
||||
for k, v := range parameters {
|
||||
r.parsedConfig.Variables[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// parseConfig parses testcase config, stores to parsedConfig.
|
||||
func (r *SessionRunner) parseConfig() error {
|
||||
cfg := r.testCase.Config
|
||||
|
||||
r.parsedConfig = &TConfig{}
|
||||
// deep copy config to avoid data racing
|
||||
if err := copier.Copy(r.parsedConfig, cfg); err != nil {
|
||||
log.Error().Err(err).Msg("copy testcase config failed")
|
||||
return err
|
||||
}
|
||||
|
||||
// parse config variables
|
||||
parsedVariables, err := r.parser.ParseVariables(cfg.Variables)
|
||||
if err != nil {
|
||||
log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed")
|
||||
return err
|
||||
}
|
||||
r.parsedConfig.Variables = parsedVariables
|
||||
|
||||
// parse config name
|
||||
parsedName, err := r.parser.ParseString(cfg.Name, parsedVariables)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse config name failed")
|
||||
}
|
||||
r.parsedConfig.Name = convertString(parsedName)
|
||||
|
||||
// parse config base url
|
||||
parsedBaseURL, err := r.parser.ParseString(cfg.BaseURL, parsedVariables)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse config base url failed")
|
||||
}
|
||||
r.parsedConfig.BaseURL = convertString(parsedBaseURL)
|
||||
|
||||
// ensure correction of think time config
|
||||
r.parsedConfig.ThinkTimeSetting.checkThinkTime()
|
||||
|
||||
// parse testcase config parameters
|
||||
err = initParameterIterator(r.parsedConfig, "runner")
|
||||
if err != nil {
|
||||
log.Error().Interface("parameters", r.parsedConfig.Parameters).Err(err).Msg("parse config parameters failed")
|
||||
return errors.Wrap(err, "parse testcase config parameters failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SessionRunner) GetSummary() *TestCaseSummary {
|
||||
caseSummary := r.summary
|
||||
caseSummary.Name = r.parsedConfig.Name
|
||||
|
||||
@@ -38,6 +38,7 @@ type Summary struct {
|
||||
Time *TestCaseTime `json:"time" yaml:"time"`
|
||||
Platform *Platform `json:"platform" yaml:"platform"`
|
||||
Details []*TestCaseSummary `json:"details" yaml:"details"`
|
||||
rootDir string
|
||||
}
|
||||
|
||||
func (s *Summary) appendCaseSummary(caseSummary *TestCaseSummary) {
|
||||
@@ -53,15 +54,24 @@ func (s *Summary) appendCaseSummary(caseSummary *TestCaseSummary) {
|
||||
s.Stat.TestSteps.Failures += caseSummary.Stat.Failures
|
||||
s.Details = append(s.Details, caseSummary)
|
||||
s.Success = s.Success && caseSummary.Success
|
||||
|
||||
// specify output reports dir
|
||||
if len(s.Details) == 1 {
|
||||
s.rootDir = caseSummary.RootDir
|
||||
} else if s.rootDir != caseSummary.RootDir {
|
||||
// if multiple testcases have different root path, use current working dir
|
||||
s.rootDir, _ = os.Getwd()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Summary) genHTMLReport() error {
|
||||
dir, _ := filepath.Split(reportPath)
|
||||
err := builtin.EnsureFolderExists(dir)
|
||||
reportsDir := filepath.Join(s.rootDir, resultsDir)
|
||||
err := builtin.EnsureFolderExists(reportsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reportPath := fmt.Sprintf(reportPath, s.Time.StartAt.Unix())
|
||||
|
||||
reportPath := filepath.Join(reportsDir, fmt.Sprintf("report-%v.html", s.Time.StartAt.Unix()))
|
||||
file, err := os.OpenFile(reportPath, os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("open file failed")
|
||||
@@ -84,13 +94,25 @@ func (s *Summary) genHTMLReport() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Summary) genSummary() error {
|
||||
reportsDir := filepath.Join(s.rootDir, resultsDir)
|
||||
err := builtin.EnsureFolderExists(reportsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
summaryPath := filepath.Join(reportsDir, fmt.Sprintf("summary-%v.json", s.Time.StartAt.Unix()))
|
||||
err = builtin.Dump2JSON(s, summaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed internal/scaffold/templates/report/template.html
|
||||
var reportTemplate string
|
||||
|
||||
var (
|
||||
reportPath = "reports/report-%v.html"
|
||||
summaryPath = "reports/summary-%v.json"
|
||||
)
|
||||
const resultsDir = "reports"
|
||||
|
||||
type Stat struct {
|
||||
TestCases TestCaseStat `json:"testcases" yaml:"test_cases"`
|
||||
@@ -130,6 +152,7 @@ type TestCaseSummary struct {
|
||||
InOut *TestCaseInOut `json:"in_out" yaml:"in_out"`
|
||||
Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO
|
||||
Records []*StepResult `json:"records" yaml:"records"`
|
||||
RootDir string `json:"root_dir" yaml:"root_dir"`
|
||||
}
|
||||
|
||||
type TestCaseInOut struct {
|
||||
|
||||
@@ -156,21 +156,21 @@ func TestGenDemoTestCase(t *testing.T) {
|
||||
tCase := demoTestCaseWithPlugin.ToTCase()
|
||||
err := builtin.Dump2JSON(tCase, demoTestCaseWithPluginJSONPath.GetPath())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, demoTestCaseWithPluginYAMLPath.GetPath())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
tCase = demoTestCaseWithoutPlugin.ToTCase()
|
||||
err = builtin.Dump2JSON(tCase, demoTestCaseWithoutPluginJSONPath.GetPath())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
err = builtin.Dump2YAML(tCase, demoTestCaseWithoutPluginYAMLPath.GetPath())
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,24 +179,24 @@ func TestLoadCase(t *testing.T) {
|
||||
tcYAML := &TCase{}
|
||||
err := builtin.LoadFile(demoTestCaseWithPluginJSONPath.GetPath(), tcJSON)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
err = builtin.LoadFile(demoTestCaseWithPluginYAMLPath.GetPath(), tcYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.TestSteps[1].Name, tcYAML.TestSteps[1].Name) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.TestSteps[1].Request, tcYAML.TestSteps[1].Request) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ func TestConvertCheckExpr(t *testing.T) {
|
||||
}
|
||||
for _, expr := range exprs {
|
||||
if !assert.Equal(t, convertCheckExpr(expr.before), expr.after) {
|
||||
t.Fail()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user