From b98d1da094429c5d14f82d9c1dc3ad4e4b9e238f Mon Sep 17 00:00:00 2001 From: floraachy <1622042529@qq.com> Date: Mon, 26 Jun 2023 11:21:27 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BC=98=E5=8C=96run.py=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=20=202.=20=E5=9C=A8conftest.py=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0pytest=5Fterminal=5Fsummary=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=BF=90=E8=A1=8C=E6=83=85=E5=86=B5=E5=B9=B6?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E5=9C=A8=E6=97=A5=E5=BF=97=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common_utils/files_handle.py | 10 +- conftest.py | 105 +++++++++++---- pytest.ini | 11 +- run.py | 246 +++++++++++++++++++++-------------- 4 files changed, 239 insertions(+), 133 deletions(-) diff --git a/common_utils/files_handle.py b/common_utils/files_handle.py index d1ef39a..5c8627a 100644 --- a/common_utils/files_handle.py +++ b/common_utils/files_handle.py @@ -75,7 +75,7 @@ def zip_file(in_path: str, out_path: str): """ # 如果传入的路径是一个目录才进行压缩操作 if os.path.isdir(in_path): - print("目标路径是一个目录,开始进行压缩......") + print(f"目标路径:{in_path} 是一个目录,开始进行压缩......") # 写入 zip = zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) for path, dirnames, filenames in os.walk(in_path): @@ -87,9 +87,9 @@ def zip_file(in_path: str, out_path: str): path, filename), os.path.join( fpath, filename)) zip.close() - print("压缩完成!") + print(f"目标路径:{in_path} 压缩完成!, 压缩文件路径:{out_path}") else: - print("目标路径不是一个目录,请检查!") + print(f"目标路径:{in_path} 不是一个目录,请检查!") def delete_dir_file(file_path): @@ -99,7 +99,7 @@ def delete_dir_file(file_path): """ paths = os.listdir(file_path) if paths: - print("目标目录存在文件或目录,进行删除操作") + print(f"目标目录: {file_path} 存在文件或目录,进行删除操作") for item in paths: path = os.path.join(file_path, item) # 如果目标路径是一个文件,使用os.remove删除 @@ -109,7 +109,7 @@ def delete_dir_file(file_path): if os.path.isdir(path): os.rmdir(path) else: - print("目标目录不存在文件或目录,不需要删除") + print(f"目标目录: {file_path} 不存在文件或目录,不需要删除") def copy_file(src_file_path, dest_dir_path): diff --git a/conftest.py b/conftest.py index 716e238..04078b5 100644 --- a/conftest.py +++ b/conftest.py @@ -5,36 +5,17 @@ # @File : conftest.py # @Software: PyCharm # @Desc: 这是文件的描述信息 -import os.path -from config.global_vars import ENV_VARS, GLOBAL_VARS -import pytest -from py._xmlgen import html # 安装pytest-html,版本最好是2.1.1 +import time from time import strftime import re +import pytest +from py._xmlgen import html # 安装pytest-html,版本最好是2.1.1 +from config.global_vars import ENV_VARS, GLOBAL_VARS +from loguru import logger -# ------------------------------------- START: 报告处理 ---------------------------------------# -@pytest.mark.hookwrapper -def pytest_runtest_makereport(item, call): - """设置列"用例描述"的值为用例的标题title""" - outcome = yield - # 获取调用结果的测试报告,返回一个report对象 - # report对象的属性包括when(steup, call, teardown三个值)、nodeid(测试用例的名字)、outcome(用例的执行结果,passed,failed) - report = outcome.get_result() - # 将测试用例的title作为测试报告"用例描述"列的值。 - # 注意参数传递时需要这样写:@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases]) - report.description = re.findall('\[(.*?)\]', report.nodeid)[0] - report.func = report.nodeid.split("[")[0] - - -def pytest_html_report_title(report): - """ - 修改报告标题 - """ - report.title = f'{ENV_VARS["common"]["project_name"]} {ENV_VARS["common"]["report_title"]}' - - +# ------------------------------------- START: pytest钩子函数处理---------------------------------------# def pytest_configure(config): """ # 在测试运行前,修改Environment部分信息,配置测试报告环境信息 @@ -45,6 +26,10 @@ def pytest_configure(config): # 给环境表 移除packages 及plugins config._metadata.pop("Packages") config._metadata.pop("Plugins") + # 向pytest的配置中添加marker + # TODO 暂时还没给用例添加 + config.addinivalue_line("markers", 'smoke') + config.addinivalue_line("markers", '回归测试') @pytest.hookimpl(tryfirst=True) @@ -56,6 +41,74 @@ def pytest_sessionfinish(session, exitstatus): session.config._metadata['项目环境'] = GLOBAL_VARS.get("host", "") +@pytest.mark.hookwrapper +def pytest_runtest_makereport(item, call): + """设置列"用例描述"的值为用例的标题title""" + outcome = yield + # 获取调用结果的测试报告,返回一个report对象 + # report对象的属性包括when(steup, call, teardown三个值)、nodeid(测试用例的名字)、outcome(用例的执行结果,passed,failed) + report = outcome.get_result() + # 将测试用例的title作为测试报告"用例描述"列的值。 + # 注意参数传递时需要这样写:@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases]) + report.description = re.findall('\\[(.*?)\\]', report.nodeid)[0] + report.func = report.nodeid.split("[")[0] + + +def pytest_terminal_summary(terminalreporter, config): + """ + 收集测试结果 + """ + _RERUN = len([i for i in terminalreporter.stats.get('rerun', []) if i.when != 'teardown']) + try: + # 获取pytest传参--reruns的值 + reruns_value = int(config.getoption("--reruns")) + _RERUN = int(_RERUN / reruns_value) + except Exception: + reruns_value = "未配置--reruns参数" + _RERUN = len([i for i in terminalreporter.stats.get('rerun', []) if i.when != 'teardown']) + + _PASSED = len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown']) + _ERROR = len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown']) + _FAILED = len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown']) + _SKIPPED = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown']) + _XPASSED = len([i for i in terminalreporter.stats.get('xpassed', []) if i.when != 'teardown']) + _XFAILED = len([i for i in terminalreporter.stats.get('xfailed', []) if i.when != 'teardown']) + + _TOTAL = terminalreporter._numcollected + _TIMES = time.time() - terminalreporter._sessionstarttime + logger.info(f"\n======================================================\n" + "-------------测试结果--------------------\n" + f"用例总数: {_TOTAL}\n" + f"跳过用例数: {_SKIPPED}\n" + f"实际执行用例总数: {_TOTAL - _SKIPPED}\n\n" + f"异常用例数: {_ERROR}\n" + f"失败用例数: {_FAILED}\n" + f"重跑的用例数 || 重跑次数: {_RERUN} || {reruns_value}\n" + f"意外通过的用例数: {_XPASSED}\n" + f"预期失败的用例数: {_XFAILED}\n\n" + "用例执行时长: %.2f" % _TIMES + " s\n") + try: + _RATE = _PASSED / (_TOTAL - _SKIPPED) * 100 + logger.info( + f"\n用例成功率: %.2f" % _RATE + " %\n" + "=====================================================") + except ZeroDivisionError: + logger.info( + f"用例成功率: 0.00 %\n" + "=====================================================") + + +# ------------------------------------- END: pytest钩子函数处理---------------------------------------# + +# ------------------------------------- START: pytest-html钩子函数处理 ---------------------------------------# + +def pytest_html_report_title(report): + """ + 修改报告标题 + """ + report.title = f'{ENV_VARS["common"]["project_name"]} {ENV_VARS["common"]["report_title"]}' + + def pytest_html_results_summary(prefix, summary, postfix): """ 修改Summary部分的信息 @@ -97,4 +150,4 @@ def pytest_html_results_table_html(report, data): del data[:] data.append(html.div("这条用例通过啦!", class_="empty log")) -# ------------------------------------- END: 报告处理 ---------------------------------------# +# ------------------------------------- END: pytest-html钩子函数处理 ---------------------------------------# diff --git a/pytest.ini b/pytest.ini index 526825f..d355842 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,11 +1,14 @@ [pytest] # cmd params -addopts = -s --cache-clear --capture=sys --self-contained-html --reruns=0 --reruns-delay=0 +addopts = + -s + --cache-clear + --capture=sys + --self-contained-html + --reruns=1 + --reruns-delay=5 disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True markers = test_login_demo: login - test_gitlink_demo_dlmkexcel: login excel - test_gitlink_demo_xjxmmkexcel: new project excel - test_new_project_demo: new_project auto: auto generate case test_demo: demo case \ No newline at end of file diff --git a/run.py b/run.py index 0d5c816..8e2bc3c 100644 --- a/run.py +++ b/run.py @@ -13,6 +13,22 @@ > python run.py -env live 在live环境运行测试用例 > python run.py -env=test 在test环境运行测试用例 > python run.py -report=pytest-html (默认在test环境运行测试用例, 报告采用pytest-html) + +pytest相关参数:以下也可通过pytest.ini配置 + --reruns: 失败重跑次数 + --reruns-delay 失败重跑间隔时间 + --count: 重复执行次数 + -v: 显示错误位置以及错误的详细信息 + -s: 等价于 pytest --capture=no 可以捕获print函数的输出 + -q: 简化输出信息 + -m: 运行指定标签的测试用例 + -x: 一旦错误,则停止运行 + --cache-clear 清除pytest的缓存,包括测试结果缓存、抓取的fixture实例缓存和收集器信息缓存等 + --maxfail: 设置最大失败次数,当超出这个阈值时,则不会在执行测试用例 + "--reruns=3", "--reruns-delay=2" + + allure相关参数: + –-alluredir这个选项用于指定存储测试结果的路径 """ import os @@ -32,120 +48,154 @@ from config.global_vars import GLOBAL_VARS, ENV_VARS from common_utils.files_handle import zip_file, copy_file +def capture_all_logs(level=LOG_LEVEL): + logger.info(""" + _ _ _ _____ _ + __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_ + / _` | "_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __| + | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_ + \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__| + |_| + Starting ... ... ... + """) + if level: + # 仅捕获指定级别日志 + logger.add( + os.path.join(LOG_DIR, "runtime_{time}.log"), + enqueue=True, + encoding="utf-8", + rotation="00:00", + level=LOG_LEVEL.upper(), + format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", + ) + else: + # 捕获所有日志 + logger.add( + os.path.join(LOG_DIR, "runtime_{time}_{level}.log"), + enqueue=True, + encoding="utf-8", + rotation="00:00", + format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", + ) + + +# 封装生成测试用例的函数 +def auto_generate_test_cases(): + # 删除原有的测试用例,以便生成新的测试用例 + if os.path.exists(AUTO_CASE_DIR): + shutil.rmtree(AUTO_CASE_DIR) + + # 根据data里面的yaml/excel文件,自动生成测试用例 + generate_cases() + + +# 封装执行 pytest 的函数 +def run_pytest(report_type, mark_param): + arg_list = [] + + # 执行指定的测试用例 + if mark_param is not None: + arg_list.append(f"-m {mark_param}") + + current_time = datetime.now().strftime("%Y-%m-%d+%H_%M_%S") + + # 生成 Allure 报告 + if report_type.lower() == "allure": + arg_list.extend( + [ + "-q", + "--cache-clear", + f'--alluredir={ALLURE_RESULTS_DIR}', + "--clean-alluredir", + ] + ) + pytest.main(args=arg_list) + + # ------------------------ 使用allure生成测试报告 ------------------------ + + # 从LIB_DIR目录中寻找以allure开头的目录作为allure模块的目录,并进入bin目录下 + allure_path = os.path.join(LIB_DIR, [i for i in os.listdir(LIB_DIR) if i.startswith("allure")][0], "bin") + # 根据windows或linux环境判断, 执行指定的命令。 + cmd = PlatformHandle().allure[1].format( + os.path.join(allure_path, PlatformHandle().allure[0]), + ALLURE_RESULTS_DIR, + ALLURE_HTML_DIR, + ) + os.popen(cmd).read() + # ------------------------ 美化allure测试报告 ------------------------ + # 设置allure报告窗口标题 + AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_windows_title( + new_title=ENV_VARS["common"]["project_name"] + ) + # 设置allure报告名称 + AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_name( + new_name=ENV_VARS["common"]["report_title"] + ) + # 往allure测试报告中写入环境配置相关信息 + env_info = ENV_VARS["common"] + env_info["run_env"] = GLOBAL_VARS.get("host", "") + AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_env_on_html( + env_info=env_info + ) + # 复制http_server.exe以及双击查看报告.bat文件到allure-html根目录下,用于支撑电脑在未安装allure服务的情况下打开allure-html报告 + # 注意:ZIP文件的名称包含某些特殊字符,会导致无法使用.bat文件打开allure-html报告, 例如空格,/ 等 + allure_config_path = os.path.join(CONF_DIR, "allure_config") + + copy_file(src_file_path=os.path.join(allure_config_path, + [i for i in os.listdir(allure_config_path) if i.endswith(".exe")][0]), + dest_dir_path=ALLURE_HTML_DIR) + copy_file(src_file_path=os.path.join(allure_config_path, + [i for i in os.listdir(allure_config_path) if i.endswith(".bat")][0]), + dest_dir_path=ALLURE_HTML_DIR) + + # ------------------------ allure测试报告生成完毕,压缩allure测试报告为ZIP文件 ------------------------ + # report_path以及attachment_path,后面发送测试结果需要用到 + report_path = ALLURE_HTML_DIR + attachment_path = os.path.join(REPORT_DIR, f'autotest_{str(current_time)}.zip') + # 压缩allure-html报告为一个压缩文件zip + zip_file(in_path=report_path, out_path=attachment_path) + + # 生成 pytest-html 报告 + else: + report_path = os.path.join(REPORT_DIR, "autotest_" + str(current_time) + ".html") + attachment_path = report_path + pytest_html_config_path = os.path.join(CONF_DIR, "pytest_html_config") + report_css = os.path.join(pytest_html_config_path, "pytest_html_report.css") + arg_list.extend([f"--html={report_path}", f"--css={report_css}"]) + pytest.main(args=arg_list) + + return report_path, attachment_path + + +# 主函数 @click.command() @click.option("-report", default="allure", help="选择需要生成的测试报告:pytest-html, allure") @click.option("-env", default="test", help="输入运行环境:test 或 live") @click.option("-m", default=None, help="选择需要运行的用例:python.ini配置的名称") -def run(env, m, report): +def run(report, env, m): try: - # ------------------------ 捕获日志 ------------------------ - # 捕获所有日志 - logger.add(os.path.join(LOG_DIR, "runtime_{time}_all.log"), enqueue=True, encoding="utf-8", rotation="00:00", - format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}") - # # 仅捕获指定级别日志 - # logger.add(os.path.join(LOG_DIR, "runtime_{time}.log"), enqueue=True, encoding="utf-8", rotation="00:00", - # level=LOG_LEVEL.upper(), - # format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}") - logger.info(""" - _ _ _ _____ _ - __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_ - / _` | "_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __| - | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_ - \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__| - |_| - Starting ... ... ... - """) + # ------------------------ 捕获日志---------------------------- + capture_all_logs() + # ------------------------ 设置全局变量 ------------------------ # 根据指定的环境参数,将运行环境所需相关配置数据保存到GLOBAL_VARS GLOBAL_VARS["env_key"] = env.lower() - if ENV_VARS.get(env.lower(), None): - for k, v in ENV_VARS[env.lower()].items(): - GLOBAL_VARS[k] = v - # ------------------------ 自动生成测试用例 ------------------------ - # 删除原有的测试用例,以便生成新的测试用例 - if os.path.exists(AUTO_CASE_DIR): - # 删除文件夹 - shutil.rmtree(AUTO_CASE_DIR) + if ENV_VARS.get(env.lower()): + GLOBAL_VARS.update(ENV_VARS[env.lower()]) - # 根据data里面的yaml/excel文件,自动生成测试用例 - generate_cases() + # ------------------------ 自动生成测试用例 ------------------------ + auto_generate_test_cases() # ------------------------ pytest执行测试用例 ------------------------ - """ - pytest相关参数:以下也可通过pytest.ini配置 - --reruns: 失败重跑次数 - --reruns-delay 失败重跑间隔时间 - --count: 重复执行次数 - -v: 显示错误位置以及错误的详细信息 - -s: 等价于 pytest --capture=no 可以捕获print函数的输出 - -q: 简化输出信息 - -m: 运行指定标签的测试用例 - -x: 一旦错误,则停止运行 - --cache-clear 清除pytest的缓存,包括测试结果缓存、抓取的fixture实例缓存和收集器信息缓存等 - --maxfail: 设置最大失败次数,当超出这个阈值时,则不会在执行测试用例 - "--reruns=3", "--reruns-delay=2" - """ - arg_list = [] - # 执行指定测试用例 - if m is not None: - arg_list.append(f"-m {m}") - current_time = datetime.now().strftime("%Y-%m-%d+%H_%M_%S") - if report.lower() == "allure": - arg_list.extend(['-q', '--cache-clear', f'--alluredir={ALLURE_RESULTS_DIR}', '--clean-alluredir']) - """ - allure相关参数: - –-alluredir这个选项用于指定存储测试结果的路径 - """ - pytest.main(args=arg_list) - # ------------------------ 使用allure生成测试报告 ------------------------ - logger.debug("-------开始生成allure测试报告-------") - plat = PlatformHandle() - # 从LIB_DIR目录中寻找以allure开头的目录作为allure模块的目录,并进入bin目录下 - allure_path = os.path.join(LIB_DIR, [i for i in os.listdir(LIB_DIR) if i.startswith("allure")][0], "bin") - # 根据windows或linux环境判断, 执行指定的命令。plat.allure[0]=cmd, plat.allure[1]=cmd2 - cmd = plat.allure[1].format(os.path.join(allure_path, plat.allure[0]), ALLURE_RESULTS_DIR, ALLURE_HTML_DIR) - # 执行命令行命令,并通过read()方法将命令的结果返回;os.popen() 方法用于从一个命令打开一个管道。在Unix,Windows中有效 - os.popen(cmd).read() - logger.debug("-------美化allure测试报告-------") - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_windows_title( - new_title=ENV_VARS["common"]["project_name"]) - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_name( - new_name=ENV_VARS["common"]["report_title"]) - logger.debug("-------allure测试报告生成完毕,开始发送测试报告-------") - # 往allure测试报告中写入环境配置相关信息 - env_info = ENV_VARS["common"] - env_info["run_env"] = GLOBAL_VARS.get("host", env) - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_env_on_html( - env_info=env_info) - # 复制http_server.exe以及双击查看报告.bat文件到allure-html根目录下,用于支撑电脑在未安装allure服务的情况下打开allure-html报告 - # 注意:ZIP文件的名称包含某些特殊字符,会导致无法使用.bat文件打开allure-html报告, 例如空格,/ 等 - allure_config_path = os.path.join(CONF_DIR, "allure_config") - copy_file(src_file_path=os.path.join(allure_config_path, - [i for i in os.listdir(allure_config_path) if i.endswith(".exe")][0]), - dest_dir_path=ALLURE_HTML_DIR) - copy_file(src_file_path=os.path.join(allure_config_path, - [i for i in os.listdir(allure_config_path) if i.endswith(".bat")][0]), - dest_dir_path=ALLURE_HTML_DIR) - # report_path以及attachment_path,后面发送测试结果需要用到 - report_path = ALLURE_HTML_DIR - attachment_path = os.path.join(REPORT_DIR, f'autotest_{str(current_time)}.zip') - # 压缩allure-html报告为一个压缩文件zip - zip_file(in_path=ALLURE_HTML_DIR, out_path=attachment_path) + report_path, attachment_path = run_pytest(report_type=report, mark_param=m) - else: - # report_path以及attachment_path,后面发送测试结果需要用到 - report_path = os.path.join(REPORT_DIR, "autotest_" + str(current_time) + ".html") - attachment_path = report_path - pytest_html_config_path = os.path.join(CONF_DIR, "pytest_html_config") - report_css = os.path.join(pytest_html_config_path, "pytest_html_report.css") - arg_list.extend([f'--html={report_path}', f"--css={report_css}"]) - pytest.main(args=arg_list) - logger.debug("-------测试完成,发送测试报告-------") + # ------------------------ 发送测试结果 ------------------------ # 发送通知 send_result(report_path, report_type=report, attachment_path=attachment_path) + except Exception as e: raise e -if __name__ == '__main__': +if __name__ == "__main__": run()