fix skip错误和增加mark功能和增加meta功能收集用例 (#1733)

* fix skip错误和增加mark功能和增加meta功能收集用例

* 增加自定义验证器

* 在allure报告中增加运行日志文件

* 测试用例元数据收集增加用例文件路径

* 增加公司的全链路追踪ID

* allure测试报告增加用例repo地址link
This commit is contained in:
august-jupiter
2025-08-04 19:35:29 +08:00
committed by GitHub
parent 9be3df885c
commit 63aa44d24b
6 changed files with 132 additions and 36 deletions

View File

@@ -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(

View File

@@ -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:]

View File

@@ -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)

View File

@@ -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"]

View File

@@ -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}")

View File

@@ -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