mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-07 16:59:34 +08:00
Merge pull request #133 from xucong053/support-api-layer-for-testcase
feat: support api layer and global headers for testcase #94 #95
This commit is contained in:
@@ -251,7 +251,7 @@ func TestCaseDemo(t *testing.T) {
|
|||||||
}).
|
}).
|
||||||
GET("/get").
|
GET("/get").
|
||||||
WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params
|
WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params
|
||||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers
|
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers
|
||||||
Extract().
|
Extract().
|
||||||
WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath
|
WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath
|
||||||
Validate().
|
Validate().
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ func (b *HRPBoomer) Quit() {
|
|||||||
|
|
||||||
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
|
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
|
||||||
hrpRunner := NewRunner(nil)
|
hrpRunner := NewRunner(nil)
|
||||||
|
// set client transport for high concurrency load testing
|
||||||
|
hrpRunner.SetClientTransport(b.GetSpawnCount(), b.GetDisableKeepAlive(), b.GetDisableCompression())
|
||||||
config := testcase.Config
|
config := testcase.Config
|
||||||
|
|
||||||
// each testcase has its own plugin process
|
// each testcase has its own plugin process
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
|
|||||||
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testcase2 := &TestCasePath{demoTestCaseJSONPath}
|
testcase2 := &demoTestCaseJSONPath
|
||||||
|
|
||||||
b := NewBoomer(2, 1)
|
b := NewBoomer(2, 1)
|
||||||
go b.Run(testcase1, testcase2)
|
go b.Run(testcase1, testcase2)
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ var boomCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
var paths []hrp.ITestCase
|
var paths []hrp.ITestCase
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
paths = append(paths, &hrp.TestCasePath{Path: arg})
|
path := hrp.TestCasePath(arg)
|
||||||
|
paths = append(paths, &path)
|
||||||
}
|
}
|
||||||
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate)
|
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate)
|
||||||
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate)
|
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate)
|
||||||
@@ -38,6 +39,8 @@ var boomCmd = &cobra.Command{
|
|||||||
if prometheusPushgatewayURL != "" {
|
if prometheusPushgatewayURL != "" {
|
||||||
hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp"))
|
hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp"))
|
||||||
}
|
}
|
||||||
|
hrpBoomer.SetDisableKeepAlive(disableKeepalive)
|
||||||
|
hrpBoomer.SetDisableCompression(disableCompression)
|
||||||
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration)
|
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration)
|
||||||
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration)
|
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration)
|
||||||
hrpBoomer.Run(paths...)
|
hrpBoomer.Run(paths...)
|
||||||
@@ -56,6 +59,8 @@ var (
|
|||||||
cpuProfileDuration time.Duration
|
cpuProfileDuration time.Duration
|
||||||
prometheusPushgatewayURL string
|
prometheusPushgatewayURL string
|
||||||
disableConsoleOutput bool
|
disableConsoleOutput bool
|
||||||
|
disableCompression bool
|
||||||
|
disableKeepalive bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -72,4 +77,6 @@ func init() {
|
|||||||
boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.")
|
boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.")
|
||||||
boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.")
|
boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.")
|
||||||
boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.")
|
boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.")
|
||||||
|
boomCmd.Flags().BoolVar(&disableCompression, "disable-compression", false, "Disable compression")
|
||||||
|
boomCmd.Flags().BoolVar(&disableKeepalive, "disable-keepalive", false, "Disable keepalive")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ var runCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
var paths []hrp.ITestCase
|
var paths []hrp.ITestCase
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
paths = append(paths, &hrp.TestCasePath{Path: arg})
|
path := hrp.TestCasePath(arg)
|
||||||
|
paths = append(paths, &path)
|
||||||
}
|
}
|
||||||
runner := hrp.NewRunner(nil).
|
runner := hrp.NewRunner(nil).
|
||||||
SetFailfast(!continueOnFailure).
|
SetFailfast(!continueOnFailure).
|
||||||
|
|||||||
186
convert.go
186
convert.go
@@ -13,52 +13,80 @@ import (
|
|||||||
"github.com/httprunner/hrp/internal/json"
|
"github.com/httprunner/hrp/internal/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadFromJSON(path string) (*TCase, error) {
|
func loadFromJSON(path string, structObj interface{}) error {
|
||||||
path, err := filepath.Abs(path)
|
path, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
log.Info().Str("path", path).Msg("load json testcase")
|
log.Info().Str("path", path).Msg("load json")
|
||||||
|
|
||||||
file, err := os.ReadFile(path)
|
file, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("load json path failed")
|
log.Error().Err(err).Msg("load json path failed")
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := &TCase{}
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(file))
|
decoder := json.NewDecoder(bytes.NewReader(file))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
err = decoder.Decode(tc)
|
err = decoder.Decode(structObj)
|
||||||
if err != nil {
|
return err
|
||||||
return tc, err
|
|
||||||
}
|
|
||||||
err = convertCompatTestCase(tc)
|
|
||||||
return tc, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFromYAML(path string) (*TCase, error) {
|
func loadFromYAML(path string, structObj interface{}) error {
|
||||||
path, err := filepath.Abs(path)
|
path, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
log.Info().Str("path", path).Msg("load yaml testcase")
|
log.Info().Str("path", path).Msg("load yaml")
|
||||||
|
|
||||||
file, err := os.ReadFile(path)
|
file, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("load yaml path failed")
|
log.Error().Err(err).Msg("load yaml path failed")
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := &TCase{}
|
err = yaml.Unmarshal(file, structObj)
|
||||||
err = yaml.Unmarshal(file, tc)
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return tc, 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = convertCompatTestCase(tc)
|
return nil
|
||||||
return tc, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertCompatTestCase(tc *TCase) (err error) {
|
func convertCompatTestCase(tc *TCase) (err error) {
|
||||||
@@ -79,42 +107,12 @@ func convertCompatTestCase(tc *TCase) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. deal with validators compatible with HttpRunner
|
// 2. deal with validators compatible with HttpRunner
|
||||||
for i, iValidator := range step.Validators {
|
err = convertCompatValidator(step.Validators)
|
||||||
validatorMap := iValidator.(map[string]interface{})
|
if err != nil {
|
||||||
validator := Validator{}
|
return err
|
||||||
_, 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)
|
|
||||||
step.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)
|
|
||||||
step.Validators[i] = validator
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertCheckExpr deals with check expression including hyphen
|
// convertCheckExpr deals with check expression including hyphen
|
||||||
@@ -136,14 +134,32 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
|
|||||||
Config: tc.Config,
|
Config: tc.Config,
|
||||||
}
|
}
|
||||||
for _, step := range tc.TestSteps {
|
for _, step := range tc.TestSteps {
|
||||||
if step.Request != nil {
|
if step.APIPath != "" {
|
||||||
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
|
refAPI := APIPath(step.APIPath)
|
||||||
|
step.APIContent = &refAPI
|
||||||
|
apiContent, err := step.APIContent.ToAPI()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
step.APIContent = apiContent
|
||||||
|
testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{
|
||||||
step: step,
|
step: step,
|
||||||
})
|
})
|
||||||
} else if step.TestCase != nil {
|
} else if step.TestCasePath != "" {
|
||||||
|
refTestCase := TestCasePath(step.TestCasePath)
|
||||||
|
step.TestCaseContent = &refTestCase
|
||||||
|
tc, err := step.TestCaseContent.ToTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
step.TestCaseContent = tc
|
||||||
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
|
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
|
||||||
step: step,
|
step: step,
|
||||||
})
|
})
|
||||||
|
} else if step.Request != nil {
|
||||||
|
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
|
||||||
|
step: step,
|
||||||
|
})
|
||||||
} else if step.Transaction != nil {
|
} else if step.Transaction != nil {
|
||||||
testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{
|
testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{
|
||||||
step: step,
|
step: step,
|
||||||
@@ -161,29 +177,63 @@ func (tc *TCase) ToTestCase() (*TestCase, error) {
|
|||||||
|
|
||||||
var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension")
|
var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension")
|
||||||
|
|
||||||
// TestCasePath implements ITestCase interface.
|
// APIPath implements IAPI interface.
|
||||||
type TestCasePath struct {
|
type APIPath string
|
||||||
Path string
|
|
||||||
|
func (path *APIPath) ToString() string {
|
||||||
|
return fmt.Sprintf("%v", *path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
func (path *APIPath) ToAPI() (*API, error) {
|
||||||
var tc *TCase
|
api := &API{}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
casePath := path.Path
|
apiPath := path.ToString()
|
||||||
ext := filepath.Ext(casePath)
|
ext := filepath.Ext(apiPath)
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".json":
|
case ".json":
|
||||||
tc, err = loadFromJSON(casePath)
|
err = loadFromJSON(apiPath, api)
|
||||||
case ".yaml", ".yml":
|
case ".yaml", ".yml":
|
||||||
tc, err = loadFromYAML(casePath)
|
err = loadFromYAML(apiPath, api)
|
||||||
default:
|
default:
|
||||||
err = ErrUnsupportedFileExt
|
err = ErrUnsupportedFileExt
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tc.Config.Path = path.Path
|
err = convertCompatValidator(api.Validators)
|
||||||
|
return api, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCasePath implements ITestCase interface.
|
||||||
|
type TestCasePath string
|
||||||
|
|
||||||
|
func (path *TestCasePath) ToString() string {
|
||||||
|
return fmt.Sprintf("%v", *path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
||||||
|
tc := &TCase{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
casePath := path.ToString()
|
||||||
|
ext := filepath.Ext(casePath)
|
||||||
|
switch ext {
|
||||||
|
case ".json":
|
||||||
|
err = loadFromJSON(casePath, tc)
|
||||||
|
case ".yaml", ".yml":
|
||||||
|
err = loadFromYAML(casePath, tc)
|
||||||
|
default:
|
||||||
|
err = ErrUnsupportedFileExt
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = convertCompatTestCase(tc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tc.Config.Path = path.ToString()
|
||||||
testcase, err := tc.ToTestCase()
|
testcase, err := tc.ToTestCase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,16 +7,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
demoTestCaseJSONPath = "examples/demo.json"
|
demoTestCaseJSONPath TestCasePath = "examples/demo.json"
|
||||||
demoTestCaseYAMLPath = "examples/demo.yaml"
|
demoTestCaseYAMLPath TestCasePath = "examples/demo.yaml"
|
||||||
|
demoRefAPIYAMLPath TestCasePath = "examples/ref_api_test.yaml"
|
||||||
|
demoRefTestCaseJSONPath TestCasePath = "examples/ref_testcase_test.json"
|
||||||
|
demoAPIYAMLPath APIPath = "examples/api/put.yml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadCase(t *testing.T) {
|
func TestLoadCase(t *testing.T) {
|
||||||
tcJSON, err := loadFromJSON(demoTestCaseJSONPath)
|
tcJSON := &TCase{}
|
||||||
|
tcYAML := &TCase{}
|
||||||
|
err := loadFromJSON(demoTestCaseJSONPath.ToString(), tcJSON)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
tcYAML, err := loadFromYAML(demoTestCaseYAMLPath)
|
err = loadFromYAML(demoTestCaseYAMLPath.ToString(), tcYAML)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ Copyright 2021 debugtalk
|
|||||||
* [hrp run](hrp_run.md) - run API test
|
* [hrp run](hrp_run.md) - run API test
|
||||||
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 10-Mar-2022
|
###### Auto generated by spf13/cobra on 15-Mar-2022
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ hrp boom [flags]
|
|||||||
```
|
```
|
||||||
--cpu-profile string Enable CPU profiling.
|
--cpu-profile string Enable CPU profiling.
|
||||||
--cpu-profile-duration duration CPU profile duration. (default 30s)
|
--cpu-profile-duration duration CPU profile duration. (default 30s)
|
||||||
|
--disable-compression Disable compression
|
||||||
--disable-console-output Disable console output.
|
--disable-console-output Disable console output.
|
||||||
|
--disable-keepalive Disable keepalive
|
||||||
-h, --help help for boom
|
-h, --help help for boom
|
||||||
--loop-count int The specify running cycles for load testing (default -1)
|
--loop-count int The specify running cycles for load testing (default -1)
|
||||||
--max-rps int Max RPS that boomer can generate, disabled by default.
|
--max-rps int Max RPS that boomer can generate, disabled by default.
|
||||||
@@ -39,4 +41,4 @@ hrp boom [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 10-Mar-2022
|
###### Auto generated by spf13/cobra on 15-Mar-2022
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ hrp har2case $har_path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 10-Mar-2022
|
###### Auto generated by spf13/cobra on 15-Mar-2022
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ hrp run $path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 10-Mar-2022
|
###### Auto generated by spf13/cobra on 15-Mar-2022
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ hrp startproject $project_name [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 10-Mar-2022
|
###### Auto generated by spf13/cobra on 15-Mar-2022
|
||||||
|
|||||||
34
examples/api/get.json
Normal file
34
examples/api/get.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/get",
|
||||||
|
"params": {
|
||||||
|
"foo1": "bar1",
|
||||||
|
"foo2": "bar2"
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "assert response status code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "headers.\"Content-Type\"",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "application/json; charset=utf-8",
|
||||||
|
"msg": "assert response header Content-Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.url",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "https://postman-echo.com/get?foo1=bar1&foo2=bar2",
|
||||||
|
"msg": "assert response body url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
examples/api/get.yml
Normal file
22
examples/api/get.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: ""
|
||||||
|
request:
|
||||||
|
method: GET
|
||||||
|
url: /get
|
||||||
|
params:
|
||||||
|
foo1: bar1
|
||||||
|
foo2: bar2
|
||||||
|
headers:
|
||||||
|
Postman-Token: ea19464c-ddd4-4724-abe9-5e2b254c2723
|
||||||
|
validate:
|
||||||
|
- check: status_code
|
||||||
|
assert: equals
|
||||||
|
expect: 200
|
||||||
|
msg: assert response status code
|
||||||
|
- check: headers."Content-Type"
|
||||||
|
assert: equals
|
||||||
|
expect: application/json; charset=utf-8
|
||||||
|
msg: assert response header Content-Type
|
||||||
|
- check: body.url
|
||||||
|
assert: equals
|
||||||
|
expect: https://postman-echo.com/get?foo1=bar1&foo2=bar2
|
||||||
|
msg: assert response body url
|
||||||
45
examples/api/post.json
Normal file
45
examples/api/post.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"headers": {
|
||||||
|
"Content-Length": "58",
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Postman-Token": "$session_token"
|
||||||
|
},
|
||||||
|
"body": "This is expected to be sent back as part of response body."
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "assert response status code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "headers.\"Content-Type\"",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "application/json; charset=utf-8",
|
||||||
|
"msg": "assert response header Content-Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.data",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "This is expected to be sent back as part of response body.",
|
||||||
|
"msg": "assert response body data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.json",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": null,
|
||||||
|
"msg": "assert response body json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.url",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "https://postman-echo.com/post",
|
||||||
|
"msg": "assert response body url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
examples/api/post.yml
Normal file
30
examples/api/post.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: ""
|
||||||
|
request:
|
||||||
|
method: POST
|
||||||
|
url: /post
|
||||||
|
headers:
|
||||||
|
Content-Length: "58"
|
||||||
|
Content-Type: text/plain
|
||||||
|
Postman-Token: $session_token
|
||||||
|
body: This is expected to be sent back as part of response body.
|
||||||
|
validate:
|
||||||
|
- check: status_code
|
||||||
|
assert: equals
|
||||||
|
expect: 200
|
||||||
|
msg: assert response status code
|
||||||
|
- check: headers."Content-Type"
|
||||||
|
assert: equals
|
||||||
|
expect: application/json; charset=utf-8
|
||||||
|
msg: assert response header Content-Type
|
||||||
|
- check: body.data
|
||||||
|
assert: equals
|
||||||
|
expect: This is expected to be sent back as part of response body.
|
||||||
|
msg: assert response body data
|
||||||
|
- check: body.json
|
||||||
|
assert: equals
|
||||||
|
expect: null
|
||||||
|
msg: assert response body json
|
||||||
|
- check: body.url
|
||||||
|
assert: equals
|
||||||
|
expect: https://postman-echo.com/post
|
||||||
|
msg: assert response body url
|
||||||
45
examples/api/put.json
Normal file
45
examples/api/put.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"url": "/put",
|
||||||
|
"headers": {
|
||||||
|
"Content-Length": "58",
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"Postman-Token": "5d357b2b-0f10-4ded-bc9a-299ebef7a2d5"
|
||||||
|
},
|
||||||
|
"body": "This is expected to be sent back as part of response body."
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "assert response status code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "headers.\"Content-Type\"",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "application/json; charset=utf-8",
|
||||||
|
"msg": "assert response header Content-Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.data",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "This is expected to be sent back as part of response body.",
|
||||||
|
"msg": "assert response body data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.json",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": null,
|
||||||
|
"msg": "assert response body json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.url",
|
||||||
|
"assert": "equals",
|
||||||
|
"expect": "https://postman-echo.com/put",
|
||||||
|
"msg": "assert response body url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
examples/api/put.yml
Normal file
30
examples/api/put.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: ""
|
||||||
|
request:
|
||||||
|
method: PUT
|
||||||
|
url: /put
|
||||||
|
headers:
|
||||||
|
Content-Length: "58"
|
||||||
|
Content-Type: text/plain
|
||||||
|
Postman-Token: 5d357b2b-0f10-4ded-bc9a-299ebef7a2d5
|
||||||
|
body: This is expected to be sent back as part of response body.
|
||||||
|
validate:
|
||||||
|
- check: status_code
|
||||||
|
assert: equals
|
||||||
|
expect: 200
|
||||||
|
msg: assert response status code
|
||||||
|
- check: headers."Content-Type"
|
||||||
|
assert: equals
|
||||||
|
expect: application/json; charset=utf-8
|
||||||
|
msg: assert response header Content-Type
|
||||||
|
- check: body.data
|
||||||
|
assert: equals
|
||||||
|
expect: This is expected to be sent back as part of response body.
|
||||||
|
msg: assert response body data
|
||||||
|
- check: body.json
|
||||||
|
assert: equals
|
||||||
|
expect: null
|
||||||
|
msg: assert response body json
|
||||||
|
- check: body.url
|
||||||
|
assert: equals
|
||||||
|
expect: https://postman-echo.com/put
|
||||||
|
msg: assert response body url
|
||||||
@@ -7,18 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// generated by examples/har/demo.har using HttpRunner v3.1.6
|
// generated by examples/har/demo.har using HttpRunner v3.1.6
|
||||||
const demoHttpRunnerJSONPath = "demo_httprunner.json"
|
var (
|
||||||
const demoHttpRunnerYAMLPath = "demo_httprunner.yaml"
|
demoHttpRunnerJSONPath hrp.TestCasePath = "demo_httprunner.json"
|
||||||
|
demoHttpRunnerYAMLPath hrp.TestCasePath = "demo_httprunner.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCompatTestCase(t *testing.T) {
|
func TestCompatTestCase(t *testing.T) {
|
||||||
testcaseFromJSON := &hrp.TestCasePath{Path: demoHttpRunnerJSONPath}
|
err := hrp.NewRunner(t).Run(&demoHttpRunnerJSONPath)
|
||||||
err := hrp.NewRunner(t).Run(testcaseFromJSON)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("run testcase error: %v", err)
|
t.Fatalf("run testcase error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testcaseFromYAML := &hrp.TestCasePath{Path: demoHttpRunnerYAMLPath}
|
err = hrp.NewRunner(t).Run(&demoHttpRunnerYAMLPath)
|
||||||
err = hrp.NewRunner(t).Run(testcaseFromYAML)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("run testcase error: %v", err)
|
t.Fatalf("run testcase error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,13 +585,13 @@
|
|||||||
{
|
{
|
||||||
"check": "status_code",
|
"check": "status_code",
|
||||||
"assert": "equals",
|
"assert": "equals",
|
||||||
"expect": 302,
|
"expect": 200,
|
||||||
"msg": "assert response status code"
|
"msg": "assert response status code"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check": "headers.\"Content-Type\"",
|
"check": "headers.\"Content-Type\"",
|
||||||
"assert": "equals",
|
"assert": "equals",
|
||||||
"expect": "text/plain; charset=utf-8",
|
"expect": "application/json; charset=utf-8",
|
||||||
"msg": "assert response header Content-Type"
|
"msg": "assert response header Content-Type"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -695,13 +695,13 @@
|
|||||||
{
|
{
|
||||||
"check": "status_code",
|
"check": "status_code",
|
||||||
"assert": "equals",
|
"assert": "equals",
|
||||||
"expect": 302,
|
"expect": 200,
|
||||||
"msg": "assert response status code"
|
"msg": "assert response status code"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check": "headers.\"Content-Type\"",
|
"check": "headers.\"Content-Type\"",
|
||||||
"assert": "equals",
|
"assert": "equals",
|
||||||
"expect": "text/plain; charset=utf-8",
|
"expect": "application/json; charset=utf-8",
|
||||||
"msg": "assert response header Content-Type"
|
"msg": "assert response header Content-Type"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -411,11 +411,11 @@ teststeps:
|
|||||||
validate:
|
validate:
|
||||||
- check: status_code
|
- check: status_code
|
||||||
assert: equals
|
assert: equals
|
||||||
expect: 302
|
expect: 200
|
||||||
msg: assert response status code
|
msg: assert response status code
|
||||||
- check: headers."Content-Type"
|
- check: headers."Content-Type"
|
||||||
assert: equals
|
assert: equals
|
||||||
expect: text/plain; charset=utf-8
|
expect: application/json; charset=utf-8
|
||||||
msg: assert response header Content-Type
|
msg: assert response header Content-Type
|
||||||
- name: ""
|
- name: ""
|
||||||
request:
|
request:
|
||||||
@@ -490,11 +490,11 @@ teststeps:
|
|||||||
validate:
|
validate:
|
||||||
- check: status_code
|
- check: status_code
|
||||||
assert: equals
|
assert: equals
|
||||||
expect: 302
|
expect: 200
|
||||||
msg: assert response status code
|
msg: assert response status code
|
||||||
- check: headers."Content-Type"
|
- check: headers."Content-Type"
|
||||||
assert: equals
|
assert: equals
|
||||||
expect: text/plain; charset=utf-8
|
expect: application/json; charset=utf-8
|
||||||
msg: assert response header Content-Type
|
msg: assert response header Content-Type
|
||||||
- name: ""
|
- name: ""
|
||||||
request:
|
request:
|
||||||
|
|||||||
78
examples/ref_api_test.json
Normal file
78
examples/ref_api_test.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "api test demo",
|
||||||
|
"variables": {
|
||||||
|
"user_agent": "iOS/10.3",
|
||||||
|
"device_sn": "TESTCASE_SETUP_XXX",
|
||||||
|
"os_platform": "ios",
|
||||||
|
"app_version": "2.8.6"
|
||||||
|
},
|
||||||
|
"base_url": "https://postman-echo.com",
|
||||||
|
"herader": [
|
||||||
|
{
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Host": "postman-echo.com",
|
||||||
|
"User-Agent": "PostmanRuntime/7.28.4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"verify": false,
|
||||||
|
"export": [
|
||||||
|
"session_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "test api /get",
|
||||||
|
"api": "examples/api/get.json",
|
||||||
|
"variables": {
|
||||||
|
"user_agent": "iOS/10.4",
|
||||||
|
"device_sn": "$device_sn",
|
||||||
|
"os_platform": "ios",
|
||||||
|
"app_version": "2.8.7"
|
||||||
|
},
|
||||||
|
"extract": {
|
||||||
|
"session_token": "body.headers.\"postman-token\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test api /post",
|
||||||
|
"api": "examples/api/post.json",
|
||||||
|
"variables": {
|
||||||
|
"user_agent": "iOS/10.5",
|
||||||
|
"device_sn": "$device_sn",
|
||||||
|
"os_platform": "ios",
|
||||||
|
"app_version": "2.8.9"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"eq": [
|
||||||
|
"status_code",
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eq": [
|
||||||
|
"body.headers.postman-token",
|
||||||
|
"ea19464c-ddd4-4724-abe9-5e2b254c2723"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test api /put",
|
||||||
|
"api": "examples/api/put.json",
|
||||||
|
"variables": {
|
||||||
|
"user_agent": "iOS/10.6",
|
||||||
|
"device_sn": "$device_sn",
|
||||||
|
"os_platform": "ios",
|
||||||
|
"app_version": "2.8.10"
|
||||||
|
},
|
||||||
|
"extract": {
|
||||||
|
"session_token": "body.headers.\"postman-token\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
47
examples/ref_api_test.yaml
Normal file
47
examples/ref_api_test.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
config:
|
||||||
|
name: 'api test demo'
|
||||||
|
variables:
|
||||||
|
user_agent: iOS/10.3
|
||||||
|
device_sn: TESTCASE_SETUP_XXX
|
||||||
|
os_platform: ios
|
||||||
|
app_version: 2.8.6
|
||||||
|
base_url: 'https://postman-echo.com'
|
||||||
|
herader:
|
||||||
|
- Accept: '*/*'
|
||||||
|
Accept-Encoding: 'gzip, deflate, br'
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Connection: keep-alive
|
||||||
|
Host: postman-echo.com
|
||||||
|
User-Agent: PostmanRuntime/7.28.4
|
||||||
|
verify: false
|
||||||
|
export:
|
||||||
|
- session_token
|
||||||
|
teststeps:
|
||||||
|
- name: 'test api /get'
|
||||||
|
api: examples/api/get.json
|
||||||
|
variables:
|
||||||
|
user_agent: iOS/10.4
|
||||||
|
device_sn: $device_sn
|
||||||
|
os_platform: ios
|
||||||
|
app_version: 2.8.7
|
||||||
|
extract:
|
||||||
|
session_token: 'body.headers."postman-token"'
|
||||||
|
- name: 'test api /post'
|
||||||
|
api: examples/api/post.json
|
||||||
|
variables:
|
||||||
|
user_agent: iOS/10.5
|
||||||
|
device_sn: $device_sn
|
||||||
|
os_platform: ios
|
||||||
|
app_version: 2.8.9
|
||||||
|
validate:
|
||||||
|
- { eq: [ status_code, 200 ] }
|
||||||
|
- { eq: [ body.headers.postman-token, ea19464c-ddd4-4724-abe9-5e2b254c2723 ] }
|
||||||
|
- name: 'test api /put'
|
||||||
|
api: examples/api/put.json
|
||||||
|
variables:
|
||||||
|
user_agent: iOS/10.6
|
||||||
|
device_sn: $device_sn
|
||||||
|
os_platform: ios
|
||||||
|
app_version: 2.8.10
|
||||||
|
extract:
|
||||||
|
session_token: 'body.headers."postman-token"'
|
||||||
18
examples/ref_testcase_test.json
Normal file
18
examples/ref_testcase_test.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "reference testcase test",
|
||||||
|
"base_url": "https://postman-echo.com",
|
||||||
|
"variables": {
|
||||||
|
"os_platform": "ios"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "run demo_httprunner.json",
|
||||||
|
"testcase": "examples/demo_httprunner.json",
|
||||||
|
"variables": {
|
||||||
|
"os_platform": "$os_platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
examples/ref_testcase_test.yaml
Normal file
11
examples/ref_testcase_test.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
config:
|
||||||
|
name: "reference testcase test"
|
||||||
|
base_url: "https://postman-echo.com"
|
||||||
|
variables:
|
||||||
|
os_platform: 'ios'
|
||||||
|
|
||||||
|
teststeps:
|
||||||
|
- name: run demo_httprunner.yaml
|
||||||
|
testcase: examples/demo_httprunner.yaml
|
||||||
|
variables:
|
||||||
|
os_platform: $os_platform
|
||||||
@@ -16,6 +16,9 @@ type Boomer struct {
|
|||||||
|
|
||||||
memoryProfile string
|
memoryProfile string
|
||||||
memoryProfileDuration time.Duration
|
memoryProfileDuration time.Duration
|
||||||
|
|
||||||
|
disableKeepalive bool
|
||||||
|
disableCompression bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStandaloneBoomer returns a new Boomer, which can run without master.
|
// NewStandaloneBoomer returns a new Boomer, which can run without master.
|
||||||
@@ -52,6 +55,24 @@ func (b *Boomer) SetRateLimiter(maxRPS int64, requestIncreaseRate string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisableKeepAlive disable keep-alive for tcp
|
||||||
|
func (b *Boomer) SetDisableKeepAlive(disableKeepalive bool) {
|
||||||
|
b.disableKeepalive = disableKeepalive
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDisableCompression disable compression to prevent the Transport from requesting compression with an "Accept-Encoding: gzip"
|
||||||
|
func (b *Boomer) SetDisableCompression(disableCompression bool) {
|
||||||
|
b.disableCompression = disableCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Boomer) GetDisableKeepAlive() bool {
|
||||||
|
return b.disableKeepalive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Boomer) GetDisableCompression() bool {
|
||||||
|
return b.disableCompression
|
||||||
|
}
|
||||||
|
|
||||||
// SetLoopCount set loop count for test.
|
// SetLoopCount set loop count for test.
|
||||||
func (b *Boomer) SetLoopCount(loopCount int64) {
|
func (b *Boomer) SetLoopCount(loopCount int64) {
|
||||||
b.localRunner.loop = &Loop{loopCount: loopCount}
|
b.localRunner.loop = &Loop{loopCount: loopCount}
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ func deserializeStatsEntry(stat interface{}) (entryOutput *statsEntryOutput, err
|
|||||||
var duration float64
|
var duration float64
|
||||||
if entry.Name == "Total" {
|
if entry.Name == "Total" {
|
||||||
duration = float64(entry.LastRequestTimestamp - entry.StartTime)
|
duration = float64(entry.LastRequestTimestamp - entry.StartTime)
|
||||||
|
// fix: avoid divide by zero
|
||||||
|
if duration < 1 {
|
||||||
|
duration = 1
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
duration = float64(reportStatsInterval / time.Second)
|
duration = float64(reportStatsInterval / time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,3 +211,12 @@ func EnsureFolderExists(folderPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Contains(s []string, e string) bool {
|
||||||
|
for _, a := range s {
|
||||||
|
if a == e {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ var (
|
|||||||
MarshalIndent = json.MarshalIndent
|
MarshalIndent = json.MarshalIndent
|
||||||
Unmarshal = json.Unmarshal
|
Unmarshal = json.Unmarshal
|
||||||
NewDecoder = json.NewDecoder
|
NewDecoder = json.NewDecoder
|
||||||
|
Get = json.Get
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
demoTestCaseJSONPath = "../../examples/demo.json"
|
demoTestCaseJSONPath hrp.TestCasePath = "../../examples/demo.json"
|
||||||
demoTestCaseYAMLPath = "../../examples/demo.yaml"
|
demoTestCaseYAMLPath hrp.TestCasePath = "../../examples/demo.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildHashicorpPlugin() {
|
func buildHashicorpPlugin() {
|
||||||
@@ -33,11 +33,11 @@ func removeHashicorpPlugin() {
|
|||||||
|
|
||||||
func TestGenDemoTestCase(t *testing.T) {
|
func TestGenDemoTestCase(t *testing.T) {
|
||||||
tCase, _ := demoTestCase.ToTCase()
|
tCase, _ := demoTestCase.ToTCase()
|
||||||
err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath)
|
err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath.ToString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath)
|
err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath.ToString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
@@ -58,8 +58,7 @@ func TestJsonDemo(t *testing.T) {
|
|||||||
buildHashicorpPlugin()
|
buildHashicorpPlugin()
|
||||||
defer removeHashicorpPlugin()
|
defer removeHashicorpPlugin()
|
||||||
|
|
||||||
testCase := &hrp.TestCasePath{Path: demoTestCaseJSONPath}
|
err := hrp.NewRunner(nil).Run(&demoTestCaseJSONPath) // hrp.Run(testCase)
|
||||||
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
@@ -69,8 +68,7 @@ func TestYamlDemo(t *testing.T) {
|
|||||||
buildHashicorpPlugin()
|
buildHashicorpPlugin()
|
||||||
defer removeHashicorpPlugin()
|
defer removeHashicorpPlugin()
|
||||||
|
|
||||||
testCase := &hrp.TestCasePath{Path: demoTestCaseYAMLPath}
|
err := hrp.NewRunner(nil).Run(&demoTestCaseYAMLPath) // hrp.Run(testCase)
|
||||||
err := hrp.NewRunner(nil).Run(testCase) // hrp.Run(testCase)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|||||||
47
models.go
47
models.go
@@ -26,6 +26,7 @@ type TConfig struct {
|
|||||||
Name string `json:"name" yaml:"name"` // required
|
Name string `json:"name" yaml:"name"` // required
|
||||||
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
|
||||||
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"`
|
||||||
|
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||||
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
|
||||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||||
@@ -104,6 +105,21 @@ type Request struct {
|
|||||||
Verify bool `json:"verify,omitempty" yaml:"verify,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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) ToAPI() (*API, error) {
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Validator represents validator for one HTTP response.
|
// Validator represents validator for one HTTP response.
|
||||||
type Validator struct {
|
type Validator struct {
|
||||||
Check string `json:"check" yaml:"check"` // get value with jmespath
|
Check string `json:"check" yaml:"check"` // get value with jmespath
|
||||||
@@ -112,20 +128,29 @@ type Validator struct {
|
|||||||
Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional
|
Message string `json:"msg,omitempty" yaml:"msg,omitempty"` // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IAPI represents interface for api,
|
||||||
|
// includes API and APIPath.
|
||||||
|
type IAPI interface {
|
||||||
|
ToAPI() (*API, error)
|
||||||
|
}
|
||||||
|
|
||||||
// TStep represents teststep data structure.
|
// TStep represents teststep data structure.
|
||||||
// Each step maybe two different type: make one HTTP request or reference another testcase.
|
// Each step maybe two different type: make one HTTP request or reference another testcase.
|
||||||
type TStep struct {
|
type TStep struct {
|
||||||
Name string `json:"name" yaml:"name"` // required
|
Name string `json:"name" yaml:"name"` // required
|
||||||
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
Request *Request `json:"request,omitempty" yaml:"request,omitempty"`
|
||||||
TestCase *TestCase `json:"testcase,omitempty" yaml:"testcase,omitempty"`
|
APIPath string `json:"api,omitempty" yaml:"api,omitempty"`
|
||||||
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
|
TestCasePath string `json:"testcase,omitempty" yaml:"testcase,omitempty"`
|
||||||
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
|
APIContent IAPI `json:"api_content,omitempty" yaml:"api_content,omitempty"`
|
||||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
TestCaseContent ITestCase `json:"testcase_content,omitempty" yaml:"testcase_content,omitempty"`
|
||||||
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
Transaction *Transaction `json:"transaction,omitempty" yaml:"transaction,omitempty"`
|
||||||
TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"`
|
Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"`
|
||||||
Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"`
|
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||||
Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"`
|
SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"`
|
||||||
Export []string `json:"export,omitempty" yaml:"export,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
|
type stepType string
|
||||||
|
|||||||
93
parser.go
93
parser.go
@@ -281,6 +281,99 @@ func mergeVariables(variables, overriddenVariables map[string]interface{}) map[s
|
|||||||
return mergedVariables
|
return mergedVariables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// merge two map, the first map have higher priority
|
||||||
|
func mergeMap(m, overriddenMap map[string]string) map[string]string {
|
||||||
|
if overriddenMap == nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
if m == nil {
|
||||||
|
return overriddenMap
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedMap := make(map[string]string)
|
||||||
|
for k, v := range overriddenMap {
|
||||||
|
mergedMap[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
mergedMap[k] = v
|
||||||
|
}
|
||||||
|
return mergedMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge two validators slice, the first validators have higher priority
|
||||||
|
func mergeValidators(validators, overriddenValidators []interface{}) []interface{} {
|
||||||
|
if validators == nil {
|
||||||
|
return overriddenValidators
|
||||||
|
}
|
||||||
|
if overriddenValidators == nil {
|
||||||
|
return validators
|
||||||
|
}
|
||||||
|
var mergedValidators []interface{}
|
||||||
|
validators = append(validators, overriddenValidators...)
|
||||||
|
for _, validator := range validators {
|
||||||
|
flag := true
|
||||||
|
for _, mergedValidator := range mergedValidators {
|
||||||
|
if validator.(Validator).Check == mergedValidator.(Validator).Check {
|
||||||
|
flag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flag {
|
||||||
|
mergedValidators = append(mergedValidators, validator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedValidators
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge two slices, the first slice have higher priority
|
||||||
|
func mergeSlices(slice, overriddenSlice []string) []string {
|
||||||
|
if slice == nil {
|
||||||
|
return overriddenSlice
|
||||||
|
}
|
||||||
|
if overriddenSlice == nil {
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range overriddenSlice {
|
||||||
|
if !builtin.Contains(slice, value) {
|
||||||
|
slice = append(slice, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend teststep with api, teststep will merge and override referenced api
|
||||||
|
func extendWithAPI(testStep *TStep, overriddenStep *API) {
|
||||||
|
// override api name
|
||||||
|
if testStep.Name == "" {
|
||||||
|
testStep.Name = overriddenStep.Name
|
||||||
|
}
|
||||||
|
// merge & override request
|
||||||
|
testStep.Request = overriddenStep.Request
|
||||||
|
// merge & override variables
|
||||||
|
testStep.Variables = mergeVariables(testStep.Variables, overriddenStep.Variables)
|
||||||
|
// merge & override extractors
|
||||||
|
testStep.Extract = mergeMap(testStep.Extract, overriddenStep.Extract)
|
||||||
|
// merge & override validators
|
||||||
|
testStep.Validators = mergeValidators(testStep.Validators, overriddenStep.Validators)
|
||||||
|
// merge & override setupHooks
|
||||||
|
testStep.SetupHooks = mergeSlices(testStep.SetupHooks, overriddenStep.SetupHooks)
|
||||||
|
// merge & override teardownHooks
|
||||||
|
testStep.TeardownHooks = mergeSlices(testStep.TeardownHooks, overriddenStep.TeardownHooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend referenced testcase with teststep, teststep config merge and override referenced testcase config
|
||||||
|
func extendWithTestCase(testStep *TStep, overriddenTestCase *TestCase) {
|
||||||
|
// override testcase name
|
||||||
|
if testStep.Name != "" {
|
||||||
|
overriddenTestCase.Config.Name = testStep.Name
|
||||||
|
}
|
||||||
|
// merge & override variables
|
||||||
|
overriddenTestCase.Config.Variables = mergeVariables(testStep.Variables, overriddenTestCase.Config.Variables)
|
||||||
|
// merge & override extractors
|
||||||
|
overriddenTestCase.Config.Export = mergeSlices(testStep.Export, overriddenTestCase.Config.Export)
|
||||||
|
}
|
||||||
|
|
||||||
var eval = goval.NewEvaluator()
|
var eval = goval.NewEvaluator()
|
||||||
|
|
||||||
// literalEval parse string to number if possible
|
// literalEval parse string to number if possible
|
||||||
|
|||||||
106
parser_test.go
106
parser_test.go
@@ -333,6 +333,112 @@ func TestMergeVariables(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeMap(t *testing.T) {
|
||||||
|
testData := []struct {
|
||||||
|
m map[string]string
|
||||||
|
overriddenMap map[string]string
|
||||||
|
expectMap map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||||
|
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||||
|
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Cache-Control": "no-cache"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"},
|
||||||
|
map[string]string{"Host": "Postman-echo.com", "Connection": "keep-alive", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b342c2723"},
|
||||||
|
map[string]string{"Host": "postman-echo.com", "Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723", "Connection": "keep-alive"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||||
|
nil,
|
||||||
|
map[string]string{"Accept": "*/*", "Accept-Encoding": "gzip, deflate, br", "Connection": "close"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||||
|
map[string]string{"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range testData {
|
||||||
|
mergedMap := mergeMap(data.m, data.overriddenMap)
|
||||||
|
if !assert.Equal(t, data.expectMap, mergedMap) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSlices(t *testing.T) {
|
||||||
|
testData := []struct {
|
||||||
|
slice []string
|
||||||
|
overriddenSlice []string
|
||||||
|
expectSlice []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||||
|
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||||
|
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}", "${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||||
|
nil,
|
||||||
|
[]string{"${setup_hook_example1($name)}", "${setup_hook_example2($name)}"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||||
|
[]string{"${setup_hook_example3($name)}", "${setup_hook_example4($name)}"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range testData {
|
||||||
|
mergedSlice := mergeSlices(data.slice, data.overriddenSlice)
|
||||||
|
if !assert.Equal(t, data.expectSlice, mergedSlice) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeValidators(t *testing.T) {
|
||||||
|
testData := []struct {
|
||||||
|
validators []interface{}
|
||||||
|
overriddenValidators []interface{}
|
||||||
|
expectValidators []interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||||
|
[]interface{}{Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"}},
|
||||||
|
[]interface{}{
|
||||||
|
Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"},
|
||||||
|
Validator{Check: `headers."Content-Type"`, Assert: "equals", Expect: "application/json; charset=utf-8", Message: "assert response header Content-Typ"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 200, Message: "assert response status code"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||||
|
nil,
|
||||||
|
[]interface{}{Validator{Check: "status_code", Assert: "equals", Expect: 302, Message: "assert response status code"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range testData {
|
||||||
|
mergedValidators := mergeValidators(data.validators, data.overriddenValidators)
|
||||||
|
if !assert.Equal(t, data.expectValidators, mergedValidators) {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCallBuiltinFunction(t *testing.T) {
|
func TestCallBuiltinFunction(t *testing.T) {
|
||||||
parser := newParser()
|
parser := newParser()
|
||||||
|
|
||||||
|
|||||||
48
runner.go
48
runner.go
@@ -3,13 +3,14 @@ package hrp
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/flate"
|
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -74,6 +75,20 @@ type HRPRunner struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetClientTransport configures transport of http client for high concurrency load testing
|
||||||
|
func (r *HRPRunner) SetClientTransport(maxConns int, disableKeepAlive bool, disableCompression bool) *HRPRunner {
|
||||||
|
log.Info().Int("maxConns", maxConns).Msg("[init] SetClientTransport")
|
||||||
|
r.client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
DialContext: (&net.Dialer{}).DialContext,
|
||||||
|
MaxIdleConns: 0,
|
||||||
|
MaxIdleConnsPerHost: maxConns,
|
||||||
|
DisableKeepAlives: disableKeepAlive,
|
||||||
|
DisableCompression: disableCompression,
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// SetFailfast configures whether to stop running when one step fails.
|
// SetFailfast configures whether to stop running when one step fails.
|
||||||
func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner {
|
func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner {
|
||||||
log.Info().Bool("failfast", failfast).Msg("[init] SetFailfast")
|
log.Info().Bool("failfast", failfast).Msg("[init] SetFailfast")
|
||||||
@@ -356,12 +371,21 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa
|
|||||||
if _, ok := step.(*StepTestCaseWithOptionalArgs); ok {
|
if _, ok := step.(*StepTestCaseWithOptionalArgs); ok {
|
||||||
// run referenced testcase
|
// run referenced testcase
|
||||||
log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase")
|
log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase")
|
||||||
// TODO: override testcase config
|
|
||||||
stepResult, err = r.runStepTestCase(copiedStep)
|
stepResult, err = r.runStepTestCase(copiedStep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("run referenced testcase step failed")
|
log.Error().Err(err).Msg("run referenced testcase step failed")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if _, ok := step.(*StepAPIWithOptionalArgs); ok {
|
||||||
|
// run referenced API
|
||||||
|
log.Info().Str("api", copiedStep.Name).Msg("run referenced api")
|
||||||
|
api, _ := copiedStep.APIContent.ToAPI()
|
||||||
|
extendWithAPI(copiedStep, api)
|
||||||
|
}
|
||||||
|
// override headers
|
||||||
|
if caseConfig.Headers != nil {
|
||||||
|
copiedStep.Request.Headers = mergeMap(copiedStep.Request.Headers, caseConfig.Headers)
|
||||||
|
}
|
||||||
// parse step request url
|
// parse step request url
|
||||||
var requestUrl interface{}
|
var requestUrl interface{}
|
||||||
requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables)
|
requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables)
|
||||||
@@ -634,7 +658,6 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
|
|||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
ProtoMajor: 1,
|
ProtoMajor: 1,
|
||||||
ProtoMinor: 1,
|
ProtoMinor: 1,
|
||||||
Close: true, // prevent the connection from being re-used
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare request headers
|
// prepare request headers
|
||||||
@@ -906,19 +929,22 @@ func shouldPrintBody(contentType string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeResponseBody(resp *http.Response) error {
|
func decodeResponseBody(resp *http.Response) (err error) {
|
||||||
switch resp.Header.Get("Content-Encoding") {
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
case "br":
|
case "br":
|
||||||
resp.Body = io.NopCloser(brotli.NewReader(resp.Body))
|
resp.Body = io.NopCloser(brotli.NewReader(resp.Body))
|
||||||
case "gzip":
|
case "gzip":
|
||||||
gr, err := gzip.NewReader(resp.Body)
|
resp.Body, err = gzip.NewReader(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp.Body = gr
|
|
||||||
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
|
resp.ContentLength = -1 // set to unknown to avoid Content-Length mismatched
|
||||||
case "deflate":
|
case "deflate":
|
||||||
resp.Body = flate.NewReader(resp.Body)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -929,7 +955,7 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
|
|||||||
StepType: stepTypeTestCase,
|
StepType: stepTypeTestCase,
|
||||||
Success: false,
|
Success: false,
|
||||||
}
|
}
|
||||||
testcase := step.TestCase
|
testcase := step.TestCaseContent
|
||||||
|
|
||||||
// copy testcase to avoid data racing
|
// copy testcase to avoid data racing
|
||||||
copiedTestCase := &TestCase{}
|
copiedTestCase := &TestCase{}
|
||||||
@@ -937,6 +963,8 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
|
|||||||
log.Error().Err(err).Msg("copy testcase failed")
|
log.Error().Err(err).Msg("copy testcase failed")
|
||||||
return stepResult, err
|
return stepResult, err
|
||||||
}
|
}
|
||||||
|
// override testcase config
|
||||||
|
extendWithTestCase(step, copiedTestCase)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase)
|
caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase)
|
||||||
@@ -946,6 +974,8 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
|
|||||||
return stepResult, err
|
return stepResult, err
|
||||||
}
|
}
|
||||||
stepResult.Data = caseRunnerObj.getSummary()
|
stepResult.Data = caseRunnerObj.getSummary()
|
||||||
|
// export testcase export variables
|
||||||
|
stepResult.ExportVars = caseRunnerObj.summary.InOut.ExportVars
|
||||||
stepResult.Success = true
|
stepResult.Success = true
|
||||||
return stepResult, nil
|
return stepResult, nil
|
||||||
}
|
}
|
||||||
@@ -991,7 +1021,7 @@ func (r *caseRunner) getSummary() *testCaseSummary {
|
|||||||
caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
|
caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
|
||||||
exportVars := make(map[string]interface{})
|
exportVars := make(map[string]interface{})
|
||||||
for _, value := range r.Config.Export {
|
for _, value := range r.Config.Export {
|
||||||
exportVars[value] = r.Config.Variables[value]
|
exportVars[value] = r.sessionVariables[value]
|
||||||
}
|
}
|
||||||
caseSummary.InOut.ExportVars = exportVars
|
caseSummary.InOut.ExportVars = exportVars
|
||||||
caseSummary.InOut.ConfigVars = r.Config.Variables
|
caseSummary.InOut.ConfigVars = r.Config.Variables
|
||||||
|
|||||||
@@ -49,17 +49,26 @@ func TestHttpRunner(t *testing.T) {
|
|||||||
AssertEqual("status_code", 200, "check status code").
|
AssertEqual("status_code", 200, "check status code").
|
||||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||||
}}),
|
}}),
|
||||||
|
NewStep("TestCase4").CallRefCase(&demoRefAPIYAMLPath),
|
||||||
|
NewStep("TestCase5").CallRefCase(&demoTestCaseJSONPath),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testcase2 := &TestCase{
|
testcase2 := &TestCase{
|
||||||
Config: NewConfig("TestCase2").SetWeight(3),
|
Config: NewConfig("TestCase2").SetWeight(3),
|
||||||
}
|
}
|
||||||
testcase3 := &TestCasePath{demoTestCaseJSONPath}
|
testcase3 := &TestCase{
|
||||||
|
Config: NewConfig("TestCase1").
|
||||||
|
SetBaseURL("https://postman-echo.com"),
|
||||||
|
TestSteps: []IStep{
|
||||||
|
NewStep("TestCase5").CallRefAPI(&demoAPIYAMLPath),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testcase4 := &demoRefTestCaseJSONPath
|
||||||
|
|
||||||
r := NewRunner(t)
|
r := NewRunner(t)
|
||||||
r.saveTests = true
|
r.saveTests = true
|
||||||
r.genHTMLReport = true
|
r.genHTMLReport = true
|
||||||
err := r.Run(testcase1, testcase2, testcase3)
|
err := r.Run(testcase1, testcase2, testcase3, testcase4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("run testcase error: %v", err)
|
t.Fatalf("run testcase error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
55
step.go
55
step.go
@@ -22,6 +22,12 @@ func (c *TConfig) SetBaseURL(baseURL string) *TConfig {
|
|||||||
return c
|
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.
|
// SetVerifySSL sets whether to verify SSL for current testcase.
|
||||||
func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
|
func (c *TConfig) SetVerifySSL(verify bool) *TConfig {
|
||||||
c.Verify = verify
|
c.Verify = verify
|
||||||
@@ -150,13 +156,21 @@ func (s *StepRequest) PATCH(url string) *StepRequestWithOptionalArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CallRefCase calls a referenced testcase.
|
// CallRefCase calls a referenced testcase.
|
||||||
func (s *StepRequest) CallRefCase(tc *TestCase) *StepTestCaseWithOptionalArgs {
|
func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs {
|
||||||
s.step.TestCase = tc
|
s.step.TestCaseContent, _ = tc.ToTestCase()
|
||||||
return &StepTestCaseWithOptionalArgs{
|
return &StepTestCaseWithOptionalArgs{
|
||||||
step: s.step,
|
step: s.step,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallRefAPI calls a referenced api.
|
||||||
|
func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs {
|
||||||
|
s.step.APIContent, _ = api.ToAPI()
|
||||||
|
return &StepAPIWithOptionalArgs{
|
||||||
|
step: s.step,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StartTransaction starts a transaction.
|
// StartTransaction starts a transaction.
|
||||||
func (s *StepRequest) StartTransaction(name string) *StepTransaction {
|
func (s *StepRequest) StartTransaction(name string) *StepTransaction {
|
||||||
s.step.Transaction = &Transaction{
|
s.step.Transaction = &Transaction{
|
||||||
@@ -274,6 +288,40 @@ func (s *StepRequestWithOptionalArgs) ToStruct() *TStep {
|
|||||||
return s.step
|
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, _ := s.step.APIContent.ToAPI()
|
||||||
|
s.step.Export = append(api.Export, names...)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepAPIWithOptionalArgs) Name() string {
|
||||||
|
if s.step.Name != "" {
|
||||||
|
return s.step.Name
|
||||||
|
}
|
||||||
|
api, _ := s.step.APIContent.ToAPI()
|
||||||
|
return api.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepAPIWithOptionalArgs) Type() string {
|
||||||
|
return "api"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepAPIWithOptionalArgs) ToStruct() *TStep {
|
||||||
|
return s.step
|
||||||
|
}
|
||||||
|
|
||||||
// StepTestCaseWithOptionalArgs implements IStep interface.
|
// StepTestCaseWithOptionalArgs implements IStep interface.
|
||||||
type StepTestCaseWithOptionalArgs struct {
|
type StepTestCaseWithOptionalArgs struct {
|
||||||
step *TStep
|
step *TStep
|
||||||
@@ -295,7 +343,8 @@ func (s *StepTestCaseWithOptionalArgs) Name() string {
|
|||||||
if s.step.Name != "" {
|
if s.step.Name != "" {
|
||||||
return s.step.Name
|
return s.step.Name
|
||||||
}
|
}
|
||||||
return s.step.TestCase.Config.Name
|
ts, _ := s.step.TestCaseContent.ToTestCase()
|
||||||
|
return ts.Config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepTestCaseWithOptionalArgs) Type() string {
|
func (s *StepTestCaseWithOptionalArgs) Type() string {
|
||||||
|
|||||||
14
step_test.go
14
step_test.go
@@ -16,13 +16,13 @@ var (
|
|||||||
AssertEqual("body.args.foo1", "bar1", "check param foo1").
|
AssertEqual("body.args.foo1", "bar1", "check param foo1").
|
||||||
AssertEqual("body.args.foo2", "bar2", "check param foo2")
|
AssertEqual("body.args.foo2", "bar2", "check param foo2")
|
||||||
stepPOSTData = NewStep("post form data").
|
stepPOSTData = NewStep("post form data").
|
||||||
POST("/post").
|
POST("/post").
|
||||||
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
|
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus", "Content-Type": "application/x-www-form-urlencoded"}).
|
||||||
WithBody("a=1&b=2").
|
WithBody("a=1&b=2").
|
||||||
WithCookies(map[string]string{"user": "debugtalk"}).
|
WithCookies(map[string]string{"user": "debugtalk"}).
|
||||||
Validate().
|
Validate().
|
||||||
AssertEqual("status_code", 200, "check status code")
|
AssertEqual("status_code", 200, "check status code")
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunRequestGetToStruct(t *testing.T) {
|
func TestRunRequestGetToStruct(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user