Merge remote-tracking branch 'upstream/master' into merge-master-into-features

This commit is contained in:
Bruno Oliveira 2016-10-20 21:51:42 -02:00
commit 9d00615bbf
19 changed files with 131 additions and 42 deletions

View File

@ -3,6 +3,9 @@ Thanks for submitting a PR, your contribution is really appreciated!
Here's a quick checklist that should be present in PRs: Here's a quick checklist that should be present in PRs:
- [ ] Target: for bug or doc fixes, target `master`; for new features, target `features`; - [ ] Target: for bug or doc fixes, target `master`; for new features, target `features`;
Unless your change is trivial documentation fix (e.g., a typo or reword of a small section) please:
- [ ] Make sure to include one or more tests for your change; - [ ] Make sure to include one or more tests for your change;
- [ ] Add yourself to `AUTHORS`; - [ ] Add yourself to `AUTHORS`;
- [ ] Add a new entry to `CHANGELOG.rst` - [ ] Add a new entry to `CHANGELOG.rst`

View File

@ -22,7 +22,8 @@ env:
- TESTENV=py27-trial - TESTENV=py27-trial
- TESTENV=py35-pexpect - TESTENV=py35-pexpect
- TESTENV=py35-xdist - TESTENV=py35-xdist
- TESTENV=py35-trial # Disable py35-trial temporarily: #1989
#- TESTENV=py35-trial
- TESTENV=py27-nobyte - TESTENV=py27-nobyte
- TESTENV=doctesting - TESTENV=doctesting
- TESTENV=freeze - TESTENV=freeze

View File

@ -59,6 +59,7 @@ Georgy Dyuldin
Graham Horler Graham Horler
Greg Price Greg Price
Grig Gheorghiu Grig Gheorghiu
Grigorii Eremeev (budulianin)
Guido Wesdorp Guido Wesdorp
Harald Armin Massa Harald Armin Massa
Ian Bicking Ian Bicking

View File

@ -41,11 +41,25 @@ Changes
* *
* * Import errors when collecting test modules now display the full traceback (`#1976`_).
Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR.
* Fix confusing command-line help message for custom options with two or more `metavar` properties (`#2004`_).
Thanks `@okulynyak`_ and `@davehunt`_ for the report and `@nicoddemus`_ for the PR.
* When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_).
Thanks `@nicoddemus`_ for the PR.
* *
*
.. _@cwitty: https://github.com/cwitty
.. _@okulynyak: https://github.com/okulynyak
.. _#1976: https://github.com/pytest-dev/pytest/issues/1976
.. _#1998: https://github.com/pytest-dev/pytest/issues/1998
.. _#2004: https://github.com/pytest-dev/pytest/issues/2004
3.0.3 3.0.3

View File

@ -216,3 +216,17 @@ def _is_unittest_unexpected_success_a_failure():
unexpectedSuccesses from tests marked with the expectedFailure() decorator. unexpectedSuccesses from tests marked with the expectedFailure() decorator.
""" """
return sys.version_info >= (3, 4) return sys.version_info >= (3, 4)
if _PY3:
def safe_str(v):
"""returns v as string"""
return str(v)
else:
def safe_str(v):
"""returns v as string, converting to ascii if necessary"""
try:
return str(v)
except UnicodeError:
errors = 'replace'
return v.encode('ascii', errors)

View File

@ -12,6 +12,7 @@ import _pytest._code
import _pytest.hookspec # the extension point definitions import _pytest.hookspec # the extension point definitions
import _pytest.assertion import _pytest.assertion
from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker
from _pytest.compat import safe_str
hookimpl = HookimplMarker("pytest") hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")
@ -405,7 +406,7 @@ class PytestPluginManager(PluginManager):
try: try:
__import__(importspec) __import__(importspec)
except ImportError as e: except ImportError as e:
new_exc = ImportError('Error importing plugin "%s": %s' % (modname, e)) new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0])))
# copy over name and path attributes # copy over name and path attributes
for attr in ('name', 'path'): for attr in ('name', 'path'):
if hasattr(e, attr): if hasattr(e, attr):
@ -792,7 +793,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
if len(option) == 2 or option[2] == ' ': if len(option) == 2 or option[2] == ' ':
return_list.append(option) return_list.append(option)
if option[2:] == short_long.get(option.replace('-', '')): if option[2:] == short_long.get(option.replace('-', '')):
return_list.append(option.replace(' ', '=')) return_list.append(option.replace(' ', '=', 1))
action._formatted_action_invocation = ', '.join(return_list) action._formatted_action_invocation = ', '.join(return_list)
return action._formatted_action_invocation return action._formatted_action_invocation

View File

@ -71,8 +71,8 @@ def showhelp(config):
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()
tw.line() tw.line()
tw.line("[pytest] ini-options in the next " tw.line("[pytest] ini-options in the first "
"pytest.ini|tox.ini|setup.cfg file:") "pytest.ini|tox.ini|setup.cfg file found:")
tw.line() tw.line()
for name in config._parser._ininames: for name in config._parser._ininames:

View File

@ -22,11 +22,16 @@ from _pytest.compat import (
getlocation, enum, getlocation, enum,
) )
cutdir2 = py.path.local(_pytest.__file__).dirpath()
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) 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): 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 # entry.path might sometimes return a str object when the entry
# points to dynamically generated code # points to dynamically generated code
# see https://bitbucket.org/pytest-dev/py/issues/71 # see https://bitbucket.org/pytest-dev/py/issues/71
@ -37,7 +42,7 @@ def filter_traceback(entry):
# entry.path might point to an inexisting file, in which case it will # entry.path might point to an inexisting file, in which case it will
# alsso return a str object. see #1133 # alsso return a str object. see #1133
p = py.path.local(entry.path) 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)
@ -425,20 +430,25 @@ class Module(pytest.File, PyCollector):
% e.args % e.args
) )
except ImportError: 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()
formatted_tb = py._builtin._totext(exc_repr)
raise self.CollectError( raise self.CollectError(
"ImportError while importing test module '%s'.\n" "ImportError while importing test module '{fspath}'.\n"
"Original error message:\n'%s'\n" "Hint: make sure your test modules/packages have valid Python names.\n"
"Make sure your test modules/packages have valid Python names." "Traceback:\n"
% (self.fspath, exc or exc_class) "{traceback}".format(fspath=self.fspath, traceback=formatted_tb)
) )
except _pytest.runner.Skipped as e: except _pytest.runner.Skipped as e:
if e.allow_module_level: if e.allow_module_level:
raise raise
raise self.CollectError( raise self.CollectError(
"Using @pytest.skip outside of a test (e.g. as a test " "Using pytest.skip outside of a test is not allowed. If you are "
"function decorator) is not allowed. Use @pytest.mark.skip or " "trying to decorate a test function, use the @pytest.mark.skip "
"@pytest.mark.skipif instead." "or @pytest.mark.skipif decorators instead."
) )
self.config.pluginmanager.consider_module(mod) self.config.pluginmanager.consider_module(mod)
return mod return mod

View File

@ -701,7 +701,7 @@ Using fixtures from classes, modules or projects
Sometimes test functions do not directly need access to a fixture object. Sometimes test functions do not directly need access to a fixture object.
For example, tests may require to operate with an empty directory as the For example, tests may require to operate with an empty directory as the
current working directory but otherwise do not care for the concrete current working directory but otherwise do not care for the concrete
directory. Here is how you can can use the standard `tempfile directory. Here is how you can use the standard `tempfile
<http://docs.python.org/library/tempfile.html>`_ and pytest fixtures to <http://docs.python.org/library/tempfile.html>`_ and pytest fixtures to
achieve it. We separate the creation of the fixture into a conftest.py achieve it. We separate the creation of the fixture into a conftest.py
file:: file::

View File

@ -236,9 +236,10 @@ your own setuptools Test command for invoking pytest.
self.pytest_args = [] self.pytest_args = []
def run_tests(self): def run_tests(self):
import shlex
#import here, cause outside the eggs aren't loaded #import here, cause outside the eggs aren't loaded
import pytest import pytest
errno = pytest.main(self.pytest_args) errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno) sys.exit(errno)

View File

@ -55,6 +55,14 @@ will delete the method ``request.session.Session.request``
so that any attempts within tests to create http requests will fail. so that any attempts within tests to create http requests will fail.
.. note::
Be advised that it is not recommended to patch builtin functions such as ``open``,
``compile``, etc., because it might break pytest's internals. If that's
unavoidable, passing ``--tb=native``, ``--assert=plain`` and ``--capture=no`` might
help althought there's no guarantee.
Method reference of the monkeypatch fixture Method reference of the monkeypatch fixture
------------------------------------------- -------------------------------------------

View File

@ -310,10 +310,6 @@ You can pass in options and arguments::
pytest.main(['-x', 'mytestdir']) pytest.main(['-x', 'mytestdir'])
or pass in a string::
pytest.main("-x mytestdir")
You can specify additional plugins to ``pytest.main``:: You can specify additional plugins to ``pytest.main``::
# content of myinvoke.py # content of myinvoke.py

View File

@ -120,7 +120,7 @@ class TestGeneralUsage:
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
#XXX on jython this fails: "> import import_fails", #XXX on jython this fails: "> import import_fails",
"ImportError while importing test module*", "ImportError while importing test module*",
"'No module named *does_not_work*", "*No module named *does_not_work*",
]) ])
assert result.ret == 2 assert result.ret == 2

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import sys import sys
from textwrap import dedent from textwrap import dedent
@ -68,9 +69,41 @@ class TestModule:
result = testdir.runpytest("-rw") result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"ImportError while importing test module*test_one.part1*", "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: class TestClass:
def test_class_with_init_warning(self, testdir): def test_class_with_init_warning(self, testdir):

View File

@ -172,17 +172,6 @@ class TestCollectPluginHookRelay:
assert "world" in wascalled assert "world" in wascalled
class TestPrunetraceback: 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): def test_custom_repr_failure(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""

View File

@ -250,6 +250,18 @@ class TestParser:
help = parser.optparser.format_help() help = parser.optparser.format_help()
assert '-doit, --func-args foo' in help assert '-doit, --func-args foo' in help
def test_multiple_metavar_help(self, parser):
"""
Help text for options with a metavar tuple should display help
in the form "--preferences=value1 value2 value3" (#2004).
"""
group = parser.getgroup("general")
group.addoption('--preferences', metavar=('value1', 'value2', 'value3'), nargs=3)
group._addoption("-h", "--help", action="store_true", dest="help")
parser.parse(['-h'])
help = parser.optparser.format_help()
assert '--preferences=value1 value2 value3' in help
def test_argcomplete(testdir, monkeypatch): def test_argcomplete(testdir, monkeypatch):
if not py.path.local.sysfind('bash'): if not py.path.local.sysfind('bash'):

View File

@ -1,3 +1,4 @@
# encoding: UTF-8
import pytest import pytest
import py import py
import os import os
@ -179,15 +180,20 @@ def test_default_markers(testdir):
]) ])
def test_importplugin_issue375(testdir, pytestpm): def test_importplugin_error_message(testdir, pytestpm):
"""Don't hide import errors when importing plugins and provide """Don't hide import errors when importing plugins and provide
an easy to debug message. an easy to debug message.
See #375 and #1998.
""" """
testdir.syspathinsert(testdir.tmpdir) testdir.syspathinsert(testdir.tmpdir)
testdir.makepyfile(qwe="import aaaa") testdir.makepyfile(qwe="""
# encoding: UTF-8
raise ImportError(u'Not possible to import: ☺')
""")
with pytest.raises(ImportError) as excinfo: with pytest.raises(ImportError) as excinfo:
pytestpm.import_plugin("qwe") pytestpm.import_plugin("qwe")
expected = '.*Error importing plugin "qwe": No module named \'?aaaa\'?' expected = '.*Error importing plugin "qwe": Not possible to import: .'
assert py.std.re.match(expected, str(excinfo.value)) assert py.std.re.match(expected, str(excinfo.value))

View File

@ -967,5 +967,5 @@ def test_module_level_skip_error(testdir):
""") """)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
"*Using @pytest.skip outside of a test * is not allowed*" "*Using pytest.skip outside of a test is not allowed*"
) )

View File

@ -667,7 +667,7 @@ class TestGenericReporting:
result = testdir.runpytest(*option.args) result = testdir.runpytest(*option.args)
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"ImportError while importing*", "ImportError while importing*",
"'No module named *xyz*", "*No module named *xyz*",
"*1 error*", "*1 error*",
]) ])