py.test回溯:高亮我的代码,折叠框架帧
我在用pytest测试的时候,错误信息太长了。
因为pytest会把周围的代码行和很多其他信息都包含进去。
如果错误信息是我自己代码里的,我希望能看到这些信息。但如果是来自某个库或框架的,我就不想看到。
我找不到办法来过滤或者折叠这些信息。
有没有什么建议呢?
更新,8年后:我觉得是时候告别ASCII,拥抱HTML了。用HTML的话,你可以展开或折叠某些部分(就像很棒的django调试视图那样)。
可惜的是,pytest的输出似乎没有像sentry那样提供一个好的界面。
3 个回答
0
还有一种方法可以修改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
5
我在我的 conftest.py
文件里加了一个小改动,当我想要这样做的时候(只在使用 --tb=native
时有效):
这个改动是把 Jeremy Allen 在这个链接 https://stackoverflow.com/a/24679193/59412 上的内容,移植到最新版本的 pytest,6.2.3。虽然这个方法不是最完美的,但它可以把所有来自第三方依赖的代码(比如通过 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...]/framework_code.py", line 1, in test_thing
line = tblines.pop(0)
if should_skip_line(line):
# the 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
1
我很抱歉,目前这还不可能实现,这其实是一个功能改进的请求:https://bitbucket.org/hpk42/pytest/issue/283/traceback-filtering-hook
现在你能控制的只有 --tb=short
这样的选项。