3

テストランナーを書いています。例外をキャッチして保存できるオブジェクトがあり、後でテスト失敗レポートの一部として文字列としてフォーマットされます。例外をフォーマットする手順を単体テストしようとしています。

私のテスト セットアップでは、オブジェクトがキャッチするために実際に例外をスローしたくありません。これは主に、トレースバックが予測できないことを意味するためです。(ファイルの長さが変わると、トレースバックの行番号が変わります。)

偽のトレースバックを例外に添付して、それがフォーマットされている方法についてアサーションできるようにするにはどうすればよいですか? これは可能ですか?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
""")
4

3 に答える 3

5

のソースをtraceback.py読んで、正しい方向に私を指摘しました。これは、トレースバックが通常参照を保持するフレームとコードオブジェクトを偽造することを含む、私のハックなソリューションです。

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、コードの元となった実際のファイル (今回の場合は存在しません) を探すためです。これを機能させたい場合は 、以下の@Userの回答linecacheに従って、のキャッシュに偽のデータを挿入する必要があります(tracebackを使用linecacheしてソースコードを取得するため)。

  • また、実際にレイズ excして、偽のトレースバックが生き残ることを期待することもできません。

  • より一般的には、トレースバックをトラバースする方法とは異なる方法でトレースバックをトラバースするクライアント コードtraceback(モジュールの大部分などinspect ) がある場合、これらの偽物はおそらく機能しません。クライアントコードが期待する追加の属性を追加する必要があります。

これらの制限は、私の目的には問題ありません。呼び出しを行うコードのテスト用として使用しているだけtracebackです。

于 2013-10-08T21:23:41.960 に答える
3

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'、テストするモジュールのソースです。

EDIT1:

これまでの私の解決策:

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)

それが何らかの形で役立つことを願っています。

于 2013-10-08T19:35:46.623 に答える