- 新增 pytest_addoption 增加 "--caps_name" 获取配置文件中的设备/平台名称 - 修复 get_caps 的 Capabilities 加载逻辑 - 优化 其他优化 enums.py - 删除 移除部分文档,代码,第三方包(loguru)
201 lines
7.1 KiB
Python
201 lines
7.1 KiB
Python
#!/usr/bin/env python
|
||
# coding=utf-8
|
||
|
||
"""
|
||
@author: CNWei,ChenWei
|
||
@Software: PyCharm
|
||
@contact: t6g888@163.com
|
||
@file: conftest
|
||
@date: 2026/1/16 10:52
|
||
@desc: Pytest 核心配置与 Fixture 管理
|
||
"""
|
||
import logging
|
||
import secrets
|
||
from pathlib import Path
|
||
from typing import Generator, Any
|
||
|
||
import pytest
|
||
import allure
|
||
from dotenv import load_dotenv
|
||
|
||
from core.run_appium import start_appium_service, stop_appium_service
|
||
from core.driver import CoreDriver
|
||
from core.settings import APPIUM_HOST, APPIUM_PORT
|
||
from core.enums import AppPlatform
|
||
from core.config_loader import get_caps
|
||
|
||
load_dotenv()
|
||
|
||
|
||
# 注册命令行参数
|
||
def pytest_addoption(parser: Any) -> None:
|
||
"""
|
||
注册自定义命令行参数
|
||
:param parser: Pytest 参数解析器对象
|
||
"""
|
||
parser.addoption("--platform", action="store", default="android1", help="目标平台: Android or IOS")
|
||
parser.addoption("--caps_name", action="store", default=None, help="配置文件中的设备/平台名称")
|
||
parser.addoption("--udid", action="store", default=None, help="设备唯一标识")
|
||
parser.addoption("--host", action="store", default=APPIUM_HOST, help="Appium Server Host")
|
||
parser.addoption("--port", action="store", default=str(APPIUM_PORT), help="Appium Server Port")
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def appium_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]:
|
||
"""
|
||
第一层:管理 Appium Server 进程。
|
||
:param request: Pytest 请求对象
|
||
:return: Appium 服务进程实例
|
||
"""
|
||
# 获取命令行参数
|
||
host = request.config.getoption("--host")
|
||
port = int(request.config.getoption("--port"))
|
||
|
||
service = start_appium_service(host, port)
|
||
yield service
|
||
stop_appium_service(service)
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def driver_session(request: pytest.FixtureRequest, appium_server: Any) -> Generator[CoreDriver, None, None]:
|
||
"""
|
||
第二层:全局单例 Driver 管理 (Session Scope)。
|
||
负责创建和销毁 Driver,整个测试过程只启动一次 App。
|
||
:param request: Pytest 请求对象
|
||
:param appium_server: Appium 服务 fixture 依赖
|
||
:return: CoreDriver 实例
|
||
"""
|
||
platform: AppPlatform = request.config.getoption("--platform")
|
||
# 配置名称(caps_name)(决定去 YAML 哪个节点拿数据,默认等于 platform)
|
||
caps_name = request.config.getoption("--caps_name") or platform
|
||
ud_id = request.config.getoption("--udid")
|
||
host = request.config.getoption("--host")
|
||
port = int(request.config.getoption("--port"))
|
||
|
||
# 1. 获取基础 Caps
|
||
caps = get_caps(caps_name)
|
||
|
||
# 2. 动态注入参数
|
||
if ud_id: caps["udid"] = ud_id
|
||
|
||
# 将最终生效的 caps 存入 pytest 配置,方便报告读取
|
||
request.config._final_caps = caps
|
||
request.config._caps_name = caps_name
|
||
|
||
# 3. 初始化 Driver
|
||
driver_helper = CoreDriver()
|
||
driver_helper.server_config(host=host, port=port)
|
||
|
||
try:
|
||
driver_helper.connect(platform=platform, caps=caps)
|
||
except Exception as e:
|
||
pytest.exit(f"无法初始化 Driver: {e}")
|
||
|
||
yield driver_helper
|
||
|
||
# 4. 清理
|
||
driver_helper.quit()
|
||
|
||
|
||
@pytest.fixture(scope="function")
|
||
def driver(driver_session: CoreDriver) -> Generator[Any, None, None]:
|
||
"""
|
||
第三层:用例级 Driver 注入。
|
||
每个用例直接获取已存在的 Driver 实例。
|
||
可以在这里添加 reset_app() 逻辑,确保用例间独立性。
|
||
:param driver_session: CoreDriver 会话实例
|
||
:return: 原始 Appium Driver 对象 (webdriver.Remote)
|
||
"""
|
||
# 可选:如果需要在每个用例前重置 App 状态
|
||
# driver_session.driver.reset()
|
||
yield driver_session.driver
|
||
|
||
|
||
def pytest_exception_interact(node: Any, call: Any, report: Any) -> None:
|
||
"""
|
||
当测试用例抛出异常(断言失败或代码报错)时,Pytest 会调用这个钩子。
|
||
我们在这里手动把错误信息喂给 logging。
|
||
:param node: 测试节点
|
||
:param call: 调用信息
|
||
:param report: 测试报告
|
||
"""
|
||
logger = logging.getLogger("pytest")
|
||
|
||
if report.failed:
|
||
# 获取详细的错误堆栈(包含 assert 的对比信息)
|
||
# long,short,no-locals
|
||
exc_info = call.excinfo.getrepr(style='short')
|
||
screenshot_name = f"异常截图_{secrets.token_hex(4)}"
|
||
|
||
logger.error(f"\n{'=' * 40} TEST FAILED {'=' * 40}\n"
|
||
f"Node ID: {node.nodeid}\n"
|
||
f"截图名称: {screenshot_name}\n"
|
||
f"详细错误信息:\n{exc_info}"
|
||
)
|
||
|
||
# 尝试获取 driver_session (CoreDriver 实例)
|
||
if "driver_session" in node.funcargs:
|
||
helper = node.funcargs["driver_session"]
|
||
try:
|
||
# 截图并附加到 Allure
|
||
screenshot_bytes = helper.driver.get_screenshot_as_png()
|
||
allure.attach(
|
||
screenshot_bytes,
|
||
name=screenshot_name,
|
||
attachment_type=allure.attachment_type.PNG
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"执行异常截图失败: {e}")
|
||
logger.error("=" * 93 + "\n")
|
||
|
||
|
||
def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
|
||
"""
|
||
测试会话结束时,收集环境信息到 Allure 报告
|
||
:param session: Pytest 会话对象
|
||
:param exitstatus: 退出状态码
|
||
"""
|
||
match exitstatus:
|
||
case pytest.ExitCode.OK:
|
||
logging.info("测试全部通过!")
|
||
case pytest.ExitCode.TESTS_FAILED:
|
||
logging.warning("部分测试用例执行失败,请检查报告。")
|
||
case pytest.ExitCode.INTERRUPTED:
|
||
logging.error("测试被用户手动中断(Ctrl+C)。")
|
||
case pytest.ExitCode.INTERNAL_ERROR:
|
||
logging.critical("Pytest 发生内部错误!")
|
||
case pytest.ExitCode.USAGE_ERROR:
|
||
logging.error("Pytest 命令行参数错误或用法不当。")
|
||
case pytest.ExitCode.NO_TESTS_COLLECTED:
|
||
logging.warning("未发现任何测试用例。")
|
||
case _:
|
||
logging.error(f"未知错误状态码: {exitstatus}")
|
||
|
||
report_dir = session.config.getoption("--alluredir")
|
||
final_caps = getattr(session.config, "_final_caps", {})
|
||
caps_name = getattr(session.config, "_caps_name", '')
|
||
|
||
if not report_dir:
|
||
return
|
||
report_path = Path(report_dir)
|
||
# 收集环境信息
|
||
env_info = {
|
||
"Platform": session.config.getoption("--platform"),
|
||
"UDID": final_caps.get("udid") or session.config.getoption("--udid") or "Not Specified",
|
||
"CapsName": caps_name,
|
||
"Host": session.config.getoption("--host"),
|
||
"Python": "3.11+"
|
||
}
|
||
|
||
try:
|
||
if not report_path.exists():
|
||
report_path.mkdir(parents=True, exist_ok=True)
|
||
# 生成 environment.properties 文件
|
||
env_file = report_path / "environment.properties"
|
||
with env_file.open("w", encoding="utf-8") as f:
|
||
for k, v in env_info.items():
|
||
f.write(f"{k}={v}\n")
|
||
logging.info("Allure 环境信息已生成。")
|
||
except Exception as e:
|
||
logging.error(f"无法写入环境属性: {e}")
|