mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-12 16:01:27 +08:00
Merge pull request #4 from billduan/feat_allure
增加allure的内容,1.allure附加log文件 2.allure添加请求和返回详情
This commit is contained in:
22
.github/workflows/hrp-scaffold.yml
vendored
22
.github/workflows/hrp-scaffold.yml
vendored
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- v2
|
||||||
|
- v3
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -32,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:
|
||||||
@@ -56,16 +58,18 @@ jobs:
|
|||||||
run: make build
|
run: make build
|
||||||
- name: Run start project
|
- name: Run start project
|
||||||
run: ./output/hrp startproject demo --go
|
run: ./output/hrp startproject demo --go
|
||||||
|
- name: Build plugin
|
||||||
|
run: ./output/hrp build -o demo/debugtalk.bin demo/plugin/debugtalk.go
|
||||||
- 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: |
|
run: |
|
||||||
go build -o examples/demo-with-go-plugin/debugtalk.bin examples/demo-with-go-plugin/plugin/debugtalk.go
|
./output/hrp 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
|
./output/hrp 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:
|
||||||
@@ -87,9 +91,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
|
||||||
|
|||||||
2
.github/workflows/smoketest.yml
vendored
2
.github/workflows/smoketest.yml
vendored
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- v2
|
||||||
|
- v3
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/unittest.yml
vendored
2
.github/workflows/unittest.yml
vendored
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- v2
|
||||||
|
- v3
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -18,6 +18,11 @@ build: ## build hrp cli tool
|
|||||||
@echo "[info] build hrp cli tool"
|
@echo "[info] build hrp cli tool"
|
||||||
@. scripts/build.sh
|
@. scripts/build.sh
|
||||||
|
|
||||||
|
.PHONY: install-hooks
|
||||||
|
install-hooks: ## install git hooks
|
||||||
|
@find scripts -name "install-*-hook" | awk -F'-' '{s=$$2;for(i=3;i<NF;i++){s=s"-"$$i;}print s;}' | while read f; do bash "scripts/install-$$f-hook"; done
|
||||||
|
@echo "[OK] install all hooks"
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## print make commands
|
help: ## print make commands
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
|
|||||||
11
README.en.md
11
README.en.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
|
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[CHANGELOG] | [中文]
|
[CHANGELOG] | [中文]
|
||||||
|
|
||||||
@@ -104,13 +104,18 @@ Flags:
|
|||||||
Use "hrp [command] --help" for more information about a command.
|
Use "hrp [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User Cases
|
||||||
|
|
||||||
|
<a href="https://httprunner.com/docs/cases/dji-ibg"><img src="https://httprunner.com/image/logo/dji.jpeg" title="大疆 - 基于 HttpRunner 构建完整的自动化测试体系" width="60"></a>
|
||||||
|
<a href="https://httprunner.com/docs/cases/bytedance-feishu"><img src="https://httprunner.com/image/logo/feishu.jpeg" title="飞书 - 使用 HttpRunner 替换已有测试平台的执行引擎" width="60"></a>
|
||||||
|
|
||||||
## Subscribe
|
## Subscribe
|
||||||
|
|
||||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||||
|
|
||||||
<img src="docs/assets/qrcode.jpg" alt="HttpRunner" width="200">
|
<img src="https://httprunner.com/image/qrcode.png" alt="HttpRunner" width="400">
|
||||||
|
|
||||||
如果你期望加入 HttpRunner 核心用户群,请填写[用户调研问卷][survey]并留下你的联系方式,作者将拉你进群。
|
如果你期望加入 HttpRunner 用户群,请看这里:[HttpRunner v4 用户交流群,它来啦!](https://httprunner.com/blog/join-chat-group)
|
||||||
|
|
||||||
[HttpRunner]: https://github.com/httprunner/httprunner
|
[HttpRunner]: https://github.com/httprunner/httprunner
|
||||||
[boomer]: https://github.com/myzhan/boomer
|
[boomer]: https://github.com/myzhan/boomer
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
|
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[版本发布日志] | [English]
|
[版本发布日志] | [English]
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
- 网络协议:完整支持 HTTP(S)/1.1 和 HTTP/2,可扩展支持 WebSocket/TCP/RPC 等更多协议
|
- 网络协议:完整支持 HTTP(S)/HTTP2/WebSocket,可扩展支持 TCP/UDP/RPC 等更多协议
|
||||||
- 多格式可选:测试用例支持 YAML/JSON/go test/pytest 格式,并且支持格式互相转换
|
- 多格式可选:测试用例支持 YAML/JSON/go test/pytest 格式,并且支持格式互相转换
|
||||||
- 双执行引擎:同时支持 golang/python 两个执行引擎,兼具 go 的高性能和 [pytest] 的丰富生态
|
- 双执行引擎:同时支持 golang/python 两个执行引擎,兼具 go 的高性能和 [pytest] 的丰富生态
|
||||||
- 录制 & 生成:可使用 [HAR]/Postman/Swagger/curl 等生成测试用例;基于链式调用的方法提示也可快速编写测试用例
|
- 录制 & 生成:可使用 [HAR]/Postman/Swagger/curl 等生成测试用例;基于链式调用的方法提示也可快速编写测试用例
|
||||||
@@ -97,11 +97,16 @@ Flags:
|
|||||||
Use "hrp [command] --help" for more information about a command.
|
Use "hrp [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 用户案例
|
||||||
|
|
||||||
|
<a href="https://httprunner.com/docs/cases/dji-ibg"><img src="https://httprunner.com/image/logo/dji.jpeg" title="大疆 - 基于 HttpRunner 构建完整的自动化测试体系" width="60"></a>
|
||||||
|
<a href="https://httprunner.com/docs/cases/bytedance-feishu"><img src="https://httprunner.com/image/logo/feishu.jpeg" title="飞书 - 使用 HttpRunner 替换已有测试平台的执行引擎" width="60"></a>
|
||||||
|
|
||||||
## 赞助商
|
## 赞助商
|
||||||
|
|
||||||
### 金牌赞助商
|
### 金牌赞助商
|
||||||
|
|
||||||
[<img src="docs/assets/hogwarts.jpeg" alt="霍格沃兹测试开发学社" width="400">](https://ceshiren.com/)
|
[<img src="https://httprunner.com/image/hogwarts.jpeg" alt="霍格沃兹测试开发学社" width="400">](https://ceshiren.com/)
|
||||||
|
|
||||||
> [霍格沃兹测试开发学社](http://qrcode.testing-studio.com/f?from=httprunner&url=https://ceshiren.com)是业界领先的测试开发技术高端教育品牌,隶属于[测吧(北京)科技有限公司](http://qrcode.testing-studio.com/f?from=httprunner&url=https://www.testing-studio.com) 。学院课程由一线大厂测试经理与资深测试开发专家参与研发,实战驱动。课程涵盖 web/app 自动化测试、接口测试、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移&右移、精准测试、测试平台开发、测试管理等内容,帮助测试工程师实现测试开发技术转型。通过优秀的学社制度(奖学金、内推返学费、行业竞赛等多种方式)来实现学员、学社及用人企业的三方共赢。
|
> [霍格沃兹测试开发学社](http://qrcode.testing-studio.com/f?from=httprunner&url=https://ceshiren.com)是业界领先的测试开发技术高端教育品牌,隶属于[测吧(北京)科技有限公司](http://qrcode.testing-studio.com/f?from=httprunner&url=https://www.testing-studio.com) 。学院课程由一线大厂测试经理与资深测试开发专家参与研发,实战驱动。课程涵盖 web/app 自动化测试、接口测试、性能测试、安全测试、持续集成/持续交付/DevOps,测试左移&右移、精准测试、测试平台开发、测试管理等内容,帮助测试工程师实现测试开发技术转型。通过优秀的学社制度(奖学金、内推返学费、行业竞赛等多种方式)来实现学员、学社及用人企业的三方共赢。
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ Use "hrp [command] --help" for more information about a command.
|
|||||||
|
|
||||||
### 开源服务赞助商
|
### 开源服务赞助商
|
||||||
|
|
||||||
[<img src="docs/assets/sentry-logo-black.svg" alt="Sentry" width="150">](https://sentry.io/_/open-source/)
|
[<img src="https://httprunner.com/image/sentry-logo-black.svg" alt="Sentry" width="150">](https://sentry.io/_/open-source/)
|
||||||
|
|
||||||
HttpRunner is in Sentry Sponsored plan.
|
HttpRunner is in Sentry Sponsored plan.
|
||||||
|
|
||||||
@@ -117,9 +122,9 @@ HttpRunner is in Sentry Sponsored plan.
|
|||||||
|
|
||||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||||
|
|
||||||
<img src="docs/assets/qrcode.jpg" alt="HttpRunner" width="200">
|
<img src="https://httprunner.com/image/qrcode.png" alt="HttpRunner" width="400">
|
||||||
|
|
||||||
如果你期望加入 HttpRunner 核心用户群,请填写[用户调研问卷][survey]并留下你的联系方式,作者将拉你进群。
|
如果你期望加入 HttpRunner 用户群,请看这里:[HttpRunner v4 用户交流群,它来啦!](https://httprunner.com/blog/join-chat-group)
|
||||||
|
|
||||||
[HttpRunner]: https://github.com/httprunner/httprunner
|
[HttpRunner]: https://github.com/httprunner/httprunner
|
||||||
[boomer]: https://github.com/myzhan/boomer
|
[boomer]: https://github.com/myzhan/boomer
|
||||||
|
|||||||
@@ -1,9 +1,59 @@
|
|||||||
# Release History
|
# Release History
|
||||||
|
|
||||||
## v4.0.0-beta2 (2022-05-03)
|
## v4.1.2 (2022-06-05)
|
||||||
|
|
||||||
|
- fix #1331: use `str_eq` to assert string and digit equality
|
||||||
|
- fix #1336: extract package in Windows
|
||||||
|
- fix: install package on MinGW64
|
||||||
|
- change: remove `hrp har2case`, replace with `hrp convert`
|
||||||
|
|
||||||
|
## v4.1.1 (2022-05-31)
|
||||||
|
|
||||||
|
- fix: failed to build debugtalk.go without go.mod
|
||||||
|
- fix: avoid to escape from html special characters like '&' in converted JSON testcase
|
||||||
|
- fix: display the full step name when referencing testcase in html report
|
||||||
|
- fix: failed to regenerate debugtalk_gen.go and .debugtalk_gen.py correctly
|
||||||
|
|
||||||
|
## v4.1.0 (2022-05-29)
|
||||||
|
|
||||||
|
- feat: add `wiki` sub-command to open httprunner website
|
||||||
|
- feat: add `build` sub-command for function plugin
|
||||||
|
|
||||||
**go version**
|
**go version**
|
||||||
|
|
||||||
|
- feat #1268: convert postman collection to HttpRunner testcase
|
||||||
|
- feat #1291: run testcases in v2/v3 JSON/YAML format with hrp run/boom command
|
||||||
|
- feat #1280: support creating empty scaffold project
|
||||||
|
- fix #1308: load `.env` file as environment variables
|
||||||
|
- fix #1309: locate plugin file upward recursively until system root dir
|
||||||
|
- fix #1315: failed to generate a report in failfast mode
|
||||||
|
- refactor: move base_url to config `environs`
|
||||||
|
- refactor: implement testcase conversions with `hrp convert`
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- fix: panic when config didn't exist in testcase file
|
||||||
|
- fix: disable keep alive and improve RPS accuracy
|
||||||
|
- fix: improve RPS accuracy
|
||||||
|
|
||||||
|
**python version**
|
||||||
|
|
||||||
|
- feat: support new step type with SQL operation
|
||||||
|
- feat: support new step type with thrift protocol
|
||||||
|
|
||||||
|
## v4.0.0 (2022-05-05)
|
||||||
|
|
||||||
|
**go version**
|
||||||
|
|
||||||
|
- feat: stat HTTP request latencies (DNSLookup, TCP Connection and so on)
|
||||||
- feat: add builtin function `environ`/`ENV`
|
- feat: add builtin function `environ`/`ENV`
|
||||||
- fix: demo function compatibility
|
- fix: demo function compatibility
|
||||||
- fix #1240: losing host port in har2case
|
- fix #1240: losing host port in har2case
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 353 KiB |
@@ -30,10 +30,12 @@ Copyright 2017 debugtalk
|
|||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [hrp boom](hrp_boom.md) - run load test with boomer
|
* [hrp boom](hrp_boom.md) - run load test with boomer
|
||||||
* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts
|
* [hrp build](hrp_build.md) - build plugin for testing
|
||||||
|
* [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases
|
||||||
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
|
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
|
||||||
* [hrp pytest](hrp_pytest.md) - run API test with pytest
|
* [hrp pytest](hrp_pytest.md) - run API test with pytest
|
||||||
* [hrp run](hrp_run.md) - run API test with go engine
|
* [hrp run](hrp_run.md) - run API test with go engine
|
||||||
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
||||||
|
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -41,4 +42,4 @@ hrp boom [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
31
docs/cmd/hrp_build.md
Normal file
31
docs/cmd/hrp_build.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## hrp build
|
||||||
|
|
||||||
|
build plugin for testing
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
build python/go plugin for testing
|
||||||
|
|
||||||
|
```
|
||||||
|
hrp build $path ... [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
$ hrp build plugin/debugtalk.go
|
||||||
|
$ hrp build plugin/debugtalk.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for build
|
||||||
|
-o, --output string funplugin product output path, default: cwd
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
## hrp convert
|
## hrp convert
|
||||||
|
|
||||||
convert JSON/YAML testcases to pytest/gotest scripts
|
convert to JSON/YAML/gotest/pytest testcases
|
||||||
|
|
||||||
```
|
```
|
||||||
hrp convert $path... [flags]
|
hrp convert $path... [flags]
|
||||||
@@ -9,13 +9,17 @@ hrp convert $path... [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--gotest convert to gotest scripts (TODO)
|
-h, --help help for convert
|
||||||
-h, --help help for convert
|
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||||
--pytest convert to pytest scripts (default true)
|
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
|
||||||
|
--to-gotest convert to gotest scripts (TODO)
|
||||||
|
--to-json convert to JSON scripts (default)
|
||||||
|
--to-pytest convert to pytest scripts
|
||||||
|
--to-yaml convert to YAML scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ hrp har2case $har_path... [flags]
|
|||||||
-h, --help help for har2case
|
-h, --help help for har2case
|
||||||
-d, --output-dir string specify output directory, default to the same dir with har file
|
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||||
-p, --profile string specify profile path to override headers and cookies
|
-p, --profile string specify profile path to override headers and cookies
|
||||||
-j, --to-json convert to JSON format (default true)
|
-j, --to-json convert to JSON format (default)
|
||||||
-y, --to-yaml convert to YAML format
|
-y, --to-yaml convert to YAML format
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,4 +24,4 @@ hrp har2case $har_path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ hrp run $path... [flags]
|
|||||||
-c, --continue-on-failure continue running next step when failure occurs
|
-c, --continue-on-failure continue running next step when failure occurs
|
||||||
-g, --gen-html-report generate html report
|
-g, --gen-html-report generate html report
|
||||||
-h, --help help for run
|
-h, --help help for run
|
||||||
|
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
|
||||||
--log-plugin turn on plugin logging
|
--log-plugin turn on plugin logging
|
||||||
--log-requests-off turn off request & response details logging
|
--log-requests-off turn off request & response details logging
|
||||||
-p, --proxy-url string set proxy url
|
-p, --proxy-url string set proxy url
|
||||||
@@ -34,4 +35,4 @@ hrp run $path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ hrp startproject $project_name [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
|
--empty generate empty project
|
||||||
-f, --force force to overwrite existing project
|
-f, --force force to overwrite existing project
|
||||||
--go generate hashicorp go plugin
|
--go generate hashicorp go plugin
|
||||||
-h, --help help for startproject
|
-h, --help help for startproject
|
||||||
@@ -20,4 +21,4 @@ hrp startproject $project_name [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 3-May-2022
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
|
|||||||
19
docs/cmd/hrp_wiki.md
Normal file
19
docs/cmd/hrp_wiki.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## hrp wiki
|
||||||
|
|
||||||
|
visit https://httprunner.com
|
||||||
|
|
||||||
|
```
|
||||||
|
hrp wiki [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for wiki
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [hrp](hrp.md) - Next-Generation API Testing Solution.
|
||||||
|
|
||||||
|
###### Auto generated by spf13/cobra on 29-May-2022
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"name": "testcase description"
|
|
||||||
},
|
|
||||||
"teststeps": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "https://postman-echo.com/get",
|
|
||||||
"params": {
|
|
||||||
"foo1": "HDnY8",
|
|
||||||
"foo2": "34.5"
|
|
||||||
},
|
|
||||||
"headers": {
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Host": "postman-echo.com",
|
|
||||||
"User-Agent": "HttpRunnerPlus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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=HDnY8\u0026foo2=34.5",
|
|
||||||
"msg": "assert response body url"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"url": "https://postman-echo.com/post",
|
|
||||||
"headers": {
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Content-Length": "28",
|
|
||||||
"Content-Type": "application/json; charset=UTF-8",
|
|
||||||
"Host": "postman-echo.com",
|
|
||||||
"User-Agent": "Go-http-client/1.1"
|
|
||||||
},
|
|
||||||
"cookies": {
|
|
||||||
"sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"foo1": "HDnY8",
|
|
||||||
"foo2": 12.3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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/post",
|
|
||||||
"msg": "assert response body url"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"url": "https://postman-echo.com/post",
|
|
||||||
"headers": {
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Content-Length": "20",
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
||||||
"Host": "postman-echo.com",
|
|
||||||
"User-Agent": "Go-http-client/1.1"
|
|
||||||
},
|
|
||||||
"cookies": {
|
|
||||||
"sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
|
|
||||||
},
|
|
||||||
"body": "foo1=HDnY8\u0026foo2=12.3"
|
|
||||||
},
|
|
||||||
"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": "",
|
|
||||||
"msg": "assert response body data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"check": "body.url",
|
|
||||||
"assert": "equals",
|
|
||||||
"expect": "https://postman-echo.com/post",
|
|
||||||
"msg": "assert response body url"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
override: true
|
||||||
headers:
|
headers:
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
Content-Type: "application/x-www-form-urlencoded"
|
||||||
cookies:
|
cookies:
|
||||||
488
examples/data/postman/postman_collection.json
Normal file
488
examples/data/postman/postman_collection.json
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "0417a445-b206-4ea2-b1d2-5441afd6c6b9",
|
||||||
|
"name": "postman collection demo",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "folder1",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "folder2",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get with params",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "k1",
|
||||||
|
"value": "v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
"value": "v2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k3",
|
||||||
|
"value": "v3",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "get"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Get with params case1",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "k1",
|
||||||
|
"value": "v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
"value": "v2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k3",
|
||||||
|
"value": "v3",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "get"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Date",
|
||||||
|
"value": "Mon, 16 May 2022 12:12:28 GMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json; charset=utf-8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Length",
|
||||||
|
"value": "508"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Connection",
|
||||||
|
"value": "keep-alive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ETag",
|
||||||
|
"value": "W/\"1fc-x4EIPFQzoLX0HenCFPx6HNfG0lc\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Vary",
|
||||||
|
"value": "Accept-Encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "set-cookie",
|
||||||
|
"value": "sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils; Path=/; HttpOnly"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get with params case2",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path?k1=v1&k3=v3",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "k1",
|
||||||
|
"value": "v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
"value": "v2",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k3",
|
||||||
|
"value": "v3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "get"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Date",
|
||||||
|
"value": "Mon, 16 May 2022 12:14:04 GMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json; charset=utf-8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Length",
|
||||||
|
"value": "504"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Connection",
|
||||||
|
"value": "keep-alive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ETag",
|
||||||
|
"value": "W/\"1f8-tMaKs4xmwr+3su3I8mcgR0p+ucw\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Vary",
|
||||||
|
"value": "Accept-Encoding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "set-cookie",
|
||||||
|
"value": "sails.sid=s%3AMNuX_i0KgaP_KuuMpYB8RtCNipCGJWVw.4ETfPHxE81Omqb6Yli%2FezUU8CXyYBcN3%2Bxkx5htwh8Y; Path=/; HttpOnly"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k3\": \"v3\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k3=v3\"\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "folder3",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Post form-data",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "formdata",
|
||||||
|
"formdata": [
|
||||||
|
{
|
||||||
|
"key": "k1",
|
||||||
|
"value": "v1",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
"value": "v2",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k3",
|
||||||
|
"value": "v3",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "post"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Post x-www-form-urlencoded",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "urlencoded",
|
||||||
|
"urlencoded": [
|
||||||
|
{
|
||||||
|
"key": "k1",
|
||||||
|
"value": "v1",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k2",
|
||||||
|
"value": "v2",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "k3",
|
||||||
|
"value": "v3",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "post"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Post raw json",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "post"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Post raw text",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "have a nice day",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "post"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get request headers",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "User-Agent",
|
||||||
|
"value": "HttpRunner",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "User-Name",
|
||||||
|
"value": "bbx",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Connection",
|
||||||
|
"value": "close",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "headers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Get request headers case1",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "User-Agent",
|
||||||
|
"value": "HttpRunner",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "User-Name",
|
||||||
|
"value": "bbx",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Cookie",
|
||||||
|
"value": "Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "https://postman-echo.com/:path",
|
||||||
|
"protocol": "https",
|
||||||
|
"host": [
|
||||||
|
"postman-echo",
|
||||||
|
"com"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
":path"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"value": "headers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Date",
|
||||||
|
"value": "Mon, 16 May 2022 12:14:25 GMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json; charset=utf-8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Length",
|
||||||
|
"value": "541"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Connection",
|
||||||
|
"value": "keep-alive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ETag",
|
||||||
|
"value": "W/\"21d-ld5UvFTaRM6lihVnvCj6mZm5Of0\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Vary",
|
||||||
|
"value": "Accept-Encoding"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"HttpRunner\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n }\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
examples/data/postman/profile.yml
Normal file
4
examples/data/postman/profile.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
headers:
|
||||||
|
User-Agent: "this header will be created or updated"
|
||||||
|
cookies:
|
||||||
|
Cookie1: "this cookie will be created or updated"
|
||||||
5
examples/data/postman/profile_override.yml
Normal file
5
examples/data/postman/profile_override.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
override: true
|
||||||
|
headers:
|
||||||
|
Header1: "all original headers will be overridden"
|
||||||
|
cookies:
|
||||||
|
Cookie1: "all original cookies will be overridden"
|
||||||
3
examples/demo-empty-project/.env
Normal file
3
examples/demo-empty-project/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
base_url=https://postman-echo.com
|
||||||
|
USERNAME=debugtalk
|
||||||
|
PASSWORD=123456
|
||||||
14
examples/demo-empty-project/.gitignore
vendored
Normal file
14
examples/demo-empty-project/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
reports/
|
||||||
|
*.so
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
output/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.python-version
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# plugin
|
||||||
|
debugtalk.bin
|
||||||
|
debugtalk.so
|
||||||
0
examples/demo-empty-project/har/.keep
Normal file
0
examples/demo-empty-project/har/.keep
Normal file
5
examples/demo-empty-project/proj.json
Normal file
5
examples/demo-empty-project/proj.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"project_name": "demo-empty-project",
|
||||||
|
"create_time": "2022-05-31T15:05:51.196187+08:00",
|
||||||
|
"hrp_version": "v4.1.1"
|
||||||
|
}
|
||||||
25
examples/demo-empty-project/testcases/requests.json
Normal file
25
examples/demo-empty-project/testcases/requests.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "request methods testcase: empty testcase",
|
||||||
|
"variables": null,
|
||||||
|
"verify": false
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"variables": null,
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "https://"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
examples/demo-with-go-plugin/.env
Normal file
3
examples/demo-with-go-plugin/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
base_url=https://postman-echo.com
|
||||||
|
USERNAME=debugtalk
|
||||||
|
PASSWORD=123456
|
||||||
1
examples/demo-with-go-plugin/.gitignore
vendored
1
examples/demo-with-go-plugin/.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
.env
|
|
||||||
reports/
|
reports/
|
||||||
*.so
|
*.so
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/fungo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SumTwoInt(a, b int) int {
|
func SumTwoInt(a, b int) int {
|
||||||
@@ -41,17 +39,6 @@ func TeardownHookExample(args string) string {
|
|||||||
return fmt.Sprintf("step name: %v, teardown...", args)
|
return fmt.Sprintf("step name: %v, teardown...", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVersion() string {
|
func GetUserAgent() string {
|
||||||
return fungo.Version
|
return "hrp/fungo"
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fungo.Register("get_version", GetVersion)
|
|
||||||
fungo.Register("sum_ints", SumInts)
|
|
||||||
fungo.Register("sum_two_int", SumTwoInt)
|
|
||||||
fungo.Register("sum_two", SumTwoInt)
|
|
||||||
fungo.Register("sum", Sum)
|
|
||||||
fungo.Register("setup_hook_example", SetupHookExample)
|
|
||||||
fungo.Register("teardown_hook_example", TeardownHookExample)
|
|
||||||
fungo.Serve()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module plugin
|
|
||||||
|
|
||||||
go 1.16
|
|
||||||
|
|
||||||
require github.com/httprunner/funplugin v0.4.3 // indirect
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
|
||||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
|
||||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
|
||||||
github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw=
|
|
||||||
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
|
||||||
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
|
|
||||||
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.3 h1:mxdxQh54NZLQnK/FXZxpZV0rhqZQzckrWKEnBW5w2Vg=
|
|
||||||
github.com/httprunner/funplugin v0.4.3/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=
|
|
||||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
|
||||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
|
||||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
|
|
||||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
|
||||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
||||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
|
||||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
|
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE=
|
|
||||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
|
||||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
|
||||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
|
||||||
google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
|
|
||||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
5
examples/demo-with-go-plugin/proj.json
Normal file
5
examples/demo-with-go-plugin/proj.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"project_name": "demo-with-go-plugin",
|
||||||
|
"create_time": "2022-05-31T15:05:49.894029+08:00",
|
||||||
|
"hrp_version": "v4.1.1"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
-
|
-
|
||||||
@@ -24,10 +24,10 @@ teststeps:
|
|||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: /post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
User-Agent: ${get_user_agent()}
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
Content-Type: "application/x-www-form-urlencoded"
|
||||||
data: "foo1=$foo1&foo2=$foo3"
|
body: "foo1=$foo1&foo2=$foo3"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.form.foo1", "bar1"]
|
- eq: ["body.form.foo1", "bar1"]
|
||||||
- eq: ["body.form.foo2", "bar21"]
|
- eq: ["body.form.foo2", "bar21"]
|
||||||
136
examples/demo-with-go-plugin/testcases/requests.json
Normal file
136
examples/demo-with-go-plugin/testcases/requests.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "request methods testcase with functions",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "config_bar1",
|
||||||
|
"foo2": "config_bar2",
|
||||||
|
"expect_foo1": "config_bar1",
|
||||||
|
"expect_foo2": "config_bar2"
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "${get_user_agent()}"
|
||||||
|
},
|
||||||
|
"base_url": "https://postman-echo.com",
|
||||||
|
"verify": false,
|
||||||
|
"export": [
|
||||||
|
"foo3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "get with params",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "${ENV(USERNAME)}",
|
||||||
|
"foo2": "bar21",
|
||||||
|
"sum_v": "${sum_two_int(1, 2)}"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/get",
|
||||||
|
"params": {
|
||||||
|
"foo1": "$foo1",
|
||||||
|
"foo2": "$foo2",
|
||||||
|
"sum_v": "$sum_v"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract": {
|
||||||
|
"foo3": "body.args.foo2"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.foo1",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "debugtalk",
|
||||||
|
"msg": "check body.args.foo1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.sum_v",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "3",
|
||||||
|
"msg": "check body.args.sum_v"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.foo2",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar21",
|
||||||
|
"msg": "check body.args.foo2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post raw text",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "bar12",
|
||||||
|
"foo3": "bar32"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
},
|
||||||
|
"body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.data",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
|
||||||
|
"msg": "check body.data"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post form data",
|
||||||
|
"variables": {
|
||||||
|
"foo2": "bar23"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
"body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo1",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "$expect_foo1",
|
||||||
|
"msg": "check body.form.foo1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo2",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar23",
|
||||||
|
"msg": "check body.form.foo2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo3",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar21",
|
||||||
|
"msg": "check body.form.foo3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ config:
|
|||||||
foo2: config_bar2
|
foo2: config_bar2
|
||||||
expect_foo1: config_bar1
|
expect_foo1: config_bar1
|
||||||
expect_foo2: config_bar2
|
expect_foo2: config_bar2
|
||||||
base_url: "https://postman-echo.com"
|
headers:
|
||||||
|
User-Agent: ${get_user_agent()}
|
||||||
verify: False
|
verify: False
|
||||||
export: ["foo3"]
|
export: ["foo3"]
|
||||||
|
|
||||||
@@ -13,23 +14,21 @@ teststeps:
|
|||||||
-
|
-
|
||||||
name: get with params
|
name: get with params
|
||||||
variables:
|
variables:
|
||||||
foo1: bar11
|
foo1: ${ENV(USERNAME)}
|
||||||
foo2: bar21
|
foo2: bar21
|
||||||
sum_v: "${sum_two_int(1, 2)}"
|
sum_v: "${sum_two_int(1, 2)}"
|
||||||
request:
|
request:
|
||||||
method: GET
|
method: GET
|
||||||
url: /get
|
url: $base_url/get
|
||||||
params:
|
params:
|
||||||
foo1: $foo1
|
foo1: $foo1
|
||||||
foo2: $foo2
|
foo2: $foo2
|
||||||
sum_v: $sum_v
|
sum_v: $sum_v
|
||||||
headers:
|
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
extract:
|
extract:
|
||||||
foo3: "body.args.foo2"
|
foo3: "body.args.foo2"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.args.foo1", "bar11"]
|
- eq: ["body.args.foo1", "debugtalk"]
|
||||||
- eq: ["body.args.sum_v", "3"]
|
- eq: ["body.args.sum_v", "3"]
|
||||||
- eq: ["body.args.foo2", "bar21"]
|
- eq: ["body.args.foo2", "bar21"]
|
||||||
-
|
-
|
||||||
@@ -39,11 +38,10 @@ teststeps:
|
|||||||
foo3: "bar32"
|
foo3: "bar32"
|
||||||
request:
|
request:
|
||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: $base_url/post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
Content-Type: "text/plain"
|
Content-Type: "text/plain"
|
||||||
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
|
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
|
||||||
@@ -53,11 +51,10 @@ teststeps:
|
|||||||
foo2: bar23
|
foo2: bar23
|
||||||
request:
|
request:
|
||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: $base_url/post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
Content-Type: "application/x-www-form-urlencoded"
|
||||||
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
body: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.form.foo1", "$expect_foo1"]
|
- eq: ["body.form.foo1", "$expect_foo1"]
|
||||||
75
examples/demo-with-py-plugin/.debugtalk_gen.py
Normal file
75
examples/demo-with-py-plugin/.debugtalk_gen.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# NOTE: Generated By hrp v4.1.1, DO NOT EDIT!
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import funppy
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_agent():
|
||||||
|
return "hrp/funppy"
|
||||||
|
|
||||||
|
|
||||||
|
def sleep(n_secs):
|
||||||
|
time.sleep(n_secs)
|
||||||
|
|
||||||
|
|
||||||
|
def sum(*args):
|
||||||
|
result = 0
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sum_ints(*args: List[int]) -> int:
|
||||||
|
result = 0
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sum_two_int(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
|
def sum_two_string(a: str, b: str) -> str:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
|
def sum_strings(*args: List[str]) -> str:
|
||||||
|
result = ""
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def concatenate(*args: List[str]) -> str:
|
||||||
|
result = ""
|
||||||
|
for arg in args:
|
||||||
|
result += str(arg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def setup_hook_example(name):
|
||||||
|
logging.warning("setup_hook_example")
|
||||||
|
return f"setup_hook_example: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_hook_example(name):
|
||||||
|
logging.warning("teardown_hook_example")
|
||||||
|
return f"teardown_hook_example: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
funppy.register("get_user_agent", get_user_agent)
|
||||||
|
funppy.register("sleep", sleep)
|
||||||
|
funppy.register("sum", sum)
|
||||||
|
funppy.register("sum_ints", sum_ints)
|
||||||
|
funppy.register("sum_two_int", sum_two_int)
|
||||||
|
funppy.register("sum_two_string", sum_two_string)
|
||||||
|
funppy.register("sum_strings", sum_strings)
|
||||||
|
funppy.register("concatenate", concatenate)
|
||||||
|
funppy.register("setup_hook_example", setup_hook_example)
|
||||||
|
funppy.register("teardown_hook_example", teardown_hook_example)
|
||||||
|
funppy.serve()
|
||||||
3
examples/demo-with-py-plugin/.env
Normal file
3
examples/demo-with-py-plugin/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
base_url=https://postman-echo.com
|
||||||
|
USERNAME=debugtalk
|
||||||
|
PASSWORD=123456
|
||||||
1
examples/demo-with-py-plugin/.gitignore
vendored
1
examples/demo-with-py-plugin/.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
.env
|
|
||||||
reports/
|
reports/
|
||||||
*.so
|
*.so
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import funppy
|
|
||||||
|
|
||||||
|
def get_user_agent():
|
||||||
def get_version():
|
return "hrp/funppy"
|
||||||
return funppy.__version__
|
|
||||||
|
|
||||||
|
|
||||||
def sleep(n_secs):
|
def sleep(n_secs):
|
||||||
@@ -57,17 +55,3 @@ def setup_hook_example(name):
|
|||||||
def teardown_hook_example(name):
|
def teardown_hook_example(name):
|
||||||
logging.warning("teardown_hook_example")
|
logging.warning("teardown_hook_example")
|
||||||
return f"teardown_hook_example: {name}"
|
return f"teardown_hook_example: {name}"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
funppy.register("get_version", get_version)
|
|
||||||
funppy.register("sum", sum)
|
|
||||||
funppy.register("sum_ints", sum_ints)
|
|
||||||
funppy.register("concatenate", concatenate)
|
|
||||||
funppy.register("sum_two_int", sum_two_int)
|
|
||||||
funppy.register("sum_two", sum_two_int)
|
|
||||||
funppy.register("sum_two_string", sum_two_string)
|
|
||||||
funppy.register("sum_strings", sum_strings)
|
|
||||||
funppy.register("setup_hook_example", setup_hook_example)
|
|
||||||
funppy.register("teardown_hook_example", teardown_hook_example)
|
|
||||||
funppy.serve()
|
|
||||||
|
|||||||
5
examples/demo-with-py-plugin/proj.json
Normal file
5
examples/demo-with-py-plugin/proj.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"project_name": "demo-with-py-plugin",
|
||||||
|
"create_time": "2022-05-31T15:05:50.036068+08:00",
|
||||||
|
"hrp_version": "v4.1.1"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
-
|
-
|
||||||
@@ -24,10 +24,10 @@ teststeps:
|
|||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: /post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
User-Agent: ${get_user_agent()}
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
Content-Type: "application/x-www-form-urlencoded"
|
||||||
data: "foo1=$foo1&foo2=$foo3"
|
body: "foo1=$foo1&foo2=$foo3"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.form.foo1", "bar1"]
|
- eq: ["body.form.foo1", "bar1"]
|
||||||
- eq: ["body.form.foo2", "bar21"]
|
- eq: ["body.form.foo2", "bar21"]
|
||||||
136
examples/demo-with-py-plugin/testcases/requests.json
Normal file
136
examples/demo-with-py-plugin/testcases/requests.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "request methods testcase with functions",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "config_bar1",
|
||||||
|
"foo2": "config_bar2",
|
||||||
|
"expect_foo1": "config_bar1",
|
||||||
|
"expect_foo2": "config_bar2"
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "${get_user_agent()}"
|
||||||
|
},
|
||||||
|
"base_url": "https://postman-echo.com",
|
||||||
|
"verify": false,
|
||||||
|
"export": [
|
||||||
|
"foo3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "get with params",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "${ENV(USERNAME)}",
|
||||||
|
"foo2": "bar21",
|
||||||
|
"sum_v": "${sum_two_int(1, 2)}"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/get",
|
||||||
|
"params": {
|
||||||
|
"foo1": "$foo1",
|
||||||
|
"foo2": "$foo2",
|
||||||
|
"sum_v": "$sum_v"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract": {
|
||||||
|
"foo3": "body.args.foo2"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.foo1",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "debugtalk",
|
||||||
|
"msg": "check body.args.foo1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.sum_v",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "3",
|
||||||
|
"msg": "check body.args.sum_v"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.args.foo2",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar21",
|
||||||
|
"msg": "check body.args.foo2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post raw text",
|
||||||
|
"variables": {
|
||||||
|
"foo1": "bar12",
|
||||||
|
"foo3": "bar32"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
},
|
||||||
|
"body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.data",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
|
||||||
|
"msg": "check body.data"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post form data",
|
||||||
|
"variables": {
|
||||||
|
"foo2": "bar23"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
"body": "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "status_code",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": 200,
|
||||||
|
"msg": "check status_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo1",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "$expect_foo1",
|
||||||
|
"msg": "check body.form.foo1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo2",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar23",
|
||||||
|
"msg": "check body.form.foo2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": "body.form.foo3",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "bar21",
|
||||||
|
"msg": "check body.form.foo3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ config:
|
|||||||
foo2: config_bar2
|
foo2: config_bar2
|
||||||
expect_foo1: config_bar1
|
expect_foo1: config_bar1
|
||||||
expect_foo2: config_bar2
|
expect_foo2: config_bar2
|
||||||
base_url: "https://postman-echo.com"
|
headers:
|
||||||
|
User-Agent: ${get_user_agent()}
|
||||||
verify: False
|
verify: False
|
||||||
export: ["foo3"]
|
export: ["foo3"]
|
||||||
|
|
||||||
@@ -13,23 +14,21 @@ teststeps:
|
|||||||
-
|
-
|
||||||
name: get with params
|
name: get with params
|
||||||
variables:
|
variables:
|
||||||
foo1: bar11
|
foo1: ${ENV(USERNAME)}
|
||||||
foo2: bar21
|
foo2: bar21
|
||||||
sum_v: "${sum_two_int(1, 2)}"
|
sum_v: "${sum_two_int(1, 2)}"
|
||||||
request:
|
request:
|
||||||
method: GET
|
method: GET
|
||||||
url: /get
|
url: $base_url/get
|
||||||
params:
|
params:
|
||||||
foo1: $foo1
|
foo1: $foo1
|
||||||
foo2: $foo2
|
foo2: $foo2
|
||||||
sum_v: $sum_v
|
sum_v: $sum_v
|
||||||
headers:
|
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
extract:
|
extract:
|
||||||
foo3: "body.args.foo2"
|
foo3: "body.args.foo2"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.args.foo1", "bar11"]
|
- eq: ["body.args.foo1", "debugtalk"]
|
||||||
- eq: ["body.args.sum_v", "3"]
|
- eq: ["body.args.sum_v", "3"]
|
||||||
- eq: ["body.args.foo2", "bar21"]
|
- eq: ["body.args.foo2", "bar21"]
|
||||||
-
|
-
|
||||||
@@ -39,11 +38,10 @@ teststeps:
|
|||||||
foo3: "bar32"
|
foo3: "bar32"
|
||||||
request:
|
request:
|
||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: $base_url/post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
Content-Type: "text/plain"
|
Content-Type: "text/plain"
|
||||||
data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
|
- eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."]
|
||||||
@@ -53,11 +51,10 @@ teststeps:
|
|||||||
foo2: bar23
|
foo2: bar23
|
||||||
request:
|
request:
|
||||||
method: POST
|
method: POST
|
||||||
url: /post
|
url: $base_url/post
|
||||||
headers:
|
headers:
|
||||||
User-Agent: funplugin/${get_version()}
|
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
Content-Type: "application/x-www-form-urlencoded"
|
||||||
data: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
body: "foo1=$foo1&foo2=$foo2&foo3=$foo3"
|
||||||
validate:
|
validate:
|
||||||
- eq: ["status_code", 200]
|
- eq: ["status_code", 200]
|
||||||
- eq: ["body.form.foo1", "$expect_foo1"]
|
- eq: ["body.form.foo1", "$expect_foo1"]
|
||||||
3
examples/demo-without-plugin/.env
Normal file
3
examples/demo-without-plugin/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
base_url=https://postman-echo.com
|
||||||
|
USERNAME=debugtalk
|
||||||
|
PASSWORD=123456
|
||||||
1
examples/demo-without-plugin/.gitignore
vendored
1
examples/demo-without-plugin/.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
.env
|
|
||||||
reports/
|
reports/
|
||||||
*.so
|
*.so
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
5
examples/demo-without-plugin/proj.json
Normal file
5
examples/demo-without-plugin/proj.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"project_name": "demo-without-plugin",
|
||||||
|
"create_time": "2022-05-31T15:05:51.066376+08:00",
|
||||||
|
"hrp_version": "v4.1.1"
|
||||||
|
}
|
||||||
@@ -9,9 +9,15 @@
|
|||||||
"username-password": "${parameterize($file)}"
|
"username-password": "${parameterize($file)}"
|
||||||
},
|
},
|
||||||
"parameters_setting": {
|
"parameters_setting": {
|
||||||
|
"pick_order": "random",
|
||||||
"strategies": {
|
"strategies": {
|
||||||
"user_agent": "sequential",
|
"user_agent": {
|
||||||
"username-password": "random"
|
"name": "user-identity",
|
||||||
|
"pick_order": "sequential"
|
||||||
|
},
|
||||||
|
"username-password": {
|
||||||
|
"name": "user-info"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"limit": 6
|
"limit": 6
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ config:
|
|||||||
user_agent: [ "iOS/10.1", "iOS/10.2" ]
|
user_agent: [ "iOS/10.1", "iOS/10.2" ]
|
||||||
username-password: ${parameterize($file)}
|
username-password: ${parameterize($file)}
|
||||||
parameters_setting:
|
parameters_setting:
|
||||||
|
pick_order: "random"
|
||||||
strategies:
|
strategies:
|
||||||
user_agent: "sequential"
|
user_agent:
|
||||||
username-password: "random"
|
name: "user-identity"
|
||||||
|
pick_order: "sequential"
|
||||||
|
username-password:
|
||||||
|
name: "user-info"
|
||||||
limit: 6
|
limit: 6
|
||||||
variables:
|
variables:
|
||||||
app_version: v1
|
app_version: v1
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -1,14 +1,16 @@
|
|||||||
module github.com/httprunner/httprunner
|
module github.com/httprunner/httprunner/v4
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4
|
github.com/andybalholm/brotli v1.0.4
|
||||||
github.com/denisbrodbeck/machineid v1.0.1
|
github.com/denisbrodbeck/machineid v1.0.1
|
||||||
|
github.com/fatih/color v1.13.0
|
||||||
github.com/getsentry/sentry-go v0.13.0
|
github.com/getsentry/sentry-go v0.13.0
|
||||||
|
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.4
|
github.com/httprunner/funplugin v0.4.8
|
||||||
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
|
||||||
@@ -21,7 +23,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.2.1
|
github.com/spf13/cobra v1.2.1
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
gopkg.in/yaml.v3 v3.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/httprunner/funplugin => ../funplugin
|
// replace github.com/httprunner/funplugin => ../funplugin
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -106,8 +107,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
@@ -130,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
|
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||||
|
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
@@ -241,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.4 h1:IVt603Y57WfSbn6DZ0R4iLeGQJ1yj944gmYwEOSBzGo=
|
github.com/httprunner/funplugin v0.4.8 h1:G785jrEn6EAEg2nwuPcCQUHBTgwgoaSz5qdQU4X3JpI=
|
||||||
github.com/httprunner/funplugin v0.4.4/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
|
github.com/httprunner/funplugin v0.4.8/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=
|
||||||
@@ -261,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
|
|||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
@@ -287,22 +301,27 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
|
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
|
||||||
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
|
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
@@ -346,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl
|
|||||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
@@ -835,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
@@ -852,8 +874,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin"
|
"github.com/httprunner/httprunner/v4/hrp/internal/boomer"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/boomer"
|
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
|
func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
|
||||||
@@ -19,9 +19,6 @@ func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.hrpRunner = NewRunner(nil)
|
b.hrpRunner = NewRunner(nil)
|
||||||
// set client transport for high concurrency load testing
|
|
||||||
b.hrpRunner.SetClientTransport(b.GetSpawnCount(), b.GetDisableKeepAlive(), b.GetDisableCompression())
|
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +29,11 @@ type HRPBoomer struct {
|
|||||||
pluginsMutex *sync.RWMutex // avoid data race
|
pluginsMutex *sync.RWMutex // avoid data race
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *HRPBoomer) SetClientTransport() {
|
||||||
|
// set client transport for high concurrency load testing
|
||||||
|
b.hrpRunner.SetClientTransport(b.GetSpawnCount(), b.GetDisableKeepAlive(), b.GetDisableCompression())
|
||||||
|
}
|
||||||
|
|
||||||
// Run starts to run load test for one or multiple testcases.
|
// Run starts to run load test for one or multiple testcases.
|
||||||
func (b *HRPBoomer) Run(testcases ...ITestCase) {
|
func (b *HRPBoomer) Run(testcases ...ITestCase) {
|
||||||
event := sdk.EventTracking{
|
event := sdk.EventTracking{
|
||||||
@@ -97,6 +99,9 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
|||||||
parametersIterator := caseRunner.parametersIterator
|
parametersIterator := caseRunner.parametersIterator
|
||||||
parametersIterator.SetUnlimitedMode()
|
parametersIterator.SetUnlimitedMode()
|
||||||
|
|
||||||
|
// reset start time only once
|
||||||
|
once := sync.Once{}
|
||||||
|
|
||||||
return &boomer.Task{
|
return &boomer.Task{
|
||||||
Name: testcase.Config.Name,
|
Name: testcase.Config.Name,
|
||||||
Weight: testcase.Config.Weight,
|
Weight: testcase.Config.Weight,
|
||||||
@@ -113,6 +118,10 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
|||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
for _, step := range testcase.TestSteps {
|
for _, step := range testcase.TestSteps {
|
||||||
|
// reset start time only once before step
|
||||||
|
once.Do(func() {
|
||||||
|
b.Boomer.ResetStartTime()
|
||||||
|
})
|
||||||
stepResult, err := step.Run(sessionRunner)
|
stepResult, err := step.Run(sessionRunner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// step failed
|
// step failed
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ func TestBoomerStandaloneRun(t *testing.T) {
|
|||||||
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testcase2 := &demoTestCaseWithPluginJSONPath
|
testcase2 := TestCasePath(demoTestCaseWithPluginJSONPath)
|
||||||
|
|
||||||
b := NewBoomer(2, 1)
|
b := NewBoomer(2, 1)
|
||||||
go b.Run(testcase1, testcase2)
|
go b.Run(testcase1, &testcase2)
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
b.Quit()
|
b.Quit()
|
||||||
}
|
}
|
||||||
|
|||||||
291
hrp/build.go
Normal file
291
hrp/build.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package hrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin/shared"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
funppy = `import funppy`
|
||||||
|
fungo = `"github.com/httprunner/funplugin/fungo"`
|
||||||
|
regexPythonFunctionName = `def ([a-zA-Z_]\w*)\(.*\)`
|
||||||
|
regexGoImports = `import \(([\s\S]*?)\)`
|
||||||
|
regexGoImport = `import (\"[\s\S]*\")`
|
||||||
|
regexGoFunctionName = `func ([A-Z][a-zA-Z_]\w*)\(.*\)`
|
||||||
|
regexGoFunctionContent = `func [\s\S]*?\n}`
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed internal/scaffold/templates/plugin/debugtalkPythonTemplate
|
||||||
|
var pyTemplate string
|
||||||
|
|
||||||
|
//go:embed internal/scaffold/templates/plugin/debugtalkGoTemplate
|
||||||
|
var goTemplate string
|
||||||
|
|
||||||
|
type TemplateContent struct {
|
||||||
|
Version string // hrp version
|
||||||
|
Fun string // funplugin package
|
||||||
|
Regexps *Regexps // match import/function
|
||||||
|
Imports []string // python/go import
|
||||||
|
FromImports []string // python from...import...
|
||||||
|
Functions []string // python/go function
|
||||||
|
FunctionNames []string // function name set by user
|
||||||
|
}
|
||||||
|
|
||||||
|
type Regexps struct {
|
||||||
|
Import *regexp.Regexp
|
||||||
|
Imports *regexp.Regexp
|
||||||
|
FunctionName *regexp.Regexp
|
||||||
|
FunctionContent *regexp.Regexp // including function define and body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TemplateContent) parseGoContent(path string) error {
|
||||||
|
log.Info().Str("path", path).Msg("start to parse debugtalk.go")
|
||||||
|
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to read file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
originalContent := string(content)
|
||||||
|
|
||||||
|
// parse imports
|
||||||
|
importSlice := t.Regexps.Imports.FindAllStringSubmatch(originalContent, -1)
|
||||||
|
if len(importSlice) != 0 {
|
||||||
|
imports := strings.Replace(importSlice[0][1], "\t", "", -1)
|
||||||
|
for _, elem := range strings.Split(imports, "\n") {
|
||||||
|
t.Imports = append(t.Imports, strings.TrimSpace(elem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// parse import
|
||||||
|
importSlice = t.Regexps.Import.FindAllStringSubmatch(originalContent, -1)
|
||||||
|
if len(importSlice) != 0 {
|
||||||
|
for _, elem := range importSlice {
|
||||||
|
t.Imports = append(t.Imports, strings.TrimSpace(elem[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// import fungo package
|
||||||
|
if !builtin.Contains(t.Imports, fungo) {
|
||||||
|
t.Imports = append(t.Imports, t.Fun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse function name
|
||||||
|
functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(originalContent, -1)
|
||||||
|
for _, elem := range functionNameSlice {
|
||||||
|
name := strings.Trim(elem[1], " ")
|
||||||
|
if name == "main" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.FunctionNames = append(t.FunctionNames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse function content
|
||||||
|
functionContentSlice := t.Regexps.FunctionContent.FindAllStringSubmatch(originalContent, -1)
|
||||||
|
for _, f := range functionContentSlice {
|
||||||
|
if strings.Contains(f[0], "func main") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Functions = append(t.Functions, strings.Trim(f[0], "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TemplateContent) parsePyContent(path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", path).Msg("failed to open file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
r := bufio.NewReader(file)
|
||||||
|
|
||||||
|
// record content excluding import and main
|
||||||
|
content := ""
|
||||||
|
|
||||||
|
// parse python content line by line
|
||||||
|
for {
|
||||||
|
l, _, err := r.ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
line := string(l)
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "import") {
|
||||||
|
t.Imports = append(t.Imports, strings.Trim(line, " "))
|
||||||
|
} else if strings.HasPrefix(line, "from") {
|
||||||
|
t.FromImports = append(t.FromImports, strings.Trim(line, " "))
|
||||||
|
} else {
|
||||||
|
// no parse content at under of `if __name__ == "__main__"`
|
||||||
|
if strings.HasPrefix(line, "if __name__") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "def") {
|
||||||
|
functionNameSlice := t.Regexps.FunctionName.FindAllStringSubmatch(line, -1)
|
||||||
|
if len(functionNameSlice) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.FunctionNames = append(t.FunctionNames, functionNameSlice[0][1])
|
||||||
|
}
|
||||||
|
content += line + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// function content
|
||||||
|
t.Functions = append(t.Functions, strings.Trim(content, "\n"))
|
||||||
|
|
||||||
|
// import funppy
|
||||||
|
if !builtin.Contains(t.Imports, t.Fun) {
|
||||||
|
t.Imports = append(t.Imports, t.Fun)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TemplateContent) genDebugTalk(path string, templ string) error {
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("open file failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
tmpl := template.Must(template.New("debugtalk").Parse(templ))
|
||||||
|
err = tmpl.Execute(writer, t)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = writer.Flush()
|
||||||
|
if err == nil {
|
||||||
|
log.Info().Str("path", path).Msg("generate debugtalk success")
|
||||||
|
} else {
|
||||||
|
log.Error().Str("path", path).Msg("generate debugtalk failed")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGo builds debugtalk.go to debugtalk.bin
|
||||||
|
func buildGo(path string, output string) error {
|
||||||
|
templateContent := &TemplateContent{
|
||||||
|
Version: version.VERSION,
|
||||||
|
Fun: fungo,
|
||||||
|
Regexps: &Regexps{
|
||||||
|
Import: regexp.MustCompile(regexGoImport),
|
||||||
|
Imports: regexp.MustCompile(regexGoImports),
|
||||||
|
FunctionName: regexp.MustCompile(regexGoFunctionName),
|
||||||
|
FunctionContent: regexp.MustCompile(regexGoFunctionContent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := filepath.Dir(path)
|
||||||
|
|
||||||
|
// check go sdk in tempDir
|
||||||
|
if err := builtin.ExecCommandInDir(exec.Command("go", "version"), pluginDir); err != nil {
|
||||||
|
return errors.Wrap(err, "go sdk not installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse debugtalk.go in pluginDir
|
||||||
|
err := templateContent.parseGoContent(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate debugtalk.go in pluginDir
|
||||||
|
err = templateContent.genDebugTalk(filepath.Join(pluginDir, PluginGoSourceGenFile), goTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) {
|
||||||
|
// create go mod
|
||||||
|
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "main"), pluginDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// download plugin dependency
|
||||||
|
// funplugin version should be locked
|
||||||
|
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version)
|
||||||
|
if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add missing and remove unused modules
|
||||||
|
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "tidy"), pluginDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if output == "" {
|
||||||
|
dir, _ := os.Getwd()
|
||||||
|
output = filepath.Join(dir, PluginHashicorpGoBuiltFile)
|
||||||
|
} else if builtin.IsFolderPathExists(output) {
|
||||||
|
output = filepath.Join(output, PluginHashicorpGoBuiltFile)
|
||||||
|
}
|
||||||
|
outputPath, err := filepath.Abs(output)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build plugin debugtalk.bin
|
||||||
|
cmd := exec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(path))
|
||||||
|
if err := builtin.ExecCommandInDir(cmd, pluginDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info().Str("output", outputPath).Str("plugin", path).Msg("build plugin successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPy completes funppy information in debugtalk.py
|
||||||
|
func buildPy(path string, output string) error {
|
||||||
|
templateContent := &TemplateContent{
|
||||||
|
Version: version.VERSION,
|
||||||
|
Fun: funppy,
|
||||||
|
Regexps: &Regexps{
|
||||||
|
FunctionName: regexp.MustCompile(regexPythonFunctionName),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := templateContent.parsePyContent(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate .debugtalk_gen.py
|
||||||
|
if output == "" {
|
||||||
|
dir, _ := os.Getwd()
|
||||||
|
output = filepath.Join(dir, PluginPySourceGenFile)
|
||||||
|
} else if builtin.IsFolderPathExists(output) {
|
||||||
|
output = filepath.Join(output, PluginPySourceGenFile)
|
||||||
|
}
|
||||||
|
err = templateContent.genDebugTalk(output, pyTemplate)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPlugin(path string, output string) (err error) {
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
switch ext {
|
||||||
|
case ".py":
|
||||||
|
err = buildPy(path, output)
|
||||||
|
case ".go":
|
||||||
|
err = buildGo(path, output)
|
||||||
|
default:
|
||||||
|
return errors.New("type error, expected .py or .go")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("arg", path).Msg("build plugin failed")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
hrp/build_test.go
Normal file
44
hrp/build_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package hrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
err := BuildPlugin(tmpl("plugin/debugtalk.go"), "./debugtalk.bin")
|
||||||
|
if !assert.Nil(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
genDebugTalkPyPath := filepath.Join(tmpl("plugin/"), PluginPySourceGenFile)
|
||||||
|
err = BuildPlugin(tmpl("plugin/debugtalk.py"), genDebugTalkPyPath)
|
||||||
|
if !assert.Nil(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBytes, err := builtin.ReadFile(genDebugTalkPyPath)
|
||||||
|
if !assert.Nil(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(contentBytes)
|
||||||
|
if !assert.Contains(t, content, "import funppy") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assert.Contains(t, content, "funppy.register") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
reg, _ := regexp.Compile(`funppy\.register`)
|
||||||
|
matchedSlice := reg.FindAllStringSubmatch(content, -1)
|
||||||
|
if !assert.Len(t, matchedSlice, 10) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/hrp"
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
"github.com/httprunner/httprunner/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,56 +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.AddOutput(boomer.NewPrometheusPusherOutput(boomArgs.PrometheusPushgatewayURL, "hrp", hrpBoomer.GetMode()))
|
||||||
}
|
}
|
||||||
hrpBoomer.SetDisableKeepAlive(disableKeepalive)
|
hrpBoomer.SetDisableKeepAlive(boomArgs.DisableKeepalive)
|
||||||
hrpBoomer.SetDisableCompression(disableCompression)
|
hrpBoomer.SetDisableCompression(boomArgs.DisableCompression)
|
||||||
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration)
|
hrpBoomer.SetClientTransport()
|
||||||
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration)
|
hrpBoomer.EnableCPUProfile(boomArgs.CPUProfile, boomArgs.CPUProfileDuration)
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
30
hrp/cmd/build.go
Normal file
30
hrp/cmd/build.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var buildCmd = &cobra.Command{
|
||||||
|
Use: "build $path ...",
|
||||||
|
Short: "build plugin for testing",
|
||||||
|
Long: `build python/go plugin for testing`,
|
||||||
|
Example: ` $ hrp build plugin/debugtalk.go
|
||||||
|
$ hrp build plugin/debugtalk.py`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setLogLevel(logLevel)
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return hrp.BuildPlugin(args[0], output)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var output string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(buildCmd)
|
||||||
|
|
||||||
|
buildCmd.Flags().StringVarP(&output, "output", "o", "", "funplugin product output path, default: cwd")
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/cmd"
|
"github.com/httprunner/httprunner/v4/hrp/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -2,47 +2,60 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/convert"
|
"github.com/httprunner/httprunner/v4/hrp/internal/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var convertCmd = &cobra.Command{
|
var convertCmd = &cobra.Command{
|
||||||
Use: "convert $path...",
|
Use: "convert $path...",
|
||||||
Short: "convert JSON/YAML testcases to pytest/gotest scripts",
|
Short: "convert to JSON/YAML/gotest/pytest testcases",
|
||||||
Args: cobra.ExactValidArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
setLogLevel(logLevel)
|
setLogLevel(logLevel)
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if !pytestFlag && !gotestFlag {
|
var flagCount int
|
||||||
return errors.New("please specify convertion type")
|
var outputType convert.OutputType
|
||||||
|
if toJSONFlag {
|
||||||
|
flagCount++
|
||||||
}
|
}
|
||||||
|
if toYAMLFlag {
|
||||||
var err error
|
flagCount++
|
||||||
if gotestFlag {
|
outputType = convert.OutputTypeYAML
|
||||||
err = convert.Convert2TestScripts("gotest", args...)
|
|
||||||
} else {
|
|
||||||
err = convert.Convert2TestScripts("pytest", args...)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if toGoTestFlag {
|
||||||
log.Error().Err(err).Msg("convert test scripts failed")
|
flagCount++
|
||||||
os.Exit(1)
|
outputType = convert.OutputTypeGoTest
|
||||||
}
|
}
|
||||||
|
if toPyTestFlag {
|
||||||
|
flagCount++
|
||||||
|
outputType = convert.OutputTypePyTest
|
||||||
|
}
|
||||||
|
if flagCount > 1 {
|
||||||
|
return errors.New("please specify at most one conversion flag")
|
||||||
|
}
|
||||||
|
convert.Run(outputType, outputDir, profilePath, args)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pytestFlag bool
|
toJSONFlag bool
|
||||||
gotestFlag bool
|
toYAMLFlag bool
|
||||||
|
toGoTestFlag bool
|
||||||
|
toPyTestFlag bool
|
||||||
|
outputDir string
|
||||||
|
profilePath string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(convertCmd)
|
rootCmd.AddCommand(convertCmd)
|
||||||
convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts")
|
convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
|
||||||
convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)")
|
convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)")
|
||||||
|
convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)")
|
||||||
|
convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts")
|
||||||
|
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
|
||||||
|
convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/har2case"
|
|
||||||
)
|
|
||||||
|
|
||||||
// har2caseCmd represents the har2case command
|
|
||||||
var har2caseCmd = &cobra.Command{
|
|
||||||
Use: "har2case $har_path...",
|
|
||||||
Short: "convert HAR to json/yaml testcase files",
|
|
||||||
Long: `convert HAR to json/yaml testcase files`,
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
setLogLevel(logLevel)
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
var outputFiles []string
|
|
||||||
for _, arg := range args {
|
|
||||||
// must choose one
|
|
||||||
if !genYAMLFlag && !genJSONFlag {
|
|
||||||
return errors.New("please select convert format type")
|
|
||||||
}
|
|
||||||
var outputPath string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
har := har2case.NewHAR(arg)
|
|
||||||
|
|
||||||
// specify output dir
|
|
||||||
if outputDir != "" {
|
|
||||||
har.SetOutputDir(outputDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// specify profile
|
|
||||||
if profilePath != "" {
|
|
||||||
har.SetProfile(profilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate json/yaml files
|
|
||||||
if genYAMLFlag {
|
|
||||||
outputPath, err = har.GenYAML()
|
|
||||||
} else {
|
|
||||||
outputPath, err = har.GenJSON() // default
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outputFiles = append(outputFiles, outputPath)
|
|
||||||
}
|
|
||||||
log.Info().Strs("output", outputFiles).Msg("convert testcase success")
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
genJSONFlag bool
|
|
||||||
genYAMLFlag bool
|
|
||||||
outputDir string
|
|
||||||
profilePath string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(har2caseCmd)
|
|
||||||
har2caseCmd.Flags().BoolVarP(&genJSONFlag, "to-json", "j", true, "convert to JSON format")
|
|
||||||
har2caseCmd.Flags().BoolVarP(&genYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
|
|
||||||
har2caseCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
|
|
||||||
har2caseCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies")
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/pytest"
|
"github.com/httprunner/httprunner/v4/hrp/internal/pytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pytestCmd = &cobra.Command{
|
var pytestCmd = &cobra.Command{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands
|
// rootCmd represents the base command when called without any subcommands
|
||||||
@@ -33,7 +33,7 @@ Website: https://httprunner.com
|
|||||||
Github: https://github.com/httprunner/httprunner
|
Github: https://github.com/httprunner/httprunner
|
||||||
Copyright 2017 debugtalk`,
|
Copyright 2017 debugtalk`,
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
var noColor = false
|
noColor := false
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
noColor = true
|
noColor = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp"
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runCmd represents the run command
|
// runCmd represents the run command
|
||||||
@@ -35,6 +35,9 @@ var runCmd = &cobra.Command{
|
|||||||
if !requestsLogOff {
|
if !requestsLogOff {
|
||||||
runner.SetRequestsLogOn()
|
runner.SetRequestsLogOn()
|
||||||
}
|
}
|
||||||
|
if httpStatOn {
|
||||||
|
runner.SetHTTPStatOn()
|
||||||
|
}
|
||||||
if pluginLogOn {
|
if pluginLogOn {
|
||||||
runner.SetPluginLogOn()
|
runner.SetPluginLogOn()
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,7 @@ var runCmd = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
continueOnFailure bool
|
continueOnFailure bool
|
||||||
requestsLogOff bool
|
requestsLogOff bool
|
||||||
|
httpStatOn bool
|
||||||
pluginLogOn bool
|
pluginLogOn bool
|
||||||
proxyUrl string
|
proxyUrl string
|
||||||
saveTests bool
|
saveTests bool
|
||||||
@@ -61,6 +65,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs")
|
runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs")
|
||||||
runCmd.Flags().BoolVar(&requestsLogOff, "log-requests-off", false, "turn off request & response details logging")
|
runCmd.Flags().BoolVar(&requestsLogOff, "log-requests-off", false, "turn off request & response details logging")
|
||||||
|
runCmd.Flags().BoolVar(&httpStatOn, "http-stat", false, "turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)")
|
||||||
runCmd.Flags().BoolVar(&pluginLogOn, "log-plugin", false, "turn on plugin logging")
|
runCmd.Flags().BoolVar(&pluginLogOn, "log-plugin", false, "turn on plugin logging")
|
||||||
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
|
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
|
||||||
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
|
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/scaffold"
|
"github.com/httprunner/httprunner/v4/hrp/internal/scaffold"
|
||||||
)
|
)
|
||||||
|
|
||||||
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,7 +24,9 @@ var scaffoldCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pluginType scaffold.PluginType
|
var pluginType scaffold.PluginType
|
||||||
if ignorePlugin {
|
if empty {
|
||||||
|
pluginType = scaffold.Empty
|
||||||
|
} else if ignorePlugin {
|
||||||
pluginType = scaffold.Ignore
|
pluginType = scaffold.Ignore
|
||||||
} else if genGoPlugin {
|
} else if genGoPlugin {
|
||||||
pluginType = scaffold.Go
|
pluginType = scaffold.Go
|
||||||
@@ -42,6 +45,7 @@ var scaffoldCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
empty bool
|
||||||
ignorePlugin bool
|
ignorePlugin bool
|
||||||
genPythonPlugin bool
|
genPythonPlugin bool
|
||||||
genGoPlugin bool
|
genGoPlugin bool
|
||||||
@@ -54,4 +58,5 @@ func init() {
|
|||||||
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
|
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
|
||||||
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
|
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
|
||||||
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")
|
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")
|
||||||
|
scaffoldCmd.Flags().BoolVar(&empty, "empty", false, "generate empty project")
|
||||||
}
|
}
|
||||||
|
|||||||
23
hrp/cmd/wiki.go
Normal file
23
hrp/cmd/wiki.go
Normal 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,13 +3,14 @@ package hrp
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewConfig returns a new constructed testcase config with specified testcase name.
|
// NewConfig returns a new constructed testcase config with specified testcase name.
|
||||||
func NewConfig(name string) *TConfig {
|
func NewConfig(name string) *TConfig {
|
||||||
return &TConfig{
|
return &TConfig{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Environs: make(map[string]string),
|
||||||
Variables: make(map[string]interface{}),
|
Variables: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,9 +20,10 @@ func NewConfig(name string) *TConfig {
|
|||||||
type TConfig struct {
|
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"` // deprecated in v4.1, moved to env
|
||||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // public request headers
|
||||||
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"`
|
Environs map[string]string `json:"environs,omitempty" yaml:"environs,omitempty"` // environment variables
|
||||||
|
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables
|
||||||
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"`
|
||||||
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||||
@@ -161,6 +163,4 @@ const (
|
|||||||
thinkTimeDefaultMultiply = 1
|
thinkTimeDefaultMultiply = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5}
|
||||||
thinkTimeDefaultRandom = map[string]float64{"min_percentage": 0.5, "max_percentage": 1.5}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,11 +7,26 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mode is the running mode of boomer, both standalone and distributed are supported.
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DistributedMasterMode requires being connected by each worker.
|
||||||
|
DistributedMasterMode Mode = iota
|
||||||
|
// DistributedWorkerMode requires connecting to a master.
|
||||||
|
DistributedWorkerMode
|
||||||
|
// StandaloneMode will run without a master.
|
||||||
|
StandaloneMode
|
||||||
|
)
|
||||||
|
|
||||||
// A Boomer is used to run tasks.
|
// A Boomer is used to run tasks.
|
||||||
type Boomer struct {
|
type Boomer struct {
|
||||||
|
mode Mode
|
||||||
|
|
||||||
localRunner *localRunner
|
localRunner *localRunner
|
||||||
|
|
||||||
cpuProfile string
|
cpuProfile string
|
||||||
@@ -24,9 +39,39 @@ type Boomer struct {
|
|||||||
disableCompression bool
|
disableCompression bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMode only accepts boomer.DistributedMasterMode、boomer.DistributedWorkerMode and boomer.StandaloneMode.
|
||||||
|
func (b *Boomer) SetMode(mode Mode) {
|
||||||
|
switch mode {
|
||||||
|
case DistributedMasterMode:
|
||||||
|
b.mode = DistributedMasterMode
|
||||||
|
case DistributedWorkerMode:
|
||||||
|
b.mode = DistributedWorkerMode
|
||||||
|
case StandaloneMode:
|
||||||
|
b.mode = StandaloneMode
|
||||||
|
default:
|
||||||
|
log.Error().Err(errors.New("Invalid mode, ignored!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMode returns boomer operating mode
|
||||||
|
func (b *Boomer) GetMode() string {
|
||||||
|
switch b.mode {
|
||||||
|
case DistributedMasterMode:
|
||||||
|
return "master"
|
||||||
|
case DistributedWorkerMode:
|
||||||
|
return "worker"
|
||||||
|
case StandaloneMode:
|
||||||
|
return "standalone"
|
||||||
|
default:
|
||||||
|
log.Error().Err(errors.New("Invalid mode, ignored!"))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewStandaloneBoomer returns a new Boomer, which can run without master.
|
// NewStandaloneBoomer returns a new Boomer, which can run without master.
|
||||||
func NewStandaloneBoomer(spawnCount int, spawnRate float64) *Boomer {
|
func NewStandaloneBoomer(spawnCount int, spawnRate float64) *Boomer {
|
||||||
return &Boomer{
|
return &Boomer{
|
||||||
|
mode: StandaloneMode,
|
||||||
localRunner: newLocalRunner(spawnCount, spawnRate),
|
localRunner: newLocalRunner(spawnCount, spawnRate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,3 +215,7 @@ func (b *Boomer) GetSpawnDoneChan() chan struct{} {
|
|||||||
func (b *Boomer) GetSpawnCount() int {
|
func (b *Boomer) GetSpawnCount() int {
|
||||||
return b.localRunner.spawnCount
|
return b.localRunner.spawnCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Boomer) ResetStartTime() {
|
||||||
|
b.localRunner.stats.total.resetStartTime()
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/push"
|
"github.com/prometheus/client_golang/prometheus/push"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Output is primarily responsible for printing test results to different destinations
|
// Output is primarily responsible for printing test results to different destinations
|
||||||
@@ -37,8 +38,7 @@ type Output interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConsoleOutput is the default output for standalone mode.
|
// ConsoleOutput is the default output for standalone mode.
|
||||||
type ConsoleOutput struct {
|
type ConsoleOutput struct{}
|
||||||
}
|
|
||||||
|
|
||||||
// NewConsoleOutput returns a ConsoleOutput.
|
// NewConsoleOutput returns a ConsoleOutput.
|
||||||
func NewConsoleOutput() *ConsoleOutput {
|
func NewConsoleOutput() *ConsoleOutput {
|
||||||
@@ -102,12 +102,10 @@ func getTotalFailRatio(totalRequests, totalFailures int64) (failRatio float64) {
|
|||||||
|
|
||||||
// OnStart of ConsoleOutput has nothing to do.
|
// OnStart of ConsoleOutput has nothing to do.
|
||||||
func (o *ConsoleOutput) OnStart() {
|
func (o *ConsoleOutput) OnStart() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnStop of ConsoleOutput has nothing to do.
|
// OnStop of ConsoleOutput has nothing to do.
|
||||||
func (o *ConsoleOutput) OnStop() {
|
func (o *ConsoleOutput) OnStop() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnEvent will print to the console.
|
// OnEvent will print to the console.
|
||||||
@@ -264,10 +262,9 @@ 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) / 1e3
|
||||||
// fix: avoid divide by zero
|
if duration == 0 {
|
||||||
if duration < 1 {
|
return nil, errors.New("no step specified")
|
||||||
duration = 1
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
duration = float64(reportStatsInterval / time.Second)
|
duration = float64(reportStatsInterval / time.Second)
|
||||||
@@ -474,10 +471,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewPrometheusPusherOutput returns a PrometheusPusherOutput.
|
// NewPrometheusPusherOutput returns a PrometheusPusherOutput.
|
||||||
func NewPrometheusPusherOutput(gatewayURL, jobName string) *PrometheusPusherOutput {
|
func NewPrometheusPusherOutput(gatewayURL, jobName string, mode string) *PrometheusPusherOutput {
|
||||||
nodeUUID, _ := uuid.NewUUID()
|
nodeUUID, _ := uuid.NewUUID()
|
||||||
return &PrometheusPusherOutput{
|
return &PrometheusPusherOutput{
|
||||||
pusher: push.New(gatewayURL, jobName).Grouping("instance", nodeUUID.String()),
|
pusher: push.New(gatewayURL, jobName).
|
||||||
|
Grouping("instance", nodeUUID.String()).
|
||||||
|
Grouping("mode", mode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,7 +153,7 @@ func (r *runner) reportTestResult() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
duration := time.Duration(entryTotalOutput.LastRequestTimestamp-entryTotalOutput.StartTime) * time.Second
|
duration := time.Duration(entryTotalOutput.LastRequestTimestamp-entryTotalOutput.StartTime) * time.Millisecond
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
println(fmt.Sprint("=========================================== Statistics Summary =========================================="))
|
println(fmt.Sprint("=========================================== Statistics Summary =========================================="))
|
||||||
println(fmt.Sprintf("Current time: %s, Users: %v, Duration: %v, Accumulated Transactions: %d Passed, %d Failed",
|
println(fmt.Sprintf("Current time: %s, Users: %v, Duration: %v, Accumulated Transactions: %d Passed, %d Failed",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package boomer
|
package boomer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
type transaction struct {
|
type transaction struct {
|
||||||
@@ -101,8 +102,6 @@ func (s *requestStats) get(name string, method string) (entry *statsEntry) {
|
|||||||
newEntry := &statsEntry{
|
newEntry := &statsEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
Method: method,
|
Method: method,
|
||||||
NumReqsPerSec: make(map[int64]int64),
|
|
||||||
NumFailPerSec: make(map[int64]int64),
|
|
||||||
ResponseTimes: make(map[int64]int64),
|
ResponseTimes: make(map[int64]int64),
|
||||||
}
|
}
|
||||||
s.entries[name+method] = newEntry
|
s.entries[name+method] = newEntry
|
||||||
@@ -171,10 +170,6 @@ type statsEntry struct {
|
|||||||
MinResponseTime int64 `json:"min_response_time"`
|
MinResponseTime int64 `json:"min_response_time"`
|
||||||
// Maximum response time
|
// Maximum response time
|
||||||
MaxResponseTime int64 `json:"max_response_time"`
|
MaxResponseTime int64 `json:"max_response_time"`
|
||||||
// A {second => request_count} dict that holds the number of requests made per second
|
|
||||||
NumReqsPerSec map[int64]int64 `json:"num_reqs_per_sec"`
|
|
||||||
// A (second => failure_count) dict that hold the number of failures per second
|
|
||||||
NumFailPerSec map[int64]int64 `json:"num_fail_per_sec"`
|
|
||||||
// A {response_time => count} dict that holds the response time distribution of all the requests
|
// A {response_time => count} dict that holds the response time distribution of all the requests
|
||||||
// The keys (the response time in ms) are rounded to store 1, 2, ... 9, 10, 20. .. 90,
|
// The keys (the response time in ms) are rounded to store 1, 2, ... 9, 10, 20. .. 90,
|
||||||
// 100, 200 .. 900, 1000, 2000 ... 9000, in order to save memory.
|
// 100, 200 .. 900, 1000, 2000 ... 9000, in order to save memory.
|
||||||
@@ -191,17 +186,19 @@ type statsEntry struct {
|
|||||||
NumNoneRequests int64 `json:"num_none_requests"`
|
NumNoneRequests int64 `json:"num_none_requests"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *statsEntry) resetStartTime() {
|
||||||
|
atomic.StoreInt64(&s.StartTime, time.Duration(time.Now().UnixNano()).Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *statsEntry) reset() {
|
func (s *statsEntry) reset() {
|
||||||
s.StartTime = time.Now().Unix()
|
atomic.StoreInt64(&s.StartTime, time.Duration(time.Now().UnixNano()).Milliseconds())
|
||||||
s.NumRequests = 0
|
s.NumRequests = 0
|
||||||
s.NumFailures = 0
|
s.NumFailures = 0
|
||||||
s.TotalResponseTime = 0
|
s.TotalResponseTime = 0
|
||||||
s.ResponseTimes = make(map[int64]int64)
|
s.ResponseTimes = make(map[int64]int64)
|
||||||
s.MinResponseTime = 0
|
s.MinResponseTime = 0
|
||||||
s.MaxResponseTime = 0
|
s.MaxResponseTime = 0
|
||||||
s.LastRequestTimestamp = time.Now().Unix()
|
s.LastRequestTimestamp = time.Duration(time.Now().UnixNano()).Milliseconds()
|
||||||
s.NumReqsPerSec = make(map[int64]int64)
|
|
||||||
s.NumFailPerSec = make(map[int64]int64)
|
|
||||||
s.TotalContentLength = 0
|
s.TotalContentLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,15 +212,7 @@ func (s *statsEntry) log(responseTime int64, contentLength int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *statsEntry) logTimeOfRequest() {
|
func (s *statsEntry) logTimeOfRequest() {
|
||||||
key := time.Now().Unix()
|
s.LastRequestTimestamp = time.Duration(time.Now().UnixNano()).Milliseconds()
|
||||||
_, ok := s.NumReqsPerSec[key]
|
|
||||||
if !ok {
|
|
||||||
s.NumReqsPerSec[key] = 1
|
|
||||||
} else {
|
|
||||||
s.NumReqsPerSec[key]++
|
|
||||||
}
|
|
||||||
|
|
||||||
s.LastRequestTimestamp = key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statsEntry) logResponseTime(responseTime int64) {
|
func (s *statsEntry) logResponseTime(responseTime int64) {
|
||||||
@@ -267,13 +256,6 @@ func (s *statsEntry) logResponseTime(responseTime int64) {
|
|||||||
|
|
||||||
func (s *statsEntry) logFailures() {
|
func (s *statsEntry) logFailures() {
|
||||||
s.NumFailures++
|
s.NumFailures++
|
||||||
key := time.Now().Unix()
|
|
||||||
_, ok := s.NumFailPerSec[key]
|
|
||||||
if !ok {
|
|
||||||
s.NumFailPerSec[key] = 1
|
|
||||||
} else {
|
|
||||||
s.NumFailPerSec[key]++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statsEntry) serialize() map[string]interface{} {
|
func (s *statsEntry) serialize() map[string]interface{} {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func SetUlimit(limit uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rLimit.Cur = limit
|
rLimit.Cur = limit
|
||||||
|
rLimit.Max = limit
|
||||||
log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit")
|
log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit")
|
||||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected
|
|||||||
"contained_by": ContainedBy,
|
"contained_by": ContainedBy,
|
||||||
"str_eq": StringEqual,
|
"str_eq": StringEqual,
|
||||||
"string_equals": StringEqual,
|
"string_equals": StringEqual,
|
||||||
|
"equal_fold": EqualFold,
|
||||||
"regex_match": RegexMatch,
|
"regex_match": RegexMatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +158,12 @@ func ContainedBy(t assert.TestingT, actual, expected interface{}, msgAndArgs ...
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StringEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
func StringEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||||
|
a := fmt.Sprintf("%v", actual)
|
||||||
|
e := fmt.Sprintf("%v", expected)
|
||||||
|
return assert.True(t, a == e, msgAndArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EqualFold(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||||
if !assert.IsType(t, "string", actual, msgAndArgs) {
|
if !assert.IsType(t, "string", actual, msgAndArgs) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,27 @@ func TestContainedBy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStringEqual(t *testing.T) {
|
func TestStringEqual(t *testing.T) {
|
||||||
|
testData := []struct {
|
||||||
|
raw interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{"abcd", "abcd"},
|
||||||
|
{"0", 0},
|
||||||
|
{"123", 123},
|
||||||
|
// {"123.0", 123.0}, // FIXME
|
||||||
|
{"12.3", 12.3},
|
||||||
|
{"-12.3", -12.3},
|
||||||
|
{"-123", -123},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range testData {
|
||||||
|
if !assert.True(t, StringEqual(t, data.raw, data.expected)) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqualFold(t *testing.T) {
|
||||||
testData := []struct {
|
testData := []struct {
|
||||||
raw interface{}
|
raw interface{}
|
||||||
expected interface{}
|
expected interface{}
|
||||||
@@ -168,7 +189,7 @@ func TestStringEqual(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, data := range testData {
|
for _, data := range testData {
|
||||||
if !assert.True(t, StringEqual(t, data.raw, data.expected)) {
|
if !assert.True(t, EqualFold(t, data.raw, data.expected)) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin/shared"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/shared"
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Dump2JSON(data interface{}, path string) error {
|
func Dump2JSON(data interface{}, path string) error {
|
||||||
@@ -28,8 +28,19 @@ func Dump2JSON(data interface{}, path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Info().Str("path", path).Msg("dump data to json")
|
log.Info().Str("path", path).Msg("dump data to json")
|
||||||
file, _ := json.MarshalIndent(data, "", " ")
|
|
||||||
err = os.WriteFile(path, file, 0644)
|
// init json encoder
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
err = encoder.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(path, buffer.Bytes(), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("dump json path failed")
|
log.Error().Err(err).Msg("dump json path failed")
|
||||||
return err
|
return err
|
||||||
@@ -56,7 +67,7 @@ func Dump2YAML(data interface{}, path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, buffer.Bytes(), 0644)
|
err = os.WriteFile(path, buffer.Bytes(), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("dump yaml path failed")
|
log.Error().Err(err).Msg("dump yaml path failed")
|
||||||
return err
|
return err
|
||||||
@@ -281,11 +292,12 @@ var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension")
|
|||||||
// LoadFile loads file content with file extension and assigns to structObj
|
// LoadFile loads file content with file extension and assigns to structObj
|
||||||
func LoadFile(path string, structObj interface{}) (err error) {
|
func LoadFile(path string, structObj interface{}) (err error) {
|
||||||
log.Info().Str("path", path).Msg("load file")
|
log.Info().Str("path", path).Msg("load file")
|
||||||
file, err := readFile(path)
|
file, err := ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "read file failed")
|
return errors.Wrap(err, "read file failed")
|
||||||
}
|
}
|
||||||
|
// remove BOM at the beginning of file
|
||||||
|
file = bytes.TrimLeft(file, "\xef\xbb\xbf")
|
||||||
ext := filepath.Ext(path)
|
ext := filepath.Ext(path)
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".json", ".har":
|
case ".json", ".har":
|
||||||
@@ -294,15 +306,47 @@ func LoadFile(path string, structObj interface{}) (err error) {
|
|||||||
err = decoder.Decode(structObj)
|
err = decoder.Decode(structObj)
|
||||||
case ".yaml", ".yml":
|
case ".yaml", ".yml":
|
||||||
err = yaml.Unmarshal(file, structObj)
|
err = yaml.Unmarshal(file, structObj)
|
||||||
|
case ".env":
|
||||||
|
err = parseEnvContent(file, structObj)
|
||||||
default:
|
default:
|
||||||
err = ErrUnsupportedFileExt
|
err = ErrUnsupportedFileExt
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseEnvContent(file []byte, obj interface{}) error {
|
||||||
|
envMap := obj.(map[string]string)
|
||||||
|
lines := strings.Split(string(file), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
// empty line or comment line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var kv []string
|
||||||
|
if strings.Contains(line, "=") {
|
||||||
|
kv = strings.SplitN(line, "=", 2)
|
||||||
|
} else if strings.Contains(line, ":") {
|
||||||
|
kv = strings.SplitN(line, ":", 2)
|
||||||
|
}
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return errors.New(".env format error")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(kv[0])
|
||||||
|
value := strings.TrimSpace(kv[1])
|
||||||
|
envMap[key] = value
|
||||||
|
|
||||||
|
// set env
|
||||||
|
log.Info().Str("key", key).Msg("set env")
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func loadFromCSV(path string) []map[string]interface{} {
|
func loadFromCSV(path string) []map[string]interface{} {
|
||||||
log.Info().Str("path", path).Msg("load csv file")
|
log.Info().Str("path", path).Msg("load csv file")
|
||||||
file, err := readFile(path)
|
file, err := ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("read csv file failed")
|
log.Error().Err(err).Msg("read csv file failed")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -328,7 +372,7 @@ func loadFromCSV(path string) []map[string]interface{} {
|
|||||||
|
|
||||||
func loadMessage(path string) []byte {
|
func loadMessage(path string) []byte {
|
||||||
log.Info().Str("path", path).Msg("load message file")
|
log.Info().Str("path", path).Msg("load message file")
|
||||||
file, err := readFile(path)
|
file, err := ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("read message file failed")
|
log.Error().Err(err).Msg("read message file failed")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -336,7 +380,7 @@ func loadMessage(path string) []byte {
|
|||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFile(path string) ([]byte, error) {
|
func ReadFile(path string) ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
path, err = filepath.Abs(path)
|
path, err = filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -351,3 +395,9 @@ func readFile(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOutputNameWithoutExtension(path string) string {
|
||||||
|
base := filepath.Base(path)
|
||||||
|
ext := filepath.Ext(base)
|
||||||
|
return base[0:len(base)-len(ext)] + "_test"
|
||||||
|
}
|
||||||
|
|||||||
77
hrp/internal/convert/README.md
Normal file
77
hrp/internal/convert/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# hrp convert
|
||||||
|
|
||||||
|
## 快速上手
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ hrp convert -h
|
||||||
|
convert to JSON/YAML/gotest/pytest testcases
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
hrp convert $path... [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for convert
|
||||||
|
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||||
|
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
|
||||||
|
--to-gotest convert to gotest scripts (TODO)
|
||||||
|
--to-json convert to JSON scripts (default true)
|
||||||
|
--to-pytest convert to pytest scripts
|
||||||
|
--to-yaml convert to YAML scripts
|
||||||
|
|
||||||
|
Global Flags:
|
||||||
|
--log-json set log to json format
|
||||||
|
-l, --log-level string set log level (default "INFO")
|
||||||
|
```
|
||||||
|
|
||||||
|
`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化。
|
||||||
|
|
||||||
|
该指令所有选项的详细说明如下:
|
||||||
|
|
||||||
|
1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例
|
||||||
|
2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹
|
||||||
|
3. `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例:
|
||||||
|
- 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息
|
||||||
|
```yaml
|
||||||
|
headers:
|
||||||
|
Header1: "this header will be created or updated"
|
||||||
|
cookies:
|
||||||
|
Cookie1: "this cookie will be created or updated"
|
||||||
|
```
|
||||||
|
- 根据 profile 覆盖原有的 `Headers` 和 `Cookies` 信息
|
||||||
|
```yaml
|
||||||
|
override: true
|
||||||
|
headers:
|
||||||
|
Header1: "all original headers will be overridden"
|
||||||
|
cookies:
|
||||||
|
Cookie1: "all original cookies will be overridden"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 输出的测试用例文件名格式为 `Postman 工程文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖
|
||||||
|
2. `hrp convert` 可以自动识别输入类型,因此不需要通过选项来手动制定输入类型,如遇到无法识别、不支持或转换失败的情况,则会输出错误日志并跳过,不会影响其他转换过程的正常进行
|
||||||
|
3. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式
|
||||||
|
4. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格
|
||||||
|
|
||||||
|
|
||||||
|
## 转换流程图
|
||||||
|
|
||||||
|
`hrp convert` 的转换过程流程图如下:
|
||||||
|

|
||||||
|
|
||||||
|
## 开发进度
|
||||||
|
|
||||||
|
`hrp convert` 当前的开发进度如下:
|
||||||
|
|
||||||
|
| from \ to | JSON | YAML | GoTest | PyTest |
|
||||||
|
|:---------:|:----:|:----:|:------:|:------:|
|
||||||
|
| HAR | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Postman | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| JMeter | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Swagger | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| curl | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Apache ab | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| JSON | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| YAML | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| GoTest | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| PyTest | ❌ | ❌ | ❌ | ❌ |
|
||||||
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
376
hrp/internal/convert/converter.go
Normal file
376
hrp/internal/convert/converter.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
suffixJSON = ".json"
|
||||||
|
suffixYAML = ".yaml"
|
||||||
|
suffixGoTest = ".go"
|
||||||
|
suffixPyTest = ".py"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
InputTypeUnknown InputType = iota // default input type: unknown
|
||||||
|
InputTypeHAR
|
||||||
|
InputTypePostman
|
||||||
|
InputTypeSwagger
|
||||||
|
InputTypeJMeter
|
||||||
|
InputTypeJSON
|
||||||
|
InputTypeYAML
|
||||||
|
InputTypeGoTest
|
||||||
|
InputTypePyTest
|
||||||
|
)
|
||||||
|
|
||||||
|
func (inputType InputType) String() string {
|
||||||
|
switch inputType {
|
||||||
|
case InputTypeHAR:
|
||||||
|
return "har"
|
||||||
|
case InputTypePostman:
|
||||||
|
return "postman"
|
||||||
|
case InputTypeSwagger:
|
||||||
|
return "swagger"
|
||||||
|
case InputTypeJMeter:
|
||||||
|
return "jmeter"
|
||||||
|
case InputTypeJSON:
|
||||||
|
return "json testcase"
|
||||||
|
case InputTypeYAML:
|
||||||
|
return "yaml testcase"
|
||||||
|
case InputTypeGoTest:
|
||||||
|
return "gotest script"
|
||||||
|
case InputTypePyTest:
|
||||||
|
return "pytest script"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OutputTypeJSON OutputType = iota // default output type: JSON
|
||||||
|
OutputTypeYAML
|
||||||
|
OutputTypeGoTest
|
||||||
|
OutputTypePyTest
|
||||||
|
)
|
||||||
|
|
||||||
|
func (outputType OutputType) String() string {
|
||||||
|
switch outputType {
|
||||||
|
case OutputTypeYAML:
|
||||||
|
return "yaml"
|
||||||
|
case OutputTypeGoTest:
|
||||||
|
return "gotest"
|
||||||
|
case OutputTypePyTest:
|
||||||
|
return "pytest"
|
||||||
|
default:
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCaseConverter holds the common properties of case converter
|
||||||
|
type TCaseConverter struct {
|
||||||
|
InputPath string
|
||||||
|
OutputDir string
|
||||||
|
Profile *Profile
|
||||||
|
InputType InputType
|
||||||
|
OutputType OutputType
|
||||||
|
CaseHAR *CaseHar
|
||||||
|
CasePostman *CasePostman
|
||||||
|
CaseSwagger *spec.Swagger
|
||||||
|
TCase *hrp.TCase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile is used to override or update(create if not existed) original headers and cookies
|
||||||
|
type Profile struct {
|
||||||
|
Override bool `json:"override" yaml:"override"`
|
||||||
|
Headers map[string]string `json:"headers" yaml:"headers"`
|
||||||
|
Cookies map[string]string `json:"cookies" yaml:"cookies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
|
||||||
|
tCaseConverter = &TCaseConverter{
|
||||||
|
InputPath: path,
|
||||||
|
InputType: InputTypeUnknown,
|
||||||
|
}
|
||||||
|
extName := filepath.Ext(path)
|
||||||
|
if extName == "" {
|
||||||
|
log.Warn().Msg("extension name should be specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch extName {
|
||||||
|
case ".har":
|
||||||
|
caseHAR := new(CaseHar)
|
||||||
|
err = builtin.LoadFile(path, caseHAR)
|
||||||
|
if err == nil && !reflect.ValueOf(*caseHAR).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypeHAR
|
||||||
|
tCaseConverter.CaseHAR = caseHAR
|
||||||
|
}
|
||||||
|
case ".json":
|
||||||
|
tCase := new(hrp.TCase)
|
||||||
|
err = builtin.LoadFile(path, tCase)
|
||||||
|
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypeJSON
|
||||||
|
tCaseConverter.TCase = tCase
|
||||||
|
break
|
||||||
|
}
|
||||||
|
casePostman := new(CasePostman)
|
||||||
|
err = builtin.LoadFile(path, casePostman)
|
||||||
|
// deal with postman field name conflict with swagger
|
||||||
|
descriptionBackup := casePostman.Info.Description
|
||||||
|
casePostman.Info.Description = ""
|
||||||
|
if err == nil && !reflect.ValueOf(*casePostman).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypePostman
|
||||||
|
casePostman.Info.Description = descriptionBackup
|
||||||
|
tCaseConverter.CasePostman = casePostman
|
||||||
|
break
|
||||||
|
}
|
||||||
|
caseSwagger := new(spec.Swagger)
|
||||||
|
err = builtin.LoadFile(path, caseSwagger)
|
||||||
|
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypeSwagger
|
||||||
|
tCaseConverter.CaseSwagger = caseSwagger
|
||||||
|
}
|
||||||
|
case ".yaml", ".yml":
|
||||||
|
tCase := new(hrp.TCase)
|
||||||
|
err = builtin.LoadFile(path, tCase)
|
||||||
|
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypeYAML
|
||||||
|
tCaseConverter.TCase = tCase
|
||||||
|
break
|
||||||
|
}
|
||||||
|
caseSwagger := new(spec.Swagger)
|
||||||
|
err = builtin.LoadFile(path, caseSwagger)
|
||||||
|
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
|
||||||
|
tCaseConverter.InputType = InputTypeSwagger
|
||||||
|
tCaseConverter.CaseSwagger = caseSwagger
|
||||||
|
}
|
||||||
|
case ".go": // TODO
|
||||||
|
tCaseConverter.InputType = InputTypeGoTest
|
||||||
|
case ".py": // TODO
|
||||||
|
tCaseConverter.InputType = InputTypePyTest
|
||||||
|
case ".jmx": // TODO
|
||||||
|
tCaseConverter.InputType = InputTypeJMeter
|
||||||
|
default:
|
||||||
|
log.Warn().
|
||||||
|
Str("input path", tCaseConverter.InputPath).
|
||||||
|
Msgf("unsupported file type: %v", extName)
|
||||||
|
}
|
||||||
|
if tCaseConverter.InputType != InputTypeUnknown {
|
||||||
|
log.Info().
|
||||||
|
Str("input path", tCaseConverter.InputPath).
|
||||||
|
Msgf("load case as: %s", tCaseConverter.InputType.String())
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("input path", tCaseConverter.InputPath).
|
||||||
|
Msgf("failed to load case")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCaseConverter) SetProfile(path string) {
|
||||||
|
log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
|
||||||
|
profile := new(Profile)
|
||||||
|
err := builtin.LoadFile(path, profile)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Str("path", path).
|
||||||
|
Msg("failed to load profile, ignore!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Profile = profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCaseConverter) SetOutputDir(dir string) {
|
||||||
|
log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
|
||||||
|
c.OutputDir = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCaseConverter) genOutputPath(suffix string) string {
|
||||||
|
outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
|
||||||
|
if c.OutputDir != "" {
|
||||||
|
return filepath.Join(c.OutputDir, outFileFullName)
|
||||||
|
} else {
|
||||||
|
return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
|
||||||
|
}
|
||||||
|
// TODO avoid outFileFullName conflict?
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCaseConverter) ToPyTest() (string, error) {
|
||||||
|
script := convertConfig(c.TCase.Config)
|
||||||
|
println(script)
|
||||||
|
return script, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertConfig(config *hrp.TConfig) string {
|
||||||
|
script := fmt.Sprintf("Config('%s')", config.Name)
|
||||||
|
|
||||||
|
if config.Variables != nil {
|
||||||
|
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
|
||||||
|
}
|
||||||
|
if config.BaseURL != "" {
|
||||||
|
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
|
||||||
|
}
|
||||||
|
if config.Export != nil {
|
||||||
|
script += fmt.Sprintf(".export(*%v)", config.Export)
|
||||||
|
}
|
||||||
|
script += fmt.Sprintf(".verify(%v)", config.Verify)
|
||||||
|
|
||||||
|
return script
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TCaseConverter) ToGoTest() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
|
||||||
|
type ICaseConverter interface {
|
||||||
|
Struct() *TCaseConverter
|
||||||
|
ToJSON() (string, error)
|
||||||
|
ToYAML() (string, error)
|
||||||
|
ToGoTest() (string, error)
|
||||||
|
ToPyTest() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(outputType OutputType, outputDir, profilePath string, args []string) {
|
||||||
|
// report event
|
||||||
|
sdk.SendEvent(sdk.EventTracking{
|
||||||
|
Category: "ConvertTests",
|
||||||
|
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()),
|
||||||
|
})
|
||||||
|
|
||||||
|
// identify input and load converters
|
||||||
|
var iCaseConverters []ICaseConverter
|
||||||
|
for _, arg := range args {
|
||||||
|
tCaseConverter := NewTCaseConverter(arg)
|
||||||
|
tCaseConverter.OutputType = outputType
|
||||||
|
if outputDir != "" {
|
||||||
|
tCaseConverter.SetOutputDir(outputDir)
|
||||||
|
}
|
||||||
|
if profilePath != "" {
|
||||||
|
tCaseConverter.SetProfile(profilePath)
|
||||||
|
}
|
||||||
|
switch tCaseConverter.InputType {
|
||||||
|
case InputTypeHAR:
|
||||||
|
iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
|
||||||
|
case InputTypePostman:
|
||||||
|
iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
|
||||||
|
case InputTypeJSON:
|
||||||
|
iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
|
||||||
|
case InputTypeYAML:
|
||||||
|
iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
|
||||||
|
case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
|
||||||
|
log.Warn().
|
||||||
|
Str("input path", tCaseConverter.InputPath).
|
||||||
|
Msg("case type not supported yet, ignore!")
|
||||||
|
default:
|
||||||
|
log.Warn().
|
||||||
|
Str("input path", tCaseConverter.InputPath).
|
||||||
|
Msg("unknown case type, ignore!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start converting
|
||||||
|
var outputFiles []string
|
||||||
|
var err error
|
||||||
|
for _, iCaseConverter := range iCaseConverters {
|
||||||
|
log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
|
||||||
|
var outputFile string
|
||||||
|
switch iCaseConverter.Struct().OutputType {
|
||||||
|
case OutputTypeYAML:
|
||||||
|
outputFile, err = iCaseConverter.ToYAML()
|
||||||
|
case OutputTypeGoTest:
|
||||||
|
outputFile, err = iCaseConverter.ToGoTest()
|
||||||
|
case OutputTypePyTest:
|
||||||
|
outputFile, err = iCaseConverter.ToPyTest()
|
||||||
|
default:
|
||||||
|
outputFile, err = iCaseConverter.ToJSON()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("input path", iCaseConverter.Struct().InputPath).
|
||||||
|
Msg("error occurs during converting")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outputFiles = append(outputFiles, outputFile)
|
||||||
|
}
|
||||||
|
log.Info().Strs("output files", outputFiles).Msg("conversion completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
|
||||||
|
tCase := iCaseConverter.Struct().TCase
|
||||||
|
if tCase == nil {
|
||||||
|
return nil, errors.Errorf("empty json/yaml testcase occurs")
|
||||||
|
}
|
||||||
|
profile := iCaseConverter.Struct().Profile
|
||||||
|
if profile == nil {
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
|
for _, step := range tCase.TestSteps {
|
||||||
|
// override original headers and cookies
|
||||||
|
if profile.Override {
|
||||||
|
step.Request.Headers = make(map[string]string)
|
||||||
|
step.Request.Cookies = make(map[string]string)
|
||||||
|
}
|
||||||
|
// update (create if not existed) original headers and cookies
|
||||||
|
if step.Request.Headers == nil {
|
||||||
|
step.Request.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
if step.Request.Cookies == nil {
|
||||||
|
step.Request.Cookies = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range profile.Headers {
|
||||||
|
step.Request.Headers[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range profile.Cookies {
|
||||||
|
step.Request.Cookies[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
|
||||||
|
// convert to temporary json testcase
|
||||||
|
jsonPath, err := iCaseConverter.ToJSON()
|
||||||
|
inputType := iCaseConverter.Struct().InputType
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if jsonPath != "" {
|
||||||
|
if err = os.Remove(jsonPath); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// convert from temporary json testcase to pytest
|
||||||
|
converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
|
||||||
|
pyTestPath, err := converterJSON.MakePyTestScript()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename resultant pytest
|
||||||
|
renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
|
||||||
|
err = os.Rename(pyTestPath, renamedPyTestPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
|
||||||
|
return pyTestPath, nil
|
||||||
|
}
|
||||||
|
return renamedPyTestPath, nil
|
||||||
|
}
|
||||||
60
hrp/internal/convert/converter_gotest.go
Normal file
60
hrp/internal/convert/converter_gotest.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convert2GoTestScripts(paths ...string) error {
|
||||||
|
log.Warn().Msg("convert to gotest scripts is not supported yet")
|
||||||
|
os.Exit(1)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
var testCasePaths []hrp.ITestCase
|
||||||
|
for _, path := range paths {
|
||||||
|
testCasePath := hrp.TestCasePath(path)
|
||||||
|
testCasePaths = append(testCasePaths, &testCasePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases, err := hrp.LoadTestCases(testCasePaths...)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to load testcases")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pytestPaths []string
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
tc := testCase.ToTCase()
|
||||||
|
converter := TCaseConverter{
|
||||||
|
TCase: tc,
|
||||||
|
}
|
||||||
|
pytestPath, err := converter.ToPyTest()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("originPath", tc.Config.Path).
|
||||||
|
Msg("convert to pytest failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Info().
|
||||||
|
Str("pytestPath", pytestPath).
|
||||||
|
Str("originPath", tc.Config.Path).
|
||||||
|
Msg("convert to pytest success")
|
||||||
|
pytestPaths = append(pytestPaths, pytestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// format pytest scripts with black
|
||||||
|
python3, err := builtin.EnsurePython3Venv("black")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args := append([]string{"-m", "black"}, pytestPaths...)
|
||||||
|
return builtin.ExecCommand(python3, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed testcase.tmpl
|
||||||
|
var testcaseTemplate string
|
||||||
@@ -1,6 +1,22 @@
|
|||||||
package har2case
|
package convert
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== model definition starts here ====================
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HTTP Archive (HAR) format
|
HTTP Archive (HAR) format
|
||||||
@@ -8,8 +24,8 @@ https://w3c.github.io/web-performance/specs/HAR/Overview.html
|
|||||||
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
|
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Har is a container type for deserialization
|
// CaseHar is a container type for deserialization
|
||||||
type Har struct {
|
type CaseHar struct {
|
||||||
Log Log `json:"log"`
|
Log Log `json:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,3 +354,333 @@ type TestResult struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
HarFile string `json:"harfile"`
|
HarFile string `json:"harfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== model definition ends here ====================
|
||||||
|
|
||||||
|
func NewConverterHAR(converter *TCaseConverter) *ConverterHAR {
|
||||||
|
return &ConverterHAR{
|
||||||
|
converter: converter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConverterHAR struct {
|
||||||
|
converter *TCaseConverter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) Struct() *TCaseConverter {
|
||||||
|
return c.converter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) ToJSON() (string, error) {
|
||||||
|
tCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||||
|
err = builtin.Dump2JSON(tCase, jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsonPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) ToYAML() (string, error) {
|
||||||
|
tCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||||
|
err = builtin.Dump2YAML(tCase, yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return yamlPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) ToGoTest() (string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
return "", errors.New("convert from har to gotest scripts is not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) ToPyTest() (string, error) {
|
||||||
|
return convertToPyTest(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) {
|
||||||
|
teststeps, err := c.prepareTestSteps()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tCase := &hrp.TCase{
|
||||||
|
Config: c.prepareConfig(),
|
||||||
|
TestSteps: teststeps,
|
||||||
|
}
|
||||||
|
err = tCase.MakeCompat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) load() (*CaseHar, error) {
|
||||||
|
har := c.converter.CaseHAR
|
||||||
|
if har == nil {
|
||||||
|
return nil, errors.New("empty har case occurs")
|
||||||
|
}
|
||||||
|
return har, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) prepareConfig() *hrp.TConfig {
|
||||||
|
return hrp.NewConfig("testcase description").
|
||||||
|
SetVerifySSL(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) {
|
||||||
|
har, err := c.load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps []*hrp.TStep
|
||||||
|
for _, entry := range har.Log.Entries {
|
||||||
|
step, err := c.prepareTestStep(&entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
steps = append(steps, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
|
||||||
|
log.Info().
|
||||||
|
Str("method", entry.Request.Method).
|
||||||
|
Str("url", entry.Request.URL).
|
||||||
|
Msg("convert teststep")
|
||||||
|
|
||||||
|
step := &stepFromHAR{
|
||||||
|
TStep: hrp.TStep{
|
||||||
|
Request: &hrp.Request{},
|
||||||
|
Validators: make([]interface{}, 0),
|
||||||
|
},
|
||||||
|
profile: c.converter.Profile,
|
||||||
|
}
|
||||||
|
if err := step.makeRequestMethod(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestURL(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestParams(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestCookies(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestHeaders(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestBody(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeValidate(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &step.TStep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stepFromHAR struct {
|
||||||
|
hrp.TStep
|
||||||
|
profile *Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
|
||||||
|
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
|
||||||
|
u, err := url.Parse(entry.Request.URL)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("make request url failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
|
||||||
|
s.Request.Params = make(map[string]interface{})
|
||||||
|
for _, param := range entry.Request.QueryString {
|
||||||
|
s.Request.Params[param.Name] = param.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
|
||||||
|
// use cookies from har
|
||||||
|
s.Request.Cookies = make(map[string]string)
|
||||||
|
for _, cookie := range entry.Request.Cookies {
|
||||||
|
s.Request.Cookies[cookie.Name] = cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// override all cookies according to the profile
|
||||||
|
if s.profile.Override {
|
||||||
|
s.Request.Cookies = make(map[string]string)
|
||||||
|
}
|
||||||
|
// create or update the cookies according to the profile
|
||||||
|
for k, v := range s.profile.Cookies {
|
||||||
|
s.Request.Cookies[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
|
||||||
|
// use headers from har
|
||||||
|
s.Request.Headers = make(map[string]string)
|
||||||
|
for _, header := range entry.Request.Headers {
|
||||||
|
if strings.EqualFold(header.Name, "cookie") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Request.Headers[header.Name] = header.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// override all headers according to the profile
|
||||||
|
if s.profile.Override {
|
||||||
|
s.Request.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
// create or update the headers according to the profile
|
||||||
|
for k, v := range s.profile.Headers {
|
||||||
|
s.Request.Headers[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
|
||||||
|
mimeType := entry.Request.PostData.MimeType
|
||||||
|
if mimeType == "" {
|
||||||
|
// GET/HEAD/DELETE without body
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST/PUT with body
|
||||||
|
if strings.HasPrefix(mimeType, "application/json") {
|
||||||
|
// post json
|
||||||
|
var body interface{}
|
||||||
|
if entry.Request.PostData.Text == "" {
|
||||||
|
body = nil
|
||||||
|
} else {
|
||||||
|
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("make request body failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Request.Body = body
|
||||||
|
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
|
||||||
|
// post form
|
||||||
|
paramsMap := make(map[string]string)
|
||||||
|
for _, param := range entry.Request.PostData.Params {
|
||||||
|
paramsMap[param.Name] = param.Value
|
||||||
|
}
|
||||||
|
s.Request.Body = paramsMap
|
||||||
|
} else if strings.HasPrefix(mimeType, "text/plain") {
|
||||||
|
// post raw data
|
||||||
|
s.Request.Body = entry.Request.PostData.Text
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromHAR) makeValidate(entry *Entry) error {
|
||||||
|
// make validator for response status code
|
||||||
|
s.Validators = append(s.Validators, hrp.Validator{
|
||||||
|
Check: "status_code",
|
||||||
|
Assert: "equals",
|
||||||
|
Expect: entry.Response.Status,
|
||||||
|
Message: "assert response status code",
|
||||||
|
})
|
||||||
|
|
||||||
|
// make validators for response headers
|
||||||
|
for _, header := range entry.Response.Headers {
|
||||||
|
// assert Content-Type
|
||||||
|
if strings.EqualFold(header.Name, "Content-Type") {
|
||||||
|
s.Validators = append(s.Validators, hrp.Validator{
|
||||||
|
Check: "headers.\"Content-Type\"",
|
||||||
|
Assert: "equals",
|
||||||
|
Expect: header.Value,
|
||||||
|
Message: "assert response header Content-Type",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make validators for response body
|
||||||
|
respBody := entry.Response.Content
|
||||||
|
if respBody.Text == "" {
|
||||||
|
// response body is empty
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(respBody.MimeType, "application/json") {
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
// response body is json
|
||||||
|
if respBody.Encoding == "base64" {
|
||||||
|
// decode base64 text
|
||||||
|
data, err = base64.StdEncoding.DecodeString(respBody.Text)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "decode base64 error")
|
||||||
|
}
|
||||||
|
} else if respBody.Encoding == "" {
|
||||||
|
// no encoding
|
||||||
|
data = []byte(respBody.Text)
|
||||||
|
} else {
|
||||||
|
// other encoding type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// convert to json
|
||||||
|
var body interface{}
|
||||||
|
if err = json.Unmarshal(data, &body); err != nil {
|
||||||
|
return errors.Wrap(err, "json.Unmarshal body error")
|
||||||
|
}
|
||||||
|
jsonBody, ok := body.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("response body is not json, not matched with MimeType")
|
||||||
|
}
|
||||||
|
|
||||||
|
// response body is json
|
||||||
|
keys := make([]string, 0, len(jsonBody))
|
||||||
|
for k := range jsonBody {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
// sort map keys to keep validators in stable order
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
value := jsonBody[key]
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
continue
|
||||||
|
case []interface{}:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
s.Validators = append(s.Validators, hrp.Validator{
|
||||||
|
Check: fmt.Sprintf("body.%s", key),
|
||||||
|
Assert: "equals",
|
||||||
|
Expect: v,
|
||||||
|
Message: fmt.Sprintf("assert response body %s", key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
package har2case
|
package convert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp"
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
harPath = "../../../examples/data/har/demo.har"
|
harPath = "../../../examples/data/har/demo.har"
|
||||||
harPath2 = "../../../examples/data/har/postman-echo.har"
|
harPath2 = "../../../examples/data/har/postman-echo.har"
|
||||||
profilePath = "../../../examples/data/har/profile.yml"
|
harProfileOverridePath = "../../../examples/data/har/profile_override.yml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenJSON(t *testing.T) {
|
var converterHAR = NewConverterHAR(NewTCaseConverter(harPath))
|
||||||
jsonPath, err := NewHAR(harPath).GenJSON()
|
var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2))
|
||||||
|
|
||||||
|
func TestHAR2JSON(t *testing.T) {
|
||||||
|
jsonPath, err := converterHAR.ToJSON()
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -24,8 +27,8 @@ func TestGenJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenYAML(t *testing.T) {
|
func TestHAR2YAML(t *testing.T) {
|
||||||
yamlPath, err := NewHAR(harPath2).GenYAML()
|
yamlPath, err := converterHAR2.ToYAML()
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -35,8 +38,7 @@ func TestGenYAML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadHAR(t *testing.T) {
|
func TestLoadHAR(t *testing.T) {
|
||||||
har := NewHAR(harPath)
|
h, err := converterHAR.load()
|
||||||
h, err := har.load()
|
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -49,28 +51,28 @@ func TestLoadHAR(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadHARWithProfile(t *testing.T) {
|
func TestLoadHARWithProfile(t *testing.T) {
|
||||||
har := NewHAR(harPath)
|
tCaseConverter := NewTCaseConverter(harPath)
|
||||||
har.SetProfile(profilePath)
|
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||||
_, err := har.load()
|
h := NewConverterHAR(tCaseConverter)
|
||||||
|
_, err := h.load()
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !assert.Equal(t,
|
if !assert.Equal(t,
|
||||||
map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"},
|
map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
har.profile["headers"]) {
|
h.converter.Profile.Headers) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
if !assert.Equal(t,
|
if !assert.Equal(t,
|
||||||
map[string]interface{}{"UserName": "debugtalk"},
|
map[string]string{"UserName": "debugtalk"},
|
||||||
har.profile["cookies"]) {
|
h.converter.Profile.Cookies) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeTestCase(t *testing.T) {
|
func TestMakeTestCaseFromHAR(t *testing.T) {
|
||||||
har := NewHAR(harPath)
|
tCase, err := converterHAR.makeTestCase()
|
||||||
tCase, err := har.makeTestCase()
|
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,7 @@ func TestMakeTestCase(t *testing.T) {
|
|||||||
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) {
|
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) {
|
if !assert.Equal(t, map[string]string{"foo1": "HDnY8", "foo2": "12.3"}, tCase.TestSteps[2].Request.Body) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,21 +137,13 @@ func TestMakeTestCase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetFilenameWithoutExtension(t *testing.T) {
|
|
||||||
filename := getFilenameWithoutExtension(harPath2)
|
|
||||||
if !assert.Equal(t, "postman-echo", filename) {
|
|
||||||
t.Fatal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeRequestURL(t *testing.T) {
|
func TestMakeRequestURL(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
URL: "http://127.0.0.1:8080/api/login",
|
URL: "http://127.0.0.1:8080/api/login",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -160,7 +154,6 @@ func TestMakeRequestURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestHeaders(t *testing.T) {
|
func TestMakeRequestHeaders(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -169,7 +162,7 @@ func TestMakeRequestHeaders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -181,9 +174,10 @@ func TestMakeRequestHeaders(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
func TestMakeRequestHeadersWithProfileOverride(t *testing.T) {
|
||||||
har := NewHAR("")
|
tCaseConverter := NewTCaseConverter(harPath)
|
||||||
har.SetProfile(profilePath)
|
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||||
|
h := NewConverterHAR(tCaseConverter)
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -192,7 +186,7 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := h.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -205,7 +199,6 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestCookies(t *testing.T) {
|
func TestMakeRequestCookies(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -215,7 +208,7 @@ func TestMakeRequestCookies(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -228,9 +221,10 @@ func TestMakeRequestCookies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
func TestMakeRequestCookiesWithProfileOverride(t *testing.T) {
|
||||||
har := NewHAR("")
|
tCaseConverter := NewTCaseConverter(harPath)
|
||||||
har.SetProfile(profilePath)
|
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||||
|
h := NewConverterHAR(tCaseConverter)
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -240,7 +234,7 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := h.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -253,7 +247,6 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestDataParams(t *testing.T) {
|
func TestMakeRequestDataParams(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -266,18 +259,17 @@ func TestMakeRequestDataParams(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !assert.Equal(t, "a=1&b=2", step.Request.Body) {
|
if !assert.Equal(t, map[string]string{"a": "1", "b": "2"}, step.Request.Body) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestDataJSON(t *testing.T) {
|
func TestMakeRequestDataJSON(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -287,7 +279,7 @@ func TestMakeRequestDataJSON(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -298,7 +290,6 @@ func TestMakeRequestDataJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeRequestDataTextEmpty(t *testing.T) {
|
func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Request: Request{
|
Request: Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -308,7 +299,7 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -319,7 +310,6 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeValidate(t *testing.T) {
|
func TestMakeValidate(t *testing.T) {
|
||||||
har := NewHAR("")
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Response: Response{
|
Response: Response{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
@@ -335,7 +325,7 @@ func TestMakeValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
step, err := har.prepareTestStep(entry)
|
step, err := converterHAR.prepareTestStep(entry)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
@@ -348,7 +338,8 @@ func TestMakeValidate(t *testing.T) {
|
|||||||
Check: "status_code",
|
Check: "status_code",
|
||||||
Expect: 200,
|
Expect: 200,
|
||||||
Assert: "equals",
|
Assert: "equals",
|
||||||
Message: "assert response status code"}) {
|
Message: "assert response status code",
|
||||||
|
}) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +352,8 @@ func TestMakeValidate(t *testing.T) {
|
|||||||
Check: "headers.\"Content-Type\"",
|
Check: "headers.\"Content-Type\"",
|
||||||
Expect: "application/json; charset=utf-8",
|
Expect: "application/json; charset=utf-8",
|
||||||
Assert: "equals",
|
Assert: "equals",
|
||||||
Message: "assert response header Content-Type"}) {
|
Message: "assert response header Content-Type",
|
||||||
|
}) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +366,8 @@ func TestMakeValidate(t *testing.T) {
|
|||||||
Check: "body.Code",
|
Check: "body.Code",
|
||||||
Expect: float64(200), // TODO
|
Expect: float64(200), // TODO
|
||||||
Assert: "equals",
|
Assert: "equals",
|
||||||
Message: "assert response body Code"}) {
|
Message: "assert response body Code",
|
||||||
|
}) {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
86
hrp/internal/convert/converter_json.go
Normal file
86
hrp/internal/convert/converter_json.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConverterJSON(converter *TCaseConverter) *ConverterJSON {
|
||||||
|
return &ConverterJSON{
|
||||||
|
converter: converter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConverterJSON struct {
|
||||||
|
converter *TCaseConverter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) Struct() *TCaseConverter {
|
||||||
|
return c.converter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) ToJSON() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||||
|
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsonPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) ToYAML() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||||
|
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return yamlPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) ToGoTest() (string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
return "", errors.New("convert from json testcase to gotest scripts is not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) ToPyTest() (string, error) {
|
||||||
|
return convertToPyTest(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) MakePyTestScript() (string, error) {
|
||||||
|
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
||||||
|
python3, err := builtin.EnsurePython3Venv(httprunner)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath)
|
||||||
|
err = builtin.ExecCommand(python3, args...)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return c.converter.genOutputPath(suffixPyTest), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) {
|
||||||
|
tCase, err := makeTestCaseFromJSONYAML(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tCase.MakeCompat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
488
hrp/internal/convert/converter_postman.go
Normal file
488
hrp/internal/convert/converter_postman.go
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== model definition starts here ====================
|
||||||
|
|
||||||
|
/*
|
||||||
|
Postman Collection format reference:
|
||||||
|
https://schema.postman.com/json/collection/v2.0.0/collection.json
|
||||||
|
https://schema.postman.com/json/collection/v2.1.0/collection.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CasePostman represents the postman exported file
|
||||||
|
type CasePostman struct {
|
||||||
|
Info TInfo `json:"info"`
|
||||||
|
Items []TItem `json:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TInfo gives information about the collection
|
||||||
|
type TInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Schema string `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TItem contains the detail information of request and expected responses
|
||||||
|
// item could be defined recursively
|
||||||
|
type TItem struct {
|
||||||
|
Items []TItem `json:"item"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Request TRequest `json:"request"`
|
||||||
|
Responses []TResponse `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TRequest struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Headers []TField `json:"header"`
|
||||||
|
Body TBody `json:"body"`
|
||||||
|
URL TUrl `json:"url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OriginalRequest TRequest `json:"originalRequest"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Headers []TField `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TUrl struct {
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Path []string `json:"path"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Query []TField `json:"query"`
|
||||||
|
Variable []TField `json:"variable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TField struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Src string `json:"src"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TBody struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
FormData []TField `json:"formdata"`
|
||||||
|
URLEncoded []TField `json:"urlencoded"`
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
Options interface{} `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== model definition ends here ====================
|
||||||
|
|
||||||
|
const (
|
||||||
|
enumBodyRaw = "raw"
|
||||||
|
enumBodyUrlEncoded = "urlencoded"
|
||||||
|
enumBodyFormData = "formdata"
|
||||||
|
enumBodyFile = "file"
|
||||||
|
enumBodyGraphQL = "graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enumFieldTypeText = "text"
|
||||||
|
enumFieldTypeFile = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
var contentTypeMap = map[string]string{
|
||||||
|
"text": "text/plain",
|
||||||
|
"javascript": "application/javascript",
|
||||||
|
"json": "application/json",
|
||||||
|
"html": "text/html",
|
||||||
|
"xml": "application/xml",
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConverterPostman(converter *TCaseConverter) *ConverterPostman {
|
||||||
|
return &ConverterPostman{
|
||||||
|
converter: converter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConverterPostman struct {
|
||||||
|
converter *TCaseConverter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) Struct() *TCaseConverter {
|
||||||
|
return c.converter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) ToJSON() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||||
|
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsonPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) ToYAML() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||||
|
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return yamlPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) ToGoTest() (string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
return "", errors.New("convert from postman to gotest scripts is not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) ToPyTest() (string, error) {
|
||||||
|
return convertToPyTest(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) {
|
||||||
|
casePostman, err := c.load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
teststeps, err := c.prepareTestSteps(casePostman)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tCase := &hrp.TCase{
|
||||||
|
Config: c.prepareConfig(casePostman),
|
||||||
|
TestSteps: teststeps,
|
||||||
|
}
|
||||||
|
err = tCase.MakeCompat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) load() (*CasePostman, error) {
|
||||||
|
casePostman := c.converter.CasePostman
|
||||||
|
if casePostman == nil {
|
||||||
|
return nil, errors.New("empty postman case occurs")
|
||||||
|
}
|
||||||
|
return casePostman, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig {
|
||||||
|
return hrp.NewConfig(casePostman.Info.Name).
|
||||||
|
SetVerifySSL(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) {
|
||||||
|
// recursively convert collection items into a list
|
||||||
|
var itemList []TItem
|
||||||
|
for _, item := range casePostman.Items {
|
||||||
|
extractItemList(item, &itemList)
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps []*hrp.TStep
|
||||||
|
for _, item := range itemList {
|
||||||
|
step, err := c.prepareTestStep(&item, steps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
steps = append(steps, step)
|
||||||
|
}
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractItemList(item TItem, itemList *[]TItem) {
|
||||||
|
// current item contains no other items and request is not empty
|
||||||
|
if len(item.Items) == 0 {
|
||||||
|
if !reflect.DeepEqual(item.Request, TRequest{}) {
|
||||||
|
*itemList = append(*itemList, item)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up all items inside
|
||||||
|
for _, i := range item.Items {
|
||||||
|
// append item name
|
||||||
|
i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name)
|
||||||
|
extractItemList(i, itemList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) {
|
||||||
|
log.Info().
|
||||||
|
Str("method", item.Request.Method).
|
||||||
|
Str("url", item.Request.URL.Raw).
|
||||||
|
Msg("convert teststep")
|
||||||
|
|
||||||
|
step := &stepFromPostman{
|
||||||
|
TStep: hrp.TStep{
|
||||||
|
Request: &hrp.Request{},
|
||||||
|
Validators: make([]interface{}, 0),
|
||||||
|
},
|
||||||
|
profile: c.converter.Profile,
|
||||||
|
}
|
||||||
|
if err := step.makeRequestName(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestMethod(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestURL(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestParams(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestHeaders(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestCookies(item); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := step.makeRequestBody(item, steps); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &step.TStep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stepFromPostman struct {
|
||||||
|
hrp.TStep
|
||||||
|
profile *Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRequestName indicates the step name the same as item name
|
||||||
|
func (s *stepFromPostman) makeRequestName(item *TItem) error {
|
||||||
|
s.Name = item.Name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
|
||||||
|
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestURL(item *TItem) error {
|
||||||
|
rawUrl := item.Request.URL.Raw
|
||||||
|
// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
|
||||||
|
for _, field := range item.Request.URL.Variable {
|
||||||
|
pathVar := ":" + field.Key
|
||||||
|
rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parse URL error")
|
||||||
|
}
|
||||||
|
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestParams(item *TItem) error {
|
||||||
|
s.Request.Params = make(map[string]interface{})
|
||||||
|
for _, field := range item.Request.URL.Query {
|
||||||
|
if field.Disabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Request.Params[field.Key] = field.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
|
||||||
|
// headers defined in postman collection
|
||||||
|
s.Request.Headers = make(map[string]string)
|
||||||
|
for _, field := range item.Request.Headers {
|
||||||
|
if field.Disabled || strings.EqualFold(field.Key, "cookie") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Request.Headers[field.Key] = field.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// override all headers according to the profile
|
||||||
|
if s.profile.Override {
|
||||||
|
s.Request.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
// create or update the headers according to the profile
|
||||||
|
for k, v := range s.profile.Headers {
|
||||||
|
s.Request.Headers[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
|
||||||
|
// cookies defined in postman collection
|
||||||
|
s.Request.Cookies = make(map[string]string)
|
||||||
|
for _, field := range item.Request.Headers {
|
||||||
|
if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.parseRequestCookiesMap(field.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// override all cookies according to the profile
|
||||||
|
if s.profile.Override {
|
||||||
|
s.Request.Cookies = make(map[string]string)
|
||||||
|
}
|
||||||
|
// create or update the cookies according to the profile
|
||||||
|
for k, v := range s.profile.Cookies {
|
||||||
|
s.Request.Cookies[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
|
||||||
|
for _, cookie := range strings.Split(cookies, ";") {
|
||||||
|
cookie = strings.TrimSpace(cookie)
|
||||||
|
index := strings.Index(cookie, "=")
|
||||||
|
if index == -1 {
|
||||||
|
log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Request.Cookies[cookie[:index]] = cookie[index+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error {
|
||||||
|
mode := item.Request.Body.Mode
|
||||||
|
if mode == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case enumBodyRaw:
|
||||||
|
return s.makeRequestBodyRaw(item)
|
||||||
|
case enumBodyFormData:
|
||||||
|
return s.makeRequestBodyFormData(item, steps)
|
||||||
|
case enumBodyUrlEncoded:
|
||||||
|
return s.makeRequestBodyUrlEncoded(item)
|
||||||
|
case enumBodyFile, enumBodyGraphQL:
|
||||||
|
return errors.Errorf("unsupported body type: %v", mode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
err = fmt.Errorf("make request body (raw) failed: %v", p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// extract language type, default languageType: text
|
||||||
|
languageType := "text"
|
||||||
|
iOptions := item.Request.Body.Options
|
||||||
|
if iOptions != nil {
|
||||||
|
iLanguage := iOptions.(map[string]interface{})["raw"]
|
||||||
|
if iLanguage != nil {
|
||||||
|
languageType = iLanguage.(map[string]interface{})["language"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make request body and indicate Content-Type
|
||||||
|
rawBody := item.Request.Body.Raw
|
||||||
|
if languageType == "json" {
|
||||||
|
var iBody interface{}
|
||||||
|
err = json.Unmarshal([]byte(rawBody), &iBody)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "make request body (raw -> json) failed")
|
||||||
|
}
|
||||||
|
s.Request.Body = iBody
|
||||||
|
} else {
|
||||||
|
s.Request.Body = rawBody
|
||||||
|
}
|
||||||
|
s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "make request body form-data failed")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
payload := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(payload)
|
||||||
|
for _, field := range item.Request.Body.FormData {
|
||||||
|
if field.Disabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// form data could be text or file
|
||||||
|
if field.Type == enumFieldTypeText {
|
||||||
|
err = writer.WriteField(field.Key, field.Value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if field.Type == enumFieldTypeFile {
|
||||||
|
err = writeFormDataFile(writer, &field)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = writer.Close()
|
||||||
|
s.Request.Body = payload.String()
|
||||||
|
s.Request.Headers["Content-Type"] = writer.FormDataContentType()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFormDataFile(writer *multipart.Writer, field *TField) error {
|
||||||
|
file, err := os.Open(field.Src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
formFile, err := writer.CreateFormFile(field.Key, filepath.Base(field.Src))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(formFile, file)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
|
||||||
|
payloadMap := make(map[string]string)
|
||||||
|
for _, field := range item.Request.Body.URLEncoded {
|
||||||
|
if field.Disabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payloadMap[field.Key] = field.Value
|
||||||
|
}
|
||||||
|
s.Request.Body = payloadMap
|
||||||
|
s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO makeValidate from example response
|
||||||
|
func (s *stepFromPostman) makeValidate(item *TItem) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
159
hrp/internal/convert/converter_postman_test.go
Normal file
159
hrp/internal/convert/converter_postman_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
collectionPath = "../../../examples/data/postman/postman_collection.json"
|
||||||
|
collectionProfileOverridePath = "../../../examples/data/postman/profile_override.yml"
|
||||||
|
collectionProfilePath = "../../../examples/data/postman/profile.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath))
|
||||||
|
|
||||||
|
func TestPostman2JSON(t *testing.T) {
|
||||||
|
jsonPath, err := converterPostman.ToJSON()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.NotEmpty(t, jsonPath) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostman2YAML(t *testing.T) {
|
||||||
|
yamlPath, err := converterPostman.ToYAML()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.NotEmpty(t, yamlPath) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadCollection(t *testing.T) {
|
||||||
|
casePostman, err := converterPostman.load()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeTestCaseFromCollection(t *testing.T) {
|
||||||
|
tCase, err := converterPostman.makeTestCase()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check name
|
||||||
|
if !assert.Equal(t, "postman collection demo", tCase.Config.Name) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check method
|
||||||
|
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check url
|
||||||
|
if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check params
|
||||||
|
if !assert.Equal(t, "v1", tCase.TestSteps[0].Request.Params["k1"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check cookies (pass, postman collection doesn't contains cookies)
|
||||||
|
// check headers
|
||||||
|
if !assert.Contains(t, tCase.TestSteps[1].Request.Headers["Content-Type"], "multipart/form-data") {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.TestSteps[2].Request.Headers["Content-Type"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "application/json", tCase.TestSteps[3].Request.Headers["Content-Type"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "text/plain", tCase.TestSteps[4].Request.Headers["Content-Type"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "HttpRunner", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// check body
|
||||||
|
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.TestSteps[2].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.TestSteps[3].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "have a nice day", tCase.TestSteps[4].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, nil, tCase.TestSteps[5].Request.Body) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeTestCaseWithProfileOverride(t *testing.T) {
|
||||||
|
tCaseConverter := NewTCaseConverter(collectionPath)
|
||||||
|
tCaseConverter.SetProfile(collectionProfileOverridePath)
|
||||||
|
c := NewConverterPostman(tCaseConverter)
|
||||||
|
tCase, err := c.makeTestCase()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
for _, step := range tCase.TestSteps {
|
||||||
|
if step.Request.Method == "GET" && !assert.Len(t, step.Request.Headers, 1) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if step.Request.Method == "POST" && !assert.Len(t, step.Request.Headers, 2) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "all original headers will be overridden", step.Request.Headers["Header1"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Len(t, step.Request.Cookies, 1) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
if !assert.Equal(t, "all original cookies will be overridden", step.Request.Cookies["Cookie1"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeTestCaseWithProfile(t *testing.T) {
|
||||||
|
tCaseConverter := NewTCaseConverter(collectionPath)
|
||||||
|
tCaseConverter.SetProfile(collectionProfilePath)
|
||||||
|
c := NewConverterPostman(tCaseConverter)
|
||||||
|
tCase, err := c.makeTestCase()
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// create cookies Cookie1 indicated in profile
|
||||||
|
if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// update header User-Agent indicated in profile
|
||||||
|
if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
// pass header Connection which is not indicated in profile
|
||||||
|
if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
19
hrp/internal/convert/converter_pytest.go
Normal file
19
hrp/internal/convert/converter_pytest.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convert2PyTestScripts(paths ...string) error {
|
||||||
|
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
||||||
|
python3, err := builtin.EnsurePython3Venv(httprunner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := append([]string{"-m", "httprunner", "make"}, paths...)
|
||||||
|
return builtin.ExecCommand(python3, args...)
|
||||||
|
}
|
||||||
69
hrp/internal/convert/converter_yaml.go
Normal file
69
hrp/internal/convert/converter_yaml.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConverterYAML(converter *TCaseConverter) *ConverterYAML {
|
||||||
|
return &ConverterYAML{
|
||||||
|
converter: converter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConverterYAML struct {
|
||||||
|
converter *TCaseConverter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) Struct() *TCaseConverter {
|
||||||
|
return c.converter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) ToJSON() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||||
|
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jsonPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) ToYAML() (string, error) {
|
||||||
|
testCase, err := c.makeTestCase()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||||
|
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return yamlPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) ToGoTest() (string, error) {
|
||||||
|
//TODO implement me
|
||||||
|
return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) ToPyTest() (string, error) {
|
||||||
|
return convertToPyTest(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) {
|
||||||
|
tCase, err := makeTestCaseFromJSONYAML(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tCase.MakeCompat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tCase, nil
|
||||||
|
}
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Convert2TestScripts(destType string, paths ...string) error {
|
|
||||||
// report event
|
|
||||||
sdk.SendEvent(sdk.EventTracking{
|
|
||||||
Category: "ConvertTests",
|
|
||||||
Action: fmt.Sprintf("hrp convert --%s", destType),
|
|
||||||
})
|
|
||||||
|
|
||||||
if destType == "gotest" {
|
|
||||||
return convert2GoTestScripts(paths...)
|
|
||||||
} else {
|
|
||||||
// default to pytest
|
|
||||||
return convert2PyTestScripts(paths...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert2PyTestScripts(paths ...string) error {
|
|
||||||
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
|
||||||
python3, err := builtin.EnsurePython3Venv(httprunner)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
args := append([]string{"-m", "httprunner", "make"}, paths...)
|
|
||||||
return builtin.ExecCommand(python3, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert2GoTestScripts(paths ...string) error {
|
|
||||||
log.Warn().Msg("convert to gotest scripts is not supported yet")
|
|
||||||
os.Exit(1)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
var testCasePaths []hrp.ITestCase
|
|
||||||
for _, path := range paths {
|
|
||||||
testCasePath := hrp.TestCasePath(path)
|
|
||||||
testCasePaths = append(testCasePaths, &testCasePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases, err := hrp.LoadTestCases(testCasePaths...)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to load testcases")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pytestPaths []string
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
tc := testCase.ToTCase()
|
|
||||||
converter := CaseConverter{
|
|
||||||
TCase: tc,
|
|
||||||
}
|
|
||||||
pytestPath, err := converter.ToPyTest()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).
|
|
||||||
Str("originPath", tc.Config.Path).
|
|
||||||
Msg("convert to pytest failed")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Info().
|
|
||||||
Str("pytestPath", pytestPath).
|
|
||||||
Str("originPath", tc.Config.Path).
|
|
||||||
Msg("convert to pytest success")
|
|
||||||
pytestPaths = append(pytestPaths, pytestPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// format pytest scripts with black
|
|
||||||
python3, err := builtin.EnsurePython3Venv("black")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
args := append([]string{"-m", "black"}, pytestPaths...)
|
|
||||||
return builtin.ExecCommand(python3, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed testcase.tmpl
|
|
||||||
var testcaseTemplate string
|
|
||||||
|
|
||||||
type CaseConverter struct {
|
|
||||||
*hrp.TCase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CaseConverter) ToPyTest() (string, error) {
|
|
||||||
script := convertConfig(c.TCase.Config)
|
|
||||||
println(script)
|
|
||||||
return script, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CaseConverter) ToGoTest() (string, error) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertConfig(config *hrp.TConfig) string {
|
|
||||||
script := fmt.Sprintf("Config('%s')", config.Name)
|
|
||||||
|
|
||||||
if config.Variables != nil {
|
|
||||||
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
|
|
||||||
}
|
|
||||||
if config.BaseURL != "" {
|
|
||||||
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
|
|
||||||
}
|
|
||||||
if config.Export != nil {
|
|
||||||
script += fmt.Sprintf(".export(*%v)", config.Export)
|
|
||||||
}
|
|
||||||
script += fmt.Sprintf(".verify(%v)", config.Verify)
|
|
||||||
|
|
||||||
return script
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# har2case
|
|
||||||
|
|
||||||
Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
package har2case
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
suffixJSON = ".json"
|
|
||||||
suffixYAML = ".yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHAR(path string) *har {
|
|
||||||
return &har{
|
|
||||||
path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type har struct {
|
|
||||||
path string
|
|
||||||
filterStr string
|
|
||||||
excludeStr string
|
|
||||||
profile map[string]interface{}
|
|
||||||
outputDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) SetProfile(path string) {
|
|
||||||
log.Info().Str("path", path).Msg("set profile")
|
|
||||||
h.profile = make(map[string]interface{})
|
|
||||||
err := builtin.LoadFile(path, h.profile)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Str("path", path).
|
|
||||||
Msg("invalid profile format, ignore!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) SetOutputDir(dir string) {
|
|
||||||
log.Info().Str("dir", dir).Msg("set output directory")
|
|
||||||
h.outputDir = dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) GenJSON() (jsonPath string, err error) {
|
|
||||||
event := sdk.EventTracking{
|
|
||||||
Category: "ConvertTests",
|
|
||||||
Action: "hrp har2case --to-json",
|
|
||||||
}
|
|
||||||
// report start event
|
|
||||||
go sdk.SendEvent(event)
|
|
||||||
// report running timing event
|
|
||||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
|
||||||
|
|
||||||
tCase, err := h.makeTestCase()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
jsonPath = h.genOutputPath(suffixJSON)
|
|
||||||
err = builtin.Dump2JSON(tCase, jsonPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) GenYAML() (yamlPath string, err error) {
|
|
||||||
event := sdk.EventTracking{
|
|
||||||
Category: "ConvertTests",
|
|
||||||
Action: "hrp har2case --to-yaml",
|
|
||||||
}
|
|
||||||
// report start event
|
|
||||||
go sdk.SendEvent(event)
|
|
||||||
// report running timing event
|
|
||||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
|
||||||
|
|
||||||
tCase, err := h.makeTestCase()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
yamlPath = h.genOutputPath(suffixYAML)
|
|
||||||
err = builtin.Dump2YAML(tCase, yamlPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) makeTestCase() (*hrp.TCase, error) {
|
|
||||||
teststeps, err := h.prepareTestSteps()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tCase := &hrp.TCase{
|
|
||||||
Config: h.prepareConfig(),
|
|
||||||
TestSteps: teststeps,
|
|
||||||
}
|
|
||||||
return tCase, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) load() (*Har, error) {
|
|
||||||
har := &Har{}
|
|
||||||
err := builtin.LoadFile(h.path, har)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "load har failed")
|
|
||||||
}
|
|
||||||
return har, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) prepareConfig() *hrp.TConfig {
|
|
||||||
return hrp.NewConfig("testcase description").
|
|
||||||
SetVerifySSL(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) prepareTestSteps() ([]*hrp.TStep, error) {
|
|
||||||
har, err := h.load()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var steps []*hrp.TStep
|
|
||||||
for _, entry := range har.Log.Entries {
|
|
||||||
step, err := h.prepareTestStep(&entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
steps = append(steps, step)
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
|
|
||||||
log.Info().
|
|
||||||
Str("method", entry.Request.Method).
|
|
||||||
Str("url", entry.Request.URL).
|
|
||||||
Msg("convert teststep")
|
|
||||||
|
|
||||||
step := &tStep{
|
|
||||||
TStep: hrp.TStep{
|
|
||||||
Request: &hrp.Request{},
|
|
||||||
Validators: make([]interface{}, 0),
|
|
||||||
},
|
|
||||||
profile: h.profile,
|
|
||||||
}
|
|
||||||
if err := step.makeRequestMethod(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeRequestURL(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeRequestParams(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeRequestCookies(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeRequestHeaders(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeRequestBody(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := step.makeValidate(entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &step.TStep, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type tStep struct {
|
|
||||||
hrp.TStep
|
|
||||||
profile map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestMethod(entry *Entry) error {
|
|
||||||
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestURL(entry *Entry) error {
|
|
||||||
u, err := url.Parse(entry.Request.URL)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("make request url failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestParams(entry *Entry) error {
|
|
||||||
s.Request.Params = make(map[string]interface{})
|
|
||||||
for _, param := range entry.Request.QueryString {
|
|
||||||
s.Request.Params[param.Name] = param.Value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestCookies(entry *Entry) error {
|
|
||||||
s.Request.Cookies = make(map[string]string)
|
|
||||||
cookies, ok := s.profile["cookies"]
|
|
||||||
if ok {
|
|
||||||
// use cookies from profile
|
|
||||||
cookies, ok := cookies.(map[string]interface{})
|
|
||||||
if ok {
|
|
||||||
for k, v := range cookies {
|
|
||||||
s.Request.Cookies[k] = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Warn().Interface("cookies", cookies).
|
|
||||||
Msg("cookies from profile is not a map, ignore!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// use cookies from har
|
|
||||||
for _, cookie := range entry.Request.Cookies {
|
|
||||||
s.Request.Cookies[cookie.Name] = cookie.Value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestHeaders(entry *Entry) error {
|
|
||||||
s.Request.Headers = make(map[string]string)
|
|
||||||
headers, ok := s.profile["headers"]
|
|
||||||
if ok {
|
|
||||||
// use headers from profile
|
|
||||||
cookies, ok := headers.(map[string]interface{})
|
|
||||||
if ok {
|
|
||||||
for k, v := range cookies {
|
|
||||||
s.Request.Headers[k] = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Warn().Interface("headers", headers).
|
|
||||||
Msg("headers from profile is not a map, ignore!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// use headers from har
|
|
||||||
for _, header := range entry.Request.Headers {
|
|
||||||
if strings.EqualFold(header.Name, "cookie") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.Request.Headers[header.Name] = header.Value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeRequestBody(entry *Entry) error {
|
|
||||||
mimeType := entry.Request.PostData.MimeType
|
|
||||||
if mimeType == "" {
|
|
||||||
// GET/HEAD/DELETE without body
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST/PUT with body
|
|
||||||
if strings.HasPrefix(mimeType, "application/json") {
|
|
||||||
// post json
|
|
||||||
var body interface{}
|
|
||||||
if entry.Request.PostData.Text == "" {
|
|
||||||
body = nil
|
|
||||||
} else {
|
|
||||||
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("make request body failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.Request.Body = body
|
|
||||||
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
|
|
||||||
// post form
|
|
||||||
var paramsList []string
|
|
||||||
for _, param := range entry.Request.PostData.Params {
|
|
||||||
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
|
|
||||||
}
|
|
||||||
s.Request.Body = strings.Join(paramsList, "&")
|
|
||||||
} else if strings.HasPrefix(mimeType, "text/plain") {
|
|
||||||
// post raw data
|
|
||||||
s.Request.Body = entry.Request.PostData.Text
|
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *tStep) makeValidate(entry *Entry) error {
|
|
||||||
// make validator for response status code
|
|
||||||
s.Validators = append(s.Validators, hrp.Validator{
|
|
||||||
Check: "status_code",
|
|
||||||
Assert: "equals",
|
|
||||||
Expect: entry.Response.Status,
|
|
||||||
Message: "assert response status code",
|
|
||||||
})
|
|
||||||
|
|
||||||
// make validators for response headers
|
|
||||||
for _, header := range entry.Response.Headers {
|
|
||||||
// assert Content-Type
|
|
||||||
if strings.EqualFold(header.Name, "Content-Type") {
|
|
||||||
s.Validators = append(s.Validators, hrp.Validator{
|
|
||||||
Check: "headers.\"Content-Type\"",
|
|
||||||
Assert: "equals",
|
|
||||||
Expect: header.Value,
|
|
||||||
Message: "assert response header Content-Type",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make validators for response body
|
|
||||||
respBody := entry.Response.Content
|
|
||||||
if respBody.Text == "" {
|
|
||||||
// response body is empty
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(respBody.MimeType, "application/json") {
|
|
||||||
var data []byte
|
|
||||||
var err error
|
|
||||||
// response body is json
|
|
||||||
if respBody.Encoding == "base64" {
|
|
||||||
// decode base64 text
|
|
||||||
data, err = base64.StdEncoding.DecodeString(respBody.Text)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "decode base64 error")
|
|
||||||
}
|
|
||||||
} else if respBody.Encoding == "" {
|
|
||||||
// no encoding
|
|
||||||
data = []byte(respBody.Text)
|
|
||||||
} else {
|
|
||||||
// other encoding type
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// convert to json
|
|
||||||
var body interface{}
|
|
||||||
if err = json.Unmarshal(data, &body); err != nil {
|
|
||||||
return errors.Wrap(err, "json.Unmarshal body error")
|
|
||||||
}
|
|
||||||
jsonBody, ok := body.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("response body is not json, not matched with MimeType")
|
|
||||||
}
|
|
||||||
|
|
||||||
// response body is json
|
|
||||||
keys := make([]string, 0, len(jsonBody))
|
|
||||||
for k := range jsonBody {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
// sort map keys to keep validators in stable order
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, key := range keys {
|
|
||||||
value := jsonBody[key]
|
|
||||||
switch v := value.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
continue
|
|
||||||
case []interface{}:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
s.Validators = append(s.Validators, hrp.Validator{
|
|
||||||
Check: fmt.Sprintf("body.%s", key),
|
|
||||||
Assert: "equals",
|
|
||||||
Expect: v,
|
|
||||||
Message: fmt.Sprintf("assert response body %s", key),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *har) genOutputPath(suffix string) string {
|
|
||||||
file := getFilenameWithoutExtension(h.path) + suffix
|
|
||||||
if h.outputDir != "" {
|
|
||||||
return filepath.Join(h.outputDir, file)
|
|
||||||
} else {
|
|
||||||
return filepath.Join(filepath.Dir(h.path), file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFilenameWithoutExtension(path string) string {
|
|
||||||
base := filepath.Base(path)
|
|
||||||
ext := filepath.Ext(base)
|
|
||||||
return base[0 : len(base)-len(ext)]
|
|
||||||
}
|
|
||||||
268
hrp/internal/httpstat/main.go
Normal file
268
hrp/internal/httpstat/main.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
// Package httpstat traces HTTP latency infomation (DNSLookup, TCP Connection and so on) on any golang HTTP request.
|
||||||
|
// It uses `httptrace` package.
|
||||||
|
// Inspired by https://github.com/tcnksm/go-httpstat and https://github.com/davecheney/httpstat
|
||||||
|
package httpstat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpsTemplate = "\n" +
|
||||||
|
` DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer` + "\n" +
|
||||||
|
`[%s | %s | %s | %s | %s ]` + "\n" +
|
||||||
|
` | | | | |` + "\n" +
|
||||||
|
` namelookup:%s | | | |` + "\n" +
|
||||||
|
` connect:%s | | |` + "\n" +
|
||||||
|
` pretransfer:%s | |` + "\n" +
|
||||||
|
` starttransfer:%s |` + "\n" +
|
||||||
|
` total:%s` + "\n\n"
|
||||||
|
|
||||||
|
httpTemplate = "\n" +
|
||||||
|
` DNS Lookup TCP Connection Server Processing Content Transfer` + "\n" +
|
||||||
|
`[ %s | %s | %s | %s ]` + "\n" +
|
||||||
|
` | | | |` + "\n" +
|
||||||
|
` namelookup:%s | | |` + "\n" +
|
||||||
|
` connect:%s | |` + "\n" +
|
||||||
|
` starttransfer:%s |` + "\n" +
|
||||||
|
` total:%s` + "\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fmta(d time.Duration) string {
|
||||||
|
return color.YellowString("%7dms", int(d.Milliseconds()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtb(d time.Duration) string {
|
||||||
|
return color.RedString("%-9s", strconv.Itoa(int(d.Milliseconds()))+"ms")
|
||||||
|
}
|
||||||
|
|
||||||
|
func grayscale(code color.Attribute) func(string, ...interface{}) string {
|
||||||
|
return color.New(code + 232).SprintfFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorize(s string) string {
|
||||||
|
v := strings.Split(s, "\n")
|
||||||
|
v[0] = grayscale(16)(v[0])
|
||||||
|
return strings.Join(v, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(color.Output, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat stores httpstat info.
|
||||||
|
type Stat struct {
|
||||||
|
// The following are duration for each phase
|
||||||
|
// DNSLookup => TCPConnection => TLSHandshake => ServerProcessing => ContentTransfer
|
||||||
|
DNSLookup time.Duration
|
||||||
|
TCPConnection time.Duration
|
||||||
|
TLSHandshake time.Duration
|
||||||
|
ServerProcessing time.Duration
|
||||||
|
ContentTransfer time.Duration // from the first response byte to tansfer done.
|
||||||
|
|
||||||
|
// The followings are timeline of request
|
||||||
|
NameLookup time.Duration // = DNSLookup
|
||||||
|
Connect time.Duration // = DNSLookup + TCPConnection
|
||||||
|
Pretransfer time.Duration // = DNSLookup + TCPConnection + TLSHandshake
|
||||||
|
StartTransfer time.Duration // = DNSLookup + TCPConnection + TLSHandshake + ServerProcessing
|
||||||
|
Total time.Duration // = DNSLookup + TCPConnection + TLSHandshake + ServerProcessing + ContentTransfer
|
||||||
|
|
||||||
|
// internal timelines, including start and finish timestamps of each phase
|
||||||
|
dnsStart time.Time
|
||||||
|
dnsDone time.Time
|
||||||
|
tcpStart time.Time
|
||||||
|
tcpDone time.Time
|
||||||
|
tlsStart time.Time
|
||||||
|
tlsDone time.Time
|
||||||
|
serverStart time.Time
|
||||||
|
serverDone time.Time
|
||||||
|
transferStart time.Time
|
||||||
|
transferDone time.Time // need to be provided from outside
|
||||||
|
|
||||||
|
// isTLS is true when connection seems to use TLS
|
||||||
|
isTLS bool
|
||||||
|
|
||||||
|
// isReused is true when connection is reused (keep-alive)
|
||||||
|
isReused bool
|
||||||
|
|
||||||
|
// https or http
|
||||||
|
schema string
|
||||||
|
|
||||||
|
// connected network info
|
||||||
|
network, addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish sets the time when reading response is done.
|
||||||
|
// This must be called after reading response body.
|
||||||
|
func (s *Stat) Finish() {
|
||||||
|
s.transferDone = time.Now()
|
||||||
|
|
||||||
|
// This means result is empty (it does nothing).
|
||||||
|
// Skip setting value (contentTransfer and total will be zero).
|
||||||
|
if s.dnsStart.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ContentTransfer = s.transferDone.Sub(s.transferStart)
|
||||||
|
s.Total = s.transferDone.Sub(s.dnsStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durations returns all durations and timelines of request latencies
|
||||||
|
func (s *Stat) Durations() map[string]int64 {
|
||||||
|
return map[string]int64{
|
||||||
|
"DNSLookup": s.DNSLookup.Milliseconds(),
|
||||||
|
"TCPConnection": s.TCPConnection.Milliseconds(),
|
||||||
|
"TLSHandshake": s.TLSHandshake.Milliseconds(),
|
||||||
|
"ServerProcessing": s.ServerProcessing.Milliseconds(),
|
||||||
|
"ContentTransfer": s.ContentTransfer.Milliseconds(),
|
||||||
|
"NameLookup": s.NameLookup.Milliseconds(),
|
||||||
|
"Connect": s.Connect.Milliseconds(),
|
||||||
|
"Pretransfer": s.Pretransfer.Milliseconds(),
|
||||||
|
"StartTransfer": s.StartTransfer.Milliseconds(),
|
||||||
|
"Total": s.Total.Milliseconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stat) Print() {
|
||||||
|
if s.network != "" && s.addr != "" {
|
||||||
|
printf("\n%s %s: %s\n",
|
||||||
|
color.CyanString("Connected to"),
|
||||||
|
color.MagentaString(s.network),
|
||||||
|
color.BlueString(s.addr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.schema {
|
||||||
|
case "https":
|
||||||
|
printf(colorize(httpsTemplate),
|
||||||
|
fmta(s.DNSLookup), // dns lookup
|
||||||
|
fmta(s.TCPConnection), // tcp connection
|
||||||
|
fmta(s.TLSHandshake), // tls handshake
|
||||||
|
fmta(s.ServerProcessing), // server processing
|
||||||
|
fmta(s.ContentTransfer), // content transfer
|
||||||
|
fmtb(s.NameLookup), // namelookup
|
||||||
|
fmtb(s.Connect), // connect
|
||||||
|
fmtb(s.Pretransfer), // pretransfer
|
||||||
|
fmtb(s.StartTransfer), // starttransfer
|
||||||
|
fmtb(s.Total), // total
|
||||||
|
)
|
||||||
|
case "http":
|
||||||
|
printf(colorize(httpTemplate),
|
||||||
|
fmta(s.DNSLookup), // dns lookup
|
||||||
|
fmta(s.TCPConnection), // tcp connection
|
||||||
|
fmta(s.ServerProcessing), // server processing
|
||||||
|
fmta(s.ContentTransfer), // content transfer
|
||||||
|
fmtb(s.NameLookup), // namelookup
|
||||||
|
fmtb(s.Connect), // connect
|
||||||
|
fmtb(s.StartTransfer), // starttransfer
|
||||||
|
fmtb(s.Total), // total
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.Info().
|
||||||
|
Interface("httpstat(ms)", s.Durations()).
|
||||||
|
Msg("HTTP latency statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPStat is a wrapper of httptrace.WithClientTrace.
|
||||||
|
// It records the time of each httptrace hooks.
|
||||||
|
func WithHTTPStat(req *http.Request, s *Stat) context.Context {
|
||||||
|
s.schema = req.URL.Scheme
|
||||||
|
return httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
|
||||||
|
DNSStart: func(i httptrace.DNSStartInfo) {
|
||||||
|
s.dnsStart = time.Now()
|
||||||
|
},
|
||||||
|
|
||||||
|
DNSDone: func(i httptrace.DNSDoneInfo) {
|
||||||
|
s.dnsDone = time.Now()
|
||||||
|
|
||||||
|
s.DNSLookup = s.dnsDone.Sub(s.dnsStart)
|
||||||
|
s.NameLookup = s.DNSLookup
|
||||||
|
},
|
||||||
|
|
||||||
|
ConnectStart: func(network, addr string) {
|
||||||
|
s.network = network
|
||||||
|
s.addr = addr
|
||||||
|
|
||||||
|
s.tcpStart = time.Now()
|
||||||
|
|
||||||
|
// When connecting to IP (When no DNS lookup)
|
||||||
|
if s.dnsStart.IsZero() {
|
||||||
|
s.dnsStart = s.tcpStart
|
||||||
|
s.dnsDone = s.tcpStart
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ConnectDone: func(network, addr string, err error) {
|
||||||
|
s.tcpDone = time.Now()
|
||||||
|
s.TCPConnection = s.tcpDone.Sub(s.tcpStart)
|
||||||
|
s.Connect = s.tcpDone.Sub(s.dnsStart)
|
||||||
|
},
|
||||||
|
|
||||||
|
TLSHandshakeStart: func() {
|
||||||
|
s.isTLS = true
|
||||||
|
s.tlsStart = time.Now()
|
||||||
|
},
|
||||||
|
|
||||||
|
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
|
||||||
|
s.tlsDone = time.Now()
|
||||||
|
s.TLSHandshake = s.tlsDone.Sub(s.tlsStart)
|
||||||
|
s.Pretransfer = s.tlsDone.Sub(s.dnsStart)
|
||||||
|
},
|
||||||
|
|
||||||
|
GotConn: func(i httptrace.GotConnInfo) {
|
||||||
|
// Handle when keep alive is used and connection is reused.
|
||||||
|
// DNSStart(Done) and ConnectStart(Done) is skipped
|
||||||
|
if i.Reused {
|
||||||
|
s.isReused = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
WroteRequest: func(info httptrace.WroteRequestInfo) {
|
||||||
|
now := time.Now()
|
||||||
|
s.serverStart = now
|
||||||
|
|
||||||
|
// When client doesn't use DialContext, DNS/TCP/TLS hook is not called.
|
||||||
|
if s.dnsStart.IsZero() && s.tcpStart.IsZero() {
|
||||||
|
s.dnsStart = now
|
||||||
|
s.dnsDone = now
|
||||||
|
s.tcpStart = now
|
||||||
|
s.tcpDone = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// When connection is re-used, DNS/TCP/TLS hook is not called.
|
||||||
|
if s.isReused {
|
||||||
|
s.dnsStart = now
|
||||||
|
s.dnsDone = now
|
||||||
|
s.tcpStart = now
|
||||||
|
s.tcpDone = now
|
||||||
|
s.tlsStart = now
|
||||||
|
s.tlsDone = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.isTLS { // https
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// http
|
||||||
|
s.TLSHandshake = 0
|
||||||
|
s.Pretransfer = s.Connect
|
||||||
|
},
|
||||||
|
|
||||||
|
GotFirstResponseByte: func() {
|
||||||
|
s.serverDone = time.Now()
|
||||||
|
s.ServerProcessing = s.serverDone.Sub(s.serverStart)
|
||||||
|
s.StartTransfer = s.serverDone.Sub(s.dnsStart)
|
||||||
|
s.transferStart = s.serverDone
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,5 +12,6 @@ var (
|
|||||||
MarshalIndent = json.MarshalIndent
|
MarshalIndent = json.MarshalIndent
|
||||||
Unmarshal = json.Unmarshal
|
Unmarshal = json.Unmarshal
|
||||||
NewDecoder = json.NewDecoder
|
NewDecoder = json.NewDecoder
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
Get = json.Get
|
Get = json.Get
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package pytest
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/sdk"
|
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunPytest(args []string) error {
|
func RunPytest(args []string) error {
|
||||||
|
|||||||
@@ -22,4 +22,10 @@ func TestGenDemoExamples(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir = "../../../examples/demo-empty-project"
|
||||||
|
err = CreateScaffold(dir, Empty, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,32 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/shared"
|
"github.com/httprunner/httprunner/v4/hrp"
|
||||||
"github.com/httprunner/httprunner/hrp/internal/builtin"
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/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
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
Empty PluginType = "empty"
|
||||||
Ignore PluginType = "ignore"
|
Ignore PluginType = "ignore"
|
||||||
Py PluginType = "py"
|
Py PluginType = "py"
|
||||||
Go PluginType = "go"
|
Go PluginType = "go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ProjectInfo struct {
|
||||||
|
ProjectName string `json:"project_name,omitempty" yaml:"project_name,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
|
||||||
|
|
||||||
@@ -88,8 +97,20 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectInfo := &ProjectInfo{
|
||||||
|
ProjectName: filepath.Base(projectName),
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -99,10 +120,19 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// create demo testcases
|
// create project testcases
|
||||||
if pluginType == Ignore {
|
if pluginType == Empty {
|
||||||
|
// create empty project
|
||||||
|
err := CopyFile("templates/testcases/demo_empty_request.json",
|
||||||
|
filepath.Join(projectName, "testcases", "requests.json"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if pluginType == Ignore {
|
||||||
|
// create project without funplugin
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -110,18 +140,24 @@ func CreateScaffold(projectName string, pluginType PluginType, force bool) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create project with funplugin
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -150,26 +186,9 @@ func createGoPlugin(projectName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err := CopyFile("templates/plugin/debugtalk.go",
|
err := CopyFile("templates/plugin/debugtalk.go",
|
||||||
filepath.Join(projectName, "plugin", "debugtalk.go"))
|
filepath.Join(projectName, "plugin", hrp.PluginGoSourceFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "copy debugtalk.go failed")
|
||||||
}
|
|
||||||
|
|
||||||
// create go mod
|
|
||||||
if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "plugin"), pluginDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// download plugin dependency
|
|
||||||
// funplugin version should be locked
|
|
||||||
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version)
|
|
||||||
if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// build plugin debugtalk.bin
|
|
||||||
if err := builtin.ExecCommandInDir(exec.Command("go", "build", "-o", filepath.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -179,13 +198,13 @@ func createPythonPlugin(projectName string) error {
|
|||||||
log.Info().Msg("start to create hashicorp python plugin")
|
log.Info().Msg("start to create hashicorp python plugin")
|
||||||
|
|
||||||
// create debugtalk.py
|
// create debugtalk.py
|
||||||
pluginFile := filepath.Join(projectName, "debugtalk.py")
|
pluginFile := filepath.Join(projectName, hrp.PluginPySourceFile)
|
||||||
err := CopyFile("templates/plugin/debugtalk.py", pluginFile)
|
err := CopyFile("templates/plugin/debugtalk.py", pluginFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "copy file failed")
|
return errors.Wrap(err, "copy file failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = builtin.EnsurePython3Venv(fmt.Sprintf("funppy>=%s", shared.Version))
|
_, err = builtin.EnsurePython3Venv("funppy")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
base_url=https://postman-echo.com
|
||||||
USERNAME=debugtalk
|
USERNAME=debugtalk
|
||||||
PASSWORD=123456
|
PASSWORD=123456
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
.env
|
|
||||||
reports/
|
reports/
|
||||||
*.so
|
*.so
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
75
hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py
Normal file
75
hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# NOTE: Generated By hrp v4.1.1, DO NOT EDIT!
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import funppy
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_agent():
|
||||||
|
return "hrp/funppy"
|
||||||
|
|
||||||
|
|
||||||
|
def sleep(n_secs):
|
||||||
|
time.sleep(n_secs)
|
||||||
|
|
||||||
|
|
||||||
|
def sum(*args):
|
||||||
|
result = 0
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sum_ints(*args: List[int]) -> int:
|
||||||
|
result = 0
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sum_two_int(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
|
def sum_two_string(a: str, b: str) -> str:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
|
def sum_strings(*args: List[str]) -> str:
|
||||||
|
result = ""
|
||||||
|
for arg in args:
|
||||||
|
result += arg
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def concatenate(*args: List[str]) -> str:
|
||||||
|
result = ""
|
||||||
|
for arg in args:
|
||||||
|
result += str(arg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def setup_hook_example(name):
|
||||||
|
logging.warning("setup_hook_example")
|
||||||
|
return f"setup_hook_example: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_hook_example(name):
|
||||||
|
logging.warning("teardown_hook_example")
|
||||||
|
return f"teardown_hook_example: {name}"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
funppy.register("get_user_agent", get_user_agent)
|
||||||
|
funppy.register("sleep", sleep)
|
||||||
|
funppy.register("sum", sum)
|
||||||
|
funppy.register("sum_ints", sum_ints)
|
||||||
|
funppy.register("sum_two_int", sum_two_int)
|
||||||
|
funppy.register("sum_two_string", sum_two_string)
|
||||||
|
funppy.register("sum_strings", sum_strings)
|
||||||
|
funppy.register("concatenate", concatenate)
|
||||||
|
funppy.register("setup_hook_example", setup_hook_example)
|
||||||
|
funppy.register("teardown_hook_example", teardown_hook_example)
|
||||||
|
funppy.serve()
|
||||||
@@ -2,8 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/fungo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SumTwoInt(a, b int) int {
|
func SumTwoInt(a, b int) int {
|
||||||
@@ -41,17 +39,6 @@ func TeardownHookExample(args string) string {
|
|||||||
return fmt.Sprintf("step name: %v, teardown...", args)
|
return fmt.Sprintf("step name: %v, teardown...", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVersion() string {
|
func GetUserAgent() string {
|
||||||
return fungo.Version
|
return "hrp/fungo"
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fungo.Register("get_version", GetVersion)
|
|
||||||
fungo.Register("sum_ints", SumInts)
|
|
||||||
fungo.Register("sum_two_int", SumTwoInt)
|
|
||||||
fungo.Register("sum_two", SumTwoInt)
|
|
||||||
fungo.Register("sum", Sum)
|
|
||||||
fungo.Register("setup_hook_example", SetupHookExample)
|
|
||||||
fungo.Register("teardown_hook_example", TeardownHookExample)
|
|
||||||
fungo.Serve()
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user