mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-12 16:01:27 +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
|
# 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)
|
## 2.4.7 (2019-12-24)
|
||||||
|
|
||||||
**Added**
|
**Added**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = "2.4.7"
|
__version__ = "2.4.8"
|
||||||
__description__ = "One-stop solution for HTTP(S) testing."
|
__description__ = "One-stop solution for HTTP(S) testing."
|
||||||
|
|
||||||
__all__ = ["__version__", "__description__"]
|
__all__ = ["__version__", "__description__"]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import sys
|
|
||||||
from httprunner.cli import main
|
from httprunner.cli import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
main()
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ class HttpRunner(object):
|
|||||||
# parse tests
|
# parse tests
|
||||||
self.exception_stage = "parse tests"
|
self.exception_stage = "parse tests"
|
||||||
parsed_testcases = parser.parse_tests(tests_mapping)
|
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:
|
if self.save_tests:
|
||||||
utils.dump_logs(parsed_testcases, project_mapping, "parsed")
|
utils.dump_logs(parsed_testcases, project_mapping, "parsed")
|
||||||
@@ -277,6 +281,8 @@ class HttpRunner(object):
|
|||||||
path_or_tests:
|
path_or_tests:
|
||||||
str: testcase/testsuite file/foler path
|
str: testcase/testsuite file/foler path
|
||||||
dict: valid testcase/testsuite data
|
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:
|
Returns:
|
||||||
dict: result summary
|
dict: result summary
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
from httprunner import __description__, __version__
|
from httprunner import __description__, __version__
|
||||||
from httprunner.api import HttpRunner
|
from httprunner.api import HttpRunner
|
||||||
from httprunner.compat import is_py2
|
from httprunner.compat import is_py2
|
||||||
@@ -64,23 +66,23 @@ def main():
|
|||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
# no argument passed
|
# no argument passed
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 0
|
sys.exit(0)
|
||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
color_print("{}".format(__version__), "GREEN")
|
color_print("{}".format(__version__), "GREEN")
|
||||||
return 0
|
sys.exit(0)
|
||||||
|
|
||||||
if args.validate:
|
if args.validate:
|
||||||
validate_json_file(args.validate)
|
validate_json_file(args.validate)
|
||||||
return 0
|
sys.exit(0)
|
||||||
if args.prettify:
|
if args.prettify:
|
||||||
prettify_json_file(args.prettify)
|
prettify_json_file(args.prettify)
|
||||||
return 0
|
sys.exit(0)
|
||||||
|
|
||||||
project_name = args.startproject
|
project_name = args.startproject
|
||||||
if project_name:
|
if project_name:
|
||||||
create_scaffold(project_name)
|
create_scaffold(project_name)
|
||||||
return 0
|
sys.exit(0)
|
||||||
|
|
||||||
runner = HttpRunner(
|
runner = HttpRunner(
|
||||||
failfast=args.failfast,
|
failfast=args.failfast,
|
||||||
@@ -104,10 +106,10 @@ def main():
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
color_print("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage), "YELLOW")
|
color_print("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage), "YELLOW")
|
||||||
capture_exception(ex)
|
capture_exception(ex)
|
||||||
raise
|
err_code = 1
|
||||||
|
|
||||||
return err_code
|
sys.exit(err_code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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)
|
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):
|
class ApiResponse(Response):
|
||||||
|
|
||||||
def raise_for_status(self):
|
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):
|
def update_last_req_resp_record(self, resp_obj):
|
||||||
"""
|
"""
|
||||||
update request and response info from Response() object.
|
update request and response info from Response() object.
|
||||||
"""
|
"""
|
||||||
self.meta_data["data"].pop()
|
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):
|
def request(self, method, url, name=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -207,7 +208,7 @@ class HttpSession(requests.Session):
|
|||||||
# record request and response histories, include 30X redirection
|
# record request and response histories, include 30X redirection
|
||||||
response_list = response.history + [response]
|
response_list = response.history + [response]
|
||||||
self.meta_data["data"] = [
|
self.meta_data["data"] = [
|
||||||
self.get_req_resp_record(resp_obj)
|
get_req_resp_record(resp_obj)
|
||||||
for resp_obj in response_list
|
for resp_obj in response_list
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ from httprunner.compat import JSONDecodeError, FileNotFoundError
|
|||||||
these exceptions will mark test as failure
|
these exceptions will mark test as failure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MyBaseFailure(Exception):
|
class MyBaseFailure(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidationFailure(MyBaseFailure):
|
class ValidationFailure(MyBaseFailure):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExtractFailure(MyBaseFailure):
|
class ExtractFailure(MyBaseFailure):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SetupHooksFailure(MyBaseFailure):
|
class SetupHooksFailure(MyBaseFailure):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TeardownHooksFailure(MyBaseFailure):
|
class TeardownHooksFailure(MyBaseFailure):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -26,35 +31,51 @@ class TeardownHooksFailure(MyBaseFailure):
|
|||||||
these exceptions will mark test as error
|
these exceptions will mark test as error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class MyBaseError(Exception):
|
class MyBaseError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileFormatError(MyBaseError):
|
class FileFormatError(MyBaseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ParamsError(MyBaseError):
|
class ParamsError(MyBaseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotFoundError(MyBaseError):
|
class NotFoundError(MyBaseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileNotFound(FileNotFoundError, NotFoundError):
|
class FileNotFound(FileNotFoundError, NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FunctionNotFound(NotFoundError):
|
class FunctionNotFound(NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VariableNotFound(NotFoundError):
|
class VariableNotFound(NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EnvNotFound(NotFoundError):
|
class EnvNotFound(NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CSVNotFound(NotFoundError):
|
class CSVNotFound(NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ApiNotFound(NotFoundError):
|
class ApiNotFound(NotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestcaseNotFound(NotFoundError):
|
class TestcaseNotFound(NotFoundError):
|
||||||
pass
|
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 notation, e.g. ${func1($var_1, $var_3)}
|
||||||
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
|
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):
|
def parse_string_value(str_value):
|
||||||
""" parse string to number if possible
|
""" 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
|
# 3, testcase_def config => testcase_def test_dict
|
||||||
test_dict = _parse_testcase(test_dict, project_mapping, session_variables_set)
|
test_dict = _parse_testcase(test_dict, project_mapping, session_variables_set)
|
||||||
|
if not test_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
elif "api_def" in test_dict:
|
elif "api_def" in test_dict:
|
||||||
# test_dict has API reference
|
# test_dict has API reference
|
||||||
@@ -1216,21 +1226,34 @@ def _parse_testcase(testcase, project_mapping, session_variables_set=None):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
testcase.setdefault("config", {})
|
testcase.setdefault("config", {})
|
||||||
prepared_config = __prepare_config(
|
|
||||||
testcase["config"],
|
try:
|
||||||
project_mapping,
|
prepared_config = __prepare_config(
|
||||||
session_variables_set
|
testcase["config"],
|
||||||
)
|
project_mapping,
|
||||||
prepared_testcase_tests = __prepare_testcase_tests(
|
session_variables_set
|
||||||
testcase["teststeps"],
|
)
|
||||||
prepared_config,
|
prepared_testcase_tests = __prepare_testcase_tests(
|
||||||
project_mapping,
|
testcase["teststeps"],
|
||||||
session_variables_set
|
prepared_config,
|
||||||
)
|
project_mapping,
|
||||||
return {
|
session_variables_set
|
||||||
"config": prepared_config,
|
)
|
||||||
"teststeps": prepared_testcase_tests
|
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):
|
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 = testcase.pop("testcase_def")
|
||||||
parsed_testcase.setdefault("config", {})
|
parsed_testcase.setdefault("config", {})
|
||||||
parsed_testcase["path"] = testcase["testcase"]
|
parsed_testcase["path"] = testcase["testcase"]
|
||||||
|
parsed_testcase["type"] = "testcase"
|
||||||
parsed_testcase["config"]["name"] = testcase_name
|
parsed_testcase["config"]["name"] = testcase_name
|
||||||
|
|
||||||
if "weight" in testcase:
|
if "weight" in testcase:
|
||||||
@@ -1331,6 +1355,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
|
|||||||
parameter_variables
|
parameter_variables
|
||||||
)
|
)
|
||||||
parsed_testcase_copied = _parse_testcase(testcase_copied, project_mapping)
|
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"] = parse_lazy_data(
|
||||||
parsed_testcase_copied["config"]["name"],
|
parsed_testcase_copied["config"]["name"],
|
||||||
testcase_copied["config"]["variables"]
|
testcase_copied["config"]["variables"]
|
||||||
@@ -1339,6 +1365,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
parsed_testcase = _parse_testcase(parsed_testcase, project_mapping)
|
parsed_testcase = _parse_testcase(parsed_testcase, project_mapping)
|
||||||
|
if not parsed_testcase:
|
||||||
|
continue
|
||||||
parsed_testcase_list.append(parsed_testcase)
|
parsed_testcase_list.append(parsed_testcase)
|
||||||
|
|
||||||
return parsed_testcase_list
|
return parsed_testcase_list
|
||||||
@@ -1435,7 +1463,10 @@ def parse_tests(tests_mapping):
|
|||||||
|
|
||||||
elif test_type == "testcases":
|
elif test_type == "testcases":
|
||||||
for testcase in tests_mapping["testcases"]:
|
for testcase in tests_mapping["testcases"]:
|
||||||
|
testcase["type"] = "testcase"
|
||||||
parsed_testcase = _parse_testcase(testcase, project_mapping)
|
parsed_testcase = _parse_testcase(testcase, project_mapping)
|
||||||
|
if not parsed_testcase:
|
||||||
|
continue
|
||||||
testcases.append(parsed_testcase)
|
testcases.append(parsed_testcase)
|
||||||
|
|
||||||
elif test_type == "apis":
|
elif test_type == "apis":
|
||||||
@@ -1445,9 +1476,13 @@ def parse_tests(tests_mapping):
|
|||||||
"config": {
|
"config": {
|
||||||
"name": api_content.get("name")
|
"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)
|
parsed_testcase = _parse_testcase(testcase, project_mapping)
|
||||||
|
if not parsed_testcase:
|
||||||
|
continue
|
||||||
testcases.append(parsed_testcase)
|
testcases.append(parsed_testcase)
|
||||||
|
|
||||||
return testcases
|
return testcases
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import datetime
|
|||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
|
||||||
from httprunner import logger
|
from httprunner import logger
|
||||||
|
from httprunner.exceptions import SummaryEmpty
|
||||||
|
|
||||||
|
|
||||||
def gen_html_report(summary, report_template=None, report_dir=None, report_file=None):
|
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.
|
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:
|
if not report_template:
|
||||||
report_template = os.path.join(
|
report_template = os.path.join(
|
||||||
os.path.abspath(os.path.dirname(__file__)),
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ class ResponseObject(object):
|
|||||||
# others
|
# others
|
||||||
else:
|
else:
|
||||||
err_msg = u"Failed to extract attribute from response! => {}\n".format(field)
|
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"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"
|
err_msg += u"response.new_attribute = 'new_attribute_value'\n"
|
||||||
logger.log_error(err_msg)
|
logger.log_error(err_msg)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from unittest.case import SkipTest
|
from unittest.case import SkipTest
|
||||||
|
|
||||||
from httprunner import exceptions, logger, response, utils
|
from httprunner import exceptions, logger, response, utils
|
||||||
@@ -8,6 +9,11 @@ from httprunner.context import SessionContext
|
|||||||
from httprunner.validator import Validator
|
from httprunner.validator import Validator
|
||||||
|
|
||||||
|
|
||||||
|
class HookTypeEnum(Enum):
|
||||||
|
SETUP = 1
|
||||||
|
TEARDOWN = 2
|
||||||
|
|
||||||
|
|
||||||
class Runner(object):
|
class Runner(object):
|
||||||
""" Running testcases.
|
""" Running testcases.
|
||||||
|
|
||||||
@@ -74,11 +80,11 @@ class Runner(object):
|
|||||||
self.session_context = SessionContext(config_variables)
|
self.session_context = SessionContext(config_variables)
|
||||||
|
|
||||||
if testcase_setup_hooks:
|
if testcase_setup_hooks:
|
||||||
self.do_hook_actions(testcase_setup_hooks, "setup")
|
self.do_hook_actions(testcase_setup_hooks, HookTypeEnum.SETUP)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self.testcase_teardown_hooks:
|
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):
|
def __clear_test_data(self):
|
||||||
""" clear request and response data
|
""" clear request and response data
|
||||||
@@ -131,10 +137,10 @@ class Runner(object):
|
|||||||
format2 (str): only call hook functions.
|
format2 (str): only call hook functions.
|
||||||
${func()}
|
${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:
|
for action in actions:
|
||||||
|
|
||||||
if isinstance(action, dict) and len(action) == 1:
|
if isinstance(action, dict) and len(action) == 1:
|
||||||
@@ -215,7 +221,7 @@ class Runner(object):
|
|||||||
# setup hooks
|
# setup hooks
|
||||||
setup_hooks = test_dict.get("setup_hooks", [])
|
setup_hooks = test_dict.get("setup_hooks", [])
|
||||||
if setup_hooks:
|
if setup_hooks:
|
||||||
self.do_hook_actions(setup_hooks, "setup")
|
self.do_hook_actions(setup_hooks, HookTypeEnum.SETUP)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
method = parsed_test_request.pop('method')
|
method = parsed_test_request.pop('method')
|
||||||
@@ -245,32 +251,7 @@ class Runner(object):
|
|||||||
)
|
)
|
||||||
resp_obj = response.ResponseObject(resp)
|
resp_obj = response.ResponseObject(resp)
|
||||||
|
|
||||||
# teardown hooks
|
def log_req_resp_details():
|
||||||
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):
|
|
||||||
err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
||||||
|
|
||||||
# log request
|
# log request
|
||||||
@@ -291,12 +272,39 @@ class Runner(object):
|
|||||||
err_msg += "body: {}\n".format(repr(resp_obj.text))
|
err_msg += "body: {}\n".format(repr(resp_obj.text))
|
||||||
logger.log_error(err_msg)
|
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
|
raise
|
||||||
|
|
||||||
finally:
|
# validate
|
||||||
# get request/response data and validate results
|
validators = test_dict.get("validate") or test_dict.get("validators") or []
|
||||||
self.meta_datas = getattr(self.http_client_session, "meta_data", {})
|
validate_script = test_dict.get("validate_script", [])
|
||||||
self.meta_datas["validators"] = validator.validation_results
|
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):
|
def _run_testcase(self, testcase_dict):
|
||||||
""" run single testcase.
|
""" run single testcase.
|
||||||
@@ -374,13 +382,18 @@ class Runner(object):
|
|||||||
self._run_testcase(test_dict)
|
self._run_testcase(test_dict)
|
||||||
else:
|
else:
|
||||||
# api
|
# api
|
||||||
|
validation_results = {}
|
||||||
try:
|
try:
|
||||||
self._run_test(test_dict)
|
validation_results = self._run_test(test_dict)
|
||||||
except Exception:
|
except Exception:
|
||||||
# log exception request_type and name for locust stat
|
# log exception request_type and name for locust stat
|
||||||
self.exception_request_type = test_dict["request"]["method"]
|
self.exception_request_type = test_dict["request"]["method"]
|
||||||
self.exception_name = test_dict.get("name")
|
self.exception_name = test_dict.get("name")
|
||||||
raise
|
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):
|
def export_variables(self, output_variables_list):
|
||||||
""" export current testcase variables
|
""" export current testcase variables
|
||||||
|
|||||||
@@ -571,6 +571,10 @@ def dump_json_file(json_data, json_file_abs_path):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return str(obj)
|
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:
|
try:
|
||||||
with io.open(json_file_abs_path, 'w', encoding='utf-8') as outfile:
|
with io.open(json_file_abs_path, 'w', encoding='utf-8') as outfile:
|
||||||
if is_py2:
|
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)
|
test_file_name, _file_suffix = os.path.splitext(test_file)
|
||||||
dump_file_name = "{}.{}.json".format(test_file_name, tag_name)
|
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)
|
dumped_json_file_abs_path = os.path.join(file_foder_path, dump_file_name)
|
||||||
return dumped_json_file_abs_path
|
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"
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
|
||||||
version = "4.5.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]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
|
||||||
@@ -227,7 +236,7 @@ termcolor = ["termcolor"]
|
|||||||
watchdog = ["watchdog"]
|
watchdog = ["watchdog"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "3a262aa9fb64682ee5fcecc0e72249d0a549b78c697fce7bbabc79648d615fef"
|
content-hash = "7b478db27fe6f36aeed7f90b6c67efe5903fb43bb899bb66a1a65b80b8637c5a"
|
||||||
python-versions = "~2.7 || ^3.5"
|
python-versions = "~2.7 || ^3.5"
|
||||||
|
|
||||||
[metadata.files]
|
[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-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"},
|
||||||
{file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"},
|
{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 = [
|
filetype = [
|
||||||
{file = "filetype-1.0.5-py2.py3-none-any.whl", hash = "sha256:4967124d982a71700d94a08c49c4926423500e79382a92070f5ab248d44fe461"},
|
{file = "filetype-1.0.5-py2.py3-none-any.whl", hash = "sha256:4967124d982a71700d94a08c49c4926423500e79382a92070f5ab248d44fe461"},
|
||||||
{file = "filetype-1.0.5.tar.gz", hash = "sha256:17a3b885f19034da29640b083d767e0f13c2dcb5dcc267945c8b6e5a5a9013c7"},
|
{file = "filetype-1.0.5.tar.gz", hash = "sha256:17a3b885f19034da29640b083d767e0f13c2dcb5dcc267945c8b6e5a5a9013c7"},
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "httprunner"
|
name = "httprunner"
|
||||||
version = "2.4.7"
|
version = "2.4.8"
|
||||||
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://github.com/httprunner/httprunner"
|
||||||
repository = "https://github.com/HttpRunner/HttpRunner"
|
repository = "https://github.com/httprunner/httprunner"
|
||||||
documentation = "https://docs.httprunner.org"
|
documentation = "https://docs.httprunner.org"
|
||||||
|
|
||||||
keywords = ["HTTP", "api", "test", "requests", "locustio"]
|
keywords = ["HTTP", "api", "test", "requests", "locustio"]
|
||||||
@@ -27,7 +27,7 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.8"
|
"Programming Language :: Python :: 3.8"
|
||||||
]
|
]
|
||||||
|
|
||||||
include = ["CHANGELOG.md", "httprunner/static/*"]
|
include = ["docs/CHANGELOG.md"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "~2.7 || ^3.5"
|
python = "~2.7 || ^3.5"
|
||||||
@@ -40,8 +40,9 @@ colorama = "^0.4.1"
|
|||||||
colorlog = "^4.0.2"
|
colorlog = "^4.0.2"
|
||||||
filetype = "^1.0.5"
|
filetype = "^1.0.5"
|
||||||
jsonpath = "^0.82"
|
jsonpath = "^0.82"
|
||||||
future = { version = "^0.18.1", python = "~2.7" }
|
|
||||||
sentry-sdk = "^0.13.5"
|
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]
|
[tool.poetry.dev-dependencies]
|
||||||
flask = "<1.0.0"
|
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):
|
def test_parse_tests_base_url_teststep_empty(self):
|
||||||
""" base_url & verify: priority test_dict > config
|
""" base_url & verify: priority test_dict > config
|
||||||
|
|||||||
Reference in New Issue
Block a user