From 6ad6b7ff84a7cb52e1935a39270466e14c3ae723 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sat, 28 Feb 2026 16:08:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(conftest,config=5Floader):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20get=5Fcaps=20=E7=9A=84=20Capabilities=20=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pytest_addoption 增加 "--caps_name" 获取配置文件中的设备/平台名称 - 修复 get_caps 的 Capabilities 加载逻辑 - 优化 其他优化 enums.py - 删除 移除部分文档,代码,第三方包(loguru) --- README.md | 1 + config/caps.yaml | 10 +++ conftest.py | 33 +++++--- core/config_loader.py | 73 ++++++++---------- core/driver.py | 5 +- core/enums.py | 58 ++++++++++++++ core/modules.py | 30 -------- core/run_appium.py | 22 +----- main.py | 16 +++- page_objects/wan_android_home.py | 13 ++-- page_objects/wan_android_project.py | 6 +- pyproject.toml | 1 - test_cases/test_wan_android_home.py | 6 +- tests/test_finder_converter.py | 10 ++- tests/test_logger.py | 115 ---------------------------- utils/data_loader.py | 4 +- utils/decorators.py | 14 ++-- utils/dirs_manager.py | 2 +- utils/finder.py | 3 +- utils/日志模块使用指南.md | 81 -------------------- 20 files changed, 175 insertions(+), 328 deletions(-) create mode 100644 core/enums.py delete mode 100644 tests/test_logger.py delete mode 100644 utils/日志模块使用指南.md diff --git a/README.md b/README.md index 9dbdd00..f7f066f 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ pytest --platform IOS --udid **可用的自定义命令行参数:** - `--platform`: 目标平台 (`Android` 或 `IOS`)。默认为 `Android`。 +- `--caps_name`: 设备/平台名称。 - `--udid`: 目标设备的唯一设备标识符 (UDID)。 - `--host`: Appium 服务器的主机地址。默认为 `127.0.0.1`。 - `--port`: Appium 服务器的端口。默认为 `4723`。 diff --git a/config/caps.yaml b/config/caps.yaml index 07418d4..758640f 100644 --- a/config/caps.yaml +++ b/config/caps.yaml @@ -17,3 +17,13 @@ ios: autoAcceptAlerts: true waitForQuiescence: false newCommandTimeout: 60 + +wan_android: + platformName: "Android" + automationName: "uiautomator2" + deviceName: "Android" + appPackage: "com.manu.wanandroid" + appActivity: "com.manu.wanandroid.ui.main.activity.MainActivity" + noReset: false + newCommandTimeout: 60 + # udid: "emulator-5554" # Can be injected via CLI \ No newline at end of file diff --git a/conftest.py b/conftest.py index 822a55b..9a332d5 100644 --- a/conftest.py +++ b/conftest.py @@ -16,12 +16,16 @@ 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: @@ -29,14 +33,15 @@ def pytest_addoption(parser: Any) -> None: 注册自定义命令行参数 :param parser: Pytest 参数解析器对象 """ - parser.addoption("--platform", action="store", default="Android", help="目标平台: Android or IOS") + 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 app_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]: +def appium_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]: """ 第一层:管理 Appium Server 进程。 :param request: Pytest 请求对象 @@ -52,41 +57,44 @@ def app_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]: @pytest.fixture(scope="session") -def driver_session(request: pytest.FixtureRequest, app_server: Any) -> Generator[CoreDriver, None, None]: +def driver_session(request: pytest.FixtureRequest, appium_server: Any) -> Generator[CoreDriver, None, None]: """ 第二层:全局单例 Driver 管理 (Session Scope)。 负责创建和销毁 Driver,整个测试过程只启动一次 App。 :param request: Pytest 请求对象 - :param app_server: Appium 服务 fixture 依赖 + :param appium_server: Appium 服务 fixture 依赖 :return: CoreDriver 实例 """ - platform = request.config.getoption("--platform") + 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(platform) + 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 - app_helper = CoreDriver() - app_helper.server_config(host=host, port=port) + driver_helper = CoreDriver() + driver_helper.server_config(host=host, port=port) try: - app_helper.connect(platform=platform, caps=caps) + driver_helper.connect(platform=platform, caps=caps) except Exception as e: pytest.exit(f"无法初始化 Driver: {e}") - yield app_helper + yield driver_helper # 4. 清理 - app_helper.quit() + driver_helper.quit() @pytest.fixture(scope="function") @@ -165,6 +173,8 @@ def pytest_sessionfinish(session: Any, exitstatus: int) -> None: 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) @@ -172,6 +182,7 @@ def pytest_sessionfinish(session: Any, exitstatus: int) -> None: 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+" } diff --git a/core/config_loader.py b/core/config_loader.py index aca1dce..2704c9c 100644 --- a/core/config_loader.py +++ b/core/config_loader.py @@ -9,59 +9,54 @@ @date: 2026/1/16 10:52 @desc: Pytest 核心配置与 Fixture 管理 """ - -from typing import Any +import logging +from typing import Any, Optional from utils.data_loader import load_yaml from core.settings import CAPS_CONFIG_PATH, ENV_CONFIG, CURRENT_ENV -from core.modules import AppPlatform + +logger = logging.getLogger(__name__) -def get_env_config() -> dict[str, str]: +def get_env_config(env_name: Optional[str] = None) -> dict[str, str]: """ - 根据当前环境 (CURRENT_ENV) 获取对应的业务配置 + 获取当前运行环境的业务配置信息。 + + 逻辑: + 1. 优先使用传入的 `env_name`。 + 2. 若未传入,使用全局设置 `CURRENT_ENV`。 + 3. 若都为空,默认为 "test"。 + 4. 如果目标环境在配置中不存在,强制回退到 "test" 环境并记录警告。 + + :param env_name: 指定的环境名称 (e.g., "dev", "prod"),可选。 + :return: 对应环境的配置字典。 """ - return ENV_CONFIG.get(CURRENT_ENV, ENV_CONFIG["test"]) + target_env = env_name or CURRENT_ENV or "test" + + if target_env not in ENV_CONFIG: + logger.warning(f"环境 '{target_env}' 未在配置中定义,将回退到 'test' 环境。") + return ENV_CONFIG.get("test", {}) + + return ENV_CONFIG[target_env] -def get_caps(platform: str) -> dict[str, Any]: +def get_caps(caps_name: str) -> dict[str, Any]: """ - 从 YAML 文件加载 Capabilities 配置 + 从 YAML 配置文件加载指定的 Appium Capabilities。 + + :param caps_name: 配置文件中的设备/平台名称 (不区分大小写),例如 "android_pixel"。 + :return: 该设备对应的 Capabilities 字典。 + :raises ValueError: 当指定的 caps_name 在配置文件中不存在时。 + :raises RuntimeError: 当配置文件加载失败或格式错误时。 """ try: all_caps = load_yaml(CAPS_CONFIG_PATH) all_caps = {k.lower(): v for k, v in all_caps.items()} - platform_key = platform.lower() + caps_key = caps_name.lower() - if platform_key not in all_caps: + if caps_key not in all_caps: + raise ValueError(f"在 {CAPS_CONFIG_PATH} 中找不到平台 '{caps_key}' 的配置") - base_caps: dict[str, Any] = { - "noReset": False, - "newCommandTimeout": 60, - } - - match platform_key: - case AppPlatform.ANDROID: - android_caps = { - "platformName": "Android", - "automationName": "uiautomator2", - "deviceName": "Android", - "appPackage": "com.manu.wanandroid", - "appActivity": "com.manu.wanandroid.ui.main.activity.MainActivity", - } - return base_caps | android_caps - case AppPlatform.IOS: - - ios_caps = { - "platformName": "iOS", - "automationName": "XCUITest", - "deviceName": "iPhone 14", - "bundleId": "com.example.app", - "autoAcceptAlerts": True, - "waitForQuiescence": False, - } - return base_caps | ios_caps - - return all_caps[platform_key] + return all_caps[caps_key] except Exception as e: - raise RuntimeError(f"无法加载“caps”内容 {CAPS_CONFIG_PATH}: {e}") + raise RuntimeError(f"加载 Capabilities 失败 ({CAPS_CONFIG_PATH}): {e}") diff --git a/core/driver.py b/core/driver.py index 2d3914f..224121b 100644 --- a/core/driver.py +++ b/core/driver.py @@ -30,10 +30,10 @@ from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.pointer_input import PointerInput +from core.enums import AppPlatform +from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT, SCREENSHOT_DIR from utils.finder import by_converter from utils.decorators import resolve_wait_method -from core.modules import AppPlatform -from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT, SCREENSHOT_DIR logger = logging.getLogger(__name__) @@ -623,6 +623,7 @@ class CoreDriver: :return: self """ return self.switch_to_context('NATIVE_APP') + def full_screen_screenshot(self, name: str | None = None) -> str: """ 截取当前完整屏幕内容 (自愈逻辑、异常报错首选) diff --git a/core/enums.py b/core/enums.py new file mode 100644 index 0000000..b1e0613 --- /dev/null +++ b/core/enums.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# coding=utf-8 + +""" +@author: CNWei,ChenWei +@Software: PyCharm +@contact: t6g888@163.com +@file: enums +@date: 2026/2/27 17:05 +@desc: +""" +from enum import Enum + + +class AppiumStatus(Enum): + """Appium 服务状态枚举""" + READY = "服务已启动" # 服务和驱动都加载完成 (HTTP 200 + ready: true) + INITIALIZING = "驱动正在加载" # 服务已响应但驱动仍在加载 (HTTP 200 + ready: false) + CONFLICT = "端口被其他程序占用" # 端口被其他非 Appium 程序占用 + OFFLINE = "服务未启动" # 服务未启动 + ERROR = "内部错误" + UNKNOWN = "未知状态" + + +class ServiceRole(Enum): + """服务角色枚举:定义服务的所有权和生命周期""" + MANAGED = "托管模式" # 由本脚本启动,负责清理 + EXTERNAL = "共享模式" # 复用现有服务,不负责清理 + NULL = "空模式" # 无效或未初始化的服务 + + +class AppPlatform(Enum): + """ + 定义支持的移动应用平台枚举。 + """ + ANDROID = "android" + IOS = "ios" + + +class Locator(str, Enum): + """ + 定义元素定位策略枚举。 + 继承 str 以便直接作为参数传递给 Selenium/Appium 方法。 + """ + # --- 原有 Selenium 支持 --- + ID = "id" + NAME = "name" + CLASS = "class" + TAG = "tag" + LINK_TEXT = "link_text" + PARTIAL_LINK_TEXT = "partial_link_text" + CSS = "css" + XPATH = "xpath" + # --- Appium 特有支持 --- + ACCESSIBILITY_ID = "accessibility_id" + AID = "aid" # 简写 + ANDROID_UIAUTOMATOR = "android_uiautomator" + IOS_PREDICATE = "ios_predicate" diff --git a/core/modules.py b/core/modules.py index 390fda3..b6be291 100644 --- a/core/modules.py +++ b/core/modules.py @@ -9,33 +9,3 @@ @date: 2026/1/20 11:54 @desc: """ - -from enum import Enum - -class AppPlatform(Enum): - """ - 定义支持的移动应用平台枚举。 - """ - ANDROID = "android" - IOS = "ios" - - -class Locator(str, Enum): - """ - 定义元素定位策略枚举。 - 继承 str 以便直接作为参数传递给 Selenium/Appium 方法。 - """ - # --- 原有 Selenium 支持 --- - ID = "id" - NAME = "name" - CLASS = "class" - TAG = "tag" - LINK_TEXT = "link_text" - PARTIAL_LINK_TEXT = "partial_link_text" - CSS = "css" - XPATH = "xpath" - # --- Appium 特有支持 --- - ACCESSIBILITY_ID = "accessibility_id" - AID = "aid" # 简写 - ANDROID_UIAUTOMATOR = "android_uiautomator" - IOS_PREDICATE = "ios_predicate" \ No newline at end of file diff --git a/core/run_appium.py b/core/run_appium.py index 8a263cc..917e4b7 100644 --- a/core/run_appium.py +++ b/core/run_appium.py @@ -5,7 +5,7 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: test +@file: run_appium @date: 2026/1/12 10:21 @desc: """ @@ -19,10 +19,11 @@ import sys import http.client import socket import json -from enum import Enum + from typing import List from core.settings import BASE_DIR, APPIUM_HOST, APPIUM_PORT, MAX_RETRIES +from core.enums import AppiumStatus, ServiceRole logger = logging.getLogger(__name__) @@ -55,23 +56,6 @@ class AppiumInternalError(AppiumStartupError): pass -class AppiumStatus(Enum): - """Appium 服务状态枚举""" - READY = "服务已启动" # 服务和驱动都加载完成 (HTTP 200 + ready: true) - INITIALIZING = "驱动正在加载" # 服务已响应但驱动仍在加载 (HTTP 200 + ready: false) - CONFLICT = "端口被其他程序占用" # 端口被其他非 Appium 程序占用 - OFFLINE = "服务未启动" # 服务未启动 - ERROR = "内部错误" - UNKNOWN = "未知状态" - - -class ServiceRole(Enum): - """服务角色枚举:定义服务的所有权和生命周期""" - MANAGED = "托管模式" # 由本脚本启动,负责清理 - EXTERNAL = "共享模式" # 复用现有服务,不负责清理 - NULL = "空模式" # 无效或未初始化的服务 - - def resolve_appium_command(host: str, port: int | str) -> List[str]: """ 解析 Appium 可执行文件的绝对路径。 diff --git a/main.py b/main.py index c82534c..a918fbf 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ from pathlib import Path import pytest from core.settings import LOG_SOURCE, LOG_BACKUP_DIR, ALLURE_TEMP +from core.enums import AppPlatform from utils.dirs_manager import ensure_dirs_ok from utils.report_handler import generate_allure_report @@ -44,6 +45,7 @@ def _archive_logs(): else: print("未找到原始日志文件,跳过备份。") + # 日志清理 def _clean_old_logs(backup_dir, keep_count=10): files = sorted(Path(backup_dir).glob("pytest_*.log"), key=lambda p: p.stat().st_mtime) @@ -54,6 +56,7 @@ def _clean_old_logs(backup_dir, keep_count=10): except OSError as e: print(f"清理旧日志失败 {file_to_remove}: {e}") + def _clean_temp_dirs(): """ 可选:如果你想在测试前清理掉旧的临时文件 @@ -64,6 +67,7 @@ def _clean_temp_dirs(): shutil.rmtree(ALLURE_TEMP, ignore_errors=True) ALLURE_TEMP.mkdir(parents=True, exist_ok=True) + def main(): try: # 1. 创建目录 @@ -73,8 +77,16 @@ def main(): _archive_logs() # 3. 执行 Pytest - # 注意:-x 表示遇到错误立即停止,如果是全量回归建议去掉 -x - pytest.main(["test_cases", "-x", "-v", f"--alluredir={ALLURE_TEMP}"]) + + args = [ + "test_cases", + "-x", # 注意:-x 表示遇到错误立即停止,如果是全量回归建议去掉 -x + "-v", + f"--alluredir={ALLURE_TEMP}", + f"--platform={AppPlatform.ANDROID.value}", + "--caps_name=wan_android" + ] + pytest.main(args) # 4. 生成报告 generate_allure_report() diff --git a/page_objects/wan_android_home.py b/page_objects/wan_android_home.py index 5c98506..501e616 100644 --- a/page_objects/wan_android_home.py +++ b/page_objects/wan_android_home.py @@ -5,7 +5,7 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: test_home +@file: wan_android_home @date: 2026/1/30 17:18 @desc: """ @@ -28,8 +28,8 @@ class HomePage(BasePage): tv_name = ("id", "com.manu.wanandroid:id/tvName") - account=("-android uiautomator",'new UiSelector().text("账号")') - pass_word=("-android uiautomator",'new UiSelector().text("密码")') + account = ("-android uiautomator", 'new UiSelector().text("账号")') + pass_word = ("-android uiautomator", 'new UiSelector().text("密码")') login_button = ("accessibility id", '登录') @@ -47,10 +47,10 @@ class HomePage(BasePage): @allure.step("登录账号:{1}") def login(self, username, password): """执行登录业务逻辑""" - account_element_id =self.find_element(*self.account).id + account_element_id = self.find_element(*self.account).id account_input = {"elementId": account_element_id, "text": username} - pwd_element_id =self.find_element(*self.pass_word).id + pwd_element_id = self.find_element(*self.pass_word).id pass_word_input = {"elementId": pwd_element_id, "text": password} if self.wait_until_visible(*self.login_button): @@ -61,5 +61,4 @@ class HomePage(BasePage): if self.wait_until_visible(*self.tv_name): self.full_screen_screenshot("登陆成功") - self.long_press(x=636,y=117,duration=300) - + self.long_press(x=636, y=117, duration=300) diff --git a/page_objects/wan_android_project.py b/page_objects/wan_android_project.py index 9fe853b..1b9497f 100644 --- a/page_objects/wan_android_project.py +++ b/page_objects/wan_android_project.py @@ -5,7 +5,7 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: test_views +@file: wan_android_project @date: 2026/1/30 17:37 @desc: """ @@ -16,13 +16,15 @@ from appium import webdriver from core.base_page import BasePage from utils.decorators import StepTracer + logger = logging.getLogger(__name__) class ProjectPage(BasePage): # 定位参数 project_title = ("-android uiautomator", 'new UiSelector().text("项目")') - pro_table_title = ("-android uiautomator",'new UiSelector().text("完整项目")') + pro_table_title = ("-android uiautomator", 'new UiSelector().text("完整项目")') + def __init__(self, driver: webdriver.Remote): super().__init__(driver) diff --git a/pyproject.toml b/pyproject.toml index 2e2a15a..768f4be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ requires-python = ">=3.11" dependencies = [ "allure-pytest==2.13.5", "appium-python-client>=5.2.4", - "loguru>=0.7.3", "pytest>=8.3.5", "PyYAML>=6.0.1", "openpyxl>=3.1.2", diff --git a/test_cases/test_wan_android_home.py b/test_cases/test_wan_android_home.py index 696df49..756cf99 100644 --- a/test_cases/test_wan_android_home.py +++ b/test_cases/test_wan_android_home.py @@ -13,13 +13,10 @@ import logging import os import allure -from dotenv import load_dotenv from page_objects.wan_android_home import HomePage from page_objects.wan_android_project import ProjectPage -load_dotenv() - # 配置日志 logger = logging.getLogger(__name__) @@ -52,11 +49,10 @@ class TestWanAndroidHome: with allure.step("断言"): assert os.getenv("USER_NAME") == 'admintest123456' - # 页面跳转 with allure.step("验证页面跳转"): project = home.go_to(ProjectPage) project.switch_to_project() - project.assert_text(*project.pro_table_title,expected_text='完整项目') + project.assert_text(*project.pro_table_title, expected_text='完整项目') project.delay(5) diff --git a/tests/test_finder_converter.py b/tests/test_finder_converter.py index e38be29..fbc6022 100644 --- a/tests/test_finder_converter.py +++ b/tests/test_finder_converter.py @@ -14,6 +14,7 @@ import pytest from appium.webdriver.common.appiumby import AppiumBy from utils.finder import by_converter, register_custom_finder, converter + class TestFinderConverter: def setup_method(self): @@ -56,7 +57,7 @@ class TestFinderConverter: """测试自定义注册功能""" register_custom_finder("my_text", "-android uiautomator") assert by_converter("my_text") == "-android uiautomator" - + # 测试注册后归一化依然生效 assert by_converter("MY_TEXT") == "-android uiautomator" @@ -64,9 +65,9 @@ class TestFinderConverter: """测试重置功能""" register_custom_finder("temp_key", "xpath") assert by_converter("temp_key") == "xpath" - + converter.clear_custom_finders() - + with pytest.raises(ValueError, match="Unsupported locator strategy"): by_converter("temp_key") @@ -81,7 +82,8 @@ class TestFinderConverter: by_converter(123) # type: ignore with pytest.raises(ValueError, match="Invalid selector type"): - by_converter(None) # type: ignore + by_converter(None) # type: ignore + if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/tests/test_logger.py b/tests/test_logger.py deleted file mode 100644 index de8bfb5..0000000 --- a/tests/test_logger.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -""" -@author: CNWei,ChenWei -@Software: PyCharm -@contact: t6g888@163.com -@file: test -@date: 2026/1/14 10:12 -@desc: -""" - -# import pytest -from enum import Enum -from typing import TypeVar -from utils.logger import logger, trace_step - - -# --- 模拟业务逻辑 --- - -class ServiceRole(Enum): - MANAGED = "受控模式" - EXTERNAL = "共享模式" - NULL = "无效模式" - - -class AppiumService: - def __init__(self, device_id: str, role: ServiceRole): - self.device_id = device_id - self.role = role - # 使用 bind 为该实例的所有日志绑定特定的设备 ID - self._log = logger.bind(source=f"Dev:{device_id}") - - @trace_step(step_desc="停止服务", source="Appium") - def stop(self, force=False): - """演示类方法追踪及逻辑分支""" - self._log.info(f"正在尝试停止服务,强制模式={force}") - - if self.role == ServiceRole.EXTERNAL: - self._log.warning("外部服务,跳过清理进程") - return "SKIPPED" - - if self.role == ServiceRole.MANAGED: - self._log.success("已发送 SIGTERM 信号清理进程") - return "SUCCESS" - - raise RuntimeError("无法停止处于未知状态的服务") - - @trace_step("简单打印") - def simple_log(self, msg: str): - self._log.info(f"消息回显: {msg}") - - -# --- 独立函数演示 --- - -@trace_step("执行数据计算", source="Calc") -def calculate_data(a: int, b: int): - if b == 0: - raise ZeroDivisionError("除数不能为 0") - return a / b - - -@trace_step("空值返回测试") -def return_none(): - return None - - -# --- 测试场景覆盖 --- - -def run_scenarios(): - print("\n" + "=" * 50) - print("🚀 开始执行全场景日志覆盖测试") - print("=" * 50 + "\n") - - # 1. 覆盖:正常类方法调用 (过滤 self) - logger.info(">>> 场景 1: 正常类方法 (MANAGED 模式)") - svc1 = AppiumService("emulator-5554", ServiceRole.MANAGED) - svc1.stop(force=True) - - # 2. 覆盖:类方法不同返回值 - logger.info(">>> 场景 2: 共享模式跳过清理") - svc2 = AppiumService("iPhone_15", ServiceRole.EXTERNAL) - svc2.stop() - - # 3. 覆盖:异常捕获 (自动记录错误日志并向上抛出) - logger.info(">>> 场景 3: 异常捕获测试") - try: - calculate_data(10, 0) - except ZeroDivisionError: - logger.warning("主流程已捕获预期的计算异常") - - # 4. 覆盖:复杂参数与 None 返回 - logger.info(">>> 场景 4: 复杂参数与 None 返回") - return_none() - - # 5. 覆盖:未定义状态导致的崩溃 - logger.info(">>> 场景 5: 业务逻辑崩溃测试") - svc3 = AppiumService("Unknown_Device", ServiceRole.NULL) - try: - svc3.stop() - except Exception as e: - logger.error(f"捕获到业务逻辑崩溃:{e}") - - # 6. 覆盖:原生 logger 与装饰器 logger 混合 - logger.info(">>> 场景 6: 验证自定义 source 标签") - # 这里会使用 setup_logger 中定义的默认 'System' 标签 - logger.debug("这是一条调试级别的原始日志") - - print("\n" + "=" * 50) - print("✅ 测试场景执行完毕,请检查 logs 文件夹中的 .log 文件") - print("=" * 50) - - -if __name__ == "__main__": - run_scenarios() \ No newline at end of file diff --git a/utils/data_loader.py b/utils/data_loader.py index ad101fa..93ad091 100644 --- a/utils/data_loader.py +++ b/utils/data_loader.py @@ -11,10 +11,10 @@ """ import yaml from pathlib import Path -from typing import Any, List +from typing import Any -def load_yaml(file_path: Path | str) -> dict[str, Any] | List[Any]: +def load_yaml(file_path: Path | str) -> dict[str, Any] | list[Any]: """ 加载 YAML 文件 :param file_path: 文件路径 diff --git a/utils/decorators.py b/utils/decorators.py index b3e590f..3472c56 100644 --- a/utils/decorators.py +++ b/utils/decorators.py @@ -15,7 +15,6 @@ import time import inspect from functools import wraps from typing import Union, Callable - from contextvars import ContextVar from contextlib import ContextDecorator @@ -41,14 +40,15 @@ class StepTracer(ContextDecorator): ... 也可作为装饰器的一部分(通过 step_trace 工厂函数)。 - - Attributes: - step_desc (str): 对当前步骤或操作的描述。 - source (str): 日志记录器的名称。 - func_info (str, optional): 关联的函数信息,用于日志输出。 """ def __init__(self, step_desc, source='wrapper', func_info=None): + """ + 初始化 StepTracer。 + :param step_desc: 对当前步骤或操作的描述。 + :param source: 日志记录器的名称。 + :param func_info: 关联的函数信息,用于日志输出。 + """ self.step_desc = step_desc self.logger = logging.getLogger(source) self.func_info = func_info @@ -167,6 +167,7 @@ def action_screenshot(func): Returns: Callable: 包装后的函数。 """ + @wraps(func) def wrapper(self, *args, **kwargs): # 1. 正常执行原方法 @@ -241,6 +242,7 @@ def step_trace(step_desc="", source='wrapper'): Returns: Callable: 一个可以装饰函数的装饰器。 """ + def decorator(func): @wraps(func) def wrapper(*args, **kwargs): diff --git a/utils/dirs_manager.py b/utils/dirs_manager.py index c73d11f..f0c308f 100644 --- a/utils/dirs_manager.py +++ b/utils/dirs_manager.py @@ -5,7 +5,7 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: path_manager +@file: dirs_manager @date: 2026/2/3 10:52 @desc: """ diff --git a/utils/finder.py b/utils/finder.py index 178b38c..cd91dfc 100644 --- a/utils/finder.py +++ b/utils/finder.py @@ -5,11 +5,12 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: locator_utils +@file: finder @date: 2026/1/20 15:40 @desc: """ from typing import Literal, Final + from appium.webdriver.common.appiumby import AppiumBy ByType = Literal[ diff --git a/utils/日志模块使用指南.md b/utils/日志模块使用指南.md deleted file mode 100644 index 1240b56..0000000 --- a/utils/日志模块使用指南.md +++ /dev/null @@ -1,81 +0,0 @@ -日志与执行追踪模块使用指南 -本模块基于 Loguru 封装,专为自动化测试项目设计,提供工业级的日志记录、多线程安全保障以及业务步骤自动追踪能力。 - -🌟 核心特性 -全局一致性:统一日志格式,控制台带颜色显示,方便快速定位。 - -自动执行追踪:使用 @trace_step 装饰器,自动记录方法入参、出参、耗时及执行状态。 - -智能参数解析:装饰器自动识别并隐藏 self 和 cls 参数,保护日志整洁。 - -上下文透传:支持通过 .bind() 绑定设备 ID 或模块标识,解决多机并行日志混杂问题。 - -分层标识:默认区分系统日志 (System) 与业务任务日志 (task)。 - -异步安全:支持多进程/多线程安全写入,不影响 Appium 执行性能。 - -🚀 快速上手 -1. 基础日志记录 -在任何模块中直接导入 logger 即可使用。 -```python -from utils.logger import logger - -logger.info("这是一条普通信息") -logger.debug("调试模式下的详细信息") -logger.error("记录一个错误") -``` -2. 使用装饰器追踪业务步骤 (@trace_step) -在 PageObject 方法或任何业务函数上添加装饰器,即可获得全链路追踪。 -```python -from utils.logger import trace_step - -@trace_step("用户登录") -def login(username, password): - # 执行逻辑... - return True - -# 日志输出: -# 🚀 [START] 用户登录 -> login('admin', '****') -# ✅ [SUCCESS] 用户登录 | 耗时: 1.25s | 返回: True -``` -3. 多机并行:上下文透传 (.bind) -在 Appium 并行测试中,用于区分不同设备的日志流。 -```python -from utils.logger import logger - -class BasePage: - def __init__(self, driver, device_id): - self.driver = driver - # 绑定设备 ID 到当前实例的 logger - self.log = logger.bind(source=device_id) - - def click_element(self, loc): - self.log.info(f"点击元素: {loc}") - -# 日志输出: -# | INFO | Pixel_6 | base_page:click_element:15 - 点击元素: id=login_btn -# | INFO | S22 | base_page:click_element:15 - 点击元素: id=login_btn -``` -🛠️ 进阶配置 -标识符说明 (source 字段) -日志格式中包含一个 source 字段(占位符为 magenta 颜色),用于区分日志来源: - -System: (默认) 框架底层日志、驱动初始化等。 - -task: (装饰器默认) 具体的业务测试步骤。 - -自定义: 通过 @trace_step(source="SQL") 或 logger.bind(source="API") 自定义。 - -日志存储 -路径: 项目根目录 /logs/。 - -滚动: 每天午夜 00:00 自动切割。 - -保留: 默认保留最近 30 天 的日志。 - -⚠️ 注意事项 -不要在装饰器内手动接收返回值:@trace_step 已经自动处理了函数的返回值记录。 - -优先使用 self.log:在 PageObject 类中,请务必使用 self.log.info() 而非全局 logger.info(),以确保 bind 的上下文信息(如设备 ID)能正确显示。 - -希望这套日志系统能显著提升您的调试效率和项目质量!如有任何疑问,请随时查阅 utils/logger.py 源码。 \ No newline at end of file