Merge pull request #919 from httprunner/v3

## 3.0.8 (2020-06-04)

**Added**

- feat: add sentry sdk
- feat: extract session variable from referenced testcase step

**Fixed**

- fix: missing request json
- fix: override testsuite/testcase config verify
- fix: only strip whitespaces and tabs, \n\r are left because they maybe used in changeset
- fix: log testcase duration before raise ValidationFailure

**Changed**

- change: add httprunner version in generated pytest file
This commit is contained in:
debugtalk
2020-06-04 18:43:04 +08:00
committed by GitHub
37 changed files with 312 additions and 108 deletions

View File

@@ -1,5 +1,23 @@
# Release History
## 3.0.8 (2020-06-04)
**Added**
- feat: add sentry sdk
- feat: extract session variable from referenced testcase step
**Fixed**
- fix: missing request json
- fix: override testsuite/testcase config verify
- fix: only strip whitespaces and tabs, \n\r are left because they maybe used in changeset
- fix: log testcase duration before raise ValidationFailure
**Changed**
- change: add httprunner version in generated pytest file
## 3.0.7 (2020-06-03)
**Added**

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/httpbin/basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/httpbin/hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/httpbin/load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/httpbin/upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/httpbin/validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
@@ -10,6 +10,7 @@ class TestCaseRequestWithFunctions(HttpRunner):
.variables(**{"foo1": "session_bar1", "var1": "testsuite_val1"})
.base_url("https://postman-echo.com")
.verify(False)
.export(*["session_foo2"])
)
teststeps = [

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
import os
@@ -26,6 +26,23 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
RunTestCase("request with functions")
.with_variables(**{"foo1": "override_bar1"})
.call(RequestWithFunctions)
.extract(*["session_foo2"])
),
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=$session_foo2")
.validate()
.assert_equal("status_code", 200)
.assert_equal("body.form.foo1", "session_bar1")
.assert_equal("body.form.foo2", "session_bar2")
),
]

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -4,6 +4,7 @@ config:
foo1: session_bar1
base_url: "https://postman-echo.com"
verify: False
export: ["session_foo2"]
teststeps:
-

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
@@ -10,6 +10,7 @@ class TestCaseRequestWithFunctions(HttpRunner):
.variables(**{"foo1": "session_bar1"})
.base_url("https://postman-echo.com")
.verify(False)
.export(*["session_foo2"])
)
teststeps = [

View File

@@ -11,3 +11,20 @@ teststeps:
variables:
foo1: override_bar1
testcase: request_methods/request_with_functions.yml
extract:
- session_foo2
-
name: post form data
variables:
foo1: bar1
request:
method: POST
url: /post
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
Content-Type: "application/x-www-form-urlencoded"
data: "foo1=$foo1&foo2=$session_foo2"
validate:
- eq: ["status_code", 200]
- eq: ["body.form.foo1", "session_bar1"]
- eq: ["body.form.foo2", "session_bar2"]

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
import os
@@ -26,6 +26,23 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
RunTestCase("request with functions")
.with_variables(**{"foo1": "override_bar1"})
.call(RequestWithFunctions)
.extract(*["session_foo2"])
),
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=$session_foo2")
.validate()
.assert_equal("status_code", 200)
.assert_equal("body.form.foo1", "session_bar1")
.assert_equal("body.form.foo2", "session_bar2")
),
]

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,4 +1,4 @@
# NOTICE: Generated By HttpRunner.
# NOTICE: Generated By HttpRunner v3.0.8
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,16 +1,13 @@
__version__ = "3.0.7"
__version__ = "3.0.8"
__description__ = "One-stop solution for HTTP(S) testing."
from httprunner.runner import HttpRunner
from httprunner.schema import TConfig, TStep
from httprunner.testcase import Config, Step, RunRequest, RunTestCase
__all__ = [
"__version__",
"__description__",
"HttpRunner",
"TConfig",
"TStep",
"Config",
"Step",
"RunRequest",

View File

@@ -1,9 +1,9 @@
import contextlib
import logging
import sys
from io import StringIO
from fastapi import APIRouter
from loguru import logger
from starlette.requests import Request
router = APIRouter()
@@ -37,6 +37,6 @@ async def debug_python(request: Request):
resp["code"] = 1
resp["message"] = "fail"
resp["result"] = str(ex)
logging.error(resp)
logger.error(resp)
return resp

View File

@@ -1,9 +1,9 @@
import logging
import subprocess
from typing import List
import pkg_resources
from fastapi import APIRouter
from loguru import logger
router = APIRouter()
@@ -29,6 +29,6 @@ async def install_dependenies(deps: List[str]):
resp["result"][dep] = False
resp["code"] = 1
resp["message"] = "fail"
logging.error(f"failed to install dependency: {dep}")
logger.error(f"failed to install dependency: {dep}")
return resp

View File

@@ -4,12 +4,16 @@ import sys
import pytest
from loguru import logger
from sentry_sdk import capture_message
from httprunner import __description__, __version__
from httprunner.compat import ensure_cli_args
from httprunner.ext.har2case import init_har2case_parser, main_har2case
from httprunner.make import init_make_parser, main_make
from httprunner.scaffold import init_parser_scaffold, main_scaffold
from httprunner.utils import init_sentry_sdk
init_sentry_sdk()
def init_parser_run(subparsers):
@@ -20,6 +24,7 @@ def init_parser_run(subparsers):
def main_run(extra_args):
capture_message("start to run")
# keep compatibility with v2
extra_args = ensure_cli_args(extra_args)

View File

@@ -11,6 +11,7 @@ from requests.exceptions import (
MissingSchema,
RequestException,
)
from sentry_sdk import capture_exception
from httprunner.schema import RequestData, ResponseData
from httprunner.schema import SessionData, ReqRespData
@@ -42,20 +43,21 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
# record actual request info
request_headers = dict(resp_obj.request.headers)
request_cookies = resp_obj.request._cookies.get_dict()
request_body = resp_obj.request.body
try:
request_body = json.loads(request_body)
except json.JSONDecodeError:
# str: Unexpected UTF-8 BOM (decode using utf-8-sig)
pass
except UnicodeDecodeError:
# bytes/bytearray: request body in protobuf
pass
except TypeError:
# neither str nor bytes/bytearray, e.g. None
pass
if request_body:
request_body = resp_obj.request.body
if request_body is not None:
try:
request_body = json.loads(request_body)
except json.JSONDecodeError:
# str: a=1&b=2
pass
except UnicodeDecodeError as ex:
# bytes/bytearray: request body in protobuf
capture_exception(ex)
except TypeError as ex:
# neither str nor bytes/bytearray, e.g. <MultipartEncoder>
capture_exception(ex)
request_content_type = lower_dict_keys(request_headers).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:
# upload file type

View File

@@ -2,11 +2,11 @@
This module handles compatibility issues between testcase format v2 and v3.
"""
import os
import sys
from typing import List, Dict, Text, Union
from loguru import logger
from httprunner import exceptions
from httprunner.loader import load_project_meta
from httprunner.utils import sort_dict_by_custom_order
@@ -28,9 +28,8 @@ def convert_jmespath(raw: Text) -> Text:
elif item.isdigit():
# convert lst.0.name to lst[0].name
if len(raw_list) == 0:
raise exceptions.FileFormatError(
f"Invalid jmespath: {raw}, jmespath should startswith headers/body/status_code/cookies"
)
logger.error(f"Invalid jmespath: {raw}")
sys.exit(1)
last_item = raw_list.pop()
item = f"{last_item}[{item}]"
@@ -60,7 +59,8 @@ def convert_extractors(extractors: Union[List, Dict]) -> Dict:
elif isinstance(extractors, Dict):
v3_extractors = extractors
else:
raise exceptions.FileFormatError(f"Invalid extractor: {extractors}")
logger.error(f"Invalid extractor: {extractors}")
sys.exit(1)
for k, v in v3_extractors.items():
v3_extractors[k] = convert_jmespath(v)
@@ -133,7 +133,10 @@ def ensure_step_attachment(step: Dict) -> Dict:
test_dict["teardown_hooks"] = step["teardown_hooks"]
if "extract" in step:
test_dict["extract"] = convert_extractors(step["extract"])
if step.get("request"):
test_dict["extract"] = convert_extractors(step["extract"])
elif step.get("testcase"):
test_dict["extract"] = step["extract"]
if "validate" in step:
test_dict["validate"] = convert_validators(step["validate"])
@@ -164,6 +167,8 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
for step in test_content["teststeps"]:
teststep = {}
teststep.update(ensure_step_attachment(step))
if "request" in step:
teststep["request"] = step.pop("request")
elif "api" in step:
@@ -171,7 +176,6 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
elif "testcase" in step:
teststep["testcase"] = step.pop("testcase")
teststep.update(ensure_step_attachment(step))
teststep = sort_step_by_custom_order(teststep)
v3_content["teststeps"].append(teststep)
@@ -207,7 +211,8 @@ def generate_conftest_for_summary(args: List):
# FIXME: several test paths maybe specified
break
else:
raise exceptions.FileNotFound(f"No test path specified!")
logger.error(f"No valid test path specified! \nargs: {args}")
sys.exit(1)
project_meta = load_project_meta(test_path)
conftest_path = os.path.join(project_meta.PWD, "conftest.py")

View File

@@ -12,6 +12,7 @@ import os
import sys
from loguru import logger
from sentry_sdk import capture_message
from httprunner.ext.har2case.core import HarParser
@@ -69,6 +70,7 @@ def main_har2case(args):
else:
output_file_type = "pytest"
capture_message(f"har2case {output_file_type}")
HarParser(har_source_file, args.filter, args.exclude).gen_testcase(output_file_type)
return 0

View File

@@ -5,6 +5,7 @@ import sys
import urllib.parse as urlparse
from loguru import logger
from sentry_sdk import capture_exception
from httprunner.ext.har2case import utils
from httprunner.make import make_testcase, format_pytest_with_black
@@ -238,14 +239,11 @@ class HarParser(object):
try:
resp_content_json = json.loads(content)
except JSONDecodeError:
logger.warning(
"response content can not be loaded as json: {}".format(
content.encode("utf-8")
)
)
logger.warning(f"response content can not be loaded as json: {content}")
return
if not isinstance(resp_content_json, dict):
# e.g. ['a', 'b']
return
for key, value in resp_content_json.items():
@@ -334,7 +332,12 @@ class HarParser(object):
logger.info(f"Start to generate testcase from {self.har_file_path}")
harfile = os.path.splitext(self.har_file_path)[0]
testcase = self._make_testcase()
try:
testcase = self._make_testcase()
except Exception as ex:
capture_exception(ex)
raise
logger.debug("prepared testcase: {}".format(testcase))
if file_type == "JSON":

View File

@@ -1,11 +1,12 @@
import io
import json
import logging
import sys
from json.decoder import JSONDecodeError
from urllib.parse import unquote
import yaml
from loguru import logger
from sentry_sdk import capture_exception
def load_har_log_entries(file_path):
@@ -32,8 +33,9 @@ def load_har_log_entries(file_path):
try:
content_json = json.loads(f.read())
return content_json["log"]["entries"]
except (KeyError, TypeError, JSONDecodeError):
logging.error("HAR file content error: {}".format(file_path))
except (KeyError, TypeError, JSONDecodeError) as ex:
capture_exception(ex)
logger.error("HAR file content error: {}".format(file_path))
sys.exit(1)
@@ -103,20 +105,20 @@ def convert_list_to_dict(origin_list):
def dump_yaml(testcase, yaml_file):
""" dump HAR entries to yaml testcase
"""
logging.info("dump testcase to YAML format.")
logger.info("dump testcase to YAML format.")
with io.open(yaml_file, "w", encoding="utf-8") as outfile:
yaml.dump(
testcase, outfile, allow_unicode=True, default_flow_style=False, indent=4
)
logging.info("Generate YAML testcase successfully: {}".format(yaml_file))
logger.info("Generate YAML testcase successfully: {}".format(yaml_file))
def dump_json(testcase, json_file):
""" dump HAR entries to json testcase
"""
logging.info("dump testcase to JSON format.")
logger.info("dump testcase to JSON format.")
with io.open(json_file, "w", encoding="utf-8") as outfile:
my_json_str = json.dumps(testcase, ensure_ascii=False, indent=4)
@@ -125,4 +127,4 @@ def dump_json(testcase, json_file):
outfile.write(my_json_str)
logging.info("Generate JSON testcase successfully: {}".format(json_file))
logger.info("Generate JSON testcase successfully: {}".format(json_file))

View File

@@ -290,8 +290,11 @@ def locate_file(start_path: Text, file_name: Text) -> Text:
return os.path.abspath(file_path)
# current working directory
if os.path.abspath(start_dir_path) == os.getcwd():
raise exceptions.FileNotFound(f"{file_name} not found in {start_path}")
cwd = os.getcwd()
if os.path.abspath(start_dir_path) == cwd:
raise exceptions.FileNotFound(
f"{file_name} not found for {start_path}\ncurrent working directory: {cwd}"
)
# system root dir
# Windows, e.g. 'E:\\'

View File

@@ -5,8 +5,9 @@ from typing import Text, List, Tuple, Dict, Set, NoReturn
import jinja2
from loguru import logger
from sentry_sdk import capture_exception
from httprunner import exceptions
from httprunner import exceptions, __version__
from httprunner.compat import ensure_testcase_v3_api, ensure_testcase_v3
from httprunner.loader import (
load_folder_files,
@@ -24,7 +25,7 @@ make_files_cache_set: Set = set()
pytest_files_set: Set = set()
__TEMPLATE__ = jinja2.Template(
"""# NOTICE: Generated By HttpRunner.
"""# NOTICE: Generated By HttpRunner v{{ version }}
# FROM: {{ testcase_path }}
{% if imports_list %}
import os
@@ -131,6 +132,7 @@ def format_pytest_with_black(*python_paths: Text) -> NoReturn:
try:
subprocess.run(["black", *python_paths])
except subprocess.CalledProcessError as ex:
capture_exception(ex)
logger.error(ex)
@@ -147,6 +149,9 @@ def make_config_chain_style(config: Dict) -> Text:
if "verify" in config:
config_chain_style += f'.verify({config["verify"]})'
if "export" in config:
config_chain_style += f'.export(*{config["export"]})'
return config_chain_style
@@ -173,6 +178,10 @@ def make_request_chain_style(request: Dict) -> Text:
data = f'"{data}"'
request_chain_style += f".with_data({data})"
if "json" in request:
req_json = request["json"]
request_chain_style += f".with_json({req_json})"
if "timeout" in request:
timeout = request["timeout"]
request_chain_style += f".set_timeout({timeout})"
@@ -198,7 +207,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
elif teststep.get("testcase"):
step_info = f'RunTestCase("{teststep["name"]}")'
else:
raise exceptions.TestCaseFormatError
raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
if "variables" in teststep:
variables = teststep["variables"]
@@ -211,11 +220,18 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
call_ref_testcase = f".call({testcase})"
step_info += call_ref_testcase
if "extract" in teststep:
step_info += ".extract()"
for extract_name, extract_path in teststep["extract"].items():
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
extract_info = teststep.get("extract")
if extract_info:
if isinstance(extract_info, Dict):
# request step
step_info += ".extract()"
for extract_name, extract_path in extract_info.items():
step_info += f'.with_jmespath("{extract_path}", "{extract_name}")'
elif isinstance(extract_info, List):
# reference testcase step
step_info += f".extract(*{extract_info})"
else:
raise exceptions.TestCaseFormatError(f"Invalid extract: {extract_info}")
if "validate" in teststep:
step_info += ".validate()"
@@ -298,6 +314,7 @@ def make_testcase(
)
data = {
"version": __version__,
"testcase_path": __ensure_cwd_relative(testcase_path),
"class_name": f"TestCase{testcase_cls_name}",
"imports_list": imports_list,
@@ -326,10 +343,10 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
# validate testsuite format
load_testsuite(testsuite)
config = testsuite["config"]
testsuite_path = config["path"]
testsuite_config = testsuite["config"]
testsuite_path = testsuite_config["path"]
testsuite_variables = config.get("variables", {})
testsuite_variables = testsuite_config.get("variables", {})
if isinstance(testsuite_variables, Text):
# get variables by function, e.g. ${get_variables()}
project_meta = load_project_meta(testsuite_path)
@@ -357,9 +374,12 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
# override testcase name
testcase_dict["config"]["name"] = testcase["name"]
# override base_url
base_url = testsuite["config"].get("base_url") or testcase.get("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
testcase_dict["config"].setdefault("variables", {})
testcase_dict["config"]["variables"].update(testcase.get("variables", {}))

View File

@@ -3,6 +3,8 @@ import builtins
import re
from typing import Any, Set, Text, Callable, List, Dict
from sentry_sdk import capture_exception
from httprunner import loader, utils, exceptions
from httprunner.schema import VariablesMapping, FunctionsMapping
@@ -70,7 +72,8 @@ def regex_findall_variables(content: Text) -> List[Text]:
for var_tuple in variable_regex_compile.findall(content):
vars_list.append(var_tuple[0] or var_tuple[1])
return vars_list
except TypeError:
except TypeError as ex:
capture_exception(ex)
return []
@@ -102,7 +105,8 @@ def regex_findall_functions(content: Text) -> List[Text]:
"""
try:
return function_regex_compile.findall(content)
except TypeError:
except TypeError as ex:
capture_exception(ex)
return []
@@ -358,7 +362,8 @@ def parse_data(
# content in string format may contains variables and functions
variables_mapping = variables_mapping or {}
functions_mapping = functions_mapping or {}
raw_data = raw_data.strip()
# only strip whitespaces and tabs, \n\r is left because they maybe used in changeset
raw_data = raw_data.strip(" \t")
return parse_string(raw_data, variables_mapping, functions_mapping)
elif isinstance(raw_data, (list, set, tuple)):

View File

@@ -46,6 +46,7 @@ class HttpRunner(object):
__step_datas: List[StepData] = None
__session: HttpSession = None
__session_variables: VariablesMapping = {}
__export_variables: VariablesMapping = {}
# time
__start_at: float = 0
__duration: float = 0
@@ -101,6 +102,7 @@ class HttpRunner(object):
method = parsed_request_dict.pop("method")
url_path = parsed_request_dict.pop("url")
url = build_url(self.__config.base_url, url_path)
parsed_request_dict["verify"] = self.__config.verify
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
# request
@@ -147,6 +149,8 @@ class HttpRunner(object):
except ValidationFailure:
self.__session.data.success = False
log_req_resp_details()
# log testcase duration before raise ValidationFailure
self.__duration = time.time() - self.__start_at
raise
finally:
# save request & response meta data
@@ -248,6 +252,7 @@ class HttpRunner(object):
self.__step_datas: List[StepData] = []
self.__session = self.__session or HttpSession()
self.__session_variables = {}
self.__export_variables = {}
# run teststeps
for step in self.__teststeps:
@@ -269,6 +274,11 @@ class HttpRunner(object):
self.__session_variables.update(extract_mapping)
self.__duration = time.time() - self.__start_at
self.__export_variables = self.get_export_variables()
if self.__export_variables:
logger.info(f"export variables: {self.__export_variables}")
return self
def run_path(self, path: Text) -> "HttpRunner":
@@ -293,6 +303,9 @@ class HttpRunner(object):
return self.__step_datas
def get_export_variables(self) -> Dict:
if self.__export_variables:
return self.__export_variables
export_vars_mapping = {}
for var_name in self.__config.export:
if var_name not in self.__session_variables:

View File

@@ -2,6 +2,7 @@ import os.path
import sys
from loguru import logger
from sentry_sdk import capture_message
def init_parser_scaffold(subparsers):
@@ -140,5 +141,6 @@ def sleep(n_secs):
def main_scaffold(args):
capture_message("startproject with scaffold")
create_scaffold(args.project_name)
sys.exit(0)

View File

@@ -66,7 +66,7 @@ class TStep(BaseModel):
variables: VariablesMapping = {}
setup_hooks: Hook = []
teardown_hooks: Hook = []
extract: Dict[Text, Text] = {}
extract: Union[Dict[Text, Text], List[Text]] = {}
validators: Validators = Field([], alias="validate")
validate_script: List[Text] = []

View File

@@ -16,6 +16,7 @@ class Config(object):
self.__variables = {}
self.__base_url = ""
self.__verify = False
self.__export = []
caller_frame = inspect.stack()[1]
self.__path = caller_frame.filename
@@ -40,45 +41,52 @@ class Config(object):
self.__verify = verify
return self
def export(self, *export_var_name: Text) -> "Config":
self.__export.extend(export_var_name)
return self
def perform(self) -> TConfig:
return TConfig(
name=self.__name,
base_url=self.__base_url,
verify=self.__verify,
variables=self.__variables,
export=list(set(self.__export)),
path=self.__path,
)
class StepValidation(object):
class StepRequestValidation(object):
def __init__(self, step: TStep):
self.__t_step = step
def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation":
def assert_equal(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"equal": [jmes_path, expected_value]})
return self
def assert_not_equal(
self, jmes_path: Text, expected_value: Any
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"not_equal": [jmes_path, expected_value]})
return self
def assert_greater_than(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"greater_than": [jmes_path, expected_value]})
return self
def assert_less_than(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"less_than": [jmes_path, expected_value]})
return self
def assert_greater_or_equals(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append(
{"greater_or_equals": [jmes_path, expected_value]}
)
@@ -86,19 +94,19 @@ class StepValidation(object):
def assert_less_or_equals(
self, jmes_path: Text, expected_value: Union[int, float]
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"less_or_equals": [jmes_path, expected_value]})
return self
def assert_length_equal(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"length_equal": [jmes_path, expected_value]})
return self
def assert_length_greater_than(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append(
{"length_greater_than": [jmes_path, expected_value]}
)
@@ -106,7 +114,7 @@ class StepValidation(object):
def assert_length_less_than(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append(
{"length_less_than": [jmes_path, expected_value]}
)
@@ -114,7 +122,7 @@ class StepValidation(object):
def assert_length_greater_or_equals(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append(
{"length_greater_or_equals": [jmes_path, expected_value]}
)
@@ -122,7 +130,7 @@ class StepValidation(object):
def assert_length_less_or_equals(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append(
{"length_less_or_equals": [jmes_path, expected_value]}
)
@@ -130,41 +138,43 @@ class StepValidation(object):
def assert_string_equals(
self, jmes_path: Text, expected_value: int
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"string_equals": [jmes_path, expected_value]})
return self
def assert_startswith(
self, jmes_path: Text, expected_value: Text
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"startswith": [jmes_path, expected_value]})
return self
def assert_endswith(
self, jmes_path: Text, expected_value: Text
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"endswith": [jmes_path, expected_value]})
return self
def assert_regex_match(
self, jmes_path: Text, expected_value: Text
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"regex_match": [jmes_path, expected_value]})
return self
def assert_contains(self, jmes_path: Text, expected_value: Any) -> "StepValidation":
def assert_contains(
self, jmes_path: Text, expected_value: Any
) -> "StepRequestValidation":
self.__t_step.validators.append({"contains": [jmes_path, expected_value]})
return self
def assert_contained_by(
self, jmes_path: Text, expected_value: Any
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"contained_by": [jmes_path, expected_value]})
return self
def assert_type_match(
self, jmes_path: Text, expected_value: Text
) -> "StepValidation":
) -> "StepRequestValidation":
self.__t_step.validators.append({"type_match": [jmes_path, expected_value]})
return self
@@ -172,11 +182,11 @@ class StepValidation(object):
return self.__t_step
class StepExtraction(object):
class StepRequestExtraction(object):
def __init__(self, step: TStep):
self.__t_step = step
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepExtraction":
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
self.__t_step.extract[var_name] = jmes_path
return self
@@ -188,8 +198,8 @@ class StepExtraction(object):
# # TODO: extract response json with jsonpath
# pass
def validate(self) -> StepValidation:
return StepValidation(self.__t_step)
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__t_step)
def perform(self) -> TStep:
return self.__t_step
@@ -215,6 +225,10 @@ class RequestWithOptionalArgs(object):
self.__t_step.request.data = data
return self
def with_json(self, req_json) -> "RequestWithOptionalArgs":
self.__t_step.request.req_json = req_json
return self
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
self.__t_step.request.timeout = timeout
return self
@@ -234,11 +248,11 @@ class RequestWithOptionalArgs(object):
# def hooks(self):
# pass
def extract(self) -> StepExtraction:
return StepExtraction(self.__t_step)
def extract(self) -> StepRequestExtraction:
return StepRequestExtraction(self.__t_step)
def validate(self) -> StepValidation:
return StepValidation(self.__t_step)
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__t_step)
def perform(self) -> TStep:
return self.__t_step
@@ -281,6 +295,19 @@ class RunRequest(object):
return RequestWithOptionalArgs(self.__t_step)
class StepRefCase(object):
def __init__(self, step: TStep):
self.__t_step = step
self.__t_step.extract = []
def extract(self, *var_name: Text) -> "StepRefCase":
self.__t_step.extract.extend(var_name)
return self
def perform(self) -> TStep:
return self.__t_step
class RunTestCase(object):
def __init__(self, name: Text):
self.__t_step = TStep(name=name)
@@ -289,9 +316,9 @@ class RunTestCase(object):
self.__t_step.variables.update(variables)
return self
def call(self, testcase: Callable):
def call(self, testcase: Callable) -> StepRefCase:
self.__t_step.testcase = testcase
return self
return StepRefCase(self.__t_step)
def perform(self) -> TStep:
return self.__t_step
@@ -301,7 +328,11 @@ class Step(object):
def __init__(
self,
step: Union[
StepValidation, StepExtraction, RequestWithOptionalArgs, RunTestCase
StepRequestValidation,
StepRequestExtraction,
RequestWithOptionalArgs,
RunTestCase,
StepRefCase,
],
):
self.__t_step = step.perform()

View File

@@ -2,14 +2,25 @@ import collections
import json
import os.path
import platform
import uuid
from typing import Dict, List, Any
import sentry_sdk
from loguru import logger
from httprunner import __version__
from httprunner import exceptions
def init_sentry_sdk():
sentry_sdk.init(
dsn="https://460e31339bcb428c879aafa6a2e78098@sentry.io/5263855",
release="httprunner@{}".format(__version__),
)
with sentry_sdk.configure_scope() as scope:
scope.set_user({"id": uuid.getnode()})
def set_os_environ(variables_mapping):
""" set variables mapping to os.environ
"""

32
poetry.lock generated
View File

@@ -451,6 +451,32 @@ version = "0.9.1"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
category = "main"
description = "Python client for Sentry (https://getsentry.com)"
name = "sentry-sdk"
optional = false
python-versions = "*"
version = "0.14.4"
[package.dependencies]
certifi = "*"
urllib3 = ">=1.10.0"
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
beam = ["beam (>=2.12)"]
bottle = ["bottle (>=0.12.13)"]
celery = ["celery (>=3)"]
django = ["django (>=1.8)"]
falcon = ["falcon (>=1.4)"]
flask = ["flask (>=0.11)", "blinker (>=1.1)"]
pyspark = ["pyspark (>=2.4.4)"]
rq = ["rq (>=0.6)"]
sanic = ["sanic (>=0.8)"]
sqlalchemy = ["sqlalchemy (>=1.2)"]
tornado = ["tornado (>=5)"]
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
@@ -572,7 +598,7 @@ allure = ["allure-pytest"]
upload = ["requests-toolbelt", "filetype"]
[metadata]
content-hash = "3b5147c8c95480574c9eaa8f035c536cf18535766f60f768d2e714b257511dae"
content-hash = "581cacf33c8afe330e5b6a965d5e16f6266718249cbdfed0d080b7536c5c4590"
python-versions = "^3.6"
[metadata.files]
@@ -850,6 +876,10 @@ requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
sentry-sdk = [
{file = "sentry-sdk-0.14.4.tar.gz", hash = "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c"},
{file = "sentry_sdk-0.14.4-py2.py3-none-any.whl", hash = "sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "httprunner"
version = "3.0.7"
version = "3.0.8"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"
@@ -38,6 +38,7 @@ jmespath = "^0.9.5"
black = "^19.10b0"
pytest = "^5.4.2"
pytest-html = "^2.1.1"
sentry-sdk = "^0.14.4"
allure-pytest = {version = "^2.8.16", optional = true}
requests-toolbelt = {version = "^0.9.1", optional = true}
filetype = {version = "^1.0.7", optional = true}

View File

@@ -1,7 +1,7 @@
import os
import unittest
from httprunner import compat, exceptions
from httprunner import compat
class TestCompat(unittest.TestCase):
@@ -19,7 +19,7 @@ class TestCompat(unittest.TestCase):
compat.convert_jmespath("body.data.buildings.0.building_id"),
"body.data.buildings[0].building_id",
)
with self.assertRaises(exceptions.FileFormatError):
with self.assertRaises(SystemExit):
compat.convert_jmespath("2.buildings.0.building_id")
def test_convert_extractors(self):

View File

@@ -25,4 +25,4 @@ class TestHttpRunner(unittest.TestCase):
self.assertTrue(result.success)
self.assertEqual(result.name, "request methods testcase: reference testcase")
self.assertEqual(result.step_datas[0].name, "request with functions")
self.assertEqual(len(result.step_datas), 1)
self.assertEqual(len(result.step_datas), 2)