Merge branch 'master' of github.com:httprunner/httprunner

This commit is contained in:
buyuxiang
2022-05-24 14:07:03 +08:00
43 changed files with 649 additions and 121 deletions
+7 -8
View File
@@ -6,7 +6,6 @@ on:
- master - master
- v2 - v2
- v3 - v3
- v4.1-dev
pull_request: pull_request:
env: env:
@@ -35,10 +34,10 @@ jobs:
- name: Run generated demo tests - name: Run generated demo tests
run: ./output/hrp run demo/testcases/ run: ./output/hrp run demo/testcases/
- name: Run API test demo in examples - name: Run API test demo in examples
run: ./output/hrp run examples/demo-with-py-plugin/testcases/demo_with_funplugin.json run: ./output/hrp run examples/demo-with-py-plugin/testcases/demo.json
- name: Run load test demo in examples - name: Run load test demo in examples
run: | run: |
./output/hrp boom examples/demo-with-py-plugin/testcases/demo_with_funplugin.json --spawn-count 10 --spawn-rate 10 --loop-count 10 ./output/hrp boom examples/demo-with-py-plugin/testcases/demo.json --spawn-count 10 --spawn-rate 10 --loop-count 10
scaffold-with-go-plugin: scaffold-with-go-plugin:
strategy: strategy:
@@ -64,11 +63,11 @@ jobs:
- name: Run API test demo in examples - name: Run API test demo in examples
run: | run: |
go build -o examples/demo-with-go-plugin/debugtalk.bin examples/demo-with-go-plugin/plugin/debugtalk.go go build -o examples/demo-with-go-plugin/debugtalk.bin examples/demo-with-go-plugin/plugin/debugtalk.go
./output/hrp run examples/demo-with-go-plugin/testcases/demo_with_funplugin.json ./output/hrp run examples/demo-with-go-plugin/testcases/demo.json
- name: Run load test demo in examples - name: Run load test demo in examples
run: | run: |
go build -o examples/demo-with-go-plugin/debugtalk.bin examples/demo-with-go-plugin/plugin/debugtalk.go go build -o examples/demo-with-go-plugin/debugtalk.bin examples/demo-with-go-plugin/plugin/debugtalk.go
./output/hrp boom examples/demo-with-go-plugin/testcases/demo_with_funplugin.json --spawn-count 10 --spawn-rate 10 --loop-count 10 ./output/hrp boom examples/demo-with-go-plugin/testcases/demo.json --spawn-count 10 --spawn-rate 10 --loop-count 10
scaffold-without-custom-plugin: scaffold-without-custom-plugin:
strategy: strategy:
@@ -90,9 +89,9 @@ jobs:
- name: Run start project - name: Run start project
run: ./output/hrp startproject demo --ignore-plugin run: ./output/hrp startproject demo --ignore-plugin
- name: Run generated demo tests - name: Run generated demo tests
run: ./output/hrp run demo/testcases/demo_without_funplugin.json run: ./output/hrp run demo/testcases/requests.json
- name: Run API test demo in examples - name: Run API test demo in examples
run: ./output/hrp run examples/demo-without-plugin/testcases/demo_without_funplugin.json run: ./output/hrp run examples/demo-without-plugin/testcases/requests.json
- name: Run load test demo in examples - name: Run load test demo in examples
run: | run: |
./output/hrp boom examples/demo-without-plugin/testcases/demo_without_funplugin.json --spawn-count 10 --spawn-rate 10 --loop-count 10 ./output/hrp boom examples/demo-without-plugin/testcases/requests.json --spawn-count 10 --spawn-rate 10 --loop-count 10
-1
View File
@@ -6,7 +6,6 @@ on:
- master - master
- v2 - v2
- v3 - v3
- v4.1-dev
pull_request: pull_request:
env: env:
-1
View File
@@ -6,7 +6,6 @@ on:
- master - master
- v2 - v2
- v3 - v3
- v4.1-dev
pull_request: pull_request:
env: env:
+1 -1
View File
@@ -12,7 +12,7 @@
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。 > HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
![flow chart](docs/assets/hrp-flow.jpg) ![flow chart](https://httprunner.com/image/hrp-flow.jpg)
[CHANGELOG] | [中文] [CHANGELOG] | [中文]
+16 -3
View File
@@ -1,15 +1,28 @@
# Release History # Release History
## v4.1.0-alpha (2022-05-09) ## v4.1.0 (2022-05-23)
- feat: add `wiki` sub-command to open httprunner website
**go version**
- fix #1309: locate plugin file upward recursively until system root dir
- feat: support converting Postman collection to HttpRunner testcase
- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter`
## v4.1.0-beta (2022-05-21)
- feat: add pre-commit-hook to format go/python code - feat: add pre-commit-hook to format go/python code
**go version** **go version**
- feat: add boomer mode(standalone/master/worker)
- feat: support load testing with specified `--profile` configuration file
- fix: step request elapsed timing should contain ContentTransfer part - fix: step request elapsed timing should contain ContentTransfer part
- fix #1288: unable to go get httprunner v4 - fix #1288: unable to go get httprunner v4
- feat: support converting Postman collection to HttpRunner testcase - fix: panic when config didn't exist in testcase file
- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter` - fix: disable keep alive and improve RPS accuracy
- fix: improve RPS accuracy
**python version** **python version**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 353 KiB

+1
View File
@@ -31,6 +31,7 @@ hrp boom [flags]
--max-rps int Max RPS that boomer can generate, disabled by default. --max-rps int Max RPS that boomer can generate, disabled by default.
--mem-profile string Enable memory profiling. --mem-profile string Enable memory profiling.
--mem-profile-duration duration Memory profile duration. (default 30s) --mem-profile-duration duration Memory profile duration. (default 30s)
--profile string profile for load testing
--prometheus-gateway string Prometheus Pushgateway url. --prometheus-gateway string Prometheus Pushgateway url.
--request-increase-rate string Request increase rate, disabled by default. (default "-1") --request-increase-rate string Request increase rate, disabled by default. (default "-1")
--spawn-count int The number of users to spawn for load testing (default 1) --spawn-count int The number of users to spawn for load testing (default 1)
+1 -1
View File
@@ -2,4 +2,4 @@ module plugin
go 1.16 go 1.16
require github.com/httprunner/funplugin v0.4.5 // indirect require github.com/httprunner/funplugin v0.4.6 // indirect
+2 -2
View File
@@ -58,8 +58,8 @@ github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos= github.com/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo=
github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/httprunner/funplugin v0.4.6/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -13,7 +13,7 @@ teststeps:
variables: variables:
foo1: testcase_ref_bar1 foo1: testcase_ref_bar1
expect_foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1
testcase: testcases/demo_requests.yml testcase: testcases/requests.yml
export: export:
- foo3 - foo3
- -
@@ -0,0 +1,138 @@
{
"config": {
"name": "request methods testcase with functions",
"variables": {
"foo1": "config_bar1",
"foo2": "config_bar2",
"expect_foo1": "config_bar1",
"expect_foo2": "config_bar2"
},
"base_url": "https://postman-echo.com",
"verify": false,
"export": [
"foo3"
]
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "bar11",
"foo2": "bar21",
"sum_v": "${sum_two_int(1, 2)}"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"sum_v": "$sum_v"
},
"headers": {
"User-Agent": "funplugin/${get_version()}"
}
},
"extract": {
"foo3": "body.args.foo2"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.args.foo1",
"bar11"
]
},
{
"eq": [
"body.args.sum_v",
"3"
]
},
{
"eq": [
"body.args.foo2",
"bar21"
]
}
]
},
{
"name": "post raw text",
"variables": {
"foo1": "bar12",
"foo3": "bar32"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain"
},
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.data",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."
]
}
]
},
{
"name": "post form data",
"variables": {
"foo2": "bar23"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded"
},
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.form.foo1",
"$expect_foo1"
]
},
{
"eq": [
"body.form.foo2",
"bar23"
]
},
{
"eq": [
"body.form.foo3",
"bar21"
]
}
]
}
]
}
@@ -13,7 +13,7 @@ teststeps:
variables: variables:
foo1: testcase_ref_bar1 foo1: testcase_ref_bar1
expect_foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1
testcase: testcases/demo_requests.yml testcase: testcases/requests.yml
export: export:
- foo3 - foo3
- -
@@ -0,0 +1,138 @@
{
"config": {
"name": "request methods testcase with functions",
"variables": {
"foo1": "config_bar1",
"foo2": "config_bar2",
"expect_foo1": "config_bar1",
"expect_foo2": "config_bar2"
},
"base_url": "https://postman-echo.com",
"verify": false,
"export": [
"foo3"
]
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "bar11",
"foo2": "bar21",
"sum_v": "${sum_two_int(1, 2)}"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"sum_v": "$sum_v"
},
"headers": {
"User-Agent": "funplugin/${get_version()}"
}
},
"extract": {
"foo3": "body.args.foo2"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.args.foo1",
"bar11"
]
},
{
"eq": [
"body.args.sum_v",
"3"
]
},
{
"eq": [
"body.args.foo2",
"bar21"
]
}
]
},
{
"name": "post raw text",
"variables": {
"foo1": "bar12",
"foo3": "bar32"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain"
},
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.data",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."
]
}
]
},
{
"name": "post form data",
"variables": {
"foo2": "bar23"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded"
},
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.form.foo1",
"$expect_foo1"
]
},
{
"eq": [
"body.form.foo2",
"bar23"
]
},
{
"eq": [
"body.form.foo3",
"bar21"
]
}
]
}
]
}
+8 -2
View File
@@ -10,8 +10,14 @@
}, },
"parameters_setting": { "parameters_setting": {
"strategies": { "strategies": {
"user_agent": "sequential", "user_agent": {
"username-password": "random" "name": "user-identity",
"pick_order": "sequential"
},
"username-password": {
"name": "user-info",
"pick_order": "random"
}
}, },
"limit": 6 "limit": 6
}, },
+6 -2
View File
@@ -5,8 +5,12 @@ config:
username-password: ${parameterize($file)} username-password: ${parameterize($file)}
parameters_setting: parameters_setting:
strategies: strategies:
user_agent: "sequential" user_agent:
username-password: "random" name: "user-identity"
pick_order: "sequential"
username-password:
name: "user-info"
pick_order: "random"
limit: 6 limit: 6
variables: variables:
app_version: v1 app_version: v1
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/go-openapi/spec v0.20.6 github.com/go-openapi/spec v0.20.6
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
github.com/httprunner/funplugin v0.4.5 github.com/httprunner/funplugin v0.4.6
github.com/jinzhu/copier v0.3.2 github.com/jinzhu/copier v0.3.2
github.com/jmespath/go-jmespath v0.4.0 github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
+2 -2
View File
@@ -253,8 +253,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos= github.com/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo=
github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/httprunner/funplugin v0.4.6/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
+55 -39
View File
@@ -1,12 +1,15 @@
package cmd package cmd
import ( import (
"os"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/boomer"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
) )
// boomCmd represents the boom command // boomCmd represents the boom command
@@ -28,57 +31,70 @@ var boomCmd = &cobra.Command{
path := hrp.TestCasePath(arg) path := hrp.TestCasePath(arg)
paths = append(paths, &path) paths = append(paths, &path)
} }
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate) // if set profile, the priority is higher than the other commands
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate) if boomArgs.profile != "" {
if loopCount > 0 { err := builtin.LoadFile(boomArgs.profile, &boomArgs)
hrpBoomer.SetLoopCount(loopCount) if err != nil {
log.Error().Err(err).Msg("failed to load profile")
os.Exit(1)
}
} }
if !disableConsoleOutput {
hrpBoomer := hrp.NewBoomer(boomArgs.SpawnCount, boomArgs.SpawnRate)
hrpBoomer.SetRateLimiter(boomArgs.MaxRPS, boomArgs.RequestIncreaseRate)
if boomArgs.LoopCount > 0 {
hrpBoomer.SetLoopCount(boomArgs.LoopCount)
}
if !boomArgs.DisableConsoleOutput {
hrpBoomer.AddOutput(boomer.NewConsoleOutput()) hrpBoomer.AddOutput(boomer.NewConsoleOutput())
} }
if prometheusPushgatewayURL != "" { if boomArgs.PrometheusPushgatewayURL != "" {
hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp", hrpBoomer.GetMode())) hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(boomArgs.PrometheusPushgatewayURL, "hrp", hrpBoomer.GetMode()))
} }
hrpBoomer.SetDisableKeepAlive(disableKeepalive) hrpBoomer.SetDisableKeepAlive(boomArgs.DisableKeepalive)
hrpBoomer.SetDisableCompression(disableCompression) hrpBoomer.SetDisableCompression(boomArgs.DisableCompression)
hrpBoomer.SetClientTransport() hrpBoomer.SetClientTransport()
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) hrpBoomer.EnableCPUProfile(boomArgs.CPUProfile, boomArgs.CPUProfileDuration)
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) hrpBoomer.EnableMemoryProfile(boomArgs.MemoryProfile, boomArgs.MemoryProfileDuration)
hrpBoomer.EnableGracefulQuit() hrpBoomer.EnableGracefulQuit()
hrpBoomer.Run(paths...) hrpBoomer.Run(paths...)
}, },
} }
var ( type BoomArgs struct {
spawnCount int SpawnCount int `json:"spawn-count,omitempty" yaml:"spawn-count,omitempty"`
spawnRate float64 SpawnRate float64 `json:"spawn-rate,omitempty" yaml:"spawn-rate,omitempty"`
maxRPS int64 MaxRPS int64 `json:"max-rps,omitempty" yaml:"max-rps,omitempty"`
loopCount int64 LoopCount int64 `json:"loop-count,omitempty" yaml:"loop-count,omitempty"`
requestIncreaseRate string RequestIncreaseRate string `json:"request-increase-rate,omitempty" yaml:"request-increase-rate,omitempty"`
memoryProfile string MemoryProfile string `json:"memory-profile,omitempty" yaml:"memory-profile,omitempty"`
memoryProfileDuration time.Duration MemoryProfileDuration time.Duration `json:"memory-profile-duration" yaml:"memory-profile-duration"`
cpuProfile string CPUProfile string `json:"cpu-profile,omitempty" yaml:"cpu-profile,omitempty"`
cpuProfileDuration time.Duration CPUProfileDuration time.Duration `json:"cpu-profile-duration,omitempty" yaml:"cpu-profile-duration,omitempty"`
prometheusPushgatewayURL string PrometheusPushgatewayURL string `json:"prometheus-gateway,omitempty" yaml:"prometheus-gateway,omitempty"`
disableConsoleOutput bool DisableConsoleOutput bool `json:"disable-console-output,omitempty" yaml:"disable-console-output,omitempty"`
disableCompression bool DisableCompression bool `json:"disable-compression,omitempty" yaml:"disable-compression,omitempty"`
disableKeepalive bool DisableKeepalive bool `json:"disable-keepalive,omitempty" yaml:"disable-keepalive,omitempty"`
) profile string
}
var boomArgs BoomArgs
func init() { func init() {
rootCmd.AddCommand(boomCmd) rootCmd.AddCommand(boomCmd)
boomCmd.Flags().Int64Var(&maxRPS, "max-rps", 0, "Max RPS that boomer can generate, disabled by default.") boomCmd.Flags().Int64Var(&boomArgs.MaxRPS, "max-rps", 0, "Max RPS that boomer can generate, disabled by default.")
boomCmd.Flags().StringVar(&requestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.") boomCmd.Flags().StringVar(&boomArgs.RequestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.")
boomCmd.Flags().IntVar(&spawnCount, "spawn-count", 1, "The number of users to spawn for load testing") boomCmd.Flags().IntVar(&boomArgs.SpawnCount, "spawn-count", 1, "The number of users to spawn for load testing")
boomCmd.Flags().Float64Var(&spawnRate, "spawn-rate", 1, "The rate for spawning users") boomCmd.Flags().Float64Var(&boomArgs.SpawnRate, "spawn-rate", 1, "The rate for spawning users")
boomCmd.Flags().Int64Var(&loopCount, "loop-count", -1, "The specify running cycles for load testing") boomCmd.Flags().Int64Var(&boomArgs.LoopCount, "loop-count", -1, "The specify running cycles for load testing")
boomCmd.Flags().StringVar(&memoryProfile, "mem-profile", "", "Enable memory profiling.") boomCmd.Flags().StringVar(&boomArgs.MemoryProfile, "mem-profile", "", "Enable memory profiling.")
boomCmd.Flags().DurationVar(&memoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.") boomCmd.Flags().DurationVar(&boomArgs.MemoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.")
boomCmd.Flags().StringVar(&cpuProfile, "cpu-profile", "", "Enable CPU profiling.") boomCmd.Flags().StringVar(&boomArgs.CPUProfile, "cpu-profile", "", "Enable CPU profiling.")
boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.") boomCmd.Flags().DurationVar(&boomArgs.CPUProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.")
boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.") boomCmd.Flags().StringVar(&boomArgs.PrometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.")
boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.") boomCmd.Flags().BoolVar(&boomArgs.DisableConsoleOutput, "disable-console-output", false, "Disable console output.")
boomCmd.Flags().BoolVar(&disableCompression, "disable-compression", false, "Disable compression") boomCmd.Flags().BoolVar(&boomArgs.DisableCompression, "disable-compression", false, "Disable compression")
boomCmd.Flags().BoolVar(&disableKeepalive, "disable-keepalive", false, "Disable keepalive") boomCmd.Flags().BoolVar(&boomArgs.DisableKeepalive, "disable-keepalive", false, "Disable keepalive")
boomCmd.Flags().StringVar(&boomArgs.profile, "profile", "", "profile for load testing")
} }
+4 -3
View File
@@ -11,9 +11,10 @@ import (
) )
var scaffoldCmd = &cobra.Command{ var scaffoldCmd = &cobra.Command{
Use: "startproject $project_name", Use: "startproject $project_name",
Short: "create a scaffold project", Aliases: []string{"scaffold"},
Args: cobra.ExactValidArgs(1), Short: "create a scaffold project",
Args: cobra.ExactValidArgs(1),
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel) setLogLevel(logLevel)
}, },
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/wiki"
)
var wikiCmd = &cobra.Command{
Use: "wiki",
Aliases: []string{"info", "docs", "doc"},
Short: "visit https://httprunner.com",
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
return wiki.OpenWiki()
},
}
func init() {
rootCmd.AddCommand(wikiCmd)
}
+3 -3
View File
@@ -39,11 +39,11 @@ const (
) )
func fmta(d time.Duration) string { func fmta(d time.Duration) string {
return color.BlueString("%7dms", int(d.Milliseconds())) return color.YellowString("%7dms", int(d.Milliseconds()))
} }
func fmtb(d time.Duration) string { func fmtb(d time.Duration) string {
return color.MagentaString("%-9s", strconv.Itoa(int(d.Milliseconds()))+"ms") return color.RedString("%-9s", strconv.Itoa(int(d.Milliseconds()))+"ms")
} }
func grayscale(code color.Attribute) func(string, ...interface{}) string { func grayscale(code color.Attribute) func(string, ...interface{}) string {
@@ -137,7 +137,7 @@ func (s *Stat) Print() {
if s.network != "" && s.addr != "" { if s.network != "" && s.addr != "" {
printf("\n%s %s: %s\n", printf("\n%s %s: %s\n",
color.CyanString("Connected to"), color.CyanString("Connected to"),
color.YellowString(s.network), color.MagentaString(s.network),
color.BlueString(s.addr), color.BlueString(s.addr),
) )
} }
+38 -5
View File
@@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"time"
"github.com/httprunner/funplugin/shared" "github.com/httprunner/funplugin/shared"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -13,6 +14,7 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
) )
type PluginType string type PluginType string
@@ -23,6 +25,13 @@ const (
Go PluginType = "go" Go PluginType = "go"
) )
type ProjectInfo struct {
ProjectName string `json:"project_name,omitempty" yaml:"project_name,omitempty"`
ProjectPath string `json:"project_path,omitempty" yaml:"project_path,omitempty"`
CreateTime time.Time `json:"create_time,omitempty" yaml:"create_time,omitempty"`
Version string `json:"hrp_version,omitempty" yaml:"hrp_version,omitempty"`
}
//go:embed templates/* //go:embed templates/*
var templatesDir embed.FS var templatesDir embed.FS
@@ -68,6 +77,12 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
os.RemoveAll(projectName) os.RemoveAll(projectName)
} }
// get project abs path
projectPath, err := filepath.Abs(projectName)
if err != nil {
projectPath = projectName
}
// create project folders // create project folders
if err := builtin.CreateFolder(projectName); err != nil { if err := builtin.CreateFolder(projectName); err != nil {
return err return err
@@ -88,8 +103,21 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
return err return err
} }
projectInfo := &ProjectInfo{
ProjectName: projectName,
ProjectPath: projectPath,
CreateTime: time.Now(),
Version: version.VERSION,
}
// dump project information to file
err = builtin.Dump2JSON(projectInfo, filepath.Join(projectName, "proj.json"))
if err != nil {
return err
}
// create .gitignore // create .gitignore
err := CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore")) err = CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore"))
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +130,7 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
// create demo testcases // create demo testcases
if pluginType == Ignore { if pluginType == Ignore {
err := CopyFile("templates/testcases/demo_without_funplugin.json", err := CopyFile("templates/testcases/demo_without_funplugin.json",
filepath.Join(projectName, "testcases", "demo_without_funplugin.json")) filepath.Join(projectName, "testcases", "requests.json"))
if err != nil { if err != nil {
return err return err
} }
@@ -111,17 +139,22 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
} }
err = CopyFile("templates/testcases/demo_with_funplugin.json", err = CopyFile("templates/testcases/demo_with_funplugin.json",
filepath.Join(projectName, "testcases", "demo_with_funplugin.json")) filepath.Join(projectName, "testcases", "demo.json"))
if err != nil {
return err
}
err = CopyFile("templates/testcases/demo_requests.json",
filepath.Join(projectName, "testcases", "requests.json"))
if err != nil { if err != nil {
return err return err
} }
err = CopyFile("templates/testcases/demo_requests.yml", err = CopyFile("templates/testcases/demo_requests.yml",
filepath.Join(projectName, "testcases", "demo_requests.yml")) filepath.Join(projectName, "testcases", "requests.yml"))
if err != nil { if err != nil {
return err return err
} }
err = CopyFile("templates/testcases/demo_ref_testcase.yml", err = CopyFile("templates/testcases/demo_ref_testcase.yml",
filepath.Join(projectName, "testcases", "demo_ref_testcase.yml")) filepath.Join(projectName, "testcases", "ref_testcase.yml"))
if err != nil { if err != nil {
return err return err
} }
@@ -13,7 +13,7 @@ teststeps:
variables: variables:
foo1: testcase_ref_bar1 foo1: testcase_ref_bar1
expect_foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1
testcase: testcases/demo_requests.yml testcase: testcases/requests.yml
export: export:
- foo3 - foo3
- -
@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v4.0.0 # NOTE: Generated By HttpRunner v4.0.0
# FROM: testcases/demo_ref_testcase.yml # FROM: testcases/ref_testcase.yml
import sys import sys
@@ -0,0 +1,138 @@
{
"config": {
"name": "request methods testcase with functions",
"variables": {
"foo1": "config_bar1",
"foo2": "config_bar2",
"expect_foo1": "config_bar1",
"expect_foo2": "config_bar2"
},
"base_url": "https://postman-echo.com",
"verify": false,
"export": [
"foo3"
]
},
"teststeps": [
{
"name": "get with params",
"variables": {
"foo1": "bar11",
"foo2": "bar21",
"sum_v": "${sum_two_int(1, 2)}"
},
"request": {
"method": "GET",
"url": "/get",
"params": {
"foo1": "$foo1",
"foo2": "$foo2",
"sum_v": "$sum_v"
},
"headers": {
"User-Agent": "funplugin/${get_version()}"
}
},
"extract": {
"foo3": "body.args.foo2"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.args.foo1",
"bar11"
]
},
{
"eq": [
"body.args.sum_v",
"3"
]
},
{
"eq": [
"body.args.foo2",
"bar21"
]
}
]
},
{
"name": "post raw text",
"variables": {
"foo1": "bar12",
"foo3": "bar32"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "text/plain"
},
"data": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.data",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."
]
}
]
},
{
"name": "post form data",
"variables": {
"foo2": "bar23"
},
"request": {
"method": "POST",
"url": "/post",
"headers": {
"User-Agent": "funplugin/${get_version()}",
"Content-Type": "application/x-www-form-urlencoded"
},
"data": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
},
"validate": [
{
"eq": [
"status_code",
200
]
},
{
"eq": [
"body.form.foo1",
"$expect_foo1"
]
},
{
"eq": [
"body.form.foo2",
"bar23"
]
},
{
"eq": [
"body.form.foo3",
"bar21"
]
}
]
}
]
}
@@ -1,8 +1,8 @@
# NOTE: Generated By HttpRunner v4.0.0 # NOTE: Generated By HttpRunner v4.0.0
# FROM: testcases/demo_requests.yml # FROM: testcases/requests.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseDemoRequests(HttpRunner): class TestCaseDemoRequests(HttpRunner):
+1 -1
View File
@@ -1 +1 @@
v4.1.0-alpha v4.1.0-beta
+12
View File
@@ -0,0 +1,12 @@
package wiki
import (
"os/exec"
"github.com/rs/zerolog/log"
)
func OpenWiki() error {
log.Info().Msgf("%s https://httprunner.com", openCmd)
return exec.Command(openCmd, "https://httprunner.com").Run()
}
+3
View File
@@ -0,0 +1,3 @@
package wiki
const openCmd = "open"
+3
View File
@@ -0,0 +1,3 @@
package wiki
const openCmd = "xdg-open"
+3
View File
@@ -0,0 +1,3 @@
package wiki
const openCmd = "explorer"
+15 -10
View File
@@ -12,17 +12,17 @@ import (
) )
type TParamsConfig struct { type TParamsConfig struct {
Strategy iteratorStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // overall strategy PickOrder iteratorPickOrder `json:"strategy,omitempty" yaml:"strategy,omitempty"` // overall pick-order strategy
Strategies map[string]iteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters Strategies map[string]iteratorStrategy `json:"strategies,omitempty" yaml:"strategies,omitempty"` // individual strategies for each parameters
Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` Limit int `json:"limit,omitempty" yaml:"limit,omitempty"`
} }
type iteratorStrategy string type iteratorPickOrder string
const ( const (
strategySequential iteratorStrategy = "sequential" pickOrderSequential iteratorPickOrder = "sequential"
strategyRandom iteratorStrategy = "random" pickOrderRandom iteratorPickOrder = "random"
strategyUnique iteratorStrategy = "unique" pickOrderUnique iteratorPickOrder = "unique"
) )
/* /*
@@ -33,6 +33,11 @@ const (
*/ */
type Parameters []map[string]interface{} type Parameters []map[string]interface{}
type iteratorStrategy struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
PickOrder iteratorPickOrder `json:"pick_order,omitempty" yaml:"pick_order,omitempty"`
}
func initParametersIterator(cfg *TConfig) (*ParametersIterator, error) { func initParametersIterator(cfg *TConfig) (*ParametersIterator, error) {
parameters, err := loadParameters(cfg.Parameters, cfg.Variables) parameters, err := loadParameters(cfg.Parameters, cfg.Variables)
if err != nil { if err != nil {
@@ -62,15 +67,15 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf
parametersList := make([]Parameters, 0) parametersList := make([]Parameters, 0)
for paramName := range parameters { for paramName := range parameters {
// check parameter individual strategy // check parameter individual pick order strategy
strategy, ok := config.Strategies[paramName] strategy, ok := config.Strategies[paramName]
if !ok { if !ok {
// default to overall strategy // default to overall pick order strategy
strategy = config.Strategy strategy.PickOrder = config.PickOrder
} }
// group parameters by strategy // group parameters by pick order strategy
if strategy == strategyRandom { if strategy.PickOrder == pickOrderRandom {
iterator.randomParameterNames = append(iterator.randomParameterNames, paramName) iterator.randomParameterNames = append(iterator.randomParameterNames, paramName)
} else { } else {
parametersList = append(parametersList, parameters[paramName]) parametersList = append(parametersList, parameters[paramName])
+14 -14
View File
@@ -137,25 +137,25 @@ func TestInitParametersIteratorCount(t *testing.T) {
}, },
6, // 3 * 2 * 1 6, // 3 * 2 * 1
}, },
// default equals to set overall parameters strategy to "sequential" // default equals to set overall parameters pick-order to "sequential"
{ {
&TConfig{ &TConfig{
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategy: "sequential", PickOrder: "sequential",
}, },
}, },
6, // 3 * 2 * 1 6, // 3 * 2 * 1
}, },
// default equals to set each individual parameters strategy to "sequential" // default equals to set each individual parameters pick-order to "sequential"
{ {
&TConfig{ &TConfig{
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{ Strategies: map[string]iteratorStrategy{
"username-password": "sequential", "username-password": {Name: "user-info", PickOrder: "sequential"},
"user_agent": "sequential", "user_agent": {Name: "user-identity", PickOrder: "sequential"},
"app_version": "sequential", "app_version": {Name: "app-version", PickOrder: "sequential"},
}, },
}, },
}, },
@@ -166,33 +166,33 @@ func TestInitParametersIteratorCount(t *testing.T) {
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{ Strategies: map[string]iteratorStrategy{
"user_agent": "sequential", "user_agent": {Name: "user-identity", PickOrder: "sequential"},
"app_version": "sequential", "app_version": {Name: "app-version", PickOrder: "sequential"},
}, },
}, },
}, },
6, // 3 * 2 * 1 6, // 3 * 2 * 1
}, },
// set overall parameters overall strategy to "random" // set overall parameters overall pick-order to "random"
// each random parameters only select one item // each random parameters only select one item
{ {
&TConfig{ &TConfig{
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategy: "random", PickOrder: "random",
}, },
}, },
1, // 1 * 1 * 1 1, // 1 * 1 * 1
}, },
// set some individual parameters strategy to "random" // set some individual parameters pick-order to "random"
// this will override overall strategy // this will override overall strategy
{ {
&TConfig{ &TConfig{
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{ Strategies: map[string]iteratorStrategy{
"user_agent": "random", "user_agent": {Name: "user-identity", PickOrder: "random"},
}, },
}, },
}, },
@@ -203,7 +203,7 @@ func TestInitParametersIteratorCount(t *testing.T) {
Parameters: configParameters, Parameters: configParameters,
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Strategies: map[string]iteratorStrategy{ Strategies: map[string]iteratorStrategy{
"username-password": "random", "username-password": {Name: "user-info", PickOrder: "random"},
}, },
}, },
}, },
@@ -349,7 +349,7 @@ func TestInitParametersIteratorContent(t *testing.T) {
ParametersSetting: &TParamsConfig{ ParametersSetting: &TParamsConfig{
Limit: 5, // limit could also be greater than total Limit: 5, // limit could also be greater than total
Strategies: map[string]iteratorStrategy{ Strategies: map[string]iteratorStrategy{
"username-password": "random", "username-password": {Name: "user-info", PickOrder: "random"},
}, },
}, },
}, },
+3 -9
View File
@@ -78,11 +78,11 @@ func locatePlugin(path string) (pluginPath string, err error) {
return return
} }
log.Warn().Err(err).Str("path", path).Msg("plugin file not found")
return "", fmt.Errorf("plugin file not found") return "", fmt.Errorf("plugin file not found")
} }
// locateFile searches destFile upward recursively until current // locateFile searches destFile upward recursively until system root dir
// working directory or system root dir.
func locateFile(startPath string, destFile string) (string, error) { func locateFile(startPath string, destFile string) (string, error) {
stat, err := os.Stat(startPath) stat, err := os.Stat(startPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -103,12 +103,6 @@ func locateFile(startPath string, destFile string) (string, error) {
return pluginPath, nil return pluginPath, nil
} }
// current working directory
cwd, _ := os.Getwd()
if startDir == cwd {
return "", fmt.Errorf("searched to CWD, plugin file not found")
}
// system root dir // system root dir
parentDir, _ := filepath.Abs(filepath.Dir(startDir)) parentDir, _ := filepath.Abs(filepath.Dir(startDir))
if parentDir == startDir { if parentDir == startDir {
@@ -118,7 +112,7 @@ func locateFile(startPath string, destFile string) (string, error) {
return locateFile(parentDir, destFile) return locateFile(parentDir, destFile)
} }
func getProjectRootDirPath(path string) (rootDir string, err error) { func GetProjectRootDirPath(path string) (rootDir string, err error) {
pluginPath, err := locatePlugin(path) pluginPath, err := locatePlugin(path)
if err == nil { if err == nil {
rootDir = filepath.Dir(pluginPath) rootDir = filepath.Dir(pluginPath)
+2 -2
View File
@@ -207,7 +207,7 @@ func TestLoadTestCases(t *testing.T) {
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }
if !assert.Equal(t, len(testCases), 3) { if !assert.Equal(t, 4, len(testCases)) {
t.Fatal() t.Fatal()
} }
@@ -217,7 +217,7 @@ func TestLoadTestCases(t *testing.T) {
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }
if !assert.Equal(t, len(testCases), 3) { if !assert.Equal(t, len(testCases), 4) {
t.Fatal() t.Fatal()
} }
+3 -3
View File
@@ -75,7 +75,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
} }
// locate project root dir by plugin path // locate project root dir by plugin path
projectRootDir, err := getProjectRootDirPath(casePath) projectRootDir, err := GetProjectRootDirPath(casePath)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get project root dir") return nil, errors.Wrap(err, "failed to get project root dir")
} }
@@ -361,8 +361,8 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
testCasePath := TestCasePath(path) testCasePath := TestCasePath(path)
tc, err := testCasePath.ToTestCase() tc, err := testCasePath.ToTestCase()
if err != nil { if err != nil {
log.Error().Err(err).Str("path", path).Msg("load testcase failed") log.Warn().Err(err).Str("path", path).Msg("load testcase failed")
return errors.Wrap(err, "load testcase failed") return nil
} }
testCases = append(testCases, tc) testCases = append(testCases, tc)
return nil return nil
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "v4.1.0-alpha" __version__ = "v4.1.0-beta"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "v4.1.0-alpha" version = "v4.1.0-beta"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"