Merge pull request #2526 from nicoddemus/merge-master-into-features

Merge master into features
This commit is contained in:
Ronny Pfannschmidt 2017-06-24 11:25:35 +02:00 committed by GitHub
commit 9b51fc646c
38 changed files with 405 additions and 230 deletions

View File

@ -25,9 +25,9 @@ Bug Fixes
- ``UnicodeWarning`` is issued from the internal pytest warnings plugin only - ``UnicodeWarning`` is issued from the internal pytest warnings plugin only
when the message contains non-ascii unicode (Python 2 only). (#2463) when the message contains non-ascii unicode (Python 2 only). (#2463)
- Added a workaround for Python 3.6 WindowsConsoleIO breaking due to Pytests's - Added a workaround for Python 3.6 ``WindowsConsoleIO`` breaking due to Pytests's
FDCapture. Other code using console handles might still be affected by the ``FDCapture``. Other code using console handles might still be affected by the
very same issue and might require further workarounds/fixes, i.e. colorama. very same issue and might require further workarounds/fixes, i.e. ``colorama``.
(#2467) (#2467)

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2004-2016 Holger Krekel and others Copyright (c) 2004-2017 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -1,39 +0,0 @@
include CHANGELOG.rst
include LICENSE
include AUTHORS
include pyproject.toml
include README.rst
include CONTRIBUTING.rst
include HOWTORELEASE.rst
include tox.ini
include setup.py
recursive-include changelog *
recursive-include scripts *.py
recursive-include scripts *.bat
include .coveragerc
recursive-include bench *.py
recursive-include extra *.py
graft testing
graft doc
prune doc/en/_build
graft tasks
exclude _pytest/impl
graft _pytest/vendored_packages
recursive-exclude * *.pyc *.pyo
recursive-exclude testing/.hypothesis *
recursive-exclude testing/freeze/~ *
recursive-exclude testing/freeze/build *
recursive-exclude testing/freeze/dist *
exclude appveyor.yml
exclude .travis.yml
prune .github

View File

@ -102,7 +102,7 @@ Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page
License License
------- -------
Copyright Holger Krekel and others, 2004-2016. Copyright Holger Krekel and others, 2004-2017.
Distributed under the terms of the `MIT`_ license, pytest is free and open source software. Distributed under the terms of the `MIT`_ license, pytest is free and open source software.

View File

@ -640,8 +640,11 @@ class FormattedExcinfo(object):
).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback)) ).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback))
traceback = traceback[:max_frames] + traceback[-max_frames:] traceback = traceback[:max_frames] + traceback[-max_frames:]
else: else:
extraline = "!!! Recursion detected (same locals & position)" if recursionindex is not None:
traceback = traceback[:recursionindex + 1] extraline = "!!! Recursion detected (same locals & position)"
traceback = traceback[:recursionindex + 1]
else:
extraline = None
return traceback, extraline return traceback, extraline

View File

@ -162,10 +162,6 @@ class AssertionRewritingHook(object):
# modules not passed explicitly on the command line are only # modules not passed explicitly on the command line are only
# rewritten if they match the naming convention for test files # rewritten if they match the naming convention for test files
for pat in self.fnpats: for pat in self.fnpats:
# use fnmatch instead of fn_pypath.fnmatch because the
# latter might trigger an import to fnmatch.fnmatch
# internally, which would cause this method to be
# called recursively
if fn_pypath.fnmatch(pat): if fn_pypath.fnmatch(pat):
state.trace("matched test file %r" % (fn,)) state.trace("matched test file %r" % (fn,))
return True return True

View File

@ -177,7 +177,6 @@ class DoctestTextfile(pytest.Module):
name = self.fspath.basename name = self.fspath.basename
globs = {'__name__': '__main__'} globs = {'__name__': '__main__'}
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_checker()) checker=_get_checker())
@ -218,9 +217,6 @@ class DoctestModule(pytest.Module):
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_checker()) checker=_get_checker())
encoding = self.config.getini("doctest_encoding")
_fix_spoof_python2(runner, encoding)
for test in finder.find(module, module.__name__): for test in finder.find(module, module.__name__):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
yield DoctestItem(test.name, self, runner, test) yield DoctestItem(test.name, self, runner, test)
@ -332,7 +328,10 @@ def _get_report_choice(key):
def _fix_spoof_python2(runner, encoding): def _fix_spoof_python2(runner, encoding):
""" """
Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This
should patch only doctests for text files because they don't have a way to declare their
encoding. Doctests in docstrings from Python modules don't have the same problem given that
Python already decoded the strings.
This fixes the problem related in issue #2434. This fixes the problem related in issue #2434.
""" """

View File

@ -733,10 +733,19 @@ class FixtureDef:
self._finalizer.append(finalizer) self._finalizer.append(finalizer)
def finish(self): def finish(self):
exceptions = []
try: try:
while self._finalizer: while self._finalizer:
func = self._finalizer.pop() try:
func() func = self._finalizer.pop()
func()
except:
exceptions.append(sys.exc_info())
if exceptions:
e = exceptions[0]
del exceptions # ensure we don't keep all frames alive because of the traceback
py.builtin._reraise(*e)
finally: finally:
ihook = self._fixturemanager.session.ihook ihook = self._fixturemanager.session.ihook
ihook.pytest_fixture_post_finalizer(fixturedef=self) ihook.pytest_fixture_post_finalizer(fixturedef=self)

View File

@ -73,7 +73,9 @@ def pytest_configure(config):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_cmdline_parse(pluginmanager, args): def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """ """return initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_cmdline_preparse(config, args): def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """ """(deprecated) modify command line arguments before option parsing. """
@ -81,7 +83,9 @@ def pytest_cmdline_preparse(config, args):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
""" called for performing the main command line action. The default """ called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """ implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_load_initial_conftests(early_config, parser, args): def pytest_load_initial_conftests(early_config, parser, args):
""" implements the loading of initial conftest files ahead """ implements the loading of initial conftest files ahead
@ -94,7 +98,9 @@ def pytest_load_initial_conftests(early_config, parser, args):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_collection(session): def pytest_collection(session):
""" perform the collection protocol for the given session. """ """ perform the collection protocol for the given session.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_collection_modifyitems(session, config, items): def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order """ called after collection has been performed, may filter or re-order
@ -108,11 +114,15 @@ def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection. """ return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling This hook is consulted for all files and directories prior to calling
more specific hooks. more specific hooks.
Stops at first non-None result, see :ref:`firstresult`
""" """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_collect_directory(path, parent): def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """ """ called before traversing a directory for collection files.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node """ return collection Node or None for the given path. Any new node
@ -133,7 +143,9 @@ def pytest_deselected(items):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_make_collect_report(collector): def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """ """ perform ``collector.collect()`` and return a CollectReport.
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Python test function related hooks # Python test function related hooks
@ -145,15 +157,20 @@ def pytest_pycollect_makemodule(path, parent):
This hook will be called for each matching test module path. This hook will be called for each matching test module path.
The pytest_collect_file hook needs to be used if you want to The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module. create test modules for files that do not match as a test module.
"""
Stops at first non-None result, see :ref:`firstresult` """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None. """ """ return custom item/collector for a python object in a module, or None.
Stops at first non-None result, see :ref:`firstresult` """
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """ """ call underlying test function.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function.""" """ generate (multiple) parametrized calls to a test function."""
@ -163,7 +180,8 @@ def pytest_make_parametrize_id(config, val, argname):
"""Return a user-friendly string representation of the given ``val`` that will be used """Return a user-friendly string representation of the given ``val`` that will be used
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
The parameter name is available as ``argname``, if required. The parameter name is available as ``argname``, if required.
"""
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# generic runtest related hooks # generic runtest related hooks
@ -172,7 +190,9 @@ def pytest_make_parametrize_id(config, val, argname):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_runtestloop(session): def pytest_runtestloop(session):
""" called for performing the main runtest loop """ called for performing the main runtest loop
(after collection finished). """ (after collection finished).
Stops at first non-None result, see :ref:`firstresult` """
def pytest_itemstart(item, node): def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """ """ (deprecated, use pytest_runtest_logstart). """
@ -190,7 +210,9 @@ def pytest_runtest_protocol(item, nextitem):
:py:func:`pytest_runtest_teardown`. :py:func:`pytest_runtest_teardown`.
:return boolean: True if no further hook implementations should be invoked. :return boolean: True if no further hook implementations should be invoked.
"""
Stops at first non-None result, see :ref:`firstresult` """
def pytest_runtest_logstart(nodeid, location): def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """ """ signal the start of running a single test item. """
@ -215,7 +237,8 @@ def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object """ return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item <_pytest.main.Item>` and for the given :py:class:`pytest.Item <_pytest.main.Item>` and
:py:class:`_pytest.runner.CallInfo`. :py:class:`_pytest.runner.CallInfo`.
"""
Stops at first non-None result, see :ref:`firstresult` """
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to """ process a test setup/call/teardown report relating to
@ -227,7 +250,9 @@ def pytest_runtest_logreport(report):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_fixture_setup(fixturedef, request): def pytest_fixture_setup(fixturedef, request):
""" performs fixture setup execution. """ """ performs fixture setup execution.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_fixture_post_finalizer(fixturedef): def pytest_fixture_post_finalizer(fixturedef):
""" called after fixture teardown, but before the cache is cleared so """ called after fixture teardown, but before the cache is cleared so
@ -277,7 +302,9 @@ def pytest_report_header(config, startdir):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting.""" """ return result-category, shortletter and verbose word for reporting.
Stops at first non-None result, see :ref:`firstresult` """
def pytest_terminal_summary(terminalreporter, exitstatus): def pytest_terminal_summary(terminalreporter, exitstatus):
""" add additional section in terminal summary reporting. """ """ add additional section in terminal summary reporting. """
@ -295,7 +322,9 @@ def pytest_logwarning(message, code, nodeid, fslocation):
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_doctest_prepare_content(content): def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest""" """ return processed content for a given doctest
Stops at first non-None result, see :ref:`firstresult` """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# error handling and internal debugging hooks # error handling and internal debugging hooks

View File

@ -763,7 +763,11 @@ class Session(FSCollector):
if not has_matched and len(rep.result) == 1 and x.name == "()": if not has_matched and len(rep.result) == 1 and x.name == "()":
nextnames.insert(0, name) nextnames.insert(0, name)
resultnodes.extend(self.matchnodes([x], nextnames)) resultnodes.extend(self.matchnodes([x], nextnames))
node.ihook.pytest_collectreport(report=rep) else:
# report collection failures here to avoid failing to run some test
# specified in the command line because the module could not be
# imported (#134)
node.ihook.pytest_collectreport(report=rep)
return resultnodes return resultnodes
def genitems(self, node): def genitems(self, node):

View File

@ -27,10 +27,8 @@ def recwarn():
def deprecated_call(func=None, *args, **kwargs): def deprecated_call(func=None, *args, **kwargs):
""" assert that calling ``func(*args, **kwargs)`` triggers a """context manager that can be used to ensure a block of code triggers a
``DeprecationWarning`` or ``PendingDeprecationWarning``. ``DeprecationWarning`` or ``PendingDeprecationWarning``::
This function can be used as a context manager::
>>> import warnings >>> import warnings
>>> def api_call_v2(): >>> def api_call_v2():
@ -40,38 +38,47 @@ def deprecated_call(func=None, *args, **kwargs):
>>> with deprecated_call(): >>> with deprecated_call():
... assert api_call_v2() == 200 ... assert api_call_v2() == 200
Note: we cannot use WarningsRecorder here because it is still subject ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
to the mechanism that prevents warnings of the same type from being in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
triggered twice for the same module. See #1190. types above.
""" """
if not func: if not func:
return WarningsChecker(expected_warning=(DeprecationWarning, PendingDeprecationWarning)) return _DeprecatedCallContext()
else:
categories = []
def warn_explicit(message, category, *args, **kwargs):
categories.append(category)
def warn(message, category=None, *args, **kwargs):
if isinstance(message, Warning):
categories.append(message.__class__)
else:
categories.append(category)
old_warn = warnings.warn
old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = warn_explicit
warnings.warn = warn
try:
ret = func(*args, **kwargs)
finally:
warnings.warn_explicit = old_warn_explicit
warnings.warn = old_warn
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(issubclass(c, deprecation_categories) for c in categories):
__tracebackhide__ = True __tracebackhide__ = True
raise AssertionError("%r did not produce DeprecationWarning" % (func,)) with _DeprecatedCallContext():
return ret return func(*args, **kwargs)
class _DeprecatedCallContext(object):
"""Implements the logic to capture deprecation warnings as a context manager."""
def __enter__(self):
self._captured_categories = []
self._old_warn = warnings.warn
self._old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = self._warn_explicit
warnings.warn = self._warn
def _warn_explicit(self, message, category, *args, **kwargs):
self._captured_categories.append(category)
def _warn(self, message, category=None, *args, **kwargs):
if isinstance(message, Warning):
self._captured_categories.append(message.__class__)
else:
self._captured_categories.append(category)
def __exit__(self, exc_type, exc_val, exc_tb):
warnings.warn_explicit = self._old_warn_explicit
warnings.warn = self._old_warn
if exc_type is None:
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(issubclass(c, deprecation_categories) for c in self._captured_categories):
__tracebackhide__ = True
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
raise AssertionError(msg)
def warns(expected_warning, *args, **kwargs): def warns(expected_warning, *args, **kwargs):

View File

@ -282,7 +282,7 @@ class TerminalReporter:
line = "collected " line = "collected "
else: else:
line = "collecting " line = "collecting "
line += str(self._numcollected) + " items" line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's')
if errors: if errors:
line += " / %d errors" % errors line += " / %d errors" % errors
if skipped: if skipped:

1
changelog/2434.bugfix Normal file
View File

@ -0,0 +1 @@
Fix decode error in Python 2 for doctests in docstrings.

1
changelog/2440.bugfix Normal file
View File

@ -0,0 +1 @@
Exceptions raised during teardown by finalizers are now suppressed until all finalizers are called, with the initial exception reraised.

1
changelog/2464.bugfix Normal file
View File

@ -0,0 +1 @@
Fix incorrect "collected items" report when specifying tests on the command-line.

4
changelog/2469.bugfix Normal file
View File

@ -0,0 +1,4 @@
``deprecated_call`` in context-manager form now captures deprecation warnings even if
the same warning has already been raised. Also, ``deprecated_call`` will always produce
the same error message (previously it would produce different messages in context-manager vs.
function-call mode).

1
changelog/2474.trivial Normal file
View File

@ -0,0 +1 @@
Create invoke tasks for updating the vendored packages.

1
changelog/2486.bugfix Normal file
View File

@ -0,0 +1 @@
Fix internal error when trying to detect the start of a recursive traceback.

1
changelog/2493.doc Normal file
View File

@ -0,0 +1 @@
Explicitly state for which hooks the calls stop after the first non-None result.

1
changelog/2499.trivial Normal file
View File

@ -0,0 +1 @@
Update copyright dates in LICENSE, README.rst and in the documentation.

View File

@ -6,14 +6,14 @@
{% endif %} {% endif %}
{% if sections[section] %} {% if sections[section] %}
{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} {% for category, val in definitions.items() if category in sections[section] %}
{{ definitions[category]['name'] }} {{ definitions[category]['name'] }}
{{ underline * definitions[category]['name']|length }} {{ underline * definitions[category]['name']|length }}
{% if definitions[category]['showcontent'] %} {% if definitions[category]['showcontent'] %}
{% for text, values in sections[section][category]|dictsort(by='value') %} {% for text, values in sections[section][category]|dictsort(by='value') %}
- {{ text }}{% if category != 'vendor' %} ({{ values|sort|join(', ') }}){% endif %} - {{ text }}{% if category != 'vendor' %} (`{{ values[0] }} <https://github.com/pytest-dev/pytest/issues/{{ values[0][1:] }}>`_){% endif %}
{% endfor %} {% endfor %}

View File

@ -73,20 +73,20 @@ marked ``smtp`` fixture function. Running the test looks like this::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 1 items collected 1 items
test_smtpsimple.py F test_smtpsimple.py F
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo ________ _______ test_ehlo ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_smtpsimple.py:11: AssertionError test_smtpsimple.py:11: AssertionError
======= 1 failed in 0.12 seconds ======== ======= 1 failed in 0.12 seconds ========
@ -123,20 +123,13 @@ with a list of available function arguments.
but is not anymore advertised as the primary means of declaring fixture but is not anymore advertised as the primary means of declaring fixture
functions. functions.
"Funcargs" a prime example of dependency injection Fixtures: a prime example of dependency injection
--------------------------------------------------- ---------------------------------------------------
When injecting fixtures to test functions, pytest-2.0 introduced the Fixtures allow test functions to easily receive and work
term "funcargs" or "funcarg mechanism" which continues to be present against specific pre-initialized application objects without having
also in docs today. It now refers to the specific case of injecting to care about import/setup/cleanup details.
fixture values as arguments to test functions. With pytest-2.3 there are It's a prime example of `dependency injection`_ where fixture
more possibilities to use fixtures but "funcargs" remain as the main way
as they allow to directly state the dependencies of a test function.
As the following examples show in more detail, funcargs allow test
functions to easily receive and work against specific pre-initialized
application objects without having to care about import/setup/cleanup
details. It's a prime example of `dependency injection`_ where fixture
functions take the role of the *injector* and test functions are the functions take the role of the *injector* and test functions are the
*consumers* of fixture objects. *consumers* of fixture objects.
@ -176,7 +169,7 @@ function (in or below the directory where ``conftest.py`` is located)::
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes assert 0 # for demo purposes
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
@ -191,32 +184,32 @@ inspect what is going on and can now run the tests::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 2 items collected 2 items
test_module.py FF test_module.py FF
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo ________ _______ test_ehlo ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:6: AssertionError test_module.py:6: AssertionError
_______ test_noop ________ _______ test_noop ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
======= 2 failed in 0.12 seconds ======== ======= 2 failed in 0.12 seconds ========
@ -267,7 +260,7 @@ Let's execute it::
$ pytest -s -q --tb=no $ pytest -s -q --tb=no
FFteardown smtp FFteardown smtp
2 failed in 0.12 seconds 2 failed in 0.12 seconds
We see that the ``smtp`` instance is finalized after the two We see that the ``smtp`` instance is finalized after the two
@ -296,6 +289,9 @@ The ``smtp`` connection will be closed after the test finished execution
because the ``smtp`` object automatically closes when because the ``smtp`` object automatically closes when
the ``with`` statement ends. the ``with`` statement ends.
Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
*teardown* code (after the ``yield``) will not be called.
.. note:: .. note::
Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one
@ -303,29 +299,51 @@ the ``with`` statement ends.
fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed
and considered deprecated. and considered deprecated.
.. note::
As historical note, another way to write teardown code is
by accepting a ``request`` object into your fixture function and can call its
``request.addfinalizer`` one or multiple times::
# content of conftest.py An alternative option for executing *teardown* code is to
make use of the ``addfinalizer`` method of the `request-context`_ object to register
finalization functions.
import smtplib Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
import pytest
@pytest.fixture(scope="module") .. code-block:: python
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value
The ``fin`` function will execute when the last test in the module has finished execution. # content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
ends, but ``addfinalizer`` has two key differences over ``yield``:
1. It is possible to register multiple finalizer functions.
2. Finalizers will always be called regardless if the fixture *setup* code raises an exception.
This is handy to properly close all resources created by a fixture even if one of them
fails to be created/acquired::
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still
be properly closed. Of course, if an exception happens before the finalize function is
registered then it will not be executed.
This method is still fully supported, but ``yield`` is recommended from 2.10 onward because
it is considered simpler and better describes the natural code flow.
.. _`request-context`: .. _`request-context`:
@ -355,7 +373,7 @@ again, nothing much has changed::
$ pytest -s -q --tb=no $ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com) FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
2 failed in 0.12 seconds 2 failed in 0.12 seconds
Let's quickly create another test module that actually sets the Let's quickly create another test module that actually sets the
@ -423,51 +441,51 @@ So let's just do another run::
FFFF FFFF
======= FAILURES ======== ======= FAILURES ========
_______ test_ehlo[smtp.gmail.com] ________ _______ test_ehlo[smtp.gmail.com] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
assert b"smtp.gmail.com" in msg assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:6: AssertionError test_module.py:6: AssertionError
_______ test_noop[smtp.gmail.com] ________ _______ test_noop[smtp.gmail.com] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
_______ test_ehlo[mail.python.org] ________ _______ test_ehlo[mail.python.org] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp): def test_ehlo(smtp):
response, msg = smtp.ehlo() response, msg = smtp.ehlo()
assert response == 250 assert response == 250
> assert b"smtp.gmail.com" in msg > assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
test_module.py:5: AssertionError test_module.py:5: AssertionError
-------------------------- Captured stdout setup --------------------------- -------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> finalizing <smtplib.SMTP object at 0xdeadbeef>
_______ test_noop[mail.python.org] ________ _______ test_noop[mail.python.org] ________
smtp = <smtplib.SMTP object at 0xdeadbeef> smtp = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp): def test_noop(smtp):
response, msg = smtp.noop() response, msg = smtp.noop()
assert response == 250 assert response == 250
> assert 0 # for demo purposes > assert 0 # for demo purposes
E assert 0 E assert 0
test_module.py:11: AssertionError test_module.py:11: AssertionError
------------------------- Captured stdout teardown ------------------------- ------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> finalizing <smtplib.SMTP object at 0xdeadbeef>
@ -539,7 +557,7 @@ Running the above tests results in the following test IDs being used::
<Function 'test_noop[smtp.gmail.com]'> <Function 'test_noop[smtp.gmail.com]'>
<Function 'test_ehlo[mail.python.org]'> <Function 'test_ehlo[mail.python.org]'>
<Function 'test_noop[mail.python.org]'> <Function 'test_noop[mail.python.org]'>
======= no tests ran in 0.12 seconds ======== ======= no tests ran in 0.12 seconds ========
.. _`interdependent fixtures`: .. _`interdependent fixtures`:
@ -578,10 +596,10 @@ Here we declare an ``app`` fixture which receives the previously defined
cachedir: .cache cachedir: .cache
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 2 items collecting ... collected 2 items
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED test_appsetup.py::test_smtp_exists[mail.python.org] PASSED
======= 2 passed in 0.12 seconds ======== ======= 2 passed in 0.12 seconds ========
Due to the parametrization of ``smtp`` the test will run twice with two Due to the parametrization of ``smtp`` the test will run twice with two
@ -647,26 +665,26 @@ Let's run the tests in verbose mode and with looking at the print-output::
cachedir: .cache cachedir: .cache
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 8 items collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1 test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1 RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2 test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2 RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1 test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1 RUN test1 with modarg mod1
PASSED PASSED
test_module.py::test_2[1-mod1] SETUP otherarg 1 test_module.py::test_2[1-mod1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1 RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod1] SETUP otherarg 2 test_module.py::test_2[2-mod1] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1 RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1 test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2 SETUP modarg mod2
RUN test1 with modarg mod2 RUN test1 with modarg mod2
@ -674,13 +692,13 @@ Let's run the tests in verbose mode and with looking at the print-output::
test_module.py::test_2[1-mod2] SETUP otherarg 1 test_module.py::test_2[1-mod2] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2 RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1 PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod2] SETUP otherarg 2 test_module.py::test_2[2-mod2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2 RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2 PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2 TEARDOWN modarg mod2
======= 8 passed in 0.12 seconds ======== ======= 8 passed in 0.12 seconds ========
You can see that the parametrized module-scoped ``modarg`` resource caused an You can see that the parametrized module-scoped ``modarg`` resource caused an
@ -782,8 +800,8 @@ Autouse fixtures (xUnit setup on steroids)
.. regendoc:wipe .. regendoc:wipe
Occasionally, you may want to have fixtures get invoked automatically Occasionally, you may want to have fixtures get invoked automatically
without a `usefixtures`_ or `funcargs`_ reference. As a practical without declaring a function argument explicitly or a `usefixtures`_ decorator.
example, suppose we have a database fixture which has a As a practical example, suppose we have a database fixture which has a
begin/rollback/commit architecture and we want to automatically surround begin/rollback/commit architecture and we want to automatically surround
each test method by a transaction and a rollback. Here is a dummy each test method by a transaction and a rollback. Here is a dummy
self-contained implementation of this idea:: self-contained implementation of this idea::

View File

@ -83,8 +83,8 @@ Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each
License License
------- -------
Copyright Holger Krekel and others, 2004-2016. Copyright Holger Krekel and others, 2004-2017.
Distributed under the terms of the `MIT`_ license, pytest is free and open source software. Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE .. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE

View File

@ -9,7 +9,7 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2004-2016 Holger Krekel and others Copyright (c) 2004-2017 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -255,11 +255,11 @@ if ``myapp.testsupport.myplugin`` also declares ``pytest_plugins``, the contents
of the variable will also be loaded as plugins, and so on. of the variable will also be loaded as plugins, and so on.
This mechanism makes it easy to share fixtures within applications or even This mechanism makes it easy to share fixtures within applications or even
external applications without the need to create external plugins using external applications without the need to create external plugins using
the ``setuptools``'s entry point technique. the ``setuptools``'s entry point technique.
Plugins imported by ``pytest_plugins`` will also automatically be marked Plugins imported by ``pytest_plugins`` will also automatically be marked
for assertion rewriting (see :func:`pytest.register_assert_rewrite`). for assertion rewriting (see :func:`pytest.register_assert_rewrite`).
However for this to have any effect the module must not be However for this to have any effect the module must not be
imported already; if it was already imported at the time the imported already; if it was already imported at the time the
``pytest_plugins`` statement is processed, a warning will result and ``pytest_plugins`` statement is processed, a warning will result and
@ -357,6 +357,8 @@ allowed to raise exceptions. Doing so will break the pytest run.
.. _firstresult:
firstresult: stop at first non-None result firstresult: stop at first non-None result
------------------------------------------- -------------------------------------------

View File

@ -31,5 +31,5 @@ template = "changelog/_template.rst"
[[tool.towncrier.type]] [[tool.towncrier.type]]
directory = "trivial" directory = "trivial"
name = "Trivial Changes" name = "Trivial/Internal Changes"
showcontent = false showcontent = true

View File

@ -1,20 +0,0 @@
"""
Script used by tox.ini to check the manifest file if we are under version control, or skip the
check altogether if not.
"check-manifest" will needs a vcs to work, which is not available when testing the package
instead of the source code (with ``devpi test`` for example).
"""
from __future__ import print_function
import os
import subprocess
import sys
from check_manifest import main
if os.path.isdir('.git'):
sys.exit(main())
else:
print('No .git directory found, skipping checking the manifest file')
sys.exit(0)

View File

@ -4,6 +4,10 @@ Invoke tasks to help with pytest development and release process.
import invoke import invoke
from . import generate from . import generate, vendoring
ns = invoke.Collection(generate)
ns = invoke.Collection(
generate,
vendoring
)

23
tasks/vendoring.py Normal file
View File

@ -0,0 +1,23 @@
from __future__ import absolute_import, print_function
import py
import invoke
VENDOR_TARGET = py.path.local("_pytest/vendored_packages")
GOOD_FILES = 'README.md', '__init__.py'
@invoke.task()
def remove_libs(ctx):
print("removing vendored libs")
for path in VENDOR_TARGET.listdir():
if path.basename not in GOOD_FILES:
print(" ", path)
path.remove()
@invoke.task(pre=[remove_libs])
def update_libs(ctx):
print("installing libs")
ctx.run("pip install -t {target} pluggy".format(target=VENDOR_TARGET))
ctx.run("git add {target}".format(target=VENDOR_TARGET))
print("Please commit to finish the update after running the tests:")
print()
print(' git commit -am "Updated vendored libs"')

View File

@ -317,8 +317,8 @@ class TestGeneralUsage(object):
]) ])
assert 'sessionstarttime' not in result.stderr.str() assert 'sessionstarttime' not in result.stderr.str()
@pytest.mark.parametrize('lookfor', ['test_fun.py', 'test_fun.py::test_a']) @pytest.mark.parametrize('lookfor', ['test_fun.py::test_a'])
def test_issue134_report_syntaxerror_when_collecting_member(self, testdir, lookfor): def test_issue134_report_error_when_collecting_member(self, testdir, lookfor):
testdir.makepyfile(test_fun=""" testdir.makepyfile(test_fun="""
def test_a(): def test_a():
pass pass

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
import operator import operator
import _pytest import _pytest
import py import py
@ -1173,3 +1174,25 @@ def test_exception_repr_extraction_error_on_recursion():
'*The following exception happened*', '*The following exception happened*',
'*ValueError: The truth value of an array*', '*ValueError: The truth value of an array*',
]) ])
def test_no_recursion_index_on_recursion_error():
"""
Ensure that we don't break in case we can't find the recursion index
during a recursion error (#2486).
"""
try:
class RecursionDepthError(object):
def __getattr__(self, attr):
return getattr(self, '_' + attr)
RecursionDepthError().trigger
except:
from _pytest._code.code import ExceptionInfo
exc_info = ExceptionInfo()
if sys.version_info[:2] == (2, 6):
assert "'RecursionDepthError' object has no attribute '___" in str(exc_info.getrepr())
else:
assert 'maximum recursion' in str(exc_info.getrepr())
else:
assert 0

View File

@ -657,6 +657,39 @@ class TestRequestBasic(object):
"*1 error*" # XXX the whole module collection fails "*1 error*" # XXX the whole module collection fails
]) ])
def test_request_subrequest_addfinalizer_exceptions(self, testdir):
"""
Ensure exceptions raised during teardown by a finalizer are suppressed
until all finalizers are called, re-raising the first exception (#2440)
"""
testdir.makepyfile("""
import pytest
l = []
def _excepts(where):
raise Exception('Error in %s fixture' % where)
@pytest.fixture
def subrequest(request):
return request
@pytest.fixture
def something(subrequest):
subrequest.addfinalizer(lambda: l.append(1))
subrequest.addfinalizer(lambda: l.append(2))
subrequest.addfinalizer(lambda: _excepts('something'))
@pytest.fixture
def excepts(subrequest):
subrequest.addfinalizer(lambda: _excepts('excepts'))
subrequest.addfinalizer(lambda: l.append(3))
def test_first(something, excepts):
pass
def test_second():
assert l == [3, 2, 1]
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines([
'*Exception: Error in excepts fixture',
'* 2 passed, 1 error in *',
])
def test_request_getmodulepath(self, testdir): def test_request_getmodulepath(self, testdir):
modcol = testdir.getmodulecol("def test_somefunc(): pass") modcol = testdir.getmodulecol("def test_somefunc(): pass")
item, = testdir.genitems([modcol]) item, = testdir.genitems([modcol])

View File

@ -369,6 +369,11 @@ class TestSession(object):
assert len(colitems) == 1 assert len(colitems) == 1
assert colitems[0].fspath == p assert colitems[0].fspath == p
def get_reported_items(self, hookrec):
"""Return pytest.Item instances reported by the pytest_collectreport hook"""
calls = hookrec.getcalls('pytest_collectreport')
return [x for call in calls for x in call.report.result
if isinstance(x, pytest.Item)]
def test_collect_protocol_single_function(self, testdir): def test_collect_protocol_single_function(self, testdir):
p = testdir.makepyfile("def test_func(): pass") p = testdir.makepyfile("def test_func(): pass")
@ -386,9 +391,10 @@ class TestSession(object):
("pytest_collectstart", "collector.fspath == p"), ("pytest_collectstart", "collector.fspath == p"),
("pytest_make_collect_report", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid.startswith(p.basename)"), ("pytest_collectreport", "report.result[0].name == 'test_func'"),
("pytest_collectreport", "report.nodeid == ''")
]) ])
# ensure we are reporting the collection of the single test item (#2464)
assert [x.name for x in self.get_reported_items(hookrec)] == ['test_func']
def test_collect_protocol_method(self, testdir): def test_collect_protocol_method(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
@ -407,6 +413,8 @@ class TestSession(object):
assert items[0].name == "test_method" assert items[0].name == "test_method"
newid = items[0].nodeid newid = items[0].nodeid
assert newid == normid assert newid == normid
# ensure we are reporting the collection of the single test item (#2464)
assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method']
def test_collect_custom_nodes_multi_id(self, testdir): def test_collect_custom_nodes_multi_id(self, testdir):
p = testdir.makepyfile("def test_func(): pass") p = testdir.makepyfile("def test_func(): pass")
@ -436,9 +444,8 @@ class TestSession(object):
"collector.__class__.__name__ == 'Module'"), "collector.__class__.__name__ == 'Module'"),
("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.nodeid.startswith(p.basename)"), ("pytest_collectreport", "report.nodeid.startswith(p.basename)"),
#("pytest_collectreport",
# "report.fspath == %r" % str(rcol.fspath)),
]) ])
assert len(self.get_reported_items(hookrec)) == 2
def test_collect_subdir_event_ordering(self, testdir): def test_collect_subdir_event_ordering(self, testdir):
p = testdir.makepyfile("def test_func(): pass") p = testdir.makepyfile("def test_func(): pass")
@ -495,11 +502,13 @@ class TestSession(object):
def test_method(self): def test_method(self):
pass pass
""") """)
arg = p.basename + ("::TestClass::test_method") arg = p.basename + "::TestClass::test_method"
items, hookrec = testdir.inline_genitems(arg) items, hookrec = testdir.inline_genitems(arg)
assert len(items) == 1 assert len(items) == 1
item, = items item, = items
assert item.nodeid.endswith("TestClass::()::test_method") assert item.nodeid.endswith("TestClass::()::test_method")
# ensure we are reporting the collection of the single test item (#2464)
assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method']
class Test_getinitialnodes(object): class Test_getinitialnodes(object):
def test_global_file(self, testdir, tmpdir): def test_global_file(self, testdir, tmpdir):

View File

@ -527,6 +527,25 @@ class TestDoctests(object):
'*1 failed*', '*1 failed*',
]) ])
def test_unicode_doctest_module(self, testdir):
"""
Test case for issue 2434: DecodeError on Python 2 when doctest docstring
contains non-ascii characters.
"""
p = testdir.makepyfile(test_unicode_doctest_module="""
# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
def fix_bad_unicode(text):
'''
>>> print(fix_bad_unicode('único'))
único
'''
return "único"
""")
result = testdir.runpytest(p, '--doctest-modules')
result.stdout.fnmatch_lines(['* 1 passed *'])
class TestLiterals(object): class TestLiterals(object):

View File

@ -77,7 +77,7 @@ class TestDeprecatedCall(object):
def test_deprecated_call_raises(self): def test_deprecated_call_raises(self):
with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError) as excinfo:
pytest.deprecated_call(self.dep, 3, 5) pytest.deprecated_call(self.dep, 3, 5)
assert str(excinfo).find("did not produce") != -1 assert 'Did not produce' in str(excinfo)
def test_deprecated_call(self): def test_deprecated_call(self):
pytest.deprecated_call(self.dep, 0, 5) pytest.deprecated_call(self.dep, 0, 5)
@ -106,31 +106,69 @@ class TestDeprecatedCall(object):
pytest.deprecated_call(self.dep_explicit, 0) pytest.deprecated_call(self.dep_explicit, 0)
pytest.deprecated_call(self.dep_explicit, 0) pytest.deprecated_call(self.dep_explicit, 0)
def test_deprecated_call_as_context_manager_no_warning(self): @pytest.mark.parametrize('mode', ['context_manager', 'call'])
with pytest.raises(pytest.fail.Exception, matches='^DID NOT WARN'): def test_deprecated_call_no_warning(self, mode):
with pytest.deprecated_call(): """Ensure deprecated_call() raises the expected failure when its block/function does
self.dep(1) not raise a deprecation warning.
"""
def f():
pass
msg = 'Did not produce DeprecationWarning or PendingDeprecationWarning'
with pytest.raises(AssertionError, matches=msg):
if mode == 'call':
pytest.deprecated_call(f)
else:
with pytest.deprecated_call():
f()
@pytest.mark.parametrize('warning_type', [PendingDeprecationWarning, DeprecationWarning]) @pytest.mark.parametrize('warning_type', [PendingDeprecationWarning, DeprecationWarning])
@pytest.mark.parametrize('mode', ['context_manager', 'call']) @pytest.mark.parametrize('mode', ['context_manager', 'call'])
def test_deprecated_call_modes(self, warning_type, mode): @pytest.mark.parametrize('call_f_first', [True, False])
def test_deprecated_call_modes(self, warning_type, mode, call_f_first):
"""Ensure deprecated_call() captures a deprecation warning as expected inside its
block/function.
"""
def f(): def f():
warnings.warn(warning_type("hi")) warnings.warn(warning_type("hi"))
return 10
# ensure deprecated_call() can capture the warning even if it has already been triggered
if call_f_first:
assert f() == 10
if mode == 'call': if mode == 'call':
pytest.deprecated_call(f) assert pytest.deprecated_call(f) == 10
else: else:
with pytest.deprecated_call(): with pytest.deprecated_call():
f() assert f() == 10
@pytest.mark.parametrize('mode', ['context_manager', 'call'])
def test_deprecated_call_exception_is_raised(self, mode):
"""If the block of the code being tested by deprecated_call() raises an exception,
it must raise the exception undisturbed.
"""
def f():
raise ValueError('some exception')
with pytest.raises(ValueError, match='some exception'):
if mode == 'call':
pytest.deprecated_call(f)
else:
with pytest.deprecated_call():
f()
def test_deprecated_call_specificity(self): def test_deprecated_call_specificity(self):
other_warnings = [Warning, UserWarning, SyntaxWarning, RuntimeWarning, other_warnings = [Warning, UserWarning, SyntaxWarning, RuntimeWarning,
FutureWarning, ImportWarning, UnicodeWarning] FutureWarning, ImportWarning, UnicodeWarning]
for warning in other_warnings: for warning in other_warnings:
def f(): def f():
py.std.warnings.warn(warning("hi")) warnings.warn(warning("hi"))
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
pytest.deprecated_call(f) pytest.deprecated_call(f)
with pytest.raises(AssertionError):
with pytest.deprecated_call():
f()
def test_deprecated_function_already_called(self, testdir): def test_deprecated_function_already_called(self, testdir):
"""deprecated_call should be able to catch a call to a deprecated """deprecated_call should be able to catch a call to a deprecated

View File

@ -513,12 +513,12 @@ def test_pytest_no_tests_collected_exit_status(testdir):
assert 1 assert 1
""") """)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines('*collected 1 items*') result.stdout.fnmatch_lines('*collected 1 item*')
result.stdout.fnmatch_lines('*1 passed*') result.stdout.fnmatch_lines('*1 passed*')
assert result.ret == main.EXIT_OK assert result.ret == main.EXIT_OK
result = testdir.runpytest('-k nonmatch') result = testdir.runpytest('-k nonmatch')
result.stdout.fnmatch_lines('*collected 1 items*') result.stdout.fnmatch_lines('*collected 1 item*')
result.stdout.fnmatch_lines('*1 deselected*') result.stdout.fnmatch_lines('*1 deselected*')
assert result.ret == main.EXIT_NOTESTSCOLLECTED assert result.ret == main.EXIT_NOTESTSCOLLECTED

View File

@ -204,6 +204,15 @@ class TestTerminal(object):
assert result.ret == 2 assert result.ret == 2
result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) result.stdout.fnmatch_lines(['*KeyboardInterrupt*'])
def test_collect_single_item(self, testdir):
"""Use singular 'item' when reporting a single test item"""
testdir.makepyfile("""
def test_foobar():
pass
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['collected 1 item'])
class TestCollectonly(object): class TestCollectonly(object):
def test_collectonly_basic(self, testdir): def test_collectonly_basic(self, testdir):

View File

@ -57,9 +57,7 @@ deps =
# pygments required by rst-lint # pygments required by rst-lint
pygments pygments
restructuredtext_lint restructuredtext_lint
check-manifest
commands = commands =
{envpython} scripts/check-manifest.py
flake8 pytest.py _pytest testing flake8 pytest.py _pytest testing
{envpython} scripts/check-rst.py {envpython} scripts/check-rst.py