diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ebfe8ca4..6fb2d697 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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** diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 209b37a6..1766552f 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.4.7" +__version__ = "2.4.8" __description__ = "One-stop solution for HTTP(S) testing." __all__ = ["__version__", "__description__"] diff --git a/httprunner/__main__.py b/httprunner/__main__.py index 70236a86..6cc9a149 100644 --- a/httprunner/__main__.py +++ b/httprunner/__main__.py @@ -1,6 +1,5 @@ -import sys from httprunner.cli import main if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/httprunner/api.py b/httprunner/api.py index 7e017213..cbb7fc0f 100644 --- a/httprunner/api.py +++ b/httprunner/api.py @@ -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 diff --git a/httprunner/cli.py b/httprunner/cli.py index dcabb184..50e365d5 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -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() diff --git a/httprunner/client.py b/httprunner/client.py index 1b5676e1..cbef3a32 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -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 ] diff --git a/httprunner/exceptions.py b/httprunner/exceptions.py index 787e864d..ad05ea53 100644 --- a/httprunner/exceptions.py +++ b/httprunner/exceptions.py @@ -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 + """ diff --git a/httprunner/parser.py b/httprunner/parser.py index ed642d0c..d5ef8214 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -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 diff --git a/httprunner/report/html/gen_report.py b/httprunner/report/html/gen_report.py index dc23d5af..43092849 100644 --- a/httprunner/report/html/gen_report.py +++ b/httprunner/report/html/gen_report.py @@ -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__)), diff --git a/httprunner/response.py b/httprunner/response.py index 9de3a8cd..62aae114 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -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) diff --git a/httprunner/runner.py b/httprunner/runner.py index 6aa41fe9..86804722 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -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 diff --git a/httprunner/utils.py b/httprunner/utils.py index 5124290d..67b972e8 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 9c336eb7..fe5355d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 363bf55f..e0b6f378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] -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" diff --git a/tests/test_parser.py b/tests/test_parser.py index fa9b248b..67356d6f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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