refactor: 优化日志系统及增加定位转换器
- 更新 pytest.ini 统一配置日志格式和基础命令。 - 优化 main.py 增加测试后的日志自动备份与定期清理功能。 - 新增 finder.py 实现定位元素转换机制
This commit is contained in:
@@ -13,7 +13,7 @@ import logging
|
||||
|
||||
import pytest
|
||||
from core.run_appium import start_appium_service, stop_appium_service
|
||||
from core.driver import AppDriver
|
||||
from core.driver import CoreDriver
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -36,7 +36,7 @@ def driver(app_server):
|
||||
依赖 app_server,确保服务 Ready 后才创建连接。
|
||||
"""
|
||||
# 实例化你提供的类结构
|
||||
app_helper = AppDriver()
|
||||
app_helper = CoreDriver()
|
||||
|
||||
# 配置Android设备参数
|
||||
capabilities = dict(
|
||||
|
||||
@@ -19,7 +19,12 @@ from appium.options.ios import XCUITestOptions
|
||||
from appium.options.common.base import AppiumOptions
|
||||
from appium.webdriver.webdriver import ExtensionBase
|
||||
from appium.webdriver.client_config import AppiumClientConfig
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from utils.finder import by_converter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,21 +33,22 @@ class AppPlatform(Enum):
|
||||
IOS = "ios"
|
||||
|
||||
|
||||
class AppDriver:
|
||||
|
||||
class CoreDriver:
|
||||
def __init__(self):
|
||||
self.driver: Optional[webdriver.Remote] = None
|
||||
self._host = "127.0.0.1"
|
||||
self._port = 4723
|
||||
|
||||
def server_config(self):
|
||||
def server_config(self, host: str = "127.0.0.1", port: int = 4723):
|
||||
"""配置服务端信息"""
|
||||
# self._host = host
|
||||
# self._port = port
|
||||
self._host = host
|
||||
self._port = port
|
||||
logger.info(f"Appium Server 指向 -> {self._host}:{self._port}")
|
||||
|
||||
def connect(self, platform: str | AppPlatform, caps: dict,
|
||||
extensions: list[Type[ExtensionBase]] | None = None,
|
||||
client_config: AppiumClientConfig | None = None) -> 'AppDriver':
|
||||
client_config: AppiumClientConfig | None = None) -> 'CoreDriver':
|
||||
"""
|
||||
参照 KeyWordDriver 逻辑,但强化了配置校验和异常处理
|
||||
"""
|
||||
@@ -89,18 +95,53 @@ class AppDriver:
|
||||
self.driver = None
|
||||
raise ConnectionError(f"无法连接到 Appium 服务,请检查端口 {self._port} 或设备状态。") from e
|
||||
|
||||
# --- 开始封装操作方法 ---
|
||||
def click(self, locator: tuple):
|
||||
"""封装点击,locator 格式如 (AppiumBy.ID, "xxx")"""
|
||||
logger.info(f"点击元素: {locator}")
|
||||
self.driver.find_element(*locator).click()
|
||||
|
||||
def input(self, locator: tuple, text: str):
|
||||
"""封装输入"""
|
||||
logger.info(f"对元素 {locator} 输入内容: {text}")
|
||||
el = self.driver.find_element(*locator)
|
||||
# --- 核心操作 ---
|
||||
def find(self, by, value, timeout=10):
|
||||
"""内部通用查找(显式等待)"""
|
||||
by = by_converter(by)
|
||||
target = (by, value)
|
||||
# self.driver.find_element()
|
||||
return WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(target))
|
||||
|
||||
def click(self, by, value, timeout=10) -> 'CoreDriver':
|
||||
target = (by_converter(by), value)
|
||||
logger.info(f"点击: {target}")
|
||||
WebDriverWait(self.driver, timeout).until(EC.element_to_be_clickable(target)).click()
|
||||
return self
|
||||
|
||||
def input(self, by, value, text, timeout=10) -> 'CoreDriver':
|
||||
target = (by_converter(by), value)
|
||||
logger.info(f"输入 '{text}' 到: {target}")
|
||||
el = WebDriverWait(self.driver, timeout).until(EC.visibility_of_element_located(target))
|
||||
el.clear()
|
||||
el.send_keys(text)
|
||||
return self
|
||||
|
||||
# --- 移动端特有:方向滑动 ---
|
||||
def swipe_to(self, direction: str = "up", duration: int = 800) -> 'CoreDriver':
|
||||
"""封装方向滑动,无需计算具体坐标"""
|
||||
if not self._size:
|
||||
self._size = self.driver.get_window_size()
|
||||
|
||||
w, h = self._size['width'], self._size['height']
|
||||
# 这里的 0.8/0.2 比例是为了避开刘海屏和虚拟按键,提高滑动成功率
|
||||
coords = {
|
||||
"up": (w * 0.5, h * 0.8, w * 0.5, h * 0.2),
|
||||
"down": (w * 0.5, h * 0.2, w * 0.5, h * 0.8),
|
||||
"left": (w * 0.9, h * 0.5, w * 0.1, h * 0.5),
|
||||
"right": (w * 0.1, h * 0.5, w * 0.9, h * 0.5)
|
||||
}
|
||||
start_x, start_y, end_x, end_y = coords.get(direction.lower(), coords["up"])
|
||||
self.driver.swipe(start_x, start_y, end_x, end_y, duration)
|
||||
return self
|
||||
|
||||
# --- 断言逻辑 ---
|
||||
def assert_text(self, by, value, expected_text) -> 'CoreDriver':
|
||||
actual = self.find(by, value).text
|
||||
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
|
||||
logger.info(f"断言通过: 文本匹配 '{actual}'")
|
||||
return self
|
||||
|
||||
|
||||
|
||||
|
||||
29
core/modules.py
Normal file
29
core/modules.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
@author: CNWei,ChenWei
|
||||
@Software: PyCharm
|
||||
@contact: t6g888@163.com,chenwei@zygj.com
|
||||
@file: modules
|
||||
@date: 2026/1/20 11:54
|
||||
@desc:
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Locator(str, Enum):
|
||||
# --- 原有 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"
|
||||
17
main.py
17
main.py
@@ -1,27 +1,38 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from core.settings import LOG_SOURCE, LOG_BACKUP_DIR, ALLURE_TEMP, REPORT_DIR
|
||||
|
||||
|
||||
# 日志自动清理
|
||||
def _clean_old_logs(backup_dir, keep_count=10):
|
||||
files = sorted(Path(backup_dir).glob("pytest_*.log"), key=os.path.getmtime)
|
||||
while len(files) > keep_count:
|
||||
os.remove(files.pop(0))
|
||||
file_to_remove = files.pop(0)
|
||||
try:
|
||||
os.remove(file_to_remove)
|
||||
except OSError as e:
|
||||
print(f"清理旧日志失败 {file_to_remove}: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# 2. 执行 Pytest
|
||||
# 建议保留你之前配置的 -s -v 等参数
|
||||
exit_code = pytest.main(["test_cases", "-x", "-v", f"--alluredir={ALLURE_TEMP}"])
|
||||
# 注意:-x 表示遇到错误立即停止,如果是全量回归建议去掉 -x
|
||||
pytest.main(["test_cases", "-x", "-v", f"--alluredir={ALLURE_TEMP}"])
|
||||
|
||||
# 3. 生成报告
|
||||
if ALLURE_TEMP.exists():
|
||||
os.system(f'allure generate {ALLURE_TEMP} -o {REPORT_DIR} --clean')
|
||||
# 使用 subprocess 替代 os.system,更安全且跨平台兼容性更好
|
||||
subprocess.run(f'allure generate {ALLURE_TEMP} -o {REPORT_DIR} --clean', shell=True, check=False)
|
||||
except Exception as e:
|
||||
print(f"自动化测试执行过程中发生异常: {e}")
|
||||
|
||||
finally:
|
||||
# 4. 备份日志 (无论测试是否崩溃都执行)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[pytest]
|
||||
addopts = -q --show-capture=no
|
||||
;addopts = --tb=short
|
||||
|
||||
# 1. 开启实时控制台日志
|
||||
log_cli = True
|
||||
log_cli_level = INFO
|
||||
|
||||
155
utils/finder.py
Normal file
155
utils/finder.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
@author: CNWei,ChenWei
|
||||
@Software: PyCharm
|
||||
@contact: t6g888@163.com,chenwei@zygj.com
|
||||
@file: locator_utils
|
||||
@date: 2026/1/20 15:40
|
||||
@desc:
|
||||
"""
|
||||
from typing import Literal, Final
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
ByType = Literal[
|
||||
# By(selenium)
|
||||
"id", "xpath", "link text", "partial link text", "name", "tag name", "class name", "css selector",
|
||||
# AppiumBy
|
||||
'-ios predicate string',
|
||||
'-ios class chain',
|
||||
'-android uiautomator',
|
||||
'-android viewtag',
|
||||
'-android datamatcher',
|
||||
'-android viewmatcher',
|
||||
'accessibility id',
|
||||
'-image',
|
||||
'-custom',
|
||||
'-flutter semantics label',
|
||||
'-flutter type',
|
||||
'-flutter key',
|
||||
'-flutter text',
|
||||
'-flutter text containing',
|
||||
# 自定义常用简写 (Shortcuts)
|
||||
"aid", "class", "css", "uiautomator", "predicate", "chain",
|
||||
]
|
||||
|
||||
class FinderConverter:
|
||||
"""
|
||||
定位查找转换工具类
|
||||
提供策略的归一化处理、简写映射及动态自定义注册
|
||||
"""
|
||||
|
||||
# 预设的常用简写
|
||||
_BUILTIN_SHORTCUTS: Final = {
|
||||
"aid": AppiumBy.ACCESSIBILITY_ID,
|
||||
"class": AppiumBy.CLASS_NAME,
|
||||
"css": AppiumBy.CSS_SELECTOR,
|
||||
"uiautomator": AppiumBy.ANDROID_UIAUTOMATOR,
|
||||
"predicate": AppiumBy.IOS_PREDICATE,
|
||||
"chain": AppiumBy.IOS_CLASS_CHAIN,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._finder_map: dict[str, str] = {}
|
||||
self._map_cache: dict[str, str] = {}
|
||||
self._initialize()
|
||||
|
||||
@staticmethod
|
||||
def _normalize(text: str) -> str:
|
||||
"""
|
||||
统一清洗逻辑:转小写、去除空格、下划线、横杠
|
||||
"""
|
||||
if not isinstance(text, str):
|
||||
raise TypeError(f"Locator strategy must be a string, got {type(text).__name__} instead.")
|
||||
return text.lower().strip().replace('_', '').replace(' ', '').replace('-', '')
|
||||
|
||||
def _initialize(self) -> None:
|
||||
"""初始化基础映射表"""
|
||||
# 1. 动态加载 AppiumBy 常量值
|
||||
for attr_name in dir(AppiumBy):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
|
||||
attr_value = getattr(AppiumBy, attr_name)
|
||||
if isinstance(attr_value, str):
|
||||
# "class name" -> classname,"class_name" -> classname
|
||||
self._finder_map[self._normalize(attr_value)] = attr_value
|
||||
|
||||
# 2. 加载内置简写(会覆盖同名的策略)
|
||||
self._finder_map.update(self._BUILTIN_SHORTCUTS)
|
||||
|
||||
# 3. 备份初始状态
|
||||
self._map_cache = self._finder_map.copy()
|
||||
|
||||
def convert(self, by_value: ByType | str) -> str:
|
||||
"""
|
||||
将模糊或简写的定位方式转换为 Appium 标准定位字符串
|
||||
:raises ValueError: 当定位方式不支持时抛出
|
||||
"""
|
||||
if not by_value or not isinstance(by_value, str):
|
||||
raise ValueError(f"Invalid selector type: {type(by_value)}. Expected a string.")
|
||||
|
||||
clean_key = self._normalize(by_value)
|
||||
target = self._finder_map.get(clean_key)
|
||||
|
||||
if target is None:
|
||||
raise ValueError(f"Unsupported locator strategy: '{by_value}'.")
|
||||
return target
|
||||
|
||||
def register_custom_finder(self, alias: str, target: str) -> None:
|
||||
"""注册自定义定位策略"""
|
||||
self._finder_map[self._normalize(alias)] = target
|
||||
|
||||
def clear_custom_finders(self) -> None:
|
||||
"""重置回初始官方/内置状态"""
|
||||
self._finder_map = self._map_cache.copy()
|
||||
|
||||
def get_all_finders(self) -> list[str]:
|
||||
"""返回当前所有支持的策略 key(用于调试)"""
|
||||
return list(self._finder_map.keys())
|
||||
|
||||
|
||||
# 导出单例,方便直接使用
|
||||
converter = FinderConverter()
|
||||
by_converter = converter.convert
|
||||
register_custom_finder = converter.register_custom_finder
|
||||
|
||||
__all__=["by_converter", "register_custom_finder"]
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 1. 测试标准转换与内置简写
|
||||
print(f"ID 转换: {by_converter('id')}") # 输出: id
|
||||
print(f"AID 简写转换: {by_converter('aid')}") # 输出: accessibility id
|
||||
print(f"CSS 简写转换: {by_converter('css')}") # 输出: css selector
|
||||
|
||||
# 2. 测试强大的归一化容错 (空格、下划线、横杠、大小写)
|
||||
print(f"类链容错: {by_converter(' -Ios-Class-Chain ')}") # 输出: -ios class chain
|
||||
print(f"UIAutomator 容错: {by_converter('UI_AUTOMATOR')}") # 输出: -android uiautomator
|
||||
|
||||
# 3. 测试自定义注册
|
||||
register_custom_finder("my_text", "-android uiautomator")
|
||||
print(f"自定义注册测试: {by_converter('my_text')}") # 输出: -android uiautomator
|
||||
|
||||
# 4. 测试重置功能
|
||||
converter.clear_custom_finders()
|
||||
print("已重置自定义查找器")
|
||||
try:
|
||||
by_converter("my_text")
|
||||
except ValueError as e:
|
||||
print(f"验证重置成功 (捕获异常): {e}")
|
||||
|
||||
# 5. 查看当前全量支持的归一化后的 Key
|
||||
print(f"当前支持的策略总数: {len(converter.get_all_finders())}")
|
||||
print(f"前 5 个策略示例: {converter.get_all_finders()[:5]}")
|
||||
# 6. 增加类型非法测试
|
||||
print("\n--- 异常类型测试 ---")
|
||||
try:
|
||||
by_converter(123) # 传入数字
|
||||
except TypeError as e:
|
||||
print(f"验证类型拦截成功: {e}")
|
||||
|
||||
try:
|
||||
by_converter(None) # 传入 None
|
||||
except TypeError as e:
|
||||
print(f"验证空值拦截成功: {e}")
|
||||
Reference in New Issue
Block a user