py.test:隐藏unittest模块的堆栈跟踪行
现在,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 个回答
还有一个方法可以修改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
所有的感谢归功于 这个链接。我只是把它更新到了最新的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:
待定
如果你知道自己用的是什么操作系统,并且不在乎目录的分隔符,那么你可以去掉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()
试着把输出结果通过管道传给grep,并使用反向匹配的模式。这样就会打印出所有不符合这个模式的行。
python all_tests.py | grep -v "usr/lib64/python2.7/unittest"
看起来你是这样调用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