diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index 726cc773..08d2ee46 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -6,7 +6,6 @@ on: - master - v2 - v3 - - v4.1-dev pull_request: env: @@ -35,10 +34,10 @@ jobs: - name: Run generated demo tests run: ./output/hrp run demo/testcases/ - 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 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: strategy: @@ -64,11 +63,11 @@ jobs: - name: Run API test demo in examples run: | 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 run: | 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: strategy: @@ -90,9 +89,9 @@ jobs: - name: Run start project run: ./output/hrp startproject demo --ignore-plugin - 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 - 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 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 diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 276deea4..f914858a 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -6,7 +6,6 @@ on: - master - v2 - v3 - - v4.1-dev pull_request: env: diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 793edc30..77e221fc 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -6,7 +6,6 @@ on: - master - v2 - v3 - - v4.1-dev pull_request: env: diff --git a/README.en.md b/README.en.md index 555de35a..ce40fffe 100644 --- a/README.en.md +++ b/README.en.md @@ -12,7 +12,7 @@ > HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。 -![flow chart](docs/assets/hrp-flow.jpg) +![flow chart](https://httprunner.com/image/hrp-flow.jpg) [CHANGELOG] | [中文] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4a06287a..67a35ebd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,15 +1,28 @@ # 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 **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 #1288: unable to go get httprunner v4 -- feat: support converting Postman collection to HttpRunner testcase -- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter` +- fix: panic when config didn't exist in testcase file +- fix: disable keep alive and improve RPS accuracy +- fix: improve RPS accuracy **python version** diff --git a/docs/assets/hrp-flow.jpg b/docs/assets/hrp-flow.jpg index 01a0d050..aa480ede 100644 Binary files a/docs/assets/hrp-flow.jpg and b/docs/assets/hrp-flow.jpg differ diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 37675fcc..2d384e99 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -31,6 +31,7 @@ hrp boom [flags] --max-rps int Max RPS that boomer can generate, disabled by default. --mem-profile string Enable memory profiling. --mem-profile-duration duration Memory profile duration. (default 30s) + --profile string profile for load testing --prometheus-gateway string Prometheus Pushgateway url. --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) diff --git a/examples/demo-with-go-plugin/plugin/go.mod b/examples/demo-with-go-plugin/plugin/go.mod index 8dabb414..08a135d0 100644 --- a/examples/demo-with-go-plugin/plugin/go.mod +++ b/examples/demo-with-go-plugin/plugin/go.mod @@ -2,4 +2,4 @@ module plugin go 1.16 -require github.com/httprunner/funplugin v0.4.5 // indirect +require github.com/httprunner/funplugin v0.4.6 // indirect diff --git a/examples/demo-with-go-plugin/plugin/go.sum b/examples/demo-with-go-plugin/plugin/go.sum index 33c01bfb..59ea6478 100644 --- a/examples/demo-with-go-plugin/plugin/go.sum +++ b/examples/demo-with-go-plugin/plugin/go.sum @@ -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/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/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos= -github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= +github.com/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= diff --git a/examples/demo-with-go-plugin/testcases/demo_with_funplugin.json b/examples/demo-with-go-plugin/testcases/demo.json similarity index 100% rename from examples/demo-with-go-plugin/testcases/demo_with_funplugin.json rename to examples/demo-with-go-plugin/testcases/demo.json diff --git a/examples/demo-with-go-plugin/testcases/demo_ref_testcase.yml b/examples/demo-with-go-plugin/testcases/ref_testcase.yml similarity index 95% rename from examples/demo-with-go-plugin/testcases/demo_ref_testcase.yml rename to examples/demo-with-go-plugin/testcases/ref_testcase.yml index 0743488e..6cf32323 100644 --- a/examples/demo-with-go-plugin/testcases/demo_ref_testcase.yml +++ b/examples/demo-with-go-plugin/testcases/ref_testcase.yml @@ -13,7 +13,7 @@ teststeps: variables: foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1 - testcase: testcases/demo_requests.yml + testcase: testcases/requests.yml export: - foo3 - diff --git a/examples/demo-with-go-plugin/testcases/requests.json b/examples/demo-with-go-plugin/testcases/requests.json new file mode 100644 index 00000000..b13f3837 --- /dev/null +++ b/examples/demo-with-go-plugin/testcases/requests.json @@ -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" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/demo-with-go-plugin/testcases/demo_requests.yml b/examples/demo-with-go-plugin/testcases/requests.yml similarity index 100% rename from examples/demo-with-go-plugin/testcases/demo_requests.yml rename to examples/demo-with-go-plugin/testcases/requests.yml diff --git a/examples/demo-with-py-plugin/testcases/demo_with_funplugin.json b/examples/demo-with-py-plugin/testcases/demo.json similarity index 100% rename from examples/demo-with-py-plugin/testcases/demo_with_funplugin.json rename to examples/demo-with-py-plugin/testcases/demo.json diff --git a/examples/demo-with-py-plugin/testcases/demo_ref_testcase.yml b/examples/demo-with-py-plugin/testcases/ref_testcase.yml similarity index 95% rename from examples/demo-with-py-plugin/testcases/demo_ref_testcase.yml rename to examples/demo-with-py-plugin/testcases/ref_testcase.yml index 0743488e..6cf32323 100644 --- a/examples/demo-with-py-plugin/testcases/demo_ref_testcase.yml +++ b/examples/demo-with-py-plugin/testcases/ref_testcase.yml @@ -13,7 +13,7 @@ teststeps: variables: foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1 - testcase: testcases/demo_requests.yml + testcase: testcases/requests.yml export: - foo3 - diff --git a/examples/demo-with-py-plugin/testcases/requests.json b/examples/demo-with-py-plugin/testcases/requests.json new file mode 100644 index 00000000..b13f3837 --- /dev/null +++ b/examples/demo-with-py-plugin/testcases/requests.json @@ -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" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/demo-with-py-plugin/testcases/demo_requests.yml b/examples/demo-with-py-plugin/testcases/requests.yml similarity index 100% rename from examples/demo-with-py-plugin/testcases/demo_requests.yml rename to examples/demo-with-py-plugin/testcases/requests.yml diff --git a/examples/demo-without-plugin/testcases/demo_without_funplugin.json b/examples/demo-without-plugin/testcases/requests.json similarity index 100% rename from examples/demo-without-plugin/testcases/demo_without_funplugin.json rename to examples/demo-without-plugin/testcases/requests.json diff --git a/examples/hrp/parameters_test.json b/examples/hrp/parameters_test.json index 84c36531..9944599e 100644 --- a/examples/hrp/parameters_test.json +++ b/examples/hrp/parameters_test.json @@ -10,8 +10,14 @@ }, "parameters_setting": { "strategies": { - "user_agent": "sequential", - "username-password": "random" + "user_agent": { + "name": "user-identity", + "pick_order": "sequential" + }, + "username-password": { + "name": "user-info", + "pick_order": "random" + } }, "limit": 6 }, diff --git a/examples/hrp/parameters_test.yaml b/examples/hrp/parameters_test.yaml index b5d06c71..aaaf9b41 100644 --- a/examples/hrp/parameters_test.yaml +++ b/examples/hrp/parameters_test.yaml @@ -5,8 +5,12 @@ config: username-password: ${parameterize($file)} parameters_setting: strategies: - user_agent: "sequential" - username-password: "random" + user_agent: + name: "user-identity" + pick_order: "sequential" + username-password: + name: "user-info" + pick_order: "random" limit: 6 variables: app_version: v1 diff --git a/go.mod b/go.mod index ddf4db03..3c1ab7a1 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-openapi/spec v0.20.6 github.com/google/uuid v1.3.0 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/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 diff --git a/go.sum b/go.sum index 26432fc7..2a44d6cd 100644 --- a/go.sum +++ b/go.sum @@ -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/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/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos= -github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= +github.com/httprunner/funplugin v0.4.6 h1:wwpjzo3G9a5BCXBkHs845w4ifKaCtVa/yQjREQjQOgo= +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-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= diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index 22d8782d..5ae038e1 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -1,12 +1,15 @@ package cmd import ( + "os" "time" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/boomer" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) // boomCmd represents the boom command @@ -28,57 +31,70 @@ var boomCmd = &cobra.Command{ path := hrp.TestCasePath(arg) paths = append(paths, &path) } - hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate) - hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate) - if loopCount > 0 { - hrpBoomer.SetLoopCount(loopCount) + // if set profile, the priority is higher than the other commands + if boomArgs.profile != "" { + err := builtin.LoadFile(boomArgs.profile, &boomArgs) + 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()) } - if prometheusPushgatewayURL != "" { - hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp", hrpBoomer.GetMode())) + if boomArgs.PrometheusPushgatewayURL != "" { + hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(boomArgs.PrometheusPushgatewayURL, "hrp", hrpBoomer.GetMode())) } - hrpBoomer.SetDisableKeepAlive(disableKeepalive) - hrpBoomer.SetDisableCompression(disableCompression) + hrpBoomer.SetDisableKeepAlive(boomArgs.DisableKeepalive) + hrpBoomer.SetDisableCompression(boomArgs.DisableCompression) hrpBoomer.SetClientTransport() - hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) - hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) + hrpBoomer.EnableCPUProfile(boomArgs.CPUProfile, boomArgs.CPUProfileDuration) + hrpBoomer.EnableMemoryProfile(boomArgs.MemoryProfile, boomArgs.MemoryProfileDuration) hrpBoomer.EnableGracefulQuit() hrpBoomer.Run(paths...) }, } -var ( - spawnCount int - spawnRate float64 - maxRPS int64 - loopCount int64 - requestIncreaseRate string - memoryProfile string - memoryProfileDuration time.Duration - cpuProfile string - cpuProfileDuration time.Duration - prometheusPushgatewayURL string - disableConsoleOutput bool - disableCompression bool - disableKeepalive bool -) +type BoomArgs struct { + SpawnCount int `json:"spawn-count,omitempty" yaml:"spawn-count,omitempty"` + SpawnRate float64 `json:"spawn-rate,omitempty" yaml:"spawn-rate,omitempty"` + MaxRPS int64 `json:"max-rps,omitempty" yaml:"max-rps,omitempty"` + LoopCount int64 `json:"loop-count,omitempty" yaml:"loop-count,omitempty"` + RequestIncreaseRate string `json:"request-increase-rate,omitempty" yaml:"request-increase-rate,omitempty"` + MemoryProfile string `json:"memory-profile,omitempty" yaml:"memory-profile,omitempty"` + MemoryProfileDuration time.Duration `json:"memory-profile-duration" yaml:"memory-profile-duration"` + CPUProfile string `json:"cpu-profile,omitempty" yaml:"cpu-profile,omitempty"` + CPUProfileDuration time.Duration `json:"cpu-profile-duration,omitempty" yaml:"cpu-profile-duration,omitempty"` + PrometheusPushgatewayURL string `json:"prometheus-gateway,omitempty" yaml:"prometheus-gateway,omitempty"` + DisableConsoleOutput bool `json:"disable-console-output,omitempty" yaml:"disable-console-output,omitempty"` + DisableCompression bool `json:"disable-compression,omitempty" yaml:"disable-compression,omitempty"` + DisableKeepalive bool `json:"disable-keepalive,omitempty" yaml:"disable-keepalive,omitempty"` + profile string +} + +var boomArgs BoomArgs func init() { rootCmd.AddCommand(boomCmd) - boomCmd.Flags().Int64Var(&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().IntVar(&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().Int64Var(&loopCount, "loop-count", -1, "The specify running cycles for load testing") - boomCmd.Flags().StringVar(&memoryProfile, "mem-profile", "", "Enable memory profiling.") - boomCmd.Flags().DurationVar(&memoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.") - boomCmd.Flags().StringVar(&cpuProfile, "cpu-profile", "", "Enable CPU profiling.") - boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.") - boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.") - 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") + boomCmd.Flags().Int64Var(&boomArgs.MaxRPS, "max-rps", 0, "Max RPS that boomer can generate, disabled by default.") + boomCmd.Flags().StringVar(&boomArgs.RequestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.") + boomCmd.Flags().IntVar(&boomArgs.SpawnCount, "spawn-count", 1, "The number of users to spawn for load testing") + boomCmd.Flags().Float64Var(&boomArgs.SpawnRate, "spawn-rate", 1, "The rate for spawning users") + boomCmd.Flags().Int64Var(&boomArgs.LoopCount, "loop-count", -1, "The specify running cycles for load testing") + boomCmd.Flags().StringVar(&boomArgs.MemoryProfile, "mem-profile", "", "Enable memory profiling.") + boomCmd.Flags().DurationVar(&boomArgs.MemoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.") + boomCmd.Flags().StringVar(&boomArgs.CPUProfile, "cpu-profile", "", "Enable CPU profiling.") + boomCmd.Flags().DurationVar(&boomArgs.CPUProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.") + boomCmd.Flags().StringVar(&boomArgs.PrometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.") + boomCmd.Flags().BoolVar(&boomArgs.DisableConsoleOutput, "disable-console-output", false, "Disable console output.") + boomCmd.Flags().BoolVar(&boomArgs.DisableCompression, "disable-compression", false, "Disable compression") + boomCmd.Flags().BoolVar(&boomArgs.DisableKeepalive, "disable-keepalive", false, "Disable keepalive") + boomCmd.Flags().StringVar(&boomArgs.profile, "profile", "", "profile for load testing") } diff --git a/hrp/cmd/scaffold.go b/hrp/cmd/scaffold.go index f3441b82..93a8e4b8 100644 --- a/hrp/cmd/scaffold.go +++ b/hrp/cmd/scaffold.go @@ -11,9 +11,10 @@ import ( ) var scaffoldCmd = &cobra.Command{ - Use: "startproject $project_name", - Short: "create a scaffold project", - Args: cobra.ExactValidArgs(1), + Use: "startproject $project_name", + Aliases: []string{"scaffold"}, + Short: "create a scaffold project", + Args: cobra.ExactValidArgs(1), PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, diff --git a/hrp/cmd/wiki.go b/hrp/cmd/wiki.go new file mode 100644 index 00000000..7774a740 --- /dev/null +++ b/hrp/cmd/wiki.go @@ -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) +} diff --git a/hrp/internal/httpstat/main.go b/hrp/internal/httpstat/main.go index 29bf464d..49242193 100644 --- a/hrp/internal/httpstat/main.go +++ b/hrp/internal/httpstat/main.go @@ -39,11 +39,11 @@ const ( ) 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 { - 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 { @@ -137,7 +137,7 @@ func (s *Stat) Print() { if s.network != "" && s.addr != "" { printf("\n%s %s: %s\n", color.CyanString("Connected to"), - color.YellowString(s.network), + color.MagentaString(s.network), color.BlueString(s.addr), ) } diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index c4824f8b..a0fe5efa 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "time" "github.com/httprunner/funplugin/shared" "github.com/pkg/errors" @@ -13,6 +14,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/internal/version" ) type PluginType string @@ -23,6 +25,13 @@ const ( 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/* var templatesDir embed.FS @@ -68,6 +77,12 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error os.RemoveAll(projectName) } + // get project abs path + projectPath, err := filepath.Abs(projectName) + if err != nil { + projectPath = projectName + } + // create project folders if err := builtin.CreateFolder(projectName); err != nil { return err @@ -88,8 +103,21 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error 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 - err := CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore")) + err = CopyFile("templates/gitignore", filepath.Join(projectName, ".gitignore")) if err != nil { return err } @@ -102,7 +130,7 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error // create demo testcases if pluginType == Ignore { 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 { return err } @@ -111,17 +139,22 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error } 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 { return err } err = CopyFile("templates/testcases/demo_requests.yml", - filepath.Join(projectName, "testcases", "demo_requests.yml")) + filepath.Join(projectName, "testcases", "requests.yml")) if err != nil { return err } 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 { return err } diff --git a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml index 0743488e..6cf32323 100644 --- a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml +++ b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase.yml @@ -13,7 +13,7 @@ teststeps: variables: foo1: testcase_ref_bar1 expect_foo1: testcase_ref_bar1 - testcase: testcases/demo_requests.yml + testcase: testcases/requests.yml export: - foo3 - diff --git a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py index 714030cd..ce77286e 100644 --- a/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py +++ b/hrp/internal/scaffold/templates/testcases/demo_ref_testcase_test.py @@ -1,5 +1,5 @@ # NOTE: Generated By HttpRunner v4.0.0 -# FROM: testcases/demo_ref_testcase.yml +# FROM: testcases/ref_testcase.yml import sys diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests.json b/hrp/internal/scaffold/templates/testcases/demo_requests.json new file mode 100644 index 00000000..b13f3837 --- /dev/null +++ b/hrp/internal/scaffold/templates/testcases/demo_requests.json @@ -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" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/hrp/internal/scaffold/templates/testcases/demo_requests_test.py b/hrp/internal/scaffold/templates/testcases/demo_requests_test.py index fc2ad5bb..6e05feb1 100644 --- a/hrp/internal/scaffold/templates/testcases/demo_requests_test.py +++ b/hrp/internal/scaffold/templates/testcases/demo_requests_test.py @@ -1,8 +1,8 @@ # 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): diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 3a670224..9453e976 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.1.0-alpha \ No newline at end of file +v4.1.0-beta \ No newline at end of file diff --git a/hrp/internal/wiki/main.go b/hrp/internal/wiki/main.go new file mode 100644 index 00000000..0c4cdb44 --- /dev/null +++ b/hrp/internal/wiki/main.go @@ -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() +} diff --git a/hrp/internal/wiki/open_darwin.go b/hrp/internal/wiki/open_darwin.go new file mode 100644 index 00000000..a8856c1f --- /dev/null +++ b/hrp/internal/wiki/open_darwin.go @@ -0,0 +1,3 @@ +package wiki + +const openCmd = "open" diff --git a/hrp/internal/wiki/open_linux.go b/hrp/internal/wiki/open_linux.go new file mode 100644 index 00000000..d20152c4 --- /dev/null +++ b/hrp/internal/wiki/open_linux.go @@ -0,0 +1,3 @@ +package wiki + +const openCmd = "xdg-open" diff --git a/hrp/internal/wiki/open_windows.go b/hrp/internal/wiki/open_windows.go new file mode 100644 index 00000000..981253da --- /dev/null +++ b/hrp/internal/wiki/open_windows.go @@ -0,0 +1,3 @@ +package wiki + +const openCmd = "explorer" diff --git a/hrp/parameters.go b/hrp/parameters.go index 5797f618..376232fa 100644 --- a/hrp/parameters.go +++ b/hrp/parameters.go @@ -12,17 +12,17 @@ import ( ) 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 Limit int `json:"limit,omitempty" yaml:"limit,omitempty"` } -type iteratorStrategy string +type iteratorPickOrder string const ( - strategySequential iteratorStrategy = "sequential" - strategyRandom iteratorStrategy = "random" - strategyUnique iteratorStrategy = "unique" + pickOrderSequential iteratorPickOrder = "sequential" + pickOrderRandom iteratorPickOrder = "random" + pickOrderUnique iteratorPickOrder = "unique" ) /* @@ -33,6 +33,11 @@ const ( */ 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) { parameters, err := loadParameters(cfg.Parameters, cfg.Variables) if err != nil { @@ -62,15 +67,15 @@ func newParametersIterator(parameters map[string]Parameters, config *TParamsConf parametersList := make([]Parameters, 0) for paramName := range parameters { - // check parameter individual strategy + // check parameter individual pick order strategy strategy, ok := config.Strategies[paramName] if !ok { - // default to overall strategy - strategy = config.Strategy + // default to overall pick order strategy + strategy.PickOrder = config.PickOrder } - // group parameters by strategy - if strategy == strategyRandom { + // group parameters by pick order strategy + if strategy.PickOrder == pickOrderRandom { iterator.randomParameterNames = append(iterator.randomParameterNames, paramName) } else { parametersList = append(parametersList, parameters[paramName]) diff --git a/hrp/parameters_test.go b/hrp/parameters_test.go index 19bb9fae..02176415 100644 --- a/hrp/parameters_test.go +++ b/hrp/parameters_test.go @@ -137,25 +137,25 @@ func TestInitParametersIteratorCount(t *testing.T) { }, 6, // 3 * 2 * 1 }, - // default equals to set overall parameters strategy to "sequential" + // default equals to set overall parameters pick-order to "sequential" { &TConfig{ Parameters: configParameters, ParametersSetting: &TParamsConfig{ - Strategy: "sequential", + PickOrder: "sequential", }, }, 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{ Parameters: configParameters, ParametersSetting: &TParamsConfig{ Strategies: map[string]iteratorStrategy{ - "username-password": "sequential", - "user_agent": "sequential", - "app_version": "sequential", + "username-password": {Name: "user-info", PickOrder: "sequential"}, + "user_agent": {Name: "user-identity", PickOrder: "sequential"}, + "app_version": {Name: "app-version", PickOrder: "sequential"}, }, }, }, @@ -166,33 +166,33 @@ func TestInitParametersIteratorCount(t *testing.T) { Parameters: configParameters, ParametersSetting: &TParamsConfig{ Strategies: map[string]iteratorStrategy{ - "user_agent": "sequential", - "app_version": "sequential", + "user_agent": {Name: "user-identity", PickOrder: "sequential"}, + "app_version": {Name: "app-version", PickOrder: "sequential"}, }, }, }, 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 { &TConfig{ Parameters: configParameters, ParametersSetting: &TParamsConfig{ - Strategy: "random", + PickOrder: "random", }, }, 1, // 1 * 1 * 1 }, - // set some individual parameters strategy to "random" + // set some individual parameters pick-order to "random" // this will override overall strategy { &TConfig{ Parameters: configParameters, ParametersSetting: &TParamsConfig{ 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, ParametersSetting: &TParamsConfig{ 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{ Limit: 5, // limit could also be greater than total Strategies: map[string]iteratorStrategy{ - "username-password": "random", + "username-password": {Name: "user-info", PickOrder: "random"}, }, }, }, diff --git a/hrp/plugin.go b/hrp/plugin.go index 7929d030..67a86cfe 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -78,11 +78,11 @@ func locatePlugin(path string) (pluginPath string, err error) { return } + log.Warn().Err(err).Str("path", path).Msg("plugin file not found") return "", fmt.Errorf("plugin file not found") } -// locateFile searches destFile upward recursively until current -// working directory or system root dir. +// locateFile searches destFile upward recursively until system root dir func locateFile(startPath string, destFile string) (string, error) { stat, err := os.Stat(startPath) if os.IsNotExist(err) { @@ -103,12 +103,6 @@ func locateFile(startPath string, destFile string) (string, error) { 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 parentDir, _ := filepath.Abs(filepath.Dir(startDir)) if parentDir == startDir { @@ -118,7 +112,7 @@ func locateFile(startPath string, destFile string) (string, error) { return locateFile(parentDir, destFile) } -func getProjectRootDirPath(path string) (rootDir string, err error) { +func GetProjectRootDirPath(path string) (rootDir string, err error) { pluginPath, err := locatePlugin(path) if err == nil { rootDir = filepath.Dir(pluginPath) diff --git a/hrp/runner_test.go b/hrp/runner_test.go index 7ba1f4c2..d03f8d75 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -207,7 +207,7 @@ func TestLoadTestCases(t *testing.T) { if !assert.Nil(t, err) { t.Fatal() } - if !assert.Equal(t, len(testCases), 3) { + if !assert.Equal(t, 4, len(testCases)) { t.Fatal() } @@ -217,7 +217,7 @@ func TestLoadTestCases(t *testing.T) { if !assert.Nil(t, err) { t.Fatal() } - if !assert.Equal(t, len(testCases), 3) { + if !assert.Equal(t, len(testCases), 4) { t.Fatal() } diff --git a/hrp/testcase.go b/hrp/testcase.go index d2c2bd80..41826716 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -75,7 +75,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) { } // locate project root dir by plugin path - projectRootDir, err := getProjectRootDirPath(casePath) + projectRootDir, err := GetProjectRootDirPath(casePath) if err != nil { return nil, errors.Wrap(err, "failed to get project root dir") } @@ -361,8 +361,8 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { testCasePath := TestCasePath(path) tc, err := testCasePath.ToTestCase() if err != nil { - log.Error().Err(err).Str("path", path).Msg("load testcase failed") - return errors.Wrap(err, "load testcase failed") + log.Warn().Err(err).Str("path", path).Msg("load testcase failed") + return nil } testCases = append(testCases, tc) return nil diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 7adbd971..704aa8d5 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.1.0-alpha" +__version__ = "v4.1.0-beta" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 408206d0..0be11d44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.1.0-alpha" +version = "v4.1.0-beta" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md"