Merge pull request #4297 from nicoddemus/release-3.10.0

Release 3.10.0
This commit is contained in:
Bruno Oliveira 2018-11-04 12:25:30 -03:00 committed by GitHub
commit d1c9c54571
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 665 additions and 131 deletions

View File

@ -59,6 +59,7 @@ Danielle Jenkins
Dave Hunt
David Díaz-Barquero
David Mohr
David Szotten
David Vierra
Daw-Ran Liou
Denis Kirisov
@ -161,6 +162,7 @@ Miro Hrončok
Nathaniel Waisbrot
Ned Batchelder
Neven Mundar
Niclas Olofsson
Nicolas Delaby
Oleg Pidsadnyi
Oleg Sushchenko
@ -202,6 +204,7 @@ Stefan Zimmermann
Stefano Taschini
Steffen Allner
Stephan Obermann
Sven-Hendrik Haase
Tadek Teleżyński
Tarcisio Fischer
Tareq Alayan

View File

@ -18,6 +18,72 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 3.10.0 (2018-11-03)
==========================
Features
--------
- `#2619 <https://github.com/pytest-dev/pytest/issues/2619>`_: Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``.
This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the
existing ``pytest_enter_pdb`` hook.
- `#4147 <https://github.com/pytest-dev/pytest/issues/4147>`_: Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation <https://docs.pytest.org/en/latest/cache.html#stepwise>`__ for more info.
- `#4188 <https://github.com/pytest-dev/pytest/issues/4188>`_: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed.
- `#4225 <https://github.com/pytest-dev/pytest/issues/4225>`_: Improve performance with collection reporting in non-quiet mode with terminals.
The "collecting …" message is only printed/updated every 0.5s.
Bug Fixes
---------
- `#2701 <https://github.com/pytest-dev/pytest/issues/2701>`_: Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings.
- `#4046 <https://github.com/pytest-dev/pytest/issues/4046>`_: Fix problems with running tests in package ``__init__.py`` files.
- `#4260 <https://github.com/pytest-dev/pytest/issues/4260>`_: Swallow warnings during anonymous compilation of source.
- `#4262 <https://github.com/pytest-dev/pytest/issues/4262>`_: Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``.
- `#611 <https://github.com/pytest-dev/pytest/issues/611>`_: Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and
should not be overwritten as it will lead to internal errors.
Improved Documentation
----------------------
- `#4255 <https://github.com/pytest-dev/pytest/issues/4255>`_: Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped.
Trivial/Internal Changes
------------------------
- `#4272 <https://github.com/pytest-dev/pytest/issues/4272>`_: Display cachedir also in non-verbose mode if non-default.
- `#4277 <https://github.com/pytest-dev/pytest/issues/4277>`_: pdb: improve message about output capturing with ``set_trace``.
Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid
confusion.
- `#4279 <https://github.com/pytest-dev/pytest/issues/4279>`_: Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``.
pytest 3.9.3 (2018-10-27)
=========================
@ -366,7 +432,7 @@ Features
the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`_ for more info.
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`__ for more info.
- `#2908 <https://github.com/pytest-dev/pytest/issues/2908>`_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is

View File

@ -1 +0,0 @@
Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings.

View File

@ -1 +0,0 @@
Fix problems with running tests in package ``__init__.py`` files.

View File

@ -1 +0,0 @@
Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped.

View File

@ -1 +0,0 @@
Swallow warnings during anonymous compilation of source.

View File

@ -1 +0,0 @@
Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``.

View File

@ -1 +0,0 @@
Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
release-3.10.0
release-3.9.3
release-3.9.2
release-3.9.1

View File

@ -0,0 +1,43 @@
pytest-3.10.0
=======================================
The pytest team is proud to announce the 3.10.0 release!
pytest is a mature Python testing tool with more than a 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
to take a look at the CHANGELOG:
https://docs.pytest.org/en/latest/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/latest/
As usual, you can upgrade from pypi via:
pip install -U pytest
Thanks to all who contributed to this release, among them:
* Anders Hovmöller
* Andreu Vallbona Plazas
* Ankit Goel
* Anthony Sottile
* Bernardo Gomes
* Brianna Laugher
* Bruno Oliveira
* Daniel Hahler
* David Szotten
* Mick Koch
* Niclas Olofsson
* Palash Chatterjee
* Ronny Pfannschmidt
* Sven-Hendrik Haase
* Ville Skyttä
* William Jamir Silva
Happy testing,
The Pytest Development Team

View File

@ -244,6 +244,8 @@ You can always peek at the content of the cache using the
{'test_caching.py::test_function': True}
cache/nodeids contains:
['test_caching.py::test_function']
cache/stepwise contains:
[]
example/value contains:
42
@ -260,3 +262,9 @@ by adding the ``--cache-clear`` option like this::
This is recommended for invocations from Continuous Integration
servers where isolation and correctness is more important
than speed.
Stepwise
--------
As an alternative to ``--lf -x``, especially for cases where you expect a large part of the test suite will fail, ``--sw``, ``--stepwise`` allows you to fix them one at a time. The test suite will run until the first failure and then stop. At the next invocation, tests will continue from the last failing test and then run until the next failing test. You may use the ``--stepwise-skip`` option to ignore one failing test and stop the test execution on the second failing test instead. This is useful if you get stuck on a failing test and just want to ignore it until later.

View File

@ -33,7 +33,7 @@ class Python(object):
dumpfile = self.picklefile.dirpath("dump.py")
dumpfile.write(
textwrap.dedent(
r"""\
r"""
import pickle
f = open({!r}, 'wb')
s = pickle.dump({!r}, f, protocol=2)
@ -49,7 +49,7 @@ class Python(object):
loadfile = self.picklefile.dirpath("load.py")
loadfile.write(
textwrap.dedent(
r"""\
r"""
import pickle
f = open({!r}, 'rb')
obj = pickle.load(f)

View File

@ -85,8 +85,9 @@ interesting to just look at the collection tree::
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
collected 2 items
<Package '$REGENDOC_TMPDIR/nonpython'>
<YamlFile 'test_simple.yml'>
<YamlItem 'hello'>
<YamlItem 'ok'>
<Package '$REGENDOC_TMPDIR/nonpython'>
<YamlFile 'test_simple.yml'>
<YamlItem 'hello'>
<YamlItem 'ok'>
======================= no tests ran in 0.12 seconds =======================

View File

@ -421,21 +421,9 @@ additionally it is possible to copy examples for an example folder before runnin
test_example.py::test_plugin
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
testdir.copy_example("test_example.py")
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead
return getattr(object, name, default)
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead
return getattr(object, name, default)
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead
return getattr(object, name, default)
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead
return getattr(object, name, default)
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead
return getattr(object, name, default)
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead
return getattr(object, name, default)
-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================== 2 passed, 7 warnings in 0.12 seconds ===================
=================== 2 passed, 1 warnings in 0.12 seconds ===================
For more information about the result object that ``runpytest()`` returns, and
the methods that it provides please check out the :py:class:`RunResult

View File

@ -319,7 +319,8 @@ def cache(request):
def pytest_report_header(config):
if config.option.verbose:
"""Display cachedir with --cache-show and if non-default."""
if config.option.verbose or config.getini("cache_dir") != ".pytest_cache":
cachedir = config.cache._cachedir
# TODO: evaluate generating upward relative paths
# starting with .., ../.. if sensible

View File

@ -102,6 +102,9 @@ class CaptureManager(object):
# Global capturing control
def is_globally_capturing(self):
return self._method != "no"
def start_global_capturing(self):
assert self._global_capturing is None
self._global_capturing = self._getcapture(self._method)

View File

@ -428,3 +428,16 @@ class FuncargnamesCompatAttr(object):
def funcargnames(self):
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
return self.fixturenames
if six.PY2:
def lru_cache(*_, **__):
def dec(fn):
return fn
return dec
else:
from functools import lru_cache # noqa: F401

View File

@ -27,6 +27,7 @@ from .findpaths import determine_setup
from .findpaths import exists
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest.compat import lru_cache
from _pytest.compat import safe_str
from _pytest.outcomes import Skipped
@ -133,6 +134,7 @@ default_plugins = (
"freeze_support",
"setuponly",
"setupplan",
"stepwise",
"warnings",
"logging",
)
@ -212,7 +214,7 @@ class PytestPluginManager(PluginManager):
self._conftest_plugins = set()
# state related to local conftest plugins
self._path2confmods = {}
self._dirpath2confmods = {}
self._conftestpath2mod = {}
self._confcutdir = None
self._noconftest = False
@ -383,31 +385,35 @@ class PytestPluginManager(PluginManager):
if x.check(dir=1):
self._getconftestmodules(x)
@lru_cache(maxsize=128)
def _getconftestmodules(self, path):
if self._noconftest:
return []
try:
return self._path2confmods[path]
except KeyError:
if path.isfile():
directory = path.dirpath()
else:
directory = path
# XXX these days we may rather want to use config.rootdir
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir
clist = []
for parent in directory.realpath().parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
mod = self._importconftest(conftestpath)
clist.append(mod)
if path.isfile():
directory = path.dirpath()
else:
directory = path
self._path2confmods[path] = clist
return clist
if six.PY2: # py2 is not using lru_cache.
try:
return self._dirpath2confmods[directory]
except KeyError:
pass
# XXX these days we may rather want to use config.rootdir
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir
clist = []
for parent in directory.realpath().parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
mod = self._importconftest(conftestpath)
clist.append(mod)
self._dirpath2confmods[directory] = clist
return clist
def _rget_with_confmod(self, name, path):
modules = self._getconftestmodules(path)
@ -448,8 +454,8 @@ class PytestPluginManager(PluginManager):
self._conftest_plugins.add(mod)
self._conftestpath2mod[conftestpath] = mod
dirpath = conftestpath.dirpath()
if dirpath in self._path2confmods:
for path, mods in self._path2confmods.items():
if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items():
if path and path.relto(dirpath) or path == dirpath:
assert mod not in mods
mods.append(mod)

View File

@ -88,10 +88,54 @@ class pytestPDB(object):
capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
if capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")
class _PdbWrapper(cls._pdb_cls, object):
_pytest_capman = capman
_continued = False
def do_continue(self, arg):
ret = super(_PdbWrapper, self).do_continue(arg)
if self._pytest_capman:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if self._pytest_capman.is_globally_capturing():
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(">", "PDB continue")
self._pytest_capman.resume_global_capture()
cls._pluginmanager.hook.pytest_leave_pdb(
config=cls._config, pdb=self
)
self._continued = True
return ret
do_c = do_cont = do_continue
def setup(self, f, tb):
"""Suspend on setup().
Needed after do_continue resumed, and entering another
breakpoint again.
"""
ret = super(_PdbWrapper, self).setup(f, tb)
if not ret and self._continued:
# pdb.setup() returns True if the command wants to exit
# from the interaction: do not suspend capturing then.
if self._pytest_capman:
self._pytest_capman.suspend_global_capture(in_=True)
return ret
_pdb = _PdbWrapper()
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
else:
_pdb = cls._pdb_cls()
if set_break:
cls._pdb_cls().set_trace(frame)
_pdb.set_trace(frame)
class PdbInvoke(object):

View File

@ -12,6 +12,7 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warning_types import UnformattedWarning
@ -57,6 +58,10 @@ FIXTURE_FUNCTION_CALL = UnformattedWarning(
"See https://docs.pytest.org/en/latest/fixture.html for more information.",
)
FIXTURE_NAMED_REQUEST = PytestDeprecationWarning(
"'request' is a reserved name for fixtures and will raise an error in future versions"
)
CFG_PYTEST_SECTION = UnformattedWarning(
RemovedInPytest4Warning,
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.",

View File

@ -34,6 +34,7 @@ from _pytest.compat import isclass
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.deprecated import FIXTURE_FUNCTION_CALL
from _pytest.deprecated import FIXTURE_NAMED_REQUEST
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
@ -1036,6 +1037,9 @@ class FixtureFunctionMarker(object):
function = wrap_function_to_warning_if_called_directly(function, self)
name = self.name or function.__name__
if name == "request":
warnings.warn(FIXTURE_NAMED_REQUEST)
function._pytestfixturefunction = self
return function

View File

@ -603,9 +603,21 @@ def pytest_exception_interact(node, call, report):
"""
def pytest_enter_pdb(config):
def pytest_enter_pdb(config, pdb):
""" called upon pdb.set_trace(), can be used by plugins to take special
action just before the python debugger enters in interactive mode.
:param _pytest.config.Config config: pytest config object
:param pdb.Pdb pdb: Pdb instance
"""
def pytest_leave_pdb(config, pdb):
""" called when leaving pdb (e.g. with continue after pdb.set_trace()).
Can be used by plugins to take special action just after the python
debugger leaves interactive mode.
:param _pytest.config.Config config: pytest config object
:param pdb.Pdb pdb: Pdb instance
"""

View File

@ -18,6 +18,7 @@ from _pytest.config import directory_arg
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.outcomes import exit
from _pytest.pathlib import parts
from _pytest.runner import collect_one_node
@ -469,8 +470,8 @@ class Session(nodes.FSCollector):
return items
def collect(self):
for parts in self._initialparts:
arg = "::".join(map(str, parts))
for initialpart in self._initialparts:
arg = "::".join(map(str, initialpart))
self.trace("processing argument", arg)
self.trace.root.indent += 1
try:
@ -488,7 +489,7 @@ class Session(nodes.FSCollector):
names = self._parsearg(arg)
argpath = names.pop(0).realpath()
paths = []
paths = set()
root = self
# Start with a Session root, and delve to argpath item (dir or file)
@ -517,21 +518,37 @@ class Session(nodes.FSCollector):
# Let the Package collector deal with subnodes, don't collect here.
if argpath.check(dir=1):
assert not names, "invalid arg %r" % (arg,)
for path in argpath.visit(
fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True
):
pkginit = path.dirpath().join("__init__.py")
if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
for x in root._collectfile(pkginit):
yield x
paths.append(x.fspath.dirpath())
if not any(x in path.parts() for x in paths):
if six.PY2:
def filter_(f):
return f.check(file=1) and not f.strpath.endswith("*.pyc")
else:
def filter_(f):
return f.check(file=1)
seen_dirs = set()
for path in argpath.visit(
fil=filter_, rec=self._recurse, bf=True, sort=True
):
dirpath = path.dirpath()
if dirpath not in seen_dirs:
seen_dirs.add(dirpath)
pkginit = dirpath.join("__init__.py")
if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths):
for x in root._collectfile(pkginit):
yield x
paths.add(x.fspath.dirpath())
if parts(path.strpath).isdisjoint(paths):
for x in root._collectfile(path):
if (type(x), x.fspath) in self._node_cache:
yield self._node_cache[(type(x), x.fspath)]
key = (type(x), x.fspath)
if key in self._node_cache:
yield self._node_cache[key]
else:
self._node_cache[(type(x), x.fspath)] = x
self._node_cache[key] = x
yield x
else:
assert argpath.check(file=1)
@ -570,15 +587,17 @@ class Session(nodes.FSCollector):
return ihook.pytest_collect_file(path=path, parent=self)
def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath())
if ihook.pytest_ignore_collect(path=path, config=self.config):
return
def _recurse(self, dirpath):
if dirpath.basename == "__pycache__":
return False
ihook = self.gethookproxy(dirpath.dirpath())
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
return False
for pat in self._norecursepatterns:
if path.check(fnmatch=pat):
if dirpath.check(fnmatch=pat):
return False
ihook = self.gethookproxy(path)
ihook.pytest_collect_directory(path=path, parent=self)
ihook = self.gethookproxy(dirpath)
ihook.pytest_collect_directory(path=dirpath, parent=self)
return True
def _tryconvertpyarg(self, x):

View File

@ -5,7 +5,6 @@ import itertools
import operator
import os
import shutil
import stat
import sys
import uuid
from functools import reduce
@ -43,17 +42,10 @@ def ensure_reset_dir(path):
path.mkdir()
def _shutil_rmtree_remove_writable(func, fspath, _):
"Clear the readonly bit and reattempt the removal"
os.chmod(fspath, stat.S_IWRITE)
func(fspath)
def rmtree(path, force=False):
if force:
# ignore_errors leaves dead folders around
# python needs a rm -rf as a followup
# the trick with _shutil_rmtree_remove_writable is unreliable
# NOTE: ignore_errors might leave dead folders around.
# Python needs a rm -rf as a followup.
shutil.rmtree(str(path), ignore_errors=True)
else:
shutil.rmtree(str(path))
@ -321,3 +313,8 @@ def fnmatch_ex(pattern, path):
else:
name = six.text_type(path)
return fnmatch.fnmatch(name, pattern)
def parts(s):
parts = s.split(sep)
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}

View File

@ -42,6 +42,7 @@ from _pytest.mark.structures import get_unpacked_marks
from _pytest.mark.structures import normalize_mark_list
from _pytest.mark.structures import transfer_markers
from _pytest.outcomes import fail
from _pytest.pathlib import parts
from _pytest.warning_types import PytestWarning
from _pytest.warning_types import RemovedInPytest4Warning
@ -517,15 +518,17 @@ class Package(Module):
self._norecursepatterns = session._norecursepatterns
self.fspath = fspath
def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath())
if ihook.pytest_ignore_collect(path=path, config=self.config):
def _recurse(self, dirpath):
if dirpath.basename == "__pycache__":
return False
ihook = self.gethookproxy(dirpath.dirpath())
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
return
for pat in self._norecursepatterns:
if path.check(fnmatch=pat):
if dirpath.check(fnmatch=pat):
return False
ihook = self.gethookproxy(path)
ihook.pytest_collect_directory(path=path, parent=self)
ihook = self.gethookproxy(dirpath)
ihook.pytest_collect_directory(path=dirpath, parent=self)
return True
def gethookproxy(self, fspath):
@ -561,19 +564,16 @@ class Package(Module):
yield Module(init_module, self)
pkg_prefixes = set()
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
# we will visit our own __init__.py file, in which case we skip it
skip = False
if path.basename == "__init__.py" and path.dirpath() == this_path:
continue
# We will visit our own __init__.py file, in which case we skip it.
if path.isfile():
if path.basename == "__init__.py" and path.dirpath() == this_path:
continue
for pkg_prefix in pkg_prefixes:
if (
pkg_prefix in path.parts()
and pkg_prefix.join("__init__.py") != path
):
skip = True
if skip:
parts_ = parts(path.strpath)
if any(
pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path
for pkg_prefix in pkg_prefixes
):
continue
if path.isdir() and path.join("__init__.py").check(file=1):

102
src/_pytest/stepwise.py Normal file
View File

@ -0,0 +1,102 @@
import pytest
def pytest_addoption(parser):
group = parser.getgroup("general")
group.addoption(
"--sw",
"--stepwise",
action="store_true",
dest="stepwise",
help="exit on test fail and continue from last failing test next time",
)
group.addoption(
"--stepwise-skip",
action="store_true",
dest="stepwise_skip",
help="ignore the first failing test but stop on the next failing test",
)
@pytest.hookimpl
def pytest_configure(config):
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
class StepwisePlugin:
def __init__(self, config):
self.config = config
self.active = config.getvalue("stepwise")
self.session = None
if self.active:
self.lastfailed = config.cache.get("cache/stepwise", None)
self.skip = config.getvalue("stepwise_skip")
def pytest_sessionstart(self, session):
self.session = session
def pytest_collection_modifyitems(self, session, config, items):
if not self.active or not self.lastfailed:
return
already_passed = []
found = False
# Make a list of all tests that have been run before the last failing one.
for item in items:
if item.nodeid == self.lastfailed:
found = True
break
else:
already_passed.append(item)
# If the previously failed test was not found among the test items,
# do not skip any tests.
if not found:
already_passed = []
for item in already_passed:
items.remove(item)
config.hook.pytest_deselected(items=already_passed)
def pytest_collectreport(self, report):
if self.active and report.failed:
self.session.shouldstop = (
"Error when collecting test, stopping test execution."
)
def pytest_runtest_logreport(self, report):
# Skip this hook if plugin is not active or the test is xfailed.
if not self.active or "xfail" in report.keywords:
return
if report.failed:
if self.skip:
# Remove test from the failed ones (if it exists) and unset the skip option
# to make sure the following tests will not be skipped.
if report.nodeid == self.lastfailed:
self.lastfailed = None
self.skip = False
else:
# Mark test as the last failing and interrupt the test session.
self.lastfailed = report.nodeid
self.session.shouldstop = (
"Test failed, continuing from this test next run."
)
else:
# If the test was actually run and did pass.
if report.when == "call":
# Remove test from the failed ones, if exists.
if report.nodeid == self.lastfailed:
self.lastfailed = None
def pytest_sessionfinish(self, session):
if self.active:
self.config.cache.set("cache/stepwise", self.lastfailed)
else:
# Clear the list of failing tests if the plugin is not active.
self.config.cache.set("cache/stepwise", [])

View File

@ -246,6 +246,7 @@ class TerminalReporter(object):
self.isatty = file.isatty()
self._progress_nodeids_reported = set()
self._show_progress_info = self._determine_show_progress_info()
self._collect_report_last_write = None
def _determine_show_progress_info(self):
"""Return True if we should display progress information based on the current config"""
@ -261,7 +262,7 @@ class TerminalReporter(object):
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
return char in self.reportchars
def write_fspath_result(self, nodeid, res):
def write_fspath_result(self, nodeid, res, **markup):
fspath = self.config.rootdir.join(nodeid.split("::")[0])
if fspath != self.currentfspath:
if self.currentfspath is not None and self._show_progress_info:
@ -270,7 +271,7 @@ class TerminalReporter(object):
fspath = self.startdir.bestrelpath(fspath)
self._tw.line()
self._tw.write(fspath + " ")
self._tw.write(res)
self._tw.write(res, **markup)
def write_ensure_prefix(self, prefix, extra="", **kwargs):
if self.currentfspath != prefix:
@ -384,22 +385,22 @@ class TerminalReporter(object):
# probably passed setup/teardown
return
running_xdist = hasattr(rep, "node")
if markup is None:
if rep.passed:
markup = {"green": True}
elif rep.failed:
markup = {"red": True}
elif rep.skipped:
markup = {"yellow": True}
else:
markup = {}
if self.verbosity <= 0:
if not running_xdist and self.showfspath:
self.write_fspath_result(rep.nodeid, letter)
self.write_fspath_result(rep.nodeid, letter, **markup)
else:
self._tw.write(letter)
self._tw.write(letter, **markup)
else:
self._progress_nodeids_reported.add(rep.nodeid)
if markup is None:
if rep.passed:
markup = {"green": True}
elif rep.failed:
markup = {"red": True}
elif rep.skipped:
markup = {"yellow": True}
else:
markup = {}
line = self._locationline(rep.nodeid, *rep.location)
if not running_xdist:
self.write_ensure_prefix(line, word, **markup)
@ -472,7 +473,11 @@ class TerminalReporter(object):
return self._tw.chars_on_current_line
def pytest_collection(self):
if not self.isatty and self.config.option.verbose >= 1:
if self.isatty:
if self.config.option.verbose >= 0:
self.write("collecting ... ", bold=True)
self._collect_report_last_write = time.time()
elif self.config.option.verbose >= 1:
self.write("collecting ... ", bold=True)
def pytest_collectreport(self, report):
@ -483,13 +488,19 @@ class TerminalReporter(object):
items = [x for x in report.result if isinstance(x, pytest.Item)]
self._numcollected += len(items)
if self.isatty:
# self.write_fspath_result(report.nodeid, 'E')
self.report_collect()
def report_collect(self, final=False):
if self.config.option.verbose < 0:
return
if not final:
# Only write "collecting" report every 0.5s.
t = time.time()
if self._collect_report_last_write > t - 0.5:
return
self._collect_report_last_write = t
errors = len(self.stats.get("error", []))
skipped = len(self.stats.get("skipped", []))
deselected = len(self.stats.get("deselected", []))

View File

@ -6,6 +6,8 @@ import os
import pytest
pytestmark = pytest.mark.pytester_example_path("deprecated")
@pytest.mark.filterwarnings("default")
def test_yield_tests_deprecation(testdir):
@ -394,3 +396,13 @@ def test_pycollector_makeitem_is_deprecated():
with pytest.warns(RemovedInPytest4Warning):
collector.makeitem("foo", "bar")
assert collector.called
def test_fixture_named_request(testdir):
testdir.copy_example()
result = testdir.runpytest()
result.stdout.fnmatch_lines(
[
"*'request' is a reserved name for fixtures and will raise an error in future versions"
]
)

View File

@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def request():
pass
def test():
pass

View File

@ -66,7 +66,8 @@ class TestNewAPI(object):
)
result = testdir.runpytest("-rw")
assert result.ret == 1
result.stdout.fnmatch_lines(["*could not create cache path*", "*2 warnings*"])
# warnings from nodeids, lastfailed, and stepwise
result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"])
def test_config_cache(self, testdir):
testdir.makeconftest(

View File

@ -49,14 +49,14 @@ class TestConftestValueAccessGlobal(object):
def test_immediate_initialiation_and_incremental_are_the_same(self, basedir):
conftest = PytestPluginManager()
len(conftest._path2confmods)
len(conftest._dirpath2confmods)
conftest._getconftestmodules(basedir)
snap1 = len(conftest._path2confmods)
# assert len(conftest._path2confmods) == snap1 + 1
snap1 = len(conftest._dirpath2confmods)
# assert len(conftest._dirpath2confmods) == snap1 + 1
conftest._getconftestmodules(basedir.join("adir"))
assert len(conftest._path2confmods) == snap1 + 1
assert len(conftest._dirpath2confmods) == snap1 + 1
conftest._getconftestmodules(basedir.join("b"))
assert len(conftest._path2confmods) == snap1 + 2
assert len(conftest._dirpath2confmods) == snap1 + 2
def test_value_access_not_existing(self, basedir):
conftest = ConftestWithSetinitial(basedir)

View File

@ -167,6 +167,7 @@ class TestPDB(object):
assert "= 1 failed in" in rest
assert "def test_1" not in rest
assert "Exit: Quitting debugger" in rest
assert "PDB continue (IO-capturing resumed)" not in rest
self.flush(child)
@staticmethod
@ -498,18 +499,39 @@ class TestPDB(object):
"""
)
child = testdir.spawn_pytest(str(p1))
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect("test_1")
child.expect("x = 3")
child.expect("Pdb")
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect("x = 4")
child.expect("Pdb")
child.sendeof()
child.expect("_ test_1 _")
child.expect("def test_1")
child.expect("Captured stdout call")
rest = child.read().decode("utf8")
assert "1 failed" in rest
assert "def test_1" in rest
assert "hello17" in rest # out is captured
assert "hello18" in rest # out is captured
assert "1 failed" in rest
self.flush(child)
def test_pdb_without_capture(self, testdir):
p1 = testdir.makepyfile(
"""
import pytest
def test_1():
pytest.set_trace()
"""
)
child = testdir.spawn_pytest("-s %s" % p1)
child.expect(r">>> PDB set_trace >>>")
child.expect("Pdb")
child.sendline("c")
child.expect(r">>> PDB continue >>>")
child.expect("1 passed")
self.flush(child)
def test_pdb_used_outside_test(self, testdir):
@ -550,15 +572,29 @@ class TestPDB(object):
["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF
)
def test_enter_pdb_hook_is_called(self, testdir):
def test_enter_leave_pdb_hooks_are_called(self, testdir):
testdir.makeconftest(
"""
def pytest_enter_pdb(config):
assert config.testing_verification == 'configured'
print('enter_pdb_hook')
mypdb = None
def pytest_configure(config):
config.testing_verification = 'configured'
def pytest_enter_pdb(config, pdb):
assert config.testing_verification == 'configured'
print('enter_pdb_hook')
global mypdb
mypdb = pdb
mypdb.set_attribute = "bar"
def pytest_leave_pdb(config, pdb):
assert config.testing_verification == 'configured'
print('leave_pdb_hook')
global mypdb
assert mypdb is pdb
assert mypdb.set_attribute == "bar"
"""
)
p1 = testdir.makepyfile(
@ -567,11 +603,17 @@ class TestPDB(object):
def test_foo():
pytest.set_trace()
assert 0
"""
)
child = testdir.spawn_pytest(str(p1))
child.expect("enter_pdb_hook")
child.send("c\n")
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
child.expect("Captured stdout call")
rest = child.read().decode("utf8")
assert "leave_pdb_hook" in rest
assert "1 failed" in rest
child.sendeof()
self.flush(child)

148
testing/test_stepwise.py Normal file
View File

@ -0,0 +1,148 @@
import pytest
@pytest.fixture
def stepwise_testdir(testdir):
# Rather than having to modify our testfile between tests, we introduce
# a flag for wether or not the second test should fail.
testdir.makeconftest(
"""
def pytest_addoption(parser):
group = parser.getgroup('general')
group.addoption('--fail', action='store_true', dest='fail')
group.addoption('--fail-last', action='store_true', dest='fail_last')
"""
)
# Create a simple test suite.
testdir.makepyfile(
test_a="""
def test_success_before_fail():
assert 1
def test_fail_on_flag(request):
assert not request.config.getvalue('fail')
def test_success_after_fail():
assert 1
def test_fail_last_on_flag(request):
assert not request.config.getvalue('fail_last')
def test_success_after_last_fail():
assert 1
"""
)
testdir.makepyfile(
test_b="""
def test_success():
assert 1
"""
)
return testdir
@pytest.fixture
def error_testdir(testdir):
testdir.makepyfile(
test_a="""
def test_error(nonexisting_fixture):
assert 1
def test_success_after_fail():
assert 1
"""
)
return testdir
@pytest.fixture
def broken_testdir(testdir):
testdir.makepyfile(
working_testfile="def test_proper(): assert 1", broken_testfile="foobar"
)
return testdir
def test_run_without_stepwise(stepwise_testdir):
result = stepwise_testdir.runpytest("-v", "--strict", "--fail")
result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"])
result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"])
result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"])
def test_fail_and_continue_with_stepwise(stepwise_testdir):
# Run the tests with a failing second test.
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "--fail")
assert not result.stderr.str()
stdout = result.stdout.str()
# Make sure we stop after first failing test.
assert "test_success_before_fail PASSED" in stdout
assert "test_fail_on_flag FAILED" in stdout
assert "test_success_after_fail" not in stdout
# "Fix" the test that failed in the last run and run it again.
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise")
assert not result.stderr.str()
stdout = result.stdout.str()
# Make sure the latest failing test runs and then continues.
assert "test_success_before_fail" not in stdout
assert "test_fail_on_flag PASSED" in stdout
assert "test_success_after_fail PASSED" in stdout
def test_run_with_skip_option(stepwise_testdir):
result = stepwise_testdir.runpytest(
"-v", "--strict", "--stepwise", "--stepwise-skip", "--fail", "--fail-last"
)
assert not result.stderr.str()
stdout = result.stdout.str()
# Make sure first fail is ignore and second fail stops the test run.
assert "test_fail_on_flag FAILED" in stdout
assert "test_success_after_fail PASSED" in stdout
assert "test_fail_last_on_flag FAILED" in stdout
assert "test_success_after_last_fail" not in stdout
def test_fail_on_errors(error_testdir):
result = error_testdir.runpytest("-v", "--strict", "--stepwise")
assert not result.stderr.str()
stdout = result.stdout.str()
assert "test_error ERROR" in stdout
assert "test_success_after_fail" not in stdout
def test_change_testfile(stepwise_testdir):
result = stepwise_testdir.runpytest(
"-v", "--strict", "--stepwise", "--fail", "test_a.py"
)
assert not result.stderr.str()
stdout = result.stdout.str()
assert "test_fail_on_flag FAILED" in stdout
# Make sure the second test run starts from the beginning, since the
# test to continue from does not exist in testfile_b.
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "test_b.py")
assert not result.stderr.str()
stdout = result.stdout.str()
assert "test_success PASSED" in stdout
def test_stop_on_collection_errors(broken_testdir):
result = broken_testdir.runpytest(
"-v", "--strict", "--stepwise", "working_testfile.py", "broken_testfile.py"
)
stdout = result.stdout.str()
assert "errors during collection" in stdout