fix(conftest,config_loader): 修复 get_caps 的 Capabilities 加载逻辑
- 新增 pytest_addoption 增加 "--caps_name" 获取配置文件中的设备/平台名称 - 修复 get_caps 的 Capabilities 加载逻辑 - 优化 其他优化 enums.py - 删除 移除部分文档,代码,第三方包(loguru)
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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
|
||||
33
conftest.py
33
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+"
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
58
core/enums.py
Normal 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"
|
||||
@@ -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"
|
||||
@@ -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
16
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
@@ -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__])
|
||||
|
||||
@@ -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()
|
||||
@@ -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: 文件路径
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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 源码。
|
||||
Reference in New Issue
Block a user