refactor(core/utils): 重构装饰器架构与日志追踪逻辑
- 优化 将 trace_step 移至 decorators.py,并引入 ContextVar 实现日志层级缩进。 - 新增 完善 StepTracer 和 action_screenshot 的 Docstrings,明确参数含义。 - 移除 清理了 logger.py - 优化 main.py 中重复的目录创建逻辑及旧版冗余注释。 - 规范 修正函数命名,提升代码在 BasePage 方案下的复用性。
This commit is contained in:
@@ -1,12 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
@author: CNWei,ChenWei
|
||||
@Software: PyCharm
|
||||
@contact: t6g888@163.com
|
||||
@file: decorators
|
||||
@date: 2026/1/15 11:30
|
||||
@desc:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Union, Callable
|
||||
|
||||
from contextvars import ContextVar
|
||||
from contextlib import ContextDecorator
|
||||
|
||||
from core.custom_expected_conditions import get_condition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 定义一个上下文变量,初始值为 0
|
||||
indent_var = ContextVar("indent_level", default=0)
|
||||
|
||||
|
||||
class StepTracer(ContextDecorator):
|
||||
"""
|
||||
既是装饰器也是上下文管理器
|
||||
职责:负责计时、日志格式化和异常记录
|
||||
"""
|
||||
|
||||
def __init__(self, step_desc, source='wrapper', func_info=None):
|
||||
self.step_desc = step_desc
|
||||
self.logger = logging.getLogger(source)
|
||||
self.func_info = func_info
|
||||
self.start_t = None
|
||||
|
||||
def __enter__(self):
|
||||
# 1. 获取当前层级并计算前缀
|
||||
level = indent_var.get()
|
||||
# 使用 " " (空格) 或 "│ " 作为缩进符号
|
||||
self.prefix = "│ " * level
|
||||
|
||||
self.start_t = time.perf_counter()
|
||||
info = f" | 方法: {self.func_info}" if self.func_info else ""
|
||||
# self.logger.info(f"[步骤开始] | {self.step_desc}{info}")
|
||||
self.logger.info(f"{self.prefix}┌── [步骤开始] | {self.step_desc}{info}")
|
||||
|
||||
# 2. 进入下一层,层级 +1
|
||||
indent_var.set(level + 1)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# 3. 恢复层级,层级 -1
|
||||
level = indent_var.get() - 1
|
||||
indent_var.set(level)
|
||||
|
||||
duration = time.perf_counter() - self.start_t
|
||||
|
||||
prefix = "│ " * level
|
||||
|
||||
if exc_type:
|
||||
# 异常发生
|
||||
# self.logger.error(
|
||||
# f"[步骤失败] {self.step_desc} | 耗时: {duration:.2f}s | 异常: {exc_type.__name__}"
|
||||
# )
|
||||
self.logger.error(
|
||||
f"{prefix}└── [步骤失败] {self.step_desc} | 耗时: {duration:.2f}s | 异常: {exc_type.__name__}"
|
||||
)
|
||||
else:
|
||||
# 执行成功
|
||||
# self.logger.info(f"[步骤成功] {self.step_desc} | 耗时: {duration:.2f}s")
|
||||
self.logger.info(
|
||||
f"{prefix}└── [步骤成功] {self.step_desc} | 耗时: {duration:.2f}s"
|
||||
)
|
||||
# return False 确保异常继续向上抛出,不拦截异常
|
||||
return False
|
||||
|
||||
|
||||
def resolve_wait_method(func):
|
||||
"""
|
||||
@@ -38,17 +111,65 @@ def resolve_wait_method(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def exception_capture(func):
|
||||
def action_screenshot(func):
|
||||
"""
|
||||
仅在原子动作失败时,触发 BasePage 层的业务截图逻辑
|
||||
显式截图装饰器:在方法执行前(或后)立即触发 BasePage 的截图逻辑。
|
||||
用于记录关键操作后的页面状态。
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
# 自动捕获:调用 BasePage 层的 log_screenshot
|
||||
if hasattr(self, "log_screenshot"):
|
||||
self.log_screenshot(f"自动异常捕获{func.__name__}")
|
||||
raise e
|
||||
# 1. 正常执行原方法
|
||||
result = func(self, *args, **kwargs)
|
||||
|
||||
# 2. 执行成功后立即截图(如果你希望在操作后的状态截图)
|
||||
if hasattr(self, "attach_screenshot_bytes"):
|
||||
try:
|
||||
class_name = self.__class__.__name__
|
||||
func_name = func.__name__
|
||||
msg = f"操作记录_{class_name}_{func_name}"
|
||||
# 传入当前方法名作为截图备注
|
||||
self.attach_screenshot_bytes(msg)
|
||||
except Exception as e:
|
||||
logger.warning(f"装饰器执行截图失败: {e}")
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _format_params(func, *args, **kwargs):
|
||||
"""辅助函数:专门处理参数过滤和格式化"""
|
||||
sig = inspect.signature(func)
|
||||
params = list(sig.parameters.values())
|
||||
display_args = args[1:] if params and params[0].name in ('self', 'cls') else args
|
||||
|
||||
# 格式化参数显示,方便阅读
|
||||
args_repr = [repr(a) for a in display_args]
|
||||
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
||||
all_params = ", ".join(args_repr + kwargs_repr)
|
||||
return all_params
|
||||
|
||||
|
||||
def step_trace(step_desc="", source='wrapper'):
|
||||
"""
|
||||
通用执行追踪装饰器:
|
||||
1. 智能识别并过滤 self/cls 参数
|
||||
2. 记录入参、出参、耗时
|
||||
3. 异常自动捕获并记录
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 1. 提取参数显示逻辑 (抽离出来可以作为工具函数)
|
||||
all_params = _format_params(func, *args, **kwargs)
|
||||
func_name = f"{func.__module__}.{func.__name__}"
|
||||
|
||||
# 2. 使用上下文管理器
|
||||
with StepTracer(step_desc, source, f"{func_name}({all_params})"):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
32
utils/dirs_manager.py
Normal file
32
utils/dirs_manager.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
@author: CNWei,ChenWei
|
||||
@Software: PyCharm
|
||||
@contact: t6g888@163.com
|
||||
@file: path_manager
|
||||
@date: 2026/2/3 10:52
|
||||
@desc:
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from core.settings import REQUIRED_DIRS
|
||||
|
||||
|
||||
def ensure_dirs_ok():
|
||||
"""
|
||||
统一管理项目目录的创建逻辑
|
||||
"""
|
||||
for folder in REQUIRED_DIRS:
|
||||
# 使用 exist_ok=True 避免并发冲突
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def ensure_dir(path: Path) -> Path:
|
||||
"""确保路径存在并返回路径本身"""
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
@@ -9,62 +9,4 @@
|
||||
@date: 2026/1/15 11:30
|
||||
@desc:
|
||||
"""
|
||||
import time
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
from core.settings import LOG_DIR
|
||||
|
||||
# 1. 确定日志存储路径
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- 核心特性:装饰器集成 ---
|
||||
def trace_step(step_desc="", source='wrapper'):
|
||||
"""
|
||||
通用执行追踪装饰器:
|
||||
1. 智能识别并过滤 self/cls 参数
|
||||
2. 记录入参、出参、耗时
|
||||
3. 异常自动捕获并记录
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 获取对应的 logger
|
||||
_logger = logging.getLogger(source)
|
||||
# _logger = logging.getLogger(f"{source}.{func.__name__}")
|
||||
|
||||
# 参数解析 (跳过 self/cls)
|
||||
sig = inspect.signature(func)
|
||||
params = list(sig.parameters.values())
|
||||
display_args = args[1:] if params and params[0].name in ('self', 'cls') else args
|
||||
|
||||
# 格式化参数显示,方便阅读
|
||||
args_repr = [repr(a) for a in display_args]
|
||||
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
||||
all_params = ", ".join(args_repr + kwargs_repr)
|
||||
|
||||
func_name = f"{func.__module__}.{func.__name__}"
|
||||
|
||||
_logger.info(f"[步骤开始] | {step_desc} | 方法: {func_name}({all_params})")
|
||||
|
||||
start_t = time.perf_counter()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.perf_counter() - start_t
|
||||
_logger.info(f"[步骤成功] {step_desc} | 耗时: {duration:.2f}s | 返回: {result!r}")
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.perf_counter() - start_t
|
||||
# logging.exception 关键点:它会自动把详细的堆栈信息写入日志文件
|
||||
_logger.error(f"[步骤失败] {step_desc}| 耗时: {duration:.2f}s|异常: {type(e).__name__}")
|
||||
# f"[步骤失败] {step_desc} | 耗时: {duration:.2f}s | 异常: {type(e).__name__}: {e}")
|
||||
raise e
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
48
utils/report_handler.py
Normal file
48
utils/report_handler.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
@author: CNWei,ChenWei
|
||||
@Software: PyCharm
|
||||
@contact: t6g888@163.com
|
||||
@file: report_handler
|
||||
@date: 2026/2/3 13:51
|
||||
@desc:
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
from core.settings import ALLURE_TEMP, REPORT_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_allure_report() -> bool:
|
||||
"""
|
||||
将 JSON 原始数据转换为 HTML 报告
|
||||
"""
|
||||
if not ALLURE_TEMP.exists() or not any(ALLURE_TEMP.iterdir()):
|
||||
logger.warning("未发现 Allure 测试数据,跳过报告生成。")
|
||||
return False
|
||||
|
||||
# 检查环境是否有 allure 命令行工具
|
||||
if not shutil.which("allure"):
|
||||
logger.error("系统未安装 Allure 命令行工具,请先安装:https://allurereport.org/docs/")
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.info("正在生成 Allure HTML 报告...")
|
||||
# --clean 会清理掉 REPORT_DIR 里的旧报告
|
||||
subprocess.run(
|
||||
f'allure generate "{ALLURE_TEMP}" -o "{REPORT_DIR}" --clean',
|
||||
shell=True,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
logger.info(f"Allure 报告已生成至: {REPORT_DIR}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Allure 报告生成失败: {e.stderr}")
|
||||
return False
|
||||
Reference in New Issue
Block a user