py.test:隐藏unittest模块的堆栈跟踪行

8 投票
5 回答
4515 浏览
提问于 2025-04-18 10:59

现在,py.test的错误追踪信息看起来是这样的:

Traceback (most recent call last):
  File "/home/foo_tbz_di476/src/djangotools/djangotools/tests/ReadonlyModelTestCommon.py", line 788, in test_stale_or_missing_content_types
    self.assertEqual([], errors, 'Stale/Missing ContentTypes: %s' % '\n'.join(errors))
  File "/usr/lib64/python2.7/unittest/case.py", line 511, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/usr/lib64/python2.7/unittest/case.py", line 740, in assertListEqual
    self.assertSequenceEqual(list1, list2, msg, seq_type=list)
  File "/usr/lib64/python2.7/unittest/case.py", line 722, in assertSequenceEqual
    self.fail(msg)
  File "/usr/lib64/python2.7/unittest/case.py", line 408, in fail
    raise self.failureException(msg)

如果输出能跳过来自unittest模块的那些行,我的人眼会觉得轻松很多。

举个例子:

Traceback (most recent call last):
  File "/home/foo_tbz_di476/src/djangotools/djangotools/tests/ReadonlyModelTestCommon.py", line 788, in test_stale_or_missing_content_types
    self.assertEqual([], errors, 'Stale/Missing ContentTypes: %s' % '\n'.join(errors))

我试过--tb=short这个选项,但它并没有做到这一点。

更新

我更希望有一个不需要使用unix管道的解决方案(比如py.test ...| grep)。

8年后的更新

我觉得是时候告别ASCII世界,进入HTML时代了。要是能有一个可以折叠和展开的错误追踪信息,那该多好啊!

5 个回答

1

还有一个方法可以修改pytest的行为,但不需要使用--tb=native这个选项。只需把下面的代码放到你的contest.py文件里:

import pathlib
from _pytest._code import code
from _pytest._code.code import ReprTraceback

def ishidden(self) -> bool:
    return self._ishidden() or 'site-packages' in pathlib.Path(self.path).parts
code.TracebackEntry._ishidden = code.TracebackEntry.ishidden
code.TracebackEntry.ishidden = ishidden

def repr_traceback(self, excinfo: code.ExceptionInfo[BaseException]) -> "ReprTraceback":
    traceback = excinfo.traceback
    if True:  # self.tbfilter:  <- filtering was not done for nested exception, so force it
        traceback = traceback.filter()
    # make sure we don't get an empty traceback list
    if len(traceback) == 0:
        traceback.append(excinfo.traceback[-1])

    if isinstance(excinfo.value, RecursionError):
        traceback, extraline = self._truncate_recursive_traceback(traceback)
    else:
        extraline = None

    last = traceback[-1]
    entries = []
    if self.style == "value":
        reprentry = self.repr_traceback_entry(last, excinfo)
        entries.append(reprentry)
        return ReprTraceback(entries, None, style=self.style)

    for index, entry in enumerate(traceback):
        einfo = (last == entry) and excinfo or None
        reprentry = self.repr_traceback_entry(entry, einfo)
        entries.append(reprentry)
    return ReprTraceback(entries, extraline, style=self.style)
code.FormattedExcinfo.repr_traceback = repr_traceback

del code

当我们使用像playwright这样的外部库时,输出的结果会好很多,这样默认的错误追踪格式就变得更实用了:

__________________________________________________________________ test_authenticated_access_to_the_application ___________________________________________________________________

page = <Page url='about:blank'>, base_url = ''

    @when('I go to the application')
    def provide_creds(page: Page, base_url: str):
        with page.expect_navigation(wait_until='networkidle', timeout=5000):
>           page.goto(base_url)
E           playwright._impl._api_types.Error: Protocol error (Page.navigate): Cannot navigate to invalid URL
E           =========================== logs ===========================
E           navigating to "", waiting until "load"
E           ============================================================

tests/gui/step_defs/star_steps.py:16: Error

During handling of the above exception, another exception occurred:

page = <Page url='about:blank'>, base_url = ''

    @when('I go to the application')
    def provide_creds(page: Page, base_url: str):
>       with page.expect_navigation(wait_until='networkidle', timeout=5000):
E       playwright._impl._api_types.TimeoutError: Timeout 5000ms exceeded.
E       =========================== logs ===========================
E       waiting for navigation until 'networkidle'
E       ============================================================

tests/gui/step_defs/star_steps.py:15: TimeoutError

和没有修改的版本相比:

__________________________________________________________________ test_authenticated_access_to_the_application ___________________________________________________________________

page = <Page url='about:blank'>, base_url = ''

    @when('I go to the application')
    def provide_creds(page: Page, base_url: str):
        with page.expect_navigation(wait_until='networkidle', timeout=5000):
>           page.goto(base_url)

tests/gui/step_defs/star_steps.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Page url='about:blank'>, url = ''

    def goto(
        self,
        url: str,
        *,
        timeout: float = None,
        wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"] = None,
        referer: str = None
    ) -> typing.Optional["Response"]:
        """Page.goto
    
        Returns the main resource response. In case of multiple redirects, the navigation will resolve with the first
        non-redirect response.
    
        The method will throw an error if:
        - there's an SSL error (e.g. in case of self-signed certificates).
        - target URL is invalid.
        - the `timeout` is exceeded during navigation.
        - the remote server does not respond or is unreachable.
        - the main resource failed to load.
    
        The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 \"Not
        Found\" and 500 \"Internal Server Error\".  The status code for such responses can be retrieved by calling
        `response.status()`.
    
        > NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to
        `about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`.
        > NOTE: Headless mode doesn't support navigation to a PDF document. See the
        [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
    
        Shortcut for main frame's `frame.goto()`
    
        Parameters
        ----------
        url : str
            URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was
            provided and the passed URL is a path, it gets merged via the
            [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
        timeout : Union[float, NoneType]
            Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be
            changed by using the `browser_context.set_default_navigation_timeout()`,
            `browser_context.set_default_timeout()`, `page.set_default_navigation_timeout()` or
            `page.set_default_timeout()` methods.
        wait_until : Union["commit", "domcontentloaded", "load", "networkidle", NoneType]
            When to consider operation succeeded, defaults to `load`. Events can be either:
            - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired.
            - `'load'` - consider operation to be finished when the `load` event is fired.
            - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms.
            - `'commit'` - consider operation to be finished when network response is received and the document started loading.
        referer : Union[str, NoneType]
            Referer header value. If provided it will take preference over the referer header value set by
            `page.set_extra_http_headers()`.
    
        Returns
        -------
        Union[Response, NoneType]
        """
    
        return mapping.from_impl_nullable(
>           self._sync(
                self._impl_obj.goto(
                    url=url, timeout=timeout, waitUntil=wait_until, referer=referer
                )
            )
        )

.venv/lib/python3.10/site-packages/playwright/sync_api/_generated.py:7285: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Page url='about:blank'>, coro = <coroutine object Page.goto at 0x7f3b6b5e1700>

    def _sync(self, coro: Awaitable) -> Any:
        __tracebackhide__ = True
        g_self = greenlet.getcurrent()
        task = self._loop.create_task(coro)
        setattr(task, "__pw_stack__", inspect.stack())
        setattr(task, "__pw_stack_trace__", traceback.extract_stack())
    
        task.add_done_callback(lambda _: g_self.switch())
        while not task.done():
            self._dispatcher_fiber.switch()
        asyncio._set_running_loop(self._loop)
>       return task.result()

.venv/lib/python3.10/site-packages/playwright/_impl/_sync_base.py:89: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Page url='about:blank'>, url = '', timeout = None, waitUntil = None, referer = None

    async def goto(
        self,
        url: str,
        timeout: float = None,
        waitUntil: DocumentLoadState = None,
        referer: str = None,
    ) -> Optional[Response]:
>       return await self._main_frame.goto(**locals_to_params(locals()))

.venv/lib/python3.10/site-packages/playwright/_impl/_page.py:496: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Frame name= url='about:blank'>, url = '', timeout = None, waitUntil = None, referer = None

    async def goto(
        self,
        url: str,
        timeout: float = None,
        waitUntil: DocumentLoadState = None,
        referer: str = None,
    ) -> Optional[Response]:
        return cast(
            Optional[Response],
            from_nullable_channel(
>               await self._channel.send("goto", locals_to_params(locals()))
            ),
        )

.venv/lib/python3.10/site-packages/playwright/_impl/_frame.py:136: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <playwright._impl._connection.Channel object at 0x7f3b7095e7a0>, method = 'goto', params = {'url': ''}

    async def send(self, method: str, params: Dict = None) -> Any:
>       return await self._connection.wrap_api_call(
            lambda: self.inner_send(method, params, False)
        )

.venv/lib/python3.10/site-packages/playwright/_impl/_connection.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <playwright._impl._connection.Connection object at 0x7f3b70976cb0>, cb = <function Channel.send.<locals>.<lambda> at 0x7f3b6b5a53f0>, is_internal = False

    async def wrap_api_call(
        self, cb: Callable[[], Any], is_internal: bool = False
    ) -> Any:
        if self._api_zone.get():
            return await cb()
        task = asyncio.current_task(self._loop)
        st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack())
        metadata = _extract_metadata_from_stack(st, is_internal)
        if metadata:
            self._api_zone.set(metadata)
        try:
>           return await cb()

.venv/lib/python3.10/site-packages/playwright/_impl/_connection.py:369: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <playwright._impl._connection.Channel object at 0x7f3b7095e7a0>, method = 'goto', params = {'url': ''}, return_as_dict = False

    async def inner_send(
        self, method: str, params: Optional[Dict], return_as_dict: bool
    ) -> Any:
        if params is None:
            params = {}
        callback = self._connection._send_message_to_server(self._guid, method, params)
        if self._connection._error:
            error = self._connection._error
            self._connection._error = None
            raise error
        done, _ = await asyncio.wait(
            {
                self._connection._transport.on_error_future,
                callback.future,
            },
            return_when=asyncio.FIRST_COMPLETED,
        )
        if not callback.future.done():
            callback.future.cancel()
>       result = next(iter(done)).result()
E       playwright._impl._api_types.Error: Protocol error (Page.navigate): Cannot navigate to invalid URL
E       =========================== logs ===========================
E       navigating to "", waiting until "load"
E       ============================================================

.venv/lib/python3.10/site-packages/playwright/_impl/_connection.py:78: Error

During handling of the above exception, another exception occurred:

page = <Page url='about:blank'>, base_url = ''

    @when('I go to the application')
    def provide_creds(page: Page, base_url: str):
>       with page.expect_navigation(wait_until='networkidle', timeout=5000):

tests/gui/step_defs/star_steps.py:15: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.10/site-packages/playwright/_impl/_sync_base.py:66: in __exit__
    self._event.value
.venv/lib/python3.10/site-packages/playwright/_impl/_sync_base.py:46: in value
    raise exception
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    async def continuation() -> Optional[Response]:
>       event = await wait_helper.result()
E       playwright._impl._api_types.TimeoutError: Timeout 5000ms exceeded.
E       =========================== logs ===========================
E       waiting for navigation until 'networkidle'
E       ============================================================

.venv/lib/python3.10/site-packages/playwright/_impl/_frame.py:197: TimeoutError
1

所有的感谢归功于 这个链接。我只是把它更新到了最新的pytest版本(写这段话时是6.2.3)。把这个放到你的 conftest.py 文件里。(不过对于 unittest 的代码是没用的,因为它不在 site-packages 里)

选项 1:

(默认情况下会过滤掉所有通过pip安装的包)

# 5c4048fc-ccf1-44ab-a683-78a29c1a98a6
import _pytest._code.code
def should_skip_line(line):
    """
    decide which lines to skip
    """
    return 'site-packages' in line

class PatchedReprEntryNative(_pytest._code.code.ReprEntryNative):
    def __init__(self, tblines):
        self.lines = []
        while len(tblines) > 0:
            # [...yourfilter...]/test_thing.py", line 1, in test_thing
            line = tblines.pop(0)
            if should_skip_line(line):
                # some line of framework code you don't want to see either...
                tblines.pop(0)
            else:
                self.lines.append(line)
_pytest._code.code.ReprEntryNative = PatchedReprEntryNative
del _pytest._code.code

这个选项只在使用 --tb=native 时有效。就像我更新到这个版本的答案一样。

选项 2:

待定

1

如果你知道自己用的是什么操作系统,并且不在乎目录的分隔符,那么你可以去掉import os这一行,把os.sep替换成合适的分隔符。

这样做会去掉所有与unittest模块相关的错误信息,以及它后面的那一行。

import os
import sys
import unittest

class Stderr(object):
    def __init__(self):
        self.unittest_location = (os.sep).join(unittest.__file__.split(os.sep)[:-1])
        self.stderr = sys.__stderr__
        self.skip = False

    def write(self, text):
        if self.skip and text.find("\n") != -1: self.skip=False
        elif self.skip: pass
        else:
            self.skip = text.find(self.unittest_location) != -1
            if not self.skip: self.stderr.write(text)

sys.stderr = Stderr()
2

试着把输出结果通过管道传给grep,并使用反向匹配的模式。这样就会打印出所有不符合这个模式的行。

python all_tests.py | grep -v "usr/lib64/python2.7/unittest"
12

看起来你是这样调用pytest的:

py.test --tb=native

这种方式会输出一个来自 traceback.format_exception 的Python标准库堆栈跟踪信息。

使用pytest时,你可以在项目中添加一个 conftest.py 文件。你可以在这里添加一些代码来修改pytest的行为。

注意!以下两种方法都使用了 猴子补丁,这可能会被一些人认为是不好的做法。

选项 1:字符串匹配

这是最简单的方法,但如果你要查找的字符串出现在你不想隐藏的行中,可能会有问题。

这种方法会修改pytest依赖的py包中的ReprEntryNative类。

把以下代码放到你的conftest.py里:

import py

def test_skip_line(line):
    """decide which lines to skip, the code below will also skip the next line if this returns true"""
    return 'unittest' in line

class PatchedReprEntryNative(py._code.code.ReprEntryNative):
    def __init__(self, tblines):
        self.lines = []
        while len(tblines) > 0:
            line = tblines.pop(0)
            if test_skip_line(line):
                # skip this line and the next
                tblines.pop(0)
            else:
                self.lines.append(line)
py._code.code.ReprEntryNative = PatchedReprEntryNative

选项 2:堆栈跟踪帧检查

如果字符串匹配不够准确,我们可以在堆栈跟踪信息被转换为字符串之前进行检查,只输出那些不来自特定模块的帧。

这种方法会修改traceback.extract_tb函数,这可能会有些不妥。

把以下代码放到你的conftest.py里:

import inspect
import linecache
import traceback
import unittest.case
import sys    

SKIPPED_MODULES = [
    unittest.case
]

def test_skip_frame(frame):
    module = inspect.getmodule(frame)
    return module in SKIPPED_MODULES

def tb_skipper(tb):
    tbnext = tb.tb_next
    while tbnext is not None:
        if test_skip_frame(tbnext.tb_frame):
            tbnext = tbnext.tb_next
        else:
            yield tbnext
    yield None

def new_extract_tb(tb, limit = None):
    if limit is None:
        if hasattr(sys, 'tracebacklimit'):
            limit = sys.tracebacklimit
    list = []
    n = 0
    new_tb_order = tb_skipper(tb) # <-- this line added
    while tb is not None and (limit is None or n < limit):
        f = tb.tb_frame
        lineno = tb.tb_lineno
        co = f.f_code
        filename = co.co_filename
        name = co.co_name
        linecache.checkcache(filename)
        line = linecache.getline(filename, lineno, f.f_globals)
        if line: line = line.strip()
        else: line = None
        list.append((filename, lineno, name, line))
        tb = next(new_tb_order) # <-- this line modified, was tb = tb.tb_next
        n = n+1
    return list
traceback.extract_tb = new_extract_tb

撰写回答