UI 自动化测试框架的 “异常处理核心模块”,核心作用是:

  1. 定义一套 统一格式的 UI 自动化相关异常(如元素未找到、超时、不可交互);
  2. 自动收集异常上下文(如哪个类 / 方法抛错、当前页面标签),方便定位问题;
  3. 提供 异常追踪、去重处理 机制,避免重复处理同一异常;
  4. 让异常信息更结构化、易读(如 JSON 格式输出),降低调试成本。

一、核心基础类:UIAutoBaseException(所有 UI 自动化异常的父类)

所有和 UI 操作相关的异常(如超时、元素未找到)都继承自它,负责统一异常的 “基础格式” 和 “上下文收集能力”。

1. __init__ 方法:初始化异常核心属性

def __init__(self, message:str = '',
             additional_info:dict = {},
             context_info:dict = {},
             exception_type="UIAutoBaseException"):
    self.message = message  # 核心错误信息(如“元素查找超时”)
    self.exception_type = exception_type  # 异常类型(如“TimeOutException”)
    self.additional_info = additional_info  # 额外信息(如定位器信息、浏览器版本)
    self.context_info = context_info  # 上下文信息(哪个类/方法抛错、文件名/行号)
    self.exception_id = id(self)  # 异常唯一标识(用于去重处理)
    self.original_exception = None  # 存储原始异常(包装其他异常时用)
    super().__init__(self.message)  # 调用父类 Exception 的初始化
    ExceptionTracker.register_exception(self)  # 自动注册到全局异常追踪器

  • 关键作用
    给所有 UI 异常统一 “四要素”:错误信息、异常类型、上下文、额外信息,避免不同异常格式混乱;同时自动把异常加入全局追踪,方便后续获取。

2. wrap 方法:包装原始异常,形成异常链

def wrap(self, original_exception):
    self.original_exception = original_exception  # 保存原始异常
    self.__cause__ = original_exception  # Python 标准异常链(用 raise ... from 关联)
    self.__suppress_context__ = True  # 抑制冗余的上下文显示(只显示核心异常链)
    return self

  • 场景举例
    若 “元素查找超时” 是因为 “网络延迟导致浏览器未响应”,可通过 wrap 把 “网络异常” 作为原始异常包装进来,调试时能看到完整的错误根源,而非只看到 “超时” 表面错误。

3. wrap_exception 静态方法:统一包装非自定义异常

@staticmethod
def wrap_exception(exception):
    if isinstance(exception, UIAutoBaseException):
        return exception  # 已是自定义异常,直接返回
    # 把 Python 内置异常(如 ValueError、TimeoutError)转成自定义格式
    return UIAutoBaseException(
        message=f"{type(exception).__name__}: {str(exception)}",
        exception_type=type(exception).__name__
    ).wrap(exception)

  • 作用
    框架中可能抛出 Python 内置异常(如 KeyError),此方法将其统一转为 UIAutoBaseException 格式,保证异常处理逻辑一致。

4. getCurrentTabTitle 方法:获取当前页面标签名

def getCurrentTabTitle(self):
    from frameworkCore.business.runContext import RunContext
    from frameworkCore.driver.by import By
    from frameworkCore.driver.locator import Locator
    currentTabLoc = Locator(By.XPATH, '//div[contains(@class,"TabLabelActive")]/span', desc="获取当前窗口文本", wait=20)
    try:
        driver = RunContext.getRunContext().driver.get_driver()  # 从上下文拿浏览器驱动
        text = driver.find_element(currentTabLoc.by, currentTabLoc.path).text.replace('\t', '')
        return text  # 返回当前激活的标签页标题(如“业务月结”)
    except Exception as e:
        return ""

  • 核心价值
    异常发生时,自动记录 “当前在哪个页面标签”(如 “客户管理”“报表统计”),让调试者一眼知道错误发生的页面,无需手动排查。

5. 上下文收集方法:get_caller_context & get_caller_context_other

这两个方法是 “异常上下文收集的核心”,通过 inspect 模块解析调用堆栈,找到 “真正抛出异常的业务类 / 方法”(而非框架底层方法)。

以 get_caller_context 为例:

@staticmethod
def get_caller_context(method_name, reason=""):
    result = dict()
    try:
        stack = inspect.stack()  # 获取调用堆栈(从当前方法往上的所有调用关系)
        for frame in stack[1:]:  # 跳过当前方法,从上层开始遍历
            funcname = frame.function  # 当前帧的方法名
            code_context = frame.code_context  # 当前帧的代码片段
            
            # 跳过框架底层方法(如 find_child、get_element),找到业务方法
            if funcname in ["find_child", "get_element", "find_global"]:
                continue
            
            # 找到包含目标方法(如“find_element”)的代码帧
            if code_context and method_name in code_context[0]:
                caller_locals = frame[0].f_locals  # 获取当前帧的局部变量
                caller_instance = caller_locals.get('self')  # 拿到“self”(业务类实例)
                lineno = frame.lineno  # 行号
                filename = frame.filename  # 文件名
                
                if caller_instance:
                    # 组装上下文:业务类名、文件名/行号、方法名、错误原因
                    result = {
                        "ctrl_name": caller_instance.__class__.__name__,  # 如“DailybsPage”
                        "filename": f"{filename}:{lineno}",  # 如“D:/test.py:29”
                        "method_name": funcname,  # 如“取消这个月的月结”
                        "reason": reason  # 如“组件查找超时”
                    }
                break  # 找到最近的业务方法后停止
        return result
    except Exception:
        return result

  • 场景举例
    若底层 find_element 方法抛错,通过遍历堆栈,能定位到是 DailybsPage 类的 取消这个月的月结 方法调用了 find_element,最终上下文会包含 ctrl_name: DailybsPage,让调试者直接知道是 “业务月结页面的取消方法出了问题”。

6. __str__ 方法:结构化输出异常信息

def __str__(self):
    msg = {
        "message": self.message,
        "exception_type": self.exception_type,
        "context_info": self.context_info,
        "additional": self.additional_info,
    }
    info = json.dumps(msg, ensure_ascii=False, indent=4)  # 转 JSON 格式,易读
    info = info.replace(r'\"', "'")  # 处理转义字符
    return info

  • 输出效果
    异常打印时会显示结构化的 JSON,例如:
    {
        "message": "查找元素超时",
        "exception_type": "TimeOutException",
        "context_info": {
            "ctrl_name": "DailybsPage",
            "filename": "D:/test.py:29",
            "method_name": "取消这个月的月结",
            "current_table": "业务月结"
        },
        "additional": {
            "locator": "控件:日期卡片 查找方式:xpath,定位://div[@class='date-card'],超时等待:10"
        }
    }
    

    比原生异常的 “一堆堆栈” 更直观,关键信息一目了然。

二、辅助工具类:异常追踪与协调

1. ExceptionTracker:全局异常追踪器

class ExceptionTracker:
    _last_exception = None  # 记录最后一个抛出的异常
    _processed_ids = set()  # 记录已处理的异常 ID(避免重复处理)

    @classmethod
    def register_exception(cls, exception):
        # 避免重复注册同一异常
        exc_id = id(exception)
        if exc_id not in cls._processed_ids:
            cls._last_exception = exception
            cls._processed_ids.add(exc_id)

    @classmethod
    def get_last_exception(cls):
        return cls._last_exception  # 测试报告/日志中可获取最新异常

    @classmethod
    def clear_last_exception(cls):
        cls._last_exception = None
        cls._processed_ids.clear()  # 重置追踪器

  • 作用
    全局记录异常,方便后续模块(如测试报告生成器)获取 “最新的异常信息”,无需手动传递异常对象。

2. ExceptionCoordinator:异常协调器(防止重复处理)

class ExceptionCoordinator:
    _handling_exceptions = {}  # 记录正在处理的异常:{exc_id: {handlers: [], current_handler: ""}}

    @classmethod
    def mark_handling(cls, exc_id, handler_name):
        # 标记异常开始处理(如“TimeOutException”处理器开始处理)
        if exc_id not in cls._handling_exceptions:
            cls._handling_exceptions[exc_id] = {
                'handlers': [],
                'current_handler': handler_name
            }
        return cls._handling_exceptions[exc_id]

    @classmethod
    def should_handle(cls, exc_id):
        # 判断异常是否需要处理(只处理一次)
        if exc_id not in cls._handling_exceptions:
            return True
        return not cls._handling_exceptions[exc_id].get('handled', False)

    @classmethod
    def mark_handled(cls, exc_id):
        # 标记异常已处理(避免其他处理器重复处理)
        if exc_id in cls._handling_exceptions:
            cls._handling_exceptions[exc_id]['handled'] = True

  • 场景举例
    框架中可能有多个异常处理器(如 “日志记录处理器”“重试处理器”),此协调器确保同一异常(如超时)只被处理一次(比如重试后成功,就不用再记录错误日志),避免资源浪费。

3. TrackedExceptionMixin:异常追踪混入类

class TrackedExceptionMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)  # 调用父类初始化
        ExceptionTracker.register_exception(self)  # 自动注册到追踪器
  • 作用
    “混入类(Mixin)” 是为了复用代码 —— 所有继承它的异常类,都不用手动写 ExceptionTracker.register_exception,初始化时会自动注册到追踪器,简化代码。

三、具体业务异常类:针对 UI 自动化的常见错误场景

这些类继承自 UIAutoBaseException 或结合 TrackedExceptionMixin,针对具体错误场景定义,让异常类型更明确。

1. TimeOutException:操作超时异常(最常见)

class TimeOutException(TrackedExceptionMixin, UIAutoBaseException):
    """操作超时异常(如查找元素超时)"""
    def __init__(self,message='', additional_info={}):
        # 收集上下文:找到调用“find_element”的业务方法
        context_info = UIAutoBaseException.get_caller_context("find_element", '组件查找超时')
        current_table = self.getCurrentTabTitle()  # 加入当前页面标签
        context_info.update({"current_table": current_table})
        # 调用父类初始化,指定异常类型为“TimeOutException”
        super().__init__(message=message,
                         context_info=context_info,
                         additional_info=additional_info,
                         exception_type="TimeOutException")
        # 标记异常开始处理(避免重复)
        ExceptionCoordinator.mark_handling(self.exception_id, "TimeOutException")

  • 触发场景
    查找元素超过指定时间(如 10 秒)未找到,会抛出此异常,自动包含 “哪个类 / 方法超时、当前页面标签、定位器信息”。

2. ElementNotFoundException:元素未找到异常

class ElementNotFoundException(TrackedExceptionMixin, UIAutoBaseException):
    """当UI元素未找到时抛出的异常"""
    def __init__(self, element_name: str, *args):
        self.message = f"元素'{element_name}'未找到"  # 明确提示哪个元素未找到
        super().__init__(self.message, *args)

  • 场景举例
    定位 “提交按钮” 时,页面中没有该元素,抛出 ElementNotFoundException("提交按钮"),错误信息直接显示 “元素 ' 提交按钮 ' 未找到”,无需额外解析。

3. ElementNotInteractableException:元素不可交互异常

class ElementNotInteractableException(UIAutoBaseException):
    """元素不可交互时抛出的异常(如按钮灰显)"""
    def __init__(self, element_name: str, *args):
        self.message = f"元素'{element_name}'不可交互"
        super().__init__(self.message, *args)

  • 场景举例
    页面上的 “删除按钮” 处于灰显状态(disabled),此时尝试点击会抛出此异常,明确提示 “元素 ' 删除按钮 ' 不可交互”。

4. CustomAssertionError:自定义断言异常

class CustomAssertionError(TrackedExceptionMixin, AssertionError):
    def __init__(self, desc):
        # 收集断言失败的上下文(如哪个方法的断言错了)
        context_info = UIAutoBaseException.get_caller_context("AssertionError", '数据比对异常')
        super().__init__(desc)  # 继承 Python 原生 AssertionError
        self.context_info = context_info

  • 场景举例
    测试 “提交后金额应为 100” 时,实际金额是 90,抛出 CustomAssertionError("金额不符:预期100,实际90"),同时包含上下文(如 “ReportPage 的 verify_amount 方法”)。

5. AvoidLoginError:登录规避相关异常

class AvoidLoginError(Exception):
    def __init__(self, err=''):
        Exception.__init__(self, err)

  • 作用
    简单的登录相关异常(如 “自动登录失败”“跳过登录时出错”),未继承 UIAutoBaseException,可能用于框架初始化阶段的登录逻辑,场景较独立。

⭐ 详细讲解 get_caller_context

第一步:先搭一个 “触发这个方法的场景”

在讲方法之前,先明确 谁会调用这个方法。我们模拟一个真实的 UI 自动化场景:

  1. 框架底层类ElementBase(有 find_element 方法,抛错时会调用 get_caller_context);
  2. 业务类DailybsPage(月结页面类,有 取消月结 方法,会调用 find_element);
  3. 测试用例:调用 DailybsPage.取消月结,触发 find_element 抛错,进而执行 get_caller_context

模拟代码如下(先看场景,不用记):

# 模拟框架底层类:ElementBase(有find_element方法)
class ElementBase:
    def find_element(self, locator):
        # 假设查找元素失败,抛异常,调用get_caller_context
        from 你的异常文件 import UIAutoBaseException
        context = UIAutoBaseException.get_caller_context(method_name="find_element", reason="元素查找超时")
        raise Exception(f"查找元素失败!上下文:{context}")

# 业务类:DailybsPage(月结页面,继承ElementBase)
class DailybsPage(ElementBase):
    def 取消月结(self):
        # 调用框架的find_element方法(这一步会抛错)
        self.find_element(locator="日期卡片")  # 假设这行在文件第15行

# 测试用例:触发整个流程
if __name__ == "__main__":
    page = DailybsPage()
    page.取消月结()  # 调用业务方法,触发find_element抛错

第二步:跟踪 get_caller_context 的执行过程

当 DailybsPage.取消月结 调用 find_element 时,find_element 会执行 UIAutoBaseException.get_caller_context(method_name="find_element", reason="元素查找超时")

  • 传入参数:method_name="find_element"reason="元素查找超时"
  • 目标:找到 “谁调用了 find_element”(即 DailybsPage.取消月结

第 1 行:初始化结果字典
result = dict()
  • 干了什么:创建一个空字典,用来存最终要返回的上下文信息(比如业务类名、文件名)。
  • 变量值result = {}
  • 输出:无(只是初始化变量)

第 2 行:进入 try 块
try:
  • 干了什么:捕获后续代码可能抛出的异常(比如 inspect.stack() 出错),避免方法本身崩溃。
  • 变量值:无变化
  • 输出:无

第 3 行:获取调用堆栈
stack = inspect.stack()
  • 干了什么:用 inspect.stack() 获取当前的 “调用堆栈”(即函数调用的顺序,从当前方法往上追溯)。
  • 变量值stack 是一个列表,每个元素是一个 “堆栈帧(frame)”,记录一个函数的信息。
    结合我们的场景,stack 内容简化后如下(按调用顺序反向排列):
  • [
        # 帧0:当前方法 get_caller_context(自己调用自己?不,是inspect记录的当前方法)
        FrameInfo(
            function='get_caller_context',  # 函数名
            code_context=['stack = inspect.stack()\n'],  # 当前行代码
            filename='异常文件.py',  # 所在文件
            lineno=100,  # 行号
            f_locals={...}  # 局部变量(暂时空)
        ),
        # 帧1:调用 get_caller_context 的方法(框架底层的 find_element)
        FrameInfo(
            function='find_element',  # 函数名:框架的find_element
            code_context=['context = UIAutoBaseException.get_caller_context(...)\n'],  # 调用代码
            filename='框架类.py',  # 所在文件
            lineno=20,  # 行号
            f_locals={'self': <ElementBase实例>, 'locator': '日期卡片'}  # 局部变量
        ),
        # 帧2:调用 find_element 的方法(业务类的 取消月结)
        FrameInfo(
            function='取消月结',  # 函数名:业务方法
            code_context=['self.find_element(locator="日期卡片")\n'],  # 调用代码
            filename='业务类.py',  # 所在文件
            lineno=15,  # 行号(我们场景中设定的行号)
            f_locals={'self': <DailybsPage实例>}  # 局部变量:self是DailybsPage的实例
        ),
        # 帧3:调用 取消月结 的方法(测试用例)
        FrameInfo(
            function='<module>',  # 模块级代码(测试用例)
            code_context=['page.取消月结()\n'],  # 调用代码
            filename='测试用例.py',  # 所在文件
            lineno=30,  # 行号
            f_locals={'page': <DailybsPage实例>}  # 局部变量
        )
    ]
    
  • 输出:无(只是把堆栈信息存到 stack 变量)

第 4 行:遍历堆栈(跳过当前方法)
for frame in stack[1:]:

  • 干了什么:遍历 stack 列表,但从索引 1 开始(跳过索引 0 的 get_caller_context 自身),因为我们要找的是 “调用当前方法的上层方法”。
  • 变量值:第一次循环时,frame 是 stack[1](即 find_element 对应的帧);第二次循环是 stack[2](即 取消月结 对应的帧)。
  • 输出:无(开始循环)

第 5-6 行:从帧中获取函数名和代码片段
funcname = frame.function
code_context = frame.code_context

  • 干了什么:从当前遍历的 “帧” 中,提取两个关键信息:
    1. funcname:当前帧对应的函数名(比如 find_element 或 取消月结);
    2. code_context:当前帧中正在执行的代码片段(比如调用 find_element 的那行代码)。
  • 第一次循环(frame=stack [1],find_element 帧)
    • funcname = "find_element"
    • code_context = ['context = UIAutoBaseException.get_caller_context(...)\n']
  • 第二次循环(frame=stack [2],取消月结帧)
    • funcname = "取消月结"
    • code_context = ['self.find_element(locator="日期卡片")\n']
  • 输出:无(只是提取变量)

第 7 行:判断代码片段是否包含目标方法名
if code_context and method_name in code_context[0]:

  • 干了什么:检查两个条件:
    1. code_context 不为空(有些帧可能没有代码片段);
    2. 代码片段中包含我们要找的 method_name(即 "find_element")。
  • 第一次循环(find_element 帧)
    • code_context 不为空,且 code_context[0] 是调用 get_caller_context 的代码,不包含 "find_element" → 条件不成立,跳过这个帧的后续逻辑,进入下一次循环。
  • 第二次循环(取消月结帧)
    • code_context 不为空,且 code_context[0] 是 "self.find_element(locator="日期卡片")\n"包含 "find_element" → 条件成立,进入后续逻辑。
  • 输出:无(只是条件判断)

第 8-17 行:跳过框架底层方法(第一次循环不触发,第二次循环也不触发)
if funcname == "find_child" or funcname == "find_childs":
    method_name = "find_child"
    continue
if funcname == "get_element" or funcname == "get_elements":
    method_name = "get_element"
    continue
if funcname == "find_global" or funcname == "find_globals":
    method_name = "find_global"
    continue

  • 干了什么:如果当前帧的函数是框架底层方法(如 find_childget_element),就跳过(continue),继续往上找业务方法。
  • 第二次循环(取消月结帧)
    • funcname = "取消月结",不是框架底层方法 → 不执行这些 if,直接进入下一步。
  • 输出:无

第 18 行:获取当前帧的局部变量
caller_locals = frame[0].f_locals

  • 干了什么frame[0] 是 “帧对象”,f_locals 是该帧对应的函数的局部变量字典(比如实例方法的 self 就在这里)。
  • 第二次循环(取消月结帧)
    • caller_locals = {'self': <DailybsPage实例>}self 是 DailybsPage 类的实例,即业务类实例)
  • 输出:无(只是提取局部变量)

第 19 行:从局部变量中拿 “self”
caller_instance = caller_locals.get('self', None)

  • 干了什么:从局部变量中获取 self(实例方法的第一个参数,代表调用该方法的类实例)。如果没有 self(比如普通函数),就返回 None
  • 第二次循环(取消月结帧)
    • caller_instance = <DailybsPage实例>(拿到了业务类的实例)
  • 输出:无

第 20-22 行:获取代码行号、文件名、函数名
lineno = frame.lineno
filename = frame.filename
funcname = frame.function

  • 干了什么:从当前帧中提取三个定位信息:
    1. lineno:代码行号;
    2. filename:代码所在文件;
    3. funcname:函数名(再次确认,避免之前的变量覆盖)。
  • 第二次循环(取消月结帧)
    • lineno = 15(我们场景中 取消月结 方法调用 find_element 的行号)
    • filename = "业务类.py"(业务类所在的文件)
    • funcname = "取消月结"(业务方法名)
  • 输出:无

第 23 行:判断是否拿到了业务类实例
if caller_instance:

  • 干了什么:检查是否拿到了 self(即业务类实例)。
  • 第二次循环(取消月结帧)
    • caller_instance = <DailybsPage实例> → 条件成立,进入 if 内部。
  • 输出:无

第 24-29 行:组装结果(核心!)
caller_class_name = caller_instance.__class__.__name__
result = {
    "ctrl_name": caller_class_name,
    "filename": filename + ": " + str(lineno),
    "method_name": funcname,
    "reason": reason,
}

  • 干了什么
    1. caller_instance.__class__.__name__:通过实例拿到它的类名(比如 DailybsPage);
    2. 把 “类名、文件名 + 行号、方法名、错误原因” 组装成字典,存入 result
  • 第二次循环(取消月结帧)
    • caller_class_name = "DailybsPage"(业务类名)
    • result = { "ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时" }
  • 输出:无(只是组装结果字典)

第 30-35 行:else 分支(当前场景不触发)
else:
    result = {
        "ctrl_name": None,
        "filename": f"{filename}:{lineno}",
        "method_name": funcname,
        "reason": reason,
    }

  • 干了什么:如果没拿到 self(比如是普通函数),就把 ctrl_name 设为 None
  • 当前场景:拿到了 self,所以不执行。
  • 输出:无

第 36 行:循环结束,返回结果
return result

  • 干了什么:遍历完堆栈后,返回组装好的 result 字典。
  • 变量值result = {"ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时"}
  • 输出:这个字典会被返回给 find_element 方法,最终作为异常信息的一部分打印出来,比如
  • 查找元素失败!上下文:{"ctrl_name": "DailybsPage", "filename": "业务类.py: 15", "method_name": "取消月结", "reason": "元素查找超时"}
    

第 37-38 行:异常捕获(当前场景不触发)
except Exception:
    return result

  • 干了什么:如果中间任何一步出错(比如 inspect.stack() 失败),就返回空的 result(避免方法抛错)。
  • 当前场景:无异常,所以不执行。
  • 输出:无

⭐ 了解多继承的混入类

一、先明确:TrackedExceptionMixin 自身的直接父类

class TrackedExceptionMixin:
    """异常追踪混入类"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)  # 确保调用父类的初始化
        ExceptionTracker.register_exception(self)  # 注册到全局追踪器

这里没有显式写 class TrackedExceptionMixin(父类):,在 Python 3 中,所有未显式指定父类的类,默认父类都是 object(Python 中所有类的 “顶层基类”)。

所以:
TrackedExceptionMixin 自身的直接父类是 object

二、但更重要的是:Mixin 类的 “协作父类”(它为什么这么设计?)

TrackedExceptionMixin 是一个 Mixin 类(混入类)—— 它的核心作用不是 “单独作为父类”,而是 “混入到其他类中,复用代码”。它的 super().__init__(*args, **kwargs) 也不是为了调用 object 的 __init__object 的 __init__ 基本没逻辑),而是为了 配合其他 “业务父类” 完成初始化

这个 Mixin 类的实际用法是 多继承

class TimeOutException(TrackedExceptionMixin, UIAutoBaseException):
    """操作超时异常"""
    def __init__(self,message='', additional_info={}):
        # ... 初始化逻辑
        super().__init__(message=message, ...)  # 调用父类初始化

这里 TimeOutException 同时继承了两个类:

  1. 第一个父类:TrackedExceptionMixin(Mixin 类,提供 “异常注册到追踪器” 的复用逻辑);
  2. 第二个父类:UIAutoBaseException业务父类,真正的异常核心逻辑,比如异常格式、上下文收集)。

三、关键:super().__init__ 到底调用哪个父类?(Python MRO 规则)

Mixin 类中的 super().__init__(*args, **kwargs) 不是调用 object 的 __init__,而是遵循 Python 的 MRO(方法解析顺序)—— 按 “继承列表的顺序 + 深度优先” 找到下一个需要初始化的父类。

以 TimeOutException 为例,它的 MRO 顺序是:
TimeOutException → TrackedExceptionMixin → UIAutoBaseException → Exception → object

所以当 TimeOutException 执行 super().__init__ 时:

  1. 先进入 TrackedExceptionMixin 的 __init__
  2. TrackedExceptionMixin 的 super().__init__(*args, **kwargs) 会按 MRO 顺序,跳过 object,直接调用下一个父类 UIAutoBaseException 的 __init__
  3. 这样就同时完成了两件事:
    • Mixin 类的逻辑:ExceptionTracker.register_exception(self)(把异常注册到全局追踪器);
    • 业务父类的逻辑:UIAutoBaseException 的初始化(设置异常信息、上下文等)。

总结:Mixin 类的父类逻辑

  1. 自身直接父类object(Python 3 默认),但这几乎不重要;
  2. 实际协作父类:它被多继承时,紧跟在它后面的 “业务类”(如 UIAutoBaseException),这才是 Mixin 类的核心 —— 通过 super() 配合业务类完成初始化,同时复用自己的代码(如异常注册)。

Mixin 类的设计精髓就是:不破坏原有类的继承关系,通过多继承 “插入” 复用逻辑,让代码更灵活(比如其他异常类如 ElementNotFoundException 也能继承这个 Mixin,快速获得 “异常追踪” 能力)。

⭐ TrackedException 类

是一个增强型异常类,继承自 Python 内置的 Exception,主要作用是在普通异常的基础上,添加上下文追踪、原始异常包装、全局注册等功能,方便在复杂系统(比如 UI 自动化框架)中更精准地定位和管理异常。

1. 方法定义与参数说明

def __init__(self, message, context_info=None, original_exception=None):

  • 参数作用
    • message:异常的核心错误信息(必填,比如 “元素点击失败”);
    • context_info:异常发生的上下文信息(可选,比如哪个类 / 方法出错、代码位置等);
    • original_exception:被包装的原始异常(可选,比如将底层的 TimeoutException 包装成 TrackedException 时使用)。

2. 确保上下文信息(context_info)规范化

# 确保 context_info 包含规范化的字段
if not context_info:
    context_info = UIAutoBaseException.get_caller_context_other("general_error", "未知异常")

  • 作用:保证 context_info 始终有值,且格式统一(方便后续日志输出或追踪)。
  • 细节
    • 如果外部调用时没传 context_info(比如 raise TrackedException("错误")),就通过 UIAutoBaseException.get_caller_context_other 生成一个默认上下文;
    • 默认上下文类型是 "general_error"(通用错误),原因是 "未知异常",同时包含调用堆栈信息(类名、方法名、代码位置等,由 get_caller_context_other 实现)。

3. 调用父类(Exception)的初始化

super().__init__(message)

  • 作用:继承 Python 内置 Exception 的核心功能,确保 TrackedException 是一个 “标准异常”(比如可以被 try-except 捕获,包含 message 属性等)。
  • 为什么要调用:如果不调用 super()Exception 的初始化逻辑(比如 message 的存储)不会执行,可能导致 str(exception) 等基础功能异常。

4. 初始化核心属性(保存上下文与原始异常)

self.context_info = context_info  # 保存上下文信息
self.original_exception = original_exception  # 保存原始异常(如果有)
self.handling_markers = {}  # 用于标记异常的处理状态(如是否被重试、忽略等)

  • 属性用途
    • context_info:存储异常发生的环境信息(比如 {"ctrl_name": "LoginPage", "method_name": "输入密码", "filename": "login.py:25"}),方便定位问题;
    • original_exception:当需要 “包装” 底层异常时使用(比如把 Selenium 的 NoSuchElementException 包装成 TrackedException),保留原始异常的全部信息;
    • handling_markers:一个空字典,用于后续标记异常的处理状态(比如 {"retried": True, "ignored": False} 表示 “已重试但未忽略”)。

5. 处理原始异常(original_exception):保留关键信息

# 保留原始堆栈
if original_exception:
    # 获取原始异常的 exception_id(如果有),否则用原始异常的内存地址作为ID
    self.exception_id = original_exception.exception_id if hasattr(original_exception, 'exception_id') else id(original_exception)
    # 获取原始异常的类型(如果有),否则用原始异常的类名
    self.exception_type = getattr(original_exception, 'exception_type', type(original_exception).__name__)
    # 保留原始异常的堆栈跟踪(__traceback__ 是 Python 存储堆栈的属性)
    self.__traceback__ = original_exception.__traceback__

  • 核心目的:当 TrackedException 用于包装其他异常时(比如将底层框架异常转换为业务异常),完整保留原始异常的关键信息,避免调试时丢失上下文。
  • 细节说明
    • exception_id:异常的唯一标识(用于全局追踪,避免重复处理),优先用原始异常自带的 exception_id(如果是 TrackedException 或其子类),否则用 id(original_exception)(内存地址,确保唯一);
    • exception_type:记录原始异常的类型(比如 NoSuchElementException),方便区分异常来源;
    • __traceback__:保留原始异常的堆栈跟踪(即错误发生的代码调用链),这样用 traceback 模块打印时,能看到最底层的错误位置。

6. 无原始异常时:为当前异常创建标识

else:
    # 为自己创建属性
    self.exception_id = id(self)  # 用当前异常实例的内存地址作为唯一ID
    self.exception_type = "TrackedException"  # 明确当前异常的类型

  • 作用:如果没有包装原始异常(即直接抛出 TrackedException),则为当前异常生成自己的 exception_id 和 exception_type,确保标识完整。

7. 注册到全局追踪器

# 注册到全局追踪器
ExceptionTracker.register_exception(self)

  • 作用:将当前异常实例注册到全局的 ExceptionTracker 中,方便后续在系统任何地方获取 “最近发生的异常” 或 “所有异常记录”。
  • 实际用途:比如在测试报告生成时,可以通过 ExceptionTracker.get_last_exception() 获取最后一个异常,自动填充错误详情;或者在异常监控中,统计异常发生的频率。

总结:TrackedException 的核心价值

这个类通过以下设计,解决了普通异常的痛点:

  1. 上下文追踪:强制保留异常发生的环境信息(类、方法、代码位置),避免 “只知道报错,不知道在哪报错”;
  2. 异常包装:可以安全包装底层异常,保留原始堆栈和标识,方便溯源;
  3. 全局管理:通过注册到 ExceptionTracker,实现异常的集中管理和查询,适合复杂系统的调试和监控。

简单说:TrackedException 是一个 “带黑匣子的异常”,既能像普通异常一样被捕获,又能记录更多调试所需的 “现场信息”,还支持全局追踪。

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐