Merge branch 'main' of https://github.com/httprunner/hrp into main

This commit is contained in:
debugtalk
2022-03-22 11:55:00 +08:00
11 changed files with 380 additions and 3 deletions

View File

@@ -150,6 +150,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
}
} else if stepData.StepType == stepTypeRendezvous {
// rendezvous
// TODO: implement rendezvous in boomer
} else if stepData.StepType == stepTypeThinkTime {
// think time
// no record required
} else {
// request or testcase step
b.RecordSuccess(step.Type(), step.Name(), stepData.Elapsed, stepData.ContentSize)

View File

@@ -156,6 +156,10 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
step: step,
})
} else if step.ThinkTime != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepThinkTime{
step: step,
})
} else if step.Request != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
step: step,

View File

@@ -11,6 +11,7 @@ var (
demoTestCaseYAMLPath TestCasePath = "examples/demo.yaml"
demoRefAPIYAMLPath TestCasePath = "examples/ref_api_test.yaml"
demoRefTestCaseJSONPath TestCasePath = "examples/ref_testcase_test.json"
demoThinkTimeJsonPath TestCasePath = "examples/think_time_test.json"
demoAPIYAMLPath APIPath = "examples/api/put.yml"
)

View File

@@ -0,0 +1,63 @@
{
"config": {
"name": "think time test demo",
"variables": {
"app_version": "v1",
"user_agent": "iOS/10.3"
},
"base_url": "https://postman-echo.com",
"think_time": {
"strategy": "random_percentage",
"setting": {
"min_percentage": 1,
"max_percentage": 1.5
},
"limit": 4
},
"verify": false
},
"teststeps": [
{
"name": "get with params",
"request": {
"method": "GET",
"url": "/get",
"headers": {
"User-Agent": "$user_agent,$app_version"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
}
]
},
{
"name": "think time 1",
"think_time": {
"time": 3
}
},
{
"name": "post with params",
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "$user_agent,$app_version"
}
},
"validate": [
{
"check": "status_code",
"assert": "equals",
"expect": 200,
"msg": "check status code"
}
]
}
]
}

View File

@@ -0,0 +1,40 @@
config:
name: "think time test demo"
variables:
app_version: v1
user_agent: iOS/10.3
base_url: "https://postman-echo.com"
think_time:
strategy: random_percentage
setting:
min_percentage: 1.0
max_percentage: 1.5
limit: 4
verify: False
teststeps:
- name: get with params
request:
method: GET
url: /get
headers:
User-Agent: $user_agent,$app_version
validate:
- check: status_code
assert: equals
expect: 200
msg: check status code
- name: think time 1
think_time:
time: 3
- name: post with params
request:
method: POST
url: /post
headers:
User-Agent: $user_agent,$app_version
validate:
- check: status_code
assert: equals
expect: 200
msg: check status code

View File

@@ -5,12 +5,15 @@ import (
"crypto/md5"
"encoding/csv"
"encoding/hex"
builtinJSON "encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
@@ -220,3 +223,38 @@ func Contains(s []string, e string) bool {
}
return false
}
func GetRandomNumber(min, max int) int {
if min > max {
return 0
}
r := rand.Intn(max - min + 1)
return min + r
}
func Interface2Float64(i interface{}) (float64, error) {
switch i.(type) {
case int:
return float64(i.(int)), nil
case int32:
return float64(i.(int32)), nil
case int64:
return float64(i.(int64)), nil
case float32:
return float64(i.(float32)), nil
case float64:
return i.(float64), nil
case string:
intVar, err := strconv.Atoi(i.(string))
if err != nil {
return 0, err
}
return float64(intVar), err
}
// json.Number
value, ok := i.(builtinJSON.Number)
if ok {
return value.Float64()
}
return 0, errors.New("failed to convert interface to float64")
}

View File

@@ -3,10 +3,12 @@ package hrp
import (
"fmt"
"math/rand"
"reflect"
"runtime"
"sync"
"time"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/version"
)
@@ -30,13 +32,14 @@ type TConfig struct {
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"`
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"` // 保存参数的迭代器
}
@@ -46,6 +49,82 @@ const (
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 {
@@ -145,6 +224,7 @@ type TStep struct {
TestCaseContent ITestCase `json:"testcase_content,omitempty" yaml:"testcase_content,omitempty"`
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"`
@@ -160,8 +240,13 @@ const (
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 (

View File

@@ -41,7 +41,6 @@ func initPlugin(path string, logOn bool) (plugin funplugin.IPlugin, err error) {
go func() {
<-c
plugin.Quit()
os.Exit(0)
}()
// report event for initializing plugin

View File

@@ -297,13 +297,16 @@ func (r *caseRunner) run() error {
func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
step := r.TestCase.TestSteps[index]
// step type priority order: transaction > rendezvous > testcase > request
// step type priority order: transaction > rendezvous > thinktime > testcase > request
if stepTran, ok := step.(*StepTransaction); ok {
// transaction step
return r.runStepTransaction(stepTran.step.Transaction)
} else if stepRend, ok := step.(*StepRendezvous); ok {
// rendezvous step
return r.runStepRendezvous(stepRend.step.Rendezvous)
} else if stepThink, ok := step.(*StepThinkTime); ok {
// think time step
return r.runStepThinkTime(stepThink.step, caseConfig.ThinkTime)
}
log.Info().Str("step", step.Name()).Msg("run step start")
@@ -377,6 +380,52 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa
return stepResult, err
}
func (r *caseRunner) runStepThinkTime(step *TStep, ttc *ThinkTimeConfig) (stepResult *stepData, err error) {
thinkTime := step.ThinkTime
log.Info().
Str("name", step.Name).
Float64("time", thinkTime.Time).
Msg("think time")
stepResult = &stepData{
Name: step.Name,
StepType: stepTypeThinkTime,
Success: true,
}
if ttc == nil {
ttc = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
}
var tt time.Duration
switch ttc.Strategy {
case thinkTimeDefault:
tt = time.Duration(thinkTime.Time*1000) * time.Millisecond
case thinkTimeRandomPercentage:
m, ok := ttc.Setting.(map[string]float64) // e.g. {"min_percentage": 0.5, "max_percentage": 1.5}
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 := ttc.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 ttc.Limit > 0 {
limit := time.Duration(ttc.Limit*1000) * time.Millisecond
if limit < tt {
tt = limit
}
}
time.Sleep(tt)
return stepResult, nil
}
func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) {
log.Info().
Str("name", transaction.Name).
@@ -966,6 +1015,9 @@ func (r *caseRunner) parseConfig(cfg *TConfig) error {
}
cfg.BaseURL = convertString(parsedBaseURL)
// ensure correction of think time config
cfg.ThinkTime.checkThinkTime()
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"testing"
"time"
"github.com/rs/zerolog/log"
)
@@ -146,6 +147,63 @@ func TestInitRendezvous(t *testing.T) {
}
}
func TestThinkTime(t *testing.T) {
buildHashicorpPlugin()
defer removeHashicorpPlugin()
testcases := []*TestCase{
{
Config: NewConfig("TestCase1"),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(2),
},
},
{
Config: NewConfig("TestCase2").
SetThinkTime(thinkTimeIgnore, nil, 0),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(0.5),
},
},
{
Config: NewConfig("TestCase3").
SetThinkTime(thinkTimeRandomPercentage, nil, 0),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(1),
},
},
{
Config: NewConfig("TestCase4").
SetThinkTime(thinkTimeRandomPercentage, map[string]interface{}{"min_percentage": 2, "max_percentage": 3}, 2.5),
TestSteps: []IStep{
NewStep("thinkTime").SetThinkTime(1),
},
},
{
Config: NewConfig("TestCase5"),
TestSteps: []IStep{
NewStep("thinkTime").CallRefCase(&demoThinkTimeJsonPath), // think time: 3s, random pct: {"min_percentage":1, "max_percentage":1.5}, limit: 4s
},
},
}
expectedMinValue := []float64{2, 0, 0.5, 2, 3}
expectedMaxValue := []float64{2.5, 0.5, 2, 3, 10}
for idx, testcase := range testcases {
r := NewRunner(t)
startTime := time.Now()
err := r.Run(testcase)
if err != nil {
t.Fatalf("run testcase error: %v", err)
}
duration := time.Since(startTime)
minValue := time.Duration(expectedMinValue[idx]*1000) * time.Millisecond
maxValue := time.Duration(expectedMaxValue[idx]*1000) * time.Millisecond
if duration < minValue || duration > maxValue {
t.Fatalf("failed to test think time, expect value: [%v, %v], actual value: %v", minValue, maxValue, duration)
}
}
}
func TestGenHTMLReport(t *testing.T) {
summary := newOutSummary()
caseSummary1 := newSummary()

33
step.go
View File

@@ -40,6 +40,12 @@ func (c *TConfig) WithParameters(parameters map[string]interface{}) *TConfig {
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
@@ -193,6 +199,16 @@ func (s *StepRequest) EndTransaction(name string) *StepTransaction {
}
}
// 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
@@ -355,6 +371,23 @@ 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