mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 07:39:44 +08:00
Merge pull request #809 from httprunner/leo_dev
2.4.8 **Added** - feat: store parse failed api/testcase/testsuite file path in `logs/xxx.parse_failed.json` - feat: add exception SummaryEmpty **Fixed** - fix: display request & response details in report when extraction failed - fix: include CHANGELOG in package
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
# Release History
|
||||
|
||||
## 2.4.8 (2019-12-25)
|
||||
|
||||
**Added**
|
||||
|
||||
- feat: store parse failed api/testcase/testsuite file path in `logs/xxx.parse_failed.json`
|
||||
- feat: add exception SummaryEmpty
|
||||
|
||||
**Fixed**
|
||||
|
||||
- fix: display request & response details in report when extraction failed
|
||||
- fix: include CHANGELOG in package
|
||||
|
||||
**Changed**
|
||||
|
||||
- change: use sys.exit(code) in hrun main
|
||||
|
||||
## 2.4.7 (2019-12-24)
|
||||
|
||||
**Added**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "2.4.7"
|
||||
__version__ = "2.4.8"
|
||||
__description__ = "One-stop solution for HTTP(S) testing."
|
||||
|
||||
__all__ = ["__version__", "__description__"]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sys
|
||||
from httprunner.cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
||||
@@ -195,6 +195,10 @@ class HttpRunner(object):
|
||||
# parse tests
|
||||
self.exception_stage = "parse tests"
|
||||
parsed_testcases = parser.parse_tests(tests_mapping)
|
||||
parse_failed_testfiles = parser.get_parse_failed_testfiles()
|
||||
if parse_failed_testfiles:
|
||||
logger.log_warning("parse failures occurred ...")
|
||||
utils.dump_logs(parse_failed_testfiles, project_mapping, "parse_failed")
|
||||
|
||||
if self.save_tests:
|
||||
utils.dump_logs(parsed_testcases, project_mapping, "parsed")
|
||||
@@ -277,6 +281,8 @@ class HttpRunner(object):
|
||||
path_or_tests:
|
||||
str: testcase/testsuite file/foler path
|
||||
dict: valid testcase/testsuite data
|
||||
dot_env_path (str): specified .env file path.
|
||||
mapping (dict): if mapping is specified, it will override variables in config block.
|
||||
|
||||
Returns:
|
||||
dict: result summary
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from httprunner import __description__, __version__
|
||||
from httprunner.api import HttpRunner
|
||||
from httprunner.compat import is_py2
|
||||
@@ -64,23 +66,23 @@ def main():
|
||||
if len(sys.argv) == 1:
|
||||
# no argument passed
|
||||
parser.print_help()
|
||||
return 0
|
||||
sys.exit(0)
|
||||
|
||||
if args.version:
|
||||
color_print("{}".format(__version__), "GREEN")
|
||||
return 0
|
||||
sys.exit(0)
|
||||
|
||||
if args.validate:
|
||||
validate_json_file(args.validate)
|
||||
return 0
|
||||
sys.exit(0)
|
||||
if args.prettify:
|
||||
prettify_json_file(args.prettify)
|
||||
return 0
|
||||
sys.exit(0)
|
||||
|
||||
project_name = args.startproject
|
||||
if project_name:
|
||||
create_scaffold(project_name)
|
||||
return 0
|
||||
sys.exit(0)
|
||||
|
||||
runner = HttpRunner(
|
||||
failfast=args.failfast,
|
||||
@@ -104,10 +106,10 @@ def main():
|
||||
except Exception as ex:
|
||||
color_print("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage), "YELLOW")
|
||||
capture_exception(ex)
|
||||
raise
|
||||
err_code = 1
|
||||
|
||||
return err_code
|
||||
sys.exit(err_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
||||
@@ -14,6 +14,74 @@ from httprunner.utils import lower_dict_keys, omit_long_data
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
def get_req_resp_record(resp_obj):
|
||||
""" get request and response info from Response() object.
|
||||
"""
|
||||
def log_print(req_resp_dict, r_type):
|
||||
msg = "\n================== {} details ==================\n".format(r_type)
|
||||
for key, value in req_resp_dict[r_type].items():
|
||||
msg += "{:<16} : {}\n".format(key, repr(value))
|
||||
logger.log_debug(msg)
|
||||
|
||||
req_resp_dict = {
|
||||
"request": {},
|
||||
"response": {}
|
||||
}
|
||||
|
||||
# record actual request info
|
||||
req_resp_dict["request"]["url"] = resp_obj.request.url
|
||||
req_resp_dict["request"]["method"] = resp_obj.request.method
|
||||
req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers)
|
||||
|
||||
request_body = resp_obj.request.body
|
||||
if request_body:
|
||||
request_content_type = lower_dict_keys(
|
||||
req_resp_dict["request"]["headers"]
|
||||
).get("content-type")
|
||||
if request_content_type and "multipart/form-data" in request_content_type:
|
||||
# upload file type
|
||||
req_resp_dict["request"]["body"] = "upload file stream (OMITTED)"
|
||||
else:
|
||||
req_resp_dict["request"]["body"] = request_body
|
||||
|
||||
# log request details in debug mode
|
||||
log_print(req_resp_dict, "request")
|
||||
|
||||
# record response info
|
||||
req_resp_dict["response"]["ok"] = resp_obj.ok
|
||||
req_resp_dict["response"]["url"] = resp_obj.url
|
||||
req_resp_dict["response"]["status_code"] = resp_obj.status_code
|
||||
req_resp_dict["response"]["reason"] = resp_obj.reason
|
||||
req_resp_dict["response"]["cookies"] = resp_obj.cookies or {}
|
||||
req_resp_dict["response"]["encoding"] = resp_obj.encoding
|
||||
resp_headers = dict(resp_obj.headers)
|
||||
req_resp_dict["response"]["headers"] = resp_headers
|
||||
|
||||
lower_resp_headers = lower_dict_keys(resp_headers)
|
||||
content_type = lower_resp_headers.get("content-type", "")
|
||||
req_resp_dict["response"]["content_type"] = content_type
|
||||
|
||||
if "image" in content_type:
|
||||
# response is image type, record bytes content only
|
||||
req_resp_dict["response"]["body"] = resp_obj.content
|
||||
else:
|
||||
try:
|
||||
# try to record json data
|
||||
if isinstance(resp_obj, response.ResponseObject):
|
||||
req_resp_dict["response"]["body"] = resp_obj.json
|
||||
else:
|
||||
req_resp_dict["response"]["body"] = resp_obj.json()
|
||||
except ValueError:
|
||||
# only record at most 512 text charactors
|
||||
resp_text = resp_obj.text
|
||||
req_resp_dict["response"]["body"] = omit_long_data(resp_text)
|
||||
|
||||
# log response details in debug mode
|
||||
log_print(req_resp_dict, "response")
|
||||
|
||||
return req_resp_dict
|
||||
|
||||
|
||||
class ApiResponse(Response):
|
||||
|
||||
def raise_for_status(self):
|
||||
@@ -62,79 +130,12 @@ class HttpSession(requests.Session):
|
||||
}
|
||||
}
|
||||
|
||||
def get_req_resp_record(self, resp_obj):
|
||||
""" get request and response info from Response() object.
|
||||
"""
|
||||
def log_print(req_resp_dict, r_type):
|
||||
msg = "\n================== {} details ==================\n".format(r_type)
|
||||
for key, value in req_resp_dict[r_type].items():
|
||||
msg += "{:<16} : {}\n".format(key, repr(value))
|
||||
logger.log_debug(msg)
|
||||
|
||||
req_resp_dict = {
|
||||
"request": {},
|
||||
"response": {}
|
||||
}
|
||||
|
||||
# record actual request info
|
||||
req_resp_dict["request"]["url"] = resp_obj.request.url
|
||||
req_resp_dict["request"]["method"] = resp_obj.request.method
|
||||
req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers)
|
||||
|
||||
request_body = resp_obj.request.body
|
||||
if request_body:
|
||||
request_content_type = lower_dict_keys(
|
||||
req_resp_dict["request"]["headers"]
|
||||
).get("content-type")
|
||||
if request_content_type and "multipart/form-data" in request_content_type:
|
||||
# upload file type
|
||||
req_resp_dict["request"]["body"] = "upload file stream (OMITTED)"
|
||||
else:
|
||||
req_resp_dict["request"]["body"] = request_body
|
||||
|
||||
# log request details in debug mode
|
||||
log_print(req_resp_dict, "request")
|
||||
|
||||
# record response info
|
||||
req_resp_dict["response"]["ok"] = resp_obj.ok
|
||||
req_resp_dict["response"]["url"] = resp_obj.url
|
||||
req_resp_dict["response"]["status_code"] = resp_obj.status_code
|
||||
req_resp_dict["response"]["reason"] = resp_obj.reason
|
||||
req_resp_dict["response"]["cookies"] = resp_obj.cookies or {}
|
||||
req_resp_dict["response"]["encoding"] = resp_obj.encoding
|
||||
resp_headers = dict(resp_obj.headers)
|
||||
req_resp_dict["response"]["headers"] = resp_headers
|
||||
|
||||
lower_resp_headers = lower_dict_keys(resp_headers)
|
||||
content_type = lower_resp_headers.get("content-type", "")
|
||||
req_resp_dict["response"]["content_type"] = content_type
|
||||
|
||||
if "image" in content_type:
|
||||
# response is image type, record bytes content only
|
||||
req_resp_dict["response"]["body"] = resp_obj.content
|
||||
else:
|
||||
try:
|
||||
# try to record json data
|
||||
if isinstance(resp_obj, response.ResponseObject):
|
||||
req_resp_dict["response"]["body"] = resp_obj.json
|
||||
else:
|
||||
req_resp_dict["response"]["body"] = resp_obj.json()
|
||||
except ValueError:
|
||||
# only record at most 512 text charactors
|
||||
resp_text = resp_obj.text
|
||||
req_resp_dict["response"]["body"] = omit_long_data(resp_text)
|
||||
|
||||
# log response details in debug mode
|
||||
log_print(req_resp_dict, "response")
|
||||
|
||||
return req_resp_dict
|
||||
|
||||
def update_last_req_resp_record(self, resp_obj):
|
||||
"""
|
||||
update request and response info from Response() object.
|
||||
"""
|
||||
self.meta_data["data"].pop()
|
||||
self.meta_data["data"].append(self.get_req_resp_record(resp_obj))
|
||||
self.meta_data["data"].append(get_req_resp_record(resp_obj))
|
||||
|
||||
def request(self, method, url, name=None, **kwargs):
|
||||
"""
|
||||
@@ -207,7 +208,7 @@ class HttpSession(requests.Session):
|
||||
# record request and response histories, include 30X redirection
|
||||
response_list = response.history + [response]
|
||||
self.meta_data["data"] = [
|
||||
self.get_req_resp_record(resp_obj)
|
||||
get_req_resp_record(resp_obj)
|
||||
for resp_obj in response_list
|
||||
]
|
||||
|
||||
|
||||
@@ -6,18 +6,23 @@ from httprunner.compat import JSONDecodeError, FileNotFoundError
|
||||
these exceptions will mark test as failure
|
||||
"""
|
||||
|
||||
|
||||
class MyBaseFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValidationFailure(MyBaseFailure):
|
||||
pass
|
||||
|
||||
|
||||
class ExtractFailure(MyBaseFailure):
|
||||
pass
|
||||
|
||||
|
||||
class SetupHooksFailure(MyBaseFailure):
|
||||
pass
|
||||
|
||||
|
||||
class TeardownHooksFailure(MyBaseFailure):
|
||||
pass
|
||||
|
||||
@@ -26,35 +31,51 @@ class TeardownHooksFailure(MyBaseFailure):
|
||||
these exceptions will mark test as error
|
||||
"""
|
||||
|
||||
|
||||
class MyBaseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FileFormatError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class ParamsError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(FileNotFoundError, NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class FunctionNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class VariableNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class EnvNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class CSVNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class ApiNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class TestcaseNotFound(NotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class SummaryEmpty(MyBaseError):
|
||||
""" test result summary data is empty
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,14 @@ variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
|
||||
# function notation, e.g. ${func1($var_1, $var_3)}
|
||||
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
|
||||
|
||||
""" Store parse failed api/testcase/testsuite file path
|
||||
"""
|
||||
parse_failed_testfiles = {}
|
||||
|
||||
|
||||
def get_parse_failed_testfiles():
|
||||
return parse_failed_testfiles
|
||||
|
||||
|
||||
def parse_string_value(str_value):
|
||||
""" parse string to number if possible
|
||||
@@ -1145,6 +1153,8 @@ def __prepare_testcase_tests(tests, config, project_mapping, session_variables_s
|
||||
|
||||
# 3, testcase_def config => testcase_def test_dict
|
||||
test_dict = _parse_testcase(test_dict, project_mapping, session_variables_set)
|
||||
if not test_dict:
|
||||
continue
|
||||
|
||||
elif "api_def" in test_dict:
|
||||
# test_dict has API reference
|
||||
@@ -1216,21 +1226,34 @@ def _parse_testcase(testcase, project_mapping, session_variables_set=None):
|
||||
|
||||
"""
|
||||
testcase.setdefault("config", {})
|
||||
prepared_config = __prepare_config(
|
||||
testcase["config"],
|
||||
project_mapping,
|
||||
session_variables_set
|
||||
)
|
||||
prepared_testcase_tests = __prepare_testcase_tests(
|
||||
testcase["teststeps"],
|
||||
prepared_config,
|
||||
project_mapping,
|
||||
session_variables_set
|
||||
)
|
||||
return {
|
||||
"config": prepared_config,
|
||||
"teststeps": prepared_testcase_tests
|
||||
}
|
||||
|
||||
try:
|
||||
prepared_config = __prepare_config(
|
||||
testcase["config"],
|
||||
project_mapping,
|
||||
session_variables_set
|
||||
)
|
||||
prepared_testcase_tests = __prepare_testcase_tests(
|
||||
testcase["teststeps"],
|
||||
prepared_config,
|
||||
project_mapping,
|
||||
session_variables_set
|
||||
)
|
||||
return {
|
||||
"config": prepared_config,
|
||||
"teststeps": prepared_testcase_tests
|
||||
}
|
||||
except (exceptions.MyBaseFailure, exceptions.MyBaseError):
|
||||
testcase_type = testcase["type"]
|
||||
testcase_path = testcase.get("path")
|
||||
|
||||
global parse_failed_testfiles
|
||||
if testcase_type not in parse_failed_testfiles:
|
||||
parse_failed_testfiles[testcase_type] = []
|
||||
|
||||
parse_failed_testfiles[testcase_type].append(testcase_path)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mapping):
|
||||
@@ -1286,6 +1309,7 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
|
||||
parsed_testcase = testcase.pop("testcase_def")
|
||||
parsed_testcase.setdefault("config", {})
|
||||
parsed_testcase["path"] = testcase["testcase"]
|
||||
parsed_testcase["type"] = "testcase"
|
||||
parsed_testcase["config"]["name"] = testcase_name
|
||||
|
||||
if "weight" in testcase:
|
||||
@@ -1331,6 +1355,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
|
||||
parameter_variables
|
||||
)
|
||||
parsed_testcase_copied = _parse_testcase(testcase_copied, project_mapping)
|
||||
if not parsed_testcase_copied:
|
||||
continue
|
||||
parsed_testcase_copied["config"]["name"] = parse_lazy_data(
|
||||
parsed_testcase_copied["config"]["name"],
|
||||
testcase_copied["config"]["variables"]
|
||||
@@ -1339,6 +1365,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
|
||||
|
||||
else:
|
||||
parsed_testcase = _parse_testcase(parsed_testcase, project_mapping)
|
||||
if not parsed_testcase:
|
||||
continue
|
||||
parsed_testcase_list.append(parsed_testcase)
|
||||
|
||||
return parsed_testcase_list
|
||||
@@ -1435,7 +1463,10 @@ def parse_tests(tests_mapping):
|
||||
|
||||
elif test_type == "testcases":
|
||||
for testcase in tests_mapping["testcases"]:
|
||||
testcase["type"] = "testcase"
|
||||
parsed_testcase = _parse_testcase(testcase, project_mapping)
|
||||
if not parsed_testcase:
|
||||
continue
|
||||
testcases.append(parsed_testcase)
|
||||
|
||||
elif test_type == "apis":
|
||||
@@ -1445,9 +1476,13 @@ def parse_tests(tests_mapping):
|
||||
"config": {
|
||||
"name": api_content.get("name")
|
||||
},
|
||||
"teststeps": [api_content]
|
||||
"teststeps": [api_content],
|
||||
"path": api_content.pop("path", None),
|
||||
"type": api_content.pop("type", "api")
|
||||
}
|
||||
parsed_testcase = _parse_testcase(testcase, project_mapping)
|
||||
if not parsed_testcase:
|
||||
continue
|
||||
testcases.append(parsed_testcase)
|
||||
|
||||
return testcases
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import datetime
|
||||
from jinja2 import Template
|
||||
|
||||
from httprunner import logger
|
||||
from httprunner.exceptions import SummaryEmpty
|
||||
|
||||
|
||||
def gen_html_report(summary, report_template=None, report_dir=None, report_file=None):
|
||||
@@ -17,6 +18,10 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
|
||||
report_file (str): specify html report file path, this has higher priority than specifying report dir.
|
||||
|
||||
"""
|
||||
if not summary["time"] or summary["stat"]["testcases"]["total"] == 0:
|
||||
logger.log_error("test result summary is empty ! {}".format(summary))
|
||||
raise SummaryEmpty
|
||||
|
||||
if not report_template:
|
||||
report_template = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
|
||||
@@ -222,7 +222,8 @@ class ResponseObject(object):
|
||||
# others
|
||||
else:
|
||||
err_msg = u"Failed to extract attribute from response! => {}\n".format(field)
|
||||
err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n"
|
||||
err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, " \
|
||||
u"text, json, encoding, ok, reason, url.\n\n"
|
||||
err_msg += u"If you want to set attribute in teardown_hooks, take the following example as reference:\n"
|
||||
err_msg += u"response.new_attribute = 'new_attribute_value'\n"
|
||||
logger.log_error(err_msg)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from enum import Enum
|
||||
from unittest.case import SkipTest
|
||||
|
||||
from httprunner import exceptions, logger, response, utils
|
||||
@@ -8,6 +9,11 @@ from httprunner.context import SessionContext
|
||||
from httprunner.validator import Validator
|
||||
|
||||
|
||||
class HookTypeEnum(Enum):
|
||||
SETUP = 1
|
||||
TEARDOWN = 2
|
||||
|
||||
|
||||
class Runner(object):
|
||||
""" Running testcases.
|
||||
|
||||
@@ -74,11 +80,11 @@ class Runner(object):
|
||||
self.session_context = SessionContext(config_variables)
|
||||
|
||||
if testcase_setup_hooks:
|
||||
self.do_hook_actions(testcase_setup_hooks, "setup")
|
||||
self.do_hook_actions(testcase_setup_hooks, HookTypeEnum.SETUP)
|
||||
|
||||
def __del__(self):
|
||||
if self.testcase_teardown_hooks:
|
||||
self.do_hook_actions(self.testcase_teardown_hooks, "teardown")
|
||||
self.do_hook_actions(self.testcase_teardown_hooks, HookTypeEnum.TEARDOWN)
|
||||
|
||||
def __clear_test_data(self):
|
||||
""" clear request and response data
|
||||
@@ -131,10 +137,10 @@ class Runner(object):
|
||||
format2 (str): only call hook functions.
|
||||
${func()}
|
||||
|
||||
hook_type (enum): setup/teardown
|
||||
hook_type (HookTypeEnum): setup/teardown
|
||||
|
||||
"""
|
||||
logger.log_debug("call {} hook actions.".format(hook_type))
|
||||
logger.log_debug("call {} hook actions.".format(hook_type.name))
|
||||
for action in actions:
|
||||
|
||||
if isinstance(action, dict) and len(action) == 1:
|
||||
@@ -215,7 +221,7 @@ class Runner(object):
|
||||
# setup hooks
|
||||
setup_hooks = test_dict.get("setup_hooks", [])
|
||||
if setup_hooks:
|
||||
self.do_hook_actions(setup_hooks, "setup")
|
||||
self.do_hook_actions(setup_hooks, HookTypeEnum.SETUP)
|
||||
|
||||
try:
|
||||
method = parsed_test_request.pop('method')
|
||||
@@ -245,32 +251,7 @@ class Runner(object):
|
||||
)
|
||||
resp_obj = response.ResponseObject(resp)
|
||||
|
||||
# teardown hooks
|
||||
teardown_hooks = test_dict.get("teardown_hooks", [])
|
||||
if teardown_hooks:
|
||||
self.session_context.update_test_variables("response", resp_obj)
|
||||
self.do_hook_actions(teardown_hooks, "teardown")
|
||||
self.http_client_session.update_last_req_resp_record(resp_obj)
|
||||
|
||||
# extract
|
||||
extractors = test_dict.get("extract", {})
|
||||
extracted_variables_mapping = resp_obj.extract_response(extractors)
|
||||
self.session_context.update_session_variables(extracted_variables_mapping)
|
||||
|
||||
# validate
|
||||
validators = test_dict.get("validate") or test_dict.get("validators") or []
|
||||
validate_script = test_dict.get("validate_script", [])
|
||||
if validate_script:
|
||||
validators.append({
|
||||
"type": "python_script",
|
||||
"script": validate_script
|
||||
})
|
||||
|
||||
validator = Validator(self.session_context, resp_obj)
|
||||
try:
|
||||
validator.validate(validators)
|
||||
except (exceptions.ParamsError,
|
||||
exceptions.ValidationFailure, exceptions.ExtractFailure):
|
||||
def log_req_resp_details():
|
||||
err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
||||
|
||||
# log request
|
||||
@@ -291,12 +272,39 @@ class Runner(object):
|
||||
err_msg += "body: {}\n".format(repr(resp_obj.text))
|
||||
logger.log_error(err_msg)
|
||||
|
||||
# teardown hooks
|
||||
teardown_hooks = test_dict.get("teardown_hooks", [])
|
||||
if teardown_hooks:
|
||||
self.session_context.update_test_variables("response", resp_obj)
|
||||
self.do_hook_actions(teardown_hooks, HookTypeEnum.TEARDOWN)
|
||||
self.http_client_session.update_last_req_resp_record(resp_obj)
|
||||
|
||||
# extract
|
||||
extractors = test_dict.get("extract", {})
|
||||
try:
|
||||
extracted_variables_mapping = resp_obj.extract_response(extractors)
|
||||
self.session_context.update_session_variables(extracted_variables_mapping)
|
||||
except (exceptions.ParamsError, exceptions.ExtractFailure):
|
||||
log_req_resp_details()
|
||||
raise
|
||||
|
||||
finally:
|
||||
# get request/response data and validate results
|
||||
self.meta_datas = getattr(self.http_client_session, "meta_data", {})
|
||||
self.meta_datas["validators"] = validator.validation_results
|
||||
# validate
|
||||
validators = test_dict.get("validate") or test_dict.get("validators") or []
|
||||
validate_script = test_dict.get("validate_script", [])
|
||||
if validate_script:
|
||||
validators.append({
|
||||
"type": "python_script",
|
||||
"script": validate_script
|
||||
})
|
||||
|
||||
validator = Validator(self.session_context, resp_obj)
|
||||
try:
|
||||
validator.validate(validators)
|
||||
except exceptions.ValidationFailure:
|
||||
log_req_resp_details()
|
||||
raise
|
||||
|
||||
return validator.validation_results
|
||||
|
||||
def _run_testcase(self, testcase_dict):
|
||||
""" run single testcase.
|
||||
@@ -374,13 +382,18 @@ class Runner(object):
|
||||
self._run_testcase(test_dict)
|
||||
else:
|
||||
# api
|
||||
validation_results = {}
|
||||
try:
|
||||
self._run_test(test_dict)
|
||||
validation_results = self._run_test(test_dict)
|
||||
except Exception:
|
||||
# log exception request_type and name for locust stat
|
||||
self.exception_request_type = test_dict["request"]["method"]
|
||||
self.exception_name = test_dict.get("name")
|
||||
raise
|
||||
finally:
|
||||
# get request/response data and validate results
|
||||
self.meta_datas = getattr(self.http_client_session, "meta_data", {})
|
||||
self.meta_datas["validators"] = validation_results
|
||||
|
||||
def export_variables(self, output_variables_list):
|
||||
""" export current testcase variables
|
||||
|
||||
@@ -571,6 +571,10 @@ def dump_json_file(json_data, json_file_abs_path):
|
||||
except TypeError:
|
||||
return str(obj)
|
||||
|
||||
file_foder_path = os.path.dirname(json_file_abs_path)
|
||||
if not os.path.isdir(file_foder_path):
|
||||
os.makedirs(file_foder_path)
|
||||
|
||||
try:
|
||||
with io.open(json_file_abs_path, 'w', encoding='utf-8') as outfile:
|
||||
if is_py2:
|
||||
@@ -627,9 +631,6 @@ def prepare_dump_json_file_abs_path(project_mapping, tag_name):
|
||||
test_file_name, _file_suffix = os.path.splitext(test_file)
|
||||
dump_file_name = "{}.{}.json".format(test_file_name, tag_name)
|
||||
|
||||
if not os.path.isdir(file_foder_path):
|
||||
os.makedirs(file_foder_path)
|
||||
|
||||
dumped_json_file_abs_path = os.path.join(file_foder_path, dump_file_name)
|
||||
return dumped_json_file_abs_path
|
||||
|
||||
|
||||
17
poetry.lock
generated
17
poetry.lock
generated
@@ -49,6 +49,15 @@ optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
|
||||
version = "4.5.4"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4"
|
||||
marker = "python_version >= \"2.7\" and python_version < \"2.8\""
|
||||
name = "enum34"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.1.6"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
||||
@@ -227,7 +236,7 @@ termcolor = ["termcolor"]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "3a262aa9fb64682ee5fcecc0e72249d0a549b78c697fce7bbabc79648d615fef"
|
||||
content-hash = "7b478db27fe6f36aeed7f90b6c67efe5903fb43bb899bb66a1a65b80b8637c5a"
|
||||
python-versions = "~2.7 || ^3.5"
|
||||
|
||||
[metadata.files]
|
||||
@@ -285,6 +294,12 @@ coverage = [
|
||||
{file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"},
|
||||
{file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"},
|
||||
]
|
||||
enum34 = [
|
||||
{file = "enum34-1.1.6-py2-none-any.whl", hash = "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79"},
|
||||
{file = "enum34-1.1.6-py3-none-any.whl", hash = "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a"},
|
||||
{file = "enum34-1.1.6.tar.gz", hash = "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"},
|
||||
{file = "enum34-1.1.6.zip", hash = "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850"},
|
||||
]
|
||||
filetype = [
|
||||
{file = "filetype-1.0.5-py2.py3-none-any.whl", hash = "sha256:4967124d982a71700d94a08c49c4926423500e79382a92070f5ab248d44fe461"},
|
||||
{file = "filetype-1.0.5.tar.gz", hash = "sha256:17a3b885f19034da29640b083d767e0f13c2dcb5dcc267945c8b6e5a5a9013c7"},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "httprunner"
|
||||
version = "2.4.7"
|
||||
version = "2.4.8"
|
||||
description = "One-stop solution for HTTP(S) testing."
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
authors = ["debugtalk <debugtalk@gmail.com>"]
|
||||
|
||||
homepage = "https://github.com/HttpRunner/HttpRunner"
|
||||
repository = "https://github.com/HttpRunner/HttpRunner"
|
||||
homepage = "https://github.com/httprunner/httprunner"
|
||||
repository = "https://github.com/httprunner/httprunner"
|
||||
documentation = "https://docs.httprunner.org"
|
||||
|
||||
keywords = ["HTTP", "api", "test", "requests", "locustio"]
|
||||
@@ -27,7 +27,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.8"
|
||||
]
|
||||
|
||||
include = ["CHANGELOG.md", "httprunner/static/*"]
|
||||
include = ["docs/CHANGELOG.md"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "~2.7 || ^3.5"
|
||||
@@ -40,8 +40,9 @@ colorama = "^0.4.1"
|
||||
colorlog = "^4.0.2"
|
||||
filetype = "^1.0.5"
|
||||
jsonpath = "^0.82"
|
||||
future = { version = "^0.18.1", python = "~2.7" }
|
||||
sentry-sdk = "^0.13.5"
|
||||
future = { version = "^0.18.1", python = "~2.7" }
|
||||
enum34 = { version = "^1.1.6", python = "~2.7" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
flask = "<1.0.0"
|
||||
|
||||
@@ -1206,8 +1206,9 @@ class TestParser(unittest.TestCase):
|
||||
}
|
||||
]
|
||||
}
|
||||
with self.assertRaises(exceptions.VariableNotFound):
|
||||
parser.parse_tests(tests_mapping)
|
||||
parser.parse_tests(tests_mapping)
|
||||
parse_failed_testfiles = parser.get_parse_failed_testfiles()
|
||||
self.assertIn("testcase", parse_failed_testfiles)
|
||||
|
||||
def test_parse_tests_base_url_teststep_empty(self):
|
||||
""" base_url & verify: priority test_dict > config
|
||||
|
||||
Reference in New Issue
Block a user