mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 08:59:44 +08:00
refactor: restructure code
This commit is contained in:
1
.github/workflows/hrp-scaffold.yml
vendored
1
.github/workflows/hrp-scaffold.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
scaffold-with-python-plugin:
|
||||
|
||||
1
.github/workflows/smoketest.yml
vendored
1
.github/workflows/smoketest.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
|
||||
1
.github/workflows/unittest.yml
vendored
1
.github/workflows/unittest.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
py-httprunner:
|
||||
|
||||
@@ -95,8 +95,9 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
Name: config.Name,
|
||||
Weight: config.Weight,
|
||||
Fn: func() {
|
||||
runner := hrpRunner.newCaseRunner(testcase)
|
||||
runner.parser.plugin = plugin
|
||||
sessionRunner := hrpRunner.NewSessionRunner(testcase)
|
||||
sessionRunner.init()
|
||||
sessionRunner.parser.plugin = plugin
|
||||
|
||||
testcaseSuccess := true // flag whole testcase result
|
||||
var transactionSuccess = true // flag current transaction result
|
||||
@@ -115,27 +116,27 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
}
|
||||
}
|
||||
|
||||
if err := runner.parseConfig(caseConfig); err != nil {
|
||||
if err := sessionRunner.parseConfig(caseConfig); err != nil {
|
||||
log.Error().Err(err).Msg("parse config failed")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
for index, step := range testcase.TestSteps {
|
||||
stepData, err := runner.runStep(index, caseConfig)
|
||||
for _, step := range testcase.TestSteps {
|
||||
stepResult, err := step.Run(sessionRunner)
|
||||
if err != nil {
|
||||
// step failed
|
||||
var elapsed int64
|
||||
if stepData != nil {
|
||||
elapsed = stepData.Elapsed
|
||||
if stepResult != nil {
|
||||
elapsed = stepResult.Elapsed
|
||||
}
|
||||
b.RecordFailure(step.Type(), step.Name(), elapsed, err.Error())
|
||||
b.RecordFailure(string(step.Type()), step.Name(), elapsed, err.Error())
|
||||
|
||||
// update flag
|
||||
testcaseSuccess = false
|
||||
transactionSuccess = false
|
||||
|
||||
if runner.hrpRunner.failfast {
|
||||
if sessionRunner.hrpRunner.failfast {
|
||||
log.Error().Msg("abort running due to failfast setting")
|
||||
break
|
||||
}
|
||||
@@ -144,28 +145,27 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
}
|
||||
|
||||
// step success
|
||||
if stepData.StepType == stepTypeTransaction {
|
||||
if stepResult.StepType == stepTypeTransaction {
|
||||
// transaction
|
||||
// FIXME: support nested transactions
|
||||
if step.ToStruct().Transaction.Type == transactionEnd { // only record when transaction ends
|
||||
b.RecordTransaction(stepData.Name, transactionSuccess, stepData.Elapsed, 0)
|
||||
b.RecordTransaction(stepResult.Name, transactionSuccess, stepResult.Elapsed, 0)
|
||||
transactionSuccess = true // reset flag for next transaction
|
||||
}
|
||||
} else if stepData.StepType == stepTypeRendezvous {
|
||||
} else if stepResult.StepType == stepTypeRendezvous {
|
||||
// rendezvous
|
||||
// TODO: implement rendezvous in boomer
|
||||
} else if stepData.StepType == stepTypeThinkTime {
|
||||
} else if stepResult.StepType == stepTypeThinkTime {
|
||||
// think time
|
||||
// no record required
|
||||
} else {
|
||||
// request or testcase step
|
||||
b.RecordSuccess(step.Type(), step.Name(), stepData.Elapsed, stepData.ContentSize)
|
||||
b.RecordSuccess(string(step.Type()), step.Name(), stepResult.Elapsed, stepResult.ContentSize)
|
||||
}
|
||||
}
|
||||
endTime := time.Now()
|
||||
|
||||
// report duration for transaction without end
|
||||
for name, transaction := range runner.transactions {
|
||||
for name, transaction := range sessionRunner.transactions {
|
||||
if len(transaction) == 1 {
|
||||
// if transaction end time not exists, use testcase end time instead
|
||||
duration := endTime.Sub(transaction[transactionStart])
|
||||
|
||||
216
hrp/config.go
Normal file
216
hrp/config.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
// NewConfig returns a new constructed testcase config with specified testcase name.
|
||||
func NewConfig(name string) *TConfig {
|
||||
return &TConfig{
|
||||
Name: name,
|
||||
Variables: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// TConfig represents config data structure for testcase.
|
||||
// Each testcase should contain one config part.
|
||||
type TConfig struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current testcase.
|
||||
func (c *TConfig) WithVariables(variables map[string]interface{}) *TConfig {
|
||||
c.Variables = variables
|
||||
return c
|
||||
}
|
||||
|
||||
// SetBaseURL sets base URL for current testcase.
|
||||
func (c *TConfig) SetBaseURL(baseURL string) *TConfig {
|
||||
c.BaseURL = baseURL
|
||||
return c
|
||||
}
|
||||
|
||||
// SetHeaders sets global headers for current testcase.
|
||||
func (c *TConfig) SetHeaders(headers map[string]string) *TConfig {
|
||||
c.Headers = headers
|
||||
return c
|
||||
}
|
||||
|
||||
// SetVerifySSL sets whether to verify SSL for current testcase.
|
||||
func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
|
||||
c.Verify = verify
|
||||
return c
|
||||
}
|
||||
|
||||
// WithParameters sets parameters for current testcase.
|
||||
func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
|
||||
c.Parameters = parameters
|
||||
return c
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time config for current testcase.
|
||||
func (c *TConfig) SetThinkTime(strategy thinkTimeStrategy, cfg interface{}, limit float64) *TConfig {
|
||||
c.ThinkTimeSetting = &ThinkTimeConfig{strategy, cfg, limit}
|
||||
return c
|
||||
}
|
||||
|
||||
// ExportVars specifies variable names to export for current testcase.
|
||||
func (c *TConfig) ExportVars(vars ...string) *TConfig {
|
||||
c.Export = vars
|
||||
return c
|
||||
}
|
||||
|
||||
// SetWeight sets weight for current testcase, which is used in load testing.
|
||||
func (c *TConfig) SetWeight(weight int) *TConfig {
|
||||
c.Weight = weight
|
||||
return c
|
||||
}
|
||||
|
||||
type ThinkTimeConfig struct {
|
||||
Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、limit、multiply、ignore
|
||||
Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5
|
||||
Limit float64 `json:"limit,omitempty" yaml:"limit,omitempty"` // limit think time no more than specific time, ignore if value <= 0
|
||||
}
|
||||
|
||||
func (ttc *ThinkTimeConfig) checkThinkTime() {
|
||||
if ttc == nil {
|
||||
return
|
||||
}
|
||||
// unset strategy, set default strategy
|
||||
if ttc.Strategy == "" {
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
// check think time
|
||||
if ttc.Strategy == thinkTimeRandomPercentage {
|
||||
if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
value, ok := ttc.Setting.(map[string]interface{})
|
||||
if !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["min_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["max_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
left, err := builtin.Interface2Float64(value["min_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
right, err := builtin.Interface2Float64(value["max_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right}
|
||||
} else if ttc.Strategy == thinkTimeMultiply {
|
||||
if ttc.Setting == nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
value, err := builtin.Interface2Float64(ttc.Setting)
|
||||
if err != nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
ttc.Setting = value
|
||||
} else if ttc.Strategy != thinkTimeIgnore {
|
||||
// unrecognized strategy, set default strategy
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
}
|
||||
|
||||
type thinkTimeStrategy string
|
||||
|
||||
const (
|
||||
thinkTimeDefault thinkTimeStrategy = "default" // as recorded
|
||||
thinkTimeRandomPercentage thinkTimeStrategy = "random_percentage" // use random percentage of recorded think time
|
||||
thinkTimeMultiply thinkTimeStrategy = "multiply" // multiply recorded think time
|
||||
thinkTimeIgnore thinkTimeStrategy = "ignore" // ignore recorded think time
|
||||
)
|
||||
|
||||
const (
|
||||
thinkTimeDefaultMultiply = 1
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StepRequestExtraction implements IStep interface.
|
||||
type StepRequestExtraction struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// WithJmesPath sets the JMESPath expression to extract from the response.
|
||||
func (s *StepRequestExtraction) WithJmesPath(jmesPath string, varName string) *StepRequestExtraction {
|
||||
s.step.Extract[varName] = jmesPath
|
||||
return s
|
||||
}
|
||||
|
||||
// Validate switches to step validation.
|
||||
func (s *StepRequestExtraction) Validate() *StepRequestValidation {
|
||||
return &StepRequestValidation{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Type() string {
|
||||
return fmt.Sprintf("request-%v", s.step.Request.Method)
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
@@ -177,7 +177,7 @@ type tStep struct {
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestMethod(entry *Entry) error {
|
||||
s.Request.Method = entry.Request.Method
|
||||
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
462
hrp/models.go
462
hrp/models.go
@@ -1,462 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
httpGET string = "GET"
|
||||
httpHEAD string = "HEAD"
|
||||
httpPOST string = "POST"
|
||||
httpPUT string = "PUT"
|
||||
httpDELETE string = "DELETE"
|
||||
httpOPTIONS string = "OPTIONS"
|
||||
httpPATCH string = "PATCH"
|
||||
)
|
||||
|
||||
// TConfig represents config data structure for testcase.
|
||||
// Each testcase should contain one config part.
|
||||
type TConfig struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
ThinkTime *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
|
||||
}
|
||||
|
||||
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"` // 保存参数的迭代器
|
||||
}
|
||||
|
||||
const (
|
||||
strategyRandom string = "random"
|
||||
strategySequential string = "Sequential"
|
||||
)
|
||||
|
||||
type ThinkTimeConfig struct {
|
||||
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、limit、multiply、ignore
|
||||
Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5
|
||||
Limit float64 `json:"limit,omitempty" yaml:"limit,omitempty"` // limit think time no more than specific time, ignore if value <= 0
|
||||
}
|
||||
|
||||
const (
|
||||
thinkTimeDefault string = "default" // as recorded
|
||||
thinkTimeRandomPercentage string = "random_percentage" // use random percentage of recorded think time
|
||||
thinkTimeMultiply string = "multiply" // multiply recorded think time
|
||||
thinkTimeIgnore string = "ignore" // ignore recorded think time
|
||||
)
|
||||
|
||||
const (
|
||||
thinkTimeDefaultMultiply = 1
|
||||
)
|
||||
|
||||
var (
|
||||
thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5}
|
||||
)
|
||||
|
||||
func (ttc *ThinkTimeConfig) checkThinkTime() {
|
||||
if ttc == nil {
|
||||
return
|
||||
}
|
||||
// unset strategy, set default strategy
|
||||
if ttc.Strategy == "" {
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
// check think time
|
||||
if ttc.Strategy == thinkTimeRandomPercentage {
|
||||
if ttc.Setting == nil || reflect.TypeOf(ttc.Setting).Kind() != reflect.Map {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
value, ok := ttc.Setting.(map[string]interface{})
|
||||
if !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["min_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
if _, ok := value["max_percentage"]; !ok {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
left, err := builtin.Interface2Float64(value["min_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
right, err := builtin.Interface2Float64(value["max_percentage"])
|
||||
if err != nil {
|
||||
ttc.Setting = thinkTimeDefaultRandom
|
||||
return
|
||||
}
|
||||
ttc.Setting = map[string]float64{"min_percentage": left, "max_percentage": right}
|
||||
} else if ttc.Strategy == thinkTimeMultiply {
|
||||
if ttc.Setting == nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
value, err := builtin.Interface2Float64(ttc.Setting)
|
||||
if err != nil {
|
||||
ttc.Setting = float64(0) // default
|
||||
return
|
||||
}
|
||||
ttc.Setting = value
|
||||
} else if ttc.Strategy != thinkTimeIgnore {
|
||||
// unrecognized strategy, set default strategy
|
||||
ttc.Strategy = thinkTimeDefault
|
||||
}
|
||||
}
|
||||
|
||||
type paramsType []map[string]interface{}
|
||||
|
||||
type Iterator struct {
|
||||
sync.Mutex
|
||||
data paramsType
|
||||
strategy string // random, sequential
|
||||
iteration int
|
||||
index int
|
||||
}
|
||||
|
||||
func (params paramsType) Iterator() *Iterator {
|
||||
return &Iterator{
|
||||
data: params,
|
||||
iteration: len(params),
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (iter *Iterator) HasNext() bool {
|
||||
if iter.iteration == -1 {
|
||||
return true
|
||||
}
|
||||
return iter.index < iter.iteration
|
||||
}
|
||||
|
||||
func (iter *Iterator) Next() (value map[string]interface{}) {
|
||||
iter.Lock()
|
||||
defer iter.Unlock()
|
||||
if len(iter.data) == 0 {
|
||||
iter.index++
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
if iter.strategy == strategyRandom {
|
||||
randSource := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
randIndex := randSource.Intn(len(iter.data))
|
||||
value = iter.data[randIndex]
|
||||
} else {
|
||||
value = iter.data[iter.index%len(iter.data)]
|
||||
}
|
||||
iter.index++
|
||||
return value
|
||||
}
|
||||
|
||||
// Request represents HTTP request data structure.
|
||||
// This is used for teststep.
|
||||
type Request struct {
|
||||
Method string `json:"method" yaml:"method"` // required
|
||||
URL string `json:"url" yaml:"url"` // required
|
||||
Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Cookies map[string]string `json:"cookies,omitempty" yaml:"cookies,omitempty"`
|
||||
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Json interface{} `json:"json,omitempty" yaml:"json,omitempty"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Timeout float32 `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
AllowRedirects bool `json:"allow_redirects,omitempty" yaml:"allow_redirects,omitempty"`
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Path string
|
||||
}
|
||||
|
||||
func (api *API) GetPath() string {
|
||||
return api.Path
|
||||
}
|
||||
|
||||
func (api *API) ToAPI() (*API, error) {
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// Validator represents validator for one HTTP response.
|
||||
type Validator struct {
|
||||
Check string `json:"check" yaml:"check"` // get value with jmespath
|
||||
Assert string `json:"assert" yaml:"assert"`
|
||||
Expect interface{} `json:"expect" yaml:"expect"`
|
||||
Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional
|
||||
}
|
||||
|
||||
// IAPI represents interface for api,
|
||||
// includes API and APIPath.
|
||||
type IAPI interface {
|
||||
GetPath() string
|
||||
ToAPI() (*API, error)
|
||||
}
|
||||
|
||||
// TStep represents teststep data structure.
|
||||
// Each step maybe two different type: make one HTTP request or reference another testcase.
|
||||
type TStep struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
||||
API interface{} `json:"api,omitempty" yaml:"api,omitempty"` // *APIPath or *API
|
||||
TestCase interface{} `json:"testcase,omitempty" yaml:"testcase,omitempty"` // *TestCasePath or *TestCase
|
||||
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
|
||||
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
|
||||
ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
}
|
||||
|
||||
type stepType string
|
||||
|
||||
const (
|
||||
stepTypeRequest stepType = "request"
|
||||
stepTypeTestCase stepType = "testcase"
|
||||
stepTypeTransaction stepType = "transaction"
|
||||
stepTypeRendezvous stepType = "rendezvous"
|
||||
stepTypeThinkTime stepType = "thinktime"
|
||||
)
|
||||
|
||||
type ThinkTime struct {
|
||||
Time float64 `json:"time" yaml:"time"`
|
||||
}
|
||||
|
||||
type transactionType string
|
||||
|
||||
const (
|
||||
transactionStart transactionType = "start"
|
||||
transactionEnd transactionType = "end"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type transactionType `json:"type" yaml:"type"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRendezvousTimeout int64 = 5000
|
||||
defaultRendezvousPercent float32 = 1.0
|
||||
)
|
||||
|
||||
type Rendezvous struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Percent float32 `json:"percent,omitempty" yaml:"percent,omitempty"` // default to 1(100%)
|
||||
Number int64 `json:"number,omitempty" yaml:"number,omitempty"`
|
||||
Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // milliseconds
|
||||
cnt int64
|
||||
releasedFlag uint32
|
||||
spawnDoneFlag uint32
|
||||
wg sync.WaitGroup
|
||||
timerResetChan chan struct{}
|
||||
activateChan chan struct{}
|
||||
releaseChan chan struct{}
|
||||
once *sync.Once
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// TCase represents testcase data structure.
|
||||
// Each testcase includes one public config and several sequential teststeps.
|
||||
type TCase struct {
|
||||
Config *TConfig `json:"config" yaml:"config"`
|
||||
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
// IStep represents interface for all types for teststeps, includes:
|
||||
// StepRequest, StepRequestWithOptionalArgs, StepRequestValidation, StepRequestExtraction,
|
||||
// StepTestCaseWithOptionalArgs,
|
||||
// StepTransaction, StepRendezvous.
|
||||
type IStep interface {
|
||||
Name() string
|
||||
Type() string
|
||||
ToStruct() *TStep
|
||||
}
|
||||
|
||||
// ITestCase represents interface for testcases,
|
||||
// includes TestCase and TestCasePath.
|
||||
type ITestCase interface {
|
||||
GetPath() string
|
||||
ToTestCase() (*TestCase, error)
|
||||
}
|
||||
|
||||
// TestCase is a container for one testcase, which is used for testcase runner.
|
||||
// TestCase implements ITestCase interface.
|
||||
type TestCase struct {
|
||||
Config *TConfig
|
||||
TestSteps []IStep
|
||||
}
|
||||
|
||||
func (tc *TestCase) GetPath() string {
|
||||
return tc.Config.Path
|
||||
}
|
||||
|
||||
func (tc *TestCase) ToTestCase() (*TestCase, error) {
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *TestCase) ToTCase() *TCase {
|
||||
tCase := &TCase{
|
||||
Config: tc.Config,
|
||||
}
|
||||
for _, step := range tc.TestSteps {
|
||||
tCase.TestSteps = append(tCase.TestSteps, step.ToStruct())
|
||||
}
|
||||
return tCase
|
||||
}
|
||||
|
||||
type testCaseStat struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Success int `json:"success" yaml:"success"`
|
||||
Fail int `json:"fail" yaml:"fail"`
|
||||
}
|
||||
|
||||
type testStepStat struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Successes int `json:"successes" yaml:"successes"`
|
||||
Failures int `json:"failures" yaml:"failures"`
|
||||
}
|
||||
|
||||
type stat struct {
|
||||
TestCases testCaseStat `json:"testcases" yaml:"test_cases"`
|
||||
TestSteps testStepStat `json:"teststeps" yaml:"test_steps"`
|
||||
}
|
||||
|
||||
type testCaseTime struct {
|
||||
StartAt time.Time `json:"start_at,omitempty" yaml:"start_at,omitempty"`
|
||||
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"`
|
||||
}
|
||||
|
||||
type platform struct {
|
||||
HttprunnerVersion string `json:"httprunner_version" yaml:"httprunner_version"`
|
||||
GoVersion string `json:"go_version" yaml:"go_version"`
|
||||
Platform string `json:"platform" yaml:"platform"`
|
||||
}
|
||||
|
||||
// Summary stores tests summary for current task execution, maybe include one or multiple testcases
|
||||
type Summary struct {
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
Stat *stat `json:"stat" yaml:"stat"`
|
||||
Time *testCaseTime `json:"time" yaml:"time"`
|
||||
Platform *platform `json:"platform" yaml:"platform"`
|
||||
Details []*testCaseSummary `json:"details" yaml:"details"`
|
||||
}
|
||||
|
||||
func newOutSummary() *Summary {
|
||||
platForm := &platform{
|
||||
HttprunnerVersion: version.VERSION,
|
||||
GoVersion: runtime.Version(),
|
||||
Platform: fmt.Sprintf("%v-%v", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
return &Summary{
|
||||
Success: true,
|
||||
Stat: &stat{},
|
||||
Time: &testCaseTime{
|
||||
StartAt: time.Now(),
|
||||
},
|
||||
Platform: platForm,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Summary) appendCaseSummary(caseSummary *testCaseSummary) {
|
||||
s.Success = s.Success && caseSummary.Success
|
||||
s.Stat.TestCases.Total += 1
|
||||
s.Stat.TestSteps.Total += len(caseSummary.Records)
|
||||
if caseSummary.Success {
|
||||
s.Stat.TestCases.Success += 1
|
||||
} else {
|
||||
s.Stat.TestCases.Fail += 1
|
||||
}
|
||||
s.Stat.TestSteps.Successes += caseSummary.Stat.Successes
|
||||
s.Stat.TestSteps.Failures += caseSummary.Stat.Failures
|
||||
s.Details = append(s.Details, caseSummary)
|
||||
s.Success = s.Success && caseSummary.Success
|
||||
}
|
||||
|
||||
type stepData struct {
|
||||
Name string `json:"name" yaml:"name"` // step name
|
||||
StepType stepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
|
||||
Success bool `json:"success" yaml:"success"` // step execution result
|
||||
Elapsed int64 `json:"elapsed_ms" yaml:"elapsed_ms"` // step execution time in millisecond(ms)
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data
|
||||
ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length
|
||||
ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables
|
||||
Attachment string `json:"attachment,omitempty" yaml:"attachment,omitempty"` // step error information
|
||||
}
|
||||
|
||||
type testCaseInOut struct {
|
||||
ConfigVars map[string]interface{} `json:"config_vars" yaml:"config_vars"`
|
||||
ExportVars map[string]interface{} `json:"export_vars" yaml:"export_vars"`
|
||||
}
|
||||
|
||||
// testCaseSummary stores tests summary for one testcase
|
||||
type testCaseSummary struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` // TODO
|
||||
Stat *testStepStat `json:"stat" yaml:"stat"`
|
||||
Time *testCaseTime `json:"time" yaml:"time"`
|
||||
InOut *testCaseInOut `json:"in_out" yaml:"in_out"`
|
||||
Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO
|
||||
Records []*stepData `json:"records" yaml:"records"`
|
||||
}
|
||||
|
||||
type validationResult struct {
|
||||
Validator
|
||||
CheckValue interface{} `json:"check_value" yaml:"check_value"`
|
||||
CheckResult string `json:"check_result" yaml:"check_result"`
|
||||
}
|
||||
|
||||
type reqResps struct {
|
||||
Request interface{} `json:"request" yaml:"request"`
|
||||
Response interface{} `json:"response" yaml:"response"`
|
||||
}
|
||||
|
||||
type address struct {
|
||||
ClientIP string `json:"client_ip,omitempty" yaml:"client_ip,omitempty"`
|
||||
ClientPort string `json:"client_port,omitempty" yaml:"client_port,omitempty"`
|
||||
ServerIP string `json:"server_ip,omitempty" yaml:"server_ip,omitempty"`
|
||||
ServerPort string `json:"server_port,omitempty" yaml:"server_port,omitempty"`
|
||||
}
|
||||
|
||||
type SessionData struct {
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
ReqResps *reqResps `json:"req_resps" yaml:"req_resps"`
|
||||
Address *address `json:"address,omitempty" yaml:"address,omitempty"` // TODO
|
||||
Validators []*validationResult `json:"validators,omitempty" yaml:"validators,omitempty"`
|
||||
}
|
||||
|
||||
func newSessionData() *SessionData {
|
||||
return &SessionData{
|
||||
Success: false,
|
||||
ReqResps: &reqResps{},
|
||||
}
|
||||
}
|
||||
@@ -553,18 +553,18 @@ func findallVariables(raw string) variableSet {
|
||||
return varSet
|
||||
}
|
||||
|
||||
func genCartesianProduct(paramsMap map[string]paramsType) paramsType {
|
||||
func genCartesianProduct(paramsMap map[string]iteratorParamsType) iteratorParamsType {
|
||||
if len(paramsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
var params []paramsType
|
||||
var params []iteratorParamsType
|
||||
for _, v := range paramsMap {
|
||||
params = append(params, v)
|
||||
}
|
||||
var cartesianProduct paramsType
|
||||
var cartesianProduct iteratorParamsType
|
||||
cartesianProduct = params[0]
|
||||
for i := 0; i < len(params)-1; i++ {
|
||||
var tempProduct paramsType
|
||||
var tempProduct iteratorParamsType
|
||||
for _, param1 := range cartesianProduct {
|
||||
for _, param2 := range params[i+1] {
|
||||
tempProduct = append(tempProduct, mergeVariables(param1, param2))
|
||||
@@ -575,14 +575,14 @@ func genCartesianProduct(paramsMap map[string]paramsType) paramsType {
|
||||
return cartesianProduct
|
||||
}
|
||||
|
||||
func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) (map[string]paramsType, error) {
|
||||
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]paramsType)
|
||||
parsedParametersSlice := make(map[string]iteratorParamsType)
|
||||
var err error
|
||||
for k, v := range parameters {
|
||||
var parameterSlice paramsType
|
||||
var parameterSlice iteratorParamsType
|
||||
rawValue := reflect.ValueOf(v)
|
||||
switch rawValue.Kind() {
|
||||
case reflect.String:
|
||||
@@ -662,7 +662,7 @@ func parseSlice(parameterName string, parameterContent interface{}) ([]map[strin
|
||||
}
|
||||
|
||||
func initParameterIterator(cfg *TConfig, mode string) (err error) {
|
||||
var parameters map[string]paramsType
|
||||
var parameters map[string]iteratorParamsType
|
||||
parameters, err = parseParameters(cfg.Parameters, cfg.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -684,7 +684,7 @@ func initParameterIterator(cfg *TConfig, mode string) (err error) {
|
||||
// use strategy if configured
|
||||
cfg.ParametersSetting.Iterators = append(
|
||||
cfg.ParametersSetting.Iterators,
|
||||
newIterator(v, rawValue.MapIndex(reflect.ValueOf(k)).Interface().(string), cfg.ParametersSetting.Iteration),
|
||||
newIterator(v, rawValue.MapIndex(reflect.ValueOf(k)).Interface().(iteratorStrategyType), cfg.ParametersSetting.Iteration),
|
||||
)
|
||||
} else {
|
||||
// use sequential strategy by default
|
||||
@@ -703,20 +703,20 @@ func initParameterIterator(cfg *TConfig, mode string) (err error) {
|
||||
}
|
||||
cfg.ParametersSetting.Iterators = append(
|
||||
cfg.ParametersSetting.Iterators,
|
||||
newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(string), cfg.ParametersSetting.Iteration),
|
||||
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.(string), cfg.ParametersSetting.Iteration),
|
||||
newIterator(genCartesianProduct(parameters), cfg.ParametersSetting.Strategy.(iteratorStrategyType), cfg.ParametersSetting.Iteration),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newIterator(parameters paramsType, strategy string, iteration int) *Iterator {
|
||||
func newIterator(parameters iteratorParamsType, strategy iteratorStrategyType, iteration int) *Iterator {
|
||||
iter := parameters.Iterator()
|
||||
iter.strategy = strategy
|
||||
if iteration > 0 {
|
||||
|
||||
@@ -115,3 +115,16 @@ func locateFile(startPath string, destFile string) (string, error) {
|
||||
|
||||
return locateFile(parentDir, destFile)
|
||||
}
|
||||
|
||||
func getProjectRootDirPath(path string) (rootDir string, err error) {
|
||||
pluginPath, err := locatePlugin(path)
|
||||
if err == nil {
|
||||
rootDir = filepath.Dir(pluginPath)
|
||||
return
|
||||
}
|
||||
|
||||
// failed to locate project root dir
|
||||
// maybe project plugin debugtalk.xx is not exist
|
||||
// use current dir instead
|
||||
return os.Getwd()
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ type responseObject struct {
|
||||
t *testing.T
|
||||
parser *parser
|
||||
respObjMeta interface{}
|
||||
validationResults []*validationResult
|
||||
validationResults []*ValidationResult
|
||||
}
|
||||
|
||||
const textExtractorSubRegexp string = `(.*)`
|
||||
@@ -147,7 +147,7 @@ func (v *responseObject) Validate(iValidators []interface{}, variablesMapping ma
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validResult := &validationResult{
|
||||
validResult := &ValidationResult{
|
||||
Validator: Validator{
|
||||
Check: validator.Check,
|
||||
Expect: expectValue,
|
||||
|
||||
961
hrp/runner.go
961
hrp/runner.go
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,14 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/scaffold"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/scaffold"
|
||||
)
|
||||
|
||||
func buildHashicorpGoPlugin() {
|
||||
@@ -97,71 +95,6 @@ func assertRunTestCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCaseWithRendezvous(t *testing.T) {
|
||||
rendezvousBoundaryTestcase := &TestCase{
|
||||
Config: NewConfig("run request with functions").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
}),
|
||||
TestSteps: []IStep{
|
||||
NewStep("test negative number").
|
||||
Rendezvous("test negative number").
|
||||
WithUserNumber(-1),
|
||||
NewStep("test overflow number").
|
||||
Rendezvous("test overflow number").
|
||||
WithUserNumber(1000000),
|
||||
NewStep("test negative percent").
|
||||
Rendezvous("test very low percent").
|
||||
WithUserPercent(-0.5),
|
||||
NewStep("test very low percent").
|
||||
Rendezvous("test very low percent").
|
||||
WithUserPercent(0.00001),
|
||||
NewStep("test overflow percent").
|
||||
Rendezvous("test overflow percent").
|
||||
WithUserPercent(1.5),
|
||||
NewStep("test conflict params").
|
||||
Rendezvous("test conflict params").
|
||||
WithUserNumber(1).
|
||||
WithUserPercent(0.123),
|
||||
NewStep("test negative timeout").
|
||||
Rendezvous("test negative timeout").
|
||||
WithTimeout(-1000),
|
||||
},
|
||||
}
|
||||
|
||||
type rendezvousParam struct {
|
||||
number int64
|
||||
percent float32
|
||||
timeout int64
|
||||
}
|
||||
expectedRendezvousParams := []rendezvousParam{
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 0, percent: 0.00001, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
}
|
||||
|
||||
rendezvousList := initRendezvous(rendezvousBoundaryTestcase, 100)
|
||||
|
||||
for i, r := range rendezvousList {
|
||||
if r.Number != expectedRendezvousParams[i].number {
|
||||
t.Fatalf("run rendezvous %v error: expected number: %v, real number: %v", r.Name, expectedRendezvousParams[i].number, r.Number)
|
||||
}
|
||||
if math.Abs(float64(r.Percent-expectedRendezvousParams[i].percent)) > 0.001 {
|
||||
t.Fatalf("run rendezvous %v error: expected percent: %v, real percent: %v", r.Name, expectedRendezvousParams[i].percent, r.Percent)
|
||||
}
|
||||
if r.Timeout != expectedRendezvousParams[i].timeout {
|
||||
t.Fatalf("run rendezvous %v error: expected timeout: %v, real timeout: %v", r.Name, expectedRendezvousParams[i].timeout, r.Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCaseWithThinkTime(t *testing.T) {
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
@@ -220,27 +153,6 @@ func TestRunCaseWithThinkTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenHTMLReport(t *testing.T) {
|
||||
summary := newOutSummary()
|
||||
caseSummary1 := newSummary()
|
||||
caseSummary2 := newSummary()
|
||||
stepResult1 := &stepData{}
|
||||
stepResult2 := &stepData{
|
||||
Name: "Test",
|
||||
StepType: stepTypeRequest,
|
||||
Success: false,
|
||||
ContentSize: 0,
|
||||
Attachment: "err",
|
||||
}
|
||||
caseSummary1.Records = []*stepData{stepResult1, stepResult2, nil}
|
||||
summary.appendCaseSummary(caseSummary1)
|
||||
summary.appendCaseSummary(caseSummary2)
|
||||
err := summary.genHTMLReport()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCaseWithPluginJSON(t *testing.T) {
|
||||
buildHashicorpGoPlugin()
|
||||
defer removeHashicorpGoPlugin()
|
||||
|
||||
150
hrp/session.go
Normal file
150
hrp/session.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// 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
|
||||
sessionVariables map[string]interface{}
|
||||
// transactions stores transaction timing info.
|
||||
// key is transaction name, value is map of transaction type and time, e.g. start time and end time.
|
||||
transactions map[string]map[transactionType]time.Time
|
||||
startTime time.Time // record start time of the testcase
|
||||
summary *TestCaseSummary // record test case summary
|
||||
}
|
||||
|
||||
func (r *SessionRunner) init() {
|
||||
log.Info().Msg("init session runner")
|
||||
r.sessionVariables = make(map[string]interface{})
|
||||
r.transactions = make(map[string]map[transactionType]time.Time)
|
||||
r.startTime = time.Now()
|
||||
r.summary.Name = r.testCase.Config.Name
|
||||
}
|
||||
|
||||
// Run runs the test steps in sequential order.
|
||||
func (r *SessionRunner) Run() error {
|
||||
config := r.testCase.Config
|
||||
log.Info().Str("testcase", config.Name).Msg("run testcase start")
|
||||
|
||||
// init session runner
|
||||
r.init()
|
||||
|
||||
// init plugin
|
||||
var err error
|
||||
if r.parser.plugin, err = initPlugin(config.Path, r.hrpRunner.pluginLogOn); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if r.parser.plugin != nil {
|
||||
r.parser.plugin.Quit()
|
||||
}
|
||||
}()
|
||||
|
||||
// parse config
|
||||
if err := r.parseConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.startTime = time.Now()
|
||||
// run step in sequential order
|
||||
for _, step := range r.testCase.TestSteps {
|
||||
_, err := step.Run(r)
|
||||
if err != nil && r.hrpRunner.failfast {
|
||||
return errors.Wrap(err, "abort running due to failfast setting")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Str("testcase", config.Name).Msg("run testcase end")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SessionRunner) overrideVariables(step *TStep) (*TStep, error) {
|
||||
// copy step and config to avoid data racing
|
||||
copiedStep := &TStep{}
|
||||
if err := copier.Copy(copiedStep, step); err != nil {
|
||||
log.Error().Err(err).Msg("copy step data failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stepVariables := copiedStep.Variables
|
||||
// override variables
|
||||
// step variables > session variables (extracted variables from previous steps)
|
||||
stepVariables = mergeVariables(stepVariables, r.sessionVariables)
|
||||
// step variables > testcase config variables
|
||||
stepVariables = mergeVariables(stepVariables, r.testCase.Config.Variables)
|
||||
|
||||
// parse step variables
|
||||
parsedVariables, err := r.parser.parseVariables(stepVariables)
|
||||
if err != nil {
|
||||
log.Error().Interface("variables", r.testCase.Config.Variables).Err(err).Msg("parse step variables failed")
|
||||
return nil, err
|
||||
}
|
||||
copiedStep.Variables = parsedVariables // avoid data racing
|
||||
return copiedStep, nil
|
||||
}
|
||||
|
||||
func (r *SessionRunner) overrideConfig(step *TStep) {
|
||||
// override headers
|
||||
if r.testCase.Config.Headers != nil {
|
||||
step.Request.Headers = mergeMap(step.Request.Headers, r.testCase.Config.Headers)
|
||||
}
|
||||
// parse step request url
|
||||
requestUrl, err := r.parser.parseString(step.Request.URL, step.Variables)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("parse request url failed")
|
||||
requestUrl = step.Variables
|
||||
}
|
||||
step.Request.URL = buildURL(r.testCase.Config.BaseURL, convertString(requestUrl)) // avoid data racing
|
||||
}
|
||||
|
||||
func (r *SessionRunner) parseConfig(cfg *TConfig) error {
|
||||
// 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
|
||||
}
|
||||
cfg.Variables = parsedVariables
|
||||
|
||||
// parse config name
|
||||
parsedName, err := r.parser.parseString(cfg.Name, cfg.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Name = convertString(parsedName)
|
||||
|
||||
// parse config base url
|
||||
parsedBaseURL, err := r.parser.parseString(cfg.BaseURL, cfg.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.BaseURL = convertString(parsedBaseURL)
|
||||
|
||||
// ensure correction of think time config
|
||||
cfg.ThinkTimeSetting.checkThinkTime()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SessionRunner) getSummary() *TestCaseSummary {
|
||||
caseSummary := r.summary
|
||||
caseSummary.Time.StartAt = r.startTime
|
||||
caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
|
||||
exportVars := make(map[string]interface{})
|
||||
for _, value := range r.testCase.Config.Export {
|
||||
exportVars[value] = r.sessionVariables[value]
|
||||
}
|
||||
caseSummary.InOut.ExportVars = exportVars
|
||||
caseSummary.InOut.ConfigVars = r.testCase.Config.Variables
|
||||
return caseSummary
|
||||
}
|
||||
520
hrp/step.go
520
hrp/step.go
@@ -1,480 +1,52 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
type StepType string
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
const (
|
||||
stepTypeRequest StepType = "request"
|
||||
stepTypeAPI StepType = "api"
|
||||
stepTypeTestCase StepType = "testcase"
|
||||
stepTypeTransaction StepType = "transaction"
|
||||
stepTypeRendezvous StepType = "rendezvous"
|
||||
stepTypeThinkTime StepType = "thinktime"
|
||||
)
|
||||
|
||||
// NewConfig returns a new constructed testcase config with specified testcase name.
|
||||
func NewConfig(name string) *TConfig {
|
||||
return &TConfig{
|
||||
Name: name,
|
||||
Variables: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current testcase.
|
||||
func (c *TConfig) WithVariables(variables map[string]interface{}) *TConfig {
|
||||
c.Variables = variables
|
||||
return c
|
||||
}
|
||||
|
||||
// SetBaseURL sets base URL for current testcase.
|
||||
func (c *TConfig) SetBaseURL(baseURL string) *TConfig {
|
||||
c.BaseURL = baseURL
|
||||
return c
|
||||
}
|
||||
|
||||
// SetHeaders sets global headers for current testcase.
|
||||
func (c *TConfig) SetHeaders(headers map[string]string) *TConfig {
|
||||
c.Headers = headers
|
||||
return c
|
||||
}
|
||||
|
||||
// SetVerifySSL sets whether to verify SSL for current testcase.
|
||||
func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
|
||||
c.Verify = verify
|
||||
return c
|
||||
}
|
||||
|
||||
// WithParameters sets parameters for current testcase.
|
||||
func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
|
||||
c.Parameters = parameters
|
||||
return c
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time config for current testcase.
|
||||
func (c *TConfig) SetThinkTime(strategy string, cfg interface{}, limit float64) *TConfig {
|
||||
c.ThinkTime = &ThinkTimeConfig{strategy, cfg, limit}
|
||||
return c
|
||||
}
|
||||
|
||||
// ExportVars specifies variable names to export for current testcase.
|
||||
func (c *TConfig) ExportVars(vars ...string) *TConfig {
|
||||
c.Export = vars
|
||||
return c
|
||||
}
|
||||
|
||||
// SetWeight sets weight for current testcase, which is used in load testing.
|
||||
func (c *TConfig) SetWeight(weight int) *TConfig {
|
||||
c.Weight = weight
|
||||
return c
|
||||
}
|
||||
|
||||
// NewStep returns a new constructed teststep with specified step name.
|
||||
func NewStep(name string) *StepRequest {
|
||||
return &StepRequest{
|
||||
step: &TStep{
|
||||
Name: name,
|
||||
Variables: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type StepRequest struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current teststep.
|
||||
func (s *StepRequest) WithVariables(variables map[string]interface{}) *StepRequest {
|
||||
s.step.Variables = variables
|
||||
return s
|
||||
}
|
||||
|
||||
// SetupHook adds a setup hook for current teststep.
|
||||
func (s *StepRequest) SetupHook(hook string) *StepRequest {
|
||||
s.step.SetupHooks = append(s.step.SetupHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// GET makes a HTTP GET request.
|
||||
func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpGET,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// HEAD makes a HTTP HEAD request.
|
||||
func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpHEAD,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// POST makes a HTTP POST request.
|
||||
func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPOST,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// PUT makes a HTTP PUT request.
|
||||
func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPUT,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE makes a HTTP DELETE request.
|
||||
func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpDELETE,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// OPTIONS makes a HTTP OPTIONS request.
|
||||
func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpOPTIONS,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH makes a HTTP PATCH request.
|
||||
func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPATCH,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// CallRefCase calls a referenced testcase.
|
||||
func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs {
|
||||
var err error
|
||||
s.step.TestCase, err = tc.ToTestCase()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load testcase")
|
||||
os.Exit(1)
|
||||
}
|
||||
return &StepTestCaseWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// CallRefAPI calls a referenced api.
|
||||
func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs {
|
||||
var err error
|
||||
s.step.API, err = api.ToAPI()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load api")
|
||||
os.Exit(1)
|
||||
}
|
||||
return &StepAPIWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// StartTransaction starts a transaction.
|
||||
func (s *StepRequest) StartTransaction(name string) *StepTransaction {
|
||||
s.step.Transaction = &Transaction{
|
||||
Name: name,
|
||||
Type: transactionStart,
|
||||
}
|
||||
return &StepTransaction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// EndTransaction ends a transaction.
|
||||
func (s *StepRequest) EndTransaction(name string) *StepTransaction {
|
||||
s.step.Transaction = &Transaction{
|
||||
Name: name,
|
||||
Type: transactionEnd,
|
||||
}
|
||||
return &StepTransaction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time.
|
||||
func (s *StepRequest) SetThinkTime(time float64) *StepThinkTime {
|
||||
s.step.ThinkTime = &ThinkTime{
|
||||
Time: time,
|
||||
}
|
||||
return &StepThinkTime{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// StepRequestWithOptionalArgs implements IStep interface.
|
||||
type StepRequestWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// SetVerify sets whether to verify SSL for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetVerify(verify bool) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Verify = verify
|
||||
return s
|
||||
}
|
||||
|
||||
// SetTimeout sets timeout for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetTimeout(timeout float32) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Timeout = timeout
|
||||
return s
|
||||
}
|
||||
|
||||
// SetProxies sets proxies for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetProxies(proxies map[string]string) *StepRequestWithOptionalArgs {
|
||||
// TODO
|
||||
return s
|
||||
}
|
||||
|
||||
// SetAllowRedirects sets whether to allow redirects for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetAllowRedirects(allowRedirects bool) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.AllowRedirects = allowRedirects
|
||||
return s
|
||||
}
|
||||
|
||||
// SetAuth sets auth for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetAuth(auth map[string]string) *StepRequestWithOptionalArgs {
|
||||
// TODO
|
||||
return s
|
||||
}
|
||||
|
||||
// WithParams sets HTTP request params for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithParams(params map[string]interface{}) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Params = params
|
||||
return s
|
||||
}
|
||||
|
||||
// WithHeaders sets HTTP request headers for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithHeaders(headers map[string]string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Headers = headers
|
||||
return s
|
||||
}
|
||||
|
||||
// WithCookies sets HTTP request cookies for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithCookies(cookies map[string]string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Cookies = cookies
|
||||
return s
|
||||
}
|
||||
|
||||
// WithBody sets HTTP request body for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithBody(body interface{}) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Body = body
|
||||
return s
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepRequestWithOptionalArgs) TeardownHook(hook string) *StepRequestWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Validate switches to step validation.
|
||||
func (s *StepRequestWithOptionalArgs) Validate() *StepRequestValidation {
|
||||
return &StepRequestValidation{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract switches to step extraction.
|
||||
func (s *StepRequestWithOptionalArgs) Extract() *StepRequestExtraction {
|
||||
s.step.Extract = make(map[string]string)
|
||||
return &StepRequestExtraction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("%s %s", s.step.Request.Method, s.step.Request.URL)
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) Type() string {
|
||||
return fmt.Sprintf("request-%v", s.step.Request.Method)
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepAPIWithOptionalArgs implements IStep interface.
|
||||
type StepAPIWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepAPIWithOptionalArgs) TeardownHook(hook string) *StepAPIWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Export specifies variable names to export from referenced api for current step.
|
||||
func (s *StepAPIWithOptionalArgs) Export(names ...string) *StepAPIWithOptionalArgs {
|
||||
api, ok := s.step.API.(*API)
|
||||
if ok {
|
||||
s.step.Export = append(api.Export, names...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
api, ok := s.step.API.(*API)
|
||||
if ok {
|
||||
return api.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) Type() string {
|
||||
return "api"
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepTestCaseWithOptionalArgs implements IStep interface.
|
||||
type StepTestCaseWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepTestCaseWithOptionalArgs) TeardownHook(hook string) *StepTestCaseWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Export specifies variable names to export from referenced testcase for current step.
|
||||
func (s *StepTestCaseWithOptionalArgs) Export(names ...string) *StepTestCaseWithOptionalArgs {
|
||||
s.step.Export = append(s.step.Export, names...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
ts, ok := s.step.TestCase.(*TestCase)
|
||||
if ok {
|
||||
return ts.Config.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) Type() string {
|
||||
return "testcase"
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepThinkTime implements IStep interface.
|
||||
type StepThinkTime struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Type() string {
|
||||
return "thinktime"
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepTransaction implements IStep interface.
|
||||
type StepTransaction struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepTransaction) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("transaction %s %s", s.step.Transaction.Name, s.step.Transaction.Type)
|
||||
}
|
||||
|
||||
func (s *StepTransaction) Type() string {
|
||||
return "transaction"
|
||||
}
|
||||
|
||||
func (s *StepTransaction) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// StepRendezvous implements IStep interface.
|
||||
type StepRendezvous struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return s.step.Rendezvous.Name
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) Type() string {
|
||||
return "rendezvous"
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
// Rendezvous creates a new rendezvous
|
||||
func (s *StepRequest) Rendezvous(name string) *StepRendezvous {
|
||||
s.step.Rendezvous = &Rendezvous{
|
||||
Name: name,
|
||||
}
|
||||
return &StepRendezvous{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserNumber sets the user number needed to release the current rendezvous
|
||||
func (s *StepRendezvous) WithUserNumber(number int64) *StepRendezvous {
|
||||
s.step.Rendezvous.Number = number
|
||||
return s
|
||||
}
|
||||
|
||||
// WithUserPercent sets the user percent needed to release the current rendezvous
|
||||
func (s *StepRendezvous) WithUserPercent(percent float32) *StepRendezvous {
|
||||
s.step.Rendezvous.Percent = percent
|
||||
return s
|
||||
}
|
||||
|
||||
// WithTimeout sets the timeout of duration between each user arriving at the current rendezvous
|
||||
func (s *StepRendezvous) WithTimeout(timeout int64) *StepRendezvous {
|
||||
s.step.Rendezvous.Timeout = timeout
|
||||
return s
|
||||
type StepResult struct {
|
||||
Name string `json:"name" yaml:"name"` // step name
|
||||
StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
|
||||
Success bool `json:"success" yaml:"success"` // step execution result
|
||||
Elapsed int64 `json:"elapsed_ms" yaml:"elapsed_ms"` // step execution time in millisecond(ms)
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data
|
||||
ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length
|
||||
ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables
|
||||
Attachment string `json:"attachment,omitempty" yaml:"attachment,omitempty"` // step error information
|
||||
}
|
||||
|
||||
// TStep represents teststep data structure.
|
||||
// Each step maybe three different types: make one request or reference another api/testcase.
|
||||
type TStep struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
||||
API interface{} `json:"api,omitempty" yaml:"api,omitempty"` // *APIPath or *API
|
||||
TestCase interface{} `json:"testcase,omitempty" yaml:"testcase,omitempty"` // *TestCasePath or *TestCase
|
||||
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
|
||||
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
|
||||
ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
}
|
||||
|
||||
// IStep represents interface for all types for teststeps, includes:
|
||||
// StepRequest, StepRequestWithOptionalArgs, StepRequestValidation, StepRequestExtraction,
|
||||
// StepTestCaseWithOptionalArgs,
|
||||
// StepTransaction, StepRendezvous.
|
||||
type IStep interface {
|
||||
Name() string
|
||||
Type() StepType
|
||||
ToStruct() *TStep
|
||||
Run(*SessionRunner) (*StepResult, error)
|
||||
}
|
||||
|
||||
109
hrp/step_api.go
Normal file
109
hrp/step_api.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
// IAPI represents interface for api,
|
||||
// includes API and APIPath.
|
||||
type IAPI interface {
|
||||
GetPath() string
|
||||
ToAPI() (*API, error)
|
||||
}
|
||||
|
||||
type API struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Path string
|
||||
}
|
||||
|
||||
func (api *API) GetPath() string {
|
||||
return api.Path
|
||||
}
|
||||
|
||||
func (api *API) ToAPI() (*API, error) {
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// APIPath implements IAPI interface.
|
||||
type APIPath string
|
||||
|
||||
func (path *APIPath) GetPath() string {
|
||||
return fmt.Sprintf("%v", *path)
|
||||
}
|
||||
|
||||
func (path *APIPath) ToAPI() (*API, error) {
|
||||
api := &API{}
|
||||
apiPath := path.GetPath()
|
||||
err := builtin.LoadFile(apiPath, api)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = convertCompatValidator(api.Validators)
|
||||
return api, err
|
||||
}
|
||||
|
||||
// StepAPIWithOptionalArgs implements IStep interface.
|
||||
type StepAPIWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepAPIWithOptionalArgs) TeardownHook(hook string) *StepAPIWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Export specifies variable names to export from referenced api for current step.
|
||||
func (s *StepAPIWithOptionalArgs) Export(names ...string) *StepAPIWithOptionalArgs {
|
||||
api, ok := s.step.API.(*API)
|
||||
if ok {
|
||||
s.step.Export = append(api.Export, names...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
api, ok := s.step.API.(*API)
|
||||
if ok {
|
||||
return api.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) Type() StepType {
|
||||
return stepTypeAPI
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
|
||||
log.Info().Str("api", s.step.Name).Msg("run referenced api")
|
||||
|
||||
// extend request with referenced API
|
||||
api, _ := s.step.API.(*API)
|
||||
extendWithAPI(s.step, api)
|
||||
|
||||
stepResult, err := runStepRequest(r, s.step)
|
||||
if err != nil {
|
||||
r.summary.Success = false
|
||||
return nil, err
|
||||
}
|
||||
stepResult.StepType = stepTypeAPI
|
||||
return stepResult, nil
|
||||
}
|
||||
259
hrp/step_rendezvous.go
Normal file
259
hrp/step_rendezvous.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// StepRendezvous implements IStep interface.
|
||||
type StepRendezvous struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return s.step.Rendezvous.Name
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) Type() StepType {
|
||||
return stepTypeRendezvous
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepRendezvous) Run(r *SessionRunner) (*StepResult, error) {
|
||||
rendezvous := s.step.Rendezvous
|
||||
log.Info().
|
||||
Str("name", rendezvous.Name).
|
||||
Float32("percent", rendezvous.Percent).
|
||||
Int64("number", rendezvous.Number).
|
||||
Int64("timeout", rendezvous.Timeout).
|
||||
Msg("rendezvous")
|
||||
|
||||
stepResult := &StepResult{
|
||||
Name: rendezvous.Name,
|
||||
StepType: stepTypeRendezvous,
|
||||
Success: true,
|
||||
}
|
||||
|
||||
// pass current rendezvous if already released, activate rendezvous sequentially after spawn done
|
||||
if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.testCase.ToTCase()) || !rendezvous.isSpawnDone() {
|
||||
return stepResult, nil
|
||||
}
|
||||
|
||||
// activate the rendezvous only once during each cycle
|
||||
rendezvous.once.Do(func() {
|
||||
close(rendezvous.activateChan)
|
||||
})
|
||||
|
||||
// check current cnt using double check lock before updating to avoid negative WaitGroup counter
|
||||
if atomic.LoadInt64(&rendezvous.cnt) < rendezvous.Number {
|
||||
rendezvous.lock.Lock()
|
||||
if atomic.LoadInt64(&rendezvous.cnt) < rendezvous.Number {
|
||||
atomic.AddInt64(&rendezvous.cnt, 1)
|
||||
rendezvous.wg.Done()
|
||||
rendezvous.timerResetChan <- struct{}{}
|
||||
}
|
||||
rendezvous.lock.Unlock()
|
||||
}
|
||||
|
||||
// block until current rendezvous released
|
||||
<-rendezvous.releaseChan
|
||||
return stepResult, nil
|
||||
}
|
||||
|
||||
func isPreRendezvousAllReleased(rendezvous *Rendezvous, testCase *TCase) bool {
|
||||
for _, step := range testCase.TestSteps {
|
||||
preRendezvous := step.Rendezvous
|
||||
if preRendezvous == nil {
|
||||
continue
|
||||
}
|
||||
// meet current rendezvous, all previous rendezvous released, return true
|
||||
if preRendezvous == rendezvous {
|
||||
return true
|
||||
}
|
||||
if !preRendezvous.isReleased() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Rendezvous creates a new rendezvous
|
||||
func (s *StepRequest) Rendezvous(name string) *StepRendezvous {
|
||||
s.step.Rendezvous = &Rendezvous{
|
||||
Name: name,
|
||||
}
|
||||
return &StepRendezvous{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserNumber sets the user number needed to release the current rendezvous
|
||||
func (s *StepRendezvous) WithUserNumber(number int64) *StepRendezvous {
|
||||
s.step.Rendezvous.Number = number
|
||||
return s
|
||||
}
|
||||
|
||||
// WithUserPercent sets the user percent needed to release the current rendezvous
|
||||
func (s *StepRendezvous) WithUserPercent(percent float32) *StepRendezvous {
|
||||
s.step.Rendezvous.Percent = percent
|
||||
return s
|
||||
}
|
||||
|
||||
// WithTimeout sets the timeout of duration between each user arriving at the current rendezvous
|
||||
func (s *StepRendezvous) WithTimeout(timeout int64) *StepRendezvous {
|
||||
s.step.Rendezvous.Timeout = timeout
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRendezvousTimeout int64 = 5000
|
||||
defaultRendezvousPercent float32 = 1.0
|
||||
)
|
||||
|
||||
type Rendezvous struct {
|
||||
Name string `json:"name" yaml:"name"` // required
|
||||
Percent float32 `json:"percent,omitempty" yaml:"percent,omitempty"` // default to 1(100%)
|
||||
Number int64 `json:"number,omitempty" yaml:"number,omitempty"`
|
||||
Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // milliseconds
|
||||
cnt int64
|
||||
releasedFlag uint32
|
||||
spawnDoneFlag uint32
|
||||
wg sync.WaitGroup
|
||||
timerResetChan chan struct{}
|
||||
activateChan chan struct{}
|
||||
releaseChan chan struct{}
|
||||
once *sync.Once
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (r *Rendezvous) reset() {
|
||||
r.cnt = 0
|
||||
r.releasedFlag = 0
|
||||
r.wg.Add(int(r.Number))
|
||||
// timerResetChan channel will not be closed, thus init only once
|
||||
if r.timerResetChan == nil {
|
||||
r.timerResetChan = make(chan struct{})
|
||||
}
|
||||
r.activateChan = make(chan struct{})
|
||||
r.releaseChan = make(chan struct{})
|
||||
r.once = new(sync.Once)
|
||||
}
|
||||
|
||||
func (r *Rendezvous) isSpawnDone() bool {
|
||||
return atomic.LoadUint32(&r.spawnDoneFlag) == 1
|
||||
}
|
||||
|
||||
func (r *Rendezvous) setSpawnDone() {
|
||||
atomic.StoreUint32(&r.spawnDoneFlag, 1)
|
||||
}
|
||||
|
||||
func (r *Rendezvous) isReleased() bool {
|
||||
return atomic.LoadUint32(&r.releasedFlag) == 1
|
||||
}
|
||||
|
||||
func (r *Rendezvous) setReleased() {
|
||||
atomic.StoreUint32(&r.releasedFlag, 1)
|
||||
}
|
||||
|
||||
func initRendezvous(testcase *TestCase, total int64) []*Rendezvous {
|
||||
tCase := testcase.ToTCase()
|
||||
var rendezvousList []*Rendezvous
|
||||
for _, step := range tCase.TestSteps {
|
||||
if step.Rendezvous == nil {
|
||||
continue
|
||||
}
|
||||
rendezvous := step.Rendezvous
|
||||
|
||||
// either number or percent should be correctly put, otherwise set to default (total)
|
||||
if rendezvous.Number == 0 && rendezvous.Percent > 0 && rendezvous.Percent <= defaultRendezvousPercent {
|
||||
rendezvous.Number = int64(rendezvous.Percent * float32(total))
|
||||
} else if rendezvous.Number > 0 && rendezvous.Number <= total && rendezvous.Percent == 0 {
|
||||
rendezvous.Percent = float32(rendezvous.Number) / float32(total)
|
||||
} else {
|
||||
log.Warn().
|
||||
Str("name", rendezvous.Name).
|
||||
Int64("default number", total).
|
||||
Float32("default percent", defaultRendezvousPercent).
|
||||
Msg("rendezvous parameter not defined or error, set to default value")
|
||||
rendezvous.Number = total
|
||||
rendezvous.Percent = defaultRendezvousPercent
|
||||
}
|
||||
|
||||
if rendezvous.Timeout <= 0 {
|
||||
rendezvous.Timeout = defaultRendezvousTimeout
|
||||
}
|
||||
|
||||
rendezvous.reset()
|
||||
rendezvousList = append(rendezvousList, rendezvous)
|
||||
}
|
||||
return rendezvousList
|
||||
}
|
||||
|
||||
func waitRendezvous(rendezvousList []*Rendezvous) {
|
||||
if rendezvousList != nil {
|
||||
lastRendezvous := rendezvousList[len(rendezvousList)-1]
|
||||
for _, rendezvous := range rendezvousList {
|
||||
go waitSingleRendezvous(rendezvous, rendezvousList, lastRendezvous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitSingleRendezvous(rendezvous *Rendezvous, rendezvousList []*Rendezvous, lastRendezvous *Rendezvous) {
|
||||
for {
|
||||
// cycle start: block current checking until current rendezvous activated
|
||||
<-rendezvous.activateChan
|
||||
stop := make(chan struct{})
|
||||
timeout := time.Duration(rendezvous.Timeout) * time.Millisecond
|
||||
timer := time.NewTimer(timeout)
|
||||
go func() {
|
||||
defer close(stop)
|
||||
rendezvous.wg.Wait()
|
||||
}()
|
||||
for !rendezvous.isReleased() {
|
||||
select {
|
||||
case <-rendezvous.timerResetChan:
|
||||
timer.Reset(timeout)
|
||||
case <-stop:
|
||||
rendezvous.setReleased()
|
||||
close(rendezvous.releaseChan)
|
||||
log.Info().
|
||||
Str("name", rendezvous.Name).
|
||||
Float32("percent", rendezvous.Percent).
|
||||
Int64("number", rendezvous.Number).
|
||||
Int64("timeout(ms)", rendezvous.Timeout).
|
||||
Int64("cnt", rendezvous.cnt).
|
||||
Str("reason", "rendezvous release condition satisfied").
|
||||
Msg("rendezvous released")
|
||||
case <-timer.C:
|
||||
rendezvous.setReleased()
|
||||
close(rendezvous.releaseChan)
|
||||
log.Info().
|
||||
Str("name", rendezvous.Name).
|
||||
Float32("percent", rendezvous.Percent).
|
||||
Int64("number", rendezvous.Number).
|
||||
Int64("timeout(ms)", rendezvous.Timeout).
|
||||
Int64("cnt", rendezvous.cnt).
|
||||
Str("reason", "time's up").
|
||||
Msg("rendezvous released")
|
||||
}
|
||||
}
|
||||
// cycle end: reset all previous rendezvous after last rendezvous released
|
||||
// otherwise, block current checker until the last rendezvous end
|
||||
if rendezvous == lastRendezvous {
|
||||
for _, r := range rendezvousList {
|
||||
r.reset()
|
||||
}
|
||||
} else {
|
||||
<-lastRendezvous.releaseChan
|
||||
}
|
||||
}
|
||||
}
|
||||
71
hrp/step_rendezvous_test.go
Normal file
71
hrp/step_rendezvous_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunCaseWithRendezvous(t *testing.T) {
|
||||
rendezvousBoundaryTestcase := &TestCase{
|
||||
Config: NewConfig("run request with functions").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
}),
|
||||
TestSteps: []IStep{
|
||||
NewStep("test negative number").
|
||||
Rendezvous("test negative number").
|
||||
WithUserNumber(-1),
|
||||
NewStep("test overflow number").
|
||||
Rendezvous("test overflow number").
|
||||
WithUserNumber(1000000),
|
||||
NewStep("test negative percent").
|
||||
Rendezvous("test very low percent").
|
||||
WithUserPercent(-0.5),
|
||||
NewStep("test very low percent").
|
||||
Rendezvous("test very low percent").
|
||||
WithUserPercent(0.00001),
|
||||
NewStep("test overflow percent").
|
||||
Rendezvous("test overflow percent").
|
||||
WithUserPercent(1.5),
|
||||
NewStep("test conflict params").
|
||||
Rendezvous("test conflict params").
|
||||
WithUserNumber(1).
|
||||
WithUserPercent(0.123),
|
||||
NewStep("test negative timeout").
|
||||
Rendezvous("test negative timeout").
|
||||
WithTimeout(-1000),
|
||||
},
|
||||
}
|
||||
|
||||
type rendezvousParam struct {
|
||||
number int64
|
||||
percent float32
|
||||
timeout int64
|
||||
}
|
||||
expectedRendezvousParams := []rendezvousParam{
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 0, percent: 0.00001, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
{number: 100, percent: 1, timeout: 5000},
|
||||
}
|
||||
|
||||
rendezvousList := initRendezvous(rendezvousBoundaryTestcase, 100)
|
||||
|
||||
for i, r := range rendezvousList {
|
||||
if r.Number != expectedRendezvousParams[i].number {
|
||||
t.Fatalf("run rendezvous %v error: expected number: %v, real number: %v", r.Name, expectedRendezvousParams[i].number, r.Number)
|
||||
}
|
||||
if math.Abs(float64(r.Percent-expectedRendezvousParams[i].percent)) > 0.001 {
|
||||
t.Fatalf("run rendezvous %v error: expected percent: %v, real percent: %v", r.Name, expectedRendezvousParams[i].percent, r.Percent)
|
||||
}
|
||||
if r.Timeout != expectedRendezvousParams[i].timeout {
|
||||
t.Fatalf("run rendezvous %v error: expected timeout: %v, real timeout: %v", r.Name, expectedRendezvousParams[i].timeout, r.Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
924
hrp/step_request.go
Normal file
924
hrp/step_request.go
Normal file
@@ -0,0 +1,924 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"compress/zlib"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
type HTTPMethod string
|
||||
|
||||
const (
|
||||
httpGET HTTPMethod = "GET"
|
||||
httpHEAD HTTPMethod = "HEAD"
|
||||
httpPOST HTTPMethod = "POST"
|
||||
httpPUT HTTPMethod = "PUT"
|
||||
httpDELETE HTTPMethod = "DELETE"
|
||||
httpOPTIONS HTTPMethod = "OPTIONS"
|
||||
httpPATCH HTTPMethod = "PATCH"
|
||||
)
|
||||
|
||||
// Request represents HTTP request data structure.
|
||||
// This is used for teststep.
|
||||
type Request struct {
|
||||
Method HTTPMethod `json:"method" yaml:"method"` // required
|
||||
URL string `json:"url" yaml:"url"` // required
|
||||
Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
Cookies map[string]string `json:"cookies,omitempty" yaml:"cookies,omitempty"`
|
||||
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
Json interface{} `json:"json,omitempty" yaml:"json,omitempty"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Timeout float32 `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
AllowRedirects bool `json:"allow_redirects,omitempty" yaml:"allow_redirects,omitempty"`
|
||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||
}
|
||||
|
||||
func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) {
|
||||
|
||||
step, err = r.overrideVariables(step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.overrideConfig(step)
|
||||
|
||||
log.Info().Str("step", step.Name).Msg("run step start")
|
||||
stepResult = &StepResult{
|
||||
Name: step.Name,
|
||||
StepType: stepTypeRequest,
|
||||
Success: false,
|
||||
ContentSize: 0,
|
||||
}
|
||||
sessionData := newSessionData()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("run request step failed")
|
||||
stepResult.Attachment = err.Error()
|
||||
r.summary.Success = false
|
||||
} else {
|
||||
log.Info().
|
||||
Str("step", step.Name).
|
||||
Bool("success", stepResult.Success).
|
||||
Interface("exportVars", stepResult.ExportVars).
|
||||
Msg("run step end")
|
||||
}
|
||||
}()
|
||||
|
||||
// convert request struct to map
|
||||
jsonRequest, _ := json.Marshal(&step.Request)
|
||||
var requestMap map[string]interface{}
|
||||
_ = json.Unmarshal(jsonRequest, &requestMap)
|
||||
|
||||
rawUrl := step.Request.URL
|
||||
method := step.Request.Method
|
||||
req := &http.Request{
|
||||
Method: string(method),
|
||||
Header: make(http.Header),
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
}
|
||||
|
||||
// prepare request headers
|
||||
if len(step.Request.Headers) > 0 {
|
||||
headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "parse headers failed")
|
||||
}
|
||||
for key, value := range headers {
|
||||
// omit pseudo header names for HTTP/1, e.g. :authority, :method, :path, :scheme
|
||||
if strings.HasPrefix(key, ":") {
|
||||
continue
|
||||
}
|
||||
req.Header.Add(key, value)
|
||||
|
||||
// prepare content length
|
||||
if strings.EqualFold(key, "Content-Length") && value != "" {
|
||||
if l, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
req.ContentLength = l
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepare request params
|
||||
var queryParams url.Values
|
||||
if len(step.Request.Params) > 0 {
|
||||
params, err := r.parser.parseData(step.Request.Params, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "parse request params failed")
|
||||
}
|
||||
parsedParams := params.(map[string]interface{})
|
||||
requestMap["params"] = parsedParams
|
||||
if len(parsedParams) > 0 {
|
||||
queryParams = make(url.Values)
|
||||
for k, v := range parsedParams {
|
||||
queryParams.Add(k, fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
if queryParams != nil {
|
||||
// append params to url
|
||||
paramStr := queryParams.Encode()
|
||||
if strings.IndexByte(rawUrl, '?') == -1 {
|
||||
rawUrl = rawUrl + "?" + paramStr
|
||||
} else {
|
||||
rawUrl = rawUrl + "&" + paramStr
|
||||
}
|
||||
}
|
||||
|
||||
// prepare request cookies
|
||||
for cookieName, cookieValue := range step.Request.Cookies {
|
||||
value, err := r.parser.parseData(cookieValue, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "parse cookie value failed")
|
||||
}
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
})
|
||||
}
|
||||
|
||||
// prepare request body
|
||||
if step.Request.Body != nil {
|
||||
data, err := r.parser.parseData(step.Request.Body, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, err
|
||||
}
|
||||
// check request body format if Content-Type specified as application/json
|
||||
if strings.HasPrefix(req.Header.Get("Content-Type"), "application/json") {
|
||||
switch data.(type) {
|
||||
case bool, float64, string, map[string]interface{}, []interface{}, nil:
|
||||
break
|
||||
default:
|
||||
return stepResult, errors.Errorf("request body type inconsistent with Content-Type: %v", req.Header.Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
requestMap["body"] = data
|
||||
var dataBytes []byte
|
||||
switch vv := data.(type) {
|
||||
case map[string]interface{}:
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
// post form data
|
||||
formData := make(url.Values)
|
||||
for k, v := range vv {
|
||||
formData.Add(k, fmt.Sprint(v))
|
||||
}
|
||||
dataBytes = []byte(formData.Encode())
|
||||
} else {
|
||||
// post json
|
||||
dataBytes, err = json.Marshal(vv)
|
||||
if err != nil {
|
||||
return stepResult, err
|
||||
}
|
||||
if contentType == "" {
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
// post json
|
||||
dataBytes, err = json.Marshal(vv)
|
||||
if err != nil {
|
||||
return stepResult, err
|
||||
}
|
||||
if contentType == "" {
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
case string:
|
||||
dataBytes = []byte(vv)
|
||||
case []byte:
|
||||
dataBytes = vv
|
||||
case bytes.Buffer:
|
||||
dataBytes = vv.Bytes()
|
||||
default: // unexpected body type
|
||||
return stepResult, errors.New("unexpected request body type")
|
||||
}
|
||||
|
||||
req.Body = io.NopCloser(bytes.NewReader(dataBytes))
|
||||
req.ContentLength = int64(len(dataBytes))
|
||||
}
|
||||
// update header
|
||||
headers := make(map[string]string)
|
||||
for key, value := range req.Header {
|
||||
headers[key] = value[0]
|
||||
}
|
||||
requestMap["headers"] = headers
|
||||
|
||||
// prepare url
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "parse url failed")
|
||||
}
|
||||
req.URL = u
|
||||
req.Host = u.Host
|
||||
|
||||
// add request object to step variables, could be used in setup hooks
|
||||
step.Variables["hrp_step_name"] = step.Name
|
||||
step.Variables["hrp_step_request"] = requestMap
|
||||
|
||||
// deal with setup hooks
|
||||
for _, setupHook := range step.SetupHooks {
|
||||
_, err = r.parser.parseData(setupHook, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run setup hooks failed")
|
||||
}
|
||||
}
|
||||
|
||||
// log & print request
|
||||
if r.hrpRunner.requestsLogOn {
|
||||
if err := printRequest(req); err != nil {
|
||||
return stepResult, err
|
||||
}
|
||||
}
|
||||
|
||||
// do request action
|
||||
start := time.Now()
|
||||
resp, err := r.hrpRunner.client.Do(req)
|
||||
stepResult.Elapsed = time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "do request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// decode response body in br/gzip/deflate formats
|
||||
err = decodeResponseBody(resp)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "decode response body failed")
|
||||
}
|
||||
|
||||
// log & print response
|
||||
if r.hrpRunner.requestsLogOn {
|
||||
if err := printResponse(resp); err != nil {
|
||||
return stepResult, err
|
||||
}
|
||||
}
|
||||
|
||||
// new response object
|
||||
respObj, err := newResponseObject(r.hrpRunner.t, r.parser, resp)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "init ResponseObject error")
|
||||
return
|
||||
}
|
||||
|
||||
// add response object to step variables, could be used in teardown hooks
|
||||
step.Variables["hrp_step_response"] = respObj.respObjMeta
|
||||
|
||||
// deal with teardown hooks
|
||||
for _, teardownHook := range step.TeardownHooks {
|
||||
_, err = r.parser.parseData(teardownHook, step.Variables)
|
||||
if err != nil {
|
||||
return stepResult, errors.Wrap(err, "run teardown hooks failed")
|
||||
}
|
||||
}
|
||||
|
||||
sessionData.ReqResps.Request = requestMap
|
||||
sessionData.ReqResps.Response = builtin.FormatResponse(respObj.respObjMeta)
|
||||
|
||||
// extract variables from response
|
||||
extractors := step.Extract
|
||||
extractMapping := respObj.Extract(extractors)
|
||||
stepResult.ExportVars = extractMapping
|
||||
|
||||
// update extracted variables
|
||||
for k, v := range stepResult.ExportVars {
|
||||
r.sessionVariables[k] = v
|
||||
}
|
||||
|
||||
// override step variables with extracted variables
|
||||
stepVariables := mergeVariables(step.Variables, extractMapping)
|
||||
|
||||
// validate response
|
||||
err = respObj.Validate(step.Validators, stepVariables)
|
||||
sessionData.Validators = respObj.validationResults
|
||||
if err == nil {
|
||||
sessionData.Success = true
|
||||
stepResult.Success = true
|
||||
}
|
||||
stepResult.ContentSize = resp.ContentLength
|
||||
stepResult.Data = sessionData
|
||||
|
||||
// append step result to summary
|
||||
r.summary.Records = append(r.summary.Records, stepResult)
|
||||
r.summary.Stat.Total += 1
|
||||
if stepResult.Success {
|
||||
r.summary.Stat.Successes += 1
|
||||
} else {
|
||||
r.summary.Stat.Failures += 1
|
||||
}
|
||||
|
||||
return stepResult, err
|
||||
}
|
||||
|
||||
func printRequest(req *http.Request) error {
|
||||
reqContentType := req.Header.Get("Content-Type")
|
||||
printBody := shouldPrintBody(reqContentType)
|
||||
reqDump, err := httputil.DumpRequest(req, printBody)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dump request failed")
|
||||
}
|
||||
fmt.Println("-------------------- request --------------------")
|
||||
reqContent := string(reqDump)
|
||||
if req.Body != nil && !printBody {
|
||||
reqContent += fmt.Sprintf("(request body omitted for Content-Type: %v)", reqContentType)
|
||||
}
|
||||
fmt.Println(reqContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printResponse(resp *http.Response) error {
|
||||
fmt.Println("==================== response ===================")
|
||||
respContentType := resp.Header.Get("Content-Type")
|
||||
printBody := shouldPrintBody(respContentType)
|
||||
respDump, err := httputil.DumpResponse(resp, printBody)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dump response failed")
|
||||
}
|
||||
respContent := string(respDump)
|
||||
if !printBody {
|
||||
respContent += fmt.Sprintf("(response body omitted for Content-Type: %v)", respContentType)
|
||||
}
|
||||
fmt.Println(respContent)
|
||||
fmt.Println("--------------------------------------------------")
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeResponseBody(resp *http.Response) (err error) {
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "br":
|
||||
resp.Body = io.NopCloser(brotli.NewReader(resp.Body))
|
||||
case "gzip":
|
||||
resp.Body, err = gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
|
||||
case "deflate":
|
||||
resp.Body, err = zlib.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldPrintBody return true if the Content-Type is printable
|
||||
// including text/*, application/json, application/xml, application/www-form-urlencoded
|
||||
func shouldPrintBody(contentType string) bool {
|
||||
if strings.HasPrefix(contentType, "text/") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(contentType, "application/xml") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewStep returns a new constructed teststep with specified step name.
|
||||
func NewStep(name string) *StepRequest {
|
||||
return &StepRequest{
|
||||
step: &TStep{
|
||||
Name: name,
|
||||
Variables: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type StepRequest struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current teststep.
|
||||
func (s *StepRequest) WithVariables(variables map[string]interface{}) *StepRequest {
|
||||
s.step.Variables = variables
|
||||
return s
|
||||
}
|
||||
|
||||
// SetupHook adds a setup hook for current teststep.
|
||||
func (s *StepRequest) SetupHook(hook string) *StepRequest {
|
||||
s.step.SetupHooks = append(s.step.SetupHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// GET makes a HTTP GET request.
|
||||
func (s *StepRequest) GET(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpGET,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// HEAD makes a HTTP HEAD request.
|
||||
func (s *StepRequest) HEAD(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpHEAD,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// POST makes a HTTP POST request.
|
||||
func (s *StepRequest) POST(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPOST,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// PUT makes a HTTP PUT request.
|
||||
func (s *StepRequest) PUT(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPUT,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE makes a HTTP DELETE request.
|
||||
func (s *StepRequest) DELETE(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpDELETE,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// OPTIONS makes a HTTP OPTIONS request.
|
||||
func (s *StepRequest) OPTIONS(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpOPTIONS,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH makes a HTTP PATCH request.
|
||||
func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request = &Request{
|
||||
Method: httpPATCH,
|
||||
URL: url,
|
||||
}
|
||||
return &StepRequestWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// CallRefCase calls a referenced testcase.
|
||||
func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs {
|
||||
var err error
|
||||
s.step.TestCase, err = tc.ToTestCase()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load testcase")
|
||||
os.Exit(1)
|
||||
}
|
||||
return &StepTestCaseWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// CallRefAPI calls a referenced api.
|
||||
func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs {
|
||||
var err error
|
||||
s.step.API, err = api.ToAPI()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load api")
|
||||
os.Exit(1)
|
||||
}
|
||||
return &StepAPIWithOptionalArgs{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// StartTransaction starts a transaction.
|
||||
func (s *StepRequest) StartTransaction(name string) *StepTransaction {
|
||||
s.step.Transaction = &Transaction{
|
||||
Name: name,
|
||||
Type: transactionStart,
|
||||
}
|
||||
return &StepTransaction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// EndTransaction ends a transaction.
|
||||
func (s *StepRequest) EndTransaction(name string) *StepTransaction {
|
||||
s.step.Transaction = &Transaction{
|
||||
Name: name,
|
||||
Type: transactionEnd,
|
||||
}
|
||||
return &StepTransaction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// SetThinkTime sets think time.
|
||||
func (s *StepRequest) SetThinkTime(time float64) *StepThinkTime {
|
||||
s.step.ThinkTime = &ThinkTime{
|
||||
Time: time,
|
||||
}
|
||||
return &StepThinkTime{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// StepRequestWithOptionalArgs implements IStep interface.
|
||||
type StepRequestWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// SetVerify sets whether to verify SSL for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetVerify(verify bool) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Verify = verify
|
||||
return s
|
||||
}
|
||||
|
||||
// SetTimeout sets timeout for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetTimeout(timeout float32) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Timeout = timeout
|
||||
return s
|
||||
}
|
||||
|
||||
// SetProxies sets proxies for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetProxies(proxies map[string]string) *StepRequestWithOptionalArgs {
|
||||
// TODO
|
||||
return s
|
||||
}
|
||||
|
||||
// SetAllowRedirects sets whether to allow redirects for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetAllowRedirects(allowRedirects bool) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.AllowRedirects = allowRedirects
|
||||
return s
|
||||
}
|
||||
|
||||
// SetAuth sets auth for current HTTP request.
|
||||
func (s *StepRequestWithOptionalArgs) SetAuth(auth map[string]string) *StepRequestWithOptionalArgs {
|
||||
// TODO
|
||||
return s
|
||||
}
|
||||
|
||||
// WithParams sets HTTP request params for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithParams(params map[string]interface{}) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Params = params
|
||||
return s
|
||||
}
|
||||
|
||||
// WithHeaders sets HTTP request headers for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithHeaders(headers map[string]string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Headers = headers
|
||||
return s
|
||||
}
|
||||
|
||||
// WithCookies sets HTTP request cookies for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithCookies(cookies map[string]string) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Cookies = cookies
|
||||
return s
|
||||
}
|
||||
|
||||
// WithBody sets HTTP request body for current step.
|
||||
func (s *StepRequestWithOptionalArgs) WithBody(body interface{}) *StepRequestWithOptionalArgs {
|
||||
s.step.Request.Body = body
|
||||
return s
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepRequestWithOptionalArgs) TeardownHook(hook string) *StepRequestWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Validate switches to step validation.
|
||||
func (s *StepRequestWithOptionalArgs) Validate() *StepRequestValidation {
|
||||
return &StepRequestValidation{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract switches to step extraction.
|
||||
func (s *StepRequestWithOptionalArgs) Extract() *StepRequestExtraction {
|
||||
s.step.Extract = make(map[string]string)
|
||||
return &StepRequestExtraction{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("%v %s", s.step.Request.Method, s.step.Request.URL)
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) Type() StepType {
|
||||
return StepType(fmt.Sprintf("request-%v", s.step.Request.Method))
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepRequestWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
|
||||
return runStepRequest(r, s.step)
|
||||
}
|
||||
|
||||
// StepRequestExtraction implements IStep interface.
|
||||
type StepRequestExtraction struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// WithJmesPath sets the JMESPath expression to extract from the response.
|
||||
func (s *StepRequestExtraction) WithJmesPath(jmesPath string, varName string) *StepRequestExtraction {
|
||||
s.step.Extract[varName] = jmesPath
|
||||
return s
|
||||
}
|
||||
|
||||
// Validate switches to step validation.
|
||||
func (s *StepRequestExtraction) Validate() *StepRequestValidation {
|
||||
return &StepRequestValidation{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Type() StepType {
|
||||
return StepType(fmt.Sprintf("request-%v", s.step.Request.Method))
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Run(r *SessionRunner) (*StepResult, error) {
|
||||
return runStepRequest(r, s.step)
|
||||
}
|
||||
|
||||
// StepRequestValidation implements IStep interface.
|
||||
type StepRequestValidation struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("%s %s", s.step.Request.Method, s.step.Request.URL)
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) Type() StepType {
|
||||
return StepType(fmt.Sprintf("request-%v", s.step.Request.Method))
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) Run(r *SessionRunner) (*StepResult, error) {
|
||||
return runStepRequest(r, s.step)
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertGreater(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "greater_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLess(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "less_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertGreaterOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "greater_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLessOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "less_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertNotEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "not_equal",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "contains",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "type_match",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertRegexp(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "regex_match",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertStartsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "startswith",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertEndsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "endswith",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "contained_by",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthLessThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_less_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "string_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthLessOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_less_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthGreaterThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_greater_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthGreaterOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_greater_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
// Validator represents validator for one HTTP response.
|
||||
type Validator struct {
|
||||
Check string `json:"check" yaml:"check"` // get value with jmespath
|
||||
Assert string `json:"assert" yaml:"assert"`
|
||||
Expect interface{} `json:"expect" yaml:"expect"`
|
||||
Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional
|
||||
}
|
||||
@@ -16,13 +16,13 @@ var (
|
||||
AssertEqual("body.args.foo1", "bar1", "check param foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check param foo2")
|
||||
stepPOSTData = NewStep("post form data").
|
||||
POST("/post").
|
||||
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
|
||||
WithBody("a=1&b=2").
|
||||
WithCookies(map[string]string{"user": "debugtalk"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code")
|
||||
POST("/post").
|
||||
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
|
||||
WithBody("a=1&b=2").
|
||||
WithCookies(map[string]string{"user": "debugtalk"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code")
|
||||
)
|
||||
|
||||
func TestRunRequestGetToStruct(t *testing.T) {
|
||||
@@ -79,11 +79,12 @@ func TestRunRequestRun(t *testing.T) {
|
||||
Config: NewConfig("test").SetBaseURL("https://postman-echo.com"),
|
||||
TestSteps: []IStep{stepGET, stepPOSTData},
|
||||
}
|
||||
runner := NewRunner(t).SetRequestsLogOn().newCaseRunner(testcase)
|
||||
if _, err := runner.runStep(0, testcase.Config); err != nil {
|
||||
t.Fatalf("tStep.Run() error: %s", err)
|
||||
runner := NewRunner(t).SetRequestsLogOn()
|
||||
sessionRunner := runner.NewSessionRunner(testcase)
|
||||
if _, err := stepGET.Run(sessionRunner); err != nil {
|
||||
t.Fatalf("stepGET.Run() error: %v", err)
|
||||
}
|
||||
if _, err := runner.runStep(1, testcase.Config); err != nil {
|
||||
t.Fatalf("tStepPOSTData.Run() error: %s", err)
|
||||
if _, err := stepPOSTData.Run(sessionRunner); err != nil {
|
||||
t.Fatalf("stepPOSTData.Run() error: %v", err)
|
||||
}
|
||||
}
|
||||
105
hrp/step_testcase.go
Normal file
105
hrp/step_testcase.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// StepTestCaseWithOptionalArgs implements IStep interface.
|
||||
type StepTestCaseWithOptionalArgs struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// TeardownHook adds a teardown hook for current teststep.
|
||||
func (s *StepTestCaseWithOptionalArgs) TeardownHook(hook string) *StepTestCaseWithOptionalArgs {
|
||||
s.step.TeardownHooks = append(s.step.TeardownHooks, hook)
|
||||
return s
|
||||
}
|
||||
|
||||
// Export specifies variable names to export from referenced testcase for current step.
|
||||
func (s *StepTestCaseWithOptionalArgs) Export(names ...string) *StepTestCaseWithOptionalArgs {
|
||||
s.step.Export = append(s.step.Export, names...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
ts, ok := s.step.TestCase.(*TestCase)
|
||||
if ok {
|
||||
return ts.Config.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) Type() StepType {
|
||||
return stepTypeTestCase
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
|
||||
copiedStep, err := r.overrideVariables(s.step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase")
|
||||
stepResult := &StepResult{
|
||||
Name: copiedStep.Name,
|
||||
StepType: stepTypeTestCase,
|
||||
Success: false,
|
||||
}
|
||||
testcase := copiedStep.TestCase.(*TestCase)
|
||||
|
||||
// copy testcase to avoid data racing
|
||||
copiedTestCase := &TestCase{}
|
||||
if err := copier.Copy(copiedTestCase, testcase); err != nil {
|
||||
log.Error().Err(err).Msg("copy testcase failed")
|
||||
return stepResult, err
|
||||
}
|
||||
// override testcase config
|
||||
extendWithTestCase(copiedStep, copiedTestCase)
|
||||
|
||||
sessionRunner := r.hrpRunner.NewSessionRunner(copiedTestCase)
|
||||
|
||||
start := time.Now()
|
||||
err = sessionRunner.Run()
|
||||
stepResult.Elapsed = time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("run referenced testcase step failed")
|
||||
log.Info().Str("step", copiedStep.Name).Bool("success", false).Msg("run step end")
|
||||
stepResult.Attachment = err.Error()
|
||||
r.summary.Success = false
|
||||
return stepResult, err
|
||||
}
|
||||
summary := sessionRunner.getSummary()
|
||||
stepResult.Data = summary
|
||||
// export testcase export variables
|
||||
stepResult.ExportVars = sessionRunner.summary.InOut.ExportVars
|
||||
stepResult.Success = true
|
||||
|
||||
// update extracted variables
|
||||
for k, v := range stepResult.ExportVars {
|
||||
r.sessionVariables[k] = v
|
||||
}
|
||||
|
||||
// merge testcase summary
|
||||
r.summary.Records = append(r.summary.Records, summary.Records...)
|
||||
r.summary.Stat.Total += summary.Stat.Total
|
||||
r.summary.Stat.Successes += summary.Stat.Successes
|
||||
r.summary.Stat.Failures += summary.Stat.Failures
|
||||
|
||||
log.Info().
|
||||
Str("step", copiedStep.Name).
|
||||
Bool("success", true).
|
||||
Interface("exportVars", stepResult.ExportVars).
|
||||
Msg("run step end")
|
||||
|
||||
return stepResult, nil
|
||||
}
|
||||
80
hrp/step_thinktime.go
Normal file
80
hrp/step_thinktime.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ThinkTime struct {
|
||||
Time float64 `json:"time" yaml:"time"`
|
||||
}
|
||||
|
||||
// StepThinkTime implements IStep interface.
|
||||
type StepThinkTime struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Type() StepType {
|
||||
return stepTypeThinkTime
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
|
||||
thinkTime := s.step.ThinkTime
|
||||
log.Info().Str("name", s.step.Name).
|
||||
Float64("time", thinkTime.Time).Msg("think time")
|
||||
|
||||
stepResult := &StepResult{
|
||||
Name: s.step.Name,
|
||||
StepType: stepTypeThinkTime,
|
||||
Success: true,
|
||||
}
|
||||
|
||||
cfg := r.testCase.Config.ThinkTimeSetting
|
||||
if cfg == nil {
|
||||
cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
|
||||
}
|
||||
|
||||
var tt time.Duration
|
||||
switch cfg.Strategy {
|
||||
case thinkTimeDefault:
|
||||
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
|
||||
case thinkTimeRandomPercentage:
|
||||
// e.g. {"min_percentage": 0.5, "max_percentage": 1.5}
|
||||
m, ok := cfg.Setting.(map[string]float64)
|
||||
if !ok {
|
||||
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
|
||||
break
|
||||
}
|
||||
res := builtin.GetRandomNumber(int(thinkTime.Time*m["min_percentage"]*1000), int(thinkTime.Time*m["max_percentage"]*1000))
|
||||
tt = time.Duration(res) * time.Millisecond
|
||||
case thinkTimeMultiply:
|
||||
value, ok := cfg.Setting.(float64) // e.g. 0.5
|
||||
if !ok || value <= 0 {
|
||||
value = thinkTimeDefaultMultiply
|
||||
}
|
||||
tt = time.Duration(thinkTime.Time*value*1000) * time.Millisecond
|
||||
case thinkTimeIgnore:
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// no more than limit
|
||||
if cfg.Limit > 0 {
|
||||
limit := time.Duration(cfg.Limit*1000) * time.Millisecond
|
||||
if limit < tt {
|
||||
tt = limit
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(tt)
|
||||
return stepResult, nil
|
||||
}
|
||||
83
hrp/step_transaction.go
Normal file
83
hrp/step_transaction.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type transactionType `json:"type" yaml:"type"`
|
||||
}
|
||||
|
||||
type transactionType string
|
||||
|
||||
const (
|
||||
transactionStart transactionType = "start"
|
||||
transactionEnd transactionType = "end"
|
||||
)
|
||||
|
||||
// StepTransaction implements IStep interface.
|
||||
type StepTransaction struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepTransaction) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("transaction %s %s", s.step.Transaction.Name, s.step.Transaction.Type)
|
||||
}
|
||||
|
||||
func (s *StepTransaction) Type() StepType {
|
||||
return stepTypeTransaction
|
||||
}
|
||||
|
||||
func (s *StepTransaction) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepTransaction) Run(r *SessionRunner) (*StepResult, error) {
|
||||
transaction := s.step.Transaction
|
||||
log.Info().
|
||||
Str("name", transaction.Name).
|
||||
Str("type", string(transaction.Type)).
|
||||
Msg("transaction")
|
||||
|
||||
stepResult := &StepResult{
|
||||
Name: transaction.Name,
|
||||
StepType: stepTypeTransaction,
|
||||
Success: true,
|
||||
Elapsed: 0,
|
||||
ContentSize: 0, // TODO: record transaction total response length
|
||||
}
|
||||
|
||||
// create transaction if not exists
|
||||
if _, ok := r.transactions[transaction.Name]; !ok {
|
||||
r.transactions[transaction.Name] = make(map[transactionType]time.Time)
|
||||
}
|
||||
|
||||
// record transaction start time, override if already exists
|
||||
if transaction.Type == transactionStart {
|
||||
r.transactions[transaction.Name][transactionStart] = time.Now()
|
||||
}
|
||||
// record transaction end time, override if already exists
|
||||
if transaction.Type == transactionEnd {
|
||||
r.transactions[transaction.Name][transactionEnd] = time.Now()
|
||||
|
||||
// if transaction start time not exists, use testcase start time instead
|
||||
if _, ok := r.transactions[transaction.Name][transactionStart]; !ok {
|
||||
r.transactions[transaction.Name][transactionStart] = r.startTime
|
||||
}
|
||||
|
||||
// calculate transaction duration
|
||||
duration := r.transactions[transaction.Name][transactionEnd].Sub(
|
||||
r.transactions[transaction.Name][transactionStart])
|
||||
stepResult.Elapsed = duration.Milliseconds()
|
||||
log.Info().Str("name", transaction.Name).Dur("elapsed", duration).Msg("transaction")
|
||||
}
|
||||
|
||||
return stepResult, nil
|
||||
}
|
||||
173
hrp/summary.go
Normal file
173
hrp/summary.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func newOutSummary() *Summary {
|
||||
platForm := &Platform{
|
||||
HttprunnerVersion: version.VERSION,
|
||||
GoVersion: runtime.Version(),
|
||||
Platform: fmt.Sprintf("%v-%v", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
return &Summary{
|
||||
Success: true,
|
||||
Stat: &Stat{},
|
||||
Time: &TestCaseTime{
|
||||
StartAt: time.Now(),
|
||||
},
|
||||
Platform: platForm,
|
||||
}
|
||||
}
|
||||
|
||||
// Summary stores tests summary for current task execution, maybe include one or multiple testcases
|
||||
type Summary struct {
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
Stat *Stat `json:"stat" yaml:"stat"`
|
||||
Time *TestCaseTime `json:"time" yaml:"time"`
|
||||
Platform *Platform `json:"platform" yaml:"platform"`
|
||||
Details []*TestCaseSummary `json:"details" yaml:"details"`
|
||||
}
|
||||
|
||||
func (s *Summary) appendCaseSummary(caseSummary *TestCaseSummary) {
|
||||
s.Success = s.Success && caseSummary.Success
|
||||
s.Stat.TestCases.Total += 1
|
||||
s.Stat.TestSteps.Total += len(caseSummary.Records)
|
||||
if caseSummary.Success {
|
||||
s.Stat.TestCases.Success += 1
|
||||
} else {
|
||||
s.Stat.TestCases.Fail += 1
|
||||
}
|
||||
s.Stat.TestSteps.Successes += caseSummary.Stat.Successes
|
||||
s.Stat.TestSteps.Failures += caseSummary.Stat.Failures
|
||||
s.Details = append(s.Details, caseSummary)
|
||||
s.Success = s.Success && caseSummary.Success
|
||||
}
|
||||
|
||||
func (s *Summary) genHTMLReport() error {
|
||||
dir, _ := filepath.Split(reportPath)
|
||||
err := builtin.EnsureFolderExists(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.OpenFile(fmt.Sprintf(reportPath, s.Time.StartAt.Unix()), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("open file failed")
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
writer := bufio.NewWriter(file)
|
||||
tmpl := template.Must(template.New("report").Parse(reportTemplate))
|
||||
err = tmpl.Execute(writer, s)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed")
|
||||
return err
|
||||
}
|
||||
err = writer.Flush()
|
||||
return err
|
||||
}
|
||||
|
||||
//go:embed internal/scaffold/templates/report/template.html
|
||||
var reportTemplate string
|
||||
|
||||
const (
|
||||
reportPath string = "reports/report-%v.html"
|
||||
summaryPath string = "reports/summary-%v.json"
|
||||
)
|
||||
|
||||
type Stat struct {
|
||||
TestCases TestCaseStat `json:"testcases" yaml:"test_cases"`
|
||||
TestSteps TestStepStat `json:"teststeps" yaml:"test_steps"`
|
||||
}
|
||||
|
||||
type TestCaseStat struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Success int `json:"success" yaml:"success"`
|
||||
Fail int `json:"fail" yaml:"fail"`
|
||||
}
|
||||
|
||||
type TestStepStat struct {
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Successes int `json:"successes" yaml:"successes"`
|
||||
Failures int `json:"failures" yaml:"failures"`
|
||||
}
|
||||
|
||||
type TestCaseTime struct {
|
||||
StartAt time.Time `json:"start_at,omitempty" yaml:"start_at,omitempty"`
|
||||
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"`
|
||||
}
|
||||
|
||||
type Platform struct {
|
||||
HttprunnerVersion string `json:"httprunner_version" yaml:"httprunner_version"`
|
||||
GoVersion string `json:"go_version" yaml:"go_version"`
|
||||
Platform string `json:"platform" yaml:"platform"`
|
||||
}
|
||||
|
||||
// TestCaseSummary stores tests summary for one testcase
|
||||
type TestCaseSummary struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` // TODO
|
||||
Stat *TestStepStat `json:"stat" yaml:"stat"`
|
||||
Time *TestCaseTime `json:"time" yaml:"time"`
|
||||
InOut *TestCaseInOut `json:"in_out" yaml:"in_out"`
|
||||
Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO
|
||||
Records []*StepResult `json:"records" yaml:"records"`
|
||||
}
|
||||
|
||||
type TestCaseInOut struct {
|
||||
ConfigVars map[string]interface{} `json:"config_vars" yaml:"config_vars"`
|
||||
ExportVars map[string]interface{} `json:"export_vars" yaml:"export_vars"`
|
||||
}
|
||||
|
||||
func newSessionData() *SessionData {
|
||||
return &SessionData{
|
||||
Success: false,
|
||||
ReqResps: &ReqResps{},
|
||||
}
|
||||
}
|
||||
|
||||
type SessionData struct {
|
||||
Success bool `json:"success" yaml:"success"`
|
||||
ReqResps *ReqResps `json:"req_resps" yaml:"req_resps"`
|
||||
Address *Address `json:"address,omitempty" yaml:"address,omitempty"` // TODO
|
||||
Validators []*ValidationResult `json:"validators,omitempty" yaml:"validators,omitempty"`
|
||||
}
|
||||
|
||||
type ReqResps struct {
|
||||
Request interface{} `json:"request" yaml:"request"`
|
||||
Response interface{} `json:"response" yaml:"response"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
ClientIP string `json:"client_ip,omitempty" yaml:"client_ip,omitempty"`
|
||||
ClientPort string `json:"client_port,omitempty" yaml:"client_port,omitempty"`
|
||||
ServerIP string `json:"server_ip,omitempty" yaml:"server_ip,omitempty"`
|
||||
ServerPort string `json:"server_port,omitempty" yaml:"server_port,omitempty"`
|
||||
}
|
||||
|
||||
type ValidationResult struct {
|
||||
Validator
|
||||
CheckValue interface{} `json:"check_value" yaml:"check_value"`
|
||||
CheckResult string `json:"check_result" yaml:"check_result"`
|
||||
}
|
||||
|
||||
func newSummary() *TestCaseSummary {
|
||||
return &TestCaseSummary{
|
||||
Success: true,
|
||||
Stat: &TestStepStat{},
|
||||
Time: &TestCaseTime{},
|
||||
InOut: &TestCaseInOut{},
|
||||
}
|
||||
}
|
||||
24
hrp/summary_test.go
Normal file
24
hrp/summary_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package hrp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenHTMLReport(t *testing.T) {
|
||||
summary := newOutSummary()
|
||||
caseSummary1 := newSummary()
|
||||
caseSummary2 := newSummary()
|
||||
stepResult1 := &StepResult{}
|
||||
stepResult2 := &StepResult{
|
||||
Name: "Test",
|
||||
StepType: stepTypeRequest,
|
||||
Success: false,
|
||||
ContentSize: 0,
|
||||
Attachment: "err",
|
||||
}
|
||||
caseSummary1.Records = []*StepResult{stepResult1, stepResult2, nil}
|
||||
summary.appendCaseSummary(caseSummary1)
|
||||
summary.appendCaseSummary(caseSummary2)
|
||||
err := summary.genHTMLReport()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -2,123 +2,46 @@ package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
func convertCompatValidator(Validators []interface{}) (err error) {
|
||||
for i, iValidator := range Validators {
|
||||
validatorMap := iValidator.(map[string]interface{})
|
||||
validator := Validator{}
|
||||
_, checkExisted := validatorMap["check"]
|
||||
_, assertExisted := validatorMap["assert"]
|
||||
_, expectExisted := validatorMap["expect"]
|
||||
// check priority: HRP > HttpRunner
|
||||
if checkExisted && assertExisted && expectExisted {
|
||||
// HRP validator format
|
||||
validator.Check = validatorMap["check"].(string)
|
||||
validator.Assert = validatorMap["assert"].(string)
|
||||
validator.Expect = validatorMap["expect"]
|
||||
if msg, existed := validatorMap["msg"]; existed {
|
||||
validator.Message = msg.(string)
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else if len(validatorMap) == 1 {
|
||||
// HttpRunner validator format
|
||||
for assertMethod, iValidatorContent := range validatorMap {
|
||||
checkAndExpect := iValidatorContent.([]interface{})
|
||||
if len(checkAndExpect) != 2 {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
validator.Check = checkAndExpect[0].(string)
|
||||
validator.Assert = assertMethod
|
||||
validator.Expect = checkAndExpect[1]
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
// ITestCase represents interface for testcases,
|
||||
// includes TestCase and TestCasePath.
|
||||
type ITestCase interface {
|
||||
GetPath() string
|
||||
ToTestCase() (*TestCase, error)
|
||||
}
|
||||
|
||||
func convertCompatTestCase(tc *TCase) (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("convert compat testcase error: %v", p)
|
||||
}
|
||||
}()
|
||||
// TestCase is a container for one testcase, which is used for testcase runner.
|
||||
// TestCase implements ITestCase interface.
|
||||
type TestCase struct {
|
||||
Config *TConfig
|
||||
TestSteps []IStep
|
||||
}
|
||||
|
||||
func (tc *TestCase) GetPath() string {
|
||||
return tc.Config.Path
|
||||
}
|
||||
|
||||
func (tc *TestCase) ToTestCase() (*TestCase, error) {
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *TestCase) ToTCase() *TCase {
|
||||
tCase := &TCase{
|
||||
Config: tc.Config,
|
||||
}
|
||||
for _, step := range tc.TestSteps {
|
||||
// 1. deal with request body compatible with HttpRunner
|
||||
if step.Request != nil && step.Request.Body == nil {
|
||||
if step.Request.Json != nil {
|
||||
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
step.Request.Body = step.Request.Json
|
||||
} else if step.Request.Data != nil {
|
||||
step.Request.Body = step.Request.Data
|
||||
}
|
||||
}
|
||||
|
||||
// 2. deal with validators compatible with HttpRunner
|
||||
err = convertCompatValidator(step.Validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tCase.TestSteps = append(tCase.TestSteps, step.ToStruct())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertCheckExpr deals with check expression including hyphen
|
||||
func convertCheckExpr(checkExpr string) string {
|
||||
if strings.Contains(checkExpr, textExtractorSubRegexp) {
|
||||
return checkExpr
|
||||
}
|
||||
checkItems := strings.Split(checkExpr, ".")
|
||||
for i, checkItem := range checkItems {
|
||||
if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") {
|
||||
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
|
||||
}
|
||||
}
|
||||
return strings.Join(checkItems, ".")
|
||||
}
|
||||
|
||||
func getProjectRootDirPath(path string) (rootDir string, err error) {
|
||||
pluginPath, err := locatePlugin(path)
|
||||
if err == nil {
|
||||
rootDir = filepath.Dir(pluginPath)
|
||||
return
|
||||
}
|
||||
|
||||
// failed to locate project root dir
|
||||
// maybe project plugin debugtalk.xx is not exist
|
||||
// use current dir instead
|
||||
return os.Getwd()
|
||||
}
|
||||
|
||||
// APIPath implements IAPI interface.
|
||||
type APIPath string
|
||||
|
||||
func (path *APIPath) GetPath() string {
|
||||
return fmt.Sprintf("%v", *path)
|
||||
}
|
||||
|
||||
func (path *APIPath) ToAPI() (*API, error) {
|
||||
api := &API{}
|
||||
apiPath := path.GetPath()
|
||||
err := builtin.LoadFile(apiPath, api)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = convertCompatValidator(api.Validators)
|
||||
return api, err
|
||||
return tCase
|
||||
}
|
||||
|
||||
// TestCasePath implements ITestCase interface.
|
||||
@@ -137,7 +60,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = convertCompatTestCase(tc)
|
||||
err = tc.makeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,3 +138,148 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
||||
}
|
||||
return testCase, nil
|
||||
}
|
||||
|
||||
// TCase represents testcase data structure.
|
||||
// Each testcase includes one public config and several sequential teststeps.
|
||||
type TCase struct {
|
||||
Config *TConfig `json:"config" yaml:"config"`
|
||||
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
// makeCompat converts TCase to compatible testcase
|
||||
func (tc *TCase) makeCompat() error {
|
||||
var err error
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("convert compat testcase error: %v", p)
|
||||
}
|
||||
}()
|
||||
for _, step := range tc.TestSteps {
|
||||
// 1. deal with request body compatible with HttpRunner
|
||||
if step.Request != nil && step.Request.Body == nil {
|
||||
if step.Request.Json != nil {
|
||||
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
step.Request.Body = step.Request.Json
|
||||
} else if step.Request.Data != nil {
|
||||
step.Request.Body = step.Request.Data
|
||||
}
|
||||
}
|
||||
|
||||
// 2. deal with validators compatible with HttpRunner
|
||||
err = convertCompatValidator(step.Validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCompatValidator(Validators []interface{}) (err error) {
|
||||
for i, iValidator := range Validators {
|
||||
validatorMap := iValidator.(map[string]interface{})
|
||||
validator := Validator{}
|
||||
_, checkExisted := validatorMap["check"]
|
||||
_, assertExisted := validatorMap["assert"]
|
||||
_, expectExisted := validatorMap["expect"]
|
||||
// check priority: HRP > HttpRunner
|
||||
if checkExisted && assertExisted && expectExisted {
|
||||
// HRP validator format
|
||||
validator.Check = validatorMap["check"].(string)
|
||||
validator.Assert = validatorMap["assert"].(string)
|
||||
validator.Expect = validatorMap["expect"]
|
||||
if msg, existed := validatorMap["msg"]; existed {
|
||||
validator.Message = msg.(string)
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else if len(validatorMap) == 1 {
|
||||
// HttpRunner validator format
|
||||
for assertMethod, iValidatorContent := range validatorMap {
|
||||
checkAndExpect := iValidatorContent.([]interface{})
|
||||
if len(checkAndExpect) != 2 {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
validator.Check = checkAndExpect[0].(string)
|
||||
validator.Assert = assertMethod
|
||||
validator.Expect = checkAndExpect[1]
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertCheckExpr deals with check expression including hyphen
|
||||
func convertCheckExpr(checkExpr string) string {
|
||||
if strings.Contains(checkExpr, textExtractorSubRegexp) {
|
||||
return checkExpr
|
||||
}
|
||||
checkItems := strings.Split(checkExpr, ".")
|
||||
for i, checkItem := range checkItems {
|
||||
if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") {
|
||||
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
|
||||
}
|
||||
}
|
||||
return strings.Join(checkItems, ".")
|
||||
}
|
||||
|
||||
func loadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
|
||||
testCases := make([]*TestCase, 0)
|
||||
|
||||
for _, iTestCase := range iTestCases {
|
||||
if _, ok := iTestCase.(*TestCase); ok {
|
||||
testcase, err := iTestCase.ToTestCase()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct")
|
||||
return nil, err
|
||||
}
|
||||
testCases = append(testCases, testcase)
|
||||
continue
|
||||
}
|
||||
|
||||
// iTestCase should be a TestCasePath, file path or folder path
|
||||
tcPath, ok := iTestCase.(*TestCasePath)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid iTestCase type")
|
||||
}
|
||||
|
||||
casePath := tcPath.GetPath()
|
||||
err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error {
|
||||
if dir == nil {
|
||||
// casePath is a file other than a dir
|
||||
path = casePath
|
||||
} else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") {
|
||||
// skip hidden folders
|
||||
return fs.SkipDir
|
||||
} else {
|
||||
// casePath is a dir
|
||||
path = filepath.Join(casePath, path)
|
||||
}
|
||||
|
||||
// ignore non-testcase files
|
||||
ext := filepath.Ext(path)
|
||||
if ext != ".yml" && ext != ".yaml" && ext != ".json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// filtered testcases
|
||||
testCasePath := TestCasePath(path)
|
||||
tc, err := testCasePath.ToTestCase()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("load testcase failed")
|
||||
return errors.Wrap(err, "load testcase failed")
|
||||
}
|
||||
testCases = append(testCases, tc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read dir failed")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
|
||||
return testCases, nil
|
||||
}
|
||||
223
hrp/validate.go
223
hrp/validate.go
@@ -1,223 +0,0 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// StepRequestValidation implements IStep interface.
|
||||
type StepRequestValidation struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) Name() string {
|
||||
if s.step.Name != "" {
|
||||
return s.step.Name
|
||||
}
|
||||
return fmt.Sprintf("%s %s", s.step.Request.Method, s.step.Request.URL)
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) Type() string {
|
||||
return fmt.Sprintf("request-%v", s.step.Request.Method)
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertGreater(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "greater_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLess(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "less_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertGreaterOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "greater_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLessOrEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "less_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertNotEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "not_equal",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertContains(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "contains",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "type_match",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertRegexp(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "regex_match",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertStartsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "startswith",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertEndsWith(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "endswith",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "contained_by",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthLessThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_less_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "string_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthLessOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_less_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthGreaterThan(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_greater_than",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StepRequestValidation) AssertLengthGreaterOrEquals(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
|
||||
v := Validator{
|
||||
Check: jmesPath,
|
||||
Assert: "length_greater_or_equals",
|
||||
Expect: expected,
|
||||
Message: msg,
|
||||
}
|
||||
s.step.Validators = append(s.step.Validators, v)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user