diff --git a/httprunner/compat.py b/httprunner/compat.py index 4b0c449a..cef3e0f8 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -217,12 +217,35 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict: def ensure_testcase_v3(test_content: Dict) -> Dict: logger.info("ensure compatibility with testcase format v2") - - v3_content = {"config": test_content["config"], "teststeps": []} + logger.info(f"test_content: {test_content}") + v3_content = {"config": test_content["config"], "teststeps": [], "meta": {"path": ""}} if "teststeps" not in test_content: logger.error(f"Miss teststeps: {test_content}") sys.exit(1) + + # 分析test_content["config"]中的config,如果没有meta + if "meta" not in test_content['config']: + logger.error(f"Miss meta test_file: {test_content['config']['path']}") + logger.error(f"file config content : {test_content['config']}") + sys.exit(1) + else: + # 判断mata中是否有author,description,service,controller没有就抛错 + if "author" not in test_content['config']['meta']: + logger.error(f"Miss author: {test_content}") + sys.exit(1) + if "description" not in test_content['config']['meta']: + logger.error(f"Miss description: {test_content}") + sys.exit(1) + if "service" not in test_content['config']['meta']: + logger.error(f"Miss service: {test_content}") + sys.exit(1) + if "controller" not in test_content['config']['meta']: + logger.error(f"Miss controller: {test_content}") + sys.exit(1) + + v3_content["meta"] = test_content['config']['meta'] + v3_content["meta"]["path"] = convert_relative_project_root_dir(test_content['config']['path']) if not isinstance(test_content["teststeps"], list): logger.error( diff --git a/httprunner/loader.py b/httprunner/loader.py index 6c81fd82..9892ad9e 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -443,5 +443,4 @@ def convert_relative_project_root_dir(abs_path: Text) -> Text: f"abs_path: {abs_path}\n" f"project_meta.RootDir: {_project_meta.RootDir}" ) - return abs_path[len(_project_meta.RootDir) + 1:] diff --git a/httprunner/make.py b/httprunner/make.py index db493144..22e2bd47 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -25,6 +25,8 @@ pytest_files_made_cache_mapping: Dict[Text, Text] = {} """ save generated pytest files to run, except referenced testcase """ pytest_files_run_set: Set = set() +"""用来记录make之后的所有case 元数据,做统计使用""" +pytest_files_meta_cases_list: List = [] __TEMPLATE__ = jinja2.Template( """# NOTE: Generated By HttpRunner v{{ version }} @@ -36,7 +38,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %})) {% endif %} -{% if parameters or skip %} +{% if parameters or skip or marks %} import pytest {% endif %} @@ -50,9 +52,20 @@ from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase {% endfor %} class {{ class_name }}(HttpRunner): - {% if parameters and skip %} + {% if parameters and skip and marks%} @pytest.mark.parametrize("param", Parameters({{parameters}})) - @pytest.mark.skip(reason={{ skip }}) + @pytest.mark.skip(reason="{{ skip }}") + {% for mark in marks %} + @pytest.mark.{{ mark }} + {% endfor %} + def test_start(self, param): + super().test_start(param) + + {% elif parameters and marks %} + @pytest.mark.parametrize("param", Parameters({{parameters}})) + {% for mark in marks %} + @pytest.mark.{{ mark }} + {% endfor %} def test_start(self, param): super().test_start(param) @@ -61,8 +74,15 @@ class {{ class_name }}(HttpRunner): def test_start(self, param): super().test_start(param) + {% elif marks %} + {% for mark in marks %} + @pytest.mark.{{ mark }} + {% endfor %} + def test_start(self): + super().test_start() + {% elif skip %} - @pytest.mark.skip(reason={{ skip }}) + @pytest.mark.skip(reason="{{ skip }}") def test_start(self): super().test_start() @@ -344,10 +364,18 @@ def make_teststep_chain_style(teststep: Dict) -> Text: expect = f'"{expect}"' message = validator["message"] - if message: - step_info += f".assert_{assert_method}({check}, {expect}, '{message}')" + + if assert_method == "custom": + custom_comparator = validator['custom_comparator'] + if message: + step_info += f".assert_{assert_method}('{custom_comparator}',{check}, {expect}, '{message}')" + else: + step_info += f".assert_{assert_method}('{custom_comparator}',{check}, {expect})" else: - step_info += f".assert_{assert_method}({check}, {expect})" + if message: + step_info += f".assert_{assert_method}({check}, {expect}, '{message}')" + else: + step_info += f".assert_{assert_method}({check}, {expect})" return f"Step({step_info})" @@ -356,7 +384,8 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: """convert valid testcase dict to pytest file path""" # ensure compatibility with testcase format v2 testcase = ensure_testcase_v3(testcase) - + + pytest_files_meta_cases_list.append(testcase['meta']) # validate testcase format load_testcase(testcase) @@ -441,10 +470,10 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: "teststeps_chain_style": [ make_teststep_chain_style(step) for step in teststeps ], + "marks": config.get("marks", []), } logger.info(make_config_skip(config)) content = __TEMPLATE__.render(data) - # ensure new file's directory exists dir_path = os.path.dirname(testcase_python_abs_path) if not os.path.exists(dir_path): @@ -526,7 +555,6 @@ def __make(tests_path: Text): tests_path: should be in absolute path """ - logger.info(f"make path: {tests_path}") test_files = [] if os.path.isdir(tests_path): files_list = load_folder_files(tests_path) @@ -624,6 +652,11 @@ def main_make(tests_paths: List[Text]) -> List[Text]: pytest_files_format_list = pytest_files_made_cache_mapping.keys() format_pytest_with_black(*pytest_files_format_list) + # 打印收集到的case数据 + logger.info(f"pytest_files_meta_cases_list: {pytest_files_meta_cases_list}") + import json + with open('pytest_files_meta_cases_list.json', 'w') as f: + json.dump(pytest_files_meta_cases_list, f) return list(pytest_files_run_set) diff --git a/httprunner/response.py b/httprunner/response.py index 7fd43bc5..04a1376d 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -88,30 +88,47 @@ def uniform_validator(validator): # format2 comparator = list(validator.keys())[0] compare_values = validator[comparator] - - if not isinstance(compare_values, list) or len(compare_values) not in [2, 3]: - raise ParamsError(f"invalid validator: {validator}") - - check_item = compare_values[0] - expect_value = compare_values[1] - if len(compare_values) == 3: - message = compare_values[2] + if comparator == "custom": + custom_comparator = compare_values[0] + check_item = compare_values[1] + expect_value = compare_values[2] + if len(compare_values) == 4: + message = compare_values[3] + else: + # len(compare_values) == 2 + message = "" else: - # len(compare_values) == 2 - message = "" + if not isinstance(compare_values, list) or len(compare_values) not in [2, 3]: + raise ParamsError(f"invalid validator: {validator}") + + check_item = compare_values[0] + expect_value = compare_values[1] + if len(compare_values) == 3: + message = compare_values[2] + else: + # len(compare_values) == 2 + message = "" else: raise ParamsError(f"invalid validator: {validator}") # uniform comparator, e.g. lt => less_than, eq => equals assert_method = get_uniform_comparator(comparator) - - return { - "check": check_item, - "expect": expect_value, - "assert": assert_method, - "message": message, - } + if assert_method == "custom": + return { + "check": check_item, + "expect": expect_value, + "assert": assert_method, + "message": message, + "custom_comparator": custom_comparator, + } + else: + return { + "check": check_item, + "expect": expect_value, + "assert": assert_method, + "message": message, + } class ResponseObject(object): @@ -194,7 +211,6 @@ class ResponseObject(object): variables_mapping: VariablesMapping = None, functions_mapping: FunctionsMapping = None, ): - variables_mapping = variables_mapping or {} functions_mapping = functions_mapping or {} @@ -206,12 +222,10 @@ class ResponseObject(object): failures = [] for v in validators: - if "validate_extractor" not in self.validation_results: self.validation_results["validate_extractor"] = [] u_validator = uniform_validator(v) - # check item check_item = u_validator["check"] if "$" in check_item: @@ -229,7 +243,11 @@ class ResponseObject(object): # comparator assert_method = u_validator["assert"] - assert_func = get_mapping_function(assert_method, functions_mapping) + if assert_method != 'custom': + assert_func = get_mapping_function(assert_method, functions_mapping) + else: + assert_method = u_validator["custom_comparator"] + assert_func = get_mapping_function(assert_method, functions_mapping) # expect item expect_item = u_validator["expect"] diff --git a/httprunner/runner.py b/httprunner/runner.py index 22f37d16..3e618b06 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -145,9 +145,16 @@ class HttpRunner(object): parsed_request_dict = parse_data( request_dict, step.variables, self.__project_meta.functions ) + case_id = f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}" + # 增加x-trace-id + parsed_request_dict["headers"].setdefault( + "x-trace-id", + case_id, + ) + parsed_request_dict["headers"].setdefault( "HRUN-Request-ID", - f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}", + case_id, ) step.variables["request"] = parsed_request_dict @@ -446,7 +453,14 @@ class HttpRunner(object): # update allure report meta allure.dynamic.title(self.__config.name) allure.dynamic.description(f"TestCase ID: {self.__case_id}") - + git_repo = self.__project_meta.env.get("git_repo", '') + file_ext = self.__project_meta.env.get("file_ext", '') + if git_repo != '' and file_ext != '': + from httprunner.loader import convert_relative_project_root_dir + # 由于运行时加载的是py文件,所以这里link的时候得做一下替换 + link = git_repo + convert_relative_project_root_dir(self.__config.path)[:-8] + '.' +file_ext + allure.dynamic.link(link) + logger.info( f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}" ) @@ -456,5 +470,6 @@ class HttpRunner(object): TestCase(config=self.__config, teststeps=self.__teststeps) ) finally: + allure.attach.file(self.__log_path, name="log", attachment_type=allure.attachment_type.TEXT) logger.remove(log_handler) logger.info(f"generate testcase log: {self.__log_path}") diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 86d1554f..4a5bba6f 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -213,6 +213,14 @@ class StepRequestValidation(object): {"type_match": [jmes_path, expected_value, message]} ) return self + + def assert_custom( + self, custom_comparator, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step_context.validators.append( + {"custom": [custom_comparator, jmes_path, expected_value, message]} + ) + return self def perform(self) -> TStep: return self.__step_context