Merge pull request #1269 from httprunner/convert-pytest

hrp convert pytest scripts

- feat: prepare python3 venv in `~/.hrp/venv` before running #1262 
- feat: convert YAML/JSON testcases to pytest scripts with `hrp convert` #1261 
- refactor: reformat all python code with black
This commit is contained in:
debugtalk
2022-04-24 21:32:26 +08:00
committed by GitHub
87 changed files with 736 additions and 796 deletions

View File

@@ -66,10 +66,14 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Build hrp binary - name: Build hrp binary
run: make build run: make build
- name: Run smoketest - postman echo - name: Run smoketest - run with parameters
run: ./output/hrp boom examples/hrp/postman-echo.json --spawn-count 10 --spawn-rate 10 --loop-count 10 run: ./output/hrp run examples/hrp/parameters_test.json
- name: Run smoketest - data driven with parameterize mechanism - name: Run smoketest - boom with parameters
run: ./output/hrp boom examples/hrp/parameters_test.json --spawn-count 10 --spawn-rate 10 --loop-count 10 run: ./output/hrp boom examples/hrp/parameters_test.json --spawn-count 10 --spawn-rate 10 --loop-count 10
- name: Run smoketest - rendezvous - name: Run smoketest - boom with rendezvous
run: | run: |
./output/hrp boom examples/hrp/rendezvous_test.json --spawn-count 10 --spawn-rate 10 --loop-count 10 ./output/hrp boom examples/hrp/rendezvous_test.json --spawn-count 10 --spawn-rate 10 --loop-count 10
- name: Run hrp convert --pytest
run: ./output/hrp convert examples/postman_echo/request_methods/
- name: Run hrp pytest
run: ./output/hrp pytest examples/postman_echo/request_methods/

View File

@@ -83,7 +83,7 @@ monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
License: Apache-2.0 License: Apache-2.0
Website: https://httprunner.com Website: https://httprunner.com
Github: https://github.com/httprunner/httprunner Github: https://github.com/httprunner/httprunner
Copyright 2021 debugtalk Copyright 2017 debugtalk
Usage: Usage:
hrp [command] hrp [command]

View File

@@ -76,7 +76,7 @@ monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
License: Apache-2.0 License: Apache-2.0
Website: https://httprunner.com Website: https://httprunner.com
Github: https://github.com/httprunner/httprunner Github: https://github.com/httprunner/httprunner
Copyright 2021 debugtalk Copyright 2017 debugtalk
Usage: Usage:
hrp [command] hrp [command]

View File

@@ -1,11 +1,12 @@
# Release History # Release History
## v4.0.0-alpha ## v4.0.0-beta (2022-04-24)
- refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine - refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine
- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types - refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types
- feat: disable GA events report by setting environment `DISABLE_GA=true` - feat: disable GA events report by setting environment `DISABLE_GA=true`
- feat: disable sentry reports by setting environment `DISABLE_SENTRY=true` - feat: disable sentry reports by setting environment `DISABLE_SENTRY=true`
- feat: prepare python3 venv in `~/.hrp/venv` before running
**go version** **go version**
@@ -13,6 +14,7 @@
- feat: support run testcases in specified folder path, including testcases in sub folders - feat: support run testcases in specified folder path, including testcases in sub folders
- feat: support HTTP/2 protocol - feat: support HTTP/2 protocol
- feat: support WebSocket protocol - feat: support WebSocket protocol
- feat: convert YAML/JSON testcases to pytest scripts with `hrp convert`
- change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis - change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis
- change: lock funplugin version when creating scaffold project - change: lock funplugin version when creating scaffold project
- fix: call referenced api/testcase with relative path - fix: call referenced api/testcase with relative path
@@ -163,6 +165,10 @@
- test: add CI test with [github actions][github-actions] - test: add CI test with [github actions][github-actions]
- test: integrate [sentry sdk][sentry sdk] for event reporting and analysis - test: integrate [sentry sdk][sentry sdk] for event reporting and analysis
## 3.1.11 (2022-04-24)
- fix #1273: ImportError by cannot import name '_unicodefun' from 'click'
## 3.1.10 (2022-04-18) ## 3.1.10 (2022-04-18)
- fix #1249: catch exceptions when requesting with disabling allow_redirects - fix #1249: catch exceptions when requesting with disabling allow_redirects

View File

@@ -19,7 +19,7 @@ monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
License: Apache-2.0 License: Apache-2.0
Website: https://httprunner.com Website: https://httprunner.com
Github: https://github.com/httprunner/httprunner Github: https://github.com/httprunner/httprunner
Copyright 2021 debugtalk Copyright 2017 debugtalk
### Options ### Options
@@ -30,9 +30,10 @@ Copyright 2021 debugtalk
### SEE ALSO ### SEE ALSO
* [hrp boom](hrp_boom.md) - run load test with boomer * [hrp boom](hrp_boom.md) - run load test with boomer
* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files * [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
* [hrp pytest](hrp_pytest.md) - run API test with pytest * [hrp pytest](hrp_pytest.md) - run API test with pytest
* [hrp run](hrp_run.md) - run API test with go engine * [hrp run](hrp_run.md) - run API test with go engine
* [hrp startproject](hrp_startproject.md) - create a scaffold project * [hrp startproject](hrp_startproject.md) - create a scaffold project
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -41,4 +41,4 @@ hrp boom [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

21
docs/cmd/hrp_convert.md Normal file
View File

@@ -0,0 +1,21 @@
## hrp convert
convert JSON/YAML testcases to pytest/gotest scripts
```
hrp convert $path... [flags]
```
### Options
```
--gotest convert to gotest scripts (TODO)
-h, --help help for convert
--pytest convert to pytest scripts (default true)
```
### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -24,4 +24,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -34,4 +34,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -9,6 +9,7 @@ hrp startproject $project_name [flags]
### Options ### Options
``` ```
-f, --force force to overwrite existing project
--go generate hashicorp go plugin --go generate hashicorp go plugin
-h, --help help for startproject -h, --help help for startproject
--ignore-plugin ignore function plugin --ignore-plugin ignore function plugin
@@ -19,4 +20,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 17-Apr-2022 ###### Auto generated by spf13/cobra on 24-Apr-2022

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: a-b.c/1.yml # FROM: a-b.c/1.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: a-b.c/2 3.yml # FROM: a-b.c/2 3.yml

View File

@@ -42,7 +42,7 @@ func TeardownHookExample(args string) string {
} }
func GetVersion() string { func GetVersion() string {
return "v4.0.0-alpha" return "v4.0.0-beta"
} }
func main() { func main() {

View File

@@ -2,4 +2,4 @@ module plugin
go 1.16 go 1.16
require github.com/httprunner/funplugin v0.4.2 // indirect require github.com/httprunner/funplugin v0.4.3 // indirect

View File

@@ -58,8 +58,8 @@ github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.2 h1:iDeg3GVCKdimgZQ40xq0kxHqhL/DQmRxs3DRjzOpUuo= github.com/httprunner/funplugin v0.4.3 h1:mxdxQh54NZLQnK/FXZxpZV0rhqZQzckrWKEnBW5w2Vg=
github.com/httprunner/funplugin v0.4.2/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/httprunner/funplugin v0.4.3/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=

View File

@@ -6,7 +6,7 @@ import funppy
def get_httprunner_version(): def get_httprunner_version():
return "v4.0.0-alpha" return "v4.0.0-beta"
def sleep(n_secs): def sleep(n_secs):
@@ -59,7 +59,7 @@ def teardown_hook_example(name):
return f"teardown_hook_example: {name}" return f"teardown_hook_example: {name}"
if __name__ == '__main__': if __name__ == "__main__":
funppy.register("get_httprunner_version", get_httprunner_version) funppy.register("get_httprunner_version", get_httprunner_version)
funppy.register("sum", sum) funppy.register("sum", sum)
funppy.register("sum_ints", sum_ints) funppy.register("sum_ints", sum_ints)

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: basic.yml # FROM: basic.yml

View File

@@ -36,8 +36,8 @@ def sum_two(m, n):
def sum_status_code(status_code, expect_sum): def sum_status_code(status_code, expect_sum):
""" sum status code digits """sum status code digits
e.g. 400 => 4, 201 => 3 e.g. 400 => 4, 201 => 3
""" """
sum_value = 0 sum_value = 0
for digit in str(status_code): for digit in str(status_code):
@@ -54,8 +54,7 @@ os.environ["TEST_ENV"] = "PRODUCTION"
def skip_test_in_production_env(): def skip_test_in_production_env():
""" skip this test in production environment """skip this test in production environment"""
"""
return os.environ["TEST_ENV"] == "PRODUCTION" return os.environ["TEST_ENV"] == "PRODUCTION"
@@ -97,8 +96,7 @@ def setup_hook_remove_kwargs(request):
def teardown_hook_sleep_N_secs(response, n_secs): def teardown_hook_sleep_N_secs(response, n_secs):
""" sleep n seconds after request """sleep n seconds after request"""
"""
if response.status_code == 200: if response.status_code == 200:
time.sleep(0.1) time.sleep(0.1)
else: else:

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: hooks.yml # FROM: hooks.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: load_image.yml # FROM: load_image.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: upload.yml # FROM: upload.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: validate.yml # FROM: validate.yml

View File

@@ -54,8 +54,7 @@ def session_fixture(request):
summary["details"].append(testcase_summary_json) summary["details"].append(testcase_summary_json)
summary_path = os.path.join( summary_path = os.path.join(
os.getcwd(), os.getcwd(), "examples/postman_echo/logs/request_methods/hardcode.summary.json"
"examples/postman_echo/logs/request_methods/hardcode.summary.json"
) )
summary_dir = os.path.dirname(summary_path) summary_dir = os.path.dirname(summary_path)
os.makedirs(summary_dir, exist_ok=True) os.makedirs(summary_dir, exist_ok=True)

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: cookie_manipulation/hardcode.yml # FROM: cookie_manipulation/hardcode.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: cookie_manipulation/set_delete_cookies.yml # FROM: cookie_manipulation/set_delete_cookies.yml

View File

@@ -1,22 +0,0 @@
config:
name: "demo testsuite"
variables: ${get_testsuite_config_variables()}
testcases:
-
name: request with functions
testcase: request_methods/request_with_functions.yml
weight: 2
variables:
foo1: testcase_ref_bar11
expect_foo1: testcase_ref_bar11
expect_foo2: testsuite_config_bar2
-
name: request with referenced testcase
testcase: request_methods/request_with_testcase_reference.yml
weight: 3
variables:
foo1: testcase_ref_bar12
expect_foo1: testcase_ref_bar12
foo2: testcase_ref_bar22
expect_foo2: testcase_ref_bar22

View File

@@ -1 +0,0 @@
# NOTICE: Generated By HttpRunner. DO NOT EDIT!

View File

@@ -1,86 +0,0 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseRequestWithFunctions(HttpRunner):
config = (
Config("request with functions")
.variables(
**{
"foo1": "testcase_ref_bar11",
"foo2": "testsuite_config_bar2",
"expect_foo1": "testcase_ref_bar11",
"expect_foo2": "testsuite_config_bar2",
}
)
.base_url("https://postman-echo.com")
.verify(False)
.export(*["foo3"])
)
teststeps = [
Step(
RunRequest("get with params")
.with_variables(
**{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"}
)
.get("/get")
.with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"})
.with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"})
.extract()
.with_jmespath("body.args.foo2", "foo3")
.validate()
.assert_equal("status_code", 200)
.assert_equal("body.args.foo1", "bar11")
.assert_equal("body.args.sum_v", "3")
.assert_equal("body.args.foo2", "bar21")
),
Step(
RunRequest("post raw text")
.with_variables(**{"foo1": "bar12", "foo3": "bar32"})
.post("/post")
.with_headers(
**{
"User-Agent": "HttpRunner/${get_httprunner_version()}",
"Content-Type": "text/plain",
}
)
.with_data(
"This is expected to be sent back as part of response body: $foo1-$foo2-$foo3."
)
.validate()
.assert_equal("status_code", 200)
.assert_equal(
"body.data",
"This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.",
)
.assert_type_match("body.json", "None")
.assert_type_match("body.json", "NoneType")
.assert_type_match("body.json", None)
),
Step(
RunRequest("post form data")
.with_variables(**{"foo2": "bar23"})
.post("/post")
.with_headers(
**{
"User-Agent": "HttpRunner/${get_httprunner_version()}",
"Content-Type": "application/x-www-form-urlencoded",
}
)
.with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3")
.validate()
.assert_equal("status_code", 200, "response status code should be 200")
.assert_equal("body.form.foo1", "$expect_foo1")
.assert_equal("body.form.foo2", "bar23")
.assert_equal("body.form.foo3", "bar21")
),
]
if __name__ == "__main__":
TestCaseRequestWithFunctions().test_start()

View File

@@ -1,65 +0,0 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: request_methods/request_with_testcase_reference.yml
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
from request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions as RequestWithFunctions,
)
class TestCaseRequestWithTestcaseReference(HttpRunner):
config = (
Config("request with referenced testcase")
.variables(
**{
"foo1": "testcase_ref_bar12",
"expect_foo1": "testcase_ref_bar12",
"expect_foo2": "testcase_ref_bar22",
"foo2": "testcase_ref_bar22",
}
)
.base_url("https://postman-echo.com")
.verify(False)
)
teststeps = [
Step(
RunTestCase("request with functions")
.with_variables(
**{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"}
)
.setup_hook("${sleep(0.1)}")
.call(RequestWithFunctions)
.teardown_hook("${sleep(0.2)}")
.export(*["foo3"])
),
Step(
RunRequest("post form data")
.with_variables(**{"foo1": "bar1"})
.post("/post")
.with_headers(
**{
"User-Agent": "HttpRunner/${get_httprunner_version()}",
"Content-Type": "application/x-www-form-urlencoded",
}
)
.with_data("foo1=$foo1&foo2=$foo3")
.validate()
.assert_equal("status_code", 200)
.assert_equal("body.form.foo1", "bar1")
.assert_equal("body.form.foo2", "bar21")
),
]
if __name__ == "__main__":
TestCaseRequestWithTestcaseReference().test_start()

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/hardcode.yml # FROM: request_methods/hardcode.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/request_with_functions.yml # FROM: request_methods/request_with_functions.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/request_with_parameters.yml # FROM: request_methods/request_with_parameters.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/request_with_testcase_reference.yml # FROM: request_methods/request_with_testcase_reference.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/request_with_variables.yml # FROM: request_methods/request_with_variables.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/validate_with_functions.yml # FROM: request_methods/validate_with_functions.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: request_methods/validate_with_variables.yml # FROM: request_methods/validate_with_variables.yml

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/getsentry/sentry-go v0.13.0 github.com/getsentry/sentry-go v0.13.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
github.com/httprunner/funplugin v0.4.2 github.com/httprunner/funplugin v0.4.3
github.com/jinzhu/copier v0.3.2 github.com/jinzhu/copier v0.3.2
github.com/jmespath/go-jmespath v0.4.0 github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12

4
go.sum
View File

@@ -241,8 +241,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.2 h1:iDeg3GVCKdimgZQ40xq0kxHqhL/DQmRxs3DRjzOpUuo= github.com/httprunner/funplugin v0.4.3 h1:mxdxQh54NZLQnK/FXZxpZV0rhqZQzckrWKEnBW5w2Vg=
github.com/httprunner/funplugin v0.4.2/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= github.com/httprunner/funplugin v0.4.3/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=

View File

@@ -46,7 +46,7 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
var taskSlice []*boomer.Task var taskSlice []*boomer.Task
// load all testcases // load all testcases
testCases, err := loadTestCases(testcases...) testCases, err := LoadTestCases(testcases...)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to load testcases") log.Error().Err(err).Msg("failed to load testcases")
os.Exit(1) os.Exit(1)

48
hrp/cmd/convert.go Normal file
View File

@@ -0,0 +1,48 @@
package cmd
import (
"errors"
"os"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/hrp/internal/convert"
)
var convertCmd = &cobra.Command{
Use: "convert $path...",
Short: "convert JSON/YAML testcases to pytest/gotest scripts",
Args: cobra.ExactValidArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
if !pytestFlag && !gotestFlag {
return errors.New("please specify convertion type")
}
var err error
if gotestFlag {
err = convert.Convert2TestScripts("gotest", args...)
} else {
err = convert.Convert2TestScripts("pytest", args...)
}
if err != nil {
log.Error().Err(err).Msg("convert test scripts failed")
os.Exit(1)
}
return nil
},
}
var (
pytestFlag bool
gotestFlag bool
)
func init() {
rootCmd.AddCommand(convertCmd)
convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts")
convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)")
}

View File

@@ -31,7 +31,7 @@ monitoring (DEM) test types. Enjoy! ✨ 🚀 ✨
License: Apache-2.0 License: Apache-2.0
Website: https://httprunner.com Website: https://httprunner.com
Github: https://github.com/httprunner/httprunner Github: https://github.com/httprunner/httprunner
Copyright 2021 debugtalk`, Copyright 2017 debugtalk`,
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
var noColor = false var noColor = false
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {

View File

@@ -19,7 +19,7 @@ var scaffoldCmd = &cobra.Command{
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if !ignorePlugin && !genPythonPlugin && !genGoPlugin { if !ignorePlugin && !genPythonPlugin && !genGoPlugin {
return errors.New("please select function plugin type") return errors.New("please specify function plugin type")
} }
var pluginType scaffold.PluginType var pluginType scaffold.PluginType
@@ -31,7 +31,7 @@ var scaffoldCmd = &cobra.Command{
pluginType = scaffold.Py // default pluginType = scaffold.Py // default
} }
err := scaffold.CreateScaffold(args[0], pluginType) err := scaffold.CreateScaffold(args[0], pluginType, force)
if err != nil { if err != nil {
log.Error().Err(err).Msg("create scaffold project failed") log.Error().Err(err).Msg("create scaffold project failed")
os.Exit(1) os.Exit(1)
@@ -45,10 +45,12 @@ var (
ignorePlugin bool ignorePlugin bool
genPythonPlugin bool genPythonPlugin bool
genGoPlugin bool genGoPlugin bool
force bool
) )
func init() { func init() {
rootCmd.AddCommand(scaffoldCmd) rootCmd.AddCommand(scaffoldCmd)
scaffoldCmd.Flags().BoolVarP(&force, "force", "f", false, "force to overwrite existing project")
scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin") scaffoldCmd.Flags().BoolVar(&genPythonPlugin, "py", true, "generate hashicorp python plugin")
scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin") scaffoldCmd.Flags().BoolVar(&genGoPlugin, "go", false, "generate hashicorp go plugin")
scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin") scaffoldCmd.Flags().BoolVar(&ignorePlugin, "ignore-plugin", false, "ignore function plugin")

View File

@@ -9,9 +9,9 @@ import (
) )
var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected interface{}, msgAndArgs ...interface{}) bool{ var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected interface{}, msgAndArgs ...interface{}) bool{
"eq": assert.EqualValues, "eq": EqualValues,
"equals": assert.EqualValues, "equals": EqualValues,
"equal": assert.EqualValues, "equal": EqualValues,
"lt": assert.Less, "lt": assert.Less,
"less_than": assert.Less, "less_than": assert.Less,
"le": assert.LessOrEqual, "le": assert.LessOrEqual,
@@ -20,8 +20,8 @@ var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected
"greater_than": assert.Greater, "greater_than": assert.Greater,
"ge": assert.GreaterOrEqual, "ge": assert.GreaterOrEqual,
"greater_or_equals": assert.GreaterOrEqual, "greater_or_equals": assert.GreaterOrEqual,
"ne": assert.NotEqual, "ne": NotEqual,
"not_equal": assert.NotEqual, "not_equal": NotEqual,
"contains": assert.Contains, "contains": assert.Contains,
"type_match": assert.IsType, "type_match": assert.IsType,
// custom assertions // custom assertions
@@ -48,6 +48,14 @@ var Assertions = map[string]func(t assert.TestingT, actual interface{}, expected
"regex_match": RegexMatch, "regex_match": RegexMatch,
} }
func EqualValues(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
return assert.EqualValues(t, expected, actual, msgAndArgs)
}
func NotEqual(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
return assert.NotEqual(t, expected, actual, msgAndArgs)
}
// StartsWith check if string starts with substring // StartsWith check if string starts with substring
func StartsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool { func StartsWith(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool {
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) { if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {

View File

@@ -17,6 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/httprunner/funplugin/shared"
"github.com/httprunner/httprunner/hrp/internal/json" "github.com/httprunner/httprunner/hrp/internal/json"
) )
@@ -76,16 +77,59 @@ func FormatResponse(raw interface{}) interface{} {
return formattedResponse return formattedResponse
} }
func ExecCommand(cmd *exec.Cmd, cwd string) error { func EnsurePython3Venv(packages ...string) (string, error) {
log.Info().Str("cmd", cmd.String()).Str("cwd", cwd).Msg("exec command") // create python venv
cmd.Dir = cwd home, err := os.UserHomeDir()
output, err := cmd.CombinedOutput()
out := strings.TrimSpace(string(output))
if err != nil { if err != nil {
log.Error().Err(err).Str("output", out).Msg("exec command failed") return "", errors.Wrap(err, "get user home dir failed")
} else if len(out) != 0 {
log.Info().Str("output", out).Msg("exec command success")
} }
venvDir := filepath.Join(home, ".hrp", "venv")
python3, err := shared.EnsurePython3Venv(venvDir, packages...)
if err != nil {
return "", errors.Wrap(err, "ensure python venv failed")
}
return python3, nil
}
func ExecCommandInDir(cmd *exec.Cmd, dir string) error {
log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command")
cmd.Dir = dir
// print output with colors
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Error().Err(err).Msg("exec command failed")
return err
}
return nil
}
func ExecCommand(cmdName string, args ...string) error {
cmd := exec.Command(cmdName, args...)
log.Info().Str("cmd", cmd.String()).Msg("exec command")
// print output with colors
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// add cmd dir path to PATH
PATH := fmt.Sprintf("%s:%s", filepath.Dir(cmdName), os.Getenv("PATH"))
if err := os.Setenv("PATH", PATH); err != nil {
log.Error().Err(err).Msg("failed to add cmd dir path to $PATH")
return err
}
err := cmd.Run()
if err != nil {
log.Error().Err(err).Msg("exec command failed")
return err
}
return err return err
} }

View File

@@ -0,0 +1,118 @@
package convert
import (
_ "embed"
"fmt"
"os"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/hrp"
"github.com/httprunner/httprunner/hrp/internal/builtin"
"github.com/httprunner/httprunner/hrp/internal/sdk"
)
func Convert2TestScripts(destType string, paths ...string) error {
// report event
sdk.SendEvent(sdk.EventTracking{
Category: "ConvertTests",
Action: fmt.Sprintf("hrp convert --%s", destType),
})
if destType == "gotest" {
return convert2GoTestScripts(paths...)
} else {
// default to pytest
return convert2PyTestScripts(paths...)
}
}
func convert2PyTestScripts(paths ...string) error {
python3, err := builtin.EnsurePython3Venv("httprunner")
if err != nil {
return err
}
args := append([]string{"-m", "httprunner", "make"}, paths...)
return builtin.ExecCommand(python3, args...)
}
func convert2GoTestScripts(paths ...string) error {
log.Warn().Msg("convert to gotest scripts is not supported yet")
os.Exit(1)
// TODO
var testCasePaths []hrp.ITestCase
for _, path := range paths {
testCasePath := hrp.TestCasePath(path)
testCasePaths = append(testCasePaths, &testCasePath)
}
testCases, err := hrp.LoadTestCases(testCasePaths...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
var pytestPaths []string
for _, testCase := range testCases {
tc := testCase.ToTCase()
converter := CaseConverter{
TCase: tc,
}
pytestPath, err := converter.ToPyTest()
if err != nil {
log.Error().Err(err).
Str("originPath", tc.Config.Path).
Msg("convert to pytest failed")
continue
}
log.Info().
Str("pytestPath", pytestPath).
Str("originPath", tc.Config.Path).
Msg("convert to pytest success")
pytestPaths = append(pytestPaths, pytestPath)
}
// format pytest scripts with black
python3, err := builtin.EnsurePython3Venv("black")
if err != nil {
return err
}
args := append([]string{"-m", "black"}, pytestPaths...)
return builtin.ExecCommand(python3, args...)
}
//go:embed testcase.tmpl
var testcaseTemplate string
type CaseConverter struct {
*hrp.TCase
}
func (c *CaseConverter) ToPyTest() (string, error) {
script := convertConfig(c.TCase.Config)
println(script)
return script, nil
}
func (c *CaseConverter) ToGoTest() (string, error) {
return "", nil
}
func convertConfig(config *hrp.TConfig) string {
script := fmt.Sprintf("Config('%s')", config.Name)
if config.Variables != nil {
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
}
if config.BaseURL != "" {
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
}
if config.Export != nil {
script += fmt.Sprintf(".export(*%v)", config.Export)
}
script += fmt.Sprintf(".verify(%v)", config.Verify)
return script
}

View File

@@ -0,0 +1,38 @@
# NOTE: Generated By HttpRunner v{{ version }}
# FROM: {{ testcase_path }}
{% if imports_list and diff_levels > 0 %}
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %}))
{% endif %}
{% if parameters %}
import pytest
from httprunner import Parameters
{% endif %}
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
{% for import_str in imports_list %}
{{ import_str }}
{% endfor %}
class {{ class_name }}(HttpRunner):
{% if parameters %}
@pytest.mark.parametrize("param", Parameters({{parameters}}))
def test_start(self, param):
super().test_start(param)
{% endif %}
config = {{ config_chain_style }}
teststeps = [
{% for step_chain_style in teststeps_chain_style %}
{{ step_chain_style }},
{% endfor %}
]
if __name__ == "__main__":
{{ class_name }}().test_start()

View File

@@ -1,23 +1,21 @@
package pytest package pytest
import ( import (
"os/exec" "github.com/httprunner/httprunner/hrp/internal/builtin"
"strings" "github.com/httprunner/httprunner/hrp/internal/sdk"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
) )
func RunPytest(args []string) error { func RunPytest(args []string) error {
cmd := exec.Command("pytest", args...) sdk.SendEvent(sdk.EventTracking{
log.Info().Str("cmd", cmd.String()).Msg("run pytest") Category: "RunAPITests",
Action: "hrp pytest",
})
output, err := cmd.CombinedOutput() python3, err := builtin.EnsurePython3Venv("httprunner")
if err != nil { if err != nil {
return errors.Wrap(err, "pytest running failed") return err
} }
out := strings.TrimSpace(string(output))
println(out)
return nil args = append([]string{"-m", "httprunner", "run"}, args...)
return builtin.ExecCommand(python3, args...)
} }

View File

@@ -1,28 +1,24 @@
package scaffold package scaffold
import ( import (
"os"
"testing" "testing"
) )
func TestGenDemoExamples(t *testing.T) { func TestGenDemoExamples(t *testing.T) {
dir := "../../../examples/demo-with-go-plugin" dir := "../../../examples/demo-with-go-plugin"
os.RemoveAll(dir) err := CreateScaffold(dir, Go, true)
err := CreateScaffold(dir, Go)
if err != nil { if err != nil {
t.Fatal() t.Fatal()
} }
dir = "../../../examples/demo-with-py-plugin" dir = "../../../examples/demo-with-py-plugin"
os.RemoveAll(dir) err = CreateScaffold(dir, Py, true)
err = CreateScaffold(dir, Py)
if err != nil { if err != nil {
t.Fatal() t.Fatal()
} }
dir = "../../../examples/demo-without-plugin" dir = "../../../examples/demo-without-plugin"
os.RemoveAll(dir) err = CreateScaffold(dir, Ignore, true)
err = CreateScaffold(dir, Ignore)
if err != nil { if err != nil {
t.Fatal() t.Fatal()
} }

View File

@@ -42,25 +42,32 @@ func CopyFile(templateFile, targetFile string) error {
return nil return nil
} }
func CreateScaffold(projectName string, pluginType PluginType) error { func CreateScaffold(projectName string, pluginType PluginType, force bool) error {
// report event // report event
sdk.SendEvent(sdk.EventTracking{ sdk.SendEvent(sdk.EventTracking{
Category: "Scaffold", Category: "Scaffold",
Action: "hrp startproject", Action: "hrp startproject",
}) })
// check if projectName exists
if _, err := os.Stat(projectName); err == nil {
log.Warn().Str("projectName", projectName).
Msg("project name already exists, please specify a new one.")
return fmt.Errorf("project name already exists")
}
log.Info(). log.Info().
Str("projectName", projectName). Str("projectName", projectName).
Str("pluginType", string(pluginType)). Str("pluginType", string(pluginType)).
Bool("force", force).
Msg("create new scaffold project") Msg("create new scaffold project")
// check if projectName exists
if _, err := os.Stat(projectName); err == nil {
if !force {
log.Warn().Str("projectName", projectName).
Msg("project name already exists, please specify a new one.")
return fmt.Errorf("project name already exists")
}
log.Warn().Str("projectName", projectName).
Msg("project name already exists, remove first !!!")
os.RemoveAll(projectName)
}
// create project folders // create project folders
if err := builtin.CreateFolder(projectName); err != nil { if err := builtin.CreateFolder(projectName); err != nil {
return err return err
@@ -133,7 +140,7 @@ func CreateScaffold(projectName string, pluginType PluginType) error {
func createGoPlugin(projectName string) error { func createGoPlugin(projectName string) error {
log.Info().Msg("start to create hashicorp go plugin") log.Info().Msg("start to create hashicorp go plugin")
// check go sdk // check go sdk
if err := builtin.ExecCommand(exec.Command("go", "version"), projectName); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "version"), projectName); err != nil {
return errors.Wrap(err, "go sdk not installed") return errors.Wrap(err, "go sdk not installed")
} }
@@ -149,19 +156,19 @@ func createGoPlugin(projectName string) error {
} }
// create go mod // create go mod
if err := builtin.ExecCommand(exec.Command("go", "mod", "init", "plugin"), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "plugin"), pluginDir); err != nil {
return err return err
} }
// download plugin dependency // download plugin dependency
// funplugin version should be locked // funplugin version should be locked
funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version) funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version)
if err := builtin.ExecCommand(exec.Command("go", "get", funplugin), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil {
return err return err
} }
// build plugin debugtalk.bin // build plugin debugtalk.bin
if err := builtin.ExecCommand(exec.Command("go", "build", "-o", filepath.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil { if err := builtin.ExecCommandInDir(exec.Command("go", "build", "-o", filepath.Join("..", "debugtalk.bin"), "debugtalk.go"), pluginDir); err != nil {
return err return err
} }
@@ -178,15 +185,9 @@ func createPythonPlugin(projectName string) error {
return errors.Wrap(err, "copy file failed") return errors.Wrap(err, "copy file failed")
} }
// create python venv _, err = builtin.EnsurePython3Venv(fmt.Sprintf("funppy==%s", shared.Version))
home, err := os.UserHomeDir()
if err != nil { if err != nil {
return errors.Wrap(err, "get user home dir failed") return err
}
venvDir := filepath.Join(home, ".hrp", "venv")
_, err = shared.EnsurePython3Venv(venvDir)
if err != nil {
return errors.Wrap(err, "ensure python venv failed")
} }
return nil return nil

View File

@@ -42,7 +42,7 @@ func TeardownHookExample(args string) string {
} }
func GetVersion() string { func GetVersion() string {
return "v4.0.0-alpha" return "v4.0.0-beta"
} }
func main() { func main() {

View File

@@ -6,7 +6,7 @@ import funppy
def get_httprunner_version(): def get_httprunner_version():
return "v4.0.0-alpha" return "v4.0.0-beta"
def sleep(n_secs): def sleep(n_secs):
@@ -59,7 +59,7 @@ def teardown_hook_example(name):
return f"teardown_hook_example: {name}" return f"teardown_hook_example: {name}"
if __name__ == '__main__': if __name__ == "__main__":
funppy.register("get_httprunner_version", get_httprunner_version) funppy.register("get_httprunner_version", get_httprunner_version)
funppy.register("sum", sum) funppy.register("sum", sum)
funppy.register("sum_ints", sum_ints) funppy.register("sum_ints", sum_ints)

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: testcases/demo_ref_testcase.yml # FROM: testcases/demo_ref_testcase.yml

View File

@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v4.0.0-alpha # NOTE: Generated By HttpRunner v4.0.0-beta
# FROM: testcases/demo_requests.yml # FROM: testcases/demo_requests.yml

View File

@@ -1,3 +1,3 @@
package version package version
const VERSION = "v4.0.0-alpha" const VERSION = "v4.0.0-beta"

View File

@@ -151,8 +151,9 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
s := newOutSummary() s := newOutSummary()
// load all testcases // load all testcases
testCases, err := loadTestCases(testcases...) testCases, err := LoadTestCases(testcases...)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err return err
} }

View File

@@ -2,20 +2,21 @@ package hrp
import ( import (
"os" "os"
"os/exec"
"testing" "testing"
"time" "time"
"github.com/httprunner/httprunner/hrp/internal/scaffold"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/httprunner/httprunner/hrp/internal/builtin"
"github.com/httprunner/httprunner/hrp/internal/scaffold"
) )
func buildHashicorpGoPlugin() { func buildHashicorpGoPlugin() {
log.Info().Msg("[init] build hashicorp go plugin") log.Info().Msg("[init] build hashicorp go plugin")
cmd := exec.Command("go", "build", err := builtin.ExecCommand("go", "build",
"-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go") "-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go")
if err := cmd.Run(); err != nil { if err != nil {
log.Error().Err(err).Msg("build hashicorp go plugin failed") log.Error().Err(err).Msg("build hashicorp go plugin failed")
os.Exit(1) os.Exit(1)
} }
@@ -202,7 +203,7 @@ func TestRunCaseWithRefAPI(t *testing.T) {
func TestLoadTestCases(t *testing.T) { func TestLoadTestCases(t *testing.T) {
// load test cases from folder path // load test cases from folder path
tc := TestCasePath("../examples/demo-with-py-plugin/testcases/") tc := TestCasePath("../examples/demo-with-py-plugin/testcases/")
testCases, err := loadTestCases(&tc) testCases, err := LoadTestCases(&tc)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }
@@ -212,7 +213,7 @@ func TestLoadTestCases(t *testing.T) {
// load test cases from folder path, including sub folders // load test cases from folder path, including sub folders
tc = TestCasePath("../examples/demo-with-py-plugin/") tc = TestCasePath("../examples/demo-with-py-plugin/")
testCases, err = loadTestCases(&tc) testCases, err = LoadTestCases(&tc)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }
@@ -222,7 +223,7 @@ func TestLoadTestCases(t *testing.T) {
// load test cases from single file path // load test cases from single file path
tc = demoTestCaseWithPluginJSONPath tc = demoTestCaseWithPluginJSONPath
testCases, err = loadTestCases(&tc) testCases, err = LoadTestCases(&tc)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }
@@ -234,7 +235,7 @@ func TestLoadTestCases(t *testing.T) {
testcase := &TestCase{ testcase := &TestCase{
Config: NewConfig("TestCase").SetWeight(3), Config: NewConfig("TestCase").SetWeight(3),
} }
testCases, err = loadTestCases(testcase) testCases, err = LoadTestCases(testcase)
if !assert.Nil(t, err) { if !assert.Nil(t, err) {
t.Fatal() t.Fatal()
} }

View File

@@ -240,7 +240,7 @@ func convertCheckExpr(checkExpr string) string {
return strings.Join(checkItems, ".") return strings.Join(checkItems, ".")
} }
func loadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
testCases := make([]*TestCase, 0) testCases := make([]*TestCase, 0)
for _, iTestCase := range iTestCases { for _, iTestCase := range iTestCases {

View File

@@ -1,4 +1,4 @@
__version__ = "4.0.0-alpha" __version__ = "4.0.0-beta"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
from httprunner.config import Config from httprunner.config import Config

View File

@@ -11,16 +11,14 @@ from httprunner.exceptions import ParamsError
def gen_random_string(str_len): def gen_random_string(str_len):
""" generate random string with specified length """generate random string with specified length"""
"""
return "".join( return "".join(
random.choice(string.ascii_letters + string.digits) for _ in range(str_len) random.choice(string.ascii_letters + string.digits) for _ in range(str_len)
) )
def get_timestamp(str_len=13): def get_timestamp(str_len=13):
""" get timestamp string, length can only between 0 and 16 """get timestamp string, length can only between 0 and 16"""
"""
if isinstance(str_len, int) and 0 < str_len < 17: if isinstance(str_len, int) and 0 < str_len < 17:
return str(time.time()).replace(".", "")[:str_len] return str(time.time()).replace(".", "")[:str_len]
@@ -28,12 +26,10 @@ def get_timestamp(str_len=13):
def get_current_date(fmt="%Y-%m-%d"): def get_current_date(fmt="%Y-%m-%d"):
""" get current date, default format is %Y-%m-%d """get current date, default format is %Y-%m-%d"""
"""
return datetime.datetime.now().strftime(fmt) return datetime.datetime.now().strftime(fmt)
def sleep(n_secs): def sleep(n_secs):
""" sleep n seconds """sleep n seconds"""
"""
time.sleep(n_secs) time.sleep(n_secs)

View File

@@ -9,7 +9,7 @@ from loguru import logger
from httprunner import __description__, __version__ from httprunner import __description__, __version__
from httprunner.compat import ensure_cli_args from httprunner.compat import ensure_cli_args
from httprunner.make import init_make_parser, main_make from httprunner.make import init_make_parser, main_make
from httprunner.utils import ga_client, init_sentry_sdk from httprunner.utils import ga_client, init_logger, init_sentry_sdk
init_sentry_sdk() init_sentry_sdk()
@@ -55,8 +55,9 @@ def main_run(extra_args) -> enum.IntEnum:
def main(): def main():
""" API test: parse command line options and run commands. """API test: parse command line options and run commands."""
""" init_logger()
parser = argparse.ArgumentParser(description=__description__) parser = argparse.ArgumentParser(description=__description__)
parser.add_argument( parser.add_argument(
"-V", "--version", dest="version", action="store_true", help="show version" "-V", "--version", dest="version", action="store_true", help="show version"
@@ -109,8 +110,8 @@ def main():
def main_hrun_alias(): def main_hrun_alias():
""" command alias """command alias
hrun = httprunner run hrun = httprunner run
""" """
if len(sys.argv) == 2: if len(sys.argv) == 2:
if sys.argv[1] in ["-V", "--version"]: if sys.argv[1] in ["-V", "--version"]:
@@ -129,8 +130,8 @@ def main_hrun_alias():
def main_make_alias(): def main_make_alias():
""" command alias """command alias
hmake = httprunner make hmake = httprunner make
""" """
sys.argv.insert(1, "make") sys.argv.insert(1, "make")
main() main()

View File

@@ -27,8 +27,7 @@ class ApiResponse(Response):
def get_req_resp_record(resp_obj: Response) -> ReqRespData: def get_req_resp_record(resp_obj: Response) -> ReqRespData:
""" get request and response info from Response() object. """get request and response info from Response() object."""
"""
def log_print(req_or_resp, r_type): def log_print(req_or_resp, r_type):
msg = f"\n================== {r_type} details ==================\n" msg = f"\n================== {r_type} details ==================\n"

View File

@@ -27,7 +27,8 @@ class TestHttpSession(unittest.TestCase):
self.session.request( self.session.request(
"get", "get",
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", "http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=True) allow_redirects=True,
)
address = self.session.data.address address = self.session.data.address
self.assertNotEqual(address.server_ip, "N/A") self.assertNotEqual(address.server_ip, "N/A")
self.assertEqual(address.server_port, 443) self.assertEqual(address.server_port, 443)
@@ -38,7 +39,8 @@ class TestHttpSession(unittest.TestCase):
self.session.request( self.session.request(
"get", "get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", "https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=True) allow_redirects=True,
)
address = self.session.data.address address = self.session.data.address
self.assertNotEqual(address.server_ip, "N/A") self.assertNotEqual(address.server_ip, "N/A")
self.assertEqual(address.server_port, 443) self.assertEqual(address.server_port, 443)
@@ -49,7 +51,8 @@ class TestHttpSession(unittest.TestCase):
self.session.request( self.session.request(
"get", "get",
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", "http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=False) allow_redirects=False,
)
address = self.session.data.address address = self.session.data.address
self.assertEqual(address.server_ip, "N/A") self.assertEqual(address.server_ip, "N/A")
self.assertEqual(address.server_port, 0) self.assertEqual(address.server_port, 0)
@@ -60,7 +63,8 @@ class TestHttpSession(unittest.TestCase):
self.session.request( self.session.request(
"get", "get",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", "https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=False) allow_redirects=False,
)
address = self.session.data.address address = self.session.data.address
self.assertEqual(address.server_ip, "N/A") self.assertEqual(address.server_ip, "N/A")
self.assertEqual(address.server_port, 0) self.assertEqual(address.server_port, 0)

View File

@@ -14,25 +14,12 @@ from httprunner.utils import sort_dict_by_custom_order
def convert_variables( def convert_variables(
raw_variables: Union[Dict, List, Text], test_path: Text raw_variables: Union[Dict, Text], test_path: Text
) -> Dict[Text, Any]: ) -> Dict[Text, Any]:
if isinstance(raw_variables, Dict): if isinstance(raw_variables, Dict):
return raw_variables return raw_variables
if isinstance(raw_variables, List):
# [{"var1": 1}, {"var2": 2}]
variables: Dict[Text, Any] = {}
for var_item in raw_variables:
if not isinstance(var_item, Dict) or len(var_item) != 1:
raise exceptions.TestCaseFormatError(
f"Invalid variables format: {raw_variables}"
)
variables.update(var_item)
return variables
elif isinstance(raw_variables, Text): elif isinstance(raw_variables, Text):
# get variables by function, e.g. ${get_variables()} # get variables by function, e.g. ${get_variables()}
project_meta = load_project_meta(test_path) project_meta = load_project_meta(test_path)
@@ -79,7 +66,7 @@ def _convert_jmespath(raw: Text) -> Text:
def _convert_extractors(extractors: Union[List, Dict]) -> Dict: def _convert_extractors(extractors: Union[List, Dict]) -> Dict:
""" convert extract list(v2) to dict(v3) """convert extract list(v2) to dict(v3)
Args: Args:
extractors: [{"varA": "content.varA"}, {"varB": "json.varB"}] extractors: [{"varA": "content.varA"}, {"varB": "json.varB"}]
@@ -251,8 +238,7 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
def ensure_cli_args(args: List) -> List: def ensure_cli_args(args: List) -> List:
""" ensure compatibility with deprecated cli args in v2 """ensure compatibility with deprecated cli args in v2"""
"""
# remove deprecated --failfast # remove deprecated --failfast
if "--failfast" in args: if "--failfast" in args:
logger.warning("remove deprecated argument: --failfast") logger.warning("remove deprecated argument: --failfast")
@@ -301,13 +287,13 @@ from httprunner.utils import get_platform, ExtendJSONEncoder
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def session_fixture(request): def session_fixture(request):
"""setup and teardown each task""" """setup and teardown each task"""
logger.info(f"start running testcases ...") logger.info("start running testcases ...")
start_at = time.time() start_at = time.time()
yield yield
logger.info(f"task finished, generate task summary for --save-tests") logger.info("task finished, generate task summary for --save-tests")
summary = { summary = {
"success": True, "success": True,
@@ -386,8 +372,7 @@ def session_fixture(request):
def ensure_path_sep(path: Text) -> Text: def ensure_path_sep(path: Text) -> Text:
""" ensure compatibility with different path separators of Linux and Windows """ensure compatibility with different path separators of Linux and Windows"""
"""
if "/" in path: if "/" in path:
path = os.sep.join(path.split("/")) path = os.sep.join(path.split("/"))

View File

@@ -9,11 +9,6 @@ class TestCompat(unittest.TestCase):
loader.project_meta = None loader.project_meta = None
def test_convert_variables(self): def test_convert_variables(self):
raw_variables = [{"var1": 1}, {"var2": "val2"}]
self.assertEqual(
compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml"),
{"var1": 1, "var2": "val2"},
)
raw_variables = {"var1": 1, "var2": "val2"} raw_variables = {"var1": 1, "var2": "val2"}
self.assertEqual( self.assertEqual(
compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml"), compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml"),
@@ -38,7 +33,7 @@ class TestCompat(unittest.TestCase):
compat._convert_jmespath("headers.Content-Type"), 'headers."Content-Type"' compat._convert_jmespath("headers.Content-Type"), 'headers."Content-Type"'
) )
self.assertEqual( self.assertEqual(
compat._convert_jmespath('headers.User-Agent'), 'headers."User-Agent"' compat._convert_jmespath("headers.User-Agent"), 'headers."User-Agent"'
) )
self.assertEqual( self.assertEqual(
compat._convert_jmespath('headers."Content-Type"'), 'headers."Content-Type"' compat._convert_jmespath('headers."Content-Type"'), 'headers."Content-Type"'

View File

@@ -5,7 +5,6 @@ from httprunner.models import TConfig, TConfigThrift
class ConfigThrift(object): class ConfigThrift(object):
def __init__(self, config: TConfig) -> None: def __init__(self, config: TConfig) -> None:
self.__config = config self.__config = config
self.__config.thrift = TConfigThrift() self.__config.thrift = TConfigThrift()
@@ -31,13 +30,9 @@ class ConfigThrift(object):
class Config(object): class Config(object):
def __init__(self, name: Text) -> None: def __init__(self, name: Text) -> None:
caller_frame = inspect.stack()[1] caller_frame = inspect.stack()[1]
self.__config = TConfig( self.__config = TConfig(name=name, path=caller_frame.filename)
name=name,
path=caller_frame.filename
)
@property @property
def name(self) -> Text: def name(self) -> Text:

View File

@@ -85,5 +85,4 @@ class TestcaseNotFound(NotFoundError):
class SummaryEmpty(MyBaseError): class SummaryEmpty(MyBaseError):
""" test result summary data is empty """test result summary data is empty"""
"""

View File

@@ -76,7 +76,7 @@ def ensure_upload_ready():
def prepare_upload_step(step: TStep, functions: FunctionsMapping): def prepare_upload_step(step: TStep, functions: FunctionsMapping):
""" preprocess for upload test """preprocess for upload test
replace `upload` info with MultipartEncoder replace `upload` info with MultipartEncoder
Args: Args:
@@ -102,9 +102,7 @@ def prepare_upload_step(step: TStep, functions: FunctionsMapping):
return return
# parse upload info # parse upload info
step.request.upload = parse_data( step.request.upload = parse_data(step.request.upload, step.variables, functions)
step.request.upload, step.variables, functions
)
ensure_upload_ready() ensure_upload_ready()
params_list = [] params_list = []
@@ -124,7 +122,7 @@ def prepare_upload_step(step: TStep, functions: FunctionsMapping):
def multipart_encoder(**kwargs): def multipart_encoder(**kwargs):
""" initialize MultipartEncoder with uploading fields. """initialize MultipartEncoder with uploading fields.
Returns: Returns:
MultipartEncoder: initialized MultipartEncoder object MultipartEncoder: initialized MultipartEncoder object
@@ -169,7 +167,7 @@ def multipart_encoder(**kwargs):
def multipart_content_type(m_encoder) -> Text: def multipart_content_type(m_encoder) -> Text:
""" prepare Content-Type for request headers """prepare Content-Type for request headers
Args: Args:
m_encoder: MultipartEncoder object m_encoder: MultipartEncoder object

View File

@@ -11,14 +11,13 @@ from loguru import logger
from pydantic import ValidationError from pydantic import ValidationError
from httprunner import builtin, exceptions, utils from httprunner import builtin, exceptions, utils
from httprunner.models import ProjectMeta, TestCase, TestSuite from httprunner.models import ProjectMeta, TestCase
project_meta: Union[ProjectMeta, None] = None project_meta: Union[ProjectMeta, None] = None
def _load_yaml_file(yaml_file: Text) -> Dict: def _load_yaml_file(yaml_file: Text) -> Dict:
""" load yaml file and check file content format """load yaml file and check file content format"""
"""
with open(yaml_file, mode="rb") as stream: with open(yaml_file, mode="rb") as stream:
try: try:
yaml_content = yaml.load(stream, Loader=yaml.FullLoader) yaml_content = yaml.load(stream, Loader=yaml.FullLoader)
@@ -31,8 +30,7 @@ def _load_yaml_file(yaml_file: Text) -> Dict:
def _load_json_file(json_file: Text) -> Dict: def _load_json_file(json_file: Text) -> Dict:
""" load json file and check file content format """load json file and check file content format"""
"""
with open(json_file, mode="rb") as data_file: with open(json_file, mode="rb") as data_file:
try: try:
json_content = json.load(data_file) json_content = json.load(data_file)
@@ -81,20 +79,8 @@ def load_testcase_file(testcase_file: Text) -> TestCase:
return testcase_obj return testcase_obj
def load_testsuite(testsuite: Dict) -> TestSuite:
path = testsuite["config"]["path"]
try:
# validate with pydantic TestCase model
testsuite_obj = TestSuite.parse_obj(testsuite)
except ValidationError as ex:
err_msg = f"TestSuite ValidationError:\nfile: {path}\nerror: {ex}"
raise exceptions.TestSuiteFormatError(err_msg)
return testsuite_obj
def load_dot_env_file(dot_env_path: Text) -> Dict: def load_dot_env_file(dot_env_path: Text) -> Dict:
""" load .env file. """load .env file.
Args: Args:
dot_env_path (str): .env file path dot_env_path (str): .env file path
@@ -140,7 +126,7 @@ def load_dot_env_file(dot_env_path: Text) -> Dict:
def load_csv_file(csv_file: Text) -> List[Dict]: def load_csv_file(csv_file: Text) -> List[Dict]:
""" load csv file and check file content format """load csv file and check file content format
Args: Args:
csv_file (str): csv file path, csv file content is like below: csv_file (str): csv file path, csv file content is like below:
@@ -186,7 +172,7 @@ def load_csv_file(csv_file: Text) -> List[Dict]:
def load_folder_files(folder_path: Text, recursive: bool = True) -> List: def load_folder_files(folder_path: Text, recursive: bool = True) -> List:
""" load folder path, return all files endswith .yml/.yaml/.json/_test.py in list. """load folder path, return all files endswith .yml/.yaml/.json/_test.py in list.
Args: Args:
folder_path (str): specified folder path to load folder_path (str): specified folder path to load
@@ -227,7 +213,7 @@ def load_folder_files(folder_path: Text, recursive: bool = True) -> List:
def load_module_functions(module) -> Dict[Text, Callable]: def load_module_functions(module) -> Dict[Text, Callable]:
""" load python module functions. """load python module functions.
Args: Args:
module: python module module: python module
@@ -251,13 +237,12 @@ def load_module_functions(module) -> Dict[Text, Callable]:
def load_builtin_functions() -> Dict[Text, Callable]: def load_builtin_functions() -> Dict[Text, Callable]:
""" load builtin module functions """load builtin module functions"""
"""
return load_module_functions(builtin) return load_module_functions(builtin)
def locate_file(start_path: Text, file_name: Text) -> Text: def locate_file(start_path: Text, file_name: Text) -> Text:
""" locate filename and return absolute file path. """locate filename and return absolute file path.
searching will be recursive upward until system root dir. searching will be recursive upward until system root dir.
Args: Args:
@@ -295,7 +280,7 @@ def locate_file(start_path: Text, file_name: Text) -> Text:
def locate_debugtalk_py(start_path: Text) -> Text: def locate_debugtalk_py(start_path: Text) -> Text:
""" locate debugtalk.py file """locate debugtalk.py file
Args: Args:
start_path (str): start locating path, start_path (str): start locating path,
@@ -315,7 +300,7 @@ def locate_debugtalk_py(start_path: Text) -> Text:
def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]: def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]:
""" locate debugtalk.py path as project root directory """locate debugtalk.py path as project root directory
Args: Args:
test_path: specified testfile path test_path: specified testfile path
@@ -352,7 +337,7 @@ def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]:
def load_debugtalk_functions() -> Dict[Text, Callable]: def load_debugtalk_functions() -> Dict[Text, Callable]:
""" load project debugtalk.py module functions """load project debugtalk.py module functions
debugtalk.py should be located in project root directory. debugtalk.py should be located in project root directory.
Returns: Returns:
@@ -376,7 +361,7 @@ def load_debugtalk_functions() -> Dict[Text, Callable]:
def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta: def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
""" load testcases, .env, debugtalk.py functions. """load testcases, .env, debugtalk.py functions.
testcases folder is relative to project_root_directory testcases folder is relative to project_root_directory
by default, project_meta will be loaded only once, unless set reload to true. by default, project_meta will be loaded only once, unless set reload to true.
@@ -428,7 +413,7 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
def convert_relative_project_root_dir(abs_path: Text) -> Text: def convert_relative_project_root_dir(abs_path: Text) -> Text:
""" convert absolute path to relative path, based on project_meta.RootDir """convert absolute path to relative path, based on project_meta.RootDir
Args: Args:
abs_path: absolute path abs_path: absolute path
@@ -444,4 +429,4 @@ def convert_relative_project_root_dir(abs_path: Text) -> Text:
f"project_meta.RootDir: {_project_meta.RootDir}" f"project_meta.RootDir: {_project_meta.RootDir}"
) )
return abs_path[len(_project_meta.RootDir) + 1:] return abs_path[len(_project_meta.RootDir) + 1 :]

View File

@@ -97,7 +97,11 @@ class TestLoader(unittest.TestCase):
) )
def test_load_env_path_not_exist(self): def test_load_env_path_not_exist(self):
dot_env_path = os.path.join(os.getcwd(), "tests", "data",) dot_env_path = os.path.join(
os.getcwd(),
"tests",
"data",
)
env_variables_mapping = loader.load_dot_env_file(dot_env_path) env_variables_mapping = loader.load_dot_env_file(dot_env_path)
self.assertEqual(env_variables_mapping, {}) self.assertEqual(env_variables_mapping, {})

View File

@@ -6,17 +6,23 @@ from typing import Dict, List, Set, Text, Tuple
import jinja2 import jinja2
from loguru import logger from loguru import logger
from sentry_sdk import capture_exception
from httprunner import __version__, exceptions from httprunner import __version__, exceptions
from httprunner.compat import (convert_variables, ensure_path_sep, from httprunner.compat import (
ensure_testcase_v3, ensure_testcase_v3_api) convert_variables,
from httprunner.loader import (convert_relative_project_root_dir, ensure_path_sep,
load_folder_files, load_project_meta, ensure_testcase_v3,
load_test_file, load_testcase, load_testsuite) ensure_testcase_v3_api,
)
from httprunner.loader import (
convert_relative_project_root_dir,
load_folder_files,
load_project_meta,
load_test_file,
load_testcase,
)
from httprunner.response import uniform_validator from httprunner.response import uniform_validator
from httprunner.utils import (ga_client, is_support_multiprocessing, from httprunner.utils import ga_client, is_support_multiprocessing
merge_variables)
""" cache converted pytest files, avoid duplicate making """ cache converted pytest files, avoid duplicate making
""" """
@@ -72,10 +78,10 @@ if __name__ == "__main__":
def __ensure_absolute(path: Text) -> Text: def __ensure_absolute(path: Text) -> Text:
if path.startswith("./"): if path.startswith("./"):
# Linux/Darwin, hrun ./test.yml # Linux/Darwin, hrun ./test.yml
path = path[len("./"):] path = path[2:]
elif path.startswith(".\\"): elif path.startswith(".\\"):
# Windows, hrun .\\test.yml # Windows, hrun .\\test.yml
path = path[len(".\\"):] path = path[3:]
path = ensure_path_sep(path) path = ensure_path_sep(path)
project_meta = load_project_meta(path) project_meta = load_project_meta(path)
@@ -93,7 +99,7 @@ def __ensure_absolute(path: Text) -> Text:
def ensure_file_abs_path_valid(file_abs_path: Text) -> Text: def ensure_file_abs_path_valid(file_abs_path: Text) -> Text:
""" ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space """ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space
Args: Args:
file_abs_path: absolute file path file_abs_path: absolute file path
@@ -134,8 +140,7 @@ def ensure_file_abs_path_valid(file_abs_path: Text) -> Text:
def __ensure_testcase_module(path: Text): def __ensure_testcase_module(path: Text):
""" ensure pytest files are in python module, generate __init__.py on demand """ensure pytest files are in python module, generate __init__.py on demand"""
"""
init_file = os.path.join(os.path.dirname(path), "__init__.py") init_file = os.path.join(os.path.dirname(path), "__init__.py")
if os.path.isfile(init_file): if os.path.isfile(init_file):
return return
@@ -169,7 +174,6 @@ def format_pytest_with_black(*python_paths: Text):
) )
[subprocess.run(["black", path]) for path in python_paths] [subprocess.run(["black", path]) for path in python_paths]
except subprocess.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
capture_exception(ex)
logger.error(ex) logger.error(ex)
sys.exit(1) sys.exit(1)
except OSError: except OSError:
@@ -433,61 +437,8 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
return testcase_python_abs_path return testcase_python_abs_path
def make_testsuite(testsuite: Dict):
"""convert valid testsuite dict to pytest folder with testcases"""
# validate testsuite format
load_testsuite(testsuite)
testsuite_config = testsuite["config"]
testsuite_path = testsuite_config["path"]
testsuite_variables = convert_variables(
testsuite_config.get("variables", {}), testsuite_path
)
logger.info(f"start to make testsuite: {testsuite_path}")
# create directory with testsuite file name, put its testcases under this directory
testsuite_path = ensure_file_abs_path_valid(testsuite_path)
testsuite_dir, file_suffix = os.path.splitext(testsuite_path)
# demo_testsuite.yml => demo_testsuite_yml
testsuite_dir = f"{testsuite_dir}_{file_suffix.lstrip('.')}"
for testcase in testsuite["testcases"]:
# get referenced testcase content
testcase_file = testcase["testcase"]
testcase_path = __ensure_absolute(testcase_file)
testcase_dict = load_test_file(testcase_path)
testcase_dict.setdefault("config", {})
testcase_dict["config"]["path"] = testcase_path
# override testcase name
testcase_dict["config"]["name"] = testcase["name"]
# override base_url
base_url = testsuite_config.get("base_url") or testcase.get("base_url")
if base_url:
testcase_dict["config"]["base_url"] = base_url
# override verify
if "verify" in testsuite_config:
testcase_dict["config"]["verify"] = testsuite_config["verify"]
# override variables
# testsuite testcase variables > testsuite config variables
testcase_variables = convert_variables(
testcase.get("variables", {}), testcase_path
)
testcase_variables = merge_variables(testcase_variables, testsuite_variables)
# testsuite testcase variables > testcase config variables
testcase_dict["config"]["variables"] = convert_variables(
testcase_dict["config"].get("variables", {}), testcase_path
)
testcase_dict["config"]["variables"].update(testcase_variables)
# make testcase
testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
pytest_files_run_set.add(testcase_pytest_path)
def __make(tests_path: Text): def __make(tests_path: Text):
""" make testcase(s) with testcase/testsuite/folder absolute path """make testcase(s) with testcase/folder absolute path
generated pytest file path will be cached in pytest_files_made_cache_mapping generated pytest file path will be cached in pytest_files_made_cache_mapping
Args: Args:
@@ -528,13 +479,12 @@ def __make(tests_path: Text):
if "config" not in test_content: if "config" not in test_content:
logger.warning( logger.warning(
f"Invalid testcase/testsuite file: {test_file}\n" f"Invalid testcase file: {test_file}\nreason: missing config part."
f"reason: missing config part."
) )
continue continue
elif not isinstance(test_content["config"], Dict): elif not isinstance(test_content["config"], Dict):
logger.warning( logger.warning(
f"Invalid testcase/testsuite file: {test_file}\n" f"Invalid testcase file: {test_file}\n"
f"reason: config should be dict type, got {test_content['config']}" f"reason: config should be dict type, got {test_content['config']}"
) )
continue continue
@@ -542,33 +492,19 @@ def __make(tests_path: Text):
# ensure path absolute # ensure path absolute
test_content.setdefault("config", {})["path"] = test_file test_content.setdefault("config", {})["path"] = test_file
# testcase
if "teststeps" in test_content:
try:
testcase_pytest_path = make_testcase(test_content)
pytest_files_run_set.add(testcase_pytest_path)
except exceptions.TestCaseFormatError as ex:
logger.warning(
f"Invalid testcase file: {test_file}\n{type(ex).__name__}: {ex}"
)
continue
# testsuite
elif "testcases" in test_content:
try:
make_testsuite(test_content)
except exceptions.TestSuiteFormatError as ex:
logger.warning(
f"Invalid testsuite file: {test_file}\n{type(ex).__name__}: {ex}"
)
continue
# invalid format # invalid format
else: if "teststeps" not in test_content:
logger.warning(f"Invalid testcase file: {test_file}")
# testcase
try:
testcase_pytest_path = make_testcase(test_content)
pytest_files_run_set.add(testcase_pytest_path)
except exceptions.TestCaseFormatError as ex:
logger.warning( logger.warning(
f"Invalid test file: {test_file}\n" f"Invalid testcase file: {test_file}\n{type(ex).__name__}: {ex}"
f"reason: file content is neither testcase nor testsuite"
) )
continue
def main_make(tests_paths: List[Text]) -> List[Text]: def main_make(tests_paths: List[Text]) -> List[Text]:
@@ -596,10 +532,10 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
def init_make_parser(subparsers): def init_make_parser(subparsers):
""" make testcases: parse command line options and run commands. """make testcases: parse command line options and run commands."""
"""
parser = subparsers.add_parser( parser = subparsers.add_parser(
"make", help="Convert YAML/JSON testcases to pytest cases.", "make",
help="Convert YAML/JSON testcases to pytest cases.",
) )
parser.add_argument( parser.add_argument(
"testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path" "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path"

View File

@@ -73,7 +73,8 @@ from request_methods.request_with_functions_test import (
content, content,
) )
self.assertIn( self.assertIn(
".call(RequestWithFunctions)", content, ".call(RequestWithFunctions)",
content,
) )
def test_make_testcase_folder(self): def test_make_testcase_folder(self):
@@ -94,9 +95,7 @@ from request_methods.request_with_functions_test import (
def test_ensure_file_path_valid(self): def test_ensure_file_path_valid(self):
self.assertEqual( self.assertEqual(
ensure_file_abs_path_valid( ensure_file_abs_path_valid(os.path.join(self.data_dir, "a-b.c", "2 3.yml")),
os.path.join(self.data_dir, "a-b.c", "2 3.yml")
),
os.path.join(self.data_dir, "a_b_c", "T2_3.yml"), os.path.join(self.data_dir, "a_b_c", "T2_3.yml"),
) )
loader.project_meta = None loader.project_meta = None
@@ -113,67 +112,31 @@ from request_methods.request_with_functions_test import (
) )
loader.project_meta = None loader.project_meta = None
self.assertEqual( self.assertEqual(
ensure_file_abs_path_valid(os.getcwd()), os.getcwd(), ensure_file_abs_path_valid(os.getcwd()),
os.getcwd(),
) )
loader.project_meta = None loader.project_meta = None
self.assertEqual( self.assertEqual(
ensure_file_abs_path_valid( ensure_file_abs_path_valid(os.path.join(self.data_dir, ".csv")),
os.path.join(self.data_dir, ".csv")
),
os.path.join(self.data_dir, ".csv"), os.path.join(self.data_dir, ".csv"),
) )
def test_convert_testcase_path(self): def test_convert_testcase_path(self):
self.assertEqual( self.assertEqual(
convert_testcase_path( convert_testcase_path(os.path.join(self.data_dir, "a-b.c", "2 3.yml")),
os.path.join(self.data_dir, "a-b.c", "2 3.yml")
),
( (
os.path.join(self.data_dir, "a_b_c", "T2_3_test.py"), os.path.join(self.data_dir, "a_b_c", "T2_3_test.py"),
"T23", "T23",
), ),
) )
self.assertEqual( self.assertEqual(
convert_testcase_path( convert_testcase_path(os.path.join(self.data_dir, "a-b.c", "中文case.yml")),
os.path.join(self.data_dir, "a-b.c", "中文case.yml")
),
( (
os.path.join(self.data_dir, "a_b_c", "中文case_test.py"), os.path.join(self.data_dir, "a_b_c", "中文case_test.py"),
"中文Case", "中文Case",
), ),
) )
def test_make_testsuite(self):
path = ["examples/postman_echo/request_methods/demo_testsuite.yml"]
testcase_python_list = main_make(path)
self.assertEqual(len(testcase_python_list), 2)
self.assertIn(
os.path.join(
os.getcwd(),
os.path.join(
"examples",
"postman_echo",
"request_methods",
"demo_testsuite_yml",
"request_with_functions_test.py",
),
),
testcase_python_list,
)
self.assertIn(
os.path.join(
os.getcwd(),
os.path.join(
"examples",
"postman_echo",
"request_methods",
"demo_testsuite_yml",
"request_with_testcase_reference_test.py",
),
),
testcase_python_list,
)
def test_make_config_chain_style(self): def test_make_config_chain_style(self):
config = { config = {
"name": "request methods testcase: validate with functions", "name": "request methods testcase: validate with functions",
@@ -190,7 +153,11 @@ from request_methods.request_with_functions_test import (
def test_make_teststep_chain_style(self): def test_make_teststep_chain_style(self):
step = { step = {
"name": "get with params", "name": "get with params",
"variables": {"foo1": "bar1", "foo2": 123, "sum_v": "${sum_two(1, 2)}",}, "variables": {
"foo1": "bar1",
"foo2": 123,
"sum_v": "${sum_two(1, 2)}",
},
"request": { "request": {
"method": "GET", "method": "GET",
"url": "/get", "url": "/get",

View File

@@ -97,7 +97,9 @@ class ProjectMeta(BaseModel):
dot_env_path: Text = "" # .env file path dot_env_path: Text = "" # .env file path
functions: FunctionsMapping = {} # functions defined in debugtalk.py functions: FunctionsMapping = {} # functions defined in debugtalk.py
env: Env = {} env: Env = {}
RootDir: Text = os.getcwd() # project root directory (ensure absolute), the path debugtalk.py located RootDir: Text = (
os.getcwd()
) # project root directory (ensure absolute), the path debugtalk.py located
class TestsMapping(BaseModel): class TestsMapping(BaseModel):
@@ -166,21 +168,20 @@ class SessionData(BaseModel):
class StepResult(BaseModel): class StepResult(BaseModel):
"""teststep data, each step maybe corresponding to one request or one testcase""" """teststep data, each step maybe corresponding to one request or one testcase"""
name: Text = "" # teststep name name: Text = "" # teststep name
step_type: Text = "" # teststep type, request or testcase step_type: Text = "" # teststep type, request or testcase
success: bool = False success: bool = False
data: Union[SessionData, List['StepResult']] = None data: Union[SessionData, List["StepResult"]] = None
elapsed: float = 0.0 # teststep elapsed time elapsed: float = 0.0 # teststep elapsed time
content_size: float = 0 # response content size content_size: float = 0 # response content size
export_vars: VariablesMapping = {} export_vars: VariablesMapping = {}
attachment: Text = "" # teststep attachment attachment: Text = "" # teststep attachment
StepResult.update_forward_refs() StepResult.update_forward_refs()
class IStep(object): class IStep(object):
def name(self) -> str: def name(self) -> str:
raise NotImplementedError raise NotImplementedError
@@ -211,18 +212,6 @@ class PlatformInfo(BaseModel):
platform: Text platform: Text
class TestCaseRef(BaseModel):
name: Text
base_url: Text = ""
testcase: Text
variables: VariablesMapping = {}
class TestSuite(BaseModel):
config: TConfig
testcases: List[TestCaseRef]
class Stat(BaseModel): class Stat(BaseModel):
total: int = 0 total: int = 0
success: int = 0 success: int = 0

View File

@@ -6,7 +6,6 @@ from typing import Any, Callable, Dict, List, Set, Text
from urllib.parse import urlparse from urllib.parse import urlparse
from loguru import logger from loguru import logger
from sentry_sdk import capture_exception
from httprunner import exceptions, loader, utils from httprunner import exceptions, loader, utils
from httprunner.models import FunctionsMapping, VariablesMapping from httprunner.models import FunctionsMapping, VariablesMapping
@@ -21,7 +20,7 @@ function_regex_compile = re.compile(r"\$\{([a-zA-Z_]\w*)\(([\$\w\.\-/\s=,]*)\)\}
def parse_string_value(str_value: Text) -> Any: def parse_string_value(str_value: Text) -> Any:
""" parse string to number if possible """parse string to number if possible
e.g. "123" => 123 e.g. "123" => 123
"12.2" => 12.3 "12.2" => 12.3
"abc" => "abc" "abc" => "abc"
@@ -37,7 +36,7 @@ def parse_string_value(str_value: Text) -> Any:
def build_url(base_url, step_url): def build_url(base_url, step_url):
""" prepend url with base_url unless it's already an absolute URL """ """prepend url with base_url unless it's already an absolute URL"""
o_step_url = urlparse(step_url) o_step_url = urlparse(step_url)
if o_step_url.netloc != "": if o_step_url.netloc != "":
# step url is absolute url # step url is absolute url
@@ -50,14 +49,16 @@ def build_url(base_url, step_url):
raise exceptions.ParamsError("base url missed!") raise exceptions.ParamsError("base url missed!")
path = o_base_url.path.rstrip("/") + "/" + o_step_url.path.lstrip("/") path = o_base_url.path.rstrip("/") + "/" + o_step_url.path.lstrip("/")
o_step_url = o_step_url._replace(scheme=o_base_url.scheme) \ o_step_url = (
._replace(netloc=o_base_url.netloc) \ o_step_url._replace(scheme=o_base_url.scheme)
._replace(netloc=o_base_url.netloc)
._replace(path=path) ._replace(path=path)
)
return o_step_url.geturl() return o_step_url.geturl()
def regex_findall_variables(raw_string: Text) -> List[Text]: def regex_findall_variables(raw_string: Text) -> List[Text]:
""" extract all variable names from content, which is in format $variable """extract all variable names from content, which is in format $variable
Args: Args:
raw_string (str): string content raw_string (str): string content
@@ -116,7 +117,7 @@ def regex_findall_variables(raw_string: Text) -> List[Text]:
def regex_findall_functions(content: Text) -> List[Text]: def regex_findall_functions(content: Text) -> List[Text]:
""" extract all functions from string content, which are in format ${fun()} """extract all functions from string content, which are in format ${fun()}
Args: Args:
content (str): string content content (str): string content
@@ -144,13 +145,12 @@ def regex_findall_functions(content: Text) -> List[Text]:
try: try:
return function_regex_compile.findall(content) return function_regex_compile.findall(content)
except TypeError as ex: except TypeError as ex:
capture_exception(ex) logger.error(f"regex findall functions error: {ex}")
return [] return []
def extract_variables(content: Any) -> Set: def extract_variables(content: Any) -> Set:
""" extract all variables in content recursively. """extract all variables in content recursively."""
"""
if isinstance(content, (list, set, tuple)): if isinstance(content, (list, set, tuple)):
variables = set() variables = set()
for item in content: for item in content:
@@ -170,7 +170,7 @@ def extract_variables(content: Any) -> Set:
def parse_function_params(params: Text) -> Dict: def parse_function_params(params: Text) -> Dict:
""" parse function params to args and kwargs. """parse function params to args and kwargs.
Args: Args:
params (str): function param in string params (str): function param in string
@@ -221,7 +221,7 @@ def parse_function_params(params: Text) -> Dict:
def get_mapping_variable( def get_mapping_variable(
variable_name: Text, variables_mapping: VariablesMapping variable_name: Text, variables_mapping: VariablesMapping
) -> Any: ) -> Any:
""" get variable from variables_mapping. """get variable from variables_mapping.
Args: Args:
variable_name (str): variable name variable_name (str): variable name
@@ -246,7 +246,7 @@ def get_mapping_variable(
def get_mapping_function( def get_mapping_function(
function_name: Text, functions_mapping: FunctionsMapping function_name: Text, functions_mapping: FunctionsMapping
) -> Callable: ) -> Callable:
""" get function from functions_mapping, """get function from functions_mapping,
if not found, then try to check if builtin function. if not found, then try to check if builtin function.
Args: Args:
@@ -296,7 +296,7 @@ def parse_string(
variables_mapping: VariablesMapping, variables_mapping: VariablesMapping,
functions_mapping: FunctionsMapping, functions_mapping: FunctionsMapping,
) -> Any: ) -> Any:
""" parse string content with variables and functions mapping. """parse string content with variables and functions mapping.
Args: Args:
raw_string: raw string content to be parsed. raw_string: raw string content to be parsed.
@@ -403,8 +403,8 @@ def parse_data(
variables_mapping: VariablesMapping = None, variables_mapping: VariablesMapping = None,
functions_mapping: FunctionsMapping = None, functions_mapping: FunctionsMapping = None,
) -> Any: ) -> Any:
""" parse raw data with evaluated variables mapping. """parse raw data with evaluated variables mapping.
Notice: variables_mapping should not contain any variable or function. Notice: variables_mapping should not contain any variable or function.
""" """
if isinstance(raw_data, str): if isinstance(raw_data, str):
# content in string format may contains variables and functions # content in string format may contains variables and functions
@@ -476,8 +476,10 @@ def parse_variables_mapping(
return parsed_variables return parsed_variables
def parse_parameters(parameters: Dict,) -> List[Dict]: def parse_parameters(
""" parse parameters and generate cartesian product. parameters: Dict,
) -> List[Dict]:
"""parse parameters and generate cartesian product.
Args: Args:
parameters (Dict) parameters: parameter name and value mapping parameters (Dict) parameters: parameter name and value mapping
@@ -584,17 +586,20 @@ def parse_parameters(parameters: Dict,) -> List[Dict]:
class Parser(object): class Parser(object):
def __init__(self, functions_mapping: FunctionsMapping = None) -> None: def __init__(self, functions_mapping: FunctionsMapping = None) -> None:
self.functions_mapping = functions_mapping self.functions_mapping = functions_mapping
def parse_string(self, raw_string: Text, variables_mapping: VariablesMapping) -> Any: def parse_string(
self, raw_string: Text, variables_mapping: VariablesMapping
) -> Any:
return parse_string(raw_string, variables_mapping, self.functions_mapping) return parse_string(raw_string, variables_mapping, self.functions_mapping)
def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping: def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping:
return parse_variables_mapping(variables_mapping, self.functions_mapping) return parse_variables_mapping(variables_mapping, self.functions_mapping)
def parse_data(self, raw_data: Any, variables_mapping: VariablesMapping = None) -> Any: def parse_data(
self, raw_data: Any, variables_mapping: VariablesMapping = None
) -> Any:
return parse_data(raw_data, variables_mapping, self.functions_mapping) return parse_data(raw_data, variables_mapping, self.functions_mapping)
def get_mapping_function(self, func_name: Text) -> Callable: def get_mapping_function(self, func_name: Text) -> Callable:

View File

@@ -8,7 +8,6 @@ from httprunner.loader import load_project_meta
class TestParserBasic(unittest.TestCase): class TestParserBasic(unittest.TestCase):
def test_build_url(self): def test_build_url(self):
url = parser.build_url("https://postman-echo.com", "/get") url = parser.build_url("https://postman-echo.com", "/get")
self.assertEqual(url, "https://postman-echo.com/get") self.assertEqual(url, "https://postman-echo.com/get")

View File

@@ -12,8 +12,7 @@ from httprunner.parser import Parser, parse_string_value
def get_uniform_comparator(comparator: Text): def get_uniform_comparator(comparator: Text):
""" convert comparator alias to uniform name """convert comparator alias to uniform name"""
"""
if comparator in ["eq", "equals", "equal"]: if comparator in ["eq", "equals", "equal"]:
return "equal" return "equal"
elif comparator in ["lt", "less_than"]: elif comparator in ["lt", "less_than"]:
@@ -52,7 +51,7 @@ def get_uniform_comparator(comparator: Text):
def uniform_validator(validator): def uniform_validator(validator):
""" unify validator """unify validator
Args: Args:
validator (dict): validator maybe in two formats: validator (dict): validator maybe in two formats:
@@ -116,7 +115,7 @@ def uniform_validator(validator):
class ResponseObject(object): class ResponseObject(object):
def __init__(self, resp_obj: requests.Response, parser: Parser): def __init__(self, resp_obj: requests.Response, parser: Parser):
""" initialize with a requests.Response object """initialize with a requests.Response object
Args: Args:
resp_obj (instance): requests.Response instance resp_obj (instance): requests.Response instance
@@ -168,20 +167,19 @@ class ResponseObject(object):
return check_value return check_value
def extract(self, def extract(
extractors: Dict[Text, Text], self,
variables_mapping: VariablesMapping = None, extractors: Dict[Text, Text],
) -> Dict[Text, Any]: variables_mapping: VariablesMapping = None,
) -> Dict[Text, Any]:
if not extractors: if not extractors:
return {} return {}
extract_mapping = {} extract_mapping = {}
for key, field in extractors.items(): for key, field in extractors.items():
if '$' in field: if "$" in field:
# field contains variable or function # field contains variable or function
field = self.parser.parse_data( field = self.parser.parse_data(field, variables_mapping)
field, variables_mapping
)
field_value = self._search_jmespath(field) field_value = self._search_jmespath(field)
extract_mapping[key] = field_value extract_mapping[key] = field_value
@@ -214,9 +212,7 @@ class ResponseObject(object):
check_item = u_validator["check"] check_item = u_validator["check"]
if "$" in check_item: if "$" in check_item:
# check_item is variable or function # check_item is variable or function
check_item = self.parser.parse_data( check_item = self.parser.parse_data(check_item, variables_mapping)
check_item, variables_mapping
)
check_item = parse_string_value(check_item) check_item = parse_string_value(check_item)
if check_item and isinstance(check_item, Text): if check_item and isinstance(check_item, Text):

View File

@@ -19,16 +19,13 @@ class TestResponse(unittest.TestCase):
] ]
}, },
) )
parser = Parser(functions_mapping={ parser = Parser(
'get_name': lambda: 'name', functions_mapping={"get_name": lambda: "name", "get_num": lambda x: x}
"get_num": lambda x: x )
})
self.resp_obj = ResponseObject(resp, parser) self.resp_obj = ResponseObject(resp, parser)
def test_extract(self): def test_extract(self):
variables_mapping = { variables_mapping = {"body": "body"}
'body': 'body'
}
extract_mapping = self.resp_obj.extract( extract_mapping = self.resp_obj.extract(
{ {
"var_1": "body.json.locations[0]", "var_1": "body.json.locations[0]",
@@ -64,6 +61,9 @@ class TestResponse(unittest.TestCase):
def test_validate_functions(self): def test_validate_functions(self):
variables_mapping = {"index": 1} variables_mapping = {"index": 1}
self.resp_obj.validate( self.resp_obj.validate(
[{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},], [
{"eq": ["${get_num(0)}", 0]},
{"eq": ["${get_num($index)}", 1]},
],
variables_mapping=variables_mapping, variables_mapping=variables_mapping,
) )

View File

@@ -12,14 +12,22 @@ except ModuleNotFoundError:
USE_ALLURE = False USE_ALLURE = False
from loguru import logger from loguru import logger
from httprunner.client import HttpSession from httprunner.client import HttpSession
from httprunner.config import Config from httprunner.config import Config
from httprunner.exceptions import ParamsError, ValidationFailure from httprunner.exceptions import ParamsError, ValidationFailure
from httprunner.loader import load_project_meta from httprunner.loader import load_project_meta
from httprunner.models import (ProjectMeta, StepResult, TConfig, TestCaseInOut, from httprunner.models import (
TestCaseSummary, TestCaseTime, VariablesMapping) ProjectMeta,
StepResult,
TConfig,
TestCaseInOut,
TestCaseSummary,
TestCaseTime,
VariablesMapping,
)
from httprunner.parser import Parser from httprunner.parser import Parser
from httprunner.utils import merge_variables from httprunner.utils import LOGGER_FORMAT, init_logger, merge_variables
class SessionRunner(object): class SessionRunner(object):
@@ -43,6 +51,7 @@ class SessionRunner(object):
__log_path: Text = "" __log_path: Text = ""
def __init(self): def __init(self):
init_logger()
self.__config = self.config.struct() self.__config = self.config.struct()
self.__session_variables = {} self.__session_variables = {}
self.__start_at = 0 self.__start_at = 0
@@ -53,9 +62,7 @@ class SessionRunner(object):
) )
self.case_id = self.case_id or str(uuid.uuid4()) self.case_id = self.case_id or str(uuid.uuid4())
self.root_dir = self.root_dir or self.__project_meta.RootDir self.root_dir = self.root_dir or self.__project_meta.RootDir
self.__log_path = os.path.join( self.__log_path = os.path.join(self.root_dir, "logs", f"{self.case_id}.run.log")
self.root_dir, "logs", f"{self.case_id}.run.log"
)
self.__step_results.clear() self.__step_results.clear()
self.session = self.session or HttpSession() self.session = self.session or HttpSession()
@@ -85,9 +92,7 @@ class SessionRunner(object):
self.__config.variables.update(self.__session_variables) self.__config.variables.update(self.__session_variables)
if param: if param:
self.__config.variables.update(param) self.__config.variables.update(param)
self.__config.variables = self.parser.parse_variables( self.__config.variables = self.parser.parse_variables(self.__config.variables)
self.__config.variables
)
# parse config name # parse config name
self.__config.name = self.parser.parse_data( self.__config.name = self.parser.parse_data(
@@ -174,10 +179,12 @@ class SessionRunner(object):
raise raise
else: else:
logger.warning( logger.warning(
f"run step {step.name()} validation failed,wait {step.retry_interval} sec and try again") f"run step {step.name()} validation failed,wait {step.retry_interval} sec and try again"
)
time.sleep(step.retry_interval) time.sleep(step.retry_interval)
logger.info( logger.info(
f"run step retry ({i+1}/{step.retry_times} time): {step.name()} >>>>>>") f"run step retry ({i+1}/{step.retry_times} time): {step.name()} >>>>>>"
)
# save extracted variables to session variables # save extracted variables to session variables
self.__session_variables.update(step_result.export_vars) self.__session_variables.update(step_result.export_vars)
@@ -188,6 +195,7 @@ class SessionRunner(object):
def test_start(self, param: Dict = None) -> "SessionRunner": def test_start(self, param: Dict = None) -> "SessionRunner":
"""main entrance, discovered by pytest""" """main entrance, discovered by pytest"""
print("\n")
self.__init() self.__init()
self.__parse_config(param) self.__parse_config(param)
@@ -200,14 +208,13 @@ class SessionRunner(object):
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}" f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}"
) )
log_handler = logger.add(self.__log_path, level="DEBUG") logger.add(self.__log_path, format=LOGGER_FORMAT, level="DEBUG")
self.__start_at = time.time() self.__start_at = time.time()
try: try:
# run step in sequential order # run step in sequential order
for step in self.teststeps: for step in self.teststeps:
self.__run_step(step) self.__run_step(step)
finally: finally:
logger.remove(log_handler)
logger.info(f"generate testcase log: {self.__log_path}") logger.info(f"generate testcase log: {self.__log_path}")
self.__duration = time.time() - self.__start_at self.__duration = time.time() - self.__start_at

View File

@@ -2,12 +2,15 @@ from typing import Union
from httprunner.models import StepResult, TRequest, TStep, TestCase from httprunner.models import StepResult, TRequest, TStep, TestCase
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner
from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation from httprunner.step_request import (
RequestWithOptionalArgs,
StepRequestExtraction,
StepRequestValidation,
)
from httprunner.step_testcase import StepRefCase from httprunner.step_testcase import StepRefCase
class Step(object): class Step(object):
def __init__( def __init__(
self, self,
step: Union[ step: Union[

View File

@@ -6,15 +6,24 @@ from loguru import logger
from httprunner import utils from httprunner import utils
from httprunner.exceptions import ValidationFailure from httprunner.exceptions import ValidationFailure
from httprunner.ext.uploader import prepare_upload_step from httprunner.ext.uploader import prepare_upload_step
from httprunner.models import (Hooks, IStep, MethodEnum, StepResult, TRequest, from httprunner.models import (
TStep, VariablesMapping) Hooks,
IStep,
MethodEnum,
StepResult,
TRequest,
TStep,
VariablesMapping,
)
from httprunner.parser import build_url from httprunner.parser import build_url
from httprunner.response import ResponseObject from httprunner.response import ResponseObject
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner
def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text): def call_hooks(
""" call hook actions. runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text
):
"""call hook actions.
Args: Args:
hooks (list): each hook in hooks list maybe in two format. hooks (list): each hook in hooks list maybe in two format.
@@ -46,9 +55,7 @@ def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMappin
elif isinstance(hook, Dict) and len(hook) == 1: elif isinstance(hook, Dict) and len(hook) == 1:
# format 2: {"var": "${func()}"} # format 2: {"var": "${func()}"}
var_name, hook_content = list(hook.items())[0] var_name, hook_content = list(hook.items())[0]
hook_content_eval = runner.parser.parse_data( hook_content_eval = runner.parser.parse_data(hook_content, step_variables)
hook_content, step_variables
)
logger.debug( logger.debug(
f"call hook function: {hook_content}, got value: {hook_content_eval}" f"call hook function: {hook_content}, got value: {hook_content_eval}"
) )
@@ -73,9 +80,7 @@ def run_step_request(runner: HttpRunner, step: TStep) -> StepResult:
prepare_upload_step(step, functions) prepare_upload_step(step, functions)
request_dict = step.request.dict() request_dict = step.request.dict()
request_dict.pop("upload", None) request_dict.pop("upload", None)
parsed_request_dict = runner.parser.parse_data( parsed_request_dict = runner.parser.parse_data(request_dict, step.variables)
request_dict, step.variables
)
parsed_request_dict["headers"].setdefault( parsed_request_dict["headers"].setdefault(
"HRUN-Request-ID", "HRUN-Request-ID",
f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}", f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}",
@@ -136,9 +141,7 @@ def run_step_request(runner: HttpRunner, step: TStep) -> StepResult:
# validate # validate
validators = step.validators validators = step.validators
try: try:
resp_obj.validate( resp_obj.validate(validators, variables_mapping)
validators, variables_mapping
)
step_result.success = True step_result.success = True
except ValidationFailure: except ValidationFailure:
log_req_resp_details() log_req_resp_details()
@@ -162,9 +165,7 @@ class StepRequestValidation(IStep):
def assert_equal( def assert_equal(
self, jmes_path: Text, expected_value: Any, message: Text = "" self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation": ) -> "StepRequestValidation":
self.__step.validators.append( self.__step.validators.append({"equal": [jmes_path, expected_value, message]})
{"equal": [jmes_path, expected_value, message]}
)
return self return self
def assert_not_equal( def assert_not_equal(
@@ -418,7 +419,6 @@ class RequestWithOptionalArgs(IStep):
class RunRequest(object): class RunRequest(object):
def __init__(self, name: Text): def __init__(self, name: Text):
self.__step = TStep(name=name) self.__step = TStep(name=name)

View File

@@ -1,10 +1,11 @@
import unittest import unittest
from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions from examples.postman_echo.request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions,
)
class TestRunRequest(unittest.TestCase): class TestRunRequest(unittest.TestCase):
def test_run_request(self): def test_run_request(self):
runner = TestCaseRequestWithFunctions().test_start() runner = TestCaseRequestWithFunctions().test_start()
summary = runner.get_summary() summary = runner.get_summary()

View File

@@ -22,11 +22,9 @@ def run_step_testcase(runner: HttpRunner, step: TStep) -> StepResult:
# step.testcase is a referenced testcase, e.g. RequestWithFunctions # step.testcase is a referenced testcase, e.g. RequestWithFunctions
ref_case_runner = step.testcase() ref_case_runner = step.testcase()
ref_case_runner.with_session(runner.session) \ ref_case_runner.with_session(runner.session).with_case_id(
.with_case_id(runner.case_id) \ runner.case_id
.with_variables(step_variables) \ ).with_variables(step_variables).with_export(step_export).test_start()
.with_export(step_export) \
.test_start()
# teardown hooks # teardown hooks
if step.teardown_hooks: if step.teardown_hooks:

View File

@@ -2,19 +2,22 @@ import unittest
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner
from httprunner.step_testcase import RunTestCase from httprunner.step_testcase import RunTestCase
from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions from examples.postman_echo.request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions,
)
class TestRunTestCase(unittest.TestCase): class TestRunTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.runner = HttpRunner() self.runner = HttpRunner()
def test_run_testcase_by_path(self): def test_run_testcase_by_path(self):
step_result = RunTestCase("run referenced testcase").call( step_result = (
TestCaseRequestWithFunctions RunTestCase("run referenced testcase")
).run(self.runner) .call(TestCaseRequestWithFunctions)
.run(self.runner)
)
self.assertTrue(step_result.success) self.assertTrue(step_result.success)
self.assertEqual(step_result.name, "run referenced testcase") self.assertEqual(step_result.name, "run referenced testcase")
self.assertEqual(len(step_result.data), 3) self.assertEqual(len(step_result.data), 3)

View File

@@ -5,6 +5,7 @@ import json
import os import os
import os.path import os.path
import platform import platform
import sys
import uuid import uuid
from multiprocessing import Queue from multiprocessing import Queue
from typing import Any, Dict, List, Text from typing import Any, Dict, List, Text
@@ -31,18 +32,20 @@ def init_sentry_sdk():
class GAClient(object): class GAClient(object):
version = '1' # GA API Version version = "1" # GA API Version
report_url = 'https://www.google-analytics.com/collect' report_url = "https://www.google-analytics.com/collect"
report_debug_url = 'https://www.google-analytics.com/debug/collect' # used for debug report_debug_url = (
"https://www.google-analytics.com/debug/collect" # used for debug
)
def __init__(self, tracking_id: Text): def __init__(self, tracking_id: Text):
self.http_client = requests.Session() self.http_client = requests.Session()
self.label = f"v{__version__}" self.label = f"v{__version__}"
self.common_params = { self.common_params = {
'v': self.version, "v": self.version,
'tid': tracking_id, # Tracking ID / Property ID, XX-XXXXXXX-X "tid": tracking_id, # Tracking ID / Property ID, XX-XXXXXXX-X
'cid': uuid.getnode(), # Anonymous Client ID "cid": uuid.getnode(), # Anonymous Client ID
'ua': f'HttpRunner/{__version__}', "ua": f"HttpRunner/{__version__}",
} }
# do not send GA events in CI environment # do not send GA events in CI environment
self.__is_ci = os.getenv("DISABLE_GA") == "true" self.__is_ci = os.getenv("DISABLE_GA") == "true"
@@ -52,16 +55,16 @@ class GAClient(object):
return return
data = { data = {
't': 'event', # Event hit type = event "t": "event", # Event hit type = event
'ec': category, # Required. Event Category. "ec": category, # Required. Event Category.
'ea': action, # Required. Event Action. "ea": action, # Required. Event Action.
'el': self.label, # Optional. Event label, used as version. "el": self.label, # Optional. Event label, used as version.
'ev': value, # Optional. Event value, must be non-negative integer "ev": value, # Optional. Event value, must be non-negative integer
} }
data.update(self.common_params) data.update(self.common_params)
try: try:
self.http_client.post(self.report_url, data=data, timeout=5) self.http_client.post(self.report_url, data=data, timeout=5)
except Exception: # ProxyError, SSLError, ConnectionError except Exception: # ProxyError, SSLError, ConnectionError
pass pass
def track_user_timing(self, category: Text, variable: Text, duration: int): def track_user_timing(self, category: Text, variable: Text, duration: int):
@@ -69,16 +72,16 @@ class GAClient(object):
return return
data = { data = {
't': 'timing', # Event hit type = timing "t": "timing", # Event hit type = timing
'utc': category, # Required. user timing category. e.g. jsonLoader "utc": category, # Required. user timing category. e.g. jsonLoader
'utv': variable, # Required. timing variable. e.g. load "utv": variable, # Required. timing variable. e.g. load
'utt': duration, # Required. time took duration. "utt": duration, # Required. time took duration.
'utl': self.label, # Optional. user timing label, used as version. "utl": self.label, # Optional. user timing label, used as version.
} }
data.update(self.common_params) data.update(self.common_params)
try: try:
self.http_client.post(self.report_url, data=data, timeout=5) self.http_client.post(self.report_url, data=data, timeout=5)
except Exception: # ProxyError, SSLError, ConnectionError except Exception: # ProxyError, SSLError, ConnectionError
pass pass
@@ -86,23 +89,21 @@ ga_client = GAClient("UA-114587036-1")
def set_os_environ(variables_mapping): def set_os_environ(variables_mapping):
""" set variables mapping to os.environ """set variables mapping to os.environ"""
"""
for variable in variables_mapping: for variable in variables_mapping:
os.environ[variable] = variables_mapping[variable] os.environ[variable] = variables_mapping[variable]
logger.debug(f"Set OS environment variable: {variable}") logger.debug(f"Set OS environment variable: {variable}")
def unset_os_environ(variables_mapping): def unset_os_environ(variables_mapping):
""" unset variables mapping to os.environ """unset variables mapping to os.environ"""
"""
for variable in variables_mapping: for variable in variables_mapping:
os.environ.pop(variable) os.environ.pop(variable)
logger.debug(f"Unset OS environment variable: {variable}") logger.debug(f"Unset OS environment variable: {variable}")
def get_os_environ(variable_name): def get_os_environ(variable_name):
""" get value of environment variable. """get value of environment variable.
Args: Args:
variable_name(str): variable name variable_name(str): variable name
@@ -121,7 +122,7 @@ def get_os_environ(variable_name):
def lower_dict_keys(origin_dict): def lower_dict_keys(origin_dict):
""" convert keys in dict to lower case """convert keys in dict to lower case
Args: Args:
origin_dict (dict): mapping data structure origin_dict (dict): mapping data structure
@@ -156,7 +157,7 @@ def lower_dict_keys(origin_dict):
def print_info(info_mapping): def print_info(info_mapping):
""" print info in mapping. """print info in mapping.
Args: Args:
info_mapping (dict): input(variables) or output mapping. info_mapping (dict): input(variables) or output mapping.
@@ -201,8 +202,7 @@ def print_info(info_mapping):
def omit_long_data(body, omit_len=512): def omit_long_data(body, omit_len=512):
""" omit too long str/bytes """omit too long str/bytes"""
"""
if not isinstance(body, (str, bytes)): if not isinstance(body, (str, bytes)):
return body return body
@@ -243,8 +243,7 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
class ExtendJSONEncoder(json.JSONEncoder): class ExtendJSONEncoder(json.JSONEncoder):
""" especially used to safely dump json data with python object, such as MultipartEncoder """especially used to safely dump json data with python object, such as MultipartEncoder"""
"""
def default(self, obj): def default(self, obj):
try: try:
@@ -256,8 +255,7 @@ class ExtendJSONEncoder(json.JSONEncoder):
def merge_variables( def merge_variables(
variables: VariablesMapping, variables_to_be_overridden: VariablesMapping variables: VariablesMapping, variables_to_be_overridden: VariablesMapping
) -> VariablesMapping: ) -> VariablesMapping:
""" merge two variables mapping, the first variables have higher priority """merge two variables mapping, the first variables have higher priority"""
"""
step_new_variables = {} step_new_variables = {}
for key, value in variables.items(): for key, value in variables.items():
if f"${key}" == value or "${" + key + "}" == value: if f"${key}" == value or "${" + key + "}" == value:
@@ -282,7 +280,7 @@ def is_support_multiprocessing() -> bool:
def gen_cartesian_product(*args: List[Dict]) -> List[Dict]: def gen_cartesian_product(*args: List[Dict]) -> List[Dict]:
""" generate cartesian product for lists """generate cartesian product for lists
Args: Args:
args (list of list): lists to be generated with cartesian product args (list of list): lists to be generated with cartesian product
@@ -320,3 +318,12 @@ def gen_cartesian_product(*args: List[Dict]) -> List[Dict]:
product_list.append(product_item_dict) product_list.append(product_item_dict)
return product_list return product_list
LOGGER_FORMAT = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level}</level> | <level>{message}</level>"
def init_logger():
# set log level to INFO
logger.remove()
logger.add(sys.stderr, format=LOGGER_FORMAT, level="INFO")

194
poetry.lock generated
View File

@@ -34,19 +34,6 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua" reference = "tsinghua"
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = "*"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua"
[[package]] [[package]]
name = "atomicwrites" name = "atomicwrites"
version = "1.4.0" version = "1.4.0"
@@ -81,23 +68,26 @@ reference = "tsinghua"
[[package]] [[package]]
name = "black" name = "black"
version = "19.10b0" version = "22.3.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6.2"
[package.dependencies] [package.dependencies]
appdirs = "*" click = ">=8.0.0"
attrs = ">=18.1.0" mypy-extensions = ">=0.4.3"
click = ">=6.5" pathspec = ">=0.9.0"
pathspec = ">=0.6,<1" platformdirs = ">=2"
regex = "*" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
toml = ">=0.9.4" typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typed-ast = ">=1.4.0" typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[package.source] [package.source]
type = "legacy" type = "legacy"
@@ -315,6 +305,19 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua" reference = "tsinghua"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "main"
optional = false
python-versions = "*"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
@@ -344,6 +347,23 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua" reference = "tsinghua"
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
@@ -486,19 +506,6 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua" reference = "tsinghua"
[[package]]
name = "regex"
version = "2022.3.15"
description = "Alternative regular expression module, to replace re."
category = "main"
optional = false
python-versions = ">=3.6"
[package.source]
type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "tsinghua"
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.27.1" version = "2.27.1"
@@ -692,7 +699,7 @@ upload = ["requests-toolbelt", "filetype"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "ce51964d1daf419593be8b6d6f003069bf8626d922b950432e2c6f9a8093d9e6" content-hash = "8dc444117b1b9a55f00d25b86e69a26d9ceaac2d331731b676415fd4a019f57a"
[metadata.files] [metadata.files]
allure-pytest = [ allure-pytest = [
@@ -703,10 +710,6 @@ allure-python-commons = [
{file = "allure-python-commons-2.9.45.tar.gz", hash = "sha256:c238d28aeac35e8c7c517d8a2327e25ae5bbf2c30b5e2313d20ef11d75f5549d"}, {file = "allure-python-commons-2.9.45.tar.gz", hash = "sha256:c238d28aeac35e8c7c517d8a2327e25ae5bbf2c30b5e2313d20ef11d75f5549d"},
{file = "allure_python_commons-2.9.45-py3-none-any.whl", hash = "sha256:3572f0526db3946fb14470c58b0b41d343483aad91d37d414e4641815e13691a"}, {file = "allure_python_commons-2.9.45-py3-none-any.whl", hash = "sha256:3572f0526db3946fb14470c58b0b41d343483aad91d37d414e4641815e13691a"},
] ]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -716,8 +719,29 @@ attrs = [
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
] ]
black = [ black = [
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
{file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
{file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
{file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
{file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
{file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
{file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
{file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
{file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
{file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
{file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
{file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
{file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
{file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
{file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
{file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
{file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
{file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
{file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
{file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
] ]
brotli = [ brotli = [
{file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
@@ -888,6 +912,10 @@ markupsafe = [
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
] ]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ -896,6 +924,10 @@ pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
] ]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
@@ -975,82 +1007,6 @@ pyyaml = [
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
] ]
regex = [
{file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"},
{file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"},
{file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"},
{file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"},
{file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"},
{file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"},
{file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"},
{file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"},
{file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"},
{file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"},
{file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"},
{file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"},
{file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"},
{file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"},
{file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"},
{file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"},
{file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"},
{file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"},
{file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"},
{file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"},
{file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"},
{file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"},
{file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"},
{file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"},
{file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"},
{file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"},
{file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"},
{file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"},
{file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"},
{file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"},
{file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"},
{file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"},
{file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"},
{file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"},
{file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"},
{file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"},
{file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"},
{file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"},
{file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"},
{file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"},
{file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"},
{file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"},
{file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"},
{file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"},
{file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"},
{file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"},
{file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"},
{file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"},
{file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"},
]
requests = [ requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},

View File

@@ -1,12 +1,12 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "4.0.0-alpha" version = "4.0.0-beta"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"
authors = ["debugtalk <debugtalk@gmail.com>"] authors = ["debugtalk <debugtalk@gmail.com>"]
homepage = "https://github.com/httprunner/httprunner" homepage = "https://httprunner.com"
repository = "https://github.com/httprunner/httprunner" repository = "https://github.com/httprunner/httprunner"
documentation = "https://httprunner.com/docs" documentation = "https://httprunner.com/docs"
@@ -35,7 +35,7 @@ pyyaml = "^5.4.1"
pydantic = "~1.8" # >=1.8.0 <1.9.0 pydantic = "~1.8" # >=1.8.0 <1.9.0
loguru = "^0.4.1" loguru = "^0.4.1"
jmespath = "^0.9.5" jmespath = "^0.9.5"
black = "^19.10b0" black = "^22.3.0"
pytest = "^7.1.1" pytest = "^7.1.1"
pytest-html = "^3.1.1" pytest-html = "^3.1.1"
sentry-sdk = "^0.14.4" sentry-sdk = "^0.14.4"
@@ -44,6 +44,7 @@ requests-toolbelt = {version = "^0.9.1", optional = true}
filetype = {version = "^1.0.7", optional = true} filetype = {version = "^1.0.7", optional = true}
Brotli = "^1.0.9" Brotli = "^1.0.9"
jinja2 = "^3.0.3" jinja2 = "^3.0.3"
toml = "^0.10.2"
[tool.poetry.extras] [tool.poetry.extras]
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure

View File

@@ -2,7 +2,7 @@
# install hrp with one shell command # install hrp with one shell command
# bash -c "$(curl -ksSL https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh)" # bash -c "$(curl -ksSL https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh)"
LATEST_VERSION="v4.0.0-alpha" LATEST_VERSION="v4.0.0-beta"
set -e set -e