【问题标题】:Faking a traceback in Python在 Python 中伪造回溯
【发布时间】:2013-10-08 13:17:55
【问题描述】:

我正在编写一个测试运行器。我有一个可以捕获和存储异常的对象,稍后将作为测试失败报告的一部分将其格式化为字符串。我正在尝试对格式化异常的过程进行单元测试。

在我的测试设置中,我不想实际上抛出异常让我的对象捕获,主要是因为这意味着回溯将无法预测。 (如果文件改变长度,回溯中的行号也会改变。)

如何将虚假的回溯附加到异常,以便我可以断言其格式化方式?这甚至可能吗?我正在使用 Python 3.3。

简化示例:

class ExceptionCatcher(object):
    def __init__(self, function_to_try):
        self.f = function_to_try
        self.exception = None
    def try_run(self):
        try:
            self.f()
        except Exception as e:
            self.exception = e

def format_exception_catcher(catcher):
    pass
    # No implementation yet - I'm doing TDD.
    # This'll probably use the 'traceback' module to stringify catcher.exception


class TestFormattingExceptions(unittest.TestCase):
    def test_formatting(self):
        catcher = ExceptionCatcher(None)
        catcher.exception = ValueError("Oh no")

        # do something to catcher.exception so that it has a traceback?

        output_str = format_exception_catcher(catcher)
        self.assertEquals(output_str,
"""Traceback (most recent call last):
  File "nonexistent_file.py", line 100, in nonexistent_function
    raise ValueError("Oh no")
ValueError: Oh no
""")

【问题讨论】:

    标签: python unit-testing testing mocking traceback


    【解决方案1】:

    阅读the source of traceback.py 为我指明了正确的方向。这是我的 hacky 解决方案,其中涉及伪造回溯通常会引用的框架和代码对象。

    import traceback
    
    class FakeCode(object):
        def __init__(self, co_filename, co_name):
            self.co_filename = co_filename
            self.co_name = co_name
    
    
    class FakeFrame(object):
        def __init__(self, f_code, f_globals):
            self.f_code = f_code
            self.f_globals = f_globals
    
    
    class FakeTraceback(object):
        def __init__(self, frames, line_nums):
            if len(frames) != len(line_nums):
                raise ValueError("Ya messed up!")
            self._frames = frames
            self._line_nums = line_nums
            self.tb_frame = frames[0]
            self.tb_lineno = line_nums[0]
    
        @property
        def tb_next(self):
            if len(self._frames) > 1:
                return FakeTraceback(self._frames[1:], self._line_nums[1:])
    
    
    class FakeException(Exception):
        def __init__(self, *args, **kwargs):
            self._tb = None
            super().__init__(*args, **kwargs)
    
        @property
        def __traceback__(self):
            return self._tb
    
        @__traceback__.setter
        def __traceback__(self, value):
            self._tb = value
    
        def with_traceback(self, value):
            self._tb = value
            return self
    
    
    code1 = FakeCode("made_up_filename.py", "non_existent_function")
    code2 = FakeCode("another_non_existent_file.py", "another_non_existent_method")
    frame1 = FakeFrame(code1, {})
    frame2 = FakeFrame(code2, {})
    tb = FakeTraceback([frame1, frame2], [1,3])
    exc = FakeException("yo").with_traceback(tb)
    
    print(''.join(traceback.format_exception(FakeException, exc, tb)))
    # Traceback (most recent call last):
    #   File "made_up_filename.py", line 1, in non_existent_function
    #   File "another_non_existent_file.py", line 3, in another_non_existent_method
    # FakeException: yo
    

    感谢@User 提供FakeException,这是必要的,因为真正的异常类型检查with_traceback() 的参数。

    这个版本确实有一些限制:

    • 它不会打印每个堆栈帧的代码行,因为它是真实的 回溯会,因为format_exception 去寻找 代码来自的真实文件(在我们的案例中不存在)。 如果你想让这个工作,你需要将假数据插入 linecache的 缓存(因为traceback 使用linecache 来获取源 代码),根据@User's answer below

    • 您实际上也不能 raise exc 并期待虚假的回溯 为了生存。

    • 更一般地说,如果您有遍历回溯的客户端代码 与traceback 不同的方式(例如大部分inspect 模块),这些假货可能不起作用。你需要添加任何东西 客户端代码期望的额外属性。

    这些限制对我的目的来说很好 - 我只是将它用作调用 traceback 的代码的测试替身 - 但如果你想做更多涉及的回溯操作,looks like 你可能不得不去到C级。

    【讨论】:

    • 我同意 C/Level 但如果您需要,也可以帮助您处理文件的行。 linecache.updatecache(filename, fake_module_with_a___loader___attribute) 是正确的方向。
    • @User 太棒了!我会调查的。
    • 刚刚调查过你。如果您有新的解决方案,请发表评论并分享! :)
    【解决方案2】:

    EDIT2:

    那是linecache的代码..我会评论它。

    def updatecache(filename, module_globals=None): # module_globals is a dict
            # ...
        if module_globals and '__loader__' in module_globals:
            name = module_globals.get('__name__')
            loader = module_globals['__loader__']
                # module_globals = dict(__name__ = 'somename', __loader__ = loader)
            get_source = getattr(loader, 'get_source', None) 
                # loader must have a 'get_source' function that returns the source
    
            if name and get_source:
                try:
                    data = get_source(name)
                except (ImportError, IOError):
                    pass
                else:
                    if data is None:
                        # No luck, the PEP302 loader cannot find the source
                        # for this module.
                        return []
                    cache[filename] = (
                        len(data), None,
                        [line+'\n' for line in data.splitlines()], fullname
                    )
                    return cache[filename][2]
    

    这意味着在您测试运行之前,只需执行以下操作:

    class Loader:
        def get_source(self):
            return 'source of the module'
    import linecache
    linecache.updatecache(filename, dict(__name__ = 'modulename without <> around', 
                                         __loader__ = Loader()))
    

    'source of the module' 是您测试的模块的来源。

    编辑1:

    到目前为止我的解决方案:

    class MyExeption(Exception):
        _traceback = None
        @property
        def __traceback__(self):
            return self._traceback
        @__traceback__.setter
        def __traceback__(self, value):
            self._traceback = value
        def with_traceback(self, tb_or_none):
            self.__traceback__ = tb_or_none
            return self
    

    现在您可以设置异常的自定义回溯:

    e = MyExeption().with_traceback(1)
    

    如果你重新引发异常,你通常会怎么做:

    raise e.with_traceback(fake_tb)
    

    所有异常打印都经过这个函数:

    import traceback
    traceback.print_exception(_type, _error, _traceback)
    

    希望它以某种方式有所帮助。

    【讨论】:

    • 你将什么样的对象传递给with_traceback()
    【解决方案3】:

    您应该能够简单地 raise 在您的测试运行中想要的任何假异常。 python 异常文档建议您创建一个类并将其作为您的异常。这是文档的第 8.5 节。

    http://docs.python.org/2/tutorial/errors.html

    创建类后应该非常简单。

    【讨论】:

    • -1。这并没有解决我的问题。我不想直接从测试代码中引发异常,因为回溯将是不可预测的行号等等。这是我想伪造的 traceback,而不是异常的类。
    • 如果您不同意创建回溯并调用它并强制执行异常,我看不出您将如何“伪造”回溯。如果您尝试使用catcher.exception 在您的问题中所说的那样获取该回溯,那么您将不得不输出一个异常来执行此操作。即使你创建了一个虚假的回溯来猜测它的格式,如果你不能通过你的实际模块测试它的实现,你会得到什么?
    • Python 允许您检查其运行时堆栈。请参阅 sys._getframe()inspect.stack()。 Tracebacks 是一流的对象,您可以根据需要手动遍历(您可以通过exception.__traceback__sys.exc_info() 获取实时回溯)。考虑到这一点,期望您可以在纯 Python 中进行自己的回溯而不抛出真正的异常并不是太极端。
    • 这个想法是将报告代码与业务逻辑分开测试。我希望能够在不破坏格式化程序测试的情况下改变ExceptionCatcher 的行为,反之亦然,因此测试应该没有相互依赖关系。更重要的是,正如我在我的问题中详述的那样,“真正的”回溯对象很难测试 - 如果我从测试文件中的某一行抛出异常,然后测试文件更改长度,我必须更新行号断言(或以某种方式使断言与行号无关,可能使用正则表达式)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-19
    • 2017-08-31
    相关资源
    最近更新 更多