fix(conftest,config_loader): 修复 get_caps 的 Capabilities 加载逻辑

- 新增 pytest_addoption 增加 "--caps_name" 获取配置文件中的设备/平台名称
- 修复 get_caps 的 Capabilities 加载逻辑
- 优化 其他优化 enums.py
- 删除 移除部分文档,代码,第三方包(loguru)
This commit is contained in:
2026-02-28 16:08:14 +08:00
parent 332deb3666
commit 6ad6b7ff84
20 changed files with 175 additions and 328 deletions

View File

@@ -138,6 +138,7 @@ pytest --platform IOS --udid <your_iphone_udid>
**可用的自定义命令行参数:**
- `--platform`: 目标平台 (`Android` 或 `IOS`)。默认为 `Android`。
- `--caps_name`: 设备/平台名称。
- `--udid`: 目标设备的唯一设备标识符 (UDID)。
- `--host`: Appium 服务器的主机地址。默认为 `127.0.0.1`。
- `--port`: Appium 服务器的端口。默认为 `4723`。

View File

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

View File

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

View File

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

View File

@@ -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:
"""
截取当前完整屏幕内容 (自愈逻辑、异常报错首选)

58
core/enums.py Normal file
View File

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

View File

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

View File

@@ -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 可执行文件的绝对路径。

16
main.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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):
@@ -83,5 +84,6 @@ class TestFinderConverter:
with pytest.raises(ValueError, match="Invalid selector type"):
by_converter(None) # type: ignore
if __name__ == "__main__":
pytest.main(["-v", __file__])

View File

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

View File

@@ -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: 文件路径

View File

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

View File

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

View File

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

View File

@@ -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 源码。