Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show traceback during collection #1979

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@

*

*
* Import errors when collecting test modules now display the full traceback (`#1976`_).
Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR.

*

*


.. _@cwitty: https://github.com/cwitty

.. _#1976: https://github.com/pytest-dev/pytest/issues/1976



3.0.3
=====

Expand Down
24 changes: 17 additions & 7 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
getlocation, enum,
)

cutdir2 = py.path.local(_pytest.__file__).dirpath()
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
cutdir2 = py.path.local(_pytest.__file__).dirpath()
cutdir3 = py.path.local(py.__file__).dirpath()


def filter_traceback(entry):
"""Return True if a TracebackEntry instance should be removed from tracebacks:
* dynamically generated code (no code to show up for it);
* internal traceback from pytest or its internal libraries, py and pluggy.
"""
# entry.path might sometimes return a str object when the entry
# points to dynamically generated code
# see https://bitbucket.org/pytest-dev/py/issues/71
Expand All @@ -37,7 +42,7 @@ def filter_traceback(entry):
# entry.path might point to an inexisting file, in which case it will
# alsso return a str object. see #1133
p = py.path.local(entry.path)
return p != cutdir1 and not p.relto(cutdir2)
return p != cutdir1 and not p.relto(cutdir2) and not p.relto(cutdir3)



Expand Down Expand Up @@ -424,12 +429,17 @@ def _importtestmodule(self):
% e.args
)
except ImportError:
exc_class, exc, _ = sys.exc_info()
from _pytest._code.code import ExceptionInfo
exc_info = ExceptionInfo()
if self.config.getoption('verbose') < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = exc_info.getrepr(style='short') if exc_info.traceback else exc_info.exconly()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to hard-code it to short because it is usually enough to track import problems and long may generate too much output during collection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please a comment in the code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ops, good point.

formatted_tb = py._builtin._totext(exc_repr)
raise self.CollectError(
"ImportError while importing test module '%s'.\n"
"Original error message:\n'%s'\n"
"Make sure your test modules/packages have valid Python names."
% (self.fspath, exc or exc_class)
"ImportError while importing test module '{fspath}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(fspath=self.fspath, traceback=formatted_tb)
)
except _pytest.runner.Skipped as e:
if e.allow_module_level:
Expand Down
2 changes: 1 addition & 1 deletion testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_this():
result.stdout.fnmatch_lines([
#XXX on jython this fails: "> import import_fails",
"ImportError while importing test module*",
"'No module named *does_not_work*",
"*No module named *does_not_work*",
])
assert result.ret == 2

Expand Down
35 changes: 34 additions & 1 deletion testing/python/collect.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
import sys
from textwrap import dedent

Expand Down Expand Up @@ -68,9 +69,41 @@ def test_invalid_test_module_name(self, testdir):
result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines([
"ImportError while importing test module*test_one.part1*",
"Make sure your test modules/packages have valid Python names.",
"Hint: make sure your test modules/packages have valid Python names.",
])

@pytest.mark.parametrize('verbose', [0, 1, 2])
def test_show_traceback_import_error(self, testdir, verbose):
"""Import errors when collecting modules should display the traceback (#1976).

With low verbosity we omit pytest and internal modules, otherwise show all traceback entries.
"""
testdir.makepyfile(
foo_traceback_import_error="""
from bar_traceback_import_error import NOT_AVAILABLE
""",
bar_traceback_import_error="",
)
testdir.makepyfile("""
import foo_traceback_import_error
""")
args = ('-v',) * verbose
result = testdir.runpytest(*args)
result.stdout.fnmatch_lines([
"ImportError while importing test module*",
"Traceback:",
"*from bar_traceback_import_error import NOT_AVAILABLE",
"*cannot import name *NOT_AVAILABLE*",
])
assert result.ret == 2

stdout = result.stdout.str()
for name in ('_pytest', os.path.join('py', '_path')):
if verbose == 2:
assert name in stdout
else:
assert name not in stdout


class TestClass:
def test_class_with_init_warning(self, testdir):
Expand Down
11 changes: 0 additions & 11 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,6 @@ def pytest_collect_directory(self, path, parent):
assert "world" in wascalled

class TestPrunetraceback:
def test_collection_error(self, testdir):
p = testdir.makepyfile("""
import not_exists
""")
result = testdir.runpytest(p)
assert "__import__" not in result.stdout.str(), "too long traceback"
result.stdout.fnmatch_lines([
"*ERROR collecting*",
"ImportError while importing test module*",
"'No module named *not_exists*",
])

def test_custom_repr_failure(self, testdir):
p = testdir.makepyfile("""
Expand Down
2 changes: 1 addition & 1 deletion testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ def test_collect_fail(self, testdir, option):
result = testdir.runpytest(*option.args)
result.stdout.fnmatch_lines([
"ImportError while importing*",
"'No module named *xyz*",
"*No module named *xyz*",
"*1 error*",
])

Expand Down