refactor: move files to hrp
This commit is contained in:
parent
452a1a7536
commit
5888c51386
|
@ -0,0 +1,45 @@
|
|||
name: Release hrp cli binaries
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release hrp cli binaries
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# build and publish in parallel: linux/amd64/windows
|
||||
goos: [linux, windows, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
exclude:
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Release hrp cli binaries
|
||||
uses: wangyoucao577/go-release-action@v1.23
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
project_path: "." # go build ./main.go
|
||||
binary_name: "hrp"
|
||||
ldflags: "-s -w"
|
||||
extra_files: LICENSE README.md docs/CHANGELOG.md
|
||||
post_command: |
|
||||
echo "ASSET_PATH=$INPUT_PROJECT_PATH/$BUILD_ARTIFACTS_FOLDER/$RELEASE_ASSET_FILE" >> $GITHUB_ENV
|
||||
- name: Setup aliyun OSS
|
||||
uses: manyuanrong/setup-ossutil@v2.0
|
||||
with:
|
||||
endpoint: "oss-cn-beijing.aliyuncs.com"
|
||||
access-key-id: ${{ secrets.ALIYUN_ACCESSKEY_ID }}
|
||||
access-key-secret: ${{ secrets.ALIYUN_ACCESSKEY_SECRET }}
|
||||
- name: Upload artifacts to aliyun OSS
|
||||
run: |
|
||||
ossutil cp -rf scripts/install.sh oss://httprunner/
|
||||
ossutil cp -rf ${{ env.ASSET_PATH }} oss://httprunner/
|
||||
- name: Test install.sh
|
||||
run: bash -c "$(curl -ksSL https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh)"
|
|
@ -0,0 +1,73 @@
|
|||
name: Run scaffold for hrp
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
scaffold-with-python-plugin:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.17.x
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build hrp binary
|
||||
run: make build
|
||||
- name: Run start project
|
||||
run: ./output/hrp startproject demo
|
||||
- name: Run demo tests
|
||||
run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml
|
||||
|
||||
scaffold-with-go-plugin:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.17.x
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build hrp binary
|
||||
run: make build
|
||||
- name: Run start project
|
||||
run: ./output/hrp startproject demo --go
|
||||
- name: Run demo tests
|
||||
run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml
|
||||
|
||||
scaffold-without-custom-plugin:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.17.x
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build hrp binary
|
||||
run: make build
|
||||
- name: Run start project
|
||||
run: ./output/hrp startproject demo --ignore-plugin
|
||||
- name: Run demo tests
|
||||
run: ./output/hrp run demo/testcases/demo.json demo/testcases/demo.yaml
|
|
@ -1,4 +1,4 @@
|
|||
name: integration_test
|
||||
name: run smoke tests for httprunner
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -6,9 +6,9 @@ on:
|
|||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
integration_test:
|
||||
smoke-test:
|
||||
|
||||
name: integration_test - ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
name: smoketest - ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
|
@ -1,4 +1,4 @@
|
|||
name: unittest
|
||||
name: Run unittests
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -6,9 +6,7 @@ on:
|
|||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
unittest:
|
||||
|
||||
name: unittest - ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
py-httprunner:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -54,3 +52,34 @@ jobs:
|
|||
flags: unittests
|
||||
# Specify whether or not CI build should fail if Codecov runs into an error during upload
|
||||
fail_ci_if_error: true
|
||||
|
||||
go-hrp:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.16.x
|
||||
- 1.17.x
|
||||
- 1.18.x
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install Python plugin dependencies
|
||||
run: python3 -m pip install funppy
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run coverage
|
||||
run: go test -coverprofile="cover.out" -covermode=atomic -race ./...
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: hrp (HttpRunner+) # User defined upload name. Visible in Codecov UI
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # Repository upload token
|
||||
file: ./cover.out # Path to coverage file to upload
|
||||
flags: unittests # Flag upload to group coverage metrics
|
||||
fail_ci_if_error: true # Specify whether or not CI build should fail if Codecov runs into an error during upload
|
||||
verbose: true
|
||||
|
|
|
@ -1,18 +1,41 @@
|
|||
*.pyc
|
||||
__pycache__
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# system or IDE generated files
|
||||
__debug_bin
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
.pypirc
|
||||
*/tmp/*
|
||||
build/*
|
||||
dist/*
|
||||
*.egg-info
|
||||
.python-version
|
||||
*.bak
|
||||
|
||||
# project output files
|
||||
site/
|
||||
output/
|
||||
logs
|
||||
.coverage
|
||||
site/
|
||||
reports
|
||||
.venv
|
||||
*.xml
|
||||
htmlcov/
|
||||
htmlcov/
|
||||
|
||||
# built plugins
|
||||
debugtalk.bin
|
||||
debugtalk.so
|
||||
|
||||
# python files
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
dist
|
||||
*.egg-info
|
||||
.python-version
|
||||
.pytest_cache
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
SHELL=/usr/bin/env bash
|
||||
|
||||
.DEFAULT_GOAL=help
|
||||
|
||||
.PHONY: test
|
||||
test: ## run unit tests
|
||||
@echo "[info] run unit tests"
|
||||
@echo "go test -race -v ./..."
|
||||
@go test -race -v ./...
|
||||
|
||||
.PHONY: bump
|
||||
bump: ## bump hrp version, e.g. make bump version=4.0.0
|
||||
@echo "[info] bump hrp version"
|
||||
@. scripts/bump_version.sh $(version)
|
||||
|
||||
.PHONY: build
|
||||
build: ## build hrp cli tool
|
||||
@echo "[info] build hrp cli tool"
|
||||
@. scripts/build.sh
|
||||
|
||||
.PHONY: help
|
||||
help: ## print make commands
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
cut -d ":" -f1- | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
@ -0,0 +1,36 @@
|
|||
## hrp
|
||||
|
||||
One-stop solution for HTTP(S) testing.
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
██╗ ██╗████████╗████████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗███╗ ██╗███████╗██████╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║████╗ ██║██╔════╝██╔══██╗
|
||||
███████║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██████╔╝
|
||||
██╔══██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██╔══██╗
|
||||
██║ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║██║ ╚████║███████╗██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
||||
|
||||
hrp (HttpRunner+) aims to be a one-stop solution for HTTP(S) testing, covering API testing,
|
||||
load testing and digital experience monitoring (DEM). Enjoy! ✨ 🚀 ✨
|
||||
|
||||
License: Apache-2.0
|
||||
Website: https://httprunner.com
|
||||
Github: https://github.com/httprunner/httprunner/hrp
|
||||
Copyright 2021 debugtalk
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for hrp
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp boom](hrp_boom.md) - run load test with boomer
|
||||
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
|
||||
* [hrp run](hrp_run.md) - run API test
|
||||
* [hrp startproject](hrp_startproject.md) - create a scaffold project
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Mar-2022
|
|
@ -0,0 +1,44 @@
|
|||
## hrp boom
|
||||
|
||||
run load test with boomer
|
||||
|
||||
### Synopsis
|
||||
|
||||
run yaml/json testcase files for load test
|
||||
|
||||
```
|
||||
hrp boom [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
$ hrp boom demo.json # run specified json testcase file
|
||||
$ hrp boom demo.yaml # run specified yaml testcase file
|
||||
$ hrp boom examples/ # run testcases in specified folder
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--cpu-profile string Enable CPU profiling.
|
||||
--cpu-profile-duration duration CPU profile duration. (default 30s)
|
||||
--disable-compression Disable compression
|
||||
--disable-console-output Disable console output.
|
||||
--disable-keepalive Disable keepalive
|
||||
-h, --help help for boom
|
||||
--loop-count int The specify running cycles for load testing (default -1)
|
||||
--max-rps int Max RPS that boomer can generate, disabled by default.
|
||||
--mem-profile string Enable memory profiling.
|
||||
--mem-profile-duration duration Memory profile duration. (default 30s)
|
||||
--prometheus-gateway string Prometheus Pushgateway url.
|
||||
--request-increase-rate string Request increase rate, disabled by default. (default "-1")
|
||||
--spawn-count int The number of users to spawn for load testing (default 1)
|
||||
--spawn-rate float The rate for spawning users (default 1)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Mar-2022
|
|
@ -0,0 +1,26 @@
|
|||
## hrp har2case
|
||||
|
||||
convert HAR to json/yaml testcase files
|
||||
|
||||
### Synopsis
|
||||
|
||||
convert HAR to json/yaml testcase files
|
||||
|
||||
```
|
||||
hrp har2case $har_path... [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for har2case
|
||||
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||
-j, --to-json convert to JSON format (default true)
|
||||
-y, --to-yaml convert to YAML format
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Mar-2022
|
|
@ -0,0 +1,37 @@
|
|||
## hrp run
|
||||
|
||||
run API test
|
||||
|
||||
### Synopsis
|
||||
|
||||
run yaml/json testcase files for API test
|
||||
|
||||
```
|
||||
hrp run $path... [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
$ hrp run demo.json # run specified json testcase file
|
||||
$ hrp run demo.yaml # run specified yaml testcase file
|
||||
$ hrp run examples/ # run testcases in specified folder
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-c, --continue-on-failure continue running next step when failure occurs
|
||||
-g, --gen-html-report generate html report
|
||||
-h, --help help for run
|
||||
--log-plugin turn on plugin logging
|
||||
--log-requests-off turn off request & response details logging
|
||||
-p, --proxy-url string set proxy url
|
||||
-s, --save-tests save tests summary
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Mar-2022
|
|
@ -0,0 +1,22 @@
|
|||
## hrp startproject
|
||||
|
||||
create a scaffold project
|
||||
|
||||
```
|
||||
hrp startproject $project_name [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--go generate hashicorp go plugin
|
||||
-h, --help help for startproject
|
||||
--ignore-plugin ignore function plugin
|
||||
--py generate hashicorp python plugin (default true)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp](hrp.md) - One-stop solution for HTTP(S) testing.
|
||||
|
||||
###### Auto generated by spf13/cobra on 23-Mar-2022
|
|
@ -0,0 +1 @@
|
|||
# NOTICE: Generated By HttpRunner. DO NOT EDIT!
|
|
@ -0,0 +1,4 @@
|
|||
username,password
|
||||
test1,111111
|
||||
test2,222222
|
||||
test3,333333
|
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "bar1",
|
||||
"foo2": "bar2"
|
||||
},
|
||||
"headers": {
|
||||
"Postman-Token": "ea19464c-ddd4-4724-abe9-5e2b254c2723"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/get?foo1=bar1&foo2=bar2",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
name: ""
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: bar1
|
||||
foo2: bar2
|
||||
headers:
|
||||
Postman-Token: ea19464c-ddd4-4724-abe9-5e2b254c2723
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/get?foo1=bar1&foo2=bar2
|
||||
msg: assert response body url
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Length": "58",
|
||||
"Content-Type": "text/plain",
|
||||
"Postman-Token": "$session_token"
|
||||
},
|
||||
"body": "This is expected to be sent back as part of response body."
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equals",
|
||||
"expect": "This is expected to be sent back as part of response body.",
|
||||
"msg": "assert response body data"
|
||||
},
|
||||
{
|
||||
"check": "body.json",
|
||||
"assert": "equals",
|
||||
"expect": null,
|
||||
"msg": "assert response body json"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/post",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
name: ""
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
Content-Length: "58"
|
||||
Content-Type: text/plain
|
||||
Postman-Token: $session_token
|
||||
body: This is expected to be sent back as part of response body.
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.data
|
||||
assert: equals
|
||||
expect: This is expected to be sent back as part of response body.
|
||||
msg: assert response body data
|
||||
- check: body.json
|
||||
assert: equals
|
||||
expect: null
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/post
|
||||
msg: assert response body url
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"url": "/put",
|
||||
"headers": {
|
||||
"Content-Length": "58",
|
||||
"Content-Type": "text/plain",
|
||||
"Postman-Token": "5d357b2b-0f10-4ded-bc9a-299ebef7a2d5"
|
||||
},
|
||||
"body": "This is expected to be sent back as part of response body."
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equals",
|
||||
"expect": "This is expected to be sent back as part of response body.",
|
||||
"msg": "assert response body data"
|
||||
},
|
||||
{
|
||||
"check": "body.json",
|
||||
"assert": "equals",
|
||||
"expect": null,
|
||||
"msg": "assert response body json"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/put",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
name: ""
|
||||
request:
|
||||
method: PUT
|
||||
url: /put
|
||||
headers:
|
||||
Content-Length: "58"
|
||||
Content-Type: text/plain
|
||||
Postman-Token: 5d357b2b-0f10-4ded-bc9a-299ebef7a2d5
|
||||
body: This is expected to be sent back as part of response body.
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: assert response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: equals
|
||||
expect: application/json; charset=utf-8
|
||||
msg: assert response header Content-Type
|
||||
- check: body.data
|
||||
assert: equals
|
||||
expect: This is expected to be sent back as part of response body.
|
||||
msg: assert response body data
|
||||
- check: body.json
|
||||
assert: equals
|
||||
expect: null
|
||||
msg: assert response body json
|
||||
- check: body.url
|
||||
assert: equals
|
||||
expect: https://postman-echo.com/put
|
||||
msg: assert response body url
|
|
@ -0,0 +1,25 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
// generated by examples/hrp/har/demo.har using HttpRunner v3.1.6
|
||||
var (
|
||||
demoHttpRunnerJSONPath hrp.TestCasePath = "demo_httprunner.json"
|
||||
demoHttpRunnerYAMLPath hrp.TestCasePath = "demo_httprunner.yaml"
|
||||
)
|
||||
|
||||
func TestCompatTestCase(t *testing.T) {
|
||||
err := hrp.NewRunner(t).Run(&demoHttpRunnerJSONPath)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
|
||||
err = hrp.NewRunner(t).Run(&demoHttpRunnerYAMLPath)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import logging
|
||||
from typing import List
|
||||
|
||||
import funppy
|
||||
|
||||
|
||||
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("sum", sum)
|
||||
funppy.register("sum_ints", sum_ints)
|
||||
funppy.register("concatenate", concatenate)
|
||||
funppy.register("sum_two_int", 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()
|
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "demo with complex mechanisms",
|
||||
"base_url": "https://postman-echo.com",
|
||||
"variables": {
|
||||
"a": "${sum(10, 2.3)}",
|
||||
"b": 3.45,
|
||||
"n": "${sum_ints(1, 2, 2)}",
|
||||
"varFoo1": "${gen_random_string($n)}",
|
||||
"varFoo2": "${max($a, $b)}"
|
||||
}
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "transaction 1 start",
|
||||
"transaction": {
|
||||
"name": "tran1",
|
||||
"type": "start"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "$varFoo2"
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunnerPlus"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"b": 34.5,
|
||||
"n": 3,
|
||||
"name": "get with params",
|
||||
"varFoo2": "${max($a, $b)}"
|
||||
},
|
||||
"setup_hooks": [
|
||||
"${setup_hook_example($name)}"
|
||||
],
|
||||
"teardown_hooks": [
|
||||
"${teardown_hook_example($name)}"
|
||||
],
|
||||
"extract": {
|
||||
"varFoo1": "body.args.foo1"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "startswith",
|
||||
"expect": "application/json"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "$varFoo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo2",
|
||||
"assert": "equals",
|
||||
"expect": "34.5",
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "transaction 1 end",
|
||||
"transaction": {
|
||||
"name": "tran1",
|
||||
"type": "end"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "post json data",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"body": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "${max($a, $b)}"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo2",
|
||||
"assert": "equals",
|
||||
"expect": 12.3,
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "post form data",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
},
|
||||
"body": {
|
||||
"foo1": "$varFoo1",
|
||||
"foo2": "${max($a, $b)}",
|
||||
"time": "${get_timestamp()}"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"varTime": "body.form.time"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 5,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.form.foo2",
|
||||
"assert": "equals",
|
||||
"expect": "12.3",
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get with timestamp",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"time": "$varTime"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "body.args.time",
|
||||
"assert": "length_equals",
|
||||
"expect": 13,
|
||||
"msg": "check extracted var timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
config:
|
||||
name: demo with complex mechanisms
|
||||
base_url: https://postman-echo.com
|
||||
variables:
|
||||
a: ${sum(10, 2.3)}
|
||||
b: 3.45
|
||||
"n": ${sum_ints(1, 2, 2)}
|
||||
varFoo1: ${gen_random_string($n)}
|
||||
varFoo2: ${max($a, $b)}
|
||||
teststeps:
|
||||
- name: transaction 1 start
|
||||
transaction:
|
||||
name: tran1
|
||||
type: start
|
||||
- name: get with params
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: $varFoo1
|
||||
foo2: $varFoo2
|
||||
headers:
|
||||
User-Agent: HttpRunnerPlus
|
||||
variables:
|
||||
b: 34.5
|
||||
"n": 3
|
||||
name: get with params
|
||||
varFoo2: ${max($a, $b)}
|
||||
setup_hooks:
|
||||
- ${setup_hook_example($name)}
|
||||
teardown_hooks:
|
||||
- ${teardown_hook_example($name)}
|
||||
extract:
|
||||
varFoo1: body.args.foo1
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check response status code
|
||||
- check: headers."Content-Type"
|
||||
assert: startswith
|
||||
expect: application/json
|
||||
- check: body.args.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: $varFoo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.args.foo2
|
||||
assert: equals
|
||||
expect: "34.5"
|
||||
msg: check args foo2
|
||||
- name: transaction 1 end
|
||||
transaction:
|
||||
name: tran1
|
||||
type: end
|
||||
- name: post json data
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
body:
|
||||
foo1: $varFoo1
|
||||
foo2: ${max($a, $b)}
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- check: body.json.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.json.foo2
|
||||
assert: equals
|
||||
expect: 12.3
|
||||
msg: check args foo2
|
||||
- name: post form data
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
body:
|
||||
foo1: $varFoo1
|
||||
foo2: ${max($a, $b)}
|
||||
time: ${get_timestamp()}
|
||||
extract:
|
||||
varTime: body.form.time
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- check: body.form.foo1
|
||||
assert: length_equals
|
||||
expect: 5
|
||||
msg: check args foo1
|
||||
- check: body.form.foo2
|
||||
assert: equals
|
||||
expect: "12.3"
|
||||
msg: check args foo2
|
||||
- name: get with timestamp
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
time: $varTime
|
||||
validate:
|
||||
- check: body.args.time
|
||||
assert: length_equals
|
||||
expect: 13
|
||||
msg: check extracted var timestamp
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "testcase description",
|
||||
"variables": {},
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "/get",
|
||||
"request": {
|
||||
"url": "https://postman-echo.com/get",
|
||||
"params": {
|
||||
"foo1": "HDnY8",
|
||||
"foo2": "34.5"
|
||||
},
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
"Accept-Encoding": "gzip"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"eq": [
|
||||
"status_code",
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"headers.Content-Type",
|
||||
"application/json; charset=utf-8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"body.url",
|
||||
"https://postman-echo.com/get?foo1=HDnY8&foo2=34.5"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "/post",
|
||||
"request": {
|
||||
"url": "https://postman-echo.com/post",
|
||||
"method": "POST",
|
||||
"cookies": {
|
||||
"sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
|
||||
},
|
||||
"headers": {
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "Go-http-client/1.1",
|
||||
"Content-Length": "28",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Cookie": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk",
|
||||
"Accept-Encoding": "gzip"
|
||||
},
|
||||
"json": {
|
||||
"foo1": "HDnY8",
|
||||
"foo2": 12.3
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"eq": [
|
||||
"status_code",
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"headers.Content-Type",
|
||||
"application/json; charset=utf-8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"body.url",
|
||||
"https://postman-echo.com/post"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "/post",
|
||||
"request": {
|
||||
"url": "https://postman-echo.com/post",
|
||||
"method": "POST",
|
||||
"cookies": {
|
||||
"sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
|
||||
},
|
||||
"headers": {
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "Go-http-client/1.1",
|
||||
"Content-Length": "20",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Cookie": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw",
|
||||
"Accept-Encoding": "gzip"
|
||||
},
|
||||
"data": {
|
||||
"foo1": "HDnY8",
|
||||
"foo2": "12.3"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"eq": [
|
||||
"status_code",
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"headers.Content-Type",
|
||||
"application/json; charset=utf-8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"body.data",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"body.url",
|
||||
"https://postman-echo.com/post"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
config:
|
||||
name: testcase description
|
||||
variables: {}
|
||||
verify: false
|
||||
teststeps:
|
||||
- name: /get
|
||||
request:
|
||||
headers:
|
||||
Accept-Encoding: gzip
|
||||
Host: postman-echo.com
|
||||
User-Agent: HttpRunnerPlus
|
||||
method: GET
|
||||
params:
|
||||
foo1: HDnY8
|
||||
foo2: '34.5'
|
||||
url: https://postman-echo.com/get
|
||||
validate:
|
||||
- eq:
|
||||
- status_code
|
||||
- 200
|
||||
- eq:
|
||||
- headers.Content-Type
|
||||
- application/json; charset=utf-8
|
||||
- eq:
|
||||
- body.url
|
||||
- https://postman-echo.com/get?foo1=HDnY8&foo2=34.5
|
||||
- name: /post
|
||||
request:
|
||||
cookies:
|
||||
sails.sid: s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk
|
||||
headers:
|
||||
Accept-Encoding: gzip
|
||||
Content-Length: '28'
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Cookie: sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk
|
||||
Host: postman-echo.com
|
||||
User-Agent: Go-http-client/1.1
|
||||
json:
|
||||
foo1: HDnY8
|
||||
foo2: 12.3
|
||||
method: POST
|
||||
url: https://postman-echo.com/post
|
||||
validate:
|
||||
- eq:
|
||||
- status_code
|
||||
- 200
|
||||
- eq:
|
||||
- headers.Content-Type
|
||||
- application/json; charset=utf-8
|
||||
- eq:
|
||||
- body.url
|
||||
- https://postman-echo.com/post
|
||||
- name: /post
|
||||
request:
|
||||
cookies:
|
||||
sails.sid: s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw
|
||||
data:
|
||||
foo1: HDnY8
|
||||
foo2: '12.3'
|
||||
headers:
|
||||
Accept-Encoding: gzip
|
||||
Content-Length: '20'
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Cookie: sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw
|
||||
Host: postman-echo.com
|
||||
User-Agent: Go-http-client/1.1
|
||||
method: POST
|
||||
url: https://postman-echo.com/post
|
||||
validate:
|
||||
- eq:
|
||||
- status_code
|
||||
- 200
|
||||
- eq:
|
||||
- headers.Content-Type
|
||||
- application/json; charset=utf-8
|
||||
- eq:
|
||||
- body.data
|
||||
- ''
|
||||
- eq:
|
||||
- body.url
|
||||
- https://postman-echo.com/post
|
|
@ -0,0 +1,63 @@
|
|||
# NOTE: Generated By HttpRunner v3.1.6
|
||||
# FROM: hrp/examples/demo.json
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseDemo(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("demo with complex mechanisms")
|
||||
.variables(
|
||||
**{
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
"n": 5,
|
||||
"varFoo1": "${gen_random_string($n)}",
|
||||
"varFoo2": "${max($a, $b)}",
|
||||
}
|
||||
)
|
||||
.base_url("https://postman-echo.com")
|
||||
)
|
||||
|
||||
teststeps = [
|
||||
Step(
|
||||
RunRequest("get with params")
|
||||
.with_variables(**{"b": 34.5, "n": 3, "varFoo2": "${max($a, $b)}"})
|
||||
.get("/get")
|
||||
.with_params(**{"foo1": "$varFoo1", "foo2": "$varFoo2"})
|
||||
.with_headers(**{"User-Agent": "HttpRunnerPlus"})
|
||||
.extract()
|
||||
.with_jmespath("body.args.foo1", "varFoo1")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal('headers."Content-Type"', "application/json")
|
||||
.assert_equal("body.args.foo1", 5)
|
||||
.assert_equal("$varFoo1", 5)
|
||||
.assert_equal("body.args.foo2", "34.5")
|
||||
),
|
||||
Step(
|
||||
RunRequest("post json data")
|
||||
.post("/post")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.json.foo1", 5)
|
||||
.assert_equal("body.json.foo2", 12.3)
|
||||
),
|
||||
Step(
|
||||
RunRequest("post form data")
|
||||
.post("/post")
|
||||
.with_headers(
|
||||
**{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
)
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_equal("body.form.foo1", 5)
|
||||
.assert_equal("body.form.foo2", "12.3")
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseDemo().test_start()
|
|
@ -0,0 +1,84 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
// reference extracted variables for validation in the same step
|
||||
func TestCaseExtractStepSingle(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with variables").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
"expectedStatusCode": 200,
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Extract().
|
||||
WithJmesPath("status_code", "statusCode").
|
||||
WithJmesPath("headers.\"Content-Type\"", "contentType").
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("$statusCode", "$expectedStatusCode", "check status code"). // assert with extracted variable from current step
|
||||
AssertEqual("$contentType", "application/json; charset=utf-8", "check header Content-Type"). // assert with extracted variable from current step
|
||||
AssertEqual("$varFoo1", "bar1", "check args foo1"). // assert with extracted variable from current step
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// reference extracted variables from previous step
|
||||
func TestCaseExtractStepAssociation(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with variables").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Extract().
|
||||
WithJmesPath("status_code", "statusCode").
|
||||
WithJmesPath("headers.\"Content-Type\"", "contentType").
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("$statusCode", 200, "check status code").
|
||||
AssertEqual("$contentType", "application/json; charset=utf-8", "check header Content-Type").
|
||||
AssertEqual("$varFoo1", "bar1", "check args foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
hrp.NewStep("post json data").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
WithBody(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", "$statusCode", "check status code"). // assert with extracted variable from previous step
|
||||
AssertEqual("$varFoo1", "bar1", "check json foo1"). // assert with extracted variable from previous step
|
||||
AssertEqual("body.json.foo2", "bar2", "check json foo2"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
func TestCaseCallFunction(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with functions").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
}).
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}", "foo3": "Foo3"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.args.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.args.foo2", "12.3", "check args foo2").
|
||||
AssertTypeMatch("body.args.foo3", "str", "check args foo3 is type string").
|
||||
AssertStringEqual("body.args.foo3", "foo3", "check args foo3 case-insensitivity").
|
||||
AssertContains("body.args.foo3", "Foo", "check contains ").
|
||||
AssertContainedBy("body.args.foo3", "this is Foo3 test", "check contained by"), // notice: request params value will be converted to string
|
||||
hrp.NewStep("post json data with functions").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
WithBody(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.json.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.json.foo2", 12.3, "check args foo2"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": {
|
||||
"name": "Charles Proxy",
|
||||
"version": "4.6.1"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"startedDateTime": "2021-10-15T20:29:14.396+08:00",
|
||||
"time": 1528,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://postman-echo.com/get?foo1=HDnY8&foo2=34.5",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Host",
|
||||
"value": "postman-echo.com"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "HttpRunnerPlus"
|
||||
},
|
||||
{
|
||||
"name": "Accept-Encoding",
|
||||
"value": "gzip"
|
||||
}
|
||||
],
|
||||
"queryString": [
|
||||
{
|
||||
"name": "foo1",
|
||||
"value": "HDnY8"
|
||||
},
|
||||
{
|
||||
"name": "foo2",
|
||||
"value": "34.5"
|
||||
}
|
||||
],
|
||||
"headersSize": 113,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"_charlesStatus": "COMPLETE",
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [
|
||||
{
|
||||
"name": "sails.sid",
|
||||
"value": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk",
|
||||
"path": "/",
|
||||
"domain": null,
|
||||
"expires": null,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"comment": null,
|
||||
"_maxAge": null
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Fri, 15 Oct 2021 12:29:15 GMT"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Length",
|
||||
"value": "300"
|
||||
},
|
||||
{
|
||||
"name": "ETag",
|
||||
"value": "W/\"12c-1pyB4v4mv3hdBoU+8cUmx4p37qI\""
|
||||
},
|
||||
{
|
||||
"name": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
},
|
||||
{
|
||||
"name": "set-cookie",
|
||||
"value": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk; Path=/; HttpOnly"
|
||||
},
|
||||
{
|
||||
"name": "Connection",
|
||||
"value": "keep-alive"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 300,
|
||||
"mimeType": "application/json; charset=utf-8",
|
||||
"text": "eyJhcmdzIjp7ImZvbzEiOiJIRG5ZOCIsImZvbzIiOiIzNC41In0sImhlYWRlcnMiOnsieC1mb3J3YXJkZWQtcHJvdG8iOiJodHRwcyIsIngtZm9yd2FyZGVkLXBvcnQiOiI0NDMiLCJob3N0IjoicG9zdG1hbi1lY2hvLmNvbSIsIngtYW16bi10cmFjZS1pZCI6IlJvb3Q9MS02MTY5NzQxYi01YjgyNTRjZTZjZThlNTU2NTRiNzc3MmQiLCJ1c2VyLWFnZW50IjoiSHR0cEJvb21lciIsImFjY2VwdC1lbmNvZGluZyI6Imd6aXAifSwidXJsIjoiaHR0cHM6Ly9wb3N0bWFuLWVjaG8uY29tL2dldD9mb28xPUhEblk4JmZvbzI9MzQuNSJ9",
|
||||
"encoding": "base64"
|
||||
},
|
||||
"redirectURL": null,
|
||||
"headersSize": 0,
|
||||
"bodySize": 300
|
||||
},
|
||||
"serverIPAddress": "44.193.31.23",
|
||||
"cache": {},
|
||||
"timings": {
|
||||
"dns": 105,
|
||||
"connect": 1108,
|
||||
"ssl": 721,
|
||||
"send": 1,
|
||||
"wait": 312,
|
||||
"receive": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2021-10-15T20:29:16.120+08:00",
|
||||
"time": 306,
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [
|
||||
{
|
||||
"name": "sails.sid",
|
||||
"value": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Host",
|
||||
"value": "postman-echo.com"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Go-http-client/1.1"
|
||||
},
|
||||
{
|
||||
"name": "Content-Length",
|
||||
"value": "28"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json; charset=UTF-8"
|
||||
},
|
||||
{
|
||||
"name": "Cookie",
|
||||
"value": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
|
||||
},
|
||||
{
|
||||
"name": "Accept-Encoding",
|
||||
"value": "gzip"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"postData": {
|
||||
"mimeType": "application/json; charset=UTF-8",
|
||||
"text": "{\"foo1\":\"HDnY8\",\"foo2\":12.3}"
|
||||
},
|
||||
"headersSize": 269,
|
||||
"bodySize": 28
|
||||
},
|
||||
"response": {
|
||||
"_charlesStatus": "COMPLETE",
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [
|
||||
{
|
||||
"name": "sails.sid",
|
||||
"value": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw",
|
||||
"path": "/",
|
||||
"domain": null,
|
||||
"expires": null,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"comment": null,
|
||||
"_maxAge": null
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Fri, 15 Oct 2021 12:29:16 GMT"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Length",
|
||||
"value": "526"
|
||||
},
|
||||
{
|
||||
"name": "ETag",
|
||||
"value": "W/\"20e-aXqJ0H6Q30sU41c/D7asB+yXWeQ\""
|
||||
},
|
||||
{
|
||||
"name": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
},
|
||||
{
|
||||
"name": "set-cookie",
|
||||
"value": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw; Path=/; HttpOnly"
|
||||
},
|
||||
{
|
||||
"name": "Connection",
|
||||
"value": "keep-alive"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 526,
|
||||
"mimeType": "application/json; charset=utf-8",
|
||||
"text": "eyJhcmdzIjp7fSwiZGF0YSI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoxMi4zfSwiZmlsZXMiOnt9LCJmb3JtIjp7fSwiaGVhZGVycyI6eyJ4LWZvcndhcmRlZC1wcm90byI6Imh0dHBzIiwieC1mb3J3YXJkZWQtcG9ydCI6IjQ0MyIsImhvc3QiOiJwb3N0bWFuLWVjaG8uY29tIiwieC1hbXpuLXRyYWNlLWlkIjoiUm9vdD0xLTYxNjk3NDFjLTIxN2RiMGI3MWFkYjgwYmQ3ODUxOTI2OCIsImNvbnRlbnQtbGVuZ3RoIjoiMjgiLCJ1c2VyLWFnZW50IjoiR28taHR0cC1jbGllbnQvMS4xIiwiY29udGVudC10eXBlIjoiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD1VVEYtOCIsImNvb2tpZSI6InNhaWxzLnNpZD1zJTNBel9McGdsa0t4VHZKX2VIVlVINlY2N2RyS3AwQUdXVy0uUGlkYWJhWE9uYXRMUlA0N2hWeXFxZXBsNkJkcnBFUXpSbEpRWHRiSWl3ayIsImFjY2VwdC1lbmNvZGluZyI6Imd6aXAifSwianNvbiI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoxMi4zfSwidXJsIjoiaHR0cHM6Ly9wb3N0bWFuLWVjaG8uY29tL3Bvc3QifQ==",
|
||||
"encoding": "base64"
|
||||
},
|
||||
"redirectURL": null,
|
||||
"headersSize": 0,
|
||||
"bodySize": 526
|
||||
},
|
||||
"serverIPAddress": "44.193.31.23",
|
||||
"cache": {},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 1,
|
||||
"wait": 304,
|
||||
"receive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2021-10-15T20:29:16.427+08:00",
|
||||
"time": 305,
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [
|
||||
{
|
||||
"name": "sails.sid",
|
||||
"value": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Host",
|
||||
"value": "postman-echo.com"
|
||||
},
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Go-http-client/1.1"
|
||||
},
|
||||
{
|
||||
"name": "Content-Length",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
},
|
||||
{
|
||||
"name": "Cookie",
|
||||
"value": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
|
||||
},
|
||||
{
|
||||
"name": "Accept-Encoding",
|
||||
"value": "gzip"
|
||||
}
|
||||
],
|
||||
"queryString": [],
|
||||
"postData": {
|
||||
"mimeType": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"params": [
|
||||
{
|
||||
"name": "foo1",
|
||||
"value": "HDnY8"
|
||||
},
|
||||
{
|
||||
"name": "foo2",
|
||||
"value": "12.3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headersSize": 290,
|
||||
"bodySize": 20
|
||||
},
|
||||
"response": {
|
||||
"_charlesStatus": "COMPLETE",
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [
|
||||
{
|
||||
"name": "sails.sid",
|
||||
"value": "s%3AMp2gGgeCCDM4sRS_MfL1q-hAkL3bAk84.9XT7TTW8QzueQqtQ6bQM%2BgHqiUBbkJSfgM5CbfhFreQ",
|
||||
"path": "/",
|
||||
"domain": null,
|
||||
"expires": null,
|
||||
"httpOnly": true,
|
||||
"secure": false,
|
||||
"comment": null,
|
||||
"_maxAge": null
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "Date",
|
||||
"value": "Fri, 15 Oct 2021 12:29:16 GMT"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"name": "Content-Length",
|
||||
"value": "551"
|
||||
},
|
||||
{
|
||||
"name": "ETag",
|
||||
"value": "W/\"227-micuvGYwtEZN542D1sTL0hAZaRs\""
|
||||
},
|
||||
{
|
||||
"name": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
},
|
||||
{
|
||||
"name": "set-cookie",
|
||||
"value": "sails.sid=s%3AMp2gGgeCCDM4sRS_MfL1q-hAkL3bAk84.9XT7TTW8QzueQqtQ6bQM%2BgHqiUBbkJSfgM5CbfhFreQ; Path=/; HttpOnly"
|
||||
},
|
||||
{
|
||||
"name": "Connection",
|
||||
"value": "keep-alive"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"size": 551,
|
||||
"mimeType": "application/json; charset=utf-8",
|
||||
"text": "eyJhcmdzIjp7fSwiZGF0YSI6IiIsImZpbGVzIjp7fSwiZm9ybSI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoiMTIuMyJ9LCJoZWFkZXJzIjp7IngtZm9yd2FyZGVkLXByb3RvIjoiaHR0cHMiLCJ4LWZvcndhcmRlZC1wb3J0IjoiNDQzIiwiaG9zdCI6InBvc3RtYW4tZWNoby5jb20iLCJ4LWFtem4tdHJhY2UtaWQiOiJSb290PTEtNjE2OTc0MWMtNWI5ZDEyMWI2N2FlZTI0MTUyMmQzMjE2IiwiY29udGVudC1sZW5ndGgiOiIyMCIsInVzZXItYWdlbnQiOiJHby1odHRwLWNsaWVudC8xLjEiLCJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQ7IGNoYXJzZXQ9VVRGLTgiLCJjb29raWUiOiJzYWlscy5zaWQ9cyUzQVM1ZTd3MHpRMHhBc0N3aDlMOFQ2UjdRTFlDTzdfZ3RELnI4JTJCMnc5SVdxRUlmdVZrclpqbnh6bTJ4QURJazM0ektBV1hSUGFwciUyRkF3IiwiYWNjZXB0LWVuY29kaW5nIjoiZ3ppcCJ9LCJqc29uIjp7ImZvbzEiOiJIRG5ZOCIsImZvbzIiOiIxMi4zIn0sInVybCI6Imh0dHBzOi8vcG9zdG1hbi1lY2hvLmNvbS9wb3N0In0=",
|
||||
"encoding": "base64"
|
||||
},
|
||||
"redirectURL": null,
|
||||
"headersSize": 0,
|
||||
"bodySize": 551
|
||||
},
|
||||
"serverIPAddress": "44.193.31.23",
|
||||
"cache": {},
|
||||
"timings": {
|
||||
"dns": -1,
|
||||
"connect": -1,
|
||||
"ssl": -1,
|
||||
"send": 0,
|
||||
"wait": 303,
|
||||
"receive": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "testcase description",
|
||||
"variables": {},
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "/get",
|
||||
"request": {
|
||||
"url": "http://httpbin.org/get",
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Host": "httpbin.org",
|
||||
"Connection": "keep-alive",
|
||||
"accept": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36 Edg/98.0.1108.50",
|
||||
"Referer": "http://httpbin.org/",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.origin",
|
||||
"assert": "equals",
|
||||
"expect": "117.176.133.109",
|
||||
"msg": "assert response body origin"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "http://httpbin.org/get",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "request methods testcase: validate with parameters",
|
||||
"parameters": {
|
||||
"user_agent": [
|
||||
"iOS/10.1",
|
||||
"iOS/10.2"
|
||||
],
|
||||
"username-password": "${parameterize(examples/hrp/account.csv)}"
|
||||
},
|
||||
"parameters_setting": {
|
||||
"strategy": {
|
||||
"user_agent": "sequential",
|
||||
"username-password": "random"
|
||||
},
|
||||
"iteration": 6
|
||||
},
|
||||
"variables": {
|
||||
"app_version": "v1",
|
||||
"user_agent": "iOS/10.3"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "get with params",
|
||||
"variables": {
|
||||
"foo1": "$username",
|
||||
"foo2": "$password",
|
||||
"foo3": "$user_agent"
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "$foo1",
|
||||
"foo2": "$foo2",
|
||||
"foo3": "$foo3"
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "$user_agent,$app_version"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.args.foo3",
|
||||
"assert": "not_equal",
|
||||
"expect": "iOS/10.3",
|
||||
"msg": "check app version"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
config:
|
||||
name: "request methods testcase: validate with parameters"
|
||||
parameters:
|
||||
user_agent: [ "iOS/10.1", "iOS/10.2" ]
|
||||
username-password: ${parameterize(examples/hrp/account.csv)}
|
||||
parameters_setting:
|
||||
strategy:
|
||||
user_agent: "sequential"
|
||||
username-password: "random"
|
||||
iteration: 6
|
||||
variables:
|
||||
app_version: v1
|
||||
user_agent: iOS/10.3
|
||||
base_url: "https://postman-echo.com"
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
- name: get with params
|
||||
variables:
|
||||
foo1: $username
|
||||
foo2: $password
|
||||
foo3: $user_agent
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
params:
|
||||
foo1: $foo1
|
||||
foo2: $foo2
|
||||
foo3: $foo3
|
||||
headers:
|
||||
User-Agent: $user_agent,$app_version
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- check: body.args.foo3
|
||||
assert: not_equal
|
||||
expect: iOS/10.3
|
||||
msg: check app version
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Println("plugin init function called")
|
||||
}
|
||||
|
||||
func SumTwoInt(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func SumInts(args ...int) int {
|
||||
var sum int
|
||||
for _, arg := range args {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func Sum(args ...interface{}) (interface{}, error) {
|
||||
var sum float64
|
||||
for _, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case int:
|
||||
sum += float64(v)
|
||||
case float64:
|
||||
sum += v
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type: %T", arg)
|
||||
}
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func SumTwoString(a, b string) string {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func SumStrings(s ...string) string {
|
||||
var sum string
|
||||
for _, arg := range s {
|
||||
sum += arg
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func Concatenate(args ...interface{}) (interface{}, error) {
|
||||
var result string
|
||||
for _, arg := range args {
|
||||
result += fmt.Sprintf("%v", arg)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SetupHookExample(args string) string {
|
||||
return fmt.Sprintf("step name: %v, setup...", args)
|
||||
}
|
||||
|
||||
func TeardownHookExample(args string) string {
|
||||
return fmt.Sprintf("step name: %v, teardown...", args)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/httprunner/funplugin/fungo"
|
||||
)
|
||||
|
||||
// register functions and build to plugin binary
|
||||
func main() {
|
||||
fungo.Register("sum_ints", SumInts)
|
||||
fungo.Register("sum_two_int", SumTwoInt)
|
||||
fungo.Register("sum", Sum)
|
||||
fungo.Register("sum_two_string", SumTwoString)
|
||||
fungo.Register("sum_strings", SumStrings)
|
||||
fungo.Register("concatenate", Concatenate)
|
||||
fungo.Register("setup_hook_example", SetupHookExample)
|
||||
fungo.Register("teardown_hook_example", TeardownHookExample)
|
||||
fungo.Serve()
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "api test demo",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.3",
|
||||
"device_sn": "TESTCASE_SETUP_XXX",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.6"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"herader": [
|
||||
{
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "PostmanRuntime/7.28.4"
|
||||
}
|
||||
],
|
||||
"verify": false,
|
||||
"export": [
|
||||
"session_token"
|
||||
]
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "test api /get",
|
||||
"api": "api/get.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.4",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.7"
|
||||
},
|
||||
"extract": {
|
||||
"session_token": "body.headers.\"postman-token\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test api /post",
|
||||
"api": "api/post.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.5",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.9"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"eq": [
|
||||
"status_code",
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"eq": [
|
||||
"body.headers.postman-token",
|
||||
"ea19464c-ddd4-4724-abe9-5e2b254c2723"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test api /put",
|
||||
"api": "api/put.json",
|
||||
"variables": {
|
||||
"user_agent": "iOS/10.6",
|
||||
"device_sn": "$device_sn",
|
||||
"os_platform": "ios",
|
||||
"app_version": "2.8.10"
|
||||
},
|
||||
"extract": {
|
||||
"session_token": "body.headers.\"postman-token\""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
config:
|
||||
name: 'api test demo'
|
||||
variables:
|
||||
user_agent: iOS/10.3
|
||||
device_sn: TESTCASE_SETUP_XXX
|
||||
os_platform: ios
|
||||
app_version: 2.8.6
|
||||
base_url: 'https://postman-echo.com'
|
||||
herader:
|
||||
- Accept: '*/*'
|
||||
Accept-Encoding: 'gzip, deflate, br'
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Host: postman-echo.com
|
||||
User-Agent: PostmanRuntime/7.28.4
|
||||
verify: false
|
||||
export:
|
||||
- session_token
|
||||
teststeps:
|
||||
- name: 'test api /get'
|
||||
api: api/get.json
|
||||
variables:
|
||||
user_agent: iOS/10.4
|
||||
device_sn: $device_sn
|
||||
os_platform: ios
|
||||
app_version: 2.8.7
|
||||
extract:
|
||||
session_token: 'body.headers."postman-token"'
|
||||
- name: 'test api /post'
|
||||
api: api/post.json
|
||||
variables:
|
||||
user_agent: iOS/10.5
|
||||
device_sn: $device_sn
|
||||
os_platform: ios
|
||||
app_version: 2.8.9
|
||||
validate:
|
||||
- { eq: [ status_code, 200 ] }
|
||||
- { eq: [ body.headers.postman-token, ea19464c-ddd4-4724-abe9-5e2b254c2723 ] }
|
||||
- name: 'test api /put'
|
||||
api: api/put.json
|
||||
variables:
|
||||
user_agent: iOS/10.6
|
||||
device_sn: $device_sn
|
||||
os_platform: ios
|
||||
app_version: 2.8.10
|
||||
extract:
|
||||
session_token: 'body.headers."postman-token"'
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "reference testcase test",
|
||||
"base_url": "https://postman-echo.com",
|
||||
"variables": {
|
||||
"os_platform": "ios"
|
||||
}
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "run demo_httprunner.json",
|
||||
"testcase": "demo_httprunner.json",
|
||||
"variables": {
|
||||
"os_platform": "$os_platform"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
config:
|
||||
name: "reference testcase test"
|
||||
base_url: "https://postman-echo.com"
|
||||
variables:
|
||||
os_platform: 'ios'
|
||||
|
||||
teststeps:
|
||||
- name: run demo_httprunner.yaml
|
||||
testcase: demo_httprunner.yaml
|
||||
variables:
|
||||
os_platform: $os_platform
|
|
@ -0,0 +1,56 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
const rendezvousTestJSONPath = "rendezvous_test.json"
|
||||
|
||||
var rendezvousTestcase = &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with functions").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
}),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("waiting for all users in the beginning").
|
||||
Rendezvous("rendezvous0"),
|
||||
hrp.NewStep("rendezvous before get").
|
||||
Rendezvous("rendezvous1").
|
||||
WithUserNumber(50).
|
||||
WithTimeout(3000),
|
||||
hrp.NewStep("get with params").
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "foo1", "foo2": "foo2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code"),
|
||||
hrp.NewStep("rendezvous before post").
|
||||
Rendezvous("rendezvous2").
|
||||
WithUserNumber(20).
|
||||
WithTimeout(2000),
|
||||
hrp.NewStep("post json data with functions").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
WithBody(map[string]interface{}{"foo1": "foo1", "foo2": "foo2"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.json.foo1", 4, "check args foo1").
|
||||
AssertEqual("body.json.foo2", "foo2", "check args foo2"),
|
||||
hrp.NewStep("waiting for all users in the end").
|
||||
Rendezvous("rendezvous3"),
|
||||
},
|
||||
}
|
||||
|
||||
func TestRendezvous(t *testing.T) {
|
||||
err := hrp.NewRunner(t).Run(rendezvousTestcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "run request with functions",
|
||||
"base_url": "https://postman-echo.com",
|
||||
"variables": {
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
"n": 5
|
||||
},
|
||||
"parameters_setting": {
|
||||
"strategy": "Sequential",
|
||||
"parameterIterator": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "waiting for all users in the beginning",
|
||||
"rendezvous": {
|
||||
"name": "rendezvous0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rendezvous before get",
|
||||
"rendezvous": {
|
||||
"name": "rendezvous1",
|
||||
"number": 50,
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {
|
||||
"foo1": "foo1",
|
||||
"foo2": "foo2"
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunnerPlus"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"varFoo1": "body.args.foo1"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rendezvous before post",
|
||||
"rendezvous": {
|
||||
"name": "rendezvous2",
|
||||
"number": 20,
|
||||
"timeout": 2000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "post json data with functions",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunnerPlus"
|
||||
},
|
||||
"body": {
|
||||
"foo1": "foo1",
|
||||
"foo2": "foo2"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo1",
|
||||
"assert": "length_equals",
|
||||
"expect": 4,
|
||||
"msg": "check args foo1"
|
||||
},
|
||||
{
|
||||
"check": "body.json.foo2",
|
||||
"assert": "equals",
|
||||
"expect": "foo2",
|
||||
"msg": "check args foo2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "waiting for all users in the end",
|
||||
"rendezvous": {
|
||||
"name": "rendezvous3"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
func TestCaseBasicRequest(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("request methods testcase in hardcode").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
|
||||
AssertEqual("body.args.foo1", "bar1", "check args foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2"),
|
||||
hrp.NewStep("post raw text").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
"Content-Type": "text/plain",
|
||||
}).
|
||||
WithBody("This is expected to be sent back as part of response body.").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("body.data", "This is expected to be sent back as part of response body.", "check data"),
|
||||
hrp.NewStep("post form data").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}).
|
||||
WithBody(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("body.form.foo1", "bar1", "check form foo1").
|
||||
AssertEqual("body.form.foo2", "bar2", "check form foo2"),
|
||||
hrp.NewStep("post json data").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
}).
|
||||
WithBody(map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("body.json.foo1", "bar1", "check json foo1").
|
||||
AssertEqual("body.json.foo2", "bar2", "check json foo2"),
|
||||
hrp.NewStep("put request").
|
||||
PUT("/put").
|
||||
WithHeaders(map[string]string{
|
||||
"User-Agent": "HttpRunnerPlus",
|
||||
"Content-Type": "text/plain",
|
||||
}).
|
||||
WithBody("This is expected to be sent back as part of response body.").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("body.data", "This is expected to be sent back as part of response body.", "check data"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"config": {
|
||||
"name": "think time test demo",
|
||||
"variables": {
|
||||
"app_version": "v1",
|
||||
"user_agent": "iOS/10.3"
|
||||
},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"think_time": {
|
||||
"strategy": "random_percentage",
|
||||
"setting": {
|
||||
"min_percentage": 1,
|
||||
"max_percentage": 1.5
|
||||
},
|
||||
"limit": 4
|
||||
},
|
||||
"verify": false
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"headers": {
|
||||
"User-Agent": "$user_agent,$app_version"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "think time 1",
|
||||
"think_time": {
|
||||
"time": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "post with params",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"User-Agent": "$user_agent,$app_version"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "check status code"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
config:
|
||||
name: "think time test demo"
|
||||
variables:
|
||||
app_version: v1
|
||||
user_agent: iOS/10.3
|
||||
base_url: "https://postman-echo.com"
|
||||
think_time:
|
||||
strategy: random_percentage
|
||||
setting:
|
||||
min_percentage: 1.0
|
||||
max_percentage: 1.5
|
||||
limit: 4
|
||||
verify: False
|
||||
|
||||
teststeps:
|
||||
- name: get with params
|
||||
request:
|
||||
method: GET
|
||||
url: /get
|
||||
headers:
|
||||
User-Agent: $user_agent,$app_version
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
||||
- name: think time 1
|
||||
think_time:
|
||||
time: 3
|
||||
- name: post with params
|
||||
request:
|
||||
method: POST
|
||||
url: /post
|
||||
headers:
|
||||
User-Agent: $user_agent,$app_version
|
||||
validate:
|
||||
- check: status_code
|
||||
assert: equals
|
||||
expect: 200
|
||||
msg: check status code
|
|
@ -0,0 +1,55 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
func TestCaseValidateStep(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with validation").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
"expectedStatusCode": 200,
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("status_code", "$expectedStatusCode", "check status code"). // assert status code
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type"). // assert response header, with double quotes
|
||||
AssertEqual("body.args.foo1", "bar1", "check args foo1"). // assert response json body with jmespath
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Extract().
|
||||
WithJmesPath("status_code", "statusCode").
|
||||
WithJmesPath("headers.\"Content-Type\"", "contentType").
|
||||
Validate().
|
||||
AssertEqual("$statusCode", 200, "check status code"). // assert with extracted variable from current step
|
||||
AssertEqual("$contentType", "application/json; charset=utf-8", "check header Content-Type"). // assert with extracted variable from current step
|
||||
AssertEqual("$varFoo1", "bar1", "check args foo1"). // assert with extracted variable from previous step
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2"), // assert response json body with jmespath
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
func TestCaseConfigVariables(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with variables").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
"expectedStatusCode": 200,
|
||||
}).SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", "$expectedStatusCode", "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
|
||||
AssertEqual("body.args.foo1", "bar1", "check args foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseStepVariables(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with variables").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1",
|
||||
"agent": "HttpRunnerPlus",
|
||||
"expectedStatusCode": 200,
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", "$expectedStatusCode", "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
|
||||
AssertEqual("body.args.foo1", "bar1", "check args foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseOverrideConfigVariables(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with variables").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar0",
|
||||
"agent": "HttpRunnerPlus",
|
||||
"expectedStatusCode": 200,
|
||||
}).SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"var1": "bar1", // override config variable
|
||||
"agent": "$agent", // reference config variable
|
||||
// expectedStatusCode, inherit config variable
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$var1", "foo2": "bar2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "$agent"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", "$expectedStatusCode", "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json; charset=utf-8", "check header Content-Type").
|
||||
AssertEqual("body.args.foo1", "bar1", "check args foo1").
|
||||
AssertEqual("body.args.foo2", "bar2", "check args foo2").
|
||||
AssertEqual("body.headers.\"user-agent\"", "HttpRunnerPlus", "check header user agent"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseParseVariables(t *testing.T) {
|
||||
testcase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("run request with functions").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
"varFoo1": "${gen_random_string($n)}",
|
||||
"varFoo2": "${max($a, $b)}", // 12.3
|
||||
}).SetVerifySSL(false),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{
|
||||
"n": 3,
|
||||
"b": 34.5,
|
||||
"varFoo2": "${max($a, $b)}", // 34.5
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}).
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.args.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string
|
||||
hrp.NewStep("post json data with functions").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}).
|
||||
WithBody(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.json.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.json.foo2", 12.3, "check args foo2"),
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(t).Run(testcase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
module github.com/httprunner/httprunner
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/httprunner/funplugin v0.4.0
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/maja42/goval v1.2.1
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
// replace github.com/httprunner/funplugin => ../funplugin
|
|
@ -0,0 +1,737 @@
|
|||
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=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
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-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
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.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
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-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
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/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/httprunner/funplugin v0.4.0 h1:jSptZ6Ki0Dh3uvpLDbmxE6kSqVv0FHaQnHs0Qt+6SS8=
|
||||
github.com/httprunner/funplugin v0.4.0/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
|
||||
github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
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/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/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/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/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
|
||||
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.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
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/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
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/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
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-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/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-20210603081109-ebe580a85c40/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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
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.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
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/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 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.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-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-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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
|
@ -0,0 +1,316 @@
|
|||
# hrp (HttpRunner+)
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/httprunner/hrp.svg)](https://pkg.go.dev/github.com/httprunner/hrp)
|
||||
[![Github Actions](https://github.com/httprunner/hrp/actions/workflows/unittest.yml/badge.svg)](https://github.com/httprunner/hrp/actions)
|
||||
[![codecov](https://codecov.io/gh/httprunner/hrp/branch/main/graph/badge.svg?token=HPCQWCD7KO)](https://codecov.io/gh/httprunner/hrp)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/httprunner/hrp)](https://goreportcard.com/report/github.com/httprunner/hrp)
|
||||
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B27856%2Fgithub.com%2Fhttprunner%2Fhrp.svg?type=shield)](https://app.fossa.com/reports/c2742455-c8ab-4b13-8fd7-4a35ba0b2840)
|
||||
|
||||
`hrp` aims to be a one-stop solution for HTTP(S) testing, covering API testing, load testing and digital experience monitoring (DEM).
|
||||
|
||||
See [CHANGELOG].
|
||||
|
||||
> HttpRunner [用户调研问卷][survey] 持续收集中,我们将基于用户反馈动态调整产品特性和需求优先级。
|
||||
|
||||
## Key Features
|
||||
|
||||
![flow chart](docs/assets/flow.jpg)
|
||||
|
||||
### API Testing
|
||||
|
||||
- [x] Full support for HTTP(S)/1.1 requests.
|
||||
- [ ] Support more protocols, HTTP/2, WebSocket, TCP, RPC etc.
|
||||
- [x] Testcases can be described in multiple formats, `YAML`/`JSON`/`Golang`, and they are interchangeable.
|
||||
- [x] Use Charles/Fiddler/Chrome/etc to record HTTP requests and generate testcases from exported [`HAR`][HAR].
|
||||
- [x] Supports `variables`/`extract`/`validate`/`hooks` mechanisms to create extremely complex test scenarios.
|
||||
- [x] Data driven with `parameterize` mechanism, supporting sequential/random/unique strategies to select data.
|
||||
- [ ] Built-in 100+ commonly used functions for ease, including md5sum, max/min, sleep, gen_random_string etc.
|
||||
- [x] Create and call custom functions with `plugin` mechanism, support [hashicorp plugin] and [go plugin].
|
||||
- [x] Generate html reports with rich test results.
|
||||
- [x] Using it as a `CLI tool` or a `library` are both supported.
|
||||
|
||||
### Load Testing
|
||||
|
||||
Base on the API testing testcases, you can run professional load testing without extra work.
|
||||
|
||||
- [x] Inherit all powerful features of [`locust`][locust] and [`boomer`][boomer].
|
||||
- [x] Report performance metrics to [prometheus pushgateway][pushgateway].
|
||||
- [x] Use `transaction` to define a set of end-user actions that represent the real user activities.
|
||||
- [x] Use `rendezvous` points to force Vusers to perform tasks concurrently during test execution.
|
||||
- [x] Load testing with specified concurrent users or constant RPS, also supports spawn rate.
|
||||
- [ ] Support mixed-scenario testing with custom weight.
|
||||
- [ ] Simulate browser's HTTP parallel connections.
|
||||
- [ ] IP spoofing.
|
||||
- [ ] Run in distributed mode to generate unlimited RPS.
|
||||
|
||||
### Digital Experience Monitoring (DEM)
|
||||
|
||||
You can also monitor online services for digital experience assessments.
|
||||
|
||||
- [ ] HTTP(S) latency statistics including DNSLookup, TCP connections, SSL handshakes, content transfers, etc.
|
||||
- [ ] `ping` indicators including latency, throughput and packets loss.
|
||||
- [ ] traceroute
|
||||
- [ ] DNS monitoring
|
||||
|
||||
## Quick Start
|
||||
|
||||
### use as CLI tool
|
||||
|
||||
You can install `hrp` with one shell command, which will download the latest version's released binary and install to the current system.
|
||||
|
||||
```bash
|
||||
# install via curl
|
||||
$ bash -c "$(curl -ksSL https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh)"
|
||||
# install via wget
|
||||
$ bash -c "$(wget https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh -O -)"
|
||||
```
|
||||
|
||||
If you are a golang developer, you can also install `hrp` with `go get`.
|
||||
|
||||
```bash
|
||||
$ go get github.com/httprunner/hrp/cli/hrp
|
||||
```
|
||||
|
||||
Since installed, you will get a `hrp` command with multiple sub-commands.
|
||||
|
||||
```text
|
||||
$ hrp -h
|
||||
|
||||
██╗ ██╗████████╗████████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗███╗ ██╗███████╗██████╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║████╗ ██║██╔════╝██╔══██╗
|
||||
███████║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██████╔╝
|
||||
██╔══██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██╔══██╗
|
||||
██║ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║██║ ╚████║███████╗██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
||||
|
||||
hrp (HttpRunner+) aims to be a one-stop solution for HTTP(S) testing, covering API testing,
|
||||
load testing and digital experience monitoring (DEM). Enjoy! ✨ 🚀 ✨
|
||||
|
||||
License: Apache-2.0
|
||||
Website: https://httprunner.com
|
||||
Github: https://github.com/httprunner/hrp
|
||||
Copyright 2021 debugtalk
|
||||
|
||||
Usage:
|
||||
hrp [command]
|
||||
|
||||
Available Commands:
|
||||
boom run load test with boomer
|
||||
completion generate the autocompletion script for the specified shell
|
||||
har2case convert HAR to json/yaml testcase files
|
||||
help Help about any command
|
||||
run run API test
|
||||
startproject create a scaffold project
|
||||
|
||||
Flags:
|
||||
-h, --help help for hrp
|
||||
--log-json set log to json format
|
||||
-l, --log-level string set log level (default "INFO")
|
||||
-v, --version version for hrp
|
||||
|
||||
Use "hrp [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
You can use `hrp run` command to run HttpRunner JSON/YAML testcases. The following is an example running [examples/demo.json][demo.json]
|
||||
|
||||
<details>
|
||||
<summary>$ hrp run examples/demo.json</summary>
|
||||
|
||||
```text
|
||||
5:21PM INF Set log to color console other than JSON format.
|
||||
5:21PM ??? Set log level
|
||||
5:21PM INF [init] SetDebug debug=true
|
||||
5:21PM INF [init] SetFailfast failfast=true
|
||||
5:21PM INF [init] Reset session variables
|
||||
5:21PM INF load json testcase path=/Users/debugtalk/MyProjects/HttpRunner-dev/hrp/examples/demo.json
|
||||
5:21PM INF call function success arguments=[5] funcName=gen_random_string output=A65rg
|
||||
5:21PM INF call function success arguments=[12.3,3.45] funcName=max output=12.3
|
||||
5:21PM INF run testcase start testcase="demo with complex mechanisms"
|
||||
5:21PM INF transaction name=tran1 type=start
|
||||
5:21PM INF run step start step="get with params"
|
||||
5:21PM INF call function success arguments=[12.3,34.5] funcName=max output=34.5
|
||||
-------------------- request --------------------
|
||||
GET /get?foo1=A65rg&foo2=34.5 HTTP/1.1
|
||||
Host: postman-echo.com
|
||||
User-Agent: HttpRunnerPlus
|
||||
|
||||
|
||||
==================== response ===================
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 304
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Thu, 23 Dec 2021 09:21:30 GMT
|
||||
Etag: W/"130-t7qE4M7C+OQ0jGdRWkr2R3gjq+w"
|
||||
Set-Cookie: sails.sid=s%3AAiqfRgMtWKG3oOQnXJOxRD8xk58rtAW6.eD%2BBo7FBnA82XLsLFiadeg6OcuD2zHSTyhv2l%2FDVuCk; Path=/; HttpOnly
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{"args":{"foo1":"A65rg","foo2":"34.5"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-61c43f9a-7c855775053963a4284ba464","user-agent":"HttpRunnerPlus","accept-encoding":"gzip"},"url":"https://postman-echo.com/get?foo1=A65rg&foo2=34.5"}
|
||||
--------------------------------------------------
|
||||
5:21PM INF extract value from=body.args.foo1 value=A65rg
|
||||
5:21PM INF set variable value=A65rg variable=varFoo1
|
||||
5:21PM INF validate status_code assertMethod=equals checkValue=200 expectValue=200 result=true
|
||||
5:21PM INF validate headers."Content-Type" assertMethod=startswith checkValue="application/json; charset=utf-8" expectValue=application/json result=true
|
||||
5:21PM INF validate body.args.foo1 assertMethod=length_equals checkValue=A65rg expectValue=5 result=true
|
||||
5:21PM INF validate $varFoo1 assertMethod=length_equals checkValue=A65rg expectValue=5 result=true
|
||||
5:21PM INF validate body.args.foo2 assertMethod=equals checkValue=34.5 expectValue=34.5 result=true
|
||||
5:21PM INF run step end exportVars={"varFoo1":"A65rg"} step="get with params" success=true
|
||||
5:21PM INF transaction name=tran1 type=end
|
||||
5:21PM INF transaction elapsed=1021.174113 name=tran1
|
||||
5:21PM INF run step start step="post json data"
|
||||
5:21PM INF call function success arguments=[12.3,3.45] funcName=max output=12.3
|
||||
-------------------- request --------------------
|
||||
POST /post HTTP/1.1
|
||||
Host: postman-echo.com
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
|
||||
{"foo1":"A65rg","foo2":12.3}
|
||||
==================== response ===================
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 424
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Thu, 23 Dec 2021 09:21:30 GMT
|
||||
Etag: W/"1a8-IhWXQxTXlxmnbqdRh+oBPRTLsOU"
|
||||
Set-Cookie: sails.sid=s%3AzXIPVMKipoISZG0Zj4tX73vKDbIdFtzZ.xD50I4UMHUERmcgWfp64f0a8g%2BT9YIUf0Fi1l5bXbQA; Path=/; HttpOnly
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{"args":{},"data":{"foo1":"A65rg","foo2":12.3},"files":{},"form":{},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-61c43f9a-78aab84a36a753ea6b5dd0f7","content-length":"28","user-agent":"Go-http-client/1.1","content-type":"application/json; charset=UTF-8","accept-encoding":"gzip"},"json":{"foo1":"A65rg","foo2":12.3},"url":"https://postman-echo.com/post"}
|
||||
--------------------------------------------------
|
||||
5:21PM INF validate status_code assertMethod=equals checkValue=200 expectValue=200 result=true
|
||||
5:21PM INF validate body.json.foo1 assertMethod=length_equals checkValue=A65rg expectValue=5 result=true
|
||||
5:21PM INF validate body.json.foo2 assertMethod=equals checkValue=12.3 expectValue=12.3 result=true
|
||||
5:21PM INF run step end exportVars=null step="post json data" success=true
|
||||
5:21PM INF run step start step="post form data"
|
||||
5:21PM INF call function success arguments=[12.3,3.45] funcName=max output=12.3
|
||||
-------------------- request --------------------
|
||||
POST /post HTTP/1.1
|
||||
Host: postman-echo.com
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
|
||||
foo1=A65rg&foo2=12.3
|
||||
==================== response ===================
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 445
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Thu, 23 Dec 2021 09:21:30 GMT
|
||||
Etag: W/"1bd-g4G7WmMU7EzJYzPTYgqX67Ug9iE"
|
||||
Set-Cookie: sails.sid=s%3Al3gcdxEQug7ddxPlA2Kfxvm7d_z9ImEt.4IQI1SVX5xuTefX0N0UvJPQxVvA1SAMm7ztHESkHXsY; Path=/; HttpOnly
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{"args":{},"data":"","files":{},"form":{"foo1":"A65rg","foo2":"12.3"},"headers":{"x-forwarded-proto":"https","x-forwarded-port":"443","host":"postman-echo.com","x-amzn-trace-id":"Root=1-61c43f9a-6458626c64b04fd60245714b","content-length":"20","user-agent":"Go-http-client/1.1","content-type":"application/x-www-form-urlencoded; charset=UTF-8","accept-encoding":"gzip"},"json":{"foo1":"A65rg","foo2":"12.3"},"url":"https://postman-echo.com/post"}
|
||||
--------------------------------------------------
|
||||
5:21PM INF validate status_code assertMethod=equals checkValue=200 expectValue=200 result=true
|
||||
5:21PM INF validate body.form.foo1 assertMethod=length_equals checkValue=A65rg expectValue=5 result=true
|
||||
5:21PM INF validate body.form.foo2 assertMethod=equals checkValue=12.3 expectValue=12.3 result=true
|
||||
5:21PM INF run step end exportVars=null step="post form data" success=true
|
||||
5:21PM INF run testcase end testcase="demo with complex mechanisms"
|
||||
```
|
||||
</details>
|
||||
|
||||
### use as library
|
||||
|
||||
Beside using `hrp` as a CLI tool, you can also use it as golang library.
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/httprunner/hrp
|
||||
```
|
||||
|
||||
This is an example of `HttpRunner+` testcase. You can find more in the [`examples`][examples] directory.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>demo</summary>
|
||||
|
||||
```go
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/httprunner/hrp"
|
||||
)
|
||||
|
||||
func TestCaseDemo(t *testing.T) {
|
||||
demoTestCase := &hrp.TestCase{
|
||||
Config: hrp.NewConfig("demo with complex mechanisms").
|
||||
SetBaseURL("https://postman-echo.com").
|
||||
WithVariables(map[string]interface{}{ // global level variables
|
||||
"n": 5,
|
||||
"a": 12.3,
|
||||
"b": 3.45,
|
||||
"varFoo1": "${gen_random_string($n)}",
|
||||
"varFoo2": "${max($a, $b)}", // 12.3; eval with built-in function
|
||||
}),
|
||||
TestSteps: []hrp.IStep{
|
||||
hrp.NewStep("transaction 1 start").StartTransaction("tran1"), // start transaction
|
||||
hrp.NewStep("get with params").
|
||||
WithVariables(map[string]interface{}{ // step level variables
|
||||
"n": 3, // inherit config level variables if not set in step level, a/varFoo1
|
||||
"b": 34.5, // override config level variable if existed, n/b/varFoo2
|
||||
"varFoo2": "${max($a, $b)}", // 34.5; override variable b and eval again
|
||||
}).
|
||||
GET("/get").
|
||||
WithParams(map[string]interface{}{"foo1": "$varFoo1", "foo2": "$varFoo2"}). // request with params
|
||||
WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). // request with headers
|
||||
Extract().
|
||||
WithJmesPath("body.args.foo1", "varFoo1"). // extract variable with jmespath
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check response status code"). // validate response status code
|
||||
AssertStartsWith("headers.\"Content-Type\"", "application/json", ""). // validate response header
|
||||
AssertLengthEqual("body.args.foo1", 5, "check args foo1"). // validate response body with jmespath
|
||||
AssertLengthEqual("$varFoo1", 5, "check args foo1"). // assert with extracted variable from current step
|
||||
AssertEqual("body.args.foo2", "34.5", "check args foo2"), // notice: request params value will be converted to string
|
||||
hrp.NewStep("transaction 1 end").EndTransaction("tran1"), // end transaction
|
||||
hrp.NewStep("post json data").
|
||||
POST("/post").
|
||||
WithBody(map[string]interface{}{
|
||||
"foo1": "$varFoo1", // reference former extracted variable
|
||||
"foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here
|
||||
}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.json.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.json.foo2", 12.3, "check args foo2"),
|
||||
hrp.NewStep("post form data").
|
||||
POST("/post").
|
||||
WithHeaders(map[string]string{"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}).
|
||||
WithBody(map[string]interface{}{
|
||||
"foo1": "$varFoo1", // reference former extracted variable
|
||||
"foo2": "${max($a, $b)}", // 12.3; step level variables are independent, variable b is 3.45 here
|
||||
}).
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertLengthEqual("body.form.foo1", 5, "check args foo1").
|
||||
AssertEqual("body.form.foo2", "12.3", "check args foo2"), // form data will be converted to string
|
||||
},
|
||||
}
|
||||
|
||||
err := hrp.NewRunner(nil).Run(demoTestCase) // hrp.Run(demoTestCase)
|
||||
if err != nil {
|
||||
t.Fatalf("run testcase error: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Subscribe
|
||||
|
||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||
|
||||
<img src="docs/assets/qrcode.jpg" alt="HttpRunner" width="200">
|
||||
|
||||
如果你期望加入 HttpRunner 核心用户群,请填写[用户调研问卷][survey]并留下你的联系方式,作者将拉你进群。
|
||||
|
||||
[HttpRunner]: https://github.com/httprunner/httprunner
|
||||
[boomer]: https://github.com/myzhan/boomer
|
||||
[locust]: https://github.com/locustio/locust
|
||||
[jmespath]: https://jmespath.org/
|
||||
[allure]: https://docs.qameta.io/allure/
|
||||
[HAR]: http://httparchive.org/
|
||||
[hashicorp plugin]: https://github.com/hashicorp/go-plugin
|
||||
[go plugin]: https://pkg.go.dev/plugin
|
||||
[demo.json]: https://github.com/httprunner/hrp/blob/main/examples/demo.json
|
||||
[examples]: https://github.com/httprunner/hrp/blob/main/examples/
|
||||
[CHANGELOG]: docs/CHANGELOG.md
|
||||
[pushgateway]: https://github.com/prometheus/pushgateway
|
||||
[survey]: https://wj.qq.com/s2/9699514/0d19/
|
|
@ -0,0 +1,177 @@
|
|||
package hrp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/funplugin"
|
||||
"github.com/httprunner/httprunner/hrp/internal/boomer"
|
||||
"github.com/httprunner/httprunner/hrp/internal/ga"
|
||||
)
|
||||
|
||||
func NewBoomer(spawnCount int, spawnRate float64) *HRPBoomer {
|
||||
b := &HRPBoomer{
|
||||
Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate),
|
||||
pluginsMutex: new(sync.RWMutex),
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type HRPBoomer struct {
|
||||
*boomer.Boomer
|
||||
plugins []funplugin.IPlugin // each task has its own plugin process
|
||||
pluginsMutex *sync.RWMutex // avoid data race
|
||||
}
|
||||
|
||||
// Run starts to run load test for one or multiple testcases.
|
||||
func (b *HRPBoomer) Run(testcases ...ITestCase) {
|
||||
event := ga.EventTracking{
|
||||
Category: "RunLoadTests",
|
||||
Action: "hrp boom",
|
||||
}
|
||||
// report start event
|
||||
go ga.SendEvent(event)
|
||||
// report execution timing event
|
||||
defer ga.SendEvent(event.StartTiming("execution"))
|
||||
|
||||
var taskSlice []*boomer.Task
|
||||
for _, iTestCase := range testcases {
|
||||
testcase, err := iTestCase.ToTestCase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg := testcase.Config
|
||||
err = initParameterIterator(cfg, "boomer")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rendezvousList := initRendezvous(testcase, int64(b.GetSpawnCount()))
|
||||
task := b.convertBoomerTask(testcase, rendezvousList)
|
||||
taskSlice = append(taskSlice, task)
|
||||
waitRendezvous(rendezvousList)
|
||||
}
|
||||
b.Boomer.Run(taskSlice...)
|
||||
}
|
||||
|
||||
func (b *HRPBoomer) Quit() {
|
||||
b.pluginsMutex.Lock()
|
||||
plugins := b.plugins
|
||||
b.pluginsMutex.Unlock()
|
||||
for _, plugin := range plugins {
|
||||
plugin.Quit()
|
||||
}
|
||||
b.Boomer.Quit()
|
||||
}
|
||||
|
||||
func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task {
|
||||
hrpRunner := NewRunner(nil)
|
||||
// set client transport for high concurrency load testing
|
||||
hrpRunner.SetClientTransport(b.GetSpawnCount(), b.GetDisableKeepAlive(), b.GetDisableCompression())
|
||||
config := testcase.Config
|
||||
|
||||
// each testcase has its own plugin process
|
||||
plugin, _ := initPlugin(config.Path, false)
|
||||
if plugin != nil {
|
||||
b.pluginsMutex.Lock()
|
||||
b.plugins = append(b.plugins, plugin)
|
||||
b.pluginsMutex.Unlock()
|
||||
}
|
||||
|
||||
// broadcast to all rendezvous at once when spawn done
|
||||
go func() {
|
||||
<-b.GetSpawnDoneChan()
|
||||
for _, rendezvous := range rendezvousList {
|
||||
rendezvous.setSpawnDone()
|
||||
}
|
||||
}()
|
||||
|
||||
return &boomer.Task{
|
||||
Name: config.Name,
|
||||
Weight: config.Weight,
|
||||
Fn: func() {
|
||||
runner := hrpRunner.newCaseRunner(testcase)
|
||||
runner.parser.plugin = plugin
|
||||
|
||||
testcaseSuccess := true // flag whole testcase result
|
||||
var transactionSuccess = true // flag current transaction result
|
||||
|
||||
cfg := testcase.Config
|
||||
caseConfig := &TConfig{}
|
||||
// copy config to avoid data racing
|
||||
if err := copier.Copy(caseConfig, cfg); err != nil {
|
||||
log.Error().Err(err).Msg("copy config data failed")
|
||||
return
|
||||
}
|
||||
// iterate through all parameter iterators and update case variables
|
||||
for _, it := range caseConfig.ParametersSetting.Iterators {
|
||||
if it.HasNext() {
|
||||
caseConfig.Variables = mergeVariables(it.Next(), caseConfig.Variables)
|
||||
}
|
||||
}
|
||||
|
||||
if err := runner.parseConfig(caseConfig); err != nil {
|
||||
log.Error().Err(err).Msg("parse config failed")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
for index, step := range testcase.TestSteps {
|
||||
stepData, err := runner.runStep(index, caseConfig)
|
||||
if err != nil {
|
||||
// step failed
|
||||
var elapsed int64
|
||||
if stepData != nil {
|
||||
elapsed = stepData.Elapsed
|
||||
}
|
||||
b.RecordFailure(step.Type(), step.Name(), elapsed, err.Error())
|
||||
|
||||
// update flag
|
||||
testcaseSuccess = false
|
||||
transactionSuccess = false
|
||||
|
||||
if runner.hrpRunner.failfast {
|
||||
log.Error().Msg("abort running due to failfast setting")
|
||||
break
|
||||
}
|
||||
log.Warn().Err(err).Msg("run step failed, continue next step")
|
||||
continue
|
||||
}
|
||||
|
||||
// step success
|
||||
if stepData.StepType == stepTypeTransaction {
|
||||
// transaction
|
||||
// FIXME: support nested transactions
|
||||
if step.ToStruct().Transaction.Type == transactionEnd { // only record when transaction ends
|
||||
b.RecordTransaction(stepData.Name, transactionSuccess, stepData.Elapsed, 0)
|
||||
transactionSuccess = true // reset flag for next transaction
|
||||
}
|
||||
} else if stepData.StepType == stepTypeRendezvous {
|
||||
// rendezvous
|
||||
// TODO: implement rendezvous in boomer
|
||||
} else if stepData.StepType == stepTypeThinkTime {
|
||||
// think time
|
||||
// no record required
|
||||
} else {
|
||||
// request or testcase step
|
||||
b.RecordSuccess(step.Type(), step.Name(), stepData.Elapsed, stepData.ContentSize)
|
||||
}
|
||||
}
|
||||
endTime := time.Now()
|
||||
|
||||
// report duration for transaction without end
|
||||
for name, transaction := range runner.transactions {
|
||||
if len(transaction) == 1 {
|
||||
// if transaction end time not exists, use testcase end time instead
|
||||
duration := endTime.Sub(transaction[transactionStart])
|
||||
b.RecordTransaction(name, transactionSuccess, duration.Milliseconds(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// report testcase as a whole Action transaction, inspired by LoadRunner
|
||||
b.RecordTransaction("Action", testcaseSuccess, endTime.Sub(startTime).Milliseconds(), 0)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package hrp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBoomerStandaloneRun(t *testing.T) {
|
||||
buildHashicorpPlugin()
|
||||
defer removeHashicorpPlugin()
|
||||
|
||||
testcase1 := &TestCase{
|
||||
Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"),
|
||||
TestSteps: []IStep{
|
||||
NewStep("headers").
|
||||
GET("/headers").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
NewStep("user-agent").
|
||||
GET("/user-agent").
|
||||
Validate().
|
||||
AssertEqual("status_code", 200, "check status code").
|
||||
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
|
||||
NewStep("TestCase3").CallRefCase(&TestCase{Config: NewConfig("TestCase3")}),
|
||||
},
|
||||
}
|
||||
testcase2 := &demoTestCaseJSONPath
|
||||
|
||||
b := NewBoomer(2, 1)
|
||||
go b.Run(testcase1, testcase2)
|
||||
time.Sleep(5 * time.Second)
|
||||
b.Quit()
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
"github.com/httprunner/httprunner/hrp/internal/boomer"
|
||||
)
|
||||
|
||||
// boomCmd represents the boom command
|
||||
var boomCmd = &cobra.Command{
|
||||
Use: "boom",
|
||||
Short: "run load test with boomer",
|
||||
Long: `run yaml/json testcase files for load test`,
|
||||
Example: ` $ hrp boom demo.json # run specified json testcase file
|
||||
$ hrp boom demo.yaml # run specified yaml testcase file
|
||||
$ hrp boom examples/ # run testcases in specified folder`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
boomer.SetUlimit(10240) // ulimit -n 10240
|
||||
setLogLevel("WARN") // disable info logs for load testing
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var paths []hrp.ITestCase
|
||||
for _, arg := range args {
|
||||
path := hrp.TestCasePath(arg)
|
||||
paths = append(paths, &path)
|
||||
}
|
||||
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate)
|
||||
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate)
|
||||
if loopCount > 0 {
|
||||
hrpBoomer.SetLoopCount(loopCount)
|
||||
}
|
||||
if !disableConsoleOutput {
|
||||
hrpBoomer.AddOutput(boomer.NewConsoleOutput())
|
||||
}
|
||||
if prometheusPushgatewayURL != "" {
|
||||
hrpBoomer.AddOutput(boomer.NewPrometheusPusherOutput(prometheusPushgatewayURL, "hrp"))
|
||||
}
|
||||
hrpBoomer.SetDisableKeepAlive(disableKeepalive)
|
||||
hrpBoomer.SetDisableCompression(disableCompression)
|
||||
hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration)
|
||||
hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration)
|
||||
hrpBoomer.EnableGracefulQuit()
|
||||
hrpBoomer.Run(paths...)
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
spawnCount int
|
||||
spawnRate float64
|
||||
maxRPS int64
|
||||
loopCount int64
|
||||
requestIncreaseRate string
|
||||
memoryProfile string
|
||||
memoryProfileDuration time.Duration
|
||||
cpuProfile string
|
||||
cpuProfileDuration time.Duration
|
||||
prometheusPushgatewayURL string
|
||||
disableConsoleOutput bool
|
||||
disableCompression bool
|
||||
disableKeepalive bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(boomCmd)
|
||||
|
||||
boomCmd.Flags().Int64Var(&maxRPS, "max-rps", 0, "Max RPS that boomer can generate, disabled by default.")
|
||||
boomCmd.Flags().StringVar(&requestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.")
|
||||
boomCmd.Flags().IntVar(&spawnCount, "spawn-count", 1, "The number of users to spawn for load testing")
|
||||
boomCmd.Flags().Float64Var(&spawnRate, "spawn-rate", 1, "The rate for spawning users")
|
||||
boomCmd.Flags().Int64Var(&loopCount, "loop-count", -1, "The specify running cycles for load testing")
|
||||
boomCmd.Flags().StringVar(&memoryProfile, "mem-profile", "", "Enable memory profiling.")
|
||||
boomCmd.Flags().DurationVar(&memoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.")
|
||||
boomCmd.Flags().StringVar(&cpuProfile, "cpu-profile", "", "Enable CPU profiling.")
|
||||
boomCmd.Flags().DurationVar(&cpuProfileDuration, "cpu-profile-duration", 30*time.Second, "CPU profile duration.")
|
||||
boomCmd.Flags().StringVar(&prometheusPushgatewayURL, "prometheus-gateway", "", "Prometheus Pushgateway url.")
|
||||
boomCmd.Flags().BoolVar(&disableConsoleOutput, "disable-console-output", false, "Disable console output.")
|
||||
boomCmd.Flags().BoolVar(&disableCompression, "disable-compression", false, "Disable compression")
|
||||
boomCmd.Flags().BoolVar(&disableKeepalive, "disable-keepalive", false, "Disable keepalive")
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
// run this test to generate markdown docs for hrp command
|
||||
func TestGenMarkdownTree(t *testing.T) {
|
||||
err := doc.GenMarkdownTree(rootCmd, "../../docs/cmd")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
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)
|
||||
}
|
||||
// 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
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "hrp",
|
||||
Short: "One-stop solution for HTTP(S) testing.",
|
||||
Long: `
|
||||
██╗ ██╗████████╗████████╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗███╗ ██╗███████╗██████╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║████╗ ██║██╔════╝██╔══██╗
|
||||
███████║ ██║ ██║ ██████╔╝██████╔╝██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██████╔╝
|
||||
██╔══██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██╔══██╗
|
||||
██║ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║██║ ╚████║███████╗██║ ██║
|
||||
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
|
||||
|
||||
hrp (HttpRunner+) aims to be a one-stop solution for HTTP(S) testing, covering API testing,
|
||||
load testing and digital experience monitoring (DEM). Enjoy! ✨ 🚀 ✨
|
||||
|
||||
License: Apache-2.0
|
||||
Website: https://httprunner.com
|
||||
Github: https://github.com/httprunner/httprunner/hrp
|
||||
Copyright 2021 debugtalk`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
var noColor = false
|
||||
if runtime.GOOS == "windows" {
|
||||
noColor = true
|
||||
}
|
||||
if !logJSON {
|
||||
log.Logger = zerolog.New(zerolog.ConsoleWriter{NoColor: noColor, Out: os.Stderr}).With().Timestamp().Logger()
|
||||
log.Info().Msg("Set log to color console other than JSON format.")
|
||||
}
|
||||
},
|
||||
Version: version.VERSION,
|
||||
}
|
||||
|
||||
var (
|
||||
logLevel string
|
||||
logJSON bool
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "INFO", "set log level")
|
||||
rootCmd.PersistentFlags().BoolVar(&logJSON, "log-json", false, "set log to json format")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setLogLevel(level string) {
|
||||
level = strings.ToUpper(level)
|
||||
log.Info().Str("level", level).Msg("Set log level")
|
||||
switch level {
|
||||
case "DEBUG":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "INFO":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "WARN":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "ERROR":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
case "FATAL":
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
case "PANIC":
|
||||
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
// runCmd represents the run command
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run $path...",
|
||||
Short: "run API test",
|
||||
Long: `run yaml/json testcase files for API test`,
|
||||
Example: ` $ hrp run demo.json # run specified json testcase file
|
||||
$ hrp run demo.yaml # run specified yaml testcase file
|
||||
$ hrp run examples/ # run testcases in specified folder`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var paths []hrp.ITestCase
|
||||
for _, arg := range args {
|
||||
path := hrp.TestCasePath(arg)
|
||||
paths = append(paths, &path)
|
||||
}
|
||||
runner := hrp.NewRunner(nil).
|
||||
SetFailfast(!continueOnFailure).
|
||||
SetSaveTests(saveTests)
|
||||
if genHTMLReport {
|
||||
runner.GenHTMLReport()
|
||||
}
|
||||
if !requestsLogOff {
|
||||
runner.SetRequestsLogOn()
|
||||
}
|
||||
if pluginLogOn {
|
||||
runner.SetPluginLogOn()
|
||||
}
|
||||
if proxyUrl != "" {
|
||||
runner.SetProxyUrl(proxyUrl)
|
||||
}
|
||||
err := runner.Run(paths...)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
continueOnFailure bool
|
||||
requestsLogOff bool
|
||||
pluginLogOn bool
|
||||
proxyUrl string
|
||||
saveTests bool
|
||||
genHTMLReport bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
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(&pluginLogOn, "log-plugin", false, "turn on plugin logging")
|
||||
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
|
||||
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")
|
||||
runCmd.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/scaffold"
|
||||
)
|
||||
|
||||
var scaffoldCmd = &cobra.Command{
|
||||
Use: "startproject $project_name",
|
||||
Short: "create a scaffold project",
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !ignorePlugin && !genPythonPlugin && !genGoPlugin {
|
||||
return errors.New("please select function plugin type")
|
||||
}
|
||||
|
||||
var pluginType scaffold.PluginType
|
||||
if ignorePlugin {
|
||||
pluginType = scaffold.Ignore
|
||||
} else if genGoPlugin {
|
||||
pluginType = scaffold.Go
|
||||
} else {
|
||||
pluginType = scaffold.Py // default
|
||||
}
|
||||
err := scaffold.CreateScaffold(args[0], pluginType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create scaffold project failed")
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info().Str("projectName", args[0]).Msg("create scaffold success")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
ignorePlugin bool
|
||||
genPythonPlugin bool
|
||||
genGoPlugin bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(scaffoldCmd)
|
||||
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
|
||||
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
|
||||
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package hrp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
func loadFromJSON(path string, structObj interface{}) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("load json")
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("load json path failed")
|
||||
return err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(file))
|
||||
decoder.UseNumber()
|
||||
err = decoder.Decode(structObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadFromYAML(path string, structObj interface{}) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("load yaml")
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("load yaml path failed")
|
||||
return err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(file, structObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func convertCompatValidator(Validators []interface{}) (err error) {
|
||||
for i, iValidator := range Validators {
|
||||
validatorMap := iValidator.(map[string]interface{})
|
||||
validator := Validator{}
|
||||
_, checkExisted := validatorMap["check"]
|
||||
_, assertExisted := validatorMap["assert"]
|
||||
_, expectExisted := validatorMap["expect"]
|
||||
// check priority: HRP > HttpRunner
|
||||
if checkExisted && assertExisted && expectExisted {
|
||||
// HRP validator format
|
||||
validator.Check = validatorMap["check"].(string)
|
||||
validator.Assert = validatorMap["assert"].(string)
|
||||
validator.Expect = validatorMap["expect"]
|
||||
if msg, existed := validatorMap["msg"]; existed {
|
||||
validator.Message = msg.(string)
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else if len(validatorMap) == 1 {
|
||||
// HttpRunner validator format
|
||||
for assertMethod, iValidatorContent := range validatorMap {
|
||||
checkAndExpect := iValidatorContent.([]interface{})
|
||||
if len(checkAndExpect) != 2 {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
validator.Check = checkAndExpect[0].(string)
|
||||
validator.Assert = assertMethod
|
||||
validator.Expect = checkAndExpect[1]
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCompatTestCase(tc *TCase) (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("convert compat testcase error: %v", p)
|
||||
}
|
||||
}()
|
||||
for _, step := range tc.TestSteps {
|
||||
// 1. deal with request body compatible with HttpRunner
|
||||
if step.Request != nil && step.Request.Body == nil {
|
||||
if step.Request.Json != nil {
|
||||
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
step.Request.Body = step.Request.Json
|
||||
} else if step.Request.Data != nil {
|
||||
step.Request.Body = step.Request.Data
|
||||
}
|
||||
}
|
||||
|
||||
// 2. deal with validators compatible with HttpRunner
|
||||
err = convertCompatValidator(step.Validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertCheckExpr deals with check expression including hyphen
|
||||
func convertCheckExpr(checkExpr string) string {
|
||||
if strings.Contains(checkExpr, textExtractorSubRegexp) {
|
||||
return checkExpr
|
||||
}
|
||||
checkItems := strings.Split(checkExpr, ".")
|
||||
for i, checkItem := range checkItems {
|
||||
if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") {
|
||||
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
|
||||
}
|
||||
}
|
||||
return strings.Join(checkItems, ".")
|
||||
}
|
||||
|
||||
func (tc *TCase) ToTestCase() (*TestCase, error) {
|
||||
testCase := &TestCase{
|
||||
Config: tc.Config,
|
||||
}
|
||||
for _, step := range tc.TestSteps {
|
||||
if step.APIPath != "" {
|
||||
path := filepath.Join(filepath.Dir(testCase.Config.Path), step.APIPath)
|
||||
refAPI := APIPath(path)
|
||||
step.APIContent = &refAPI
|
||||
apiContent, err := step.APIContent.ToAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step.APIContent = apiContent
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{
|
||||
step: step,
|
||||
})
|
||||
} else if step.TestCasePath != "" {
|
||||
path := filepath.Join(filepath.Dir(testCase.Config.Path), step.TestCasePath)
|
||||
refTestCase := TestCasePath(path)
|
||||
step.TestCaseContent = &refTestCase
|
||||
tc, err := step.TestCaseContent.ToTestCase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step.TestCaseContent = tc
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
|
||||
step: step,
|
||||
})
|
||||
} else if step.ThinkTime != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepThinkTime{
|
||||
step: step,
|
||||
})
|
||||
} else if step.Request != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
|
||||
step: step,
|
||||
})
|
||||
} else if step.Transaction != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{
|
||||
step: step,
|
||||
})
|
||||
} else if step.Rendezvous != nil {
|
||||
testCase.TestSteps = append(testCase.TestSteps, &StepRendezvous{
|
||||
step: step,
|
||||
})
|
||||
} else {
|
||||
log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step")
|
||||
}
|
||||
}
|
||||
return testCase, nil
|
||||
}
|
||||
|
||||
var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension")
|
||||
|
||||
// APIPath implements IAPI interface.
|
||||
type APIPath string
|
||||
|
||||
func (path *APIPath) ToString() string {
|
||||
return fmt.Sprintf("%v", *path)
|
||||
}
|
||||
|
||||
func (path *APIPath) ToAPI() (*API, error) {
|
||||
api := &API{}
|
||||
var err error
|
||||
|
||||
apiPath := path.ToString()
|
||||
ext := filepath.Ext(apiPath)
|
||||
switch ext {
|
||||
case ".json":
|
||||
err = loadFromJSON(apiPath, api)
|
||||
case ".yaml", ".yml":
|
||||
err = loadFromYAML(apiPath, api)
|
||||
default:
|
||||
err = ErrUnsupportedFileExt
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = convertCompatValidator(api.Validators)
|
||||
return api, err
|
||||
}
|
||||
|
||||
// TestCasePath implements ITestCase interface.
|
||||
type TestCasePath string
|
||||
|
||||
func (path *TestCasePath) ToString() string {
|
||||
return fmt.Sprintf("%v", *path)
|
||||
}
|
||||
|
||||
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
||||
tc := &TCase{}
|
||||
var err error
|
||||
|
||||
casePath := path.ToString()
|
||||
ext := filepath.Ext(casePath)
|
||||
switch ext {
|
||||
case ".json":
|
||||
err = loadFromJSON(casePath, tc)
|
||||
case ".yaml", ".yml":
|
||||
err = loadFromYAML(casePath, tc)
|
||||
default:
|
||||
err = ErrUnsupportedFileExt
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = convertCompatTestCase(tc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc.Config.Path = path.ToString()
|
||||
testcase, err := tc.ToTestCase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return testcase, nil
|
||||
}
|
||||
|
||||
func (path *TestCasePath) ToTCase() (*TCase, error) {
|
||||
testcase, err := path.ToTestCase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return testcase.ToTCase()
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package hrp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
demoTestCaseJSONPath TestCasePath = "../examples/hrp/demo.json"
|
||||
demoTestCaseYAMLPath TestCasePath = "../examples/hrp/demo.yaml"
|
||||
demoRefAPIYAMLPath TestCasePath = "../examples/hrp/ref_api_test.yaml"
|
||||
demoRefTestCaseJSONPath TestCasePath = "../examples/hrp/ref_testcase_test.json"
|
||||
demoThinkTimeJsonPath TestCasePath = "../examples/hrp/think_time_test.json"
|
||||
demoAPIYAMLPath APIPath = "../examples/hrp/api/put.yml"
|
||||
)
|
||||
|
||||
func TestLoadCase(t *testing.T) {
|
||||
tcJSON := &TCase{}
|
||||
tcYAML := &TCase{}
|
||||
err := loadFromJSON(demoTestCaseJSONPath.ToString(), tcJSON)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
err = loadFromYAML(demoTestCaseYAMLPath.ToString(), tcYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, tcJSON.Config.Name, tcYAML.Config.Name) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.Config.BaseURL, tcYAML.Config.BaseURL) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.TestSteps[1].Name, tcYAML.TestSteps[1].Name) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, tcJSON.TestSteps[1].Request, tcYAML.TestSteps[1].Request) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertCheckExpr(t *testing.T) {
|
||||
exprs := []struct {
|
||||
before string
|
||||
after string
|
||||
}{
|
||||
// normal check expression
|
||||
{"a.b.c", "a.b.c"},
|
||||
{"headers.\"Content-Type\"", "headers.\"Content-Type\""},
|
||||
// check expression using regex
|
||||
{"covering (.*) testing,", "covering (.*) testing,"},
|
||||
{" (.*) a-b-c", " (.*) a-b-c"},
|
||||
// abnormal check expression
|
||||
{"-", "\"-\""},
|
||||
{"b-c", "\"b-c\""},
|
||||
{"a.b-c.d", "a.\"b-c\".d"},
|
||||
{"a-b.c-d", "\"a-b\".\"c-d\""},
|
||||
{"\"a-b\".c-d", "\"a-b\".\"c-d\""},
|
||||
{"headers.Content-Type", "headers.\"Content-Type\""},
|
||||
{"body.I-am-a-Key.name", "body.\"I-am-a-Key\".name"},
|
||||
}
|
||||
for _, expr := range exprs {
|
||||
if !assert.Equal(t, convertCheckExpr(expr.before), expr.after) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
# Release History
|
||||
|
||||
## v0.8.0 (2022-03-22)
|
||||
|
||||
- feat: support hashicorp python plugin over gRPC
|
||||
- feat: create scaffold with plugin option, `--py`(default), `--go`, `--ignore-plugin`
|
||||
- feat: print statistics summary after load testing finished
|
||||
- feat: support think time for api/load testing
|
||||
- fix: update prometheus state to stopped on quit
|
||||
|
||||
## v0.7.0 (2022-03-15)
|
||||
|
||||
- feat: support API layer for testcase #94
|
||||
- feat: support global headers for testcase #95
|
||||
- feat: support call referenced testcase by path in YAML/JSON testcases
|
||||
- fix: decode failure when content-encoding is deflate
|
||||
- fix: unstable RPS when load testing in high concurrency
|
||||
|
||||
## v0.6.4 (2022-03-10)
|
||||
|
||||
- feat: both support gRPC(default) and net/rpc mode in hashicorp plugin, switch with environment `HRP_PLUGIN_TYPE`
|
||||
- refactor: move submodule `plugin` to separate repo `github.com/httprunner/funplugin`
|
||||
- refactor: replace builtin json library with `json-iterator/go` to improve performance
|
||||
|
||||
## v0.6.3 (2022-03-04)
|
||||
|
||||
- feat: support customized setup/teardown hooks (variable assignment not supported)
|
||||
- feat: add flag `--log-plugin` to turn on plugin logging
|
||||
- change: add short flag `-c` for `--continue-on-failure`
|
||||
- change: use `--log-requests-off` flag to turn off request & response details logging
|
||||
- fix: support posting body in json array format
|
||||
- fix: testcase format compatibility with HttpRunner
|
||||
|
||||
## v0.6.2 (2022-02-22)
|
||||
|
||||
- feat: support text/html extraction with regex
|
||||
- change: json unmarshal to json.Number when parsing data
|
||||
- fix: omit pseudo header names for HTTP/1, e.g. :authority
|
||||
- fix: generate `headers.\"Content-Type\"` in har2case
|
||||
- fix: incorrect data type when extracting data using jmespath
|
||||
- fix: decode response body in brotli/gzip/deflate formats
|
||||
- fix: omit print request/response body for non-text content
|
||||
- fix: parse data for request cookie value
|
||||
|
||||
## v0.6.1 (2022-02-17)
|
||||
|
||||
- change: json unmarshal to float64 when parsing data
|
||||
- fix: set request Content-Type for posting json only when not specified
|
||||
- fix: failed to generate API test report when data is null
|
||||
- fix: panic when assertion function not exists
|
||||
- fix: broadcast to all rendezvous at once when spawn done
|
||||
|
||||
## v0.6.0 (2022-02-08)
|
||||
|
||||
- feat: implement `rendezvous` mechanism for data driven
|
||||
- feat: upload release artifacts to aliyun oss
|
||||
- feat: dump tests summary for execution results
|
||||
- feat: generate html report for API testing
|
||||
- change: remove sentry sdk
|
||||
|
||||
## v0.5.3 (2022-01-25)
|
||||
|
||||
- change: download package assets from aliyun OSS
|
||||
- fix: disable color logging on Windows
|
||||
- fix: print stderr when exec command failed
|
||||
- fix: build hashicorp plugin failed when creating scaffold
|
||||
|
||||
## v0.5.2 (2022-01-19)
|
||||
|
||||
- feat: support creating and calling custom functions with [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin)
|
||||
- feat: add scaffold demo with hashicorp plugin
|
||||
- feat: report events for initializing plugin
|
||||
- fix: log failures when the assertion failed
|
||||
|
||||
## v0.5.1 (2022-01-13)
|
||||
|
||||
- feat: support specifying running cycles for load testing
|
||||
- fix: ensure last stats reported when stop running
|
||||
|
||||
## v0.5.0 (2022-01-08)
|
||||
|
||||
- feat: support creating and calling custom functions with [go plugin](https://pkg.go.dev/plugin)
|
||||
- feat: install hrp with one shell command
|
||||
- feat: add `startproject` sub-command for creating scaffold project
|
||||
- feat: report GA event for loading go plugin
|
||||
|
||||
## v0.4.0 (2022-01-05)
|
||||
|
||||
- feat: implement `parameterize` mechanism for data driven
|
||||
- feat: add multiple builtin assertion methods and builtin functions
|
||||
|
||||
## v0.3.1 (2021-12-30)
|
||||
|
||||
- fix: set ulimit to 10240 before load testing
|
||||
- fix: concurrent map writes in load testing
|
||||
|
||||
## v0.3.0 (2021-12-24)
|
||||
|
||||
- feat: implement `transaction` mechanism for load test
|
||||
- feat: continue running next step when failure occurs with `--continue-on-failure` flag, default to failfast
|
||||
- feat: report GA events with version
|
||||
- feat: run load test with the given limit and burst as rate limiter, use `--spawn-count`, `--spawn-rate` and `--request-increase-rate` flag
|
||||
- feat: report runner state to prometheus
|
||||
- refactor: fork [boomer] as submodule initially and made a lot of changes
|
||||
- change: update API models
|
||||
|
||||
## v0.2.2 (2021-12-07)
|
||||
|
||||
- refactor: update models to make API more concise
|
||||
- change: remove mkdocs, move to [repo](https://github.com/httprunner/httprunner.github.io)
|
||||
|
||||
## v0.2.1 (2021-12-02)
|
||||
|
||||
- feat: push load testing metrics to [Prometheus Pushgateway][pushgateway]
|
||||
- feat: report events with Google Analytics
|
||||
|
||||
## v0.2.0 (2021-11-19)
|
||||
|
||||
- feat: deploy mkdocs to github pages when PR merged
|
||||
- feat: release hrp cli binaries automatically with github actions
|
||||
- feat: add Makefile for running unittest and building hrp cli binary
|
||||
|
||||
## v0.1.0 (2021-11-18)
|
||||
|
||||
- feat: full support for HTTP(S)/1.1 methods
|
||||
- feat: integrate [zerolog](https://github.com/rs/zerolog) for logging, include json log and pretty color console log
|
||||
- feat: implement `har2case` for converting HAR to JSON/YAML testcases
|
||||
- feat: extract and validate json response with [`jmespath`][jmespath]
|
||||
- feat: run JSON/YAML testcases with builtin functions
|
||||
- feat: support testcase and teststep level variables mechanism
|
||||
- feat: integrate [`boomer`][boomer] standalone mode for load testing
|
||||
- docs: init documentation website with [`mkdocs`][mkdocs]
|
||||
- docs: add project badges, including go report card, codecov, github actions, FOSSA, etc.
|
||||
- test: add CI test with [github actions][github-actions]
|
||||
- test: integrate [sentry sdk][sentry sdk] for event reporting and analysis
|
||||
|
||||
[jmespath]: https://jmespath.org/
|
||||
[mkdocs]: https://www.mkdocs.org/
|
||||
[github-actions]: https://github.com/httprunner/hrp/actions
|
||||
[boomer]: github.com/myzhan/boomer
|
||||
[sentry sdk]: https://github.com/getsentry/sentry-go
|
||||
[pushgateway]: https://github.com/prometheus/pushgateway
|
|
@ -0,0 +1,9 @@
|
|||
# Links
|
||||
|
||||
- Homepage: https://httprunner.com
|
||||
- Docs
|
||||
- Repo: https://github.com/httprunner/httprunner.github.io
|
||||
- 中文: https://httprunner.com/docs
|
||||
- English: https://httprunner.com/en/docs
|
||||
- [hrp command help](cmd/hrp.md)
|
||||
- Blog: https://httprunner.com/blog
|
Binary file not shown.
After Width: | Height: | Size: 214 KiB |
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 717.11 249.68"><title>sentry-logo-black</title><path d="M430.56,143.76,386.07,86.33H375v77h11.22v-59l45.74,59h9.82v-77H430.56Zm-112-14.27H358.4v-10H318.52V96.31h45v-10H307.07v77h57v-10H318.52Zm-46.84-9.78c-15.57-3.72-19.83-6.69-19.83-13.84,0-6.46,5.71-10.81,14.22-10.81,7.09,0,14.07,2.51,21.3,7.67l6.06-8.54c-8-6.13-16.65-9-27.13-9-15.25,0-25.89,9-25.89,21.92,0,13.84,9,18.63,25.5,22.63,14.51,3.35,18.93,6.5,18.93,13.5s-6,11.38-15.35,11.38c-9.07,0-16.81-3-25-9.82l-6.79,8.08a47.82,47.82,0,0,0,31.41,11.6c16.49,0,27.14-8.87,27.14-22.6C296.27,130.23,289.38,124,271.68,119.71Zm373.9-33.37-23.19,36.31-23-36.31H586l30.51,46.54v30.47h11.56V132.53l30.5-46.19ZM450.87,96.76H476.1v66.58h11.57V96.76h25.23V86.33h-62ZM566.4,133.28c11.64-3.21,18-11.37,18-23,0-14.78-10.84-24-28.28-24H522v77h11.45V135.62h19.42l19.54,27.72h13.37l-21.1-29.58Zm-33-7.52V96.53H555c11.27,0,17.74,5.31,17.74,14.56,0,8.91-6.92,14.67-17.62,14.67ZM144.9,65.43a13.75,13.75,0,0,0-23.81,0l-19.6,33.95,5,2.87a96.14,96.14,0,0,1,47.83,77.4H140.56a82.4,82.4,0,0,0-41-65.54l-5-2.86L76.3,143l5,2.87a46.35,46.35,0,0,1,22.46,33.78H72.33a2.27,2.27,0,0,1-2-3.41l8.76-15.17a31.87,31.87,0,0,0-10-5.71L60.42,170.5a13.75,13.75,0,0,0,11.91,20.62h43.25v-5.73A57.16,57.16,0,0,0,91.84,139l6.88-11.92a70.93,70.93,0,0,1,30.56,58.26v5.74h36.65v-5.73A107.62,107.62,0,0,0,117.09,95.3L131,71.17a2.27,2.27,0,0,1,3.93,0l60.66,105.07a2.27,2.27,0,0,1-2,3.41H179.4c.18,3.83.2,7.66,0,11.48h14.24a13.75,13.75,0,0,0,11.91-20.62Z" style="fill:#221f20"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,53 @@
|
|||
# Builtin
|
||||
|
||||
## Builtin assertions
|
||||
|
||||
HttpRunner+ validation should follow the following format. `check`, `assert` and `expect` are required field.
|
||||
|
||||
```json
|
||||
{
|
||||
"check": "status_code", // target field, usually used with jmespath
|
||||
"assert": "equals", // assertion method, you can use builtin method or custom defined function
|
||||
"expect": 200, // expected value
|
||||
"msg": "check response status code" // optional, print this message if assertion failed
|
||||
}
|
||||
```
|
||||
|
||||
The `assert` method name will be mapped to a built-in function with the following function signature.
|
||||
|
||||
```go
|
||||
func(t assert.TestingT, actual interface{}, expected interface{}, msgAndArgs ...interface{}) bool
|
||||
```
|
||||
|
||||
Currently, HttpRunner+ has the following built-in assertion functions.
|
||||
|
||||
| `assert` | Description | A(check), B(expect) | examples |
|
||||
| --- | --- | --- | --- |
|
||||
| `eq`, `equals`, `equal` | value is equal | A == B | 9 eq 9 |
|
||||
| `lt`, `less_than` | less than | A < B | 7 lt 8 |
|
||||
| `le`, `less_or_equals` | less than or equals | A <= B | 7 le 8, 8 le 8 |
|
||||
| `gt`, `greater_than` | greater than | A > B | 8 gt 7 |
|
||||
| `ge`, `greater_or_equals` | greater than or equals | A >= B | 8 ge 7, 8 ge 8 |
|
||||
| `ne`, `not_equal` | not equals | A != B | 6 ne 9 |
|
||||
| `str_eq`, `string_equals` | string equals | str(A) == str(B) | 123 str_eq '123' |
|
||||
| `len_eq`, `length_equals`, `length_equal` | length equals | len(A) == B | 'abc' len_eq 3, [1,2] len_eq 2 |
|
||||
| `len_gt`, `count_gt`, `length_greater_than` | length greater than | len(A) > B | 'abc' len_gt 2, [1,2,3] len_gt 2 |
|
||||
| `len_ge`, `count_ge`, `length_greater_or_equals` | length greater than or equals | len(A) >= B | 'abc' len_ge 3, [1,2,3] len_gt 3 |
|
||||
| `len_lt`, `count_lt`, `length_less_than` | length less than | len(A) < B | 'abc' len_lt 4, [1,2,3] len_lt 4 |
|
||||
| `len_le`, `count_le`, `length_less_or_equals` | length less than or equals | len(A) <= B | 'abc' len_le 3, [1,2,3] len_le 3 |
|
||||
| `contains` | contains | [1, 2] contains 1 | 'abc' contains 'a', [1,2,3] len_lt 4 |
|
||||
| `contained_by` | contained by | A in B | 'a' contained_by 'abc', 1 contained_by [1,2] |
|
||||
| `type_match` | A and B are in the same type | type(A) == type(B) | 123 type_match 1 |
|
||||
| `regex_match` | regex matches | re.match(B, A) | 'abcdef' regex_match 'a\w+d' |
|
||||
| `startswith` | starts with | A.startswith(B) is True | 'abc' startswith 'ab' |
|
||||
| `endswith` | ends with | A.endswith(B) is True | 'abc' endswith 'bc' |
|
||||
|
||||
## Builtin functions
|
||||
|
||||
| Name | Arguments | Description |
|
||||
| --- | --- | --- |
|
||||
| `get_timestamp` | () | get the thirteen-digit timestamp of current time. |
|
||||
| `sleep` | (n int) | sleep n seconds to simulate the thinking time. |
|
||||
| `gen_random_string` | (n int) | get the n-digit random string. |
|
||||
| `max` | (m,n int) | get the maximum of two numbers m and n. |
|
||||
| `md5` | (s string) | get the MD5 of the input string s. |
|
|
@ -0,0 +1,33 @@
|
|||
package hrp
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StepRequestExtraction implements IStep interface.
|
||||
type StepRequestExtraction struct {
|
||||
step *TStep
|
||||
}
|
||||
|
||||
// WithJmesPath sets the JMESPath expression to extract from the response.
|
||||
func (s *StepRequestExtraction) WithJmesPath(jmesPath string, varName string) *StepRequestExtraction {
|
||||
s.step.Extract[varName] = jmesPath
|
||||
return s
|
||||
}
|
||||
|
||||
// Validate switches to step validation.
|
||||
func (s *StepRequestExtraction) Validate() *StepRequestValidation {
|
||||
return &StepRequestValidation{
|
||||
step: s.step,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Name() string {
|
||||
return s.step.Name
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) Type() string {
|
||||
return fmt.Sprintf("request-%v", s.step.Request.Method)
|
||||
}
|
||||
|
||||
func (s *StepRequestExtraction) ToStruct() *TStep {
|
||||
return s.step
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# boomer
|
||||
|
||||
This module is initially forked from [myzhan/boomer] and made a lot of changes.
|
||||
|
||||
[myzhan/boomer]: https://github.com/myzhan/boomer
|
|
@ -0,0 +1,171 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// A Boomer is used to run tasks.
|
||||
type Boomer struct {
|
||||
localRunner *localRunner
|
||||
|
||||
cpuProfile string
|
||||
cpuProfileDuration time.Duration
|
||||
|
||||
memoryProfile string
|
||||
memoryProfileDuration time.Duration
|
||||
|
||||
disableKeepalive bool
|
||||
disableCompression bool
|
||||
}
|
||||
|
||||
// NewStandaloneBoomer returns a new Boomer, which can run without master.
|
||||
func NewStandaloneBoomer(spawnCount int, spawnRate float64) *Boomer {
|
||||
return &Boomer{
|
||||
localRunner: newLocalRunner(spawnCount, spawnRate),
|
||||
}
|
||||
}
|
||||
|
||||
// SetRateLimiter creates rate limiter with the given limit and burst.
|
||||
func (b *Boomer) SetRateLimiter(maxRPS int64, requestIncreaseRate string) {
|
||||
var rateLimiter RateLimiter
|
||||
var err error
|
||||
if requestIncreaseRate != "-1" {
|
||||
if maxRPS <= 0 {
|
||||
maxRPS = math.MaxInt64
|
||||
}
|
||||
log.Warn().Int64("maxRPS", maxRPS).Str("increaseRate", requestIncreaseRate).Msg("set ramp up rate limiter")
|
||||
rateLimiter, err = NewRampUpRateLimiter(maxRPS, requestIncreaseRate, time.Second)
|
||||
} else {
|
||||
if maxRPS > 0 {
|
||||
log.Warn().Int64("maxRPS", maxRPS).Msg("set stable rate limiter")
|
||||
rateLimiter = NewStableRateLimiter(maxRPS, time.Second)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create rate limiter")
|
||||
return
|
||||
}
|
||||
|
||||
if rateLimiter != nil {
|
||||
b.localRunner.rateLimitEnabled = true
|
||||
b.localRunner.rateLimiter = rateLimiter
|
||||
}
|
||||
}
|
||||
|
||||
// SetDisableKeepAlive disable keep-alive for tcp
|
||||
func (b *Boomer) SetDisableKeepAlive(disableKeepalive bool) {
|
||||
b.disableKeepalive = disableKeepalive
|
||||
}
|
||||
|
||||
// SetDisableCompression disable compression to prevent the Transport from requesting compression with an "Accept-Encoding: gzip"
|
||||
func (b *Boomer) SetDisableCompression(disableCompression bool) {
|
||||
b.disableCompression = disableCompression
|
||||
}
|
||||
|
||||
func (b *Boomer) GetDisableKeepAlive() bool {
|
||||
return b.disableKeepalive
|
||||
}
|
||||
|
||||
func (b *Boomer) GetDisableCompression() bool {
|
||||
return b.disableCompression
|
||||
}
|
||||
|
||||
// SetLoopCount set loop count for test.
|
||||
func (b *Boomer) SetLoopCount(loopCount int64) {
|
||||
b.localRunner.loop = &Loop{loopCount: loopCount}
|
||||
}
|
||||
|
||||
// AddOutput accepts outputs which implements the boomer.Output interface.
|
||||
func (b *Boomer) AddOutput(o Output) {
|
||||
b.localRunner.addOutput(o)
|
||||
}
|
||||
|
||||
// EnableCPUProfile will start cpu profiling after run.
|
||||
func (b *Boomer) EnableCPUProfile(cpuProfile string, duration time.Duration) {
|
||||
b.cpuProfile = cpuProfile
|
||||
b.cpuProfileDuration = duration
|
||||
}
|
||||
|
||||
// EnableMemoryProfile will start memory profiling after run.
|
||||
func (b *Boomer) EnableMemoryProfile(memoryProfile string, duration time.Duration) {
|
||||
b.memoryProfile = memoryProfile
|
||||
b.memoryProfileDuration = duration
|
||||
}
|
||||
|
||||
// EnableGracefulQuit catch SIGINT and SIGTERM signals to quit gracefully
|
||||
func (b *Boomer) EnableGracefulQuit() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-c
|
||||
b.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
// Run accepts a slice of Task and connects to the locust master.
|
||||
func (b *Boomer) Run(tasks ...*Task) {
|
||||
if b.cpuProfile != "" {
|
||||
err := startCPUProfile(b.cpuProfile, b.cpuProfileDuration)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to start cpu profiling")
|
||||
}
|
||||
}
|
||||
if b.memoryProfile != "" {
|
||||
err := startMemoryProfile(b.memoryProfile, b.memoryProfileDuration)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to start memory profiling")
|
||||
}
|
||||
}
|
||||
|
||||
b.localRunner.setTasks(tasks)
|
||||
b.localRunner.start()
|
||||
}
|
||||
|
||||
// RecordTransaction reports a transaction stat.
|
||||
func (b *Boomer) RecordTransaction(name string, success bool, elapsedTime int64, contentSize int64) {
|
||||
b.localRunner.stats.transactionChan <- &transaction{
|
||||
name: name,
|
||||
success: success,
|
||||
elapsedTime: elapsedTime,
|
||||
contentSize: contentSize,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSuccess reports a success.
|
||||
func (b *Boomer) RecordSuccess(requestType, name string, responseTime int64, responseLength int64) {
|
||||
b.localRunner.stats.requestSuccessChan <- &requestSuccess{
|
||||
requestType: requestType,
|
||||
name: name,
|
||||
responseTime: responseTime,
|
||||
responseLength: responseLength,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordFailure reports a failure.
|
||||
func (b *Boomer) RecordFailure(requestType, name string, responseTime int64, exception string) {
|
||||
b.localRunner.stats.requestFailureChan <- &requestFailure{
|
||||
requestType: requestType,
|
||||
name: name,
|
||||
responseTime: responseTime,
|
||||
errMsg: exception,
|
||||
}
|
||||
}
|
||||
|
||||
// Quit will send a quit message to the master.
|
||||
func (b *Boomer) Quit() {
|
||||
b.localRunner.stop()
|
||||
}
|
||||
|
||||
func (b *Boomer) GetSpawnDoneChan() chan struct{} {
|
||||
return b.localRunner.spawnDone
|
||||
}
|
||||
|
||||
func (b *Boomer) GetSpawnCount() int {
|
||||
return b.localRunner.spawnCount
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewStandaloneBoomer(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
|
||||
if b.localRunner.spawnCount != 100 {
|
||||
t.Error("spawnCount should be 100")
|
||||
}
|
||||
|
||||
if b.localRunner.spawnRate != 10 {
|
||||
t.Error("spawnRate should be 10")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRateLimiter(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.SetRateLimiter(10, "10/1s")
|
||||
|
||||
if b.localRunner.rateLimiter == nil {
|
||||
t.Error("b.rateLimiter should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddOutput(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.AddOutput(NewConsoleOutput())
|
||||
b.AddOutput(NewConsoleOutput())
|
||||
|
||||
if len(b.localRunner.outputs) != 2 {
|
||||
t.Error("length of outputs should be 2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableCPUProfile(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.EnableCPUProfile("cpu.prof", time.Second)
|
||||
|
||||
if b.cpuProfile != "cpu.prof" {
|
||||
t.Error("cpuProfile should be cpu.prof")
|
||||
}
|
||||
|
||||
if b.cpuProfileDuration != time.Second {
|
||||
t.Error("cpuProfileDuration should 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableMemoryProfile(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.EnableMemoryProfile("mem.prof", time.Second)
|
||||
|
||||
if b.memoryProfile != "mem.prof" {
|
||||
t.Error("memoryProfile should be mem.prof")
|
||||
}
|
||||
|
||||
if b.memoryProfileDuration != time.Second {
|
||||
t.Error("memoryProfileDuration should 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandaloneRun(t *testing.T) {
|
||||
b := NewStandaloneBoomer(10, 10)
|
||||
b.EnableCPUProfile("cpu.pprof", 2*time.Second)
|
||||
b.EnableMemoryProfile("mem.pprof", 2*time.Second)
|
||||
|
||||
count := int64(0)
|
||||
taskA := &Task{
|
||||
Name: "increaseCount",
|
||||
Fn: func() {
|
||||
atomic.AddInt64(&count, 1)
|
||||
runtime.Goexit()
|
||||
},
|
||||
}
|
||||
go b.Run(taskA)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
b.Quit()
|
||||
|
||||
if atomic.LoadInt64(&count) != 10 {
|
||||
t.Error("count is", count, "expected: 10")
|
||||
}
|
||||
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File cpu.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
|
||||
if _, err := os.Stat("mem.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File mem.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRatelimiter(t *testing.T) {
|
||||
b := NewStandaloneBoomer(10, 10)
|
||||
b.SetRateLimiter(100, "-1")
|
||||
|
||||
if stableRateLimiter, ok := b.localRunner.rateLimiter.(*StableRateLimiter); !ok {
|
||||
t.Error("Expected stableRateLimiter")
|
||||
} else {
|
||||
if stableRateLimiter.threshold != 100 {
|
||||
t.Error("threshold should be equals to math.MaxInt64, was", stableRateLimiter.threshold)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetRateLimiter(0, "1")
|
||||
if rampUpRateLimiter, ok := b.localRunner.rateLimiter.(*RampUpRateLimiter); !ok {
|
||||
t.Error("Expected rampUpRateLimiter")
|
||||
} else {
|
||||
if rampUpRateLimiter.maxThreshold != math.MaxInt64 {
|
||||
t.Error("maxThreshold should be equals to math.MaxInt64, was", rampUpRateLimiter.maxThreshold)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpRate != "1" {
|
||||
t.Error("rampUpRate should be equals to \"1\", was", rampUpRateLimiter.rampUpRate)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetRateLimiter(10, "2/2s")
|
||||
if rampUpRateLimiter, ok := b.localRunner.rateLimiter.(*RampUpRateLimiter); !ok {
|
||||
t.Error("Expected rampUpRateLimiter")
|
||||
} else {
|
||||
if rampUpRateLimiter.maxThreshold != 10 {
|
||||
t.Error("maxThreshold should be equals to 10, was", rampUpRateLimiter.maxThreshold)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpRate != "2/2s" {
|
||||
t.Error("rampUpRate should be equals to \"2/2s\", was", rampUpRateLimiter.rampUpRate)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpStep != 2 {
|
||||
t.Error("rampUpStep should be equals to 2, was", rampUpRateLimiter.rampUpStep)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpPeroid != 2*time.Second {
|
||||
t.Error("rampUpPeroid should be equals to 2 seconds, was", rampUpRateLimiter.rampUpPeroid)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,532 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/push"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
// Output is primarily responsible for printing test results to different destinations
|
||||
// such as consoles, files. You can write you own output and add to boomer.
|
||||
// When running in standalone mode, the default output is ConsoleOutput, you can add more.
|
||||
// When running in distribute mode, test results will be reported to master with or without
|
||||
// an output.
|
||||
// All the OnXXX function will be call in a separated goroutine, just in case some output will block.
|
||||
// But it will wait for all outputs return to avoid data lost.
|
||||
type Output interface {
|
||||
// OnStart will be call before the test starts.
|
||||
OnStart()
|
||||
|
||||
// By default, each output receive stats data from runner every three seconds.
|
||||
// OnEvent is responsible for dealing with the data.
|
||||
OnEvent(data map[string]interface{})
|
||||
|
||||
// OnStop will be called before the test ends.
|
||||
OnStop()
|
||||
}
|
||||
|
||||
// ConsoleOutput is the default output for standalone mode.
|
||||
type ConsoleOutput struct {
|
||||
}
|
||||
|
||||
// NewConsoleOutput returns a ConsoleOutput.
|
||||
func NewConsoleOutput() *ConsoleOutput {
|
||||
return &ConsoleOutput{}
|
||||
}
|
||||
|
||||
func getMedianResponseTime(numRequests int64, responseTimes map[int64]int64) int64 {
|
||||
medianResponseTime := int64(0)
|
||||
if len(responseTimes) != 0 {
|
||||
pos := (numRequests - 1) / 2
|
||||
var sortedKeys []int64
|
||||
for k := range responseTimes {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Slice(sortedKeys, func(i, j int) bool {
|
||||
return sortedKeys[i] < sortedKeys[j]
|
||||
})
|
||||
for _, k := range sortedKeys {
|
||||
if pos < responseTimes[k] {
|
||||
medianResponseTime = k
|
||||
break
|
||||
}
|
||||
pos -= responseTimes[k]
|
||||
}
|
||||
}
|
||||
return medianResponseTime
|
||||
}
|
||||
|
||||
func getAvgResponseTime(numRequests int64, totalResponseTime int64) (avgResponseTime float64) {
|
||||
avgResponseTime = float64(0)
|
||||
if numRequests != 0 {
|
||||
avgResponseTime = float64(totalResponseTime) / float64(numRequests)
|
||||
}
|
||||
return avgResponseTime
|
||||
}
|
||||
|
||||
func getAvgContentLength(numRequests int64, totalContentLength int64) (avgContentLength int64) {
|
||||
avgContentLength = int64(0)
|
||||
if numRequests != 0 {
|
||||
avgContentLength = totalContentLength / numRequests
|
||||
}
|
||||
return avgContentLength
|
||||
}
|
||||
|
||||
func getCurrentRps(numRequests int64, duration float64) (currentRps float64) {
|
||||
currentRps = float64(numRequests) / duration
|
||||
return currentRps
|
||||
}
|
||||
|
||||
func getCurrentFailPerSec(numFailures int64, duration float64) (currentFailPerSec float64) {
|
||||
currentFailPerSec = float64(numFailures) / duration
|
||||
return currentFailPerSec
|
||||
}
|
||||
|
||||
func getTotalFailRatio(totalRequests, totalFailures int64) (failRatio float64) {
|
||||
if totalRequests == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(totalFailures) / float64(totalRequests)
|
||||
}
|
||||
|
||||
// OnStart of ConsoleOutput has nothing to do.
|
||||
func (o *ConsoleOutput) OnStart() {
|
||||
|
||||
}
|
||||
|
||||
// OnStop of ConsoleOutput has nothing to do.
|
||||
func (o *ConsoleOutput) OnStop() {
|
||||
|
||||
}
|
||||
|
||||
// OnEvent will print to the console.
|
||||
func (o *ConsoleOutput) OnEvent(data map[string]interface{}) {
|
||||
output, err := convertData(data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to convert data")
|
||||
return
|
||||
}
|
||||
|
||||
var state string
|
||||
switch output.State {
|
||||
case stateInit:
|
||||
state = "initializing"
|
||||
case stateSpawning:
|
||||
state = "spawning"
|
||||
case stateRunning:
|
||||
state = "running"
|
||||
case stateQuitting:
|
||||
state = "quitting"
|
||||
case stateStopped:
|
||||
state = "stopped"
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
println(fmt.Sprintf("Current time: %s, Users: %d, State: %s, Total RPS: %.1f, Total Average Response Time: %.1fms, Total Fail Ratio: %.1f%%",
|
||||
currentTime.Format("2006/01/02 15:04:05"), output.UserCount, state, output.TotalRPS, output.TotalAvgResponseTime, output.TotalFailRatio*100))
|
||||
println(fmt.Sprintf("Accumulated Transactions: %d Passed, %d Failed",
|
||||
output.TransactionsPassed, output.TransactionsFailed))
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Type", "Name", "# requests", "# fails", "Median", "Average", "Min", "Max", "Content Size", "# reqs/sec", "# fails/sec"})
|
||||
|
||||
for _, stat := range output.Stats {
|
||||
row := make([]string, 11)
|
||||
row[0] = stat.Method
|
||||
row[1] = stat.Name
|
||||
row[2] = strconv.FormatInt(stat.NumRequests, 10)
|
||||
row[3] = strconv.FormatInt(stat.NumFailures, 10)
|
||||
row[4] = strconv.FormatInt(stat.medianResponseTime, 10)
|
||||
row[5] = strconv.FormatFloat(stat.avgResponseTime, 'f', 2, 64)
|
||||
row[6] = strconv.FormatInt(stat.MinResponseTime, 10)
|
||||
row[7] = strconv.FormatInt(stat.MaxResponseTime, 10)
|
||||
row[8] = strconv.FormatInt(stat.avgContentLength, 10)
|
||||
row[9] = strconv.FormatFloat(stat.currentRps, 'f', 2, 64)
|
||||
row[10] = strconv.FormatFloat(stat.currentFailPerSec, 'f', 2, 64)
|
||||
table.Append(row)
|
||||
}
|
||||
table.Render()
|
||||
println()
|
||||
}
|
||||
|
||||
type statsEntryOutput struct {
|
||||
statsEntry
|
||||
|
||||
medianResponseTime int64 // median response time
|
||||
avgResponseTime float64 // average response time, round float to 2 decimal places
|
||||
avgContentLength int64 // average content size
|
||||
currentRps float64 // # reqs/sec
|
||||
currentFailPerSec float64 // # fails/sec
|
||||
}
|
||||
|
||||
type dataOutput struct {
|
||||
UserCount int32 `json:"user_count"`
|
||||
State int32 `json:"state"`
|
||||
TotalStats *statsEntryOutput `json:"stats_total"`
|
||||
TransactionsPassed int64 `json:"transactions_passed"`
|
||||
TransactionsFailed int64 `json:"transactions_failed"`
|
||||
TotalAvgResponseTime float64 `json:"total_avg_response_time"`
|
||||
TotalRPS float64 `json:"total_rps"`
|
||||
TotalFailRatio float64 `json:"total_fail_ratio"`
|
||||
Stats []*statsEntryOutput `json:"stats"`
|
||||
Errors map[string]map[string]interface{} `json:"errors"`
|
||||
}
|
||||
|
||||
func convertData(data map[string]interface{}) (output *dataOutput, err error) {
|
||||
userCount, ok := data["user_count"].(int32)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user_count is not int32")
|
||||
}
|
||||
state, ok := data["state"].(int32)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("state is not int32")
|
||||
}
|
||||
stats, ok := data["stats"].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("stats is not []interface{}")
|
||||
}
|
||||
|
||||
errors := data["errors"].(map[string]map[string]interface{})
|
||||
|
||||
transactions, ok := data["transactions"].(map[string]int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("transactions is not map[string]int64")
|
||||
}
|
||||
transactionsPassed := transactions["passed"]
|
||||
transactionsFailed := transactions["failed"]
|
||||
|
||||
// convert stats in total
|
||||
statsTotal, ok := data["stats_total"].(interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("stats_total is not interface{}")
|
||||
}
|
||||
entryTotalOutput, err := deserializeStatsEntry(statsTotal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output = &dataOutput{
|
||||
UserCount: userCount,
|
||||
State: state,
|
||||
TotalStats: entryTotalOutput,
|
||||
TransactionsPassed: transactionsPassed,
|
||||
TransactionsFailed: transactionsFailed,
|
||||
TotalAvgResponseTime: entryTotalOutput.avgResponseTime,
|
||||
TotalRPS: entryTotalOutput.currentRps,
|
||||
TotalFailRatio: getTotalFailRatio(entryTotalOutput.NumRequests, entryTotalOutput.NumFailures),
|
||||
Stats: make([]*statsEntryOutput, 0, len(stats)),
|
||||
Errors: errors,
|
||||
}
|
||||
|
||||
// convert stats
|
||||
for _, stat := range stats {
|
||||
entryOutput, err := deserializeStatsEntry(stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output.Stats = append(output.Stats, entryOutput)
|
||||
}
|
||||
// sort stats by type
|
||||
sort.Slice(output.Stats, func(i, j int) bool {
|
||||
return output.Stats[i].Method < output.Stats[j].Method
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func deserializeStatsEntry(stat interface{}) (entryOutput *statsEntryOutput, err error) {
|
||||
statBytes, err := json.Marshal(stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := statsEntry{}
|
||||
if err = json.Unmarshal(statBytes, &entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var duration float64
|
||||
if entry.Name == "Total" {
|
||||
duration = float64(entry.LastRequestTimestamp - entry.StartTime)
|
||||
// fix: avoid divide by zero
|
||||
if duration < 1 {
|
||||
duration = 1
|
||||
}
|
||||
} else {
|
||||
duration = float64(reportStatsInterval / time.Second)
|
||||
}
|
||||
|
||||
numRequests := entry.NumRequests
|
||||
entryOutput = &statsEntryOutput{
|
||||
statsEntry: entry,
|
||||
medianResponseTime: getMedianResponseTime(numRequests, entry.ResponseTimes),
|
||||
avgResponseTime: getAvgResponseTime(numRequests, entry.TotalResponseTime),
|
||||
avgContentLength: getAvgContentLength(numRequests, entry.TotalContentLength),
|
||||
currentRps: getCurrentRps(numRequests, duration),
|
||||
currentFailPerSec: getCurrentFailPerSec(entry.NumFailures, duration),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// gauge vectors for requests
|
||||
var (
|
||||
gaugeNumRequests = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "num_requests",
|
||||
Help: "The number of requests",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeNumFailures = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "num_failures",
|
||||
Help: "The number of failures",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMedianResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "median_response_time",
|
||||
Help: "The median response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeAverageResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "average_response_time",
|
||||
Help: "The average response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMinResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "min_response_time",
|
||||
Help: "The min response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMaxResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "max_response_time",
|
||||
Help: "The max response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeAverageContentLength = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "average_content_length",
|
||||
Help: "The average content length",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeCurrentRPS = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "current_rps",
|
||||
Help: "The current requests per second",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeCurrentFailPerSec = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "current_fail_per_sec",
|
||||
Help: "The current failure number per second",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
)
|
||||
|
||||
// counter for total
|
||||
var (
|
||||
counterErrors = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "errors",
|
||||
Help: "The errors of load testing",
|
||||
},
|
||||
[]string{"method", "name", "error"},
|
||||
)
|
||||
)
|
||||
|
||||
// summary for total
|
||||
var (
|
||||
summaryResponseTime = prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "response_time",
|
||||
Help: "The summary of response time",
|
||||
Objectives: map[float64]float64{
|
||||
0.5: 0.01,
|
||||
0.9: 0.01,
|
||||
0.95: 0.005,
|
||||
},
|
||||
AgeBuckets: 1,
|
||||
MaxAge: 100000 * time.Second,
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
)
|
||||
|
||||
// gauges for total
|
||||
var (
|
||||
gaugeUsers = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "users",
|
||||
Help: "The current number of users",
|
||||
},
|
||||
)
|
||||
gaugeState = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "state",
|
||||
Help: "The current runner state, 1=initializing, 2=spawning, 3=running, 4=quitting, 5=stopped",
|
||||
},
|
||||
)
|
||||
gaugeTotalAverageResponseTime = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_average_response_time",
|
||||
Help: "The average response time in total milliseconds",
|
||||
},
|
||||
)
|
||||
gaugeTotalRPS = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_rps",
|
||||
Help: "The requests per second in total",
|
||||
},
|
||||
)
|
||||
gaugeTotalFailRatio = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "fail_ratio",
|
||||
Help: "The ratio of request failures in total",
|
||||
},
|
||||
)
|
||||
gaugeTransactionsPassed = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "transactions_passed",
|
||||
Help: "The accumulated number of passed transactions",
|
||||
},
|
||||
)
|
||||
gaugeTransactionsFailed = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "transactions_failed",
|
||||
Help: "The accumulated number of failed transactions",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// NewPrometheusPusherOutput returns a PrometheusPusherOutput.
|
||||
func NewPrometheusPusherOutput(gatewayURL, jobName string) *PrometheusPusherOutput {
|
||||
nodeUUID, _ := uuid.NewUUID()
|
||||
return &PrometheusPusherOutput{
|
||||
pusher: push.New(gatewayURL, jobName).Grouping("instance", nodeUUID.String()),
|
||||
}
|
||||
}
|
||||
|
||||
// PrometheusPusherOutput pushes boomer stats to Prometheus Pushgateway.
|
||||
type PrometheusPusherOutput struct {
|
||||
pusher *push.Pusher // Prometheus Pushgateway Pusher
|
||||
}
|
||||
|
||||
// OnStart will register all prometheus metric collectors
|
||||
func (o *PrometheusPusherOutput) OnStart() {
|
||||
log.Info().Msg("register prometheus metric collectors")
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(
|
||||
// gauge vectors for requests
|
||||
gaugeNumRequests,
|
||||
gaugeNumFailures,
|
||||
gaugeMedianResponseTime,
|
||||
gaugeAverageResponseTime,
|
||||
gaugeMinResponseTime,
|
||||
gaugeMaxResponseTime,
|
||||
gaugeAverageContentLength,
|
||||
gaugeCurrentRPS,
|
||||
gaugeCurrentFailPerSec,
|
||||
// counter for total
|
||||
counterErrors,
|
||||
// summary for total
|
||||
summaryResponseTime,
|
||||
// gauges for total
|
||||
gaugeUsers,
|
||||
gaugeState,
|
||||
gaugeTotalAverageResponseTime,
|
||||
gaugeTotalRPS,
|
||||
gaugeTotalFailRatio,
|
||||
gaugeTransactionsPassed,
|
||||
gaugeTransactionsFailed,
|
||||
)
|
||||
o.pusher = o.pusher.Gatherer(registry)
|
||||
}
|
||||
|
||||
// OnStop of PrometheusPusherOutput has nothing to do.
|
||||
func (o *PrometheusPusherOutput) OnStop() {
|
||||
// update runner state: stopped
|
||||
gaugeState.Set(float64(stateStopped))
|
||||
if err := o.pusher.Push(); err != nil {
|
||||
log.Error().Err(err).Msg("push to Pushgateway failed")
|
||||
}
|
||||
}
|
||||
|
||||
// OnEvent will push metric to Prometheus Pushgataway
|
||||
func (o *PrometheusPusherOutput) OnEvent(data map[string]interface{}) {
|
||||
output, err := convertData(data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to convert data")
|
||||
return
|
||||
}
|
||||
|
||||
// user count
|
||||
gaugeUsers.Set(float64(output.UserCount))
|
||||
|
||||
// runner state
|
||||
gaugeState.Set(float64(output.State))
|
||||
|
||||
// avg response time in total
|
||||
gaugeTotalAverageResponseTime.Set(output.TotalAvgResponseTime)
|
||||
|
||||
// rps in total
|
||||
gaugeTotalRPS.Set(output.TotalRPS)
|
||||
|
||||
// failure ratio in total
|
||||
gaugeTotalFailRatio.Set(output.TotalFailRatio)
|
||||
|
||||
// accumulated number of transactions
|
||||
gaugeTransactionsPassed.Set(float64(output.TransactionsPassed))
|
||||
gaugeTransactionsFailed.Set(float64(output.TransactionsFailed))
|
||||
|
||||
for _, stat := range output.Stats {
|
||||
method := stat.Method
|
||||
name := stat.Name
|
||||
gaugeNumRequests.WithLabelValues(method, name).Set(float64(stat.NumRequests))
|
||||
gaugeNumFailures.WithLabelValues(method, name).Set(float64(stat.NumFailures))
|
||||
gaugeMedianResponseTime.WithLabelValues(method, name).Set(float64(stat.medianResponseTime))
|
||||
gaugeAverageResponseTime.WithLabelValues(method, name).Set(float64(stat.avgResponseTime))
|
||||
gaugeMinResponseTime.WithLabelValues(method, name).Set(float64(stat.MinResponseTime))
|
||||
gaugeMaxResponseTime.WithLabelValues(method, name).Set(float64(stat.MaxResponseTime))
|
||||
gaugeAverageContentLength.WithLabelValues(method, name).Set(float64(stat.avgContentLength))
|
||||
gaugeCurrentRPS.WithLabelValues(method, name).Set(stat.currentRps)
|
||||
gaugeCurrentFailPerSec.WithLabelValues(method, name).Set(stat.currentFailPerSec)
|
||||
for responseTime, count := range stat.ResponseTimes {
|
||||
var i int64
|
||||
for i = 0; i < count; i++ {
|
||||
summaryResponseTime.WithLabelValues(method, name).Observe(float64(responseTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errors
|
||||
for _, requestError := range output.Errors {
|
||||
counterErrors.WithLabelValues(
|
||||
requestError["method"].(string),
|
||||
requestError["name"].(string),
|
||||
requestError["error"].(string),
|
||||
).Add(float64(requestError["occurrences"].(int64)))
|
||||
}
|
||||
|
||||
if err := o.pusher.Push(); err != nil {
|
||||
log.Error().Err(err).Msg("push to Pushgateway failed")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetMedianResponseTime(t *testing.T) {
|
||||
numRequests := int64(10)
|
||||
responseTimes := map[int64]int64{
|
||||
100: 1,
|
||||
200: 3,
|
||||
300: 6,
|
||||
}
|
||||
|
||||
medianResponseTime := getMedianResponseTime(numRequests, responseTimes)
|
||||
if medianResponseTime != 300 {
|
||||
t.Error("medianResponseTime should be 300")
|
||||
}
|
||||
|
||||
responseTimes = map[int64]int64{}
|
||||
|
||||
medianResponseTime = getMedianResponseTime(numRequests, responseTimes)
|
||||
if medianResponseTime != 0 {
|
||||
t.Error("medianResponseTime should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvgResponseTime(t *testing.T) {
|
||||
numRequests := int64(3)
|
||||
totalResponseTime := int64(100)
|
||||
|
||||
avgResponseTime := getAvgResponseTime(numRequests, totalResponseTime)
|
||||
if math.Dim(float64(33.33), avgResponseTime) > 0.01 {
|
||||
t.Error("avgResponseTime should be close to 33.33")
|
||||
}
|
||||
|
||||
avgResponseTime = getAvgResponseTime(int64(0), totalResponseTime)
|
||||
if avgResponseTime != float64(0) {
|
||||
t.Error("avgResponseTime should be close to 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvgContentLength(t *testing.T) {
|
||||
numRequests := int64(3)
|
||||
totalContentLength := int64(100)
|
||||
|
||||
avgContentLength := getAvgContentLength(numRequests, totalContentLength)
|
||||
if avgContentLength != 33 {
|
||||
t.Error("avgContentLength should be 33")
|
||||
}
|
||||
|
||||
avgContentLength = getAvgContentLength(int64(0), totalContentLength)
|
||||
if avgContentLength != 0 {
|
||||
t.Error("avgContentLength should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentRps(t *testing.T) {
|
||||
duration := float64(3)
|
||||
numRequests := int64(6)
|
||||
currentRps := getCurrentRps(numRequests, duration)
|
||||
if currentRps != 2 {
|
||||
t.Error("currentRps should be 2")
|
||||
}
|
||||
|
||||
numRequests = int64(8)
|
||||
currentRps = getCurrentRps(numRequests, duration)
|
||||
if fmt.Sprintf("%.2f", currentRps) != "2.67" {
|
||||
t.Error("currentRps should be 2.67")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleOutput(t *testing.T) {
|
||||
o := NewConsoleOutput()
|
||||
o.OnStart()
|
||||
|
||||
data := map[string]interface{}{}
|
||||
stat := map[string]interface{}{}
|
||||
data["stats"] = []interface{}{stat}
|
||||
|
||||
stat["name"] = "http"
|
||||
stat["method"] = "post"
|
||||
stat["num_requests"] = int64(100)
|
||||
stat["num_failures"] = int64(10)
|
||||
stat["response_times"] = map[int64]int64{
|
||||
10: 1,
|
||||
100: 99,
|
||||
}
|
||||
stat["total_response_time"] = int64(9910)
|
||||
stat["min_response_time"] = int64(10)
|
||||
stat["max_response_time"] = int64(100)
|
||||
stat["total_content_length"] = int64(100000)
|
||||
stat["num_reqs_per_sec"] = map[int64]int64{
|
||||
1: 20,
|
||||
2: 40,
|
||||
3: 40,
|
||||
}
|
||||
|
||||
o.OnEvent(data)
|
||||
|
||||
o.OnStop()
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiter is used to put limits on task executions.
|
||||
type RateLimiter interface {
|
||||
// Start is used to enable the rate limiter.
|
||||
// It can be implemented as a noop if not needed.
|
||||
Start()
|
||||
|
||||
// Acquire() is called before executing a task.Fn function.
|
||||
// If Acquire() returns true, the task.Fn function will be executed.
|
||||
// If Acquire() returns false, the task.Fn function won't be executed this time, but Acquire() will be called very soon.
|
||||
// It works like:
|
||||
// for {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// task.Fn()
|
||||
// }
|
||||
// }
|
||||
// Acquire() should block the caller until execution is allowed.
|
||||
Acquire() bool
|
||||
|
||||
// Stop is used to disable the rate limiter.
|
||||
// It can be implemented as a noop if not needed.
|
||||
Stop()
|
||||
}
|
||||
|
||||
// A StableRateLimiter uses the token bucket algorithm.
|
||||
// the bucket is refilled according to the refill period, no burst is allowed.
|
||||
type StableRateLimiter struct {
|
||||
threshold int64
|
||||
currentThreshold int64
|
||||
refillPeriod time.Duration
|
||||
broadcastChanMux *sync.RWMutex // avoid data race
|
||||
broadcastChannel chan bool
|
||||
quitChannel chan bool
|
||||
}
|
||||
|
||||
// NewStableRateLimiter returns a StableRateLimiter.
|
||||
func NewStableRateLimiter(threshold int64, refillPeriod time.Duration) (rateLimiter *StableRateLimiter) {
|
||||
rateLimiter = &StableRateLimiter{
|
||||
threshold: threshold,
|
||||
currentThreshold: threshold,
|
||||
refillPeriod: refillPeriod,
|
||||
broadcastChanMux: new(sync.RWMutex),
|
||||
broadcastChannel: make(chan bool),
|
||||
}
|
||||
return rateLimiter
|
||||
}
|
||||
|
||||
// Start to refill the bucket periodically.
|
||||
func (limiter *StableRateLimiter) Start() {
|
||||
limiter.quitChannel = make(chan bool)
|
||||
quitChannel := limiter.quitChannel
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
atomic.StoreInt64(&limiter.currentThreshold, limiter.threshold)
|
||||
time.Sleep(limiter.refillPeriod)
|
||||
close(limiter.broadcastChannel)
|
||||
// avoid data race
|
||||
limiter.broadcastChanMux.Lock()
|
||||
limiter.broadcastChannel = make(chan bool)
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Acquire a token from the bucket, returns true if the bucket is exhausted.
|
||||
func (limiter *StableRateLimiter) Acquire() (blocked bool) {
|
||||
permit := atomic.AddInt64(&limiter.currentThreshold, -1)
|
||||
if permit < 0 {
|
||||
blocked = true
|
||||
// block until the bucket is refilled
|
||||
limiter.broadcastChanMux.Lock()
|
||||
<-limiter.broadcastChannel
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
} else {
|
||||
blocked = false
|
||||
}
|
||||
return blocked
|
||||
}
|
||||
|
||||
// Stop the rate limiter.
|
||||
func (limiter *StableRateLimiter) Stop() {
|
||||
close(limiter.quitChannel)
|
||||
}
|
||||
|
||||
// ErrParsingRampUpRate is the error returned if the format of rampUpRate is invalid.
|
||||
var ErrParsingRampUpRate = errors.New("ratelimiter: invalid format of rampUpRate, try \"1\" or \"1/1s\"")
|
||||
|
||||
// A RampUpRateLimiter uses the token bucket algorithm.
|
||||
// the threshold is updated according to the warm up rate.
|
||||
// the bucket is refilled according to the refill period, no burst is allowed.
|
||||
type RampUpRateLimiter struct {
|
||||
maxThreshold int64
|
||||
nextThreshold int64
|
||||
currentThreshold int64
|
||||
refillPeriod time.Duration
|
||||
rampUpRate string
|
||||
rampUpStep int64
|
||||
rampUpPeroid time.Duration
|
||||
|
||||
broadcastChanMux *sync.RWMutex // avoid data race
|
||||
broadcastChannel chan bool
|
||||
|
||||
rampUpChannel chan bool
|
||||
quitChannel chan bool
|
||||
}
|
||||
|
||||
// NewRampUpRateLimiter returns a RampUpRateLimiter.
|
||||
// Valid formats of rampUpRate are "1", "1/1s".
|
||||
func NewRampUpRateLimiter(maxThreshold int64, rampUpRate string, refillPeriod time.Duration) (rateLimiter *RampUpRateLimiter, err error) {
|
||||
rateLimiter = &RampUpRateLimiter{
|
||||
maxThreshold: maxThreshold,
|
||||
nextThreshold: 0,
|
||||
currentThreshold: 0,
|
||||
rampUpRate: rampUpRate,
|
||||
refillPeriod: refillPeriod,
|
||||
broadcastChanMux: new(sync.RWMutex),
|
||||
broadcastChannel: make(chan bool),
|
||||
}
|
||||
rateLimiter.rampUpStep, rateLimiter.rampUpPeroid, err = rateLimiter.parseRampUpRate(rateLimiter.rampUpRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rateLimiter, nil
|
||||
}
|
||||
|
||||
func (limiter *RampUpRateLimiter) parseRampUpRate(rampUpRate string) (rampUpStep int64, rampUpPeroid time.Duration, err error) {
|
||||
if strings.Contains(rampUpRate, "/") {
|
||||
tmp := strings.Split(rampUpRate, "/")
|
||||
if len(tmp) != 2 {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpStep, err := strconv.ParseInt(tmp[0], 10, 64)
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpPeroid, err := time.ParseDuration(tmp[1])
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
return rampUpStep, rampUpPeroid, nil
|
||||
}
|
||||
|
||||
rampUpStep, err = strconv.ParseInt(rampUpRate, 10, 64)
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpPeroid = time.Second
|
||||
return rampUpStep, rampUpPeroid, nil
|
||||
}
|
||||
|
||||
// Start to refill the bucket periodically.
|
||||
func (limiter *RampUpRateLimiter) Start() {
|
||||
limiter.quitChannel = make(chan bool)
|
||||
quitChannel := limiter.quitChannel
|
||||
// bucket updater
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
atomic.StoreInt64(&limiter.currentThreshold, atomic.LoadInt64(&limiter.nextThreshold))
|
||||
time.Sleep(limiter.refillPeriod)
|
||||
close(limiter.broadcastChannel)
|
||||
// avoid data race
|
||||
limiter.broadcastChanMux.Lock()
|
||||
limiter.broadcastChannel = make(chan bool)
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
// threshold updater
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
nextValue := atomic.LoadInt64(&limiter.nextThreshold) + limiter.rampUpStep
|
||||
if nextValue < 0 {
|
||||
// int64 overflow
|
||||
nextValue = int64(math.MaxInt64)
|
||||
}
|
||||
if nextValue > limiter.maxThreshold {
|
||||
nextValue = limiter.maxThreshold
|
||||
}
|
||||
atomic.StoreInt64(&limiter.nextThreshold, nextValue)
|
||||
time.Sleep(limiter.rampUpPeroid)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Acquire a token from the bucket, returns true if the bucket is exhausted.
|
||||
func (limiter *RampUpRateLimiter) Acquire() (blocked bool) {
|
||||
permit := atomic.AddInt64(&limiter.currentThreshold, -1)
|
||||
if permit < 0 {
|
||||
blocked = true
|
||||
// block until the bucket is refilled
|
||||
limiter.broadcastChanMux.Lock()
|
||||
<-limiter.broadcastChannel
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
} else {
|
||||
blocked = false
|
||||
}
|
||||
return blocked
|
||||
}
|
||||
|
||||
// Stop the rate limiter.
|
||||
func (limiter *RampUpRateLimiter) Stop() {
|
||||
atomic.StoreInt64(&limiter.nextThreshold, 0)
|
||||
close(limiter.quitChannel)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStableRateLimiter(t *testing.T) {
|
||||
rateLimiter := NewStableRateLimiter(1, 10*time.Millisecond)
|
||||
rateLimiter.Start()
|
||||
defer rateLimiter.Stop()
|
||||
|
||||
blocked := rateLimiter.Acquire()
|
||||
if blocked {
|
||||
t.Error("Unexpected blocked by rate limiter")
|
||||
}
|
||||
blocked = rateLimiter.Acquire()
|
||||
if !blocked {
|
||||
t.Error("Should be blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// func TestRampUpRateLimiter(t *testing.T) {
|
||||
// rateLimiter, _ := NewRampUpRateLimiter(100, "10/200ms", 100*time.Millisecond)
|
||||
// rateLimiter.Start()
|
||||
// defer rateLimiter.Stop()
|
||||
|
||||
// time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// for i := 0; i < 10; i++ {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if blocked {
|
||||
// t.Fatal("Unexpected blocked by rate limiter")
|
||||
// }
|
||||
// }
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// t.Fatal("Should be blocked")
|
||||
// }
|
||||
|
||||
// time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// // now, the threshold is 20
|
||||
// for i := 0; i < 20; i++ {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if blocked {
|
||||
// t.Fatal("Unexpected blocked by rate limiter")
|
||||
// }
|
||||
// }
|
||||
// blocked = rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// t.Fatal("Should be blocked")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestParseRampUpRate(t *testing.T) {
|
||||
rateLimiter := &RampUpRateLimiter{}
|
||||
rampUpStep, rampUpPeriod, _ := rateLimiter.parseRampUpRate("100")
|
||||
if rampUpStep != 100 {
|
||||
t.Error("Wrong rampUpStep, expected: 100, was:", rampUpStep)
|
||||
}
|
||||
if rampUpPeriod != time.Second {
|
||||
t.Error("Wrong rampUpPeriod, expected: 1s, was:", rampUpPeriod)
|
||||
}
|
||||
rampUpStep, rampUpPeriod, _ = rateLimiter.parseRampUpRate("200/10s")
|
||||
if rampUpStep != 200 {
|
||||
t.Error("Wrong rampUpStep, expected: 200, was:", rampUpStep)
|
||||
}
|
||||
if rampUpPeriod != 10*time.Second {
|
||||
t.Error("Wrong rampUpPeriod, expected: 10s, was:", rampUpPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidRampUpRate(t *testing.T) {
|
||||
rateLimiter := &RampUpRateLimiter{}
|
||||
|
||||
_, _, err := rateLimiter.parseRampUpRate("A/1m")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("A")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("200/1s/")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("200/1")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
rateLimiter, err = NewRampUpRateLimiter(1, "200/1", time.Second)
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
stateInit = iota + 1 // initializing
|
||||
stateSpawning // spawning
|
||||
stateRunning // running
|
||||
stateQuitting // quitting
|
||||
stateStopped // stopped
|
||||
)
|
||||
|
||||
const (
|
||||
reportStatsInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
loopCount int64 // more than 0
|
||||
acquiredCount int64 // count acquired of load testing
|
||||
finishedCount int64 // count finished of load testing
|
||||
}
|
||||
|
||||
func (l *Loop) isFinished() bool {
|
||||
// return true when there are no remaining loop count to test
|
||||
return atomic.LoadInt64(&l.finishedCount) == l.loopCount
|
||||
}
|
||||
|
||||
func (l *Loop) acquire() bool {
|
||||
// get one ticket when there are still remaining loop count to test
|
||||
// return true when getting ticket successfully
|
||||
if atomic.LoadInt64(&l.acquiredCount) < l.loopCount {
|
||||
atomic.AddInt64(&l.acquiredCount, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Loop) increaseFinishedCount() {
|
||||
atomic.AddInt64(&l.finishedCount, 1)
|
||||
}
|
||||
|
||||
type runner struct {
|
||||
state int32
|
||||
|
||||
tasks []*Task
|
||||
totalTaskWeight int
|
||||
|
||||
rateLimiter RateLimiter
|
||||
rateLimitEnabled bool
|
||||
stats *requestStats
|
||||
|
||||
currentClientsNum int32 // current clients count
|
||||
spawnCount int // target clients to spawn
|
||||
spawnRate float64
|
||||
loop *Loop // specify running cycles
|
||||
spawnDone chan struct{}
|
||||
|
||||
outputs []Output
|
||||
}
|
||||
|
||||
// safeRun runs fn and recovers from unexpected panics.
|
||||
// it prevents panics from Task.Fn crashing boomer.
|
||||
func (r *runner) safeRun(fn func()) {
|
||||
defer func() {
|
||||
// don't panic
|
||||
err := recover()
|
||||
if err != nil {
|
||||
stackTrace := debug.Stack()
|
||||
errMsg := fmt.Sprintf("%v", err)
|
||||
os.Stderr.Write([]byte(errMsg))
|
||||
os.Stderr.Write([]byte("\n"))
|
||||
os.Stderr.Write(stackTrace)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
|
||||
func (r *runner) addOutput(o Output) {
|
||||
r.outputs = append(r.outputs, o)
|
||||
}
|
||||
|
||||
func (r *runner) outputOnStart() {
|
||||
size := len(r.outputs)
|
||||
if size == 0 {
|
||||
return
|
||||
}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(size)
|
||||
for _, output := range r.outputs {
|
||||
go func(o Output) {
|
||||
o.OnStart()
|
||||
wg.Done()
|
||||
}(output)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *runner) outputOnEvent(data map[string]interface{}) {
|
||||
size := len(r.outputs)
|
||||
if size == 0 {
|
||||
return
|
||||
}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(size)
|
||||
for _, output := range r.outputs {
|
||||
go func(o Output) {
|
||||
o.OnEvent(data)
|
||||
wg.Done()
|
||||
}(output)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *runner) outputOnStop() {
|
||||
size := len(r.outputs)
|
||||
if size == 0 {
|
||||
return
|
||||
}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(size)
|
||||
for _, output := range r.outputs {
|
||||
go func(o Output) {
|
||||
o.OnStop()
|
||||
wg.Done()
|
||||
}(output)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *runner) reportStats() {
|
||||
data := r.stats.collectReportData()
|
||||
data["user_count"] = atomic.LoadInt32(&r.currentClientsNum)
|
||||
data["state"] = atomic.LoadInt32(&r.state)
|
||||
r.outputOnEvent(data)
|
||||
}
|
||||
|
||||
func (r *runner) reportTestResult() {
|
||||
// convert stats in total
|
||||
var statsTotal interface{} = r.stats.total.serialize()
|
||||
entryTotalOutput, err := deserializeStatsEntry(statsTotal)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
duration := time.Duration(entryTotalOutput.LastRequestTimestamp-entryTotalOutput.StartTime) * time.Second
|
||||
currentTime := time.Now()
|
||||
println(fmt.Sprint("=========================================== Statistics Summary =========================================="))
|
||||
println(fmt.Sprintf("Current time: %s, Users: %v, Duration: %v, Accumulated Transactions: %d Passed, %d Failed",
|
||||
currentTime.Format("2006/01/02 15:04:05"), atomic.LoadInt32(&r.currentClientsNum), duration, r.stats.transactionPassed, r.stats.transactionFailed))
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Name", "# requests", "# fails", "Median", "Average", "Min", "Max", "Content Size", "# reqs/sec", "# fails/sec"})
|
||||
row := make([]string, 10)
|
||||
row[0] = entryTotalOutput.Name
|
||||
row[1] = strconv.FormatInt(entryTotalOutput.NumRequests, 10)
|
||||
row[2] = strconv.FormatInt(entryTotalOutput.NumFailures, 10)
|
||||
row[3] = strconv.FormatInt(entryTotalOutput.medianResponseTime, 10)
|
||||
row[4] = strconv.FormatFloat(entryTotalOutput.avgResponseTime, 'f', 2, 64)
|
||||
row[5] = strconv.FormatInt(entryTotalOutput.MinResponseTime, 10)
|
||||
row[6] = strconv.FormatInt(entryTotalOutput.MaxResponseTime, 10)
|
||||
row[7] = strconv.FormatInt(entryTotalOutput.avgContentLength, 10)
|
||||
row[8] = strconv.FormatFloat(entryTotalOutput.currentRps, 'f', 2, 64)
|
||||
row[9] = strconv.FormatFloat(entryTotalOutput.currentFailPerSec, 'f', 2, 64)
|
||||
table.Append(row)
|
||||
table.Render()
|
||||
println()
|
||||
}
|
||||
|
||||
func (r *localRunner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool, spawnCompleteFunc func()) {
|
||||
log.Info().
|
||||
Int("spawnCount", spawnCount).
|
||||
Float64("spawnRate", spawnRate).
|
||||
Msg("Spawning workers")
|
||||
|
||||
atomic.StoreInt32(&r.state, stateSpawning)
|
||||
for i := 1; i <= spawnCount; i++ {
|
||||
// spawn workers with rate limit
|
||||
sleepTime := time.Duration(1000000/r.spawnRate) * time.Microsecond
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
select {
|
||||
case <-quit:
|
||||
// quit spawning goroutine
|
||||
log.Info().Msg("Quitting spawning workers")
|
||||
return
|
||||
default:
|
||||
atomic.AddInt32(&r.currentClientsNum, 1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
default:
|
||||
if r.loop != nil && !r.loop.acquire() {
|
||||
return
|
||||
}
|
||||
if r.rateLimitEnabled {
|
||||
blocked := r.rateLimiter.Acquire()
|
||||
if !blocked {
|
||||
task := r.getTask()
|
||||
r.safeRun(task.Fn)
|
||||
}
|
||||
} else {
|
||||
task := r.getTask()
|
||||
r.safeRun(task.Fn)
|
||||
}
|
||||
if r.loop != nil {
|
||||
r.loop.increaseFinishedCount()
|
||||
if r.loop.isFinished() {
|
||||
r.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
close(r.spawnDone)
|
||||
if spawnCompleteFunc != nil {
|
||||
spawnCompleteFunc()
|
||||
}
|
||||
atomic.StoreInt32(&r.state, stateRunning)
|
||||
}
|
||||
|
||||
// setTasks will set the runner's task list AND the total task weight
|
||||
// which is used to get a random task later
|
||||
func (r *runner) setTasks(t []*Task) {
|
||||
r.tasks = t
|
||||
|
||||
weightSum := 0
|
||||
for _, task := range r.tasks {
|
||||
weightSum += task.Weight
|
||||
}
|
||||
r.totalTaskWeight = weightSum
|
||||
}
|
||||
|
||||
func (r *runner) getTask() *Task {
|
||||
tasksCount := len(r.tasks)
|
||||
if tasksCount == 1 {
|
||||
// Fast path
|
||||
return r.tasks[0]
|
||||
}
|
||||
|
||||
rs := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
totalWeight := r.totalTaskWeight
|
||||
if totalWeight <= 0 {
|
||||
// If all the tasks have not weights defined, they have the same chance to run
|
||||
randNum := rs.Intn(tasksCount)
|
||||
return r.tasks[randNum]
|
||||
}
|
||||
|
||||
randNum := rs.Intn(totalWeight)
|
||||
runningSum := 0
|
||||
for _, task := range r.tasks {
|
||||
runningSum += task.Weight
|
||||
if runningSum > randNum {
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type localRunner struct {
|
||||
runner
|
||||
|
||||
// close this channel will stop all goroutines used in runner.
|
||||
stopChan chan bool
|
||||
}
|
||||
|
||||
func newLocalRunner(spawnCount int, spawnRate float64) *localRunner {
|
||||
return &localRunner{
|
||||
runner: runner{
|
||||
state: stateInit,
|
||||
spawnRate: spawnRate,
|
||||
spawnCount: spawnCount,
|
||||
stats: newRequestStats(),
|
||||
outputs: make([]Output, 0),
|
||||
spawnDone: make(chan struct{}),
|
||||
},
|
||||
stopChan: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *localRunner) start() {
|
||||
// init state
|
||||
atomic.StoreInt32(&r.state, stateInit)
|
||||
atomic.StoreInt32(&r.currentClientsNum, 0)
|
||||
r.stats.clearAll()
|
||||
|
||||
// start rate limiter
|
||||
if r.rateLimitEnabled {
|
||||
r.rateLimiter.Start()
|
||||
}
|
||||
|
||||
// all running workers(goroutines) will select on this channel.
|
||||
// close this channel will stop all running workers.
|
||||
quitChan := make(chan bool)
|
||||
// when this channel is closed, all statistics are reported successfully
|
||||
reportedChan := make(chan bool)
|
||||
go r.spawnWorkers(r.spawnCount, r.spawnRate, quitChan, nil)
|
||||
|
||||
// output setup
|
||||
r.outputOnStart()
|
||||
|
||||
// start running
|
||||
go func() {
|
||||
var ticker = time.NewTicker(reportStatsInterval)
|
||||
for {
|
||||
select {
|
||||
// record stats
|
||||
case t := <-r.stats.transactionChan:
|
||||
r.stats.logTransaction(t.name, t.success, t.elapsedTime, t.contentSize)
|
||||
case m := <-r.stats.requestSuccessChan:
|
||||
r.stats.logRequest(m.requestType, m.name, m.responseTime, m.responseLength)
|
||||
case n := <-r.stats.requestFailureChan:
|
||||
r.stats.logRequest(n.requestType, n.name, n.responseTime, 0)
|
||||
r.stats.logError(n.requestType, n.name, n.errMsg)
|
||||
// report stats
|
||||
case <-ticker.C:
|
||||
r.reportStats()
|
||||
// close reportedChan and return if the last stats is reported successfully
|
||||
if atomic.LoadInt32(&r.state) == stateQuitting {
|
||||
close(reportedChan)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// stop
|
||||
<-r.stopChan
|
||||
atomic.StoreInt32(&r.state, stateQuitting)
|
||||
|
||||
// stop previous goroutines without blocking
|
||||
// those goroutines will exit when r.safeRun returns
|
||||
close(quitChan)
|
||||
|
||||
// wait until all stats are reported successfully
|
||||
<-reportedChan
|
||||
|
||||
// stop rate limiter
|
||||
if r.rateLimitEnabled {
|
||||
r.rateLimiter.Stop()
|
||||
}
|
||||
|
||||
// report test result
|
||||
r.reportTestResult()
|
||||
|
||||
// output teardown
|
||||
r.outputOnStop()
|
||||
|
||||
atomic.StoreInt32(&r.state, stateStopped)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *localRunner) stop() {
|
||||
close(r.stopChan)
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type HitOutput struct {
|
||||
onStart bool
|
||||
onEvent bool
|
||||
onStop bool
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnStart() {
|
||||
o.onStart = true
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnEvent(data map[string]interface{}) {
|
||||
o.onEvent = true
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnStop() {
|
||||
o.onStop = true
|
||||
}
|
||||
|
||||
func TestSafeRun(t *testing.T) {
|
||||
runner := &runner{}
|
||||
runner.safeRun(func() {
|
||||
panic("Runner will catch this panic")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOutputOnStart(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnStart()
|
||||
if !hitOutput.onStart {
|
||||
t.Error("hitOutput's OnStart has not been called")
|
||||
}
|
||||
if !hitOutput2.onStart {
|
||||
t.Error("hitOutput2's OnStart has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputOnEvent(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnEvent(nil)
|
||||
if !hitOutput.onEvent {
|
||||
t.Error("hitOutput's OnEvent has not been called")
|
||||
}
|
||||
if !hitOutput2.onEvent {
|
||||
t.Error("hitOutput2's OnEvent has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputOnStop(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnStop()
|
||||
if !hitOutput.onStop {
|
||||
t.Error("hitOutput's OnStop has not been called")
|
||||
}
|
||||
if !hitOutput2.onStop {
|
||||
t.Error("hitOutput2's OnStop has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRunner(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Weight: 10,
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
Name: "TaskA",
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
runner := newLocalRunner(2, 2)
|
||||
runner.setTasks(tasks)
|
||||
go runner.start()
|
||||
time.Sleep(4 * time.Second)
|
||||
runner.stop()
|
||||
}
|
||||
|
||||
func TestLoopCount(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Weight: 10,
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
Name: "TaskA",
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
runner := newLocalRunner(2, 2)
|
||||
runner.loop = &Loop{loopCount: 4}
|
||||
runner.setTasks(tasks)
|
||||
go runner.start()
|
||||
ticker := time.NewTicker(4 * time.Second)
|
||||
defer ticker.Stop()
|
||||
<-ticker.C
|
||||
if !assert.Equal(t, runner.loop.loopCount, atomic.LoadInt64(&runner.loop.finishedCount)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
name string
|
||||
success bool
|
||||
elapsedTime int64
|
||||
contentSize int64
|
||||
}
|
||||
|
||||
type requestSuccess struct {
|
||||
requestType string
|
||||
name string
|
||||
responseTime int64
|
||||
responseLength int64
|
||||
}
|
||||
|
||||
type requestFailure struct {
|
||||
requestType string
|
||||
name string
|
||||
responseTime int64
|
||||
errMsg string
|
||||
}
|
||||
|
||||
type requestStats struct {
|
||||
entries map[string]*statsEntry
|
||||
errors map[string]*statsError
|
||||
total *statsEntry
|
||||
startTime int64
|
||||
|
||||
transactionChan chan *transaction
|
||||
transactionPassed int64 // accumulated number of passed transactions
|
||||
transactionFailed int64 // accumulated number of failed transactions
|
||||
|
||||
requestSuccessChan chan *requestSuccess
|
||||
requestFailureChan chan *requestFailure
|
||||
}
|
||||
|
||||
func newRequestStats() (stats *requestStats) {
|
||||
entries := make(map[string]*statsEntry)
|
||||
errors := make(map[string]*statsError)
|
||||
|
||||
stats = &requestStats{
|
||||
entries: entries,
|
||||
errors: errors,
|
||||
}
|
||||
stats.transactionChan = make(chan *transaction, 100)
|
||||
stats.requestSuccessChan = make(chan *requestSuccess, 100)
|
||||
stats.requestFailureChan = make(chan *requestFailure, 100)
|
||||
|
||||
stats.total = &statsEntry{
|
||||
Name: "Total",
|
||||
Method: "",
|
||||
}
|
||||
stats.total.reset()
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func (s *requestStats) logTransaction(name string, success bool, responseTime int64, contentLength int64) {
|
||||
if success {
|
||||
s.transactionPassed++
|
||||
} else {
|
||||
s.transactionFailed++
|
||||
s.get(name, "transaction").logFailures()
|
||||
}
|
||||
s.get(name, "transaction").log(responseTime, contentLength)
|
||||
}
|
||||
|
||||
func (s *requestStats) logRequest(method, name string, responseTime int64, contentLength int64) {
|
||||
s.total.log(responseTime, contentLength)
|
||||
s.get(name, method).log(responseTime, contentLength)
|
||||
}
|
||||
|
||||
func (s *requestStats) logError(method, name, err string) {
|
||||
s.total.logFailures()
|
||||
s.get(name, method).logFailures()
|
||||
|
||||
// store error in errors map
|
||||
key := genMD5(method, name, err)
|
||||
entry, ok := s.errors[key]
|
||||
if !ok {
|
||||
entry = &statsError{
|
||||
name: name,
|
||||
method: method,
|
||||
errMsg: err,
|
||||
}
|
||||
s.errors[key] = entry
|
||||
}
|
||||
entry.occured()
|
||||
}
|
||||
|
||||
func (s *requestStats) get(name string, method string) (entry *statsEntry) {
|
||||
entry, ok := s.entries[name+method]
|
||||
if !ok {
|
||||
newEntry := &statsEntry{
|
||||
Name: name,
|
||||
Method: method,
|
||||
NumReqsPerSec: make(map[int64]int64),
|
||||
NumFailPerSec: make(map[int64]int64),
|
||||
ResponseTimes: make(map[int64]int64),
|
||||
}
|
||||
s.entries[name+method] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (s *requestStats) clearAll() {
|
||||
s.total = &statsEntry{
|
||||
Name: "Total",
|
||||
Method: "",
|
||||
}
|
||||
s.total.reset()
|
||||
s.transactionPassed = 0
|
||||
s.transactionFailed = 0
|
||||
s.entries = make(map[string]*statsEntry)
|
||||
s.errors = make(map[string]*statsError)
|
||||
s.startTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
func (s *requestStats) serializeStats() []interface{} {
|
||||
entries := make([]interface{}, 0, len(s.entries))
|
||||
for _, v := range s.entries {
|
||||
if !(v.NumRequests == 0 && v.NumFailures == 0) {
|
||||
entries = append(entries, v.getStrippedReport())
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *requestStats) serializeErrors() map[string]map[string]interface{} {
|
||||
errors := make(map[string]map[string]interface{})
|
||||
for k, v := range s.errors {
|
||||
errors[k] = v.toMap()
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (s *requestStats) collectReportData() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
data["transactions"] = map[string]int64{
|
||||
"passed": s.transactionPassed,
|
||||
"failed": s.transactionFailed,
|
||||
}
|
||||
data["stats"] = s.serializeStats()
|
||||
data["stats_total"] = s.total.serialize()
|
||||
data["errors"] = s.serializeErrors()
|
||||
s.errors = make(map[string]*statsError)
|
||||
return data
|
||||
}
|
||||
|
||||
// statsEntry represents a single stats entry (name and method)
|
||||
type statsEntry struct {
|
||||
// Name (URL) of this stats entry
|
||||
Name string `json:"name"`
|
||||
// Method (GET, POST, PUT, etc.)
|
||||
Method string `json:"method"`
|
||||
// The number of requests made
|
||||
NumRequests int64 `json:"num_requests"`
|
||||
// Number of failed request
|
||||
NumFailures int64 `json:"num_failures"`
|
||||
// Total sum of the response times
|
||||
TotalResponseTime int64 `json:"total_response_time"`
|
||||
// Minimum response time
|
||||
MinResponseTime int64 `json:"min_response_time"`
|
||||
// Maximum 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
|
||||
// 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.
|
||||
// This dict is used to calculate the median and percentile response times.
|
||||
ResponseTimes map[int64]int64 `json:"response_times"`
|
||||
// The sum of the content length of all the requests for this entry
|
||||
TotalContentLength int64 `json:"total_content_length"`
|
||||
// Time of the first request for this entry
|
||||
StartTime int64 `json:"start_time"`
|
||||
// Time of the last request for this entry
|
||||
LastRequestTimestamp int64 `json:"last_request_timestamp"`
|
||||
// Boomer doesn't allow None response time for requests like locust.
|
||||
// num_none_requests is added to keep compatible with locust.
|
||||
NumNoneRequests int64 `json:"num_none_requests"`
|
||||
}
|
||||
|
||||
func (s *statsEntry) reset() {
|
||||
s.StartTime = time.Now().Unix()
|
||||
s.NumRequests = 0
|
||||
s.NumFailures = 0
|
||||
s.TotalResponseTime = 0
|
||||
s.ResponseTimes = make(map[int64]int64)
|
||||
s.MinResponseTime = 0
|
||||
s.MaxResponseTime = 0
|
||||
s.LastRequestTimestamp = time.Now().Unix()
|
||||
s.NumReqsPerSec = make(map[int64]int64)
|
||||
s.NumFailPerSec = make(map[int64]int64)
|
||||
s.TotalContentLength = 0
|
||||
}
|
||||
|
||||
func (s *statsEntry) log(responseTime int64, contentLength int64) {
|
||||
s.NumRequests++
|
||||
|
||||
s.logTimeOfRequest()
|
||||
s.logResponseTime(responseTime)
|
||||
|
||||
s.TotalContentLength += contentLength
|
||||
}
|
||||
|
||||
func (s *statsEntry) logTimeOfRequest() {
|
||||
key := time.Now().Unix()
|
||||
_, ok := s.NumReqsPerSec[key]
|
||||
if !ok {
|
||||
s.NumReqsPerSec[key] = 1
|
||||
} else {
|
||||
s.NumReqsPerSec[key]++
|
||||
}
|
||||
|
||||
s.LastRequestTimestamp = key
|
||||
}
|
||||
|
||||
func (s *statsEntry) logResponseTime(responseTime int64) {
|
||||
s.TotalResponseTime += responseTime
|
||||
|
||||
if s.MinResponseTime == 0 {
|
||||
s.MinResponseTime = responseTime
|
||||
}
|
||||
|
||||
if responseTime < s.MinResponseTime {
|
||||
s.MinResponseTime = responseTime
|
||||
}
|
||||
|
||||
if responseTime > s.MaxResponseTime {
|
||||
s.MaxResponseTime = responseTime
|
||||
}
|
||||
|
||||
var roundedResponseTime int64
|
||||
|
||||
// to avoid too much data that has to be transferred to the master node when
|
||||
// running in distributed mode, we save the response time rounded in a dict
|
||||
// so that 147 becomes 150, 3432 becomes 3400 and 58760 becomes 59000
|
||||
// see also locust's stats.py
|
||||
if responseTime < 100 {
|
||||
roundedResponseTime = responseTime
|
||||
} else if responseTime < 1000 {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -1))
|
||||
} else if responseTime < 10000 {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -2))
|
||||
} else {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -3))
|
||||
}
|
||||
|
||||
_, ok := s.ResponseTimes[roundedResponseTime]
|
||||
if !ok {
|
||||
s.ResponseTimes[roundedResponseTime] = 1
|
||||
} else {
|
||||
s.ResponseTimes[roundedResponseTime]++
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statsEntry) logFailures() {
|
||||
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{} {
|
||||
var result map[string]interface{}
|
||||
val, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
err = json.Unmarshal(val, &result)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *statsEntry) getStrippedReport() map[string]interface{} {
|
||||
report := s.serialize()
|
||||
s.reset()
|
||||
return report
|
||||
}
|
||||
|
||||
type statsError struct {
|
||||
name string
|
||||
method string
|
||||
errMsg string
|
||||
occurrences int64
|
||||
}
|
||||
|
||||
func (err *statsError) occured() {
|
||||
err.occurrences++
|
||||
}
|
||||
|
||||
func (err *statsError) toMap() map[string]interface{} {
|
||||
m := make(map[string]interface{})
|
||||
m["method"] = err.method
|
||||
m["name"] = err.name
|
||||
m["error"] = err.errMsg
|
||||
m["occurrences"] = err.occurrences
|
||||
return m
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogRequest(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
newStats.logRequest("http", "success", 3, 40)
|
||||
newStats.logRequest("http", "success", 2, 40)
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
entry := newStats.get("success", "http")
|
||||
|
||||
if entry.NumRequests != 4 {
|
||||
t.Error("numRequests is wrong, expected: 4, got:", entry.NumRequests)
|
||||
}
|
||||
if entry.MinResponseTime != 1 {
|
||||
t.Error("minResponseTime is wrong, expected: 1, got:", entry.MinResponseTime)
|
||||
}
|
||||
if entry.MaxResponseTime != 3 {
|
||||
t.Error("maxResponseTime is wrong, expected: 3, got:", entry.MaxResponseTime)
|
||||
}
|
||||
if entry.TotalResponseTime != 8 {
|
||||
t.Error("totalResponseTime is wrong, expected: 8, got:", entry.TotalResponseTime)
|
||||
}
|
||||
if entry.TotalContentLength != 130 {
|
||||
t.Error("totalContentLength is wrong, expected: 130, got:", entry.TotalContentLength)
|
||||
}
|
||||
|
||||
// check newStats.total
|
||||
if newStats.total.NumRequests != 4 {
|
||||
t.Error("newStats.total.numRequests is wrong, expected: 4, got:", newStats.total.NumRequests)
|
||||
}
|
||||
if newStats.total.MinResponseTime != 1 {
|
||||
t.Error("newStats.total.minResponseTime is wrong, expected: 1, got:", newStats.total.MinResponseTime)
|
||||
}
|
||||
if newStats.total.MaxResponseTime != 3 {
|
||||
t.Error("newStats.total.maxResponseTime is wrong, expected: 3, got:", newStats.total.MaxResponseTime)
|
||||
}
|
||||
if newStats.total.TotalResponseTime != 8 {
|
||||
t.Error("newStats.total.totalResponseTime is wrong, expected: 8, got:", newStats.total.TotalResponseTime)
|
||||
}
|
||||
if newStats.total.TotalContentLength != 130 {
|
||||
t.Error("newStats.total.totalContentLength is wrong, expected: 130, got:", newStats.total.TotalContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogRequest(b *testing.B) {
|
||||
newStats := newRequestStats()
|
||||
for i := 0; i < b.N; i++ {
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundedResponseTime(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 147, 1)
|
||||
newStats.logRequest("http", "success", 3432, 1)
|
||||
newStats.logRequest("http", "success", 58760, 1)
|
||||
entry := newStats.get("success", "http")
|
||||
responseTimes := entry.ResponseTimes
|
||||
|
||||
if len(responseTimes) != 3 {
|
||||
t.Error("len(responseTimes) is wrong, expected: 3, got:", len(responseTimes))
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[150]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 150)
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[3400]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 3400)
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[59000]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 59000)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogError(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
entry := newStats.get("failure", "http")
|
||||
|
||||
if entry.NumFailures != 3 {
|
||||
t.Error("numFailures is wrong, expected: 3, got:", entry.NumFailures)
|
||||
}
|
||||
|
||||
if newStats.total.NumFailures != 3 {
|
||||
t.Error("newStats.total.numFailures is wrong, expected: 3, got:", newStats.total.NumFailures)
|
||||
}
|
||||
|
||||
// md5("httpfailure500 error") = 547c38e4e4742c1c581f9e2809ba4f55
|
||||
err500 := newStats.errors["547c38e4e4742c1c581f9e2809ba4f55"]
|
||||
if err500.errMsg != "500 error" {
|
||||
t.Error("Error message is wrong, expected: 500 error, got:", err500.errMsg)
|
||||
}
|
||||
if err500.occurrences != 1 {
|
||||
t.Error("Error occurrences is wrong, expected: 1, got:", err500.occurrences)
|
||||
}
|
||||
|
||||
// md5("httpfailure400 error") = f391c310401ad8e10e929f2ee1a614e4
|
||||
err400 := newStats.errors["f391c310401ad8e10e929f2ee1a614e4"]
|
||||
if err400.errMsg != "400 error" {
|
||||
t.Error("Error message is wrong, expected: 400 error, got:", err400.errMsg)
|
||||
}
|
||||
if err400.occurrences != 2 {
|
||||
t.Error("Error occurrences is wrong, expected: 2, got:", err400.occurrences)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkLogError(b *testing.B) {
|
||||
newStats := newRequestStats()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// LogError use md5 to calculate hash keys, it may slow down the only goroutine,
|
||||
// which consumes both requestSuccessChannel and requestFailureChannel.
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAll(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
newStats.clearAll()
|
||||
|
||||
if newStats.total.NumRequests != 0 {
|
||||
t.Error("After clearAll(), newStats.total.numRequests is wrong, expected: 0, got:", newStats.total.NumRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAllByChannel(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
newStats.clearAll()
|
||||
|
||||
if newStats.total.NumRequests != 0 {
|
||||
t.Error("After clearAll(), newStats.total.numRequests is wrong, expected: 0, got:", newStats.total.NumRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeStats(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
|
||||
serialized := newStats.serializeStats()
|
||||
if len(serialized) != 1 {
|
||||
t.Error("The length of serialized results is wrong, expected: 1, got:", len(serialized))
|
||||
return
|
||||
}
|
||||
|
||||
first := serialized[0]
|
||||
entry, err := deserializeStatsEntry(first)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if entry.Name != "success" {
|
||||
t.Error("The name is wrong, expected:", "success", "got:", entry.Name)
|
||||
}
|
||||
if entry.Method != "http" {
|
||||
t.Error("The method is wrong, expected:", "http", "got:", entry.Method)
|
||||
}
|
||||
if entry.NumRequests != int64(1) {
|
||||
t.Error("The num_requests is wrong, expected:", 1, "got:", entry.NumRequests)
|
||||
}
|
||||
if entry.NumFailures != int64(0) {
|
||||
t.Error("The num_failures is wrong, expected:", 0, "got:", entry.NumFailures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeErrors(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
serialized := newStats.serializeErrors()
|
||||
|
||||
if len(serialized) != 2 {
|
||||
t.Error("The length of serialized results is wrong, expected: 2, got:", len(serialized))
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range serialized {
|
||||
if key == "f391c310401ad8e10e929f2ee1a614e4" {
|
||||
err := value["error"].(string)
|
||||
if err != "400 error" {
|
||||
t.Error("expected: 400 error, got:", err)
|
||||
}
|
||||
occurrences := value["occurrences"].(int64)
|
||||
if occurrences != int64(2) {
|
||||
t.Error("expected: 2, got:", occurrences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectReportData(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
result := newStats.collectReportData()
|
||||
|
||||
if _, ok := result["stats"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
if _, ok := result["stats_total"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
if _, ok := result["errors"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package boomer
|
||||
|
||||
// Task is like the "Locust object" in locust, the python version.
|
||||
// When boomer receives a start message from master, it will spawn several goroutines to run Task.Fn.
|
||||
// But users can keep some information in the python version, they can't do the same things in boomer.
|
||||
// Because Task.Fn is a pure function.
|
||||
type Task struct {
|
||||
// The weight is used to distribute goroutines over multiple tasks.
|
||||
Weight int
|
||||
// Fn is called by the goroutines allocated to this task, in a loop.
|
||||
Fn func()
|
||||
Name string
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// +build !windows
|
||||
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
// ulimit -n 10240
|
||||
func SetUlimit(limit uint64) {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("get ulimit failed")
|
||||
return
|
||||
}
|
||||
log.Info().Uint64("limit", rLimit.Cur).Msg("get current ulimit")
|
||||
if rLimit.Cur >= limit {
|
||||
return
|
||||
}
|
||||
|
||||
rLimit.Cur = limit
|
||||
log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit")
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("set ulimit failed")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// +build windows
|
||||
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
func SetUlimit(limit uint64) {
|
||||
log.Warn().Msg("windows does not support setting ulimit")
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func round(val float64, roundOn float64, places int) (newVal float64) {
|
||||
var round float64
|
||||
pow := math.Pow(10, float64(places))
|
||||
digit := pow * val
|
||||
_, div := math.Modf(digit)
|
||||
if div >= roundOn {
|
||||
round = math.Ceil(digit)
|
||||
} else {
|
||||
round = math.Floor(digit)
|
||||
}
|
||||
newVal = round / pow
|
||||
return
|
||||
}
|
||||
|
||||
// genMD5 returns the md5 hash of strings.
|
||||
func genMD5(slice ...string) string {
|
||||
h := md5.New()
|
||||
for _, v := range slice {
|
||||
io.WriteString(h, v)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// startMemoryProfile starts memory profiling and save the results in file.
|
||||
func startMemoryProfile(file string, duration time.Duration) (err error) {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Dur("duration", duration).Msg("Start memory profiling")
|
||||
time.AfterFunc(duration, func() {
|
||||
err := pprof.WriteHeapProfile(f)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to write memory profile")
|
||||
}
|
||||
f.Close()
|
||||
log.Info().Dur("duration", duration).Msg("Stop memory profiling")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// startCPUProfile starts cpu profiling and save the results in file.
|
||||
func startCPUProfile(file string, duration time.Duration) (err error) {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Dur("duration", duration).Msg("Start CPU profiling")
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
time.AfterFunc(duration, func() {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
log.Info().Dur("duration", duration).Msg("Stop CPU profiling")
|
||||
})
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package boomer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
if int(round(float64(147.5002), .5, -1)) != 150 {
|
||||
t.Error("147.5002 should be rounded to 150")
|
||||
}
|
||||
|
||||
if int(round(float64(3432.5002), .5, -2)) != 3400 {
|
||||
t.Error("3432.5002 should be rounded to 3400")
|
||||
}
|
||||
|
||||
roundOne := round(float64(58760.5002), .5, -3)
|
||||
roundTwo := round(float64(58960.6003), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58760.5002) should be equal to round(58960.6003)")
|
||||
}
|
||||
|
||||
roundOne = round(float64(58360.5002), .5, -3)
|
||||
roundTwo = round(float64(58460.6003), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58360.5002) should be equal to round(58460.6003)")
|
||||
}
|
||||
|
||||
roundOne = round(float64(58360), .5, -3)
|
||||
roundTwo = round(float64(58460), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58360) should be equal to round(58460)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGenMD5(t *testing.T) {
|
||||
hashValue := genMD5("Hello", "World!")
|
||||
if hashValue != "06e0e6637d27b2622ab52022db713ce2" {
|
||||
t.Error("Expected: 06e0e6637d27b2622ab52022db713ce2, Got: ", hashValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMemoryProfile(t *testing.T) {
|
||||
if _, err := os.Stat("mem.pprof"); os.IsExist(err) {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
if err := startMemoryProfile("mem.pprof", 2*time.Second); err != nil {
|
||||
t.Error("Error starting memory profiling")
|
||||
}
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
if _, err := os.Stat("mem.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File mem.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCPUProfile(t *testing.T) {
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsExist(err) {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
if err := startCPUProfile("cpu.pprof", 2*time.Second); err != nil {
|
||||
t.Error("Error starting cpu profiling")
|
||||
}
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File cpu.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected interface{}, msgAndArgs ...interface{}) bool{
|
||||
"eq": assert.EqualValues,
|
||||
"equals": assert.EqualValues,
|
||||
"equal": assert.EqualValues,
|
||||
"lt": assert.Less,
|
||||
"less_than": assert.Less,
|
||||
"le": assert.LessOrEqual,
|
||||
"less_or_equals": assert.LessOrEqual,
|
||||
"gt": assert.Greater,
|
||||
"greater_than": assert.Greater,
|
||||
"ge": assert.GreaterOrEqual,
|
||||
"greater_or_equals": assert.GreaterOrEqual,
|
||||
"ne": assert.NotEqual,
|
||||
"not_equal": assert.NotEqual,
|
||||
"contains": assert.Contains,
|
||||
"type_match": assert.IsType,
|
||||
// custom assertions
|
||||
"startswith": StartsWith,
|
||||
"endswith": EndsWith,
|
||||
"len_eq": EqualLength,
|
||||
"length_equals": EqualLength,
|
||||
"length_equal": EqualLength,
|
||||
"len_lt": LessThanLength,
|
||||
"count_lt": LessThanLength,
|
||||
"length_less_than": LessThanLength,
|
||||
"len_le": LessOrEqualsLength,
|
||||
"count_le": LessOrEqualsLength,
|
||||
"length_less_or_equals": LessOrEqualsLength,
|
||||
"len_gt": GreaterThanLength,
|
||||
"count_gt": GreaterThanLength,
|
||||
"length_greater_than": GreaterThanLength,
|
||||
"len_ge": GreaterOrEqualsLength,
|
||||
"count_ge": GreaterOrEqualsLength,
|
||||
"length_greater_or_equals": GreaterOrEqualsLength,
|
||||
"contained_by": ContainedBy,
|
||||
"str_eq": StringEqual,
|
||||
"string_equals": StringEqual,
|
||||
"regex_match": RegexMatch,
|
||||
}
|
||||
|
||||
// StartsWith check if string starts with substring
|
||||
func StartsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasPrefix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
// EndsWith check if string ends with substring
|
||||
func EndsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasSuffix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
func EqualLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
|
||||
return assert.Len(t, actual, length, msgAndArgs...)
|
||||
}
|
||||
|
||||
func GreaterThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
}
|
||||
if l <= length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GreaterOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
}
|
||||
if l < length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func LessThanLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
}
|
||||
if l >= length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be less than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func LessOrEqualsLength(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
ok, l := getLen(actual)
|
||||
if !ok {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", actual), msgAndArgs...)
|
||||
}
|
||||
if l > length {
|
||||
return assert.Fail(t, fmt.Sprintf("\"%s\" should be no more than %d item(s), but has %d", actual, length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ContainedBy assert whether actual element contains expected element
|
||||
func ContainedBy(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.Contains(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
func StringEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, msgAndArgs) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, msgAndArgs) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.EqualFold(actualString, expectedString), msgAndArgs)
|
||||
}
|
||||
|
||||
func RegexMatch(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
|
||||
return assert.Regexp(t, expected, actual, msgAndArgs)
|
||||
}
|
||||
|
||||
func convertInt(value interface{}) (int, error) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v, nil
|
||||
case int8:
|
||||
return int(v), nil
|
||||
case int16:
|
||||
return int(v), nil
|
||||
case int32:
|
||||
return int(v), nil
|
||||
case int64:
|
||||
return int(v), nil
|
||||
case uint:
|
||||
return int(v), nil
|
||||
case uint8:
|
||||
return int(v), nil
|
||||
case uint16:
|
||||
return int(v), nil
|
||||
case uint32:
|
||||
return int(v), nil
|
||||
case uint64:
|
||||
return int(v), nil
|
||||
case float32:
|
||||
return int(v), nil
|
||||
case float64:
|
||||
return int(v), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
|
||||
}
|
||||
}
|
||||
|
||||
// getLen try to get length of object.
|
||||
// return (false, 0) if impossible.
|
||||
func getLen(x interface{}) (ok bool, length int) {
|
||||
v := reflect.ValueOf(x)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ok = false
|
||||
}
|
||||
}()
|
||||
return true, v.Len()
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package builtin
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "a"},
|
||||
{"abc", "ab"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StartsWith(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "c"},
|
||||
{"abc", "bc"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EndsWith(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 0},
|
||||
{[]string{}, 0},
|
||||
{map[string]interface{}{}, 0},
|
||||
{"a", 1},
|
||||
{[]string{"a"}, 1},
|
||||
{map[string]interface{}{"a": 123}, 1},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EqualLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessThanLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 1},
|
||||
{[]string{}, 1},
|
||||
{map[string]interface{}{}, 1},
|
||||
{"a", 2},
|
||||
{[]string{"a"}, 2},
|
||||
{map[string]interface{}{"a": 123}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessThanLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessOrEqualsLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 1},
|
||||
{[]string{}, 1},
|
||||
{map[string]interface{}{"A": 111}, 1},
|
||||
{"a", 1},
|
||||
{[]string{"a"}, 2},
|
||||
{map[string]interface{}{"a": 123}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, LessOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreaterThanLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"abcd", 3},
|
||||
{[]string{"a", "b", "c"}, 2},
|
||||
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterThanLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreaterOrEqualsLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"abcd", 3},
|
||||
{[]string{"w"}, 1},
|
||||
{map[string]interface{}{"A": 111}, 1},
|
||||
{"a", 1},
|
||||
{[]string{"a", "b", "c"}, 2},
|
||||
{map[string]interface{}{"a": 123, "b": 223, "c": 323}, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, GreaterOrEqualsLength(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainedBy(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"abcd", "abcdefg"},
|
||||
{"a", []string{"a", "b", "c"}},
|
||||
{"A", map[string]interface{}{"A": 111, "B": 222}},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, ContainedBy(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqual(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"abcd", "abcd"},
|
||||
{"abcd", "ABCD"},
|
||||
{"ABcd", "abCD"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StringEqual(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatch(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"it's starting...", regexp.MustCompile("start")},
|
||||
{"it's not starting", "starting$"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, RegexMatch(t, data.raw, data.expected)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
package builtin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
builtinJSON "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
var Functions = map[string]interface{}{
|
||||
"get_timestamp": getTimestamp, // call without arguments
|
||||
"sleep": sleep, // call with one argument
|
||||
"gen_random_string": genRandomString, // call with one argument
|
||||
"max": math.Max, // call with two arguments
|
||||
"md5": MD5, // call with one argument
|
||||
"parameterize": loadFromCSV,
|
||||
"P": loadFromCSV,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func getTimestamp() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func sleep(nSecs int) {
|
||||
time.Sleep(time.Duration(nSecs) * time.Second)
|
||||
}
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
func genRandomString(n int) string {
|
||||
lettersLen := len(letters)
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(lettersLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func MD5(str string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func loadFromCSV(path string) []map[string]interface{} {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Str("path", path).Err(err).Msg("convert absolute path failed")
|
||||
panic(err)
|
||||
}
|
||||
log.Info().Str("path", path).Msg("load csv file")
|
||||
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("load csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
r := csv.NewReader(strings.NewReader(string(file)))
|
||||
content, err := r.ReadAll()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("parse csv file failed")
|
||||
panic(err)
|
||||
}
|
||||
var result []map[string]interface{}
|
||||
for i := 1; i < len(content); i++ {
|
||||
row := make(map[string]interface{})
|
||||
for j := 0; j < len(content[i]); j++ {
|
||||
row[content[0][j]] = content[i][j]
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Dump2JSON(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to json")
|
||||
file, _ := json.MarshalIndent(data, "", " ")
|
||||
err = os.WriteFile(path, file, 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump json path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Dump2YAML(data interface{}, path string) error {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert absolute path failed")
|
||||
return err
|
||||
}
|
||||
log.Info().Str("path", path).Msg("dump data to yaml")
|
||||
|
||||
// init yaml encoder
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(4)
|
||||
|
||||
// encode
|
||||
err = encoder.Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, buffer.Bytes(), 0644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dump yaml path failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatResponse(raw interface{}) interface{} {
|
||||
formattedResponse := make(map[string]interface{})
|
||||
for key, value := range raw.(map[string]interface{}) {
|
||||
// convert value to json
|
||||
if key == "body" {
|
||||
b, _ := json.MarshalIndent(&value, "", " ")
|
||||
value = string(b)
|
||||
}
|
||||
formattedResponse[key] = value
|
||||
}
|
||||
return formattedResponse
|
||||
}
|
||||
|
||||
func ExecCommand(cmd *exec.Cmd, cwd string) error {
|
||||
log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command")
|
||||
cmd.Dir = cwd
|
||||
output, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(output))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("output", out).Msg("exec command failed")
|
||||
} else if len(out) != 0 {
|
||||
log.Info().Str("output", out).Msg("exec command success")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateFolder(folderPath string) error {
|
||||
log.Info().Str("path", folderPath).Msg("create folder")
|
||||
err := os.MkdirAll(folderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create folder failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateFile(filePath string, data string) error {
|
||||
log.Info().Str("path", filePath).Msg("create file")
|
||||
err := os.WriteFile(filePath, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("create file failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists, whether path is file or dir
|
||||
func isPathExists(path string) bool {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isFilePathExists returns true if path exists and path is file
|
||||
func isFilePathExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// path not exists
|
||||
return false
|
||||
}
|
||||
|
||||
// path exists
|
||||
if info.IsDir() {
|
||||
// path is dir, not file
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EnsureFolderExists(folderPath string) error {
|
||||
if !isPathExists(folderPath) {
|
||||
err := CreateFolder(folderPath)
|
||||
return err
|
||||
} else if isFilePathExists(folderPath) {
|
||||
return fmt.Errorf("path %v should be directory", folderPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRandomNumber(min, max int) int {
|
||||
if min > max {
|
||||
return 0
|
||||
}
|
||||
r := rand.Intn(max - min + 1)
|
||||
return min + r
|
||||
}
|
||||
|
||||
func Interface2Float64(i interface{}) (float64, error) {
|
||||
switch i.(type) {
|
||||
case int:
|
||||
return float64(i.(int)), nil
|
||||
case int32:
|
||||
return float64(i.(int32)), nil
|
||||
case int64:
|
||||
return float64(i.(int64)), nil
|
||||
case float32:
|
||||
return float64(i.(float32)), nil
|
||||
case float64:
|
||||
return i.(float64), nil
|
||||
case string:
|
||||
intVar, err := strconv.Atoi(i.(string))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(intVar), err
|
||||
}
|
||||
// json.Number
|
||||
value, ok := i.(builtinJSON.Number)
|
||||
if ok {
|
||||
return value.Float64()
|
||||
}
|
||||
return 0, errors.New("failed to convert interface to float64")
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
gaAPIDebugURL = "https://www.google-analytics.com/debug/collect" // used for debug
|
||||
gaAPIURL = "https://www.google-analytics.com/collect"
|
||||
)
|
||||
|
||||
type GAClient struct {
|
||||
TrackingID string `form:"tid"` // Tracking ID / Property ID, XX-XXXXXXX-X
|
||||
ClientID string `form:"cid"` // Anonymous Client ID
|
||||
Version string `form:"v"` // Version
|
||||
httpClient *http.Client // http client session
|
||||
}
|
||||
|
||||
// NewGAClient creates a new GAClient object with the trackingID and clientID.
|
||||
func NewGAClient(trackingID, clientID string) *GAClient {
|
||||
return &GAClient{
|
||||
TrackingID: trackingID,
|
||||
ClientID: clientID,
|
||||
Version: "1", // constant v1
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GAClient) SendEvent(e IEvent) error {
|
||||
var data url.Values
|
||||
if event, ok := e.(UserTimingTracking); ok {
|
||||
event.duration = time.Since(event.startTime)
|
||||
data = event.ToUrlValues()
|
||||
} else {
|
||||
data = e.ToUrlValues()
|
||||
}
|
||||
|
||||
// append common params
|
||||
data.Add("v", g.Version)
|
||||
data.Add("tid", g.TrackingID)
|
||||
data.Add("cid", g.ClientID)
|
||||
|
||||
resp, err := g.httpClient.PostForm(gaAPIURL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("response status: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToUrlValues(i interface{}) (values url.Values) {
|
||||
values = url.Values{}
|
||||
iVal := reflect.ValueOf(i)
|
||||
for i := 0; i < iVal.NumField(); i++ {
|
||||
formTagName := iVal.Type().Field(i).Tag.Get("form")
|
||||
if formTagName == "" {
|
||||
continue
|
||||
}
|
||||
if iVal.Field(i).IsZero() {
|
||||
continue
|
||||
}
|
||||
values.Set(formTagName, fmt.Sprint(iVal.Field(i)))
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendEvents(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "SendEvents",
|
||||
Value: 123,
|
||||
}
|
||||
err := gaClient.SendEvent(event)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructToUrlValues(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "convert",
|
||||
Label: "v0.3.0",
|
||||
Value: 123,
|
||||
}
|
||||
val := structToUrlValues(event)
|
||||
if val.Encode() != "ea=convert&ec=unittest&el=v0.3.0&ev=123" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp/internal/version"
|
||||
)
|
||||
|
||||
type IEvent interface {
|
||||
ToUrlValues() url.Values
|
||||
}
|
||||
|
||||
type EventTracking struct {
|
||||
HitType string `form:"t"` // Event hit type = event
|
||||
Category string `form:"ec"` // Required. Event Category.
|
||||
Action string `form:"ea"` // Required. Event Action.
|
||||
Label string `form:"el"` // Optional. Event label, used as version.
|
||||
Value int `form:"ev"` // Optional. Event value, must be non-negative integer
|
||||
}
|
||||
|
||||
func (e EventTracking) StartTiming(variable string) UserTimingTracking {
|
||||
return UserTimingTracking{
|
||||
HitType: "timing",
|
||||
Category: e.Category,
|
||||
Variable: variable,
|
||||
Label: e.Label,
|
||||
startTime: time.Now(), // starts the timer
|
||||
}
|
||||
}
|
||||
|
||||
func (e EventTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "event"
|
||||
e.Label = version.VERSION
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type UserTimingTracking struct {
|
||||
HitType string `form:"t"` // Timing hit type
|
||||
Category string `form:"utc"` // Required. user timing category. e.g. jsonLoader
|
||||
Variable string `form:"utv"` // Required. timing variable. e.g. load
|
||||
Duration string `form:"utt"` // Required. time took duration.
|
||||
Label string `form:"utl"` // Optional. user timing label. e.g jQuery
|
||||
startTime time.Time
|
||||
duration time.Duration // time took duration
|
||||
}
|
||||
|
||||
func (e UserTimingTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "timing"
|
||||
e.Label = version.VERSION
|
||||
e.Duration = fmt.Sprintf("%d", int64(e.duration.Seconds()*1000))
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type Exception struct {
|
||||
HitType string `form:"t"` // Hit Type = exception
|
||||
Description string `form:"exd"` // exception description. i.e. IOException
|
||||
IsFatal string `form:"exf"` // if the exception was fatal
|
||||
isFatal bool
|
||||
}
|
||||
|
||||
func (e Exception) ToUrlValues() url.Values {
|
||||
e.HitType = "exception"
|
||||
if e.isFatal {
|
||||
e.IsFatal = "1"
|
||||
} else {
|
||||
e.IsFatal = "0"
|
||||
}
|
||||
return structToUrlValues(e)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ga
|
||||
|
||||
import (
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
trackingID = "UA-114587036-1" // Tracking ID for Google Analytics
|
||||
)
|
||||
|
||||
var gaClient *GAClient
|
||||
|
||||
func init() {
|
||||
clientID, err := machineid.ProtectedID("hrp")
|
||||
if err != nil {
|
||||
nodeUUID, _ := uuid.NewUUID()
|
||||
clientID = nodeUUID.String()
|
||||
}
|
||||
gaClient = NewGAClient(trackingID, clientID)
|
||||
}
|
||||
|
||||
func SendEvent(e IEvent) error {
|
||||
return gaClient.SendEvent(e)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# har2case
|
||||
|
||||
Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
|
||||
|
||||
## Install
|
||||
|
||||
## Quick Start
|
||||
|
||||
## Examples
|
|
@ -0,0 +1,352 @@
|
|||
package har2case
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"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/ga"
|
||||
"github.com/httprunner/httprunner/hrp/internal/json"
|
||||
)
|
||||
|
||||
const (
|
||||
suffixJSON = ".json"
|
||||
suffixYAML = ".yaml"
|
||||
)
|
||||
|
||||
func NewHAR(path string) *har {
|
||||
return &har{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
type har struct {
|
||||
path string
|
||||
filterStr string
|
||||
excludeStr string
|
||||
outputDir string
|
||||
}
|
||||
|
||||
func (h *har) SetOutputDir(dir string) {
|
||||
h.outputDir = dir
|
||||
}
|
||||
|
||||
func (h *har) GenJSON() (jsonPath string, err error) {
|
||||
event := ga.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: "hrp har2case --to-json",
|
||||
}
|
||||
// report start event
|
||||
go ga.SendEvent(event)
|
||||
// report running timing event
|
||||
defer ga.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 := ga.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: "hrp har2case --to-yaml",
|
||||
}
|
||||
// report start event
|
||||
go ga.SendEvent(event)
|
||||
// report running timing event
|
||||
defer ga.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) {
|
||||
fp, err := os.Open(h.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(fp)
|
||||
fp.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
har := &Har{}
|
||||
err = json.Unmarshal(data, har)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json.Unmarshal error: %w", err)
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestMethod(entry *Entry) error {
|
||||
s.Request.Method = 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.Hostname()+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)
|
||||
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)
|
||||
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{}
|
||||
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)]
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package har2case
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/httprunner/httprunner/hrp"
|
||||
)
|
||||
|
||||
var (
|
||||
harPath = "../../../examples/hrp/har/demo.har"
|
||||
harPath2 = "../../../examples/hrp/har/postman-echo.har"
|
||||
)
|
||||
|
||||
func TestGenJSON(t *testing.T) {
|
||||
jsonPath, err := NewHAR(harPath).GenJSON()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.NotEmpty(t, jsonPath) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenYAML(t *testing.T) {
|
||||
yamlPath, err := NewHAR(harPath2).GenYAML()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.NotEmpty(t, yamlPath) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadHAR(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
h, err := har.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "GET", h.Log.Entries[0].Request.Method) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "POST", h.Log.Entries[1].Request.Method) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCase(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
tCase, err := har.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request method
|
||||
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request url
|
||||
if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request params
|
||||
if !assert.Equal(t, "HDnY8", tCase.TestSteps[0].Request.Params["foo1"]) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request cookies
|
||||
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Cookies["sails.sid"]) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request headers
|
||||
if !assert.Equal(t, "HttpRunnerPlus", tCase.TestSteps[0].Request.Headers["User-Agent"]) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "postman-echo.com", tCase.TestSteps[0].Request.Headers["Host"]) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make request data
|
||||
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) {
|
||||
t.Fail()
|
||||
}
|
||||
if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// make validators
|
||||
validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "status_code", validator.Check) {
|
||||
t.Fail()
|
||||
}
|
||||
validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) {
|
||||
t.Fail()
|
||||
}
|
||||
validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator)
|
||||
if !ok || !assert.Equal(t, "body.url", validator.Check) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFilenameWithoutExtension(t *testing.T) {
|
||||
filename := getFilenameWithoutExtension("../../../examples/hrp/har/postman-echo.har")
|
||||
if !assert.Equal(t, "postman-echo", filename) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,340 @@
|
|||
package har2case
|
||||
|
||||
import "time"
|
||||
|
||||
/*
|
||||
HTTP Archive (HAR) format
|
||||
https://w3c.github.io/web-performance/specs/HAR/Overview.html
|
||||
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
|
||||
*/
|
||||
|
||||
// Har is a container type for deserialization
|
||||
type Har struct {
|
||||
Log Log `json:"log"`
|
||||
}
|
||||
|
||||
// Log represents the root of the exported data. This object MUST be present and its name MUST be "log".
|
||||
type Log struct {
|
||||
// The object contains the following name/value pairs:
|
||||
|
||||
// Required. Version number of the format.
|
||||
Version string `json:"version"`
|
||||
// Required. An object of type creator that contains the name and version
|
||||
// information of the log creator application.
|
||||
Creator Creator `json:"creator"`
|
||||
// Optional. An object of type browser that contains the name and version
|
||||
// information of the user agent.
|
||||
Browser Browser `json:"browser"`
|
||||
// Optional. An array of objects of type page, each representing one exported
|
||||
// (tracked) page. Leave out this field if the application does not support
|
||||
// grouping by pages.
|
||||
Pages []Page `json:"pages,omitempty"`
|
||||
// Required. An array of objects of type entry, each representing one
|
||||
// exported (tracked) HTTP request.
|
||||
Entries []Entry `json:"entries"`
|
||||
// Optional. A comment provided by the user or the application. Sorting
|
||||
// entries by startedDateTime (starting from the oldest) is preferred way how
|
||||
// to export data since it can make importing faster. However the reader
|
||||
// application should always make sure the array is sorted (if required for
|
||||
// the import).
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Creator contains information about the log creator application
|
||||
type Creator struct {
|
||||
// Required. The name of the application that created the log.
|
||||
Name string `json:"name"`
|
||||
// Required. The version number of the application that created the log.
|
||||
Version string `json:"version"`
|
||||
// Optional. A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Browser that created the log
|
||||
type Browser struct {
|
||||
// Required. The name of the browser that created the log.
|
||||
Name string `json:"name"`
|
||||
// Required. The version number of the browser that created the log.
|
||||
Version string `json:"version"`
|
||||
// Optional. A comment provided by the user or the browser.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Page object for every exported web page and one <entry> object for every HTTP request.
|
||||
// In case when an HTTP trace tool isn't able to group requests by a page,
|
||||
// the <pages> object is empty and individual requests doesn't have a parent page.
|
||||
type Page struct {
|
||||
/* There is one <page> object for every exported web page and one <entry>
|
||||
object for every HTTP request. In case when an HTTP trace tool isn't able to
|
||||
group requests by a page, the <pages> object is empty and individual
|
||||
requests doesn't have a parent page.
|
||||
*/
|
||||
|
||||
// Date and time stamp for the beginning of the page load
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00).
|
||||
StartedDateTime string `json:"startedDateTime"`
|
||||
// Unique identifier of a page within the . Entries use it to refer the parent page.
|
||||
ID string `json:"id"`
|
||||
// Page title.
|
||||
Title string `json:"title"`
|
||||
// Detailed timing info about page load.
|
||||
PageTiming PageTiming `json:"pageTiming"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PageTiming describes timings for various events (states) fired during the page load.
|
||||
// All times are specified in milliseconds. If a time info is not available appropriate field is set to -1.
|
||||
type PageTiming struct {
|
||||
// Content of the page loaded. Number of milliseconds since page load started
|
||||
// (page.startedDateTime). Use -1 if the timing does not apply to the current
|
||||
// request.
|
||||
// Depeding on the browser, onContentLoad property represents DOMContentLoad
|
||||
// event or document.readyState == interactive.
|
||||
OnContentLoad int `json:"onContentLoad"`
|
||||
// Page is loaded (onLoad event fired). Number of milliseconds since page
|
||||
// load started (page.startedDateTime). Use -1 if the timing does not apply
|
||||
// to the current request.
|
||||
OnLoad int `json:"onLoad"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Entry is a unique, optional Reference to the parent page.
|
||||
// Leave out this field if the application does not support grouping by pages.
|
||||
type Entry struct {
|
||||
Pageref string `json:"pageref,omitempty"`
|
||||
// Date and time stamp of the request start
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD).
|
||||
StartedDateTime string `json:"startedDateTime"`
|
||||
// Total elapsed time of the request in milliseconds. This is the sum of all
|
||||
// timings available in the timings object (i.e. not including -1 values) .
|
||||
Time float32 `json:"time"`
|
||||
// Detailed info about the request.
|
||||
Request Request `json:"request"`
|
||||
// Detailed info about the response.
|
||||
Response Response `json:"response"`
|
||||
// Info about cache usage.
|
||||
Cache Cache `json:"cache"`
|
||||
// Detailed timing info about request/response round trip.
|
||||
PageTimings PageTimings `json:"pageTimings"`
|
||||
// optional (new in 1.2) IP address of the server that was connected
|
||||
// (result of DNS resolution).
|
||||
ServerIPAddress string `json:"serverIPAddress,omitempty"`
|
||||
// optional (new in 1.2) Unique ID of the parent TCP/IP connection, can be
|
||||
// the client port number. Note that a port number doesn't have to be unique
|
||||
// identifier in cases where the port is shared for more connections. If the
|
||||
// port isn't available for the application, any other unique connection ID
|
||||
// can be used instead (e.g. connection index). Leave out this field if the
|
||||
// application doesn't support this info.
|
||||
Connection string `json:"connection,omitempty"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Request contains detailed info about performed request.
|
||||
type Request struct {
|
||||
// Request method (GET, POST, ...).
|
||||
Method string `json:"method"`
|
||||
// Absolute URL of the request (fragments are not included).
|
||||
URL string `json:"url"`
|
||||
// Request HTTP Version.
|
||||
HTTPVersion string `json:"httpVersion"`
|
||||
// List of cookie objects.
|
||||
Cookies []Cookie `json:"cookies"`
|
||||
// List of header objects.
|
||||
Headers []NVP `json:"headers"`
|
||||
// List of query parameter objects.
|
||||
QueryString []NVP `json:"queryString"`
|
||||
// Posted data.
|
||||
PostData PostData `json:"postData"`
|
||||
// Total number of bytes from the start of the HTTP request message until
|
||||
// (and including) the double CRLF before the body. Set to -1 if the info
|
||||
// is not available.
|
||||
HeaderSize int `json:"headerSize"`
|
||||
// Size of the request body (POST data payload) in bytes. Set to -1 if the
|
||||
// info is not available.
|
||||
BodySize int `json:"bodySize"`
|
||||
// (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
// Response contains detailed info about the response.
|
||||
type Response struct {
|
||||
// Response status.
|
||||
Status int `json:"status"`
|
||||
// Response status description.
|
||||
StatusText string `json:"statusText"`
|
||||
// Response HTTP Version.
|
||||
HTTPVersion string `json:"httpVersion"`
|
||||
// List of cookie objects.
|
||||
Cookies []Cookie `json:"cookies"`
|
||||
// List of header objects.
|
||||
Headers []NVP `json:"headers"`
|
||||
// Details about the response body.
|
||||
Content Content `json:"content"`
|
||||
// Redirection target URL from the Location response header.
|
||||
RedirectURL string `json:"redirectURL"`
|
||||
// Total number of bytes from the start of the HTTP response message until
|
||||
// (and including) the double CRLF before the body. Set to -1 if the info is
|
||||
// not available.
|
||||
// The size of received response-headers is computed only from headers that
|
||||
// are really received from the server. Additional headers appended by the
|
||||
// browser are not included in this number, but they appear in the list of
|
||||
// header objects.
|
||||
HeadersSize int `json:"headersSize"`
|
||||
// Size of the received response body in bytes. Set to zero in case of
|
||||
// responses coming from the cache (304). Set to -1 if the info is not
|
||||
// available.
|
||||
BodySize int `json:"bodySize"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Cookie contains list of all cookies (used in <request> and <response> objects).
|
||||
type Cookie struct {
|
||||
// The name of the cookie.
|
||||
Name string `json:"name"`
|
||||
// The cookie value.
|
||||
Value string `json:"value"`
|
||||
// optional The path pertaining to the cookie.
|
||||
Path string `json:"path,omitempty"`
|
||||
// optional The host of the cookie.
|
||||
Domain string `json:"domain,omitempty"`
|
||||
// optional Cookie expiration time.
|
||||
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.123+02:00).
|
||||
Expires string `json:"expires,omitempty"`
|
||||
// optional Set to true if the cookie is HTTP only, false otherwise.
|
||||
HTTPOnly bool `json:"httpOnly,omitempty"`
|
||||
// optional (new in 1.2) True if the cookie was transmitted over ssl, false
|
||||
// otherwise.
|
||||
Secure bool `json:"secure,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment bool `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// NVP is simply a name/value pair with a comment
|
||||
type NVP struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PostData describes posted data, if any (embedded in <request> object).
|
||||
type PostData struct {
|
||||
// Mime type of posted data.
|
||||
MimeType string `json:"mimeType"`
|
||||
// List of posted parameters (in case of URL encoded parameters).
|
||||
Params []PostParam `json:"params"`
|
||||
// Plain text posted data
|
||||
Text string `json:"text"`
|
||||
// optional (new in 1.2) A comment provided by the user or the
|
||||
// application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PostParam is a list of posted parameters, if any (embedded in <postData> object).
|
||||
type PostParam struct {
|
||||
// name of a posted parameter.
|
||||
Name string `json:"name"`
|
||||
// optional value of a posted parameter or content of a posted file.
|
||||
Value string `json:"value,omitempty"`
|
||||
// optional name of a posted file.
|
||||
FileName string `json:"fileName,omitempty"`
|
||||
// optional content type of a posted file.
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Content describes details about response content (embedded in <response> object).
|
||||
type Content struct {
|
||||
// Length of the returned content in bytes. Should be equal to
|
||||
// response.bodySize if there is no compression and bigger when the content
|
||||
// has been compressed.
|
||||
Size int `json:"size"`
|
||||
// optional Number of bytes saved. Leave out this field if the information
|
||||
// is not available.
|
||||
Compression int `json:"compression,omitempty"`
|
||||
// MIME type of the response text (value of the Content-Type response
|
||||
// header). The charset attribute of the MIME type is included (if
|
||||
// available).
|
||||
MimeType string `json:"mimeType"`
|
||||
// optional Response body sent from the server or loaded from the browser
|
||||
// cache. This field is populated with textual content only. The text field
|
||||
// is either HTTP decoded text or a encoded (e.g. "base64") representation of
|
||||
// the response body. Leave out this field if the information is not
|
||||
// available.
|
||||
Text string `json:"text,omitempty"`
|
||||
// optional (new in 1.2) Encoding used for response text field e.g
|
||||
// "base64". Leave out this field if the text field is HTTP decoded
|
||||
// (decompressed & unchunked), than trans-coded from its original character
|
||||
// set into UTF-8.
|
||||
Encoding string `json:"encoding,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Cache contains info about a request coming from browser cache.
|
||||
type Cache struct {
|
||||
// optional State of a cache entry before the request. Leave out this field
|
||||
// if the information is not available.
|
||||
BeforeRequest CacheObject `json:"beforeRequest,omitempty"`
|
||||
// optional State of a cache entry after the request. Leave out this field if
|
||||
// the information is not available.
|
||||
AfterRequest CacheObject `json:"afterRequest,omitempty"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// CacheObject is used by both beforeRequest and afterRequest
|
||||
type CacheObject struct {
|
||||
// optional - Expiration time of the cache entry.
|
||||
Expires string `json:"expires,omitempty"`
|
||||
// The last time the cache entry was opened.
|
||||
LastAccess string `json:"lastAccess"`
|
||||
// Etag
|
||||
ETag string `json:"eTag"`
|
||||
// The number of times the cache entry has been opened.
|
||||
HitCount int `json:"hitCount"`
|
||||
// optional (new in 1.2) A comment provided by the user or the application.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// PageTimings describes various phases within request-response round trip.
|
||||
// All times are specified in milliseconds.
|
||||
type PageTimings struct {
|
||||
Blocked int `json:"blocked,omitempty"`
|
||||
// optional - Time spent in a queue waiting for a network connection. Use -1
|
||||
// if the timing does not apply to the current request.
|
||||
DNS int `json:"dns,omitempty"`
|
||||
// optional - DNS resolution time. The time required to resolve a host name.
|
||||
// Use -1 if the timing does not apply to the current request.
|
||||
Connect int `json:"connect,omitempty"`
|
||||
// optional - Time required to create TCP connection. Use -1 if the timing
|
||||
// does not apply to the current request.
|
||||
Send int `json:"send"`
|
||||
// Time required to send HTTP request to the server.
|
||||
Wait int `json:"wait"`
|
||||
// Waiting for a response from the server.
|
||||
Receive int `json:"receive"`
|
||||
// Time required to read entire response from the server (or cache).
|
||||
Ssl int `json:"ssl,omitempty"`
|
||||
// optional (new in 1.2) - Time required for SSL/TLS negotiation. If this
|
||||
// field is defined then the time is also included in the connect field (to
|
||||
// ensure backward compatibility with HAR 1.1). Use -1 if the timing does not
|
||||
// apply to the current request.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// optional (new in 1.2) - A comment provided by the user or the application.
|
||||
}
|
||||
|
||||
// TestResult contains results for an individual HTTP request
|
||||
type TestResult struct {
|
||||
URL string `json:"url"`
|
||||
Status int `json:"status"` // 200, 500, etc.
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Latency int `json:"latency"` // milliseconds
|
||||
Method string `json:"method"`
|
||||
HarFile string `json:"harfile"`
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package json
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// replace with third-party json library to improve performance
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
var (
|
||||
Marshal = json.Marshal
|
||||
MarshalIndent = json.MarshalIndent
|
||||
Unmarshal = json.Unmarshal
|
||||
NewDecoder = json.NewDecoder
|
||||
Get = json.Get
|
||||
)
|
|
@ -0,0 +1,359 @@
|
|||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestReport</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
color: #333;
|
||||
margin: 0 auto;
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
#summary {
|
||||
width: 960px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#summary th {
|
||||
background-color: skyblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
#summary td {
|
||||
background-color: lightblue;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 960px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.details th {
|
||||
background-color: skyblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.details tr .passed {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.details tr .failed {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.details tr .unchecked {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.details td {
|
||||
background-color: lightblue;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.details .detail {
|
||||
background-color: lightgrey;
|
||||
font-size: smaller;
|
||||
padding: 5px 10px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.details .success {
|
||||
background-color: greenyellow;
|
||||
}
|
||||
|
||||
.details .error {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.details .failure {
|
||||
background-color: salmon;
|
||||
}
|
||||
|
||||
.details .skipped {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 1em;
|
||||
padding: 6px;
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
background-color: #06d85f;
|
||||
border-radius: 20px/50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
a.button {
|
||||
color: gray;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #2cffbd;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transition: opacity 500ms;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.overlay:target {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.popup {
|
||||
margin: 70px auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 50%;
|
||||
position: relative;
|
||||
transition: all 3s ease-in-out;
|
||||
}
|
||||
|
||||
.popup h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-family: Tahoma, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.popup .close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
transition: all 200ms;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popup .close:hover {
|
||||
color: #06d85f;
|
||||
}
|
||||
|
||||
.popup .content {
|
||||
max-height: 80%;
|
||||
overflow: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.popup .separator {
|
||||
color: royalblue
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.box {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>API Test Report</h1>
|
||||
|
||||
<h2>Summary</h2>
|
||||
<table id="summary">
|
||||
<tr>
|
||||
<th>START AT</th>
|
||||
<td colspan="4">{{.Time.StartAt}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DURATION</th>
|
||||
<td colspan="4">{{ .Time.Duration }} seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>PLATFORM</th>
|
||||
<td>HttpRunnerPlus {{ .Platform.HttprunnerVersion }}</td>
|
||||
<td>{{ .Platform.GoVersion }}</td>
|
||||
<td colspan="2">{{ .Platform.Platform }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>STAT</th>
|
||||
<th colspan="2">TESTCASES (success/fail)</th>
|
||||
<th colspan="2">TESTSTEPS (success/fail/error/skip)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>total (details) =></td>
|
||||
<td colspan="2">{{.Stat.TestCases.Total}} ({{.Stat.TestCases.Success}}/{{.Stat.TestCases.Fail}})</td>
|
||||
<td colspan="2">{{.Stat.TestSteps.Total}} ({{.Stat.TestSteps.Successes}}/0/{{.Stat.TestSteps.Failures}}/0)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Details</h2>
|
||||
{{ range $suite_index, $detail := .Details }}
|
||||
<h3>{{.Name}}</h3>
|
||||
<table id="suite_{{$suite_index}}" class="details">
|
||||
<tr>
|
||||
<td>TOTAL: {{.Stat.Total}}</td>
|
||||
<td>SUCCESS: {{.Stat.Successes}}</td>
|
||||
<td>FAILED: 0</td>
|
||||
<td>ERROR: {{.Stat.Failures}}</td>
|
||||
<td>SKIPPED: 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th colspan="2">Name</th>
|
||||
<th>Response Time</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
{{- range $loop_index, $record := .Records }}
|
||||
{{- with $record}}
|
||||
{{- $status := "error"}}
|
||||
{{- if .Success }} {{ $status = "success" }} {{ end }}
|
||||
<tr id="record_{{$suite_index}}_{{$loop_index}}">
|
||||
<th class={{$status}} style="width:5em;">{{$status}}</th>
|
||||
<td colspan="2">{{.Name}}</td>
|
||||
<td style="text-align:center;width:6em;">{{ .Elapsed }} ms</td>
|
||||
<td class="detail">
|
||||
<a class="button" href="#popup_log_{{$suite_index}}_{{$loop_index}}">log</a>
|
||||
<div id="popup_log_{{$suite_index}}_{{$loop_index}}" class="overlay">
|
||||
<div class="popup">
|
||||
<h2>Request and Response data</h2>
|
||||
<a class="close" href="#record_{{$suite_index}}_{{$loop_index}}">×</a>
|
||||
<div class="content">
|
||||
<h3>Name: {{ .Name }}</h3>
|
||||
{{- if .Data}}
|
||||
<h3>Request:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
{{- range $key, $value := .Data.ReqResps.Request}}
|
||||
<tr>
|
||||
<th>{{$key}}</th>
|
||||
<td align="left">
|
||||
{{- if eq $key "headers" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "params" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "cookies" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else }}
|
||||
<pre>{{$value}}</pre>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</div>
|
||||
<h3>Response:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
{{- range $key, $value := .Data.ReqResps.Response}}
|
||||
<tr>
|
||||
<th>{{$key}}</th>
|
||||
<td align="left">
|
||||
{{- if eq $key "headers" }}
|
||||
{{- range $k, $v := $value}}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else if eq $key "cookies" }}
|
||||
{{- range $k, $v := $value }}
|
||||
<pre>{{$k}}: {{$v}}</pre>
|
||||
{{- end -}}
|
||||
{{- else }}
|
||||
<pre>{{ $value }}</pre>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Validators:</h3>
|
||||
<div style="overflow: auto">
|
||||
{{- if .Data.Validators }}
|
||||
<table>
|
||||
<tr>
|
||||
<th>check</th>
|
||||
<th>comparator</th>
|
||||
<th>expect value</th>
|
||||
<th>actual value</th>
|
||||
</tr>
|
||||
{{- range $validator := .Data.Validators }}
|
||||
<tr>
|
||||
{{- if eq $validator.CheckResult "pass" }}
|
||||
<td class="passed">
|
||||
{{- else if eq $validator.CheckResult "fail" }}
|
||||
<td class="failed">
|
||||
{{- else if eq $validator.CheckResult "unchecked" }}
|
||||
<td class="unchecked">
|
||||
{{- end }}
|
||||
{{$validator.Check}}
|
||||
</td>
|
||||
<td>{{$validator.Assert}}</td>
|
||||
<td>{{$validator.Expect}}</td>
|
||||
<td>{{$validator.CheckValue}}</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
{{- end }}
|
||||
|
||||
<h3>Statistics:</h3>
|
||||
<div style="overflow: auto">
|
||||
<table>
|
||||
<tr>
|
||||
<th>content_size(bytes)</th>
|
||||
<td>{{ .ContentSize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>response_time(ms)</th>
|
||||
<td>{{ .Elapsed }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>elapsed(ms)</th>
|
||||
<td>{{ .Elapsed }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Attachment }}
|
||||
<a class="button" href="#popup_attachment_{{$suite_index}}_{{$loop_index}}">traceback</a>
|
||||
<div id="popup_attachment_{{$suite_index}}_{{$loop_index}}" class="overlay">
|
||||
<div class="popup">
|
||||
<h2>Traceback Message</h2>
|
||||
<a class="close" href="#record_{{$suite_index}}_{{$loop_index}}">×</a>
|
||||
<div class="content">
|
||||
<pre>{{ .Attachment }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</table>
|
||||
{{- end }}
|
||||
</body>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue