From dc8c27037a7e6e6cd24dd5e3fd2ee6bc56db3594 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Feb 2019 18:35:38 +0100 Subject: [PATCH 01/95] AppVeyor: drop pluggymaster --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 773e16b1b..da4def8f1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,14 +6,13 @@ environment: - TOXENV: "py34-xdist" - TOXENV: "py35-xdist" - TOXENV: "py36-xdist" + # NOTE: pypy-xdist is buggy currently (https://github.com/pytest-dev/pytest-xdist/issues/142). - TOXENV: "pypy" PYTEST_NO_COVERAGE: "1" # Specialized factors for py27. - TOXENV: "py27-trial,py27-numpy,py27-nobyte" - - TOXENV: "py27-pluggymaster" # Specialized factors for py37. - TOXENV: "py37-trial,py37-numpy" - - TOXENV: "py37-pluggymaster" - TOXENV: "py37-freeze" PYTEST_NO_COVERAGE: "1" From ede6387caabea85e82849481e24c3b875acb04b3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 24 Feb 2019 12:11:08 -0300 Subject: [PATCH 02/95] Require funcsigs>=1.0 on Python 2.7 Fix #4815 --- changelog/4815.trivial.rst | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/4815.trivial.rst diff --git a/changelog/4815.trivial.rst b/changelog/4815.trivial.rst new file mode 100644 index 000000000..d7d91b899 --- /dev/null +++ b/changelog/4815.trivial.rst @@ -0,0 +1 @@ +``funcsigs>=1.0`` is now required for Python 2.7. diff --git a/setup.py b/setup.py index b286a4f20..9d3093914 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ INSTALL_REQUIRES = [ 'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"', 'more-itertools>=4.0.0;python_version>"2.7"', "atomicwrites>=1.0", - 'funcsigs;python_version<"3.0"', + 'funcsigs>=1.0;python_version<"3.0"', 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', ] From a0207274f4148c28222b4fdc4466d927a2551ed6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Feb 2019 20:59:10 -0200 Subject: [PATCH 03/95] -p option now can be used to early-load plugins by entry-point name Fixes #4718 --- changelog/4718.feature.rst | 6 ++++ changelog/4718.trivial.rst | 1 + doc/en/plugins.rst | 2 +- doc/en/usage.rst | 16 ++++++++++ setup.py | 2 +- src/_pytest/config/__init__.py | 26 ++++++++++------ src/_pytest/helpconfig.py | 2 +- testing/acceptance_test.py | 55 ++++++++++++++++++++++++++++++++++ testing/test_config.py | 25 +++++++++++++++- 9 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 changelog/4718.feature.rst create mode 100644 changelog/4718.trivial.rst diff --git a/changelog/4718.feature.rst b/changelog/4718.feature.rst new file mode 100644 index 000000000..35d5fffb9 --- /dev/null +++ b/changelog/4718.feature.rst @@ -0,0 +1,6 @@ +The ``-p`` option can now be used to early-load plugins also by entry-point name, instead of just +by module name. + +This makes it possible to early load external plugins like ``pytest-cov`` in the command-line:: + + pytest -p pytest_cov diff --git a/changelog/4718.trivial.rst b/changelog/4718.trivial.rst new file mode 100644 index 000000000..8b4e019bc --- /dev/null +++ b/changelog/4718.trivial.rst @@ -0,0 +1 @@ +``pluggy>=0.9`` is now required. diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index 7c7e1132d..e80969193 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -27,7 +27,7 @@ Here is a little annotated list for some popular plugins: for `twisted `_ apps, starting a reactor and processing deferreds from test functions. -* `pytest-cov `_: +* `pytest-cov `__: coverage reporting, compatible with distributed testing * `pytest-xdist `_: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 2efb63ae2..523608569 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -680,6 +680,22 @@ for example ``-x`` if you only want to send one particular failure. Currently only pasting to the http://bpaste.net service is implemented. +Early loading plugins +--------------------- + +You can early-load plugins (internal and external) explicitly in the command-line with the ``-p`` option:: + + pytest -p mypluginmodule + +The option receives a ``name`` parameter, which can be: + +* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable. +* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is + registered. For example to early-load the `pytest-cov `__ plugin you can use:: + + pytest -p pytest_cov + + Disabling plugins ----------------- diff --git a/setup.py b/setup.py index b286a4f20..d9d68c8ae 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ INSTALL_REQUIRES = [ # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ: - INSTALL_REQUIRES.append("pluggy>=0.7") + INSTALL_REQUIRES.append("pluggy>=0.9") def main(): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4258032b4..1da72032b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -497,7 +497,7 @@ class PytestPluginManager(PluginManager): if not name.startswith("pytest_"): self.set_blocked("pytest_" + name) else: - self.import_plugin(arg) + self.import_plugin(arg, consider_entry_points=True) def consider_conftest(self, conftestmodule): self.register(conftestmodule, name=conftestmodule.__file__) @@ -513,7 +513,11 @@ class PytestPluginManager(PluginManager): for import_spec in plugins: self.import_plugin(import_spec) - def import_plugin(self, modname): + def import_plugin(self, modname, consider_entry_points=False): + """ + Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point + names are also considered to find a plugin. + """ # most often modname refers to builtin modules, e.g. "pytester", # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the @@ -524,22 +528,26 @@ class PytestPluginManager(PluginManager): modname = str(modname) if self.is_blocked(modname) or self.get_plugin(modname) is not None: return - if modname in builtin_plugins: - importspec = "_pytest." + modname - else: - importspec = modname + + importspec = "_pytest." + modname if modname in builtin_plugins else modname self.rewrite_hook.mark_rewrite(importspec) + + if consider_entry_points: + loaded = self.load_setuptools_entrypoints("pytest11", name=modname) + if loaded: + return + try: __import__(importspec) except ImportError as e: - new_exc_type = ImportError new_exc_message = 'Error importing plugin "%s": %s' % ( modname, safe_str(e.args[0]), ) - new_exc = new_exc_type(new_exc_message) + new_exc = ImportError(new_exc_message) + tb = sys.exc_info()[2] - six.reraise(new_exc_type, new_exc, sys.exc_info()[2]) + six.reraise(ImportError, new_exc, tb) except Skipped as e: from _pytest.warnings import _issue_warning_captured diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index d5c4c043a..8117ee6bc 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -60,7 +60,7 @@ def pytest_addoption(parser): dest="plugins", default=[], metavar="name", - help="early-load given plugin (multi-allowed). " + help="early-load given plugin module name or entry point (multi-allowed). " "To avoid loading of plugins, use the `no:` prefix, e.g. " "`no:doctest`.", ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 59771185f..17111369b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -8,6 +8,7 @@ import sys import textwrap import types +import attr import py import six @@ -108,6 +109,60 @@ class TestGeneralUsage(object): assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) + @pytest.mark.parametrize("load_cov_early", [True, False]) + def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): + pkg_resources = pytest.importorskip("pkg_resources") + + testdir.makepyfile(mytestplugin1_module="") + testdir.makepyfile(mytestplugin2_module="") + testdir.makepyfile(mycov_module="") + testdir.syspathinsert() + + loaded = [] + + @attr.s + class DummyEntryPoint(object): + name = attr.ib() + module = attr.ib() + version = "1.0" + + @property + def project_name(self): + return self.name + + def load(self): + __import__(self.module) + loaded.append(self.name) + return sys.modules[self.module] + + @property + def dist(self): + return self + + def _get_metadata(self, *args): + return [] + + entry_points = [ + DummyEntryPoint("myplugin1", "mytestplugin1_module"), + DummyEntryPoint("myplugin2", "mytestplugin2_module"), + DummyEntryPoint("mycov", "mycov_module"), + ] + + def my_iter(group, name=None): + assert group == "pytest11" + for ep in entry_points: + if name is not None and ep.name != name: + continue + yield ep + + monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + params = ("-p", "mycov") if load_cov_early else () + testdir.runpytest_inprocess(*params) + if load_cov_early: + assert loaded == ["mycov", "myplugin1", "myplugin2"] + else: + assert loaded == ["myplugin1", "myplugin2", "mycov"] + def test_assertion_magic(self, testdir): p = testdir.makepyfile( """ diff --git a/testing/test_config.py b/testing/test_config.py index 1e29b83f1..c5c0ca939 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -5,6 +5,8 @@ from __future__ import print_function import sys import textwrap +import attr + import _pytest._code import pytest from _pytest.config import _iter_rewritable_modules @@ -622,7 +624,28 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): pkg_resources = pytest.importorskip("pkg_resources") def my_iter(group, name=None): - raise AssertionError("Should not be called") + assert group == "pytest11" + assert name == "mytestplugin" + return iter([DummyEntryPoint()]) + + @attr.s + class DummyEntryPoint(object): + name = "mytestplugin" + version = "1.0" + + @property + def project_name(self): + return self.name + + def load(self): + return sys.modules[self.name] + + @property + def dist(self): + return self + + def _get_metadata(self, *args): + return [] class PseudoPlugin(object): x = 42 From a868a9ac13a2ac4a021a09c926b2564dccdfc70f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Feb 2019 17:46:09 +0100 Subject: [PATCH 04/95] pdb: validate --pdbcls option --- src/_pytest/debugging.py | 27 ++++++++++++++++++++++----- testing/test_pdb.py | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6b401aa0b..d37728a15 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import argparse import pdb import sys from doctest import UnexpectedException @@ -12,6 +13,24 @@ from _pytest.config import hookimpl def pytest_addoption(parser): + def validate_usepdb_cls(value): + try: + modname, classname = value.split(":") + except ValueError: + raise argparse.ArgumentTypeError( + "{!r} is not in the format 'modname:classname'".format(value) + ) + + try: + __import__(modname) + pdb_cls = getattr(sys.modules[modname], classname) + except Exception as exc: + raise argparse.ArgumentTypeError( + "could not get pdb class for {!r}: {}".format(value, exc) + ) + + return pdb_cls + group = parser.getgroup("general") group._addoption( "--pdb", @@ -23,6 +42,7 @@ def pytest_addoption(parser): "--pdbcls", dest="usepdb_cls", metavar="modulename:classname", + type=validate_usepdb_cls, help="start a custom interactive Python debugger on errors. " "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", ) @@ -35,11 +55,8 @@ def pytest_addoption(parser): def pytest_configure(config): - if config.getvalue("usepdb_cls"): - modname, classname = config.getvalue("usepdb_cls").split(":") - __import__(modname) - pdb_cls = getattr(sys.modules[modname], classname) - else: + pdb_cls = config.getvalue("usepdb_cls") + if not pdb_cls: pdb_cls = pdb.Pdb if config.getvalue("trace"): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 43d640614..f0cef2788 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -688,6 +688,20 @@ class TestPDB(object): result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) assert custom_pdb_calls == ["init", "reset", "interaction"] + def test_pdb_custom_cls_invalid(self, testdir): + result = testdir.runpytest_inprocess("--pdbcls=invalid") + result.stderr.fnmatch_lines( + [ + "*: error: argument --pdbcls: 'invalid' is not in the format 'modname:classname'" + ] + ) + result = testdir.runpytest_inprocess("--pdbcls=pdb:DoesNotExist") + result.stderr.fnmatch_lines( + [ + "*: error: argument --pdbcls: could not get pdb class for 'pdb:DoesNotExist':*" + ] + ) + def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) result = testdir.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1) From 9cb71af9e57fda20b25834b7b4562605139705d7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 1 Mar 2019 13:50:14 +0100 Subject: [PATCH 05/95] _pytest.assertion.rewrite: move _format_explanation import --- src/_pytest/assertion/rewrite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 301bdedc5..19192c9d9 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -21,6 +21,9 @@ import six from _pytest._io.saferepr import saferepr from _pytest.assertion import util +from _pytest.assertion.util import ( # noqa: F401 + format_explanation as _format_explanation, +) from _pytest.compat import spec_from_file_location from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import PurePath @@ -483,9 +486,6 @@ def _saferepr(obj): return r.replace(u"\n", u"\\n") -from _pytest.assertion.util import format_explanation as _format_explanation # noqa - - def _format_assertmsg(obj): """Format the custom assertion message given. From f7a3e001f74a53411f0ee682eed04eef0a0bfc30 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Feb 2019 18:10:57 +0100 Subject: [PATCH 06/95] pdb: allow for --pdbclass=mod:attr.class --- changelog/4855.feature.rst | 4 ++++ src/_pytest/debugging.py | 39 ++++++++++++++++++++++---------------- testing/test_pdb.py | 17 +++++++++++------ 3 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 changelog/4855.feature.rst diff --git a/changelog/4855.feature.rst b/changelog/4855.feature.rst new file mode 100644 index 000000000..274d3991f --- /dev/null +++ b/changelog/4855.feature.rst @@ -0,0 +1,4 @@ +The ``--pdbcls`` option handles classes via module attributes now (e.g. +``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved. + +.. _pdb++: https://pypi.org/project/pdbpp/ diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index d37728a15..271a590a1 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -12,25 +12,32 @@ from _pytest import outcomes from _pytest.config import hookimpl -def pytest_addoption(parser): - def validate_usepdb_cls(value): - try: - modname, classname = value.split(":") - except ValueError: - raise argparse.ArgumentTypeError( - "{!r} is not in the format 'modname:classname'".format(value) - ) +def _validate_usepdb_cls(value): + try: + modname, classname = value.split(":") + except ValueError: + raise argparse.ArgumentTypeError( + "{!r} is not in the format 'modname:classname'".format(value) + ) - try: - __import__(modname) - pdb_cls = getattr(sys.modules[modname], classname) - except Exception as exc: - raise argparse.ArgumentTypeError( - "could not get pdb class for {!r}: {}".format(value, exc) - ) + try: + __import__(modname) + mod = sys.modules[modname] + + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) return pdb_cls + except Exception as exc: + raise argparse.ArgumentTypeError( + "could not get pdb class for {!r}: {}".format(value, exc) + ) + +def pytest_addoption(parser): group = parser.getgroup("general") group._addoption( "--pdb", @@ -42,7 +49,7 @@ def pytest_addoption(parser): "--pdbcls", dest="usepdb_cls", metavar="modulename:classname", - type=validate_usepdb_cls, + type=_validate_usepdb_cls, help="start a custom interactive Python debugger on errors. " "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", ) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index f0cef2788..f9a050b10 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,12 +2,14 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import argparse import os import platform import sys import _pytest._code import pytest +from _pytest.debugging import _validate_usepdb_cls try: breakpoint @@ -695,12 +697,15 @@ class TestPDB(object): "*: error: argument --pdbcls: 'invalid' is not in the format 'modname:classname'" ] ) - result = testdir.runpytest_inprocess("--pdbcls=pdb:DoesNotExist") - result.stderr.fnmatch_lines( - [ - "*: error: argument --pdbcls: could not get pdb class for 'pdb:DoesNotExist':*" - ] - ) + + def test_pdb_validate_usepdb_cls(self, testdir): + assert _validate_usepdb_cls("os.path:dirname.__name__") == "dirname" + + with pytest.raises( + argparse.ArgumentTypeError, + match=r"^could not get pdb class for 'pdb:DoesNotExist': .*'DoesNotExist'", + ): + _validate_usepdb_cls("pdb:DoesNotExist") def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) From db5cc35b44848cb8cc454b6d0c0fa216bf5cccc2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Feb 2019 13:23:08 +0100 Subject: [PATCH 07/95] pytester: unset PYTEST_ADDOPTS --- changelog/4851.bugfix.rst | 1 + src/_pytest/pytester.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/4851.bugfix.rst diff --git a/changelog/4851.bugfix.rst b/changelog/4851.bugfix.rst new file mode 100644 index 000000000..7b532af3e --- /dev/null +++ b/changelog/4851.bugfix.rst @@ -0,0 +1 @@ +pytester unsets ``PYTEST_ADDOPTS`` now to not use outer options with ``testdir.runpytest()``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index fae243a50..4aab1a3eb 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -509,6 +509,7 @@ class Testdir(object): self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot) os.environ.pop("TOX_ENV_DIR", None) # Ensure that it is not used for caching. + os.environ.pop("PYTEST_ADDOPTS", None) # Do not use outer options. self.plugins = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() From 2d2f6cd4fdc5b871d3137a5d0ac805ec8359f0c9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 1 Mar 2019 22:50:37 +0100 Subject: [PATCH 08/95] cacheprovider: _ensure_supporting_files: remove unused branches It is only called with empty/new dirs since 0385c273. --- src/_pytest/cacheprovider.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 87b2e5426..0c25914bb 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -132,24 +132,21 @@ class Cache(object): else: with f: json.dump(value, f, indent=2, sort_keys=True) + if not cache_dir_exists_already: self._ensure_supporting_files() def _ensure_supporting_files(self): """Create supporting files in the cache dir that are not really part of the cache.""" - if self._cachedir.is_dir(): - readme_path = self._cachedir / "README.md" - if not readme_path.is_file(): - readme_path.write_text(README_CONTENT) + readme_path = self._cachedir / "README.md" + readme_path.write_text(README_CONTENT) - gitignore_path = self._cachedir.joinpath(".gitignore") - if not gitignore_path.is_file(): - msg = u"# Created by pytest automatically.\n*" - gitignore_path.write_text(msg, encoding="UTF-8") + gitignore_path = self._cachedir.joinpath(".gitignore") + msg = u"# Created by pytest automatically.\n*" + gitignore_path.write_text(msg, encoding="UTF-8") - cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") - if not cachedir_tag_path.is_file(): - cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) + cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") + cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) class LFPlugin(object): From 7dceabfcb2a8d178761bd862d37e01cbcc23f6d1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 27 Feb 2019 21:10:37 -0300 Subject: [PATCH 09/95] Ensure fixtures obtained with getfixturevalue() are finalized in the correct order Fix #1895 --- src/_pytest/fixtures.py | 23 ++++++++++++++++++----- src/_pytest/runner.py | 1 + testing/python/fixture.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b2ad9aae3..fa45dffc1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -585,11 +585,13 @@ class FixtureRequest(FuncargnamesCompatAttr): # call the fixture function fixturedef.execute(request=subrequest) finally: - # if fixture function failed it might have registered finalizers - self.session._setupstate.addfinalizer( - functools.partial(fixturedef.finish, request=subrequest), - subrequest.node, - ) + self._schedule_finalizers(fixturedef, subrequest) + + def _schedule_finalizers(self, fixturedef, subrequest): + # if fixture function failed it might have registered finalizers + self.session._setupstate.addfinalizer( + functools.partial(fixturedef.finish, request=subrequest), subrequest.node + ) def _check_scope(self, argname, invoking_scope, requested_scope): if argname == "request": @@ -659,6 +661,16 @@ class SubRequest(FixtureRequest): def addfinalizer(self, finalizer): self._fixturedef.addfinalizer(finalizer) + def _schedule_finalizers(self, fixturedef, subrequest): + # if the executing fixturedef was not explicitly requested in the argument list (via + # getfixturevalue inside the fixture call) then ensure this fixture def will be finished + # first + if fixturedef.argname not in self.funcargnames: + fixturedef.addfinalizer( + functools.partial(self._fixturedef.finish, request=self) + ) + super(SubRequest, self)._schedule_finalizers(fixturedef, subrequest) + scopes = "session package module class function".split() scopenum_function = scopes.index("function") @@ -858,6 +870,7 @@ class FixtureDef(object): def execute(self, request): # get required arguments and register our own finish() # with their finalization + # TODO CHECK HOW TO AVOID EXPLICITLY FINALIZING AGAINST ARGNAMES for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 8357991fe..55dcd8054 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -327,6 +327,7 @@ class SetupState(object): assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ self._finalizers.setdefault(colitem, []).append(finalizer) + pass def _pop_and_teardown(self): colitem = self.stack.pop() diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 3d557cec8..d9a512d70 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1866,7 +1866,7 @@ class TestAutouseManagement(object): "setup-2", "step1-2", "step2-2", "teardown-2",] """ ) - reprec = testdir.inline_run("-s") + reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=5) def test_ordering_autouse_before_explicit(self, testdir): From 525639eaa00aed6d545244b63b10f9f8facce737 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 28 Feb 2019 20:28:54 -0300 Subject: [PATCH 10/95] Rename fixtures testing file to be consistent with the module name --- testing/python/{fixture.py => fixtures.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename testing/python/{fixture.py => fixtures.py} (100%) diff --git a/testing/python/fixture.py b/testing/python/fixtures.py similarity index 100% rename from testing/python/fixture.py rename to testing/python/fixtures.py From d97473e551ef70831c601f9c46f55ef030bb646f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 28 Feb 2019 20:59:37 -0300 Subject: [PATCH 11/95] Add test and CHANGELOG for #1895 --- changelog/1895.bugfix.rst | 2 ++ testing/python/fixtures.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 changelog/1895.bugfix.rst diff --git a/changelog/1895.bugfix.rst b/changelog/1895.bugfix.rst new file mode 100644 index 000000000..44b921ad9 --- /dev/null +++ b/changelog/1895.bugfix.rst @@ -0,0 +1,2 @@ +Fix bug where fixtures requested dynamically via ``request.getfixturevalue()`` might be teardown +before the requesting fixture. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d9a512d70..45c7a17ae 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -562,6 +562,44 @@ class TestRequestBasic(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_getfixturevalue_teardown(self, testdir): + """ + Issue #1895 + + `test_inner` requests `inner` fixture, which in turns requests `resource` + using getfixturevalue. `test_func` then requests `resource`. + + `resource` is teardown before `inner` because the fixture mechanism won't consider + `inner` dependent on `resource` when it is get via `getfixturevalue`: `test_func` + will then cause the `resource`'s finalizer to be called first because of this. + """ + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(scope='session') + def resource(): + r = ['value'] + yield r + r.pop() + + @pytest.fixture(scope='session') + def inner(request): + resource = request.getfixturevalue('resource') + assert resource == ['value'] + yield + assert resource == ['value'] + + def test_inner(inner): + pass + + def test_func(resource): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* 2 passed in *") + @pytest.mark.parametrize("getfixmethod", ("getfixturevalue", "getfuncargvalue")) def test_getfixturevalue(self, testdir, getfixmethod): item = testdir.getitem( From 6a2d122a5069ca339f4f5d5faf8759162e2fbabc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2019 09:56:15 -0300 Subject: [PATCH 12/95] Remove code debugging leftovers --- src/_pytest/fixtures.py | 1 - src/_pytest/runner.py | 1 - testing/python/fixtures.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fa45dffc1..2635d095e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -870,7 +870,6 @@ class FixtureDef(object): def execute(self, request): # get required arguments and register our own finish() # with their finalization - # TODO CHECK HOW TO AVOID EXPLICITLY FINALIZING AGAINST ARGNAMES for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 55dcd8054..8357991fe 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -327,7 +327,6 @@ class SetupState(object): assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ self._finalizers.setdefault(colitem, []).append(finalizer) - pass def _pop_and_teardown(self): colitem = self.stack.pop() diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 45c7a17ae..7b328c5b2 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1904,7 +1904,7 @@ class TestAutouseManagement(object): "setup-2", "step1-2", "step2-2", "teardown-2",] """ ) - reprec = testdir.inline_run("-v") + reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=5) def test_ordering_autouse_before_explicit(self, testdir): From 53b8aa065c5c05438918eca524fcbabb19b1a269 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2019 11:17:43 -0300 Subject: [PATCH 13/95] Show testpaths option in the header if it has been used for collection Fix #4875 --- changelog/4875.feature.rst | 3 +++ src/_pytest/terminal.py | 7 ++++++- testing/test_terminal.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 changelog/4875.feature.rst diff --git a/changelog/4875.feature.rst b/changelog/4875.feature.rst new file mode 100644 index 000000000..6d91fb465 --- /dev/null +++ b/changelog/4875.feature.rst @@ -0,0 +1,3 @@ +The `testpaths `__ configuration option is now displayed next +to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were +not explicitly passed in the command line. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index eda0c0905..fc78f4de4 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -586,8 +586,13 @@ class TerminalReporter(object): inifile = "" if config.inifile: inifile = " " + config.rootdir.bestrelpath(config.inifile) - lines = ["rootdir: %s, inifile:%s" % (config.rootdir, inifile)] + line = "rootdir: %s, inifile:%s" % (config.rootdir, inifile) + testpaths = config.getini("testpaths") + if testpaths and config.args == testpaths: + rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths] + line += ", testpaths: {}".format(", ".join(rel_paths)) + lines = [line] plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 798e8c16a..abe9549e2 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -567,6 +567,26 @@ class TestTerminalFunctional(object): if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.fnmatch_lines(["plugins: *"]) + def test_header(self, testdir, request): + testdir.tmpdir.join("tests").ensure_dir() + testdir.tmpdir.join("gui").ensure_dir() + result = testdir.runpytest() + result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile:"]) + + testdir.makeini( + """ + [pytest] + testpaths = tests gui + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + ["rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui"] + ) + + result = testdir.runpytest("tests") + result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + def test_showlocals(self, testdir): p1 = testdir.makepyfile( """ From 0deb7b1696bdc97f4d6dcc132bf665f794204448 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 Mar 2019 11:31:09 -0300 Subject: [PATCH 14/95] Do not show "inifile:" string if there's no configuration file --- changelog/4875.feature.rst | 2 ++ src/_pytest/terminal.py | 16 ++++++++-------- testing/test_config.py | 6 +++--- testing/test_session.py | 2 +- testing/test_terminal.py | 22 +++++++++++----------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/changelog/4875.feature.rst b/changelog/4875.feature.rst index 6d91fb465..d9fb65ca5 100644 --- a/changelog/4875.feature.rst +++ b/changelog/4875.feature.rst @@ -1,3 +1,5 @@ The `testpaths `__ configuration option is now displayed next to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were not explicitly passed in the command line. + +Also, ``inifile`` is only displayed if there's a configuration file, instead of an empty ``inifile:`` string. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index fc78f4de4..0868bd751 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -583,21 +583,21 @@ class TerminalReporter(object): self.write_line(line) def pytest_report_header(self, config): - inifile = "" - if config.inifile: - inifile = " " + config.rootdir.bestrelpath(config.inifile) + line = "rootdir: %s" % config.rootdir + + if config.inifile: + line += ", inifile: " + config.rootdir.bestrelpath(config.inifile) - line = "rootdir: %s, inifile:%s" % (config.rootdir, inifile) testpaths = config.getini("testpaths") if testpaths and config.args == testpaths: rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths] line += ", testpaths: {}".format(", ".join(rel_paths)) - lines = [line] + result = [line] + plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: - - lines.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) - return lines + result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) + return result def pytest_collection_finish(self, session): if self.config.getoption("collectonly"): diff --git a/testing/test_config.py b/testing/test_config.py index c5c0ca939..3dd707bfa 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -697,9 +697,9 @@ def test_invalid_options_show_extra_information(testdir): ["-v", "dir2", "dir1"], ], ) -def test_consider_args_after_options_for_rootdir_and_inifile(testdir, args): +def test_consider_args_after_options_for_rootdir(testdir, args): """ - Consider all arguments in the command-line for rootdir and inifile + Consider all arguments in the command-line for rootdir discovery, even if they happen to occur after an option. #949 """ # replace "dir1" and "dir2" from "args" into their real directory @@ -713,7 +713,7 @@ def test_consider_args_after_options_for_rootdir_and_inifile(testdir, args): args[i] = d2 with root.as_cwd(): result = testdir.runpytest(*args) - result.stdout.fnmatch_lines(["*rootdir: *myroot, inifile:"]) + result.stdout.fnmatch_lines(["*rootdir: *myroot"]) @pytest.mark.skipif("sys.platform == 'win32'") diff --git a/testing/test_session.py b/testing/test_session.py index 6b185f76b..e5eb081d4 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -332,7 +332,7 @@ def test_rootdir_option_arg(testdir, monkeypatch, path): result = testdir.runpytest("--rootdir={}".format(path)) result.stdout.fnmatch_lines( [ - "*rootdir: {}/root, inifile:*".format(testdir.tmpdir), + "*rootdir: {}/root".format(testdir.tmpdir), "root/test_rootdir_option_arg.py *", "*1 passed*", ] diff --git a/testing/test_terminal.py b/testing/test_terminal.py index abe9549e2..fe67a6aec 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -570,9 +570,17 @@ class TestTerminalFunctional(object): def test_header(self, testdir, request): testdir.tmpdir.join("tests").ensure_dir() testdir.tmpdir.join("gui").ensure_dir() - result = testdir.runpytest() - result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile:"]) + # no ini file + result = testdir.runpytest() + result.stdout.fnmatch_lines(["rootdir: *test_header0"]) + + # with inifile + testdir.makeini("""[pytest]""") + result = testdir.runpytest() + result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + + # with testpaths option, and not passing anything in the command-line testdir.makeini( """ [pytest] @@ -584,6 +592,7 @@ class TestTerminalFunctional(object): ["rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui"] ) + # with testpaths option, passing directory in command-line: do not show testpaths then result = testdir.runpytest("tests") result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) @@ -1219,15 +1228,6 @@ def test_summary_stats(exp_line, exp_color, stats_arg): assert color == exp_color -def test_no_trailing_whitespace_after_inifile_word(testdir): - result = testdir.runpytest("") - assert "inifile:\n" in result.stdout.str() - - testdir.makeini("[pytest]") - result = testdir.runpytest("") - assert "inifile: tox.ini\n" in result.stdout.str() - - class TestClassicOutputStyle(object): """Ensure classic output style works as expected (#3883)""" From c334adc78ff54a7fef992b62e2fc425d924bc8a1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 3 Mar 2019 11:20:00 -0300 Subject: [PATCH 15/95] Apply suggestions from code review Co-Authored-By: nicoddemus --- testing/python/fixtures.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 7b328c5b2..1ea37f85c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -566,11 +566,11 @@ class TestRequestBasic(object): """ Issue #1895 - `test_inner` requests `inner` fixture, which in turns requests `resource` - using getfixturevalue. `test_func` then requests `resource`. + `test_inner` requests `inner` fixture, which in turn requests `resource` + using `getfixturevalue`. `test_func` then requests `resource`. `resource` is teardown before `inner` because the fixture mechanism won't consider - `inner` dependent on `resource` when it is get via `getfixturevalue`: `test_func` + `inner` dependent on `resource` when it is used via `getfixturevalue`: `test_func` will then cause the `resource`'s finalizer to be called first because of this. """ testdir.makepyfile( From c86d2daf81e084981f62e76728df0198fbda4261 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Feb 2019 12:14:31 +0100 Subject: [PATCH 16/95] pytester: remove unused anypython fixture This became unused after ab9f6a75 (in 2009). --- changelog/4890.trivial.rst | 1 + src/_pytest/pytester.py | 42 -------------------------------------- 2 files changed, 1 insertion(+), 42 deletions(-) create mode 100644 changelog/4890.trivial.rst diff --git a/changelog/4890.trivial.rst b/changelog/4890.trivial.rst new file mode 100644 index 000000000..a3a08bc11 --- /dev/null +++ b/changelog/4890.trivial.rst @@ -0,0 +1 @@ +Remove internally unused ``anypython`` fixture from the pytester plugin. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4aab1a3eb..d667452d1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -4,7 +4,6 @@ from __future__ import division from __future__ import print_function import codecs -import distutils.spawn import gc import os import platform @@ -151,47 +150,6 @@ winpymap = { } -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = distutils.spawn.find_executable(name) - if executable: - import subprocess - - popen = subprocess.Popen( - [str(executable), "--version"], - universal_newlines=True, - stderr=subprocess.PIPE, - ) - out, err = popen.communicate() - if name == "jython": - if not err or "2.5" not in err: - executable = None - if "2.5.2" in err: - executable = None # http://bugs.jython.org/issue1790 - elif popen.returncode != 0: - # handle pyenv's 127 - executable = None - cache[name] = executable - return executable - - -@pytest.fixture(params=["python2.7", "python3.4", "pypy", "pypy3"]) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - pytest.skip("no suitable %s found" % (name,)) - return executable - - # used at least by pytest-xdist plugin From 47bd1688ed0da460fc6b1885e82bb9aa5bd1c3e0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Feb 2019 19:37:27 -0300 Subject: [PATCH 17/95] Remove dead-code related to yield tests Just noticed some code that no longer is needed when we removed yield-tests --- changelog/4829.trivial.rst | 1 + src/_pytest/debugging.py | 17 ++++++----------- src/_pytest/python.py | 31 +++++++++---------------------- 3 files changed, 16 insertions(+), 33 deletions(-) create mode 100644 changelog/4829.trivial.rst diff --git a/changelog/4829.trivial.rst b/changelog/4829.trivial.rst new file mode 100644 index 000000000..a1935b462 --- /dev/null +++ b/changelog/4829.trivial.rst @@ -0,0 +1 @@ +Some left-over internal code related to ``yield`` tests has been removed. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 271a590a1..6dbc0499a 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -209,17 +209,12 @@ def _test_pytest_function(pyfuncitem): _pdb = pytestPDB._init_pdb() testfunction = pyfuncitem.obj pyfuncitem.obj = _pdb.runcall - if pyfuncitem._isyieldedfunction(): - arg_list = list(pyfuncitem._args) - arg_list.insert(0, testfunction) - pyfuncitem._args = tuple(arg_list) - else: - if "func" in pyfuncitem._fixtureinfo.argnames: - raise ValueError("--trace can't be used with a fixture named func!") - pyfuncitem.funcargs["func"] = testfunction - new_list = list(pyfuncitem._fixtureinfo.argnames) - new_list.append("func") - pyfuncitem._fixtureinfo.argnames = tuple(new_list) + if "func" in pyfuncitem._fixtureinfo.argnames: + raise ValueError("--trace can't be used with a fixture named func!") + pyfuncitem.funcargs["func"] = testfunction + new_list = list(pyfuncitem._fixtureinfo.argnames) + new_list.append("func") + pyfuncitem._fixtureinfo.argnames = tuple(new_list) def _enter_pdb(node, excinfo, rep): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 215015d27..537c42d0d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -156,14 +156,9 @@ def pytest_configure(config): @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj - if pyfuncitem._isyieldedfunction(): - testfunction(*pyfuncitem._args) - else: - funcargs = pyfuncitem.funcargs - testargs = {} - for arg in pyfuncitem._fixtureinfo.argnames: - testargs[arg] = funcargs[arg] - testfunction(**testargs) + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + testfunction(**testargs) return True @@ -1405,7 +1400,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): if fixtureinfo is None: fixtureinfo = self.session._fixturemanager.getfixtureinfo( - self, self.obj, self.cls, funcargs=not self._isyieldedfunction() + self, self.obj, self.cls, funcargs=True ) self._fixtureinfo = fixtureinfo self.fixturenames = fixtureinfo.names_closure @@ -1419,16 +1414,11 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): def _initrequest(self): self.funcargs = {} - if self._isyieldedfunction(): - assert not hasattr( - self, "callspec" - ), "yielded functions (deprecated) cannot have funcargs" - else: - if hasattr(self, "callspec"): - callspec = self.callspec - assert not callspec.funcargs - if hasattr(callspec, "param"): - self.param = callspec.param + if hasattr(self, "callspec"): + callspec = self.callspec + assert not callspec.funcargs + if hasattr(callspec, "param"): + self.param = callspec.param self._request = fixtures.FixtureRequest(self) @property @@ -1448,9 +1438,6 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): "(compatonly) for code expecting pytest-2.2 style request objects" return self - def _isyieldedfunction(self): - return getattr(self, "_args", None) is not None - def runtest(self): """ execute the underlying test function. """ self.ihook.pytest_pyfunc_call(pyfuncitem=self) From 148e6a30c82fd707e00fdd3d169fa6f4b0cf42dd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Mar 2019 19:01:27 -0300 Subject: [PATCH 18/95] Improve coverage --- src/_pytest/debugging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6dbc0499a..6d51ec59c 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -209,7 +209,7 @@ def _test_pytest_function(pyfuncitem): _pdb = pytestPDB._init_pdb() testfunction = pyfuncitem.obj pyfuncitem.obj = _pdb.runcall - if "func" in pyfuncitem._fixtureinfo.argnames: + if "func" in pyfuncitem._fixtureinfo.argnames: # noqa raise ValueError("--trace can't be used with a fixture named func!") pyfuncitem.funcargs["func"] = testfunction new_list = list(pyfuncitem._fixtureinfo.argnames) From b7ae7a654b672ef3d6f496385c7dd6884e077ea7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Mar 2019 19:08:34 -0300 Subject: [PATCH 19/95] Remove callspec related block of code It seems this is no longer required now that we don't support yield tests anymore. The param attribute was added here: https://github.com/pytest-dev/pytest/blob/91b6f2bda8668e0f74190ed223a9eec01d09a0a7/_pytest/python.py#L888-L891 --- src/_pytest/python.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 537c42d0d..e41acfecc 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1414,11 +1414,6 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): def _initrequest(self): self.funcargs = {} - if hasattr(self, "callspec"): - callspec = self.callspec - assert not callspec.funcargs - if hasattr(callspec, "param"): - self.param = callspec.param self._request = fixtures.FixtureRequest(self) @property From 4d21dc4f2dec1098ce675116d5967cbb0cd78e50 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Mar 2019 22:46:34 +0100 Subject: [PATCH 20/95] Optimize TracebackEntry.ishidden The expected behavior is that there is no "__tracebackhide__" attribute, so use `getattr` instead of multiple try/except. --- src/_pytest/_code/code.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index a38af989a..1105310cf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -241,25 +241,20 @@ class TracebackEntry(object): def ishidden(self): """ return True if the current frame has a var __tracebackhide__ - resolving to True + resolving to True. If __tracebackhide__ is a callable, it gets called with the ExceptionInfo instance and can decide whether to hide the traceback. mostly for internal use """ - try: - tbh = self.frame.f_locals["__tracebackhide__"] - except KeyError: - try: - tbh = self.frame.f_globals["__tracebackhide__"] - except KeyError: - return False - - if callable(tbh): + f = self.frame + tbh = f.f_locals.get( + "__tracebackhide__", f.f_globals.get("__tracebackhide__", False) + ) + if tbh and callable(tbh): return tbh(None if self._excinfo is None else self._excinfo()) - else: - return tbh + return tbh def __str__(self): try: From 520af9d76784b364e8f442f792bd9589861bc851 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 14 Mar 2019 19:19:17 +0100 Subject: [PATCH 21/95] pdb: post_mortem: use super() This is good practice in general, and I've seen it cause problems (MRO) with pdb++. --- src/_pytest/debugging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6d51ec59c..5b780d101 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -263,9 +263,9 @@ def _find_last_non_hidden_frame(stack): def post_mortem(t): - class Pdb(pytestPDB._pdb_cls): + class Pdb(pytestPDB._pdb_cls, object): def get_stack(self, f, t): - stack, i = pdb.Pdb.get_stack(self, f, t) + stack, i = super(Pdb, self).get_stack(f, t) if f is None: i = _find_last_non_hidden_frame(stack) return stack, i From 40072b951151ad5706c06c9d588cad087954a576 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Feb 2019 20:06:26 -0300 Subject: [PATCH 22/95] Emit a warning when a async def function is not handled by a plugin Fix #2224 --- changelog/2224.feature.rst | 4 ++++ src/_pytest/python.py | 10 ++++++++++ testing/acceptance_test.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 changelog/2224.feature.rst diff --git a/changelog/2224.feature.rst b/changelog/2224.feature.rst new file mode 100644 index 000000000..6f0df93ae --- /dev/null +++ b/changelog/2224.feature.rst @@ -0,0 +1,4 @@ +``async`` test functions are skipped and a warning is emitted when a suitable +async plugin is not installed (such as ``pytest-asyncio`` or ``pytest-trio``). + +Previously ``async`` functions would not execute at all but still be marked as "passed". diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e41acfecc..d63c2bc29 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -43,6 +43,7 @@ from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail +from _pytest.outcomes import skip from _pytest.pathlib import parts from _pytest.warning_types import PytestWarning @@ -156,6 +157,15 @@ def pytest_configure(config): @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj + iscoroutinefunction = getattr(inspect, "iscoroutinefunction", None) + if iscoroutinefunction is not None and iscoroutinefunction(testfunction): + msg = "Coroutine functions are not natively supported and have been skipped.\n" + msg += "You need to install a suitable plugin for your async framework, for example:\n" + msg += " - pytest-asyncio\n" + msg += " - pytest-trio\n" + msg += " - pytest-tornasync" + warnings.warn(PytestWarning(msg.format(pyfuncitem.nodeid))) + skip(msg="coroutine function and no async plugin installed (see warnings)") funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} testfunction(**testargs) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 17111369b..39bd2fd77 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1179,3 +1179,31 @@ def test_fixture_mock_integration(testdir): def test_usage_error_code(testdir): result = testdir.runpytest("-unknown-option-") assert result.ret == EXIT_USAGEERROR + + +@pytest.mark.skipif( + sys.version_info[:2] < (3, 5), reason="async def syntax python 3.5+ only" +) +@pytest.mark.filterwarnings("default") +def test_warn_on_async_function(testdir): + testdir.makepyfile( + test_async=""" + async def test_1(): + pass + async def test_2(): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "test_async.py::test_1", + "test_async.py::test_2", + "*Coroutine functions are not natively supported*", + "*2 skipped, 2 warnings in*", + ] + ) + # ensure our warning message appears only once + assert ( + result.stdout.str().count("Coroutine functions are not natively supported") == 1 + ) From 43aee15ba31f32781c3e91333db3e4dd65aaaab3 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 15 Mar 2019 10:16:11 +0900 Subject: [PATCH 23/95] Make pytest.skip work in doctest --- src/_pytest/doctest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index d34cb638c..34b6c9d8c 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -15,6 +15,7 @@ from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest +from _pytest.outcomes import Skipped DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -153,6 +154,8 @@ def _init_runner_class(): raise failure def report_unexpected_exception(self, out, test, example, exc_info): + if isinstance(exc_info[1], Skipped): + raise exc_info[1] failure = doctest.UnexpectedException(test, example, exc_info) if self.continue_on_failure: out.append(failure) From fa3cca51e15b373d9bac567bca1fb941e96302f8 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 15 Mar 2019 11:06:57 +0900 Subject: [PATCH 24/95] Test pytest.skip in doctest --- testing/test_doctest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index e7b6b060f..09f7c331d 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -188,6 +188,18 @@ class TestDoctests(object): ] ) + def test_doctest_skip(self, testdir): + testdir.maketxtfile( + """ + >>> 1 + 1 + >>> import pytest + >>> pytest.skip("") + """ + ) + result = testdir.runpytest("--doctest-modules") + result.stdout.fnmatch_lines(["*1 skipped*"]) + def test_docstring_partial_context_around_error(self, testdir): """Test that we show some context before the actual line of a failing doctest. From 62f96eea6b769930cc3a2297a72857a920d41184 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 15 Mar 2019 11:14:50 +0900 Subject: [PATCH 25/95] Include documentation --- AUTHORS | 1 + changelog/4911.feature.rst | 1 + src/_pytest/outcomes.py | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog/4911.feature.rst diff --git a/AUTHORS b/AUTHORS index 85fe6aff0..d6f6ce828 100644 --- a/AUTHORS +++ b/AUTHORS @@ -222,6 +222,7 @@ Steffen Allner Stephan Obermann Sven-Hendrik Haase Tadek Teleżyński +Takafumi Arakaki Tarcisio Fischer Tareq Alayan Ted Xiao diff --git a/changelog/4911.feature.rst b/changelog/4911.feature.rst new file mode 100644 index 000000000..5106e6fb9 --- /dev/null +++ b/changelog/4911.feature.rst @@ -0,0 +1 @@ +Doctest can be now skipped dynamically with `pytest.skip`. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 14c6e9ab6..b08dbc7b3 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -80,7 +80,8 @@ def skip(msg="", **kwargs): Skip an executing test with the given message. This function should be called only during testing (setup, call or teardown) or - during collection by using the ``allow_module_level`` flag. + during collection by using the ``allow_module_level`` flag. This function can + be called in doctest as well. :kwarg bool allow_module_level: allows this function to be called at module level, skipping the rest of the module. Default to False. @@ -89,6 +90,9 @@ def skip(msg="", **kwargs): It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. + Similarly, use ``# doctest: +SKIP`` directive (see `doctest.SKIP + `_) + to skip a doctest statically. """ __tracebackhide__ = True allow_module_level = kwargs.pop("allow_module_level", False) From 57be1d60ddd856f16bd98fdf4650d5b34c19bdb8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 11:29:16 +0900 Subject: [PATCH 26/95] Apply suggestions from code review Co-Authored-By: tkf --- changelog/4911.feature.rst | 2 +- src/_pytest/outcomes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/4911.feature.rst b/changelog/4911.feature.rst index 5106e6fb9..5aef92d76 100644 --- a/changelog/4911.feature.rst +++ b/changelog/4911.feature.rst @@ -1 +1 @@ -Doctest can be now skipped dynamically with `pytest.skip`. +Doctests can be skipped now dynamically using ``pytest.skip()``. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index b08dbc7b3..06532dc62 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -81,7 +81,7 @@ def skip(msg="", **kwargs): This function should be called only during testing (setup, call or teardown) or during collection by using the ``allow_module_level`` flag. This function can - be called in doctest as well. + be called in doctests as well. :kwarg bool allow_module_level: allows this function to be called at module level, skipping the rest of the module. Default to False. From 95701566f3de4766a3cc50fd3028b047b4201a55 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 12:21:48 +0900 Subject: [PATCH 27/95] Update src/_pytest/outcomes.py Co-Authored-By: tkf --- src/_pytest/outcomes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 06532dc62..6c2dfb5ae 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -90,7 +90,7 @@ def skip(msg="", **kwargs): It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. - Similarly, use ``# doctest: +SKIP`` directive (see `doctest.SKIP + Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP `_) to skip a doctest statically. """ From 5e27ea552867830d44d436cfc54b1292da969e4a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Mar 2019 04:14:07 +0100 Subject: [PATCH 28/95] pytester: LineMatcher: assert Sequence when matching in order This can be helpful when passing a set accidentally. --- changelog/4931.feature.rst | 1 + src/_pytest/pytester.py | 2 ++ testing/test_pytester.py | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 changelog/4931.feature.rst diff --git a/changelog/4931.feature.rst b/changelog/4931.feature.rst new file mode 100644 index 000000000..bfc35a4b7 --- /dev/null +++ b/changelog/4931.feature.rst @@ -0,0 +1 @@ +pytester's ``LineMatcher`` asserts that the passed lines are a sequence. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 810041de7..d5faed418 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -25,6 +25,7 @@ from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.capture import MultiCapture from _pytest.capture import SysCapture from _pytest.compat import safe_str +from _pytest.compat import Sequence from _pytest.main import EXIT_INTERRUPTED from _pytest.main import EXIT_OK from _pytest.main import Session @@ -1325,6 +1326,7 @@ class LineMatcher(object): will be logged to stdout when a match occurs """ + assert isinstance(lines2, Sequence) lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 675108460..08167ec90 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -17,6 +17,7 @@ from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED from _pytest.pytester import CwdSnapshot from _pytest.pytester import HookRecorder +from _pytest.pytester import LineMatcher from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot @@ -453,3 +454,18 @@ def test_testdir_run_timeout_expires(testdir): ) with pytest.raises(testdir.TimeoutExpired): testdir.runpytest_subprocess(testfile, timeout=1) + + +def test_linematcher_with_nonlist(): + """Test LineMatcher with regard to passing in a set (accidentally).""" + lm = LineMatcher([]) + + with pytest.raises(AssertionError): + lm.fnmatch_lines(set()) + with pytest.raises(AssertionError): + lm.fnmatch_lines({}) + lm.fnmatch_lines([]) + lm.fnmatch_lines(()) + + assert lm._getlines({}) == {} + assert lm._getlines(set()) == set() From 15fe8c6e9020370b07e4c6d4b0c95e4993b0c511 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 15:29:27 +0100 Subject: [PATCH 29/95] Handle `-p plug` after `-p no:plug`. This can be used to override a blocked plugin (e.g. in "addopts") from the command line etc. --- changelog/4936.feature.rst | 4 ++++ src/_pytest/config/__init__.py | 8 ++++++++ testing/test_pluginmanager.py | 7 +++++++ 3 files changed, 19 insertions(+) create mode 100644 changelog/4936.feature.rst diff --git a/changelog/4936.feature.rst b/changelog/4936.feature.rst new file mode 100644 index 000000000..744af1297 --- /dev/null +++ b/changelog/4936.feature.rst @@ -0,0 +1,4 @@ +Handle ``-p plug`` after ``-p no:plug``. + +This can be used to override a blocked plugin (e.g. in "addopts") from the +command line etc. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 22eeaa33d..c44392073 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -496,6 +496,14 @@ class PytestPluginManager(PluginManager): if not name.startswith("pytest_"): self.set_blocked("pytest_" + name) else: + name = arg + # Unblock the plugin. None indicates that it has been blocked. + # There is no interface with pluggy for this. + if self._name2plugin.get(name, -1) is None: + del self._name2plugin[name] + if not name.startswith("pytest_"): + if self._name2plugin.get("pytest_" + name, -1) is None: + del self._name2plugin["pytest_" + name] self.import_plugin(arg, consider_entry_points=True) def consider_conftest(self, conftestmodule): diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 80817932e..4e0c0ed1e 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -346,3 +346,10 @@ class TestPytestPluginManagerBootstrapming(object): l2 = pytestpm.get_plugins() assert 42 not in l2 assert 43 not in l2 + + def test_blocked_plugin_can_be_used(self, pytestpm): + pytestpm.consider_preparse(["xyz", "-p", "no:abc", "-p", "abc"]) + + assert pytestpm.has_plugin("abc") + assert not pytestpm.is_blocked("abc") + assert not pytestpm.is_blocked("pytest_abc") From c75dd1067195753c47e0125ea436b60b1b0d68e7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 01:17:32 +0100 Subject: [PATCH 30/95] pytester: testdir: set $HOME to tmpdir This avoids loading user configuration, which might interfere with test results, e.g. a `~/.pdbrc.py` with pdb++. Also sets USERPROFILE, which will be required with Python 3.8 [1]. 1: https://bugs.python.org/issue36264 --- changelog/4941.feature.rst | 3 +++ src/_pytest/pytester.py | 2 ++ testing/test_junitxml.py | 6 +----- testing/test_pdb.py | 3 --- 4 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 changelog/4941.feature.rst diff --git a/changelog/4941.feature.rst b/changelog/4941.feature.rst new file mode 100644 index 000000000..846b43f45 --- /dev/null +++ b/changelog/4941.feature.rst @@ -0,0 +1,3 @@ +``pytester``'s ``Testdir`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory. + +This ensures to not load configuration files from the real user's home directory. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d5faed418..8f1a7d1d5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -469,6 +469,8 @@ class Testdir(object): os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot) os.environ.pop("TOX_ENV_DIR", None) # Ensure that it is not used for caching. os.environ.pop("PYTEST_ADDOPTS", None) # Do not use outer options. + os.environ["HOME"] = str(self.tmpdir) # Do not load user config. + os.environ["USERPROFILE"] = os.environ["HOME"] self.plugins = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 2765dfc60..4748aa581 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -816,16 +816,12 @@ def test_invalid_xml_escape(): assert chr(i) == bin_xml_escape(unichr(i)).uniobj -def test_logxml_path_expansion(tmpdir, monkeypatch): +def test_logxml_path_expansion(tmpdir): home_tilde = py.path.local(os.path.expanduser("~")).join("test.xml") - xml_tilde = LogXML("~%stest.xml" % tmpdir.sep, None) assert xml_tilde.logfile == home_tilde - # this is here for when $HOME is not set correct - monkeypatch.setenv("HOME", str(tmpdir)) home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml")) - xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None) assert xml_var.logfile == home_var diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 5b62def2d..05cb9bd77 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -470,9 +470,6 @@ class TestPDB(object): ''' """ ) - # Prevent ~/.pdbrc etc to output anything. - monkeypatch.setenv("HOME", str(testdir)) - child = testdir.spawn_pytest("--doctest-modules --pdb %s" % p1) child.expect("Pdb") From 920bffbfbb21f3fa79596129a67294f32ed44ee1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 01:06:50 +0100 Subject: [PATCH 31/95] Revisit _pytest.capture: repr, doc fixes, minor --- src/_pytest/capture.py | 65 ++++++++++++++++++++++++----------------- testing/test_capture.py | 2 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7bd319b1a..867b976b5 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -91,6 +91,13 @@ class CaptureManager(object): self._global_capturing = None self._current_item = None + def __repr__(self): + return "" % ( + self._method, + self._global_capturing, + self._current_item, + ) + def _getcapture(self, method): if method == "fd": return MultiCapture(out=True, err=True, Capture=FDCapture) @@ -98,8 +105,7 @@ class CaptureManager(object): return MultiCapture(out=True, err=True, Capture=SysCapture) elif method == "no": return MultiCapture(out=False, err=False, in_=False) - else: - raise ValueError("unknown capturing method: %r" % method) + raise ValueError("unknown capturing method: %r" % method) # pragma: no cover # Global capturing control @@ -161,8 +167,8 @@ class CaptureManager(object): @contextlib.contextmanager def global_and_fixture_disabled(self): - """Context manager to temporarily disables global and current fixture capturing.""" - # Need to undo local capsys-et-al if exists before disabling global capture + """Context manager to temporarily disable global and current fixture capturing.""" + # Need to undo local capsys-et-al if it exists before disabling global capture. self.suspend_fixture(self._current_item) self.suspend_global_capture(in_=False) try: @@ -247,10 +253,11 @@ def _ensure_only_one_capture_fixture(request, name): @pytest.fixture def capsys(request): - """Enable capturing of writes to ``sys.stdout`` and ``sys.stderr`` and make - captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` - objects. + """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. """ _ensure_only_one_capture_fixture(request, "capsys") with _install_capture_fixture_on_item(request, SysCapture) as fixture: @@ -259,26 +266,28 @@ def capsys(request): @pytest.fixture def capsysbinary(request): - """Enable capturing of writes to ``sys.stdout`` and ``sys.stderr`` and make - captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes`` - objects. + """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsysbinary.readouterr()`` + method calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``bytes`` objects. """ _ensure_only_one_capture_fixture(request, "capsysbinary") # Currently, the implementation uses the python3 specific `.buffer` # property of CaptureIO. if sys.version_info < (3,): - raise request.raiseerror("capsysbinary is only supported on python 3") + raise request.raiseerror("capsysbinary is only supported on Python 3") with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture: yield fixture @pytest.fixture def capfd(request): - """Enable capturing of writes to file descriptors ``1`` and ``2`` and make - captured output available via ``capfd.readouterr()`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` - objects. + """Enable text capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. """ _ensure_only_one_capture_fixture(request, "capfd") if not hasattr(os, "dup"): @@ -291,10 +300,11 @@ def capfd(request): @pytest.fixture def capfdbinary(request): - """Enable capturing of write to file descriptors 1 and 2 and make - captured output available via ``capfdbinary.readouterr`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be - ``bytes`` objects. + """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``byte`` objects. """ _ensure_only_one_capture_fixture(request, "capfdbinary") if not hasattr(os, "dup"): @@ -316,9 +326,9 @@ def _install_capture_fixture_on_item(request, capture_class): """ request.node._capture_fixture = fixture = CaptureFixture(capture_class, request) capmanager = request.config.pluginmanager.getplugin("capturemanager") - # need to active this fixture right away in case it is being used by another fixture (setup phase) - # if this fixture is being used only by a test function (call phase), then we wouldn't need this - # activation, but it doesn't hurt + # Need to active this fixture right away in case it is being used by another fixture (setup phase). + # If this fixture is being used only by a test function (call phase), then we wouldn't need this + # activation, but it doesn't hurt. capmanager.activate_fixture(request.node) yield fixture fixture.close() @@ -357,7 +367,7 @@ class CaptureFixture(object): def readouterr(self): """Read and return the captured output so far, resetting the internal buffer. - :return: captured content as a namedtuple with ``out`` and ``err`` string attributes + :return: captured content as a namedtuple with ``out`` and ``err`` string attributes """ captured_out, captured_err = self._captured_out, self._captured_err if self._capture is not None: @@ -446,6 +456,9 @@ class MultiCapture(object): if err: self.err = Capture(2) + def __repr__(self): + return "" % (self.out, self.err, self.in_) + def start_capturing(self): if self.in_: self.in_.start() @@ -590,7 +603,7 @@ class FDCapture(FDCaptureBinary): EMPTY_BUFFER = str() def snap(self): - res = FDCaptureBinary.snap(self) + res = super(FDCapture, self).snap() enc = getattr(self.tmpfile, "encoding", None) if enc and isinstance(res, bytes): res = six.text_type(res, enc, "replace") diff --git a/testing/test_capture.py b/testing/test_capture.py index 91cf8d8cf..b21c216cd 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -566,7 +566,7 @@ class TestCaptureFixture(object): result.stdout.fnmatch_lines( [ "*test_hello*", - "*capsysbinary is only supported on python 3*", + "*capsysbinary is only supported on Python 3*", "*1 error in*", ] ) From 7395501d1d3074a0f9f723a66c1616b24769f310 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 01:37:08 +0100 Subject: [PATCH 32/95] Easier read with _colorama_workaround/_readline_workaround --- src/_pytest/capture.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 867b976b5..c6f834545 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -706,13 +706,11 @@ def _colorama_workaround(): first import of colorama while I/O capture is active, colorama will fail in various ways. """ - - if not sys.platform.startswith("win32"): - return - try: - import colorama # noqa - except ImportError: - pass + if sys.platform.startswith("win32"): + try: + import colorama # noqa: F401 + except ImportError: + pass def _readline_workaround(): @@ -733,13 +731,11 @@ def _readline_workaround(): See https://github.com/pytest-dev/pytest/pull/1281 """ - - if not sys.platform.startswith("win32"): - return - try: - import readline # noqa - except ImportError: - pass + if sys.platform.startswith("win32"): + try: + import readline # noqa: F401 + except ImportError: + pass def _py36_windowsconsoleio_workaround(stream): From 1a119a22d115b367e30a6810823c0970cbb55422 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Mar 2019 18:54:45 -0300 Subject: [PATCH 33/95] Internal refactorings in order to support the new pytest-subtests plugin Related to #1367 --- changelog/4920.feature.rst | 6 +++ src/_pytest/reports.py | 83 ++++++++++++++++++++++++++++++++++++++ src/_pytest/runner.py | 38 +---------------- src/_pytest/terminal.py | 39 ++++++++++-------- testing/test_terminal.py | 15 +++++++ 5 files changed, 127 insertions(+), 54 deletions(-) create mode 100644 changelog/4920.feature.rst diff --git a/changelog/4920.feature.rst b/changelog/4920.feature.rst new file mode 100644 index 000000000..5eb152482 --- /dev/null +++ b/changelog/4920.feature.rst @@ -0,0 +1,6 @@ +Internal refactorings have been made in order to make the implementation of the +`pytest-subtests `__ plugin +possible, which adds unittest sub-test support and a new ``subtests`` fixture as discussed in +`#1367 `__. + +For details on the internal refactorings, please see the details on the related PR. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index eabfe88e5..23c7cbdd9 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,8 @@ import py +from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr +from _pytest.outcomes import skip def getslaveinfoline(node): @@ -20,6 +22,7 @@ def getslaveinfoline(node): class BaseReport(object): when = None + location = None def __init__(self, **kw): self.__dict__.update(kw) @@ -97,6 +100,43 @@ class BaseReport(object): def fspath(self): return self.nodeid.split("::")[0] + @property + def count_towards_summary(self): + """ + **Experimental** + + Returns True if this report should be counted towards the totals shown at the end of the + test session: "1 passed, 1 failure, etc". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + return True + + @property + def head_line(self): + """ + **Experimental** + + Returns the head line shown with longrepr output for this report, more commonly during + traceback representation during failures:: + + ________ Test.foo ________ + + + In the example above, the head_line is "Test.foo". + + .. note:: + + This function is considered **experimental**, so beware that it is subject to changes + even in patch releases. + """ + if self.location is not None: + fspath, lineno, domain = self.location + return domain + class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if @@ -159,6 +199,49 @@ class TestReport(BaseReport): self.outcome, ) + @classmethod + def from_item_and_call(cls, item, call): + """ + Factory method to create and fill a TestReport with standard item and call info. + """ + when = call.when + duration = call.stop - call.start + keywords = {x: 1 for x in item.keywords} + excinfo = call.excinfo + sections = [] + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + if not isinstance(excinfo, ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(skip.Exception): + outcome = "skipped" + r = excinfo._getreprcrash() + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py( + excinfo, style=item.config.option.tbstyle + ) + for rwhen, key, content in item._report_sections: + sections.append(("Captured %s %s" % (key, rwhen), content)) + return cls( + item.nodeid, + item.location, + keywords, + outcome, + longrepr, + when, + sections, + duration, + user_properties=item.user_properties, + ) + class CollectReport(BaseReport): when = "collect" diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 8357991fe..6ca438849 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -246,43 +246,7 @@ class CallInfo(object): def pytest_runtest_makereport(item, call): - when = call.when - duration = call.stop - call.start - keywords = {x: 1 for x in item.keywords} - excinfo = call.excinfo - sections = [] - if not call.excinfo: - outcome = "passed" - longrepr = None - else: - if not isinstance(excinfo, ExceptionInfo): - outcome = "failed" - longrepr = excinfo - elif excinfo.errisinstance(skip.Exception): - outcome = "skipped" - r = excinfo._getreprcrash() - longrepr = (str(r.path), r.lineno, r.message) - else: - outcome = "failed" - if call.when == "call": - longrepr = item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = item._repr_failure_py( - excinfo, style=item.config.option.tbstyle - ) - for rwhen, key, content in item._report_sections: - sections.append(("Captured %s %s" % (key, rwhen), content)) - return TestReport( - item.nodeid, - item.location, - keywords, - outcome, - longrepr, - when, - sections, - duration, - user_properties=item.user_properties, - ) + return TestReport.from_item_and_call(item, call) def pytest_make_collect_report(collector): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 0868bd751..3d46ec896 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -197,6 +197,7 @@ class WarningReport(object): message = attr.ib() nodeid = attr.ib(default=None) fslocation = attr.ib(default=None) + count_towards_summary = True def get_location(self, config): """ @@ -383,6 +384,7 @@ class TerminalReporter(object): self.write_fspath_result(fsid, "") def pytest_runtest_logreport(self, report): + self._tests_ran = True rep = report res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) category, letter, word = res @@ -391,7 +393,6 @@ class TerminalReporter(object): else: markup = None self.stats.setdefault(category, []).append(rep) - self._tests_ran = True if not letter and not word: # probably passed setup/teardown return @@ -724,9 +725,8 @@ class TerminalReporter(object): return res + " " def _getfailureheadline(self, rep): - if hasattr(rep, "location"): - fspath, lineno, domain = rep.location - return domain + if rep.head_line: + return rep.head_line else: return "test session" # XXX? @@ -874,18 +874,23 @@ class TerminalReporter(object): def build_summary_stats_line(stats): - keys = ("failed passed skipped deselected xfailed xpassed warnings error").split() - unknown_key_seen = False - for key in stats.keys(): - if key not in keys: - if key: # setup/teardown reports have an empty key, ignore them - keys.append(key) - unknown_key_seen = True + known_types = ( + "failed passed skipped deselected xfailed xpassed warnings error".split() + ) + unknown_type_seen = False + for found_type in stats: + if found_type not in known_types: + if found_type: # setup/teardown reports have an empty key, ignore them + known_types.append(found_type) + unknown_type_seen = True parts = [] - for key in keys: - val = stats.get(key, None) - if val: - parts.append("%d %s" % (len(val), key)) + for key in known_types: + reports = stats.get(key, None) + if reports: + count = sum( + 1 for rep in reports if getattr(rep, "count_towards_summary", True) + ) + parts.append("%d %s" % (count, key)) if parts: line = ", ".join(parts) @@ -894,14 +899,14 @@ def build_summary_stats_line(stats): if "failed" in stats or "error" in stats: color = "red" - elif "warnings" in stats or unknown_key_seen: + elif "warnings" in stats or unknown_type_seen: color = "yellow" elif "passed" in stats: color = "green" else: color = "yellow" - return (line, color) + return line, color def _plugin_nameversions(plugininfo): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index fe67a6aec..164a33943 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -15,6 +15,7 @@ import py import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.reports import BaseReport from _pytest.terminal import _plugin_nameversions from _pytest.terminal import build_summary_stats_line from _pytest.terminal import getreportopt @@ -1228,6 +1229,20 @@ def test_summary_stats(exp_line, exp_color, stats_arg): assert color == exp_color +def test_skip_counting_towards_summary(): + class DummyReport(BaseReport): + count_towards_summary = True + + r1 = DummyReport() + r2 = DummyReport() + res = build_summary_stats_line({"failed": (r1, r2)}) + assert res == ("2 failed", "red") + + r1.count_towards_summary = False + res = build_summary_stats_line({"failed": (r1, r2)}) + assert res == ("1 failed", "red") + + class TestClassicOutputStyle(object): """Ensure classic output style works as expected (#3883)""" From a50b92ea67ed46374c63486ce554245be9ca71e8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Mar 2019 22:30:50 +0100 Subject: [PATCH 34/95] pytester: set HOME only with inline_run/popen Ref: https://github.com/pytest-dev/pytest/issues/4955 --- changelog/4941.feature.rst | 3 --- changelog/4956.feature.rst | 3 +++ src/_pytest/pytester.py | 12 ++++++++++-- testing/test_junitxml.py | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) delete mode 100644 changelog/4941.feature.rst create mode 100644 changelog/4956.feature.rst diff --git a/changelog/4941.feature.rst b/changelog/4941.feature.rst deleted file mode 100644 index 846b43f45..000000000 --- a/changelog/4941.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -``pytester``'s ``Testdir`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory. - -This ensures to not load configuration files from the real user's home directory. diff --git a/changelog/4956.feature.rst b/changelog/4956.feature.rst new file mode 100644 index 000000000..1dfbd7e97 --- /dev/null +++ b/changelog/4956.feature.rst @@ -0,0 +1,3 @@ +``pytester`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory during test runs. + +This ensures to not load configuration files from the real user's home directory. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8f1a7d1d5..ffcd2982a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -29,6 +29,7 @@ from _pytest.compat import Sequence from _pytest.main import EXIT_INTERRUPTED from _pytest.main import EXIT_OK from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path IGNORE_PAM = [ # filenames added when obtaining details about the current user @@ -469,8 +470,6 @@ class Testdir(object): os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot) os.environ.pop("TOX_ENV_DIR", None) # Ensure that it is not used for caching. os.environ.pop("PYTEST_ADDOPTS", None) # Do not use outer options. - os.environ["HOME"] = str(self.tmpdir) # Do not load user config. - os.environ["USERPROFILE"] = os.environ["HOME"] self.plugins = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() @@ -788,6 +787,12 @@ class Testdir(object): """ finalizers = [] try: + # Do not load user config. + monkeypatch = MonkeyPatch() + monkeypatch.setenv("HOME", str(self.tmpdir)) + monkeypatch.setenv("USERPROFILE", str(self.tmpdir)) + finalizers.append(monkeypatch.undo) + # When running pytest inline any plugins active in the main test # process are already imported. So this disables the warning which # will trigger to say they can no longer be rewritten, which is @@ -1018,6 +1023,9 @@ class Testdir(object): env["PYTHONPATH"] = os.pathsep.join( filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) ) + # Do not load user config. + env["HOME"] = str(self.tmpdir) + env["USERPROFILE"] = env["HOME"] kw["env"] = env popen = subprocess.Popen( diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 4748aa581..769e8e8a7 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -816,11 +816,12 @@ def test_invalid_xml_escape(): assert chr(i) == bin_xml_escape(unichr(i)).uniobj -def test_logxml_path_expansion(tmpdir): +def test_logxml_path_expansion(tmpdir, monkeypatch): home_tilde = py.path.local(os.path.expanduser("~")).join("test.xml") xml_tilde = LogXML("~%stest.xml" % tmpdir.sep, None) assert xml_tilde.logfile == home_tilde + monkeypatch.setenv("HOME", str(tmpdir)) home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml")) xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None) assert xml_var.logfile == home_var From 415899d428650d3eb85a339a4811ee75bd2be43d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 02:25:35 +0100 Subject: [PATCH 35/95] config: handle `-p no:plugin` with default plugins `-p no:capture` should not load its fixtures in the first place. --- changelog/4957.bugfix.rst | 3 +++ src/_pytest/config/__init__.py | 11 ++++++++--- testing/test_config.py | 13 +++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 changelog/4957.bugfix.rst diff --git a/changelog/4957.bugfix.rst b/changelog/4957.bugfix.rst new file mode 100644 index 000000000..ade73ce22 --- /dev/null +++ b/changelog/4957.bugfix.rst @@ -0,0 +1,3 @@ +``-p no:plugin`` is handled correctly for default (internal) plugins now, e.g. with ``-p no:capture``. + +Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c44392073..b2e4fd774 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -147,10 +147,15 @@ builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") -def get_config(): +def get_config(args=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() config = Config(pluginmanager) + + if args is not None: + # Handle any "-p no:plugin" args. + pluginmanager.consider_preparse(args) + for spec in default_plugins: pluginmanager.import_plugin(spec) return config @@ -178,7 +183,7 @@ def _prepareconfig(args=None, plugins=None): msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - config = get_config() + config = get_config(args) pluginmanager = config.pluginmanager try: if plugins: @@ -713,7 +718,7 @@ class Config(object): @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - config = get_config() + config = get_config(args) config.option.__dict__.update(option_dict) config.parse(args, addopts=False) for x in config.option.plugins: diff --git a/testing/test_config.py b/testing/test_config.py index 3dd707bfa..8c2e7a49e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,6 +15,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import EXIT_TESTSFAILED from _pytest.main import EXIT_USAGEERROR @@ -1176,3 +1177,15 @@ def test_help_and_version_after_argument_error(testdir): ["*pytest*{}*imported from*".format(pytest.__version__)] ) assert result.ret == EXIT_USAGEERROR + + +def test_config_does_not_load_blocked_plugin_from_args(testdir): + """This tests that pytest's config setup handles "-p no:X".""" + p = testdir.makepyfile("def test(capfd): pass") + result = testdir.runpytest(str(p), "-pno:capture", "--tb=native") + result.stdout.fnmatch_lines(["E fixture 'capfd' not found"]) + assert result.ret == EXIT_TESTSFAILED + + result = testdir.runpytest(str(p), "-pno:capture", "-s") + result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) + assert result.ret == EXIT_USAGEERROR From c7c120fba61209997ea69937f304f5a51fb2fea0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 02:27:47 +0100 Subject: [PATCH 36/95] terminal: handle "capture" option not being available This is the case with `-p no:capture` now. --- src/_pytest/terminal.py | 4 ++-- testing/test_config.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3d46ec896..291da9723 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -246,7 +246,7 @@ class TerminalReporter(object): def _determine_show_progress_info(self): """Return True if we should display progress information based on the current config""" # do not show progress if we are not capturing output (#3038) - if self.config.getoption("capture") == "no": + if self.config.getoption("capture", "no") == "no": return False # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow"): @@ -456,7 +456,7 @@ class TerminalReporter(object): self._tw.write(msg + "\n", cyan=True) def _get_progress_information_message(self): - if self.config.getoption("capture") == "no": + if self.config.getoption("capture", "no") == "no": return "" collected = self._session.testscollected if self.config.getini("console_output_style") == "count": diff --git a/testing/test_config.py b/testing/test_config.py index 8c2e7a49e..50a40a2cb 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1182,7 +1182,7 @@ def test_help_and_version_after_argument_error(testdir): def test_config_does_not_load_blocked_plugin_from_args(testdir): """This tests that pytest's config setup handles "-p no:X".""" p = testdir.makepyfile("def test(capfd): pass") - result = testdir.runpytest(str(p), "-pno:capture", "--tb=native") + result = testdir.runpytest(str(p), "-pno:capture") result.stdout.fnmatch_lines(["E fixture 'capfd' not found"]) assert result.ret == EXIT_TESTSFAILED From f7171034f9014f423b0cee5051d29979c4338e46 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 03:04:41 +0100 Subject: [PATCH 37/95] terminal: remove unnecessary check in _get_progress_information_message All calls to _get_progress_information_message are only done for `_show_progress_info`, which is `False` with `capture=no`. --- src/_pytest/terminal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 291da9723..d15456ee8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -456,8 +456,6 @@ class TerminalReporter(object): self._tw.write(msg + "\n", cyan=True) def _get_progress_information_message(self): - if self.config.getoption("capture", "no") == "no": - return "" collected = self._session.testscollected if self.config.getini("console_output_style") == "count": if collected: From bcdbb6b677dad6219d9b3956fe957269d65d0542 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 18:51:53 +0100 Subject: [PATCH 38/95] Revisit mkdir/_ensure_supporting_files in cacheprovider - cacheprovider: move call to _ensure_supporting_files This makes it less likely to have a race here (which is not critical), but happened previously probably with xdist, causing flaky coverage with `if not readme_path.is_file():` etc checks in `_ensure_supporting_files`, which has been removed in the `features` branch already. --- src/_pytest/cacheprovider.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 0c25914bb..ebba0f935 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -121,10 +121,12 @@ class Cache(object): cache_dir_exists_already = True else: cache_dir_exists_already = self._cachedir.exists() - path.parent.mkdir(exist_ok=True, parents=True) + path.parent.mkdir(exist_ok=True, parents=True) except (IOError, OSError): self.warn("could not create cache path {path}", path=path) return + if not cache_dir_exists_already: + self._ensure_supporting_files() try: f = path.open("wb" if PY2 else "w") except (IOError, OSError): @@ -133,9 +135,6 @@ class Cache(object): with f: json.dump(value, f, indent=2, sort_keys=True) - if not cache_dir_exists_already: - self._ensure_supporting_files() - def _ensure_supporting_files(self): """Create supporting files in the cache dir that are not really part of the cache.""" readme_path = self._cachedir / "README.md" From cc6e5ec345d60c31013bda367ada5392d1e06ad9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 18:24:25 +0100 Subject: [PATCH 39/95] tests: add test_report_collect_after_half_a_second This is meant for stable coverage with "collecting X item(s)". --- src/_pytest/terminal.py | 4 +++- testing/test_terminal.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d15456ee8..3537ae445 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -26,6 +26,8 @@ from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED from _pytest.main import EXIT_USAGEERROR +REPORT_COLLECTING_RESOLUTION = 0.5 + class MoreQuietAction(argparse.Action): """ @@ -512,7 +514,7 @@ class TerminalReporter(object): t = time.time() if ( self._collect_report_last_write is not None - and self._collect_report_last_write > t - 0.5 + and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION ): return self._collect_report_last_write = t diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 164a33943..1f3fff8c5 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -142,6 +142,31 @@ class TestTerminal(object): child.sendeof() child.kill(15) + def test_report_collect_after_half_a_second(self, testdir): + """Test for "collecting" being updated after 0.5s""" + + testdir.makepyfile( + **{ + "test1.py": """ + import _pytest.terminal + + _pytest.terminal.REPORT_COLLECTING_RESOLUTION = 0 + + def test_1(): + pass + """, + "test2.py": "def test_2(): pass", + } + ) + + child = testdir.spawn_pytest("-v test1.py test2.py") + child.expect(r"collecting \.\.\.") + child.expect(r"collecting 1 item") + child.expect(r"collecting 2 items") + child.expect(r"collected 2 items") + rest = child.read().decode("utf8") + assert "2 passed in" in rest + def test_itemreport_subclasses_show_subclassed_file(self, testdir): testdir.makepyfile( test_p1=""" From 553951c44381bf554a20d656534f616873f0fbc6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 21 Mar 2019 04:50:51 +0100 Subject: [PATCH 40/95] Fix some issues related to "-p no:X" with default_plugins --- src/_pytest/config/__init__.py | 2 +- src/_pytest/main.py | 2 +- src/_pytest/runner.py | 6 +++--- src/_pytest/terminal.py | 2 +- testing/test_config.py | 39 ++++++++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b2e4fd774..2ab1d0790 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -762,7 +762,7 @@ class Config(object): by the importhook. """ ns, unknown_args = self._parser.parse_known_and_unknown_args(args) - mode = ns.assertmode + mode = getattr(ns, "assertmode", "plain") if mode == "rewrite": try: hook = _pytest.assertion.install_importhook(self) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d8478d4fc..3effbfb6e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -548,7 +548,7 @@ class Session(nodes.FSCollector): # Start with a Session root, and delve to argpath item (dir or file) # and stack all Packages found on the way. # No point in finding packages when collecting doctests - if not self.config.option.doctestmodules: + if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager for parent in reversed(argpath.parts()): if pm._confcutdir and pm._confcutdir.relto(parent): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 6ca438849..1f09f42e8 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -87,9 +87,9 @@ def runtestprotocol(item, log=True, nextitem=None): rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: - if item.config.option.setupshow: + if item.config.getoption("setupshow", False): show_test_item(item) - if not item.config.option.setuponly: + if not item.config.getoption("setuponly", False): reports.append(call_and_report(item, "call", log)) reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) # after all teardown hooks have been called @@ -192,7 +192,7 @@ def call_runtest_hook(item, when, **kwds): hookname = "pytest_runtest_" + when ihook = getattr(item.ihook, hookname) reraise = (Exit,) - if not item.config.getvalue("usepdb"): + if not item.config.getoption("usepdb", False): reraise += (KeyboardInterrupt,) return CallInfo.from_call( lambda: ihook(item=item, **kwds), when=when, reraise=reraise diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3537ae445..06604fdf1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -251,7 +251,7 @@ class TerminalReporter(object): if self.config.getoption("capture", "no") == "no": return False # do not show progress if we are showing fixture setup/teardown - if self.config.getoption("setupshow"): + if self.config.getoption("setupshow", False): return False return self.config.getini("console_output_style") in ("progress", "count") diff --git a/testing/test_config.py b/testing/test_config.py index 50a40a2cb..57cbd10ee 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,6 +15,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED from _pytest.main import EXIT_USAGEERROR @@ -1189,3 +1190,41 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): result = testdir.runpytest(str(p), "-pno:capture", "-s") result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) assert result.ret == EXIT_USAGEERROR + + +@pytest.mark.parametrize( + "plugin", + [ + x + for x in _pytest.config.default_plugins + if x + not in [ + "fixtures", + "helpconfig", # Provides -p. + "main", + "mark", + "python", + "runner", + "terminal", # works in OK case (no output), but not with failures. + ] + ], +) +def test_config_blocked_default_plugins(testdir, plugin): + if plugin == "debugging": + # https://github.com/pytest-dev/pytest-xdist/pull/422 + try: + import xdist # noqa: F401 + except ImportError: + pass + else: + pytest.skip("does not work with xdist currently") + + p = testdir.makepyfile("def test(): pass") + result = testdir.runpytest(str(p), "-pno:%s" % plugin) + assert result.ret == EXIT_OK + result.stdout.fnmatch_lines(["* 1 passed in *"]) + + p = testdir.makepyfile("def test(): assert 0") + result = testdir.runpytest(str(p), "-pno:%s" % plugin) + assert result.ret == EXIT_TESTSFAILED + result.stdout.fnmatch_lines(["* 1 failed in *"]) From ea2c6b8a88c1e167a086efa2fc697f72885e8ace Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 21 Mar 2019 04:38:11 +0100 Subject: [PATCH 41/95] config: fix consider_preparse with missing argument to -p This is only required after/with 415899d4 - otherwise argparse ensures there is an argument already. --- src/_pytest/config/__init__.py | 5 ++++- testing/test_pluginmanager.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b2e4fd774..41519595f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -481,7 +481,10 @@ class PytestPluginManager(PluginManager): i += 1 if isinstance(opt, six.string_types): if opt == "-p": - parg = args[i] + try: + parg = args[i] + except IndexError: + return i += 1 elif opt.startswith("-p"): parg = opt[2:] diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 4e0c0ed1e..6b44f3e0c 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -313,6 +313,9 @@ class TestPytestPluginManagerBootstrapming(object): assert '"hello123"' in excinfo.value.args[0] pytestpm.consider_preparse(["-pno:hello123"]) + # Handles -p without following arg (when used without argparse). + pytestpm.consider_preparse(["-p"]) + def test_plugin_prevent_register(self, pytestpm): pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) l1 = pytestpm.get_plugins() From ea7357bc580a081e81a81a37ae9d6f36403d499f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 27 Feb 2019 18:31:40 +0100 Subject: [PATCH 42/95] ci: PYTEST_ADDOPTS=-vv in general This is useful when viewing logs, especially with hanging tests. Uses non-verbose mode with a single job for full coverage. --- .travis.yml | 13 +++++++++---- azure-pipelines.yml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 750f93f81..c2a6035ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,13 @@ stages: if: repo = pytest-dev/pytest AND tag IS NOT present - name: deploy if: repo = pytest-dev/pytest AND tag IS present -python: - - '3.7' +python: '3.7' +cache: false + +env: + global: + - PYTEST_ADDOPTS=-vv + install: - python -m pip install --upgrade --pre tox @@ -57,7 +62,8 @@ jobs: # - pytester's LsofFdLeakChecker # - TestArgComplete (linux only) # - numpy - - env: TOXENV=py37-lsof-numpy-xdist PYTEST_COVERAGE=1 + # Empty PYTEST_ADDOPTS to run this non-verbose. + - env: TOXENV=py37-lsof-numpy-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= # Specialized factors for py27. - env: TOXENV=py27-nobyte-numpy-xdist @@ -147,4 +153,3 @@ notifications: skip_join: true email: - pytest-commit@python.org -cache: false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 712106c94..89ed9791f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,7 +3,7 @@ trigger: - features variables: - PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml" + PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml -vv" python.needs_vc: False python.exe: "python" COVERAGE_FILE: "$(Build.Repository.LocalPath)/.coverage" From 2e7d6a6202bedf9555a02d1576adf789f49ae852 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 03:05:33 +0100 Subject: [PATCH 43/95] Fix test_assertrewrite in verbose mode Fixes https://github.com/pytest-dev/pytest/issues/4879. --- testing/test_assertrewrite.py | 67 ++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index bdfbf823c..2d57d0494 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -127,7 +127,7 @@ class TestAssertionRewrite(object): result = testdir.runpytest_subprocess() assert "warnings" not in "".join(result.outlines) - def test_name(self): + def test_name(self, request): def f(): assert False @@ -147,17 +147,41 @@ class TestAssertionRewrite(object): def f(): assert sys == 42 - assert getmsg(f, {"sys": sys}) == "assert sys == 42" + verbose = request.config.getoption("verbose") + msg = getmsg(f, {"sys": sys}) + if verbose > 0: + assert msg == ( + "assert == 42\n" + " -\n" + " +42" + ) + else: + assert msg == "assert sys == 42" def f(): - assert cls == 42 # noqa + assert cls == 42 # noqa: F821 class X(object): pass - assert getmsg(f, {"cls": X}) == "assert cls == 42" + msg = getmsg(f, {"cls": X}).splitlines() + if verbose > 0: + if six.PY2: + assert msg == [ + "assert == 42", + " -", + " +42", + ] + else: + assert msg == [ + "assert .X'> == 42", + " -.X'>", + " +42", + ] + else: + assert msg == ["assert cls == 42"] - def test_dont_rewrite_if_hasattr_fails(self): + def test_dont_rewrite_if_hasattr_fails(self, request): class Y(object): """ A class whos getattr fails, but not with `AttributeError` """ @@ -173,10 +197,16 @@ class TestAssertionRewrite(object): def f(): assert cls().foo == 2 # noqa - message = getmsg(f, {"cls": Y}) - assert "assert 3 == 2" in message - assert "+ where 3 = Y.foo" in message - assert "+ where Y = cls()" in message + # XXX: looks like the "where" should also be there in verbose mode?! + message = getmsg(f, {"cls": Y}).splitlines() + if request.config.getoption("verbose") > 0: + assert message == ["assert 3 == 2", " -3", " +2"] + else: + assert message == [ + "assert 3 == 2", + " + where 3 = Y.foo", + " + where Y = cls()", + ] def test_assert_already_has_message(self): def f(): @@ -552,15 +582,16 @@ class TestAssertionRewrite(object): getmsg(f, must_pass=True) - def test_len(self): + def test_len(self, request): def f(): values = list(range(10)) assert len(values) == 11 - assert getmsg(f).startswith( - """assert 10 == 11 - + where 10 = len([""" - ) + msg = getmsg(f) + if request.config.getoption("verbose") > 0: + assert msg == "assert 10 == 11\n -10\n +11" + else: + assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])" def test_custom_reprcompare(self, monkeypatch): def my_reprcompare(op, left, right): @@ -608,7 +639,7 @@ class TestAssertionRewrite(object): assert getmsg(f).startswith("assert '%test' == 'test'") - def test_custom_repr(self): + def test_custom_repr(self, request): def f(): class Foo(object): a = 1 @@ -619,7 +650,11 @@ class TestAssertionRewrite(object): f = Foo() assert 0 == f.a - assert r"where 1 = \n{ \n~ \n}.a" in util._format_lines([getmsg(f)])[0] + lines = util._format_lines([getmsg(f)]) + if request.config.getoption("verbose") > 0: + assert lines == ["assert 0 == 1\n -0\n +1"] + else: + assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] def test_custom_repr_non_ascii(self): def f(): From 5a544d4fac810b28465eac27f431866ae22bf118 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 19 Nov 2018 12:49:38 +0100 Subject: [PATCH 44/95] tox.ini: usedevelop implies skipsdist --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 97cac6f6b..91a09be32 100644 --- a/tox.ini +++ b/tox.ini @@ -81,7 +81,6 @@ commands = {[testenv:py27-trial]commands} [testenv:docs] basepython = python3 -skipsdist = True usedevelop = True changedir = doc/en deps = -r{toxinidir}/doc/en/requirements.txt @@ -135,7 +134,6 @@ commands = [testenv:release] decription = do a release, required posarg of the version number basepython = python3.6 -skipsdist = True usedevelop = True passenv = * deps = From 5c26ba9cb17edca3f5b978e0a037bc8885a95437 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 10 Dec 2018 05:19:26 +0100 Subject: [PATCH 45/95] minor: wrap_session: s/Spurious/unexpected/ --- src/_pytest/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3effbfb6e..e08b1fa1d 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -225,7 +225,7 @@ def wrap_session(config, doit): config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + sys.stderr.write("mainloop: caught unexpected SystemExit!\n") finally: excinfo = None # Explicitly break reference cycle. From ade773390ac919f3850d634065e43b4eac89d992 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 4 Mar 2019 16:42:51 +0100 Subject: [PATCH 46/95] minor: rename inner test --- testing/test_cacheprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 35a7232ab..082b097ee 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -884,7 +884,7 @@ class TestReadme(object): def test_readme_failed(self, testdir): testdir.makepyfile( """ - def test_always_passes(): + def test_always_fails(): assert 0 """ ) From 8e125c9759b704abc376758b97a7920ab8724c95 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Mar 2019 12:06:33 +0100 Subject: [PATCH 47/95] doc/en/reference.rst: whitespace/alignment --- doc/en/reference.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bd89d024d..7c5ee2e4b 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1,4 +1,3 @@ - Reference ========= @@ -49,7 +48,7 @@ pytest.main .. autofunction:: _pytest.config.main pytest.param -~~~~~~~~~~~~~ +~~~~~~~~~~~~ .. autofunction:: pytest.param(*values, [id], [marks]) From 7a6bcc363934f4fc8cce4115d24118a72ed35f11 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 13:22:57 +0100 Subject: [PATCH 48/95] Add reference to test_cmdline_python_namespace_package --- testing/acceptance_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 39bd2fd77..1342a416e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -677,6 +677,8 @@ class TestInvocationVariants(object): def test_cmdline_python_namespace_package(self, testdir, monkeypatch): """ test --pyargs option with namespace packages (#1567) + + Ref: https://packaging.python.org/guides/packaging-namespace-packages/ """ monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) From 475119988ce4960bc7356a82d8ea2518d2c6089d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 14:36:11 +0100 Subject: [PATCH 49/95] monkeypatch.syspath_prepend: call fixup_namespace_packages Without the patch the test fails as follows: # Prepending should call fixup_namespace_packages. monkeypatch.syspath_prepend("world") > import ns_pkg.world E ModuleNotFoundError: No module named 'ns_pkg.world' --- changelog/4980.feature.rst | 1 + src/_pytest/monkeypatch.py | 5 +++++ testing/test_monkeypatch.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 changelog/4980.feature.rst diff --git a/changelog/4980.feature.rst b/changelog/4980.feature.rst new file mode 100644 index 000000000..1c42547c1 --- /dev/null +++ b/changelog/4980.feature.rst @@ -0,0 +1 @@ +``monkeypatch.syspath_prepend`` calls ``pkg_resources.fixup_namespace_packages`` to handle namespace packages better. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 46d9718da..f6c134664 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -262,10 +262,15 @@ class MonkeyPatch(object): def syspath_prepend(self, path): """ Prepend ``path`` to ``sys.path`` list of import locations. """ + from pkg_resources import fixup_namespace_packages + if self._savesyspath is None: self._savesyspath = sys.path[:] sys.path.insert(0, str(path)) + # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 + fixup_namespace_packages(str(path)) + def chdir(self, path): """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 0a953d3f1..d43fb6bab 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -437,3 +437,28 @@ def test_context(): m.setattr(functools, "partial", 3) assert not inspect.isclass(functools.partial) assert inspect.isclass(functools.partial) + + +def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): + for dirname in "hello", "world": + d = testdir.mkdir(dirname) + ns = d.mkdir("ns_pkg") + ns.join("__init__.py").write( + "__import__('pkg_resources').declare_namespace(__name__)" + ) + lib = ns.mkdir(dirname) + lib.join("__init__.py").write("def check(): return %r" % dirname) + + monkeypatch.syspath_prepend("hello") + import ns_pkg.hello + + assert ns_pkg.hello.check() == "hello" + + with pytest.raises(ImportError): + import ns_pkg.world + + # Prepending should call fixup_namespace_packages. + monkeypatch.syspath_prepend("world") + import ns_pkg.world + + assert ns_pkg.world.check() == "world" From 05d55b86df7c955a29ddd439a52bc66f78e5ab25 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 16:20:55 +0100 Subject: [PATCH 50/95] tests: minor sys.path cleanup --- testing/python/collect.py | 2 -- testing/test_assertrewrite.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/python/collect.py b/testing/python/collect.py index bc7462674..df6070b8d 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -34,8 +34,6 @@ class TestModule(object): ) def test_import_prepend_append(self, testdir, monkeypatch): - syspath = list(sys.path) - monkeypatch.setattr(sys, "path", syspath) root1 = testdir.mkdir("root1") root2 = testdir.mkdir("root2") root1.ensure("x456.py") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index bdfbf823c..a92ffcf75 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1335,7 +1335,7 @@ class TestEarlyRewriteBailout(object): # Setup conditions for py's fspath trying to import pathlib on py34 # always (previously triggered via xdist only). # Ref: https://github.com/pytest-dev/py/pull/207 - monkeypatch.setattr(sys, "path", [""] + sys.path) + monkeypatch.syspath_prepend("") monkeypatch.delitem(sys.modules, "pathlib", raising=False) testdir.makepyfile( From 5df45f5b278ce3ed5e8dbfba67d317416dcc6c84 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 16:26:55 +0100 Subject: [PATCH 51/95] Use fixup_namespace_packages also with pytester.syspathinsert --- changelog/4980.feature.rst | 2 +- src/_pytest/pytester.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog/4980.feature.rst b/changelog/4980.feature.rst index 1c42547c1..40f1de9c1 100644 --- a/changelog/4980.feature.rst +++ b/changelog/4980.feature.rst @@ -1 +1 @@ -``monkeypatch.syspath_prepend`` calls ``pkg_resources.fixup_namespace_packages`` to handle namespace packages better. +Namespace packages are handled better with ``monkeypatch.syspath_prepend`` and ``testdir.syspathinsert`` (via ``pkg_resources.fixup_namespace_packages``). diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ffcd2982a..bc1405176 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -593,11 +593,16 @@ class Testdir(object): This is undone automatically when this object dies at the end of each test. - """ + from pkg_resources import fixup_namespace_packages + if path is None: path = self.tmpdir - sys.path.insert(0, str(path)) + + dirname = str(path) + sys.path.insert(0, dirname) + fixup_namespace_packages(dirname) + # a call to syspathinsert() usually means that the caller wants to # import some dynamically created files, thus with python3 we # invalidate its import caches From 56dc01ffe04501089a3228267c60405007ecfa1a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 16:29:02 +0100 Subject: [PATCH 52/95] minor: revisit _possibly_invalidate_import_caches --- src/_pytest/pytester.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index bc1405176..f8a79ebc9 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -611,12 +611,10 @@ class Testdir(object): def _possibly_invalidate_import_caches(self): # invalidate caches if we can (py33 and above) try: - import importlib + from importlib import invalidate_caches except ImportError: - pass - else: - if hasattr(importlib, "invalidate_caches"): - importlib.invalidate_caches() + return + invalidate_caches() def mkdir(self, name): """Create a new (sub)directory.""" From fd64fa1863e347246b9f9bf0a0e3dd7af5f6284b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 16:56:00 +0100 Subject: [PATCH 53/95] Revisit test_importplugin_error_message Should be more helpful in case of errors than before: > assert re.match(expected_message, str(excinfo.value)) E _pytest.warning_types.PytestWarning: asserting the value None, please use "assert is None" https://travis-ci.org/pytest-dev/pytest/jobs/509970576#L208 --- testing/test_pluginmanager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 6b44f3e0c..10b54c112 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -4,7 +4,6 @@ from __future__ import division from __future__ import print_function import os -import re import sys import types @@ -165,10 +164,10 @@ def test_importplugin_error_message(testdir, pytestpm): with pytest.raises(ImportError) as excinfo: pytestpm.import_plugin("qwe") - expected_message = '.*Error importing plugin "qwe": Not possible to import: .' - expected_traceback = ".*in test_traceback" - assert re.match(expected_message, str(excinfo.value)) - assert re.match(expected_traceback, str(excinfo.traceback[-1])) + assert str(excinfo.value).endswith( + 'Error importing plugin "qwe": Not possible to import: ☺' + ) + assert "in test_traceback" in str(excinfo.traceback[-1]) class TestPytestPluginManager(object): From afa985c135957cae6ebed01748e5f4fcc18ba975 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 17:16:08 +0100 Subject: [PATCH 54/95] Revisit coverage in some tests --- testing/test_compat.py | 21 +++++++++++++-------- testing/test_modimport.py | 11 ++++++----- testing/test_session.py | 6 ++---- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/testing/test_compat.py b/testing/test_compat.py index 79224deef..6c4d24398 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -18,10 +18,10 @@ from _pytest.outcomes import OutcomeException def test_is_generator(): def zap(): - yield + yield # pragma: no cover def foo(): - pass + pass # pragma: no cover assert is_generator(zap) assert not is_generator(foo) @@ -37,15 +37,20 @@ def test_real_func_loop_limit(): def __getattr__(self, attr): if not self.left: - raise RuntimeError("its over") + raise RuntimeError("it's over") # pragma: no cover self.left -= 1 return self evil = Evil() - with pytest.raises(ValueError): - res = get_real_func(evil) - print(res) + with pytest.raises( + ValueError, + match=( + "could not find real function of \n" + "stopped at " + ), + ): + get_real_func(evil) def test_get_real_func(): @@ -54,14 +59,14 @@ def test_get_real_func(): def decorator(f): @wraps(f) def inner(): - pass + pass # pragma: no cover if six.PY2: inner.__wrapped__ = f return inner def func(): - pass + pass # pragma: no cover wrapped_func = decorator(decorator(func)) assert get_real_func(wrapped_func) is func diff --git a/testing/test_modimport.py b/testing/test_modimport.py index cb5f5ccd3..33862799b 100644 --- a/testing/test_modimport.py +++ b/testing/test_modimport.py @@ -30,8 +30,9 @@ def test_fileimport(modfile): stderr=subprocess.PIPE, ) (out, err) = p.communicate() - if p.returncode != 0: - pytest.fail( - "importing %s failed (exitcode %d): out=%r, err=%r" - % (modfile, p.returncode, out, err) - ) + assert p.returncode == 0, "importing %s failed (exitcode %d): out=%r, err=%r" % ( + modfile, + p.returncode, + out, + err, + ) diff --git a/testing/test_session.py b/testing/test_session.py index e5eb081d4..20079cd1b 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -68,9 +68,7 @@ class SessionTests(object): passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 out = failed[0].longrepr.reprcrash.message - if not out.find("DID NOT RAISE") != -1: - print(out) - pytest.fail("incorrect raises() output") + assert "DID NOT RAISE" in out def test_syntax_error_module(self, testdir): reprec = testdir.inline_runsource("this is really not python") @@ -148,7 +146,7 @@ class SessionTests(object): ) try: reprec = testdir.inline_run(testdir.tmpdir) - except pytest.skip.Exception: + except pytest.skip.Exception: # pragma: no covers pytest.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") assert len(reports) == 1 From 2d690b83bf5c434a03bce4842e269418aa928141 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 23 Mar 2019 00:29:36 +0100 Subject: [PATCH 55/95] ExceptionInfo.from_current: assert current exception --- src/_pytest/_code/code.py | 1 + testing/code/test_code.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 1105310cf..79845db73 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -413,6 +413,7 @@ class ExceptionInfo(object): to the exception message/``__str__()`` """ tup = sys.exc_info() + assert tup[0] is not None, "no current exception" _striptext = "" if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], "msg", None) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 3362d4604..81a87481c 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -172,6 +172,10 @@ class TestExceptionInfo(object): exci = _pytest._code.ExceptionInfo.from_current() assert exci.getrepr() + def test_from_current_with_missing(self): + with pytest.raises(AssertionError, match="no current exception"): + _pytest._code.ExceptionInfo.from_current() + class TestTracebackEntry(object): def test_getsource(self): From 08f3b02dfcfe8f0b41a5c8deb66c897e99196c50 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 23 Mar 2019 11:36:18 +0100 Subject: [PATCH 56/95] tests: fnmatch_lines: use list For strings fnmatch_lines converts it into a Source objects, splitted on newlines. This is not necessary here, and it is more consistent to use lists here in the first place. --- testing/acceptance_test.py | 4 ++-- testing/deprecated_test.py | 2 +- testing/logging/test_reporting.py | 2 +- testing/python/collect.py | 18 +++++++++--------- testing/python/fixtures.py | 12 ++++++------ testing/test_assertrewrite.py | 14 +++++++------- testing/test_cacheprovider.py | 14 +++++++------- testing/test_capture.py | 2 +- testing/test_collection.py | 8 ++++---- testing/test_config.py | 2 +- testing/test_doctest.py | 2 +- testing/test_nose.py | 2 +- testing/test_runner.py | 10 +++++----- testing/test_skipping.py | 16 +++++++++------- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 4 ++-- 16 files changed, 58 insertions(+), 56 deletions(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 39bd2fd77..bed7d7080 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1033,7 +1033,7 @@ def test_pytest_plugins_as_module(testdir): } ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 1 passed in *") + result.stdout.fnmatch_lines(["* 1 passed in *"]) def test_deferred_hook_checking(testdir): @@ -1173,7 +1173,7 @@ def test_fixture_mock_integration(testdir): """Test that decorators applied to fixture are left working (#3774)""" p = testdir.copy_example("acceptance/fixture_mock_integration.py") result = testdir.runpytest(p) - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_usage_error_code(testdir): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 536370f92..4818379fe 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -147,7 +147,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( if use_pyargs: assert msg not in res.stdout.str() else: - res.stdout.fnmatch_lines("*{msg}*".format(msg=msg)) + res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 90db8813e..4f30157f4 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -747,7 +747,7 @@ def test_log_level_not_changed_by_default(testdir): """ ) result = testdir.runpytest("-s") - result.stdout.fnmatch_lines("* 1 passed in *") + result.stdout.fnmatch_lines(["* 1 passed in *"]) def test_log_file_ini(testdir): diff --git a/testing/python/collect.py b/testing/python/collect.py index bc7462674..860e164b6 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -560,7 +560,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") + result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"]) def test_parametrize_skip(self, testdir): testdir.makepyfile( @@ -575,7 +575,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") + result.stdout.fnmatch_lines(["* 2 passed, 1 skipped in *"]) def test_parametrize_skipif_no_skip(self, testdir): testdir.makepyfile( @@ -590,7 +590,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 1 failed, 2 passed in *") + result.stdout.fnmatch_lines(["* 1 failed, 2 passed in *"]) def test_parametrize_xfail(self, testdir): testdir.makepyfile( @@ -605,7 +605,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *") + result.stdout.fnmatch_lines(["* 2 passed, 1 xfailed in *"]) def test_parametrize_passed(self, testdir): testdir.makepyfile( @@ -620,7 +620,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *") + result.stdout.fnmatch_lines(["* 2 passed, 1 xpassed in *"]) def test_parametrize_xfail_passed(self, testdir): testdir.makepyfile( @@ -635,7 +635,7 @@ class TestFunction(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 3 passed in *") + result.stdout.fnmatch_lines(["* 3 passed in *"]) def test_function_original_name(self, testdir): items = testdir.getitems( @@ -833,7 +833,7 @@ class TestConftestCustomization(object): ) # Use runpytest_subprocess, since we're futzing with sys.meta_path. result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_setup_only_available_in_subdir(testdir): @@ -1298,14 +1298,14 @@ def test_keep_duplicates(testdir): def test_package_collection_infinite_recursion(testdir): testdir.copy_example("collect/package_infinite_recursion") result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_package_collection_init_given_as_argument(testdir): """Regression test for #3749""" p = testdir.copy_example("collect/package_init_given_as_arg") result = testdir.runpytest(p / "pkg" / "__init__.py") - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_package_with_modules(testdir): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 1ea37f85c..70917946e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -536,7 +536,7 @@ class TestRequestBasic(object): """ ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines("* 1 passed in *") + result.stdout.fnmatch_lines(["* 1 passed in *"]) def test_getfixturevalue_recursive(self, testdir): testdir.makeconftest( @@ -598,7 +598,7 @@ class TestRequestBasic(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 2 passed in *") + result.stdout.fnmatch_lines(["* 2 passed in *"]) @pytest.mark.parametrize("getfixmethod", ("getfixturevalue", "getfuncargvalue")) def test_getfixturevalue(self, testdir, getfixmethod): @@ -787,7 +787,7 @@ class TestRequestBasic(object): """Regression test for #3057""" testdir.copy_example("fixtures/test_getfixturevalue_dynamic.py") result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_funcargnames_compatattr(self, testdir): testdir.makepyfile( @@ -1527,7 +1527,7 @@ class TestFixtureManagerParseFactories(object): def test_collect_custom_items(self, testdir): testdir.copy_example("fixtures/custom_item") result = testdir.runpytest("foo") - result.stdout.fnmatch_lines("*passed*") + result.stdout.fnmatch_lines(["*passed*"]) class TestAutouseDiscovery(object): @@ -2609,7 +2609,7 @@ class TestFixtureMarker(object): ) reprec = testdir.runpytest("-s") for test in ["test_browser"]: - reprec.stdout.fnmatch_lines("*Finalized*") + reprec.stdout.fnmatch_lines(["*Finalized*"]) def test_class_scope_with_normal_tests(self, testdir): testpath = testdir.makepyfile( @@ -3450,7 +3450,7 @@ class TestContextManagerFixtureFuncs(object): """ ) result = testdir.runpytest("-s") - result.stdout.fnmatch_lines("*mew*") + result.stdout.fnmatch_lines(["*mew*"]) class TestParameterizedSubRequest(object): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index bdfbf823c..a331bcc6e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -796,7 +796,7 @@ def test_rewritten(): ) # needs to be a subprocess because pytester explicitly disables this warning result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines("*Module already imported*: _pytest") + result.stdout.fnmatch_lines(["*Module already imported*: _pytest"]) def test_rewrite_module_imported_from_conftest(self, testdir): testdir.makeconftest( @@ -1123,7 +1123,7 @@ class TestAssertionRewriteHookDetails(object): ) path.join("data.txt").write("Hey") result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) def test_issue731(testdir): @@ -1154,7 +1154,7 @@ class TestIssue925(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*E*assert (False == False) == False") + result.stdout.fnmatch_lines(["*E*assert (False == False) == False"]) def test_long_case(self, testdir): testdir.makepyfile( @@ -1164,7 +1164,7 @@ class TestIssue925(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*E*assert (False == True) == True") + result.stdout.fnmatch_lines(["*E*assert (False == True) == True"]) def test_many_brackets(self, testdir): testdir.makepyfile( @@ -1174,7 +1174,7 @@ class TestIssue925(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*E*assert True == ((False == True) == True)") + result.stdout.fnmatch_lines(["*E*assert True == ((False == True) == True)"]) class TestIssue2121: @@ -1194,7 +1194,7 @@ class TestIssue2121: """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*E*assert (1 + 1) == 3") + result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) @pytest.mark.parametrize("offset", [-1, +1]) @@ -1356,4 +1356,4 @@ class TestEarlyRewriteBailout(object): } ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 1 passed in *") + result.stdout.fnmatch_lines(["* 1 passed in *"]) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 35a7232ab..7639d332e 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -393,7 +393,7 @@ class TestLastFailed(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 failed in*") + result.stdout.fnmatch_lines(["*1 failed in*"]) def test_terminal_report_lastfailed(self, testdir): test_a = testdir.makepyfile( @@ -574,7 +574,7 @@ class TestLastFailed(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 xfailed*") + result.stdout.fnmatch_lines(["*1 xfailed*"]) assert self.get_cached_last_failed(testdir) == [] def test_xfail_strict_considered_failure(self, testdir): @@ -587,7 +587,7 @@ class TestLastFailed(object): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 failed*") + result.stdout.fnmatch_lines(["*1 failed*"]) assert self.get_cached_last_failed(testdir) == [ "test_xfail_strict_considered_failure.py::test" ] @@ -680,12 +680,12 @@ class TestLastFailed(object): """ ) result = testdir.runpytest(test_bar) - result.stdout.fnmatch_lines("*2 passed*") + result.stdout.fnmatch_lines(["*2 passed*"]) # ensure cache does not forget that test_foo_4 failed once before assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] result = testdir.runpytest("--last-failed") - result.stdout.fnmatch_lines("*1 failed, 3 deselected*") + result.stdout.fnmatch_lines(["*1 failed, 3 deselected*"]) assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] # 3. fix test_foo_4, run only test_foo.py @@ -698,11 +698,11 @@ class TestLastFailed(object): """ ) result = testdir.runpytest(test_foo, "--last-failed") - result.stdout.fnmatch_lines("*1 passed, 1 deselected*") + result.stdout.fnmatch_lines(["*1 passed, 1 deselected*"]) assert self.get_cached_last_failed(testdir) == [] result = testdir.runpytest("--last-failed") - result.stdout.fnmatch_lines("*4 passed*") + result.stdout.fnmatch_lines(["*4 passed*"]) assert self.get_cached_last_failed(testdir) == [] def test_lastfailed_no_failures_behavior_all_passed(self, testdir): diff --git a/testing/test_capture.py b/testing/test_capture.py index fa0bad5fc..c99b843c0 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -673,7 +673,7 @@ class TestCaptureFixture(object): ) ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) assert "stdout contents begin" not in result.stdout.str() assert "stderr contents begin" not in result.stdout.str() diff --git a/testing/test_collection.py b/testing/test_collection.py index 37f7ad89c..94f57c69d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -350,10 +350,10 @@ class TestCustomConftests(object): p = testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest(p) assert result.ret == 0 - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest() assert result.ret == EXIT_NOTESTSCOLLECTED - result.stdout.fnmatch_lines("*collected 0 items*") + result.stdout.fnmatch_lines(["*collected 0 items*"]) def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest( @@ -390,10 +390,10 @@ class TestCustomConftests(object): testdir.makepyfile(test_welt="def test_hallo(): pass") result = testdir.runpytest() assert result.ret == EXIT_NOTESTSCOLLECTED - result.stdout.fnmatch_lines("*collected 0 items*") + result.stdout.fnmatch_lines(["*collected 0 items*"]) result = testdir.runpytest("--XX") assert result.ret == 0 - result.stdout.fnmatch_lines("*2 passed*") + result.stdout.fnmatch_lines(["*2 passed*"]) def test_pytest_fs_collect_hooks_are_seen(self, testdir): testdir.makeconftest( diff --git a/testing/test_config.py b/testing/test_config.py index 57cbd10ee..07654e5ad 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -805,7 +805,7 @@ def test_collect_pytest_prefix_bug_integration(testdir): """Integration test for issue #3775""" p = testdir.copy_example("config/collect_pytest_prefix") result = testdir.runpytest(p) - result.stdout.fnmatch_lines("* 1 passed *") + result.stdout.fnmatch_lines(["* 1 passed *"]) def test_collect_pytest_prefix_bug(pytestconfig): diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 09f7c331d..db6c4c2d3 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -968,7 +968,7 @@ class TestDoctestAutoUseFixtures(object): """ ) result = testdir.runpytest("--doctest-modules") - result.stdout.fnmatch_lines("*2 passed*") + result.stdout.fnmatch_lines(["*2 passed*"]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("enable_doctest", [True, False]) diff --git a/testing/test_nose.py b/testing/test_nose.py index 6f3d292dd..2fe49177a 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -380,4 +380,4 @@ def test_skip_test_with_unicode(testdir): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("* 1 skipped *") + result.stdout.fnmatch_lines(["* 1 skipped *"]) diff --git a/testing/test_runner.py b/testing/test_runner.py index 66ae702a4..72484fb72 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -640,7 +640,7 @@ def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): def test_pytest_no_tests_collected_exit_status(testdir): result = testdir.runpytest() - result.stdout.fnmatch_lines("*collected 0 items*") + result.stdout.fnmatch_lines(["*collected 0 items*"]) assert result.ret == main.EXIT_NOTESTSCOLLECTED testdir.makepyfile( @@ -650,13 +650,13 @@ def test_pytest_no_tests_collected_exit_status(testdir): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*collected 1 item*") - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*collected 1 item*"]) + result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == main.EXIT_OK result = testdir.runpytest("-k nonmatch") - result.stdout.fnmatch_lines("*collected 1 item*") - result.stdout.fnmatch_lines("*1 deselected*") + result.stdout.fnmatch_lines(["*collected 1 item*"]) + result.stdout.fnmatch_lines(["*1 deselected*"]) assert result.ret == main.EXIT_NOTESTSCOLLECTED diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 33878c8f4..e5206a44e 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -331,7 +331,7 @@ class TestXFail(object): result = testdir.runpytest(p, "-rx") result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*reason:*hello*"]) result = testdir.runpytest(p, "--runxfail") - result.stdout.fnmatch_lines("*1 pass*") + result.stdout.fnmatch_lines(["*1 pass*"]) def test_xfail_imperative_in_setup_function(self, testdir): p = testdir.makepyfile( @@ -477,7 +477,7 @@ class TestXFail(object): % strict ) result = testdir.runpytest(p, "-rxX") - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @pytest.mark.parametrize("strict", [True, False]) @@ -493,7 +493,7 @@ class TestXFail(object): % strict ) result = testdir.runpytest(p, "-rxX") - result.stdout.fnmatch_lines("*1 passed*") + result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @pytest.mark.parametrize("strict_val", ["true", "false"]) @@ -515,7 +515,7 @@ class TestXFail(object): ) result = testdir.runpytest(p, "-rxX") strict = strict_val == "true" - result.stdout.fnmatch_lines("*1 failed*" if strict else "*1 xpassed*") + result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"]) assert result.ret == (1 if strict else 0) @@ -1130,7 +1130,9 @@ def test_module_level_skip_error(testdir): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*Using pytest.skip outside of a test is not allowed*") + result.stdout.fnmatch_lines( + ["*Using pytest.skip outside of a test is not allowed*"] + ) def test_module_level_skip_with_allow_module_level(testdir): @@ -1147,7 +1149,7 @@ def test_module_level_skip_with_allow_module_level(testdir): """ ) result = testdir.runpytest("-rxs") - result.stdout.fnmatch_lines("*SKIP*skip_module_level") + result.stdout.fnmatch_lines(["*SKIP*skip_module_level"]) def test_invalid_skip_keyword_parameter(testdir): @@ -1164,7 +1166,7 @@ def test_invalid_skip_keyword_parameter(testdir): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*TypeError:*['unknown']*") + result.stdout.fnmatch_lines(["*TypeError:*['unknown']*"]) def test_mark_xfail_item(testdir): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 22a5bf796..3b7a4ddc3 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -16,7 +16,7 @@ from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_tmpdir_fixture(testdir): p = testdir.copy_example("tmpdir/tmpdir_fixture.py") results = testdir.runpytest(p) - results.stdout.fnmatch_lines("*1 passed*") + results.stdout.fnmatch_lines(["*1 passed*"]) def test_ensuretemp(recwarn): diff --git a/testing/test_unittest.py b/testing/test_unittest.py index fe33855fa..a519ec255 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -794,7 +794,7 @@ def test_unittest_setup_interaction(testdir, fix_type, stmt): ) ) result = testdir.runpytest() - result.stdout.fnmatch_lines("*3 passed*") + result.stdout.fnmatch_lines(["*3 passed*"]) def test_non_unittest_no_setupclass_support(testdir): @@ -1040,4 +1040,4 @@ def test_setup_inheritance_skipping(testdir, test_name, expected_outcome): """Issue #4700""" testdir.copy_example("unittest/{}".format(test_name)) result = testdir.runpytest() - result.stdout.fnmatch_lines("* {} in *".format(expected_outcome)) + result.stdout.fnmatch_lines(["* {} in *".format(expected_outcome)]) From aa0b657e58c91c079738cbc3fc0df11749fddf09 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:01:22 +0100 Subject: [PATCH 57/95] Add Session.__repr__ --- src/_pytest/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3effbfb6e..4e3700b1e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -441,6 +441,15 @@ class Session(nodes.FSCollector): self.config.pluginmanager.register(self, name="session") + def __repr__(self): + return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( + self.__class__.__name__, + self.name, + getattr(self, "exitstatus", ""), + self.testsfailed, + self.testscollected, + ) + def _node_location_to_relpath(self, node_path): # bestrelpath is a quite slow function return self._bestrelpathcache[node_path] From 6352cf23742c53f3312f3a004c762a077bdb73d7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:15:40 +0100 Subject: [PATCH 58/95] test_implicit_bad_repr1: harden/cleanup --- testing/test_session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_session.py b/testing/test_session.py index e5eb081d4..b3aaf571d 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -126,14 +126,14 @@ class SessionTests(object): ) reprec = testdir.inline_run(p) passed, skipped, failed = reprec.listoutcomes() - assert len(failed) == 1 + assert (len(passed), len(skipped), len(failed)) == (1, 0, 1) out = failed[0].longrepr.reprcrash.message assert ( out.find( """[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""" ) != -1 - ) # ' + ) def test_skip_file_by_conftest(self, testdir): testdir.makepyfile( From d44e42ec1501ae9ee42f00578db8f2b4a55e26e6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:20:01 +0100 Subject: [PATCH 59/95] doc: improve warning_record_to_str --- src/_pytest/warnings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 764985736..3360aea9c 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -103,8 +103,9 @@ def catch_warnings_for_item(config, ihook, when, item): def warning_record_to_str(warning_message): - """Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2. + """Convert a warnings.WarningMessage to a string. + This takes lot of unicode shenaningans into account for Python 2. When Python 2 support is dropped this function can be greatly simplified. """ warn_msg = warning_message.message From 7da7b9610ce8906cc02bac036ac7aa89713583cb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:20:24 +0100 Subject: [PATCH 60/95] minor: whitespace --- testing/logging/test_reporting.py | 1 + testing/test_terminal.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 90db8813e..744a384f7 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -54,6 +54,7 @@ def test_root_logger_affected(testdir): """ import logging logger = logging.getLogger() + def test_foo(): logger.info('info text ' + 'going to logger') logger.warning('warning text ' + 'going to logger') diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1f3fff8c5..e8590b1ba 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -660,7 +660,6 @@ class TestTerminalFunctional(object): ) def test_verbose_reporting(self, verbose_testfile, testdir, pytestconfig): - result = testdir.runpytest( verbose_testfile, "-v", "-Walways::pytest.PytestWarning" ) From ce59f42ce13c135fe27054c81467755de67268b3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:21:13 +0100 Subject: [PATCH 61/95] revisit test_root_logger_affected --- testing/logging/test_reporting.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 744a384f7..c64c4cb56 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -67,15 +67,14 @@ def test_root_logger_affected(testdir): result = testdir.runpytest("--log-level=ERROR", "--log-file=pytest.log") assert result.ret == 1 - # the capture log calls in the stdout section only contain the - # logger.error msg, because --log-level=ERROR + # The capture log calls in the stdout section only contain the + # logger.error msg, because of --log-level=ERROR. result.stdout.fnmatch_lines(["*error text going to logger*"]) - with pytest.raises(pytest.fail.Exception): - result.stdout.fnmatch_lines(["*warning text going to logger*"]) - with pytest.raises(pytest.fail.Exception): - result.stdout.fnmatch_lines(["*info text going to logger*"]) + stdout = result.stdout.str() + assert "warning text going to logger" not in stdout + assert "info text going to logger" not in stdout - # the log file should contain the warning and the error log messages and + # The log file should contain the warning and the error log messages and # not the info one, because the default level of the root logger is # WARNING. assert os.path.isfile(log_file) From 5efe6ab93c1b1ce74825d540e87e8e72b7f09a1e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:22:07 +0100 Subject: [PATCH 62/95] test_log_cli_auto_enable: get stdout once --- testing/logging/test_reporting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index c64c4cb56..f5cd2a2ce 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -635,7 +635,6 @@ def test_log_cli_auto_enable(testdir, request, cli_args): """ testdir.makepyfile( """ - import pytest import logging def test_log_1(): @@ -653,6 +652,7 @@ def test_log_cli_auto_enable(testdir, request, cli_args): ) result = testdir.runpytest(cli_args) + stdout = result.stdout.str() if cli_args == "--log-cli-level=WARNING": result.stdout.fnmatch_lines( [ @@ -663,13 +663,13 @@ def test_log_cli_auto_enable(testdir, request, cli_args): "=* 1 passed in *=", ] ) - assert "INFO" not in result.stdout.str() + assert "INFO" not in stdout else: result.stdout.fnmatch_lines( ["*test_log_cli_auto_enable*100%*", "=* 1 passed in *="] ) - assert "INFO" not in result.stdout.str() - assert "WARNING" not in result.stdout.str() + assert "INFO" not in stdout + assert "WARNING" not in stdout def test_log_file_cli(testdir): From de44293d59d6aea0fb470d14382f676fd0acec89 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 23 Mar 2019 15:18:22 +0100 Subject: [PATCH 63/95] CollectError.repr_failure: honor explicit tbstyle option --- src/_pytest/nodes.py | 9 ++++++++- testing/test_collection.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index ce02e70cd..fcaf59312 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -325,7 +325,14 @@ class Collector(Node): if excinfo.errisinstance(self.CollectError): exc = excinfo.value return str(exc.args[0]) - return self._repr_failure_py(excinfo, style="short") + + # Respect explicit tbstyle option, but default to "short" + # (None._repr_failure_py defaults to "long" without "fulltrace" option). + tbstyle = self.config.getoption("tbstyle") + if tbstyle == "auto": + tbstyle = "short" + + return self._repr_failure_py(excinfo, style=tbstyle) def _prunetraceback(self, excinfo): if hasattr(self, "fspath"): diff --git a/testing/test_collection.py b/testing/test_collection.py index 37f7ad89c..b6e0ce4ac 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -11,6 +11,7 @@ import py import pytest from _pytest.main import _in_venv +from _pytest.main import EXIT_INTERRUPTED from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import Session @@ -1234,3 +1235,20 @@ def test_collect_sub_with_symlinks(use_pkg, testdir): "*2 passed in*", ] ) + + +def test_collector_respects_tbstyle(testdir): + p1 = testdir.makepyfile("assert 0") + result = testdir.runpytest(p1, "--tb=native") + assert result.ret == EXIT_INTERRUPTED + result.stdout.fnmatch_lines( + [ + "*_ ERROR collecting test_collector_respects_tbstyle.py _*", + "Traceback (most recent call last):", + ' File "*/test_collector_respects_tbstyle.py", line 1, in ', + " assert 0", + "AssertionError: assert 0", + "*! Interrupted: 1 errors during collection !*", + "*= 1 error in *", + ] + ) From 0c63f9901665ea0713cd5bc281db853001b31e78 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Mar 2019 20:24:51 -0300 Subject: [PATCH 64/95] Add experimental _to_json and _from_json to TestReport and CollectReport This methods were moved from xdist (ca03269). Our intention is to keep this code closer to the core, given that it might break easily due to refactorings. Having it in the core might also allow to improve the code by moving some responsibility to the "code" objects (ReprEntry, etc) which are often found in the reports. Finally pytest-xdist and pytest-subtests can use those functions instead of coding it themselves. --- src/_pytest/reports.py | 132 +++++++++++++++++++++++++++++ testing/test_reports.py | 183 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 testing/test_reports.py diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 23c7cbdd9..5f0777375 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,14 @@ import py +import six from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprEntry +from _pytest._code.code import ReprEntryNative +from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import ReprFuncArgs +from _pytest._code.code import ReprLocals +from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest.outcomes import skip @@ -137,6 +145,130 @@ class BaseReport(object): fspath, lineno, domain = self.location return domain + def _to_json(self): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Returns the contents of this report as a dict of builtin entries, suitable for + serialization. + + Experimental method. + """ + + def disassembled_report(rep): + reprtraceback = rep.longrepr.reprtraceback.__dict__.copy() + reprcrash = rep.longrepr.reprcrash.__dict__.copy() + + new_entries = [] + for entry in reprtraceback["reprentries"]: + entry_data = { + "type": type(entry).__name__, + "data": entry.__dict__.copy(), + } + for key, value in entry_data["data"].items(): + if hasattr(value, "__dict__"): + entry_data["data"][key] = value.__dict__.copy() + new_entries.append(entry_data) + + reprtraceback["reprentries"] = new_entries + + return { + "reprcrash": reprcrash, + "reprtraceback": reprtraceback, + "sections": rep.longrepr.sections, + } + + d = self.__dict__.copy() + if hasattr(self.longrepr, "toterminal"): + if hasattr(self.longrepr, "reprtraceback") and hasattr( + self.longrepr, "reprcrash" + ): + d["longrepr"] = disassembled_report(self) + else: + d["longrepr"] = six.text_type(self.longrepr) + else: + d["longrepr"] = self.longrepr + for name in d: + if isinstance(d[name], py.path.local): + d[name] = str(d[name]) + elif name == "result": + d[name] = None # for now + return d + + @classmethod + def _from_json(cls, reportdict): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Factory method that returns either a TestReport or CollectReport, depending on the calling + class. It's the callers responsibility to know which class to pass here. + + Experimental method. + """ + if reportdict["longrepr"]: + if ( + "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + + reprtraceback = reportdict["longrepr"]["reprtraceback"] + reprcrash = reportdict["longrepr"]["reprcrash"] + + unserialized_entries = [] + reprentry = None + for entry_data in reprtraceback["reprentries"]: + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": + reprfuncargs = None + reprfileloc = None + reprlocals = None + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) + + reprentry = ReprEntry( + lines=data["lines"], + reprfuncargs=reprfuncargs, + reprlocals=reprlocals, + filelocrepr=reprfileloc, + style=data["style"], + ) + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) + else: + _report_unserialization_failure(entry_type, cls, reportdict) + unserialized_entries.append(reprentry) + reprtraceback["reprentries"] = unserialized_entries + + exception_info = ReprExceptionInfo( + reprtraceback=ReprTraceback(**reprtraceback), + reprcrash=ReprFileLocation(**reprcrash), + ) + + for section in reportdict["longrepr"]["sections"]: + exception_info.addsection(*section) + reportdict["longrepr"] = exception_info + + return cls(**reportdict) + + +def _report_unserialization_failure(type_name, report_class, reportdict): + from pprint import pprint + + url = "https://github.com/pytest-dev/pytest/issues" + stream = py.io.TextIO() + pprint("-" * 100, stream=stream) + pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) + pprint("report_name: %s" % report_class, stream=stream) + pprint(reportdict, stream=stream) + pprint("Please report this bug at %s" % url, stream=stream) + pprint("-" * 100, stream=stream) + assert 0, stream.getvalue() + class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if diff --git a/testing/test_reports.py b/testing/test_reports.py new file mode 100644 index 000000000..322995326 --- /dev/null +++ b/testing/test_reports.py @@ -0,0 +1,183 @@ +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +class TestReportSerialization(object): + """ + All the tests in this class came originally from test_remote.py in xdist (ca03269). + """ + + def test_xdist_longrepr_to_str_issue_241(self, testdir): + testdir.makepyfile( + """ + import os + def test_a(): assert False + def test_b(): pass + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 6 + test_a_call = reports[1] + assert test_a_call.when == "call" + assert test_a_call.outcome == "failed" + assert test_a_call._to_json()["longrepr"]["reprtraceback"]["style"] == "long" + test_b_call = reports[4] + assert test_b_call.when == "call" + assert test_b_call.outcome == "passed" + assert test_b_call._to_json()["longrepr"] is None + + def test_xdist_report_longrepr_reprcrash_130(self, testdir): + reprec = testdir.inline_runsource( + """ + import py + def test_fail(): assert False, 'Expected Message' + """ + ) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + rep = reports[1] + added_section = ("Failure Metadata", str("metadata metadata"), "*") + rep.longrepr.sections.append(added_section) + d = rep._to_json() + a = TestReport._from_json(d) + # Check assembled == rep + assert a.__dict__.keys() == rep.__dict__.keys() + for key in rep.__dict__.keys(): + if key != "longrepr": + assert getattr(a, key) == getattr(rep, key) + assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno + assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message + assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path + assert rep.longrepr.reprtraceback.entrysep == a.longrepr.reprtraceback.entrysep + assert ( + rep.longrepr.reprtraceback.extraline == a.longrepr.reprtraceback.extraline + ) + assert rep.longrepr.reprtraceback.style == a.longrepr.reprtraceback.style + assert rep.longrepr.sections == a.longrepr.sections + # Missing section attribute PR171 + assert added_section in a.longrepr.sections + + def test_reprentries_serialization_170(self, testdir): + from _pytest._code.code import ReprEntry + + reprec = testdir.inline_runsource( + """ + def test_repr_entry(): + x = 0 + assert x + """, + "--showlocals", + ) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + rep = reports[1] + d = rep._to_json() + a = TestReport._from_json(d) + + rep_entries = rep.longrepr.reprtraceback.reprentries + a_entries = a.longrepr.reprtraceback.reprentries + for i in range(len(a_entries)): + assert isinstance(rep_entries[i], ReprEntry) + assert rep_entries[i].lines == a_entries[i].lines + assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno + assert ( + rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message + ) + assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path + assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args + assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines + assert rep_entries[i].style == a_entries[i].style + + def test_reprentries_serialization_196(self, testdir): + from _pytest._code.code import ReprEntryNative + + reprec = testdir.inline_runsource( + """ + def test_repr_entry_native(): + x = 0 + assert x + """, + "--tb=native", + ) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + rep = reports[1] + d = rep._to_json() + a = TestReport._from_json(d) + + rep_entries = rep.longrepr.reprtraceback.reprentries + a_entries = a.longrepr.reprtraceback.reprentries + for i in range(len(a_entries)): + assert isinstance(rep_entries[i], ReprEntryNative) + assert rep_entries[i].lines == a_entries[i].lines + + def test_itemreport_outcomes(self, testdir): + reprec = testdir.inline_runsource( + """ + import py + def test_pass(): pass + def test_fail(): 0/0 + @py.test.mark.skipif("True") + def test_skip(): pass + def test_skip_imperative(): + py.test.skip("hello") + @py.test.mark.xfail("True") + def test_xfail(): 0/0 + def test_xfail_imperative(): + py.test.xfail("hello") + """ + ) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 17 # with setup/teardown "passed" reports + for rep in reports: + d = rep._to_json() + newrep = TestReport._from_json(d) + assert newrep.passed == rep.passed + assert newrep.failed == rep.failed + assert newrep.skipped == rep.skipped + if newrep.skipped and not hasattr(newrep, "wasxfail"): + assert len(newrep.longrepr) == 3 + assert newrep.outcome == rep.outcome + assert newrep.when == rep.when + assert newrep.keywords == rep.keywords + if rep.failed: + assert newrep.longreprtext == rep.longreprtext + + def test_collectreport_passed(self, testdir): + reprec = testdir.inline_runsource("def test_func(): pass") + reports = reprec.getreports("pytest_collectreport") + for rep in reports: + d = rep._to_json() + newrep = CollectReport._from_json(d) + assert newrep.passed == rep.passed + assert newrep.failed == rep.failed + assert newrep.skipped == rep.skipped + + def test_collectreport_fail(self, testdir): + reprec = testdir.inline_runsource("qwe abc") + reports = reprec.getreports("pytest_collectreport") + assert reports + for rep in reports: + d = rep._to_json() + newrep = CollectReport._from_json(d) + assert newrep.passed == rep.passed + assert newrep.failed == rep.failed + assert newrep.skipped == rep.skipped + if rep.failed: + assert newrep.longrepr == str(rep.longrepr) + + def test_extended_report_deserialization(self, testdir): + reprec = testdir.inline_runsource("qwe abc") + reports = reprec.getreports("pytest_collectreport") + assert reports + for rep in reports: + rep.extra = True + d = rep._to_json() + newrep = CollectReport._from_json(d) + assert newrep.extra + assert newrep.passed == rep.passed + assert newrep.failed == rep.failed + assert newrep.skipped == rep.skipped + if rep.failed: + assert newrep.longrepr == str(rep.longrepr) From 7b9a41452476132edac9d142a9165dfd4f75a762 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Mar 2019 20:45:15 -0300 Subject: [PATCH 65/95] Add pytest_report_serialize and pytest_report_unserialize hooks These hooks will be used by pytest-xdist and pytest-subtests to serialize and customize reports. --- src/_pytest/config/__init__.py | 1 + src/_pytest/hookspec.py | 35 ++++++++++++++++++++++++ src/_pytest/reports.py | 16 +++++++++++ testing/test_reports.py | 50 ++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ca2bebabc..4ed9deac4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -140,6 +140,7 @@ default_plugins = ( "stepwise", "warnings", "logging", + "reports", ) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 0641e3bc5..a465923f9 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -375,6 +375,41 @@ def pytest_runtest_logreport(report): the respective phase of executing a test. """ +@hookspec(firstresult=True) +def pytest_report_serialize(config, report): + """ + .. warning:: + This hook is experimental and subject to change between pytest releases, even + bug fixes. + + The intent is for this to be used by plugins maintained by the core-devs, such + as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal + 'resultlog' plugin. + + In the future it might become part of the public hook API. + + Serializes the given report object into a data structure suitable for sending + over the wire, or converted to JSON. + """ + + +@hookspec(firstresult=True) +def pytest_report_unserialize(config, data): + """ + .. warning:: + This hook is experimental and subject to change between pytest releases, even + bug fixes. + + The intent is for this to be used by plugins maintained by the core-devs, such + as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal + 'resultlog' plugin. + + In the future it might become part of the public hook API. + + Restores a report object previously serialized with pytest_report_serialize().; + """ + + # ------------------------------------------------------------------------- # Fixture related hooks # ------------------------------------------------------------------------- diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 5f0777375..b4160eb96 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -404,3 +404,19 @@ class CollectErrorRepr(TerminalRepr): def toterminal(self, out): out.line(self.longrepr, red=True) + + +def pytest_report_serialize(report): + if isinstance(report, (TestReport, CollectReport)): + data = report._to_json() + data["_report_type"] = report.__class__.__name__ + return data + + +def pytest_report_unserialize(data): + if "_report_type" in data: + if data["_report_type"] == "TestReport": + return TestReport._from_json(data) + elif data["_report_type"] == "CollectReport": + return CollectReport._from_json(data) + assert "Unknown report_type unserialize data: {}".format(data["_report_type"]) diff --git a/testing/test_reports.py b/testing/test_reports.py index 322995326..3413a805b 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -181,3 +181,53 @@ class TestReportSerialization(object): assert newrep.skipped == rep.skipped if rep.failed: assert newrep.longrepr == str(rep.longrepr) + + +class TestHooks: + """Test that the hooks are working correctly for plugins""" + + def test_test_report(self, testdir, pytestconfig): + testdir.makepyfile( + """ + import os + def test_a(): assert False + def test_b(): pass + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 6 + for rep in reports: + data = pytestconfig.hook.pytest_report_serialize( + config=pytestconfig, report=rep + ) + assert data["_report_type"] == "TestReport" + new_rep = pytestconfig.hook.pytest_report_unserialize( + config=pytestconfig, data=data + ) + assert new_rep.nodeid == rep.nodeid + assert new_rep.when == rep.when + assert new_rep.outcome == rep.outcome + + def test_collect_report(self, testdir, pytestconfig): + testdir.makepyfile( + """ + import os + def test_a(): assert False + def test_b(): pass + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports("pytest_collectreport") + assert len(reports) == 2 + for rep in reports: + data = pytestconfig.hook.pytest_report_serialize( + config=pytestconfig, report=rep + ) + assert data["_report_type"] == "CollectReport" + new_rep = pytestconfig.hook.pytest_report_unserialize( + config=pytestconfig, data=data + ) + assert new_rep.nodeid == rep.nodeid + assert new_rep.when == "collect" + assert new_rep.outcome == rep.outcome From d856f4e51fac6be6989659f34702a533becc1a91 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 20 Mar 2019 21:05:12 -0300 Subject: [PATCH 66/95] Make sure TestReports are not collected as test classes --- src/_pytest/reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index b4160eb96..04cc52691 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -275,6 +275,8 @@ class TestReport(BaseReport): they fail). """ + __test__ = False + def __init__( self, nodeid, From f2e0c740d39259b6eff3fa58971d3509378b1366 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2019 18:39:12 -0300 Subject: [PATCH 67/95] Code review suggestions --- src/_pytest/hookspec.py | 4 ++-- src/_pytest/reports.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index a465923f9..afcee5397 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -389,7 +389,7 @@ def pytest_report_serialize(config, report): In the future it might become part of the public hook API. Serializes the given report object into a data structure suitable for sending - over the wire, or converted to JSON. + over the wire, e.g. converted to JSON. """ @@ -406,7 +406,7 @@ def pytest_report_unserialize(config, data): In the future it might become part of the public hook API. - Restores a report object previously serialized with pytest_report_serialize().; + Restores a report object previously serialized with pytest_report_serialize(). """ diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 04cc52691..74faa3f8c 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -267,7 +267,7 @@ def _report_unserialization_failure(type_name, report_class, reportdict): pprint(reportdict, stream=stream) pprint("Please report this bug at %s" % url, stream=stream) pprint("-" * 100, stream=stream) - assert 0, stream.getvalue() + raise RuntimeError(stream.getvalue()) class TestReport(BaseReport): From 645774295fcd913c15cfa6d37c31ee56cd58509f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2019 18:44:08 -0300 Subject: [PATCH 68/95] Add CHANGELOG --- changelog/4965.trivial.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/4965.trivial.rst diff --git a/changelog/4965.trivial.rst b/changelog/4965.trivial.rst new file mode 100644 index 000000000..487a22c7a --- /dev/null +++ b/changelog/4965.trivial.rst @@ -0,0 +1,9 @@ +New ``pytest_report_serialize`` and ``pytest_report_unserialize`` **experimental** hooks. + +These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for +resultlog to serialize and customize reports. + +They are experimental, meaning that their details might change or even be removed +completely in future patch releases without warning. + +Feedback is welcome from plugin authors and users alike. From e4eec3416ad382f8b23acf75435a89a594bcefa6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Mar 2019 19:12:02 -0300 Subject: [PATCH 69/95] Note that tests from xdist reference the correct xdist issues --- testing/test_reports.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/testing/test_reports.py b/testing/test_reports.py index 3413a805b..f37ead893 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -3,11 +3,12 @@ from _pytest.reports import TestReport class TestReportSerialization(object): - """ - All the tests in this class came originally from test_remote.py in xdist (ca03269). - """ - def test_xdist_longrepr_to_str_issue_241(self, testdir): + """ + Regarding issue pytest-xdist#241 + + This test came originally from test_remote.py in xdist (ca03269). + """ testdir.makepyfile( """ import os @@ -28,6 +29,10 @@ class TestReportSerialization(object): assert test_b_call._to_json()["longrepr"] is None def test_xdist_report_longrepr_reprcrash_130(self, testdir): + """Regarding issue pytest-xdist#130 + + This test came originally from test_remote.py in xdist (ca03269). + """ reprec = testdir.inline_runsource( """ import py @@ -59,6 +64,10 @@ class TestReportSerialization(object): assert added_section in a.longrepr.sections def test_reprentries_serialization_170(self, testdir): + """Regarding issue pytest-xdist#170 + + This test came originally from test_remote.py in xdist (ca03269). + """ from _pytest._code.code import ReprEntry reprec = testdir.inline_runsource( @@ -90,6 +99,10 @@ class TestReportSerialization(object): assert rep_entries[i].style == a_entries[i].style def test_reprentries_serialization_196(self, testdir): + """Regarding issue pytest-xdist#196 + + This test came originally from test_remote.py in xdist (ca03269). + """ from _pytest._code.code import ReprEntryNative reprec = testdir.inline_runsource( @@ -113,6 +126,9 @@ class TestReportSerialization(object): assert rep_entries[i].lines == a_entries[i].lines def test_itemreport_outcomes(self, testdir): + """ + This test came originally from test_remote.py in xdist (ca03269). + """ reprec = testdir.inline_runsource( """ import py @@ -145,6 +161,7 @@ class TestReportSerialization(object): assert newrep.longreprtext == rep.longreprtext def test_collectreport_passed(self, testdir): + """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("def test_func(): pass") reports = reprec.getreports("pytest_collectreport") for rep in reports: @@ -155,6 +172,7 @@ class TestReportSerialization(object): assert newrep.skipped == rep.skipped def test_collectreport_fail(self, testdir): + """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports @@ -168,6 +186,7 @@ class TestReportSerialization(object): assert newrep.longrepr == str(rep.longrepr) def test_extended_report_deserialization(self, testdir): + """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports From ceef0af1aea4c8db3b8670a2ff4f127a56028bb4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Mar 2019 19:19:59 -0300 Subject: [PATCH 70/95] Improve coverage for to_json() with paths in reports --- src/_pytest/reports.py | 3 ++- testing/test_reports.py | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 74faa3f8c..8ab42478f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -11,6 +11,7 @@ from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest.outcomes import skip +from _pytest.pathlib import Path def getslaveinfoline(node): @@ -189,7 +190,7 @@ class BaseReport(object): else: d["longrepr"] = self.longrepr for name in d: - if isinstance(d[name], py.path.local): + if isinstance(d[name], (py.path.local, Path)): d[name] = str(d[name]) elif name == "result": d[name] = None # for now diff --git a/testing/test_reports.py b/testing/test_reports.py index f37ead893..2f9162e10 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,3 +1,4 @@ +from _pytest.pathlib import Path from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -11,7 +12,6 @@ class TestReportSerialization(object): """ testdir.makepyfile( """ - import os def test_a(): assert False def test_b(): pass """ @@ -35,8 +35,8 @@ class TestReportSerialization(object): """ reprec = testdir.inline_runsource( """ - import py - def test_fail(): assert False, 'Expected Message' + def test_fail(): + assert False, 'Expected Message' """ ) reports = reprec.getreports("pytest_runtest_logreport") @@ -201,6 +201,24 @@ class TestReportSerialization(object): if rep.failed: assert newrep.longrepr == str(rep.longrepr) + def test_paths_support(self, testdir): + """Report attributes which are py.path or pathlib objects should become strings.""" + testdir.makepyfile( + """ + def test_a(): + assert False + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + test_a_call = reports[1] + test_a_call.path1 = testdir.tmpdir + test_a_call.path2 = Path(testdir.tmpdir) + data = test_a_call._to_json() + assert data["path1"] == str(testdir.tmpdir) + assert data["path2"] == str(testdir.tmpdir) + class TestHooks: """Test that the hooks are working correctly for plugins""" From 2d77018d1b0b3c1c291bacf48885321c8c87048f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 25 Mar 2019 19:26:40 -0300 Subject: [PATCH 71/95] Improve coverage for _report_unserialization_failure --- src/_pytest/reports.py | 4 ++-- testing/test_reports.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8ab42478f..cd7a0b57f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,3 +1,5 @@ +from pprint import pprint + import py import six @@ -258,8 +260,6 @@ class BaseReport(object): def _report_unserialization_failure(type_name, report_class, reportdict): - from pprint import pprint - url = "https://github.com/pytest-dev/pytest/issues" stream = py.io.TextIO() pprint("-" * 100, stream=stream) diff --git a/testing/test_reports.py b/testing/test_reports.py index 2f9162e10..879a9098d 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,3 +1,4 @@ +import pytest from _pytest.pathlib import Path from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -219,6 +220,28 @@ class TestReportSerialization(object): assert data["path1"] == str(testdir.tmpdir) assert data["path2"] == str(testdir.tmpdir) + def test_unserialization_failure(self, testdir): + """Check handling of failure during unserialization of report types.""" + testdir.makepyfile( + """ + def test_a(): + assert False + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + test_a_call = reports[1] + data = test_a_call._to_json() + entry = data["longrepr"]["reprtraceback"]["reprentries"][0] + assert entry["type"] == "ReprEntry" + + entry["type"] = "Unknown" + with pytest.raises( + RuntimeError, match="INTERNALERROR: Unknown entry type returned: Unknown" + ): + TestReport._from_json(data) + class TestHooks: """Test that the hooks are working correctly for plugins""" From b18df936eaf4467f20386946bfacc07a4a3256bc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 26 Mar 2019 10:06:11 +0100 Subject: [PATCH 72/95] changelog --- changelog/4987.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4987.trivial.rst diff --git a/changelog/4987.trivial.rst b/changelog/4987.trivial.rst new file mode 100644 index 000000000..eb79b742a --- /dev/null +++ b/changelog/4987.trivial.rst @@ -0,0 +1 @@ +``Collector.repr_failure`` respects ``--tbstyle``, but only defaults to ``short`` now (with ``auto``). From 23146e752725e67878c8400eedb3670ab9a7a996 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 07:45:43 +0100 Subject: [PATCH 73/95] Fix usages of "verbose" option With `-qq` `bool(config.getoption("verbose"))` is True; it needs to be checked for `> 0`. --- changelog/4975.bugfix.rst | 1 + doc/en/builtin.rst | 2 +- src/_pytest/assertion/util.py | 18 +++++++++--------- src/_pytest/cacheprovider.py | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/logging.py | 2 +- testing/test_terminal.py | 19 ++++++++++--------- 7 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 changelog/4975.bugfix.rst diff --git a/changelog/4975.bugfix.rst b/changelog/4975.bugfix.rst new file mode 100644 index 000000000..26c93ec18 --- /dev/null +++ b/changelog/4975.bugfix.rst @@ -0,0 +1 @@ +Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead. diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index a40dfc223..8d6a06a44 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -55,7 +55,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Example:: def test_foo(pytestconfig): - if pytestconfig.getoption("verbose"): + if pytestconfig.getoption("verbose") > 0: ... record_property Add an extra properties the calling test. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 6326dddbd..c7a46cba0 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -151,7 +151,7 @@ def assertrepr_compare(config, op, left, right): elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): type_fn = (isdatacls, isattrs) explanation = _compare_eq_cls(left, right, verbose, type_fn) - elif verbose: + elif verbose > 0: explanation = _compare_eq_verbose(left, right) if isiterable(left) and isiterable(right): expl = _compare_eq_iterable(left, right, verbose) @@ -175,8 +175,8 @@ def assertrepr_compare(config, op, left, right): return [summary] + explanation -def _diff_text(left, right, verbose=False): - """Return the explanation for the diff between text or bytes +def _diff_text(left, right, verbose=0): + """Return the explanation for the diff between text or bytes. Unless --verbose is used this will skip leading and trailing characters which are identical to keep the diff minimal. @@ -202,7 +202,7 @@ def _diff_text(left, right, verbose=False): left = escape_for_readable_diff(left) if isinstance(right, bytes): right = escape_for_readable_diff(right) - if not verbose: + if verbose < 1: i = 0 # just in case left or right has zero length for i in range(min(len(left), len(right))): if left[i] != right[i]: @@ -250,7 +250,7 @@ def _compare_eq_verbose(left, right): return explanation -def _compare_eq_iterable(left, right, verbose=False): +def _compare_eq_iterable(left, right, verbose=0): if not verbose: return [u"Use -v to get the full diff"] # dynamic import to speedup pytest @@ -273,7 +273,7 @@ def _compare_eq_iterable(left, right, verbose=False): return explanation -def _compare_eq_sequence(left, right, verbose=False): +def _compare_eq_sequence(left, right, verbose=0): explanation = [] for i in range(min(len(left), len(right))): if left[i] != right[i]: @@ -292,7 +292,7 @@ def _compare_eq_sequence(left, right, verbose=False): return explanation -def _compare_eq_set(left, right, verbose=False): +def _compare_eq_set(left, right, verbose=0): explanation = [] diff_left = left - right diff_right = right - left @@ -307,7 +307,7 @@ def _compare_eq_set(left, right, verbose=False): return explanation -def _compare_eq_dict(left, right, verbose=False): +def _compare_eq_dict(left, right, verbose=0): explanation = [] common = set(left).intersection(set(right)) same = {k: left[k] for k in common if left[k] == right[k]} @@ -368,7 +368,7 @@ def _compare_eq_cls(left, right, verbose, type_fns): return explanation -def _notin_text(term, text, verbose=False): +def _notin_text(term, text, verbose=0): index = text.find(term) head = text[:index] tail = text[index + len(term) :] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index ebba0f935..ef2039539 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -340,7 +340,7 @@ def cache(request): def pytest_report_header(config): """Display cachedir with --cache-show and if non-default.""" - if config.option.verbose or config.getini("cache_dir") != ".pytest_cache": + if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": cachedir = config.cache._cachedir # TODO: evaluate generating upward relative paths # starting with .., ../.. if sensible diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 2635d095e..d3223a74e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1065,7 +1065,7 @@ def pytestconfig(request): Example:: def test_foo(pytestconfig): - if pytestconfig.getoption("verbose"): + if pytestconfig.getoption("verbose") > 0: ... """ diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 22db44301..e395bc435 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -389,7 +389,7 @@ class LoggingPlugin(object): self._config = config # enable verbose output automatically if live logging is enabled - if self._log_cli_enabled() and not config.getoption("verbose"): + if self._log_cli_enabled() and config.getoption("verbose") < 1: config.option.verbose = 1 self.print_logs = get_option_ini(config, "log_print") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1f3fff8c5..72af2fa00 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -25,15 +25,14 @@ DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) class Option(object): - def __init__(self, verbose=False, fulltrace=False): - self.verbose = verbose + def __init__(self, verbosity=0, fulltrace=False): + self.verbosity = verbosity self.fulltrace = fulltrace @property def args(self): values = [] - if self.verbose: - values.append("-v") + values.append("--verbosity=%d" % self.verbosity) if self.fulltrace: values.append("--fulltrace") return values @@ -41,9 +40,9 @@ class Option(object): @pytest.fixture( params=[ - Option(verbose=False), - Option(verbose=True), - Option(verbose=-1), + Option(verbosity=0), + Option(verbosity=1), + Option(verbosity=-1), Option(fulltrace=True), ], ids=["default", "verbose", "quiet", "fulltrace"], @@ -87,7 +86,7 @@ class TestTerminal(object): """ ) result = testdir.runpytest(*option.args) - if option.verbose: + if option.verbosity > 0: result.stdout.fnmatch_lines( [ "*test_pass_skip_fail.py::test_ok PASS*", @@ -95,8 +94,10 @@ class TestTerminal(object): "*test_pass_skip_fail.py::test_func FAIL*", ] ) - else: + elif option.verbosity == 0: result.stdout.fnmatch_lines(["*test_pass_skip_fail.py .sF*"]) + else: + result.stdout.fnmatch_lines([".sF*"]) result.stdout.fnmatch_lines( [" def test_func():", "> assert 0", "E assert 0"] ) From 0d00be4f4f54aaef99ef94ca40dfb7b21ca16a0b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 22 Mar 2019 12:44:32 +0100 Subject: [PATCH 74/95] Do not swallow outcomes.Exit in assertrepr_compare --- changelog/4978.bugfix.rst | 1 + src/_pytest/assertion/util.py | 60 ++++++++++++++++++++--------------- testing/test_assertion.py | 11 +++++++ 3 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 changelog/4978.bugfix.rst diff --git a/changelog/4978.bugfix.rst b/changelog/4978.bugfix.rst new file mode 100644 index 000000000..259daa8da --- /dev/null +++ b/changelog/4978.bugfix.rst @@ -0,0 +1 @@ +``outcomes.Exit`` is not swallowed in ``assertrepr_compare`` anymore. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 6326dddbd..fef044873 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -9,6 +9,7 @@ import six import _pytest._code from ..compat import Sequence +from _pytest import outcomes from _pytest._io.saferepr import saferepr # The _reprcompare attribute on the util module is used by the new assertion @@ -102,6 +103,38 @@ except NameError: basestring = str +def issequence(x): + return isinstance(x, Sequence) and not isinstance(x, basestring) + + +def istext(x): + return isinstance(x, basestring) + + +def isdict(x): + return isinstance(x, dict) + + +def isset(x): + return isinstance(x, (set, frozenset)) + + +def isdatacls(obj): + return getattr(obj, "__dataclass_fields__", None) is not None + + +def isattrs(obj): + return getattr(obj, "__attrs_attrs__", None) is not None + + +def isiterable(obj): + try: + iter(obj) + return not istext(obj) + except TypeError: + return False + + def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op @@ -110,31 +143,6 @@ def assertrepr_compare(config, op, left, right): summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) - def issequence(x): - return isinstance(x, Sequence) and not isinstance(x, basestring) - - def istext(x): - return isinstance(x, basestring) - - def isdict(x): - return isinstance(x, dict) - - def isset(x): - return isinstance(x, (set, frozenset)) - - def isdatacls(obj): - return getattr(obj, "__dataclass_fields__", None) is not None - - def isattrs(obj): - return getattr(obj, "__attrs_attrs__", None) is not None - - def isiterable(obj): - try: - iter(obj) - return not istext(obj) - except TypeError: - return False - verbose = config.getoption("verbose") explanation = None try: @@ -162,6 +170,8 @@ def assertrepr_compare(config, op, left, right): elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) + except outcomes.Exit: + raise except Exception: explanation = [ u"(pytest_assertion plugin: representation of details failed. " diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e4fe56c6f..330b711af 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -11,6 +11,7 @@ import six import _pytest.assertion as plugin import pytest +from _pytest import outcomes from _pytest.assertion import truncate from _pytest.assertion import util @@ -1305,3 +1306,13 @@ def test_issue_1944(testdir): "AttributeError: 'Module' object has no attribute '_obj'" not in result.stdout.str() ) + + +def test_exit_from_assertrepr_compare(monkeypatch): + def raise_exit(obj): + outcomes.exit("Quitting debugger") + + monkeypatch.setattr(util, "istext", raise_exit) + + with pytest.raises(outcomes.Exit, match="Quitting debugger"): + callequal(1, 1) From 94a2e3dddc51b5dc216afc8c1c8587098467ea75 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 24 Mar 2019 11:13:10 +0100 Subject: [PATCH 75/95] stepwise: report status via pytest_report_collectionfinish --- changelog/4993.feature.rst | 1 + src/_pytest/stepwise.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 changelog/4993.feature.rst diff --git a/changelog/4993.feature.rst b/changelog/4993.feature.rst new file mode 100644 index 000000000..b8e1ff494 --- /dev/null +++ b/changelog/4993.feature.rst @@ -0,0 +1 @@ +The stepwise plugin reports status information now. diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 1efa2e7ca..68e53a31c 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -8,7 +8,7 @@ def pytest_addoption(parser): "--stepwise", action="store_true", dest="stepwise", - help="exit on test fail and continue from last failing test next time", + help="exit on test failure and continue from last failing test next time", ) group.addoption( "--stepwise-skip", @@ -37,7 +37,10 @@ class StepwisePlugin: self.session = session def pytest_collection_modifyitems(self, session, config, items): - if not self.active or not self.lastfailed: + if not self.active: + return + if not self.lastfailed: + self.report_status = "no previously failed tests, not skipping." return already_passed = [] @@ -54,7 +57,12 @@ class StepwisePlugin: # If the previously failed test was not found among the test items, # do not skip any tests. if not found: + self.report_status = "previously failed test not found, not skipping." already_passed = [] + else: + self.report_status = "skipping {} already passed items.".format( + len(already_passed) + ) for item in already_passed: items.remove(item) @@ -94,6 +102,10 @@ class StepwisePlugin: if report.nodeid == self.lastfailed: self.lastfailed = None + def pytest_report_collectionfinish(self): + if self.active and self.config.getoption("verbose") >= 0: + return "stepwise: %s" % self.report_status + def pytest_sessionfinish(self, session): if self.active: self.config.cache.set("cache/stepwise", self.lastfailed) From 351529cb500ba4a2e9f096b28153d1173807cb7b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 26 Mar 2019 16:27:20 +0100 Subject: [PATCH 76/95] skipping: factor out _get_pos, pass only config to _get_report_str --- src/_pytest/skipping.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index a28e95eda..22acafbdd 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -207,20 +207,22 @@ def pytest_terminal_summary(terminalreporter): def show_simple(terminalreporter, lines, stat): failed = terminalreporter.stats.get(stat) if failed: + config = terminalreporter.config for rep in failed: - verbose_word = _get_report_str(terminalreporter, rep) - pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) lines.append("%s %s" % (verbose_word, pos)) def show_xfailed(terminalreporter, lines): xfailed = terminalreporter.stats.get("xfailed") if xfailed: + config = terminalreporter.config for rep in xfailed: - verbose_word = _get_report_str(terminalreporter, rep) - pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) - reason = rep.wasxfail + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) lines.append("%s %s" % (verbose_word, pos)) + reason = rep.wasxfail if reason: lines.append(" " + str(reason)) @@ -228,9 +230,10 @@ def show_xfailed(terminalreporter, lines): def show_xpassed(terminalreporter, lines): xpassed = terminalreporter.stats.get("xpassed") if xpassed: + config = terminalreporter.config for rep in xpassed: - verbose_word = _get_report_str(terminalreporter, rep) - pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid) + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) reason = rep.wasxfail lines.append("%s %s %s" % (verbose_word, pos, reason)) @@ -261,9 +264,9 @@ def show_skipped(terminalreporter, lines): tr = terminalreporter skipped = tr.stats.get("skipped", []) if skipped: - verbose_word = _get_report_str(terminalreporter, report=skipped[0]) fskips = folded_skips(skipped) if fskips: + verbose_word = _get_report_str(terminalreporter.config, report=skipped[0]) for num, fspath, lineno, reason in fskips: if reason.startswith("Skipped: "): reason = reason[9:] @@ -283,13 +286,18 @@ def shower(stat): return show_ -def _get_report_str(terminalreporter, report): - _category, _short, verbose = terminalreporter.config.hook.pytest_report_teststatus( - report=report, config=terminalreporter.config +def _get_report_str(config, report): + _category, _short, verbose = config.hook.pytest_report_teststatus( + report=report, config=config ) return verbose +def _get_pos(config, rep): + nodeid = config.cwd_relative_nodeid(rep.nodeid) + return nodeid + + REPORTCHAR_ACTIONS = { "x": show_xfailed, "X": show_xpassed, From 9311d822c7718eadd6167f1dcd67d00ec47151bd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Mar 2019 12:47:31 -0300 Subject: [PATCH 77/95] Fix assertion in pytest_report_unserialize --- src/_pytest/reports.py | 4 +++- testing/test_reports.py | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index cd7a0b57f..679d72c9d 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -422,4 +422,6 @@ def pytest_report_unserialize(data): return TestReport._from_json(data) elif data["_report_type"] == "CollectReport": return CollectReport._from_json(data) - assert "Unknown report_type unserialize data: {}".format(data["_report_type"]) + assert False, "Unknown report_type unserialize data: {}".format( + data["_report_type"] + ) diff --git a/testing/test_reports.py b/testing/test_reports.py index 879a9098d..22d5fce34 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -249,7 +249,6 @@ class TestHooks: def test_test_report(self, testdir, pytestconfig): testdir.makepyfile( """ - import os def test_a(): assert False def test_b(): pass """ @@ -272,7 +271,6 @@ class TestHooks: def test_collect_report(self, testdir, pytestconfig): testdir.makepyfile( """ - import os def test_a(): assert False def test_b(): pass """ @@ -291,3 +289,25 @@ class TestHooks: assert new_rep.nodeid == rep.nodeid assert new_rep.when == "collect" assert new_rep.outcome == rep.outcome + + @pytest.mark.parametrize( + "hook_name", ["pytest_runtest_logreport", "pytest_collectreport"] + ) + def test_invalid_report_types(self, testdir, pytestconfig, hook_name): + testdir.makepyfile( + """ + def test_a(): pass + """ + ) + reprec = testdir.inline_run() + reports = reprec.getreports(hook_name) + assert reports + rep = reports[0] + data = pytestconfig.hook.pytest_report_serialize( + config=pytestconfig, report=rep + ) + data["_report_type"] = "Unknown" + with pytest.raises(AssertionError): + _ = pytestconfig.hook.pytest_report_unserialize( + config=pytestconfig, data=data + ) From 538efef1baafff948ad43bf7adeae62c2dfc88bc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 23 Mar 2019 09:16:14 +0100 Subject: [PATCH 78/95] logging: close log_file_handler While it should be closed in logging's shutdown [1], the following would still issue a ResourceWarning: ``` import logging log_file_handler = logging.FileHandler("temp.log", mode="w", encoding="UTF-8") root_logger = logging.getLogger() root_logger.addHandler(log_file_handler) root_logger.removeHandler(log_file_handler) root_logger.error("error") del log_file_handler ``` It looks like the weakref might get lost for some reason. See https://github.com/pytest-dev/pytest/pull/4981/commits/92ffe42b45 / #4981 for more information. 1: https://github.com/python/cpython/blob/c1419578a18d787393c7ccee149e7c1fff17a99e/Lib/logging/__init__.py#L2107-L2139 --- changelog/4988.bugfix.rst | 1 + src/_pytest/logging.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog/4988.bugfix.rst diff --git a/changelog/4988.bugfix.rst b/changelog/4988.bugfix.rst new file mode 100644 index 000000000..8cc816ed6 --- /dev/null +++ b/changelog/4988.bugfix.rst @@ -0,0 +1 @@ +Close logging's file handler explicitly when the session finishes. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 22db44301..5a31dfc5c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -577,8 +577,15 @@ class LoggingPlugin(object): if self.log_cli_handler: self.log_cli_handler.set_when("sessionfinish") if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + try: + with catching_logs( + self.log_file_handler, level=self.log_file_level + ): + yield + finally: + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() else: yield From 52730f633042eccb487aa7d7c8546fd4d963b727 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 26 Mar 2019 18:32:15 +0100 Subject: [PATCH 79/95] doc: fix note about output capturing with pdb [skip travis] --- doc/en/usage.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 523608569..51511673a 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -384,10 +384,8 @@ in your code and pytest automatically disables its output capture for that test: * Output capture in other tests is not affected. * Any prior test output that has already been captured and will be processed as such. -* Any later output produced within the same test will not be captured and will - instead get sent directly to ``sys.stdout``. Note that this holds true even - for test output occurring after you exit the interactive PDB_ tracing session - and continue with the regular test run. +* Output capture gets resumed when ending the debugger session (via the + ``continue`` command). .. _`breakpoint-builtin`: From cf6e2ceafd8cd6d295d4d6eb379558f8b9f10db0 Mon Sep 17 00:00:00 2001 From: ApaDoctor Date: Wed, 11 Oct 2017 18:11:50 +0200 Subject: [PATCH 80/95] add ini option to disable string escape for parametrization --- AUTHORS | 1 + changelog/2482.feature | 1 + doc/en/parametrize.rst | 15 +++++++++++++++ src/_pytest/python.py | 19 ++++++++++++++++++- 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 changelog/2482.feature diff --git a/AUTHORS b/AUTHORS index d6f6ce828..ea6fc5cac 100644 --- a/AUTHORS +++ b/AUTHORS @@ -242,6 +242,7 @@ Vidar T. Fauske Virgil Dupras Vitaly Lashmanov Vlad Dragos +Volodymyr Piskun Wil Cooley William Lee Wim Glenn diff --git a/changelog/2482.feature b/changelog/2482.feature new file mode 100644 index 000000000..37d5138bf --- /dev/null +++ b/changelog/2482.feature @@ -0,0 +1 @@ +Include new ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option to disable ascii-escaping in parametrized values. This may cause a series of problems and as the name makes clear, use at your own risk. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 9a237dcd7..912ae1898 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -81,6 +81,21 @@ them in turn: test_expectation.py:8: AssertionError ==================== 1 failed, 2 passed in 0.12 seconds ==================== +.. note:: + + pytest by default escapes any non-ascii characters used in unicode strings + for the parametrization because it has several downsides. + If however you would like to use unicode strings in parametrization, use this option in your ``pytest.ini``: + + .. code-block:: ini + + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True + + to disable this behavior, but keep in mind that this might cause unwanted side effects and + even bugs depending on the OS used and plugins currently installed, so use it at your own risk. + + As designed in this example, only one pair of input/output values fails the simple test function. And as usual with test function arguments, you can see the ``input`` and ``output`` values in the traceback. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 5b289c7c8..8532837a5 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -102,6 +102,13 @@ def pytest_addoption(parser): default=["test"], help="prefixes or glob names for Python test function and method discovery", ) + parser.addini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support", + type="bool", + default=False, + help="disable string escape non-ascii characters, might cause unwanted " + "side effects(use at your own risk)", + ) group.addoption( "--import-mode", @@ -1156,6 +1163,16 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" +def _disable_escaping(val, config=None): + if config is None: + escape_option = False + else: + escape_option = config.getini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" + ) + return val if escape_option else ascii_escaped(val) + + def _idval(val, argname, idx, idfn, item, config): if idfn: try: @@ -1177,7 +1194,7 @@ def _idval(val, argname, idx, idfn, item, config): return hook_id if isinstance(val, STRING_TYPES): - return ascii_escaped(val) + return _disable_escaping(val) elif isinstance(val, (float, int, bool, NoneType)): return str(val) elif isinstance(val, REGEX_TYPE): From 8b0b7156d90ed7e3872f7da2f979665d6d47cf0e Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 24 Mar 2019 20:44:48 +0900 Subject: [PATCH 81/95] Fix glitches of original patch of disable-test-id-escaping --- changelog/{2482.feature => 2482.feature.rst} | 0 src/_pytest/python.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename changelog/{2482.feature => 2482.feature.rst} (100%) diff --git a/changelog/2482.feature b/changelog/2482.feature.rst similarity index 100% rename from changelog/2482.feature rename to changelog/2482.feature.rst diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8532837a5..9e4392264 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1163,7 +1163,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" -def _disable_escaping(val, config=None): +def _ascii_escaped_by_config(val, config): if config is None: escape_option = False else: @@ -1194,7 +1194,7 @@ def _idval(val, argname, idx, idfn, item, config): return hook_id if isinstance(val, STRING_TYPES): - return _disable_escaping(val) + return _ascii_escaped_by_config(val, config) elif isinstance(val, (float, int, bool, NoneType)): return str(val) elif isinstance(val, REGEX_TYPE): From 3d9e68ecfd05dff5efd62627cdbb858f9bc63ec7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 28 Mar 2019 00:05:33 +0900 Subject: [PATCH 82/95] Update doc/en/parametrize.rst --- doc/en/parametrize.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 912ae1898..70a35ac44 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -85,14 +85,14 @@ them in turn: pytest by default escapes any non-ascii characters used in unicode strings for the parametrization because it has several downsides. - If however you would like to use unicode strings in parametrization, use this option in your ``pytest.ini``: + If however you would like to use unicode strings in parametrization and see them in the terminal as is (non-escaped), use this option in your ``pytest.ini``: .. code-block:: ini [pytest] disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True - to disable this behavior, but keep in mind that this might cause unwanted side effects and + Keep in mind however that this might cause unwanted side effects and even bugs depending on the OS used and plugins currently installed, so use it at your own risk. From 76c70cbf4c040be6fd96aade184cce540f4a4761 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 26 Mar 2019 16:37:26 +0100 Subject: [PATCH 83/95] Fix off-by-one error with lineno in mark collection error --- changelog/5003.bugfix.rst | 1 + src/_pytest/_code/source.py | 4 +++- src/_pytest/mark/structures.py | 2 +- testing/test_mark.py | 35 ++++++++++++++++++++++++---------- 4 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 changelog/5003.bugfix.rst diff --git a/changelog/5003.bugfix.rst b/changelog/5003.bugfix.rst new file mode 100644 index 000000000..8d18a50e6 --- /dev/null +++ b/changelog/5003.bugfix.rst @@ -0,0 +1 @@ +Fix line offset with mark collection error (off by one). diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 887f323f9..39701a39b 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -203,7 +203,9 @@ def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0): def getfslineno(obj): """ Return source location (path, lineno) for the given object. - If the source cannot be determined return ("", -1) + If the source cannot be determined return ("", -1). + + The line number is 0-based. """ from .code import Code diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index d65d8b9d8..0021dd5d6 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -44,7 +44,7 @@ def get_empty_parameterset_mark(config, argnames, func): f_name = func.__name__ _, lineno = getfslineno(func) raise Collector.CollectError( - "Empty parameter set in '%s' at line %d" % (f_name, lineno) + "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1) ) else: raise LookupError(requested_mark) diff --git a/testing/test_mark.py b/testing/test_mark.py index f7d8cf689..2851dbc16 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,6 +8,7 @@ import sys import six import pytest +from _pytest.main import EXIT_INTERRUPTED from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector @@ -859,20 +860,34 @@ def test_parameterset_for_fail_at_collect(testdir): config = testdir.parseconfig() from _pytest.mark import pytest_configure, get_empty_parameterset_mark - from _pytest.compat import getfslineno pytest_configure(config) - test_func = all - func_name = test_func.__name__ - _, func_lineno = getfslineno(test_func) - expected_errmsg = r"Empty parameter set in '%s' at line %d" % ( - func_name, - func_lineno, - ) + with pytest.raises( + Collector.CollectError, + match=r"Empty parameter set in 'pytest_configure' at line \d\d+", + ): + get_empty_parameterset_mark(config, ["a"], pytest_configure) - with pytest.raises(Collector.CollectError, match=expected_errmsg): - get_empty_parameterset_mark(config, ["a"], test_func) + p1 = testdir.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("empty", []) + def test(): + pass + """ + ) + result = testdir.runpytest(str(p1)) + result.stdout.fnmatch_lines( + [ + "collected 0 items / 1 errors", + "* ERROR collecting test_parameterset_for_fail_at_collect.py *", + "Empty parameter set in 'test' at line 3", + "*= 1 error in *", + ] + ) + assert result.ret == EXIT_INTERRUPTED def test_parameterset_for_parametrize_bad_markname(testdir): From d406786a8d935cddd9c8b1388535200205527c20 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 22:58:22 +0100 Subject: [PATCH 84/95] pdb: handle capturing with fixtures only --- src/_pytest/capture.py | 18 +++++++++++++++ src/_pytest/debugging.py | 31 ++++++++++++++++++++----- testing/test_pdb.py | 49 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c140757c4..3f6055bd8 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -107,6 +107,16 @@ class CaptureManager(object): return MultiCapture(out=False, err=False, in_=False) raise ValueError("unknown capturing method: %r" % method) # pragma: no cover + def is_capturing(self): + if self.is_globally_capturing(): + return "global" + capture_fixture = getattr(self._current_item, "_capture_fixture", None) + if capture_fixture is not None: + return ( + "fixture %s" % self._current_item._capture_fixture.request.fixturename + ) + return False + # Global capturing control def is_globally_capturing(self): @@ -134,6 +144,14 @@ class CaptureManager(object): if cap is not None: cap.suspend_capturing(in_=in_) + def suspend(self, in_=False): + self.suspend_fixture(self._current_item) + self.suspend_global_capture(in_) + + def resume(self): + self.resume_global_capture() + self.resume_fixture(self._current_item) + def read_global_capture(self): return self._global_capturing.readouterr() diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 5b780d101..abe4f65b6 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -109,7 +109,7 @@ class pytestPDB(object): if cls._pluginmanager is not None: capman = cls._pluginmanager.getplugin("capturemanager") if capman: - capman.suspend_global_capture(in_=True) + capman.suspend(in_=True) tw = _pytest.config.create_terminal_writer(cls._config) tw.line() if cls._recursive_debug == 0: @@ -117,10 +117,22 @@ class pytestPDB(object): header = kwargs.pop("header", None) if header is not None: tw.sep(">", header) - elif capman and capman.is_globally_capturing(): - tw.sep(">", "PDB set_trace (IO-capturing turned off)") else: - tw.sep(">", "PDB set_trace") + if capman: + capturing = capman.is_capturing() + else: + capturing = False + if capturing: + if capturing == "global": + tw.sep(">", "PDB set_trace (IO-capturing turned off)") + else: + tw.sep( + ">", + "PDB set_trace (IO-capturing turned off for %s)" + % capturing, + ) + else: + tw.sep(">", "PDB set_trace") class _PdbWrapper(cls._pdb_cls, object): _pytest_capman = capman @@ -138,11 +150,18 @@ class pytestPDB(object): tw = _pytest.config.create_terminal_writer(cls._config) tw.line() if cls._recursive_debug == 0: - if self._pytest_capman.is_globally_capturing(): + capturing = self._pytest_capman.is_capturing() + if capturing == "global": tw.sep(">", "PDB continue (IO-capturing resumed)") + elif capturing: + tw.sep( + ">", + "PDB continue (IO-capturing resumed for %s)" + % capturing, + ) else: tw.sep(">", "PDB continue") - self._pytest_capman.resume_global_capture() + self._pytest_capman.resume() cls._pluginmanager.hook.pytest_leave_pdb( config=cls._config, pdb=self ) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 05cb9bd77..e6b5fecda 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -970,3 +970,52 @@ def test_quit_with_swallowed_SystemExit(testdir): rest = child.read().decode("utf8") assert "no tests ran" in rest TestPDB.flush(child) + + +@pytest.mark.parametrize("fixture", ("capfd", "capsys")) +def test_pdb_suspends_fixture_capturing(testdir, fixture): + """Using "-s" with pytest should suspend/resume fixture capturing.""" + p1 = testdir.makepyfile( + """ + def test_inner({fixture}): + import sys + + print("out_inner_before") + sys.stderr.write("err_inner_before\\n") + + __import__("pdb").set_trace() + + print("out_inner_after") + sys.stderr.write("err_inner_after\\n") + + out, err = {fixture}.readouterr() + assert out =="out_inner_before\\nout_inner_after\\n" + assert err =="err_inner_before\\nerr_inner_after\\n" + """.format( + fixture=fixture + ) + ) + + child = testdir.spawn_pytest(str(p1) + " -s") + + child.expect("Pdb") + before = child.before.decode("utf8") + assert ( + "> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture) + in before + ) + + # Test that capturing is really suspended. + child.sendline("p 40 + 2") + child.expect("Pdb") + assert "\r\n42\r\n" in child.before.decode("utf8") + + child.sendline("c") + rest = child.read().decode("utf8") + assert "out_inner" not in rest + assert "err_inner" not in rest + + TestPDB.flush(child) + assert child.exitstatus == 0 + assert "= 1 passed in " in rest + assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest From 40718efaccb701c6458f91da5aa9f6c9d58b9567 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Mar 2019 00:47:22 +0100 Subject: [PATCH 85/95] Fix/revisit do_continue with regard to conditions --- src/_pytest/debugging.py | 13 ++++++++----- testing/test_pdb.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index abe4f65b6..5e859f0e3 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -146,22 +146,25 @@ class pytestPDB(object): def do_continue(self, arg): ret = super(_PdbWrapper, self).do_continue(arg) - if self._pytest_capman: + if cls._recursive_debug == 0: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - if cls._recursive_debug == 0: + if self._pytest_capman: capturing = self._pytest_capman.is_capturing() + else: + capturing = False + if capturing: if capturing == "global": tw.sep(">", "PDB continue (IO-capturing resumed)") - elif capturing: + else: tw.sep( ">", "PDB continue (IO-capturing resumed for %s)" % capturing, ) - else: - tw.sep(">", "PDB continue") self._pytest_capman.resume() + else: + tw.sep(">", "PDB continue") cls._pluginmanager.hook.pytest_leave_pdb( config=cls._config, pdb=self ) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index e6b5fecda..6d4fd18e8 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -576,7 +576,8 @@ class TestPDB(object): child.sendline("c") child.expect("LEAVING RECURSIVE DEBUGGER") assert b"PDB continue" not in child.before - assert b"print_from_foo" in child.before + # No extra newline. + assert child.before.endswith(b"c\r\nprint_from_foo\r\n") child.sendline("c") child.expect(r"PDB continue \(IO-capturing resumed\)") rest = child.read().decode("utf8") From ae067df941d16a6b410164ac21a1d9f7d77e97e0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Mar 2019 01:04:15 +0100 Subject: [PATCH 86/95] add test_pdb_continue_with_recursive_debug --- testing/test_pdb.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 6d4fd18e8..74d22e7ef 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -604,6 +604,57 @@ class TestPDB(object): child.expect("1 passed") self.flush(child) + @pytest.mark.parametrize("capture", (True, False)) + def test_pdb_continue_with_recursive_debug(self, capture, testdir): + """Full coverage for do_debug without capturing. + + This is very similar to test_pdb_interaction_continue_recursive, but + simpler, and providing more coverage. + """ + p1 = testdir.makepyfile( + """ + def set_trace(): + __import__('pdb').set_trace() + + def test_1(): + set_trace() + """ + ) + if capture: + child = testdir.spawn_pytest("%s" % p1) + else: + child = testdir.spawn_pytest("-s %s" % p1) + child.expect("Pdb") + before = child.before.decode("utf8") + if capture: + assert ">>> PDB set_trace (IO-capturing turned off) >>>" in before + else: + assert ">>> PDB set_trace >>>" in before + child.sendline("debug set_trace()") + child.expect(r"\(Pdb.*") + before = child.before.decode("utf8") + assert "\r\nENTERING RECURSIVE DEBUGGER\r\n" in before + child.sendline("c") + child.expect(r"\(Pdb.*") + + # No continue message with recursive debugging. + before = child.before.decode("utf8") + assert ">>> PDB continue " not in before + # No extra newline. + assert before.startswith("c\r\n\r\n--Return--") + + child.sendline("c") + child.expect("Pdb") + before = child.before.decode("utf8") + assert "\r\nLEAVING RECURSIVE DEBUGGER\r\n" in before + child.sendline("c") + rest = child.read().decode("utf8") + if capture: + assert "> PDB continue (IO-capturing resumed) >" in rest + else: + assert "> PDB continue >" in rest + assert "1 passed in" in rest + def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile( """ From 951213ee0937c3fef7ce29052fcc2004866e0ffa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Mar 2019 01:27:59 +0100 Subject: [PATCH 87/95] Use new suspend/resume in global_and_fixture_disabled --- src/_pytest/capture.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3f6055bd8..0e8a693e8 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -145,6 +145,7 @@ class CaptureManager(object): cap.suspend_capturing(in_=in_) def suspend(self, in_=False): + # Need to undo local capsys-et-al if it exists before disabling global capture. self.suspend_fixture(self._current_item) self.suspend_global_capture(in_) @@ -186,14 +187,11 @@ class CaptureManager(object): @contextlib.contextmanager def global_and_fixture_disabled(self): """Context manager to temporarily disable global and current fixture capturing.""" - # Need to undo local capsys-et-al if it exists before disabling global capture. - self.suspend_fixture(self._current_item) - self.suspend_global_capture(in_=False) + self.suspend() try: yield finally: - self.resume_global_capture() - self.resume_fixture(self._current_item) + self.resume() @contextlib.contextmanager def item_capture(self, when, item): From d53209956b4cd9345be5576d5472760b8e6de145 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Mar 2019 04:10:29 +0100 Subject: [PATCH 88/95] test_pdb_continue_with_recursive_debug: mock pdb.set_trace --- testing/test_pdb.py | 75 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 74d22e7ef..d5cf17ef9 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -604,52 +604,93 @@ class TestPDB(object): child.expect("1 passed") self.flush(child) - @pytest.mark.parametrize("capture", (True, False)) - def test_pdb_continue_with_recursive_debug(self, capture, testdir): + @pytest.mark.parametrize("capture_arg", ("", "-s", "-p no:capture")) + def test_pdb_continue_with_recursive_debug(self, capture_arg, testdir): """Full coverage for do_debug without capturing. - This is very similar to test_pdb_interaction_continue_recursive, but - simpler, and providing more coverage. + This is very similar to test_pdb_interaction_continue_recursive in general, + but mocks out ``pdb.set_trace`` for providing more coverage. """ p1 = testdir.makepyfile( """ + try: + input = raw_input + except NameError: + pass + def set_trace(): __import__('pdb').set_trace() - def test_1(): + def test_1(monkeypatch): + import _pytest.debugging + + class pytestPDBTest(_pytest.debugging.pytestPDB): + @classmethod + def set_trace(cls, *args, **kwargs): + # Init _PdbWrapper to handle capturing. + _pdb = cls._init_pdb(*args, **kwargs) + + # Mock out pdb.Pdb.do_continue. + import pdb + pdb.Pdb.do_continue = lambda self, arg: None + + print("=== SET_TRACE ===") + assert input() == "debug set_trace()" + + # Simulate _PdbWrapper.do_debug + cls._recursive_debug += 1 + print("ENTERING RECURSIVE DEBUGGER") + print("=== SET_TRACE_2 ===") + + assert input() == "c" + _pdb.do_continue("") + print("=== SET_TRACE_3 ===") + + # Simulate _PdbWrapper.do_debug + print("LEAVING RECURSIVE DEBUGGER") + cls._recursive_debug -= 1 + + print("=== SET_TRACE_4 ===") + assert input() == "c" + _pdb.do_continue("") + + def do_continue(self, arg): + print("=== do_continue") + # _PdbWrapper.do_continue("") + + monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest) + + import pdb + monkeypatch.setattr(pdb, "set_trace", pytestPDBTest.set_trace) + set_trace() """ ) - if capture: - child = testdir.spawn_pytest("%s" % p1) - else: - child = testdir.spawn_pytest("-s %s" % p1) - child.expect("Pdb") + child = testdir.spawn_pytest("%s %s" % (p1, capture_arg)) + child.expect("=== SET_TRACE ===") before = child.before.decode("utf8") - if capture: + if not capture_arg: assert ">>> PDB set_trace (IO-capturing turned off) >>>" in before else: assert ">>> PDB set_trace >>>" in before child.sendline("debug set_trace()") - child.expect(r"\(Pdb.*") + child.expect("=== SET_TRACE_2 ===") before = child.before.decode("utf8") assert "\r\nENTERING RECURSIVE DEBUGGER\r\n" in before child.sendline("c") - child.expect(r"\(Pdb.*") + child.expect("=== SET_TRACE_3 ===") # No continue message with recursive debugging. before = child.before.decode("utf8") assert ">>> PDB continue " not in before - # No extra newline. - assert before.startswith("c\r\n\r\n--Return--") child.sendline("c") - child.expect("Pdb") + child.expect("=== SET_TRACE_4 ===") before = child.before.decode("utf8") assert "\r\nLEAVING RECURSIVE DEBUGGER\r\n" in before child.sendline("c") rest = child.read().decode("utf8") - if capture: + if not capture_arg: assert "> PDB continue (IO-capturing resumed) >" in rest else: assert "> PDB continue >" in rest From 63a01bdb3308e36bdca7b4b006358af6f19e640f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 20 Mar 2019 03:39:13 +0100 Subject: [PATCH 89/95] Factor out pytestPDB._is_capturing --- src/_pytest/debugging.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 5e859f0e3..bb90d00ca 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -101,6 +101,12 @@ class pytestPDB(object): _saved = [] _recursive_debug = 0 + @classmethod + def _is_capturing(cls, capman): + if capman: + return capman.is_capturing() + return False + @classmethod def _init_pdb(cls, *args, **kwargs): """ Initialize PDB debugging, dropping any IO capturing. """ @@ -118,10 +124,7 @@ class pytestPDB(object): if header is not None: tw.sep(">", header) else: - if capman: - capturing = capman.is_capturing() - else: - capturing = False + capturing = cls._is_capturing(capman) if capturing: if capturing == "global": tw.sep(">", "PDB set_trace (IO-capturing turned off)") @@ -149,10 +152,9 @@ class pytestPDB(object): if cls._recursive_debug == 0: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - if self._pytest_capman: - capturing = self._pytest_capman.is_capturing() - else: - capturing = False + + capman = self._pytest_capman + capturing = pytestPDB._is_capturing(capman) if capturing: if capturing == "global": tw.sep(">", "PDB continue (IO-capturing resumed)") @@ -162,7 +164,7 @@ class pytestPDB(object): "PDB continue (IO-capturing resumed for %s)" % capturing, ) - self._pytest_capman.resume() + capman.resume() else: tw.sep(">", "PDB continue") cls._pluginmanager.hook.pytest_leave_pdb( From 46d9243eb076df31495d6c0399142310b4b50dba Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Mar 2019 11:56:53 +0100 Subject: [PATCH 90/95] changelog --- changelog/4951.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4951.feature.rst diff --git a/changelog/4951.feature.rst b/changelog/4951.feature.rst new file mode 100644 index 000000000..b40e03af5 --- /dev/null +++ b/changelog/4951.feature.rst @@ -0,0 +1 @@ +Output capturing is handled correctly when only capturing via fixtures (capsys, capfs) with ``pdb.set_trace()``. From 65c8e8a09e56acbb992a4cad462f01cfd82617d0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 28 Mar 2019 13:41:56 -0300 Subject: [PATCH 91/95] Rename hooks: to/from_serializable --- changelog/4965.trivial.rst | 2 +- src/_pytest/hookspec.py | 6 +++--- src/_pytest/reports.py | 4 ++-- testing/test_reports.py | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/changelog/4965.trivial.rst b/changelog/4965.trivial.rst index 487a22c7a..36db733f9 100644 --- a/changelog/4965.trivial.rst +++ b/changelog/4965.trivial.rst @@ -1,4 +1,4 @@ -New ``pytest_report_serialize`` and ``pytest_report_unserialize`` **experimental** hooks. +New ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` **experimental** hooks. These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for resultlog to serialize and customize reports. diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index afcee5397..65a7f43ed 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -376,7 +376,7 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) -def pytest_report_serialize(config, report): +def pytest_report_to_serializable(config, report): """ .. warning:: This hook is experimental and subject to change between pytest releases, even @@ -394,7 +394,7 @@ def pytest_report_serialize(config, report): @hookspec(firstresult=True) -def pytest_report_unserialize(config, data): +def pytest_report_from_serializable(config, data): """ .. warning:: This hook is experimental and subject to change between pytest releases, even @@ -406,7 +406,7 @@ def pytest_report_unserialize(config, data): In the future it might become part of the public hook API. - Restores a report object previously serialized with pytest_report_serialize(). + Restores a report object previously serialized with pytest_report_to_serializable(). """ diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 679d72c9d..d2df4d21f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -409,14 +409,14 @@ class CollectErrorRepr(TerminalRepr): out.line(self.longrepr, red=True) -def pytest_report_serialize(report): +def pytest_report_to_serializable(report): if isinstance(report, (TestReport, CollectReport)): data = report._to_json() data["_report_type"] = report.__class__.__name__ return data -def pytest_report_unserialize(data): +def pytest_report_from_serializable(data): if "_report_type" in data: if data["_report_type"] == "TestReport": return TestReport._from_json(data) diff --git a/testing/test_reports.py b/testing/test_reports.py index 22d5fce34..6d2b167f8 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -257,11 +257,11 @@ class TestHooks: reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 6 for rep in reports: - data = pytestconfig.hook.pytest_report_serialize( + data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) assert data["_report_type"] == "TestReport" - new_rep = pytestconfig.hook.pytest_report_unserialize( + new_rep = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data ) assert new_rep.nodeid == rep.nodeid @@ -279,11 +279,11 @@ class TestHooks: reports = reprec.getreports("pytest_collectreport") assert len(reports) == 2 for rep in reports: - data = pytestconfig.hook.pytest_report_serialize( + data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) assert data["_report_type"] == "CollectReport" - new_rep = pytestconfig.hook.pytest_report_unserialize( + new_rep = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data ) assert new_rep.nodeid == rep.nodeid @@ -303,11 +303,11 @@ class TestHooks: reports = reprec.getreports(hook_name) assert reports rep = reports[0] - data = pytestconfig.hook.pytest_report_serialize( + data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) data["_report_type"] = "Unknown" with pytest.raises(AssertionError): - _ = pytestconfig.hook.pytest_report_unserialize( + _ = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data ) From bfda2a0050943f0ead1fec3b18a52d32c07f34fa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Mar 2019 00:27:23 +0100 Subject: [PATCH 92/95] setup.cfg: use existing [tool:pytest] (ignoring [pytest]) --- changelog/5008.feature.rst | 3 +++ src/_pytest/config/findpaths.py | 12 ++++++------ testing/test_config.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 changelog/5008.feature.rst diff --git a/changelog/5008.feature.rst b/changelog/5008.feature.rst new file mode 100644 index 000000000..17d2770fe --- /dev/null +++ b/changelog/5008.feature.rst @@ -0,0 +1,3 @@ +If a ``setup.cfg`` file contains ``[tool:pytest]`` and also the no longer supported ``[pytest]`` section, pytest will use ``[tool:pytest]`` ignoring ``[pytest]``. Previously it would unconditionally error out. + +This makes it simpler for plugins to support old pytest versions. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index a0f16134d..fa2024470 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -33,7 +33,12 @@ def getcfg(args, config=None): p = base.join(inibasename) if exists(p): iniconfig = py.iniconfig.IniConfig(p) - if "pytest" in iniconfig.sections: + if ( + inibasename == "setup.cfg" + and "tool:pytest" in iniconfig.sections + ): + return base, p, iniconfig["tool:pytest"] + elif "pytest" in iniconfig.sections: if inibasename == "setup.cfg" and config is not None: fail( @@ -41,11 +46,6 @@ def getcfg(args, config=None): pytrace=False, ) return base, p, iniconfig["pytest"] - if ( - inibasename == "setup.cfg" - and "tool:pytest" in iniconfig.sections - ): - return base, p, iniconfig["tool:pytest"] elif inibasename == "pytest.ini": # allowed to be empty return base, p, {} diff --git a/testing/test_config.py b/testing/test_config.py index 07654e5ad..28cc3ab91 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -46,6 +46,22 @@ class TestParseIni(object): """correctly handle zero length arguments (a la pytest '')""" getcfg([""]) + def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): + p1 = testdir.makepyfile("def test(): pass") + testdir.makefile( + ".cfg", + setup=""" + [tool:pytest] + testpaths=%s + [pytest] + testpaths=ignored + """ + % p1.basename, + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*, inifile: setup.cfg, *", "* 1 passed in *"]) + assert result.ret == 0 + def test_append_parse_args(self, testdir, tmpdir, monkeypatch): monkeypatch.setenv("PYTEST_ADDOPTS", '--color no -rs --tb="short"') tmpdir.join("pytest.ini").write( From 401102182395626e8a00af83e866953a4c78d566 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 21 Mar 2019 07:22:15 +0100 Subject: [PATCH 93/95] pdb: do not raise outcomes.Exit with quit in debug --- changelog/4968.bugfix.rst | 3 +++ src/_pytest/debugging.py | 9 ++++++++- testing/test_pdb.py | 15 ++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelog/4968.bugfix.rst diff --git a/changelog/4968.bugfix.rst b/changelog/4968.bugfix.rst new file mode 100644 index 000000000..9ff61652e --- /dev/null +++ b/changelog/4968.bugfix.rst @@ -0,0 +1,3 @@ +The pdb ``quit`` command is handled properly when used after the ``debug`` command with `pdb++`_. + +.. _pdb++: https://pypi.org/project/pdbpp/ diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index bb90d00ca..cb1c964c3 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -176,8 +176,15 @@ class pytestPDB(object): do_c = do_cont = do_continue def set_quit(self): + """Raise Exit outcome when quit command is used in pdb. + + This is a bit of a hack - it would be better if BdbQuit + could be handled, but this would require to wrap the + whole pytest run, and adjust the report etc. + """ super(_PdbWrapper, self).set_quit() - outcomes.exit("Quitting debugger") + if cls._recursive_debug == 0: + outcomes.exit("Quitting debugger") def setup(self, f, tb): """Suspend on setup(). diff --git a/testing/test_pdb.py b/testing/test_pdb.py index d5cf17ef9..531846e8e 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -519,7 +519,10 @@ class TestPDB(object): assert "1 failed" in rest self.flush(child) - def test_pdb_interaction_continue_recursive(self, testdir): + def test_pdb_with_injected_do_debug(self, testdir): + """Simulates pdbpp, which injects Pdb into do_debug, and uses + self.__class__ in do_continue. + """ p1 = testdir.makepyfile( mytest=""" import pdb @@ -527,8 +530,6 @@ class TestPDB(object): count_continue = 0 - # Simulates pdbpp, which injects Pdb into do_debug, and uses - # self.__class__ in do_continue. class CustomPdb(pdb.Pdb, object): def do_debug(self, arg): import sys @@ -578,6 +579,14 @@ class TestPDB(object): assert b"PDB continue" not in child.before # No extra newline. assert child.before.endswith(b"c\r\nprint_from_foo\r\n") + + # set_debug should not raise outcomes.Exit, if used recrursively. + child.sendline("debug 42") + child.sendline("q") + child.expect("LEAVING RECURSIVE DEBUGGER") + assert b"ENTERING RECURSIVE DEBUGGER" in child.before + assert b"Quitting debugger" not in child.before + child.sendline("c") child.expect(r"PDB continue \(IO-capturing resumed\)") rest = child.read().decode("utf8") From 8881b201aaba7af6d7db8fba84fa127f11edd7b4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 29 Mar 2019 20:49:18 +0000 Subject: [PATCH 94/95] Preparing release version 4.4.0 --- CHANGELOG.rst | 158 ++++++++++++++++++++++++++++++ changelog/1895.bugfix.rst | 2 - changelog/2224.feature.rst | 4 - changelog/2482.feature.rst | 1 - changelog/4718.feature.rst | 6 -- changelog/4718.trivial.rst | 1 - changelog/4815.trivial.rst | 1 - changelog/4829.trivial.rst | 1 - changelog/4851.bugfix.rst | 1 - changelog/4855.feature.rst | 4 - changelog/4875.feature.rst | 5 - changelog/4890.trivial.rst | 1 - changelog/4903.bugfix.rst | 1 - changelog/4911.feature.rst | 1 - changelog/4912.trivial.rst | 2 - changelog/4913.trivial.rst | 1 - changelog/4920.feature.rst | 6 -- changelog/4928.bugfix.rst | 1 - changelog/4931.feature.rst | 1 - changelog/4936.feature.rst | 4 - changelog/4951.feature.rst | 1 - changelog/4956.feature.rst | 3 - changelog/4957.bugfix.rst | 3 - changelog/4965.trivial.rst | 9 -- changelog/4968.bugfix.rst | 3 - changelog/4974.doc.rst | 1 - changelog/4975.bugfix.rst | 1 - changelog/4978.bugfix.rst | 1 - changelog/4980.feature.rst | 1 - changelog/4987.trivial.rst | 1 - changelog/4988.bugfix.rst | 1 - changelog/4993.feature.rst | 1 - changelog/5003.bugfix.rst | 1 - changelog/5008.feature.rst | 3 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.4.0.rst | 39 ++++++++ doc/en/assert.rst | 4 +- doc/en/builtin.rst | 36 ++++--- doc/en/cache.rst | 96 +++++++++++++++--- doc/en/capture.rst | 2 +- doc/en/example/markers.rst | 28 +++--- doc/en/example/nonpython.rst | 6 +- doc/en/example/parametrize.rst | 16 ++- doc/en/example/reportingdemo.rst | 2 +- doc/en/example/simple.rst | 22 ++--- doc/en/fixture.rst | 12 +-- doc/en/getting-started.rst | 2 +- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 4 +- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 4 +- doc/en/unittest.rst | 2 +- doc/en/usage.rst | 6 +- doc/en/warnings.rst | 2 +- 54 files changed, 360 insertions(+), 160 deletions(-) delete mode 100644 changelog/1895.bugfix.rst delete mode 100644 changelog/2224.feature.rst delete mode 100644 changelog/2482.feature.rst delete mode 100644 changelog/4718.feature.rst delete mode 100644 changelog/4718.trivial.rst delete mode 100644 changelog/4815.trivial.rst delete mode 100644 changelog/4829.trivial.rst delete mode 100644 changelog/4851.bugfix.rst delete mode 100644 changelog/4855.feature.rst delete mode 100644 changelog/4875.feature.rst delete mode 100644 changelog/4890.trivial.rst delete mode 100644 changelog/4903.bugfix.rst delete mode 100644 changelog/4911.feature.rst delete mode 100644 changelog/4912.trivial.rst delete mode 100644 changelog/4913.trivial.rst delete mode 100644 changelog/4920.feature.rst delete mode 100644 changelog/4928.bugfix.rst delete mode 100644 changelog/4931.feature.rst delete mode 100644 changelog/4936.feature.rst delete mode 100644 changelog/4951.feature.rst delete mode 100644 changelog/4956.feature.rst delete mode 100644 changelog/4957.bugfix.rst delete mode 100644 changelog/4965.trivial.rst delete mode 100644 changelog/4968.bugfix.rst delete mode 100644 changelog/4974.doc.rst delete mode 100644 changelog/4975.bugfix.rst delete mode 100644 changelog/4978.bugfix.rst delete mode 100644 changelog/4980.feature.rst delete mode 100644 changelog/4987.trivial.rst delete mode 100644 changelog/4988.bugfix.rst delete mode 100644 changelog/4993.feature.rst delete mode 100644 changelog/5003.bugfix.rst delete mode 100644 changelog/5008.feature.rst create mode 100644 doc/en/announce/release-4.4.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 839b4c439..c3c099b10 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,164 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.4.0 (2019-03-29) +========================= + +Features +-------- + +- `#2224 `_: ``async`` test functions are skipped and a warning is emitted when a suitable + async plugin is not installed (such as ``pytest-asyncio`` or ``pytest-trio``). + + Previously ``async`` functions would not execute at all but still be marked as "passed". + + +- `#2482 `_: Include new ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option to disable ascii-escaping in parametrized values. This may cause a series of problems and as the name makes clear, use at your own risk. + + +- `#4718 `_: The ``-p`` option can now be used to early-load plugins also by entry-point name, instead of just + by module name. + + This makes it possible to early load external plugins like ``pytest-cov`` in the command-line:: + + pytest -p pytest_cov + + +- `#4855 `_: The ``--pdbcls`` option handles classes via module attributes now (e.g. + ``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved. + + .. _pdb++: https://pypi.org/project/pdbpp/ + + +- `#4875 `_: The `testpaths `__ configuration option is now displayed next + to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were + not explicitly passed in the command line. + + Also, ``inifile`` is only displayed if there's a configuration file, instead of an empty ``inifile:`` string. + + +- `#4911 `_: Doctests can be skipped now dynamically using ``pytest.skip()``. + + +- `#4920 `_: Internal refactorings have been made in order to make the implementation of the + `pytest-subtests `__ plugin + possible, which adds unittest sub-test support and a new ``subtests`` fixture as discussed in + `#1367 `__. + + For details on the internal refactorings, please see the details on the related PR. + + +- `#4931 `_: pytester's ``LineMatcher`` asserts that the passed lines are a sequence. + + +- `#4936 `_: Handle ``-p plug`` after ``-p no:plug``. + + This can be used to override a blocked plugin (e.g. in "addopts") from the + command line etc. + + +- `#4951 `_: Output capturing is handled correctly when only capturing via fixtures (capsys, capfs) with ``pdb.set_trace()``. + + +- `#4956 `_: ``pytester`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory during test runs. + + This ensures to not load configuration files from the real user's home directory. + + +- `#4980 `_: Namespace packages are handled better with ``monkeypatch.syspath_prepend`` and ``testdir.syspathinsert`` (via ``pkg_resources.fixup_namespace_packages``). + + +- `#4993 `_: The stepwise plugin reports status information now. + + +- `#5008 `_: If a ``setup.cfg`` file contains ``[tool:pytest]`` and also the no longer supported ``[pytest]`` section, pytest will use ``[tool:pytest]`` ignoring ``[pytest]``. Previously it would unconditionally error out. + + This makes it simpler for plugins to support old pytest versions. + + + +Bug Fixes +--------- + +- `#1895 `_: Fix bug where fixtures requested dynamically via ``request.getfixturevalue()`` might be teardown + before the requesting fixture. + + +- `#4851 `_: pytester unsets ``PYTEST_ADDOPTS`` now to not use outer options with ``testdir.runpytest()``. + + +- `#4903 `_: Use the correct modified time for years after 2038 in rewritten ``.pyc`` files. + + +- `#4928 `_: Fix line offsets with ``ScopeMismatch`` errors. + + +- `#4957 `_: ``-p no:plugin`` is handled correctly for default (internal) plugins now, e.g. with ``-p no:capture``. + + Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available. + + +- `#4968 `_: The pdb ``quit`` command is handled properly when used after the ``debug`` command with `pdb++`_. + + .. _pdb++: https://pypi.org/project/pdbpp/ + + +- `#4975 `_: Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead. + + +- `#4978 `_: ``outcomes.Exit`` is not swallowed in ``assertrepr_compare`` anymore. + + +- `#4988 `_: Close logging's file handler explicitly when the session finishes. + + +- `#5003 `_: Fix line offset with mark collection error (off by one). + + + +Improved Documentation +---------------------- + +- `#4974 `_: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations + + + +Trivial/Internal Changes +------------------------ + +- `#4718 `_: ``pluggy>=0.9`` is now required. + + +- `#4815 `_: ``funcsigs>=1.0`` is now required for Python 2.7. + + +- `#4829 `_: Some left-over internal code related to ``yield`` tests has been removed. + + +- `#4890 `_: Remove internally unused ``anypython`` fixture from the pytester plugin. + + +- `#4912 `_: Remove deprecated Sphinx directive, ``add_description_unit()``, + pin sphinx-removed-in to >= 0.2.0 to support Sphinx 2.0. + + +- `#4913 `_: Fix pytest tests invocation with custom ``PYTHONPATH``. + + +- `#4965 `_: New ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` **experimental** hooks. + + These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for + resultlog to serialize and customize reports. + + They are experimental, meaning that their details might change or even be removed + completely in future patch releases without warning. + + Feedback is welcome from plugin authors and users alike. + + +- `#4987 `_: ``Collector.repr_failure`` respects ``--tbstyle``, but only defaults to ``short`` now (with ``auto``). + + pytest 4.3.1 (2019-03-11) ========================= diff --git a/changelog/1895.bugfix.rst b/changelog/1895.bugfix.rst deleted file mode 100644 index 44b921ad9..000000000 --- a/changelog/1895.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix bug where fixtures requested dynamically via ``request.getfixturevalue()`` might be teardown -before the requesting fixture. diff --git a/changelog/2224.feature.rst b/changelog/2224.feature.rst deleted file mode 100644 index 6f0df93ae..000000000 --- a/changelog/2224.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -``async`` test functions are skipped and a warning is emitted when a suitable -async plugin is not installed (such as ``pytest-asyncio`` or ``pytest-trio``). - -Previously ``async`` functions would not execute at all but still be marked as "passed". diff --git a/changelog/2482.feature.rst b/changelog/2482.feature.rst deleted file mode 100644 index 37d5138bf..000000000 --- a/changelog/2482.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Include new ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option to disable ascii-escaping in parametrized values. This may cause a series of problems and as the name makes clear, use at your own risk. diff --git a/changelog/4718.feature.rst b/changelog/4718.feature.rst deleted file mode 100644 index 35d5fffb9..000000000 --- a/changelog/4718.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -The ``-p`` option can now be used to early-load plugins also by entry-point name, instead of just -by module name. - -This makes it possible to early load external plugins like ``pytest-cov`` in the command-line:: - - pytest -p pytest_cov diff --git a/changelog/4718.trivial.rst b/changelog/4718.trivial.rst deleted file mode 100644 index 8b4e019bc..000000000 --- a/changelog/4718.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``pluggy>=0.9`` is now required. diff --git a/changelog/4815.trivial.rst b/changelog/4815.trivial.rst deleted file mode 100644 index d7d91b899..000000000 --- a/changelog/4815.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``funcsigs>=1.0`` is now required for Python 2.7. diff --git a/changelog/4829.trivial.rst b/changelog/4829.trivial.rst deleted file mode 100644 index a1935b462..000000000 --- a/changelog/4829.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Some left-over internal code related to ``yield`` tests has been removed. diff --git a/changelog/4851.bugfix.rst b/changelog/4851.bugfix.rst deleted file mode 100644 index 7b532af3e..000000000 --- a/changelog/4851.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pytester unsets ``PYTEST_ADDOPTS`` now to not use outer options with ``testdir.runpytest()``. diff --git a/changelog/4855.feature.rst b/changelog/4855.feature.rst deleted file mode 100644 index 274d3991f..000000000 --- a/changelog/4855.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -The ``--pdbcls`` option handles classes via module attributes now (e.g. -``pdb:pdb.Pdb`` with `pdb++`_), and its validation was improved. - -.. _pdb++: https://pypi.org/project/pdbpp/ diff --git a/changelog/4875.feature.rst b/changelog/4875.feature.rst deleted file mode 100644 index d9fb65ca5..000000000 --- a/changelog/4875.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -The `testpaths `__ configuration option is now displayed next -to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were -not explicitly passed in the command line. - -Also, ``inifile`` is only displayed if there's a configuration file, instead of an empty ``inifile:`` string. diff --git a/changelog/4890.trivial.rst b/changelog/4890.trivial.rst deleted file mode 100644 index a3a08bc11..000000000 --- a/changelog/4890.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Remove internally unused ``anypython`` fixture from the pytester plugin. diff --git a/changelog/4903.bugfix.rst b/changelog/4903.bugfix.rst deleted file mode 100644 index 116e1b0fd..000000000 --- a/changelog/4903.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Use the correct modified time for years after 2038 in rewritten ``.pyc`` files. diff --git a/changelog/4911.feature.rst b/changelog/4911.feature.rst deleted file mode 100644 index 5aef92d76..000000000 --- a/changelog/4911.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Doctests can be skipped now dynamically using ``pytest.skip()``. diff --git a/changelog/4912.trivial.rst b/changelog/4912.trivial.rst deleted file mode 100644 index 9600c833b..000000000 --- a/changelog/4912.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove deprecated Sphinx directive, ``add_description_unit()``, -pin sphinx-removed-in to >= 0.2.0 to support Sphinx 2.0. diff --git a/changelog/4913.trivial.rst b/changelog/4913.trivial.rst deleted file mode 100644 index 7846775cc..000000000 --- a/changelog/4913.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fix pytest tests invocation with custom ``PYTHONPATH``. diff --git a/changelog/4920.feature.rst b/changelog/4920.feature.rst deleted file mode 100644 index 5eb152482..000000000 --- a/changelog/4920.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -Internal refactorings have been made in order to make the implementation of the -`pytest-subtests `__ plugin -possible, which adds unittest sub-test support and a new ``subtests`` fixture as discussed in -`#1367 `__. - -For details on the internal refactorings, please see the details on the related PR. diff --git a/changelog/4928.bugfix.rst b/changelog/4928.bugfix.rst deleted file mode 100644 index 8959efacb..000000000 --- a/changelog/4928.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix line offsets with ``ScopeMismatch`` errors. diff --git a/changelog/4931.feature.rst b/changelog/4931.feature.rst deleted file mode 100644 index bfc35a4b7..000000000 --- a/changelog/4931.feature.rst +++ /dev/null @@ -1 +0,0 @@ -pytester's ``LineMatcher`` asserts that the passed lines are a sequence. diff --git a/changelog/4936.feature.rst b/changelog/4936.feature.rst deleted file mode 100644 index 744af1297..000000000 --- a/changelog/4936.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Handle ``-p plug`` after ``-p no:plug``. - -This can be used to override a blocked plugin (e.g. in "addopts") from the -command line etc. diff --git a/changelog/4951.feature.rst b/changelog/4951.feature.rst deleted file mode 100644 index b40e03af5..000000000 --- a/changelog/4951.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Output capturing is handled correctly when only capturing via fixtures (capsys, capfs) with ``pdb.set_trace()``. diff --git a/changelog/4956.feature.rst b/changelog/4956.feature.rst deleted file mode 100644 index 1dfbd7e97..000000000 --- a/changelog/4956.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -``pytester`` sets ``$HOME`` and ``$USERPROFILE`` to the temporary directory during test runs. - -This ensures to not load configuration files from the real user's home directory. diff --git a/changelog/4957.bugfix.rst b/changelog/4957.bugfix.rst deleted file mode 100644 index ade73ce22..000000000 --- a/changelog/4957.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -``-p no:plugin`` is handled correctly for default (internal) plugins now, e.g. with ``-p no:capture``. - -Previously they were loaded (imported) always, making e.g. the ``capfd`` fixture available. diff --git a/changelog/4965.trivial.rst b/changelog/4965.trivial.rst deleted file mode 100644 index 36db733f9..000000000 --- a/changelog/4965.trivial.rst +++ /dev/null @@ -1,9 +0,0 @@ -New ``pytest_report_to_serializable`` and ``pytest_report_from_serializable`` **experimental** hooks. - -These hooks will be used by ``pytest-xdist``, ``pytest-subtests``, and the replacement for -resultlog to serialize and customize reports. - -They are experimental, meaning that their details might change or even be removed -completely in future patch releases without warning. - -Feedback is welcome from plugin authors and users alike. diff --git a/changelog/4968.bugfix.rst b/changelog/4968.bugfix.rst deleted file mode 100644 index 9ff61652e..000000000 --- a/changelog/4968.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -The pdb ``quit`` command is handled properly when used after the ``debug`` command with `pdb++`_. - -.. _pdb++: https://pypi.org/project/pdbpp/ diff --git a/changelog/4974.doc.rst b/changelog/4974.doc.rst deleted file mode 100644 index 74799c9b3..000000000 --- a/changelog/4974.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update docs for ``pytest_cmdline_parse`` hook to note availability liminations diff --git a/changelog/4975.bugfix.rst b/changelog/4975.bugfix.rst deleted file mode 100644 index 26c93ec18..000000000 --- a/changelog/4975.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the interpretation of ``-qq`` option where it was being considered as ``-v`` instead. diff --git a/changelog/4978.bugfix.rst b/changelog/4978.bugfix.rst deleted file mode 100644 index 259daa8da..000000000 --- a/changelog/4978.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``outcomes.Exit`` is not swallowed in ``assertrepr_compare`` anymore. diff --git a/changelog/4980.feature.rst b/changelog/4980.feature.rst deleted file mode 100644 index 40f1de9c1..000000000 --- a/changelog/4980.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Namespace packages are handled better with ``monkeypatch.syspath_prepend`` and ``testdir.syspathinsert`` (via ``pkg_resources.fixup_namespace_packages``). diff --git a/changelog/4987.trivial.rst b/changelog/4987.trivial.rst deleted file mode 100644 index eb79b742a..000000000 --- a/changelog/4987.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``Collector.repr_failure`` respects ``--tbstyle``, but only defaults to ``short`` now (with ``auto``). diff --git a/changelog/4988.bugfix.rst b/changelog/4988.bugfix.rst deleted file mode 100644 index 8cc816ed6..000000000 --- a/changelog/4988.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Close logging's file handler explicitly when the session finishes. diff --git a/changelog/4993.feature.rst b/changelog/4993.feature.rst deleted file mode 100644 index b8e1ff494..000000000 --- a/changelog/4993.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The stepwise plugin reports status information now. diff --git a/changelog/5003.bugfix.rst b/changelog/5003.bugfix.rst deleted file mode 100644 index 8d18a50e6..000000000 --- a/changelog/5003.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix line offset with mark collection error (off by one). diff --git a/changelog/5008.feature.rst b/changelog/5008.feature.rst deleted file mode 100644 index 17d2770fe..000000000 --- a/changelog/5008.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -If a ``setup.cfg`` file contains ``[tool:pytest]`` and also the no longer supported ``[pytest]`` section, pytest will use ``[tool:pytest]`` ignoring ``[pytest]``. Previously it would unconditionally error out. - -This makes it simpler for plugins to support old pytest versions. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 96123f3fb..7e2554656 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.4.0 release-4.3.1 release-4.3.0 release-4.2.1 diff --git a/doc/en/announce/release-4.4.0.rst b/doc/en/announce/release-4.4.0.rst new file mode 100644 index 000000000..4c5bcbc7d --- /dev/null +++ b/doc/en/announce/release-4.4.0.rst @@ -0,0 +1,39 @@ +pytest-4.4.0 +======================================= + +The pytest team is proud to announce the 4.4.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: + +* Anthony Sottile +* ApaDoctor +* Bernhard M. Wiedemann +* Brian Skinn +* Bruno Oliveira +* Daniel Hahler +* Gary Tyler +* Jeong YunWon +* Miro Hrončok +* Takafumi Arakaki +* henrykironde +* smheidrich + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index e7e78601b..996ad02a8 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -30,7 +30,7 @@ you will see the return value of the function call: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert1.py F [100%] @@ -165,7 +165,7 @@ if you run this module: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert2.py F [100%] diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 8d6a06a44..fb16140c0 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -28,25 +28,29 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. capsys - Enable capturing of writes to ``sys.stdout`` and ``sys.stderr`` and make - captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` - objects. + Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. capsysbinary - Enable capturing of writes to ``sys.stdout`` and ``sys.stderr`` and make - captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes`` - objects. + Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsysbinary.readouterr()`` + method calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``bytes`` objects. capfd - Enable capturing of writes to file descriptors ``1`` and ``2`` and make - captured output available via ``capfd.readouterr()`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` - objects. + Enable text capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. capfdbinary - Enable capturing of write to file descriptors 1 and 2 and make - captured output available via ``capfdbinary.readouterr`` method calls - which return a ``(out, err)`` tuple. ``out`` and ``err`` will be - ``bytes`` objects. + Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``byte`` objects. doctest_namespace Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. pytestconfig diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 89a1e2634..40202eb76 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -82,7 +82,7 @@ If you then run it with ``--lf``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 50 items / 48 deselected / 2 selected run-last-failure: rerun previous 2 failures @@ -126,7 +126,7 @@ of ``FF`` and dots): =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 50 items run-last-failure: rerun previous 2 failures first @@ -218,12 +218,8 @@ If you run this command for the first time, you can see the print statement: def test_function(mydata): > assert mydata == 23 E assert 42 == 23 - E -42 - E +23 test_caching.py:17: AssertionError - -------------------------- Captured stdout setup --------------------------- - running expensive computation... 1 failed in 0.12 seconds If you run it a second time the value will be retrieved from @@ -241,8 +237,6 @@ the cache and nothing will be printed: def test_function(mydata): > assert mydata == 23 E assert 42 == 23 - E -42 - E +23 test_caching.py:17: AssertionError 1 failed in 0.12 seconds @@ -262,16 +256,96 @@ You can always peek at the content of the cache using the =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache ------------------------------- cache values ------------------------------- cache/lastfailed contains: - {'test_50.py::test_num[17]': True, + {'a/test_db.py::test_a1': True, + 'a/test_db2.py::test_a2': True, + 'b/test_error.py::test_root': True, + 'failure_demo.py::TestCustomAssertMsg::test_custom_repr': True, + 'failure_demo.py::TestCustomAssertMsg::test_multiline': True, + 'failure_demo.py::TestCustomAssertMsg::test_single_line': True, + 'failure_demo.py::TestFailing::test_not': True, + 'failure_demo.py::TestFailing::test_simple': True, + 'failure_demo.py::TestFailing::test_simple_multiline': True, + 'failure_demo.py::TestMoreErrors::test_compare': True, + 'failure_demo.py::TestMoreErrors::test_complex_error': True, + 'failure_demo.py::TestMoreErrors::test_global_func': True, + 'failure_demo.py::TestMoreErrors::test_instance': True, + 'failure_demo.py::TestMoreErrors::test_startswith': True, + 'failure_demo.py::TestMoreErrors::test_startswith_nested': True, + 'failure_demo.py::TestMoreErrors::test_try_finally': True, + 'failure_demo.py::TestMoreErrors::test_z1_unpack_error': True, + 'failure_demo.py::TestMoreErrors::test_z2_type_error': True, + 'failure_demo.py::TestRaises::test_raise': True, + 'failure_demo.py::TestRaises::test_raises': True, + 'failure_demo.py::TestRaises::test_raises_doesnt': True, + 'failure_demo.py::TestRaises::test_reinterpret_fails_with_print_for_the_fun_of_it': True, + 'failure_demo.py::TestRaises::test_some_error': True, + 'failure_demo.py::TestRaises::test_tupleerror': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_attrs': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_dataclass': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_dict': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_list': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_list_long': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_long_text': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_long_text_multiline': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_multiline_text': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_set': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_similar_text': True, + 'failure_demo.py::TestSpecialisedExplanations::test_eq_text': True, + 'failure_demo.py::TestSpecialisedExplanations::test_in_list': True, + 'failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline': True, + 'failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single': True, + 'failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long': True, + 'failure_demo.py::TestSpecialisedExplanations::test_not_in_text_single_long_term': True, + 'failure_demo.py::test_attribute': True, + 'failure_demo.py::test_attribute_failure': True, + 'failure_demo.py::test_attribute_instance': True, + 'failure_demo.py::test_attribute_multiple': True, + 'failure_demo.py::test_dynamic_compile_shows_nicely': True, + 'failure_demo.py::test_generative[3-6]': True, + 'test_50.py::test_num[17]': True, 'test_50.py::test_num[25]': True, + 'test_anothersmtp.py::test_showhelo': True, 'test_assert1.py::test_function': True, 'test_assert2.py::test_set_comparison': True, + 'test_backends.py::test_db_initialized[d2]': True, 'test_caching.py::test_function': True, - 'test_foocompare.py::test_compare': True} + 'test_checkconfig.py::test_something': True, + 'test_class.py::TestClass::test_two': True, + 'test_compute.py::test_compute[4]': True, + 'test_example.py::test_error': True, + 'test_example.py::test_fail': True, + 'test_foocompare.py::test_compare': True, + 'test_module.py::test_call_fails': True, + 'test_module.py::test_ehlo': True, + 'test_module.py::test_ehlo[mail.python.org]': True, + 'test_module.py::test_ehlo[smtp.gmail.com]': True, + 'test_module.py::test_event_simple': True, + 'test_module.py::test_fail1': True, + 'test_module.py::test_fail2': True, + 'test_module.py::test_func2': True, + 'test_module.py::test_interface_complex': True, + 'test_module.py::test_interface_simple': True, + 'test_module.py::test_noop': True, + 'test_module.py::test_noop[mail.python.org]': True, + 'test_module.py::test_noop[smtp.gmail.com]': True, + 'test_module.py::test_setup_fails': True, + 'test_parametrize.py::TestClass::test_equals[1-2]': True, + 'test_sample.py::test_answer': True, + 'test_show_warnings.py::test_one': True, + 'test_simple.yml::hello': True, + 'test_smtpsimple.py::test_ehlo': True, + 'test_step.py::TestUserHandling::test_modification': True, + 'test_strings.py::test_valid_string[!]': True, + 'test_tmp_path.py::test_create_file': True, + 'test_tmpdir.py::test_create_file': True, + 'test_tmpdir.py::test_needsfiles': True, + 'test_unittest_db.py::MyTest::test_method1': True, + 'test_unittest_db.py::MyTest::test_method2': True} cache/nodeids contains: ['test_caching.py::test_function'] cache/stepwise contains: diff --git a/doc/en/capture.rst b/doc/en/capture.rst index f62ec60ca..8629350a5 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -71,7 +71,7 @@ of the failing function and hide the other one: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .F [100%] diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 5ec57caed..aeb1fdfde 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -35,7 +35,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -50,7 +50,7 @@ Or the inverse, running all tests except the webtest ones: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -72,7 +72,7 @@ tests based on their module, class, method, or function name: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -87,7 +87,7 @@ You can also select on the class: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -102,7 +102,7 @@ Or select multiple nodes: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_server.py::TestClass::test_method PASSED [ 50%] @@ -142,7 +142,7 @@ select tests based on their names: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -157,7 +157,7 @@ And you can also run all tests except the ones that match the keyword: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -174,7 +174,7 @@ Or to select "http" and "quick" tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 2 deselected / 2 selected test_server.py::test_send_http PASSED [ 50%] @@ -370,7 +370,7 @@ the test needs: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py s [100%] @@ -385,7 +385,7 @@ and here is one that specifies exactly the environment needed: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py . [100%] @@ -555,7 +555,7 @@ then you will see two tests skipped and two executed tests as expected: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items test_plat.py s.s. [100%] @@ -572,7 +572,7 @@ Note that if you specify a platform via the marker-command line option like this =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items / 3 deselected / 1 selected test_plat.py . [100%] @@ -626,7 +626,7 @@ We can now use the ``-m option`` to select one set: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items / 2 deselected / 2 selected test_module.py FF [100%] @@ -650,7 +650,7 @@ or to select both "event" and "interface" tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items / 1 deselected / 3 selected test_module.py FFF [100%] diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index bf7173ee5..0910071c1 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -31,7 +31,7 @@ now execute the test specification: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR/nonpython, inifile: + rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items test_simple.yml F. [100%] @@ -66,7 +66,7 @@ consulted when reporting in ``verbose`` mode: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR/nonpython, inifile: + rootdir: $REGENDOC_TMPDIR/nonpython collecting ... collected 2 items test_simple.yml::hello FAILED [ 50%] @@ -90,7 +90,7 @@ interesting to just look at the collection tree: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR/nonpython, inifile: + rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index b5d4693ad..4094a780c 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -146,7 +146,7 @@ objects, they are still using the default pytest representation: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 8 items @@ -205,7 +205,7 @@ this is a fully self-contained example which you can run with: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items test_scenarios.py .... [100%] @@ -220,7 +220,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items @@ -287,7 +287,7 @@ Let's first see how it looks like at collection time: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items @@ -353,7 +353,7 @@ The result of this test will be successful: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item @@ -411,8 +411,6 @@ argument sets to use for each test function. Let's run it: def test_equals(self, a, b): > assert a == b E assert 1 == 2 - E -1 - E +2 test_parametrize.py:18: AssertionError 1 failed, 2 passed in 0.12 seconds @@ -490,7 +488,7 @@ If you run this with reporting for skips enabled: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .s [100%] @@ -548,7 +546,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 17 items / 14 deselected / 3 selected test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%] diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 9fcc72ffe..b769edb64 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -15,7 +15,7 @@ get on the terminal - we are working on that): =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR/assertion, inifile: + rootdir: $REGENDOC_TMPDIR/assertion collected 44 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1c6cf36c9..e9fe1f249 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -129,7 +129,7 @@ directory with the above conftest.py: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -190,7 +190,7 @@ and when running it will see a skipped "slow" test: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .s [100%] @@ -207,7 +207,7 @@ Or run it including the ``slow`` marked test: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .. [100%] @@ -351,7 +351,7 @@ which will add the string to the test header accordingly: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache project deps: mylib-1.1 - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -381,7 +381,7 @@ which will add info only when run with "--v": cachedir: $PYTHON_PREFIX/.pytest_cache info1: did you know that ... did you? - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -394,7 +394,7 @@ and nothing when run plainly: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -434,7 +434,7 @@ Now we can profile which test functions execute the slowest: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 3 items test_some_are_slow.py ... [100%] @@ -509,7 +509,7 @@ If we run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 4 items test_step.py .Fx. [100%] @@ -593,7 +593,7 @@ We can run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 7 items test_step.py .Fx. [ 57%] @@ -707,7 +707,7 @@ and run them: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py FF [100%] @@ -811,7 +811,7 @@ and run it: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 3 items test_module.py Esetting up a test failed! test_module.py::test_setup_fails diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 390f8abcc..6cbec6ddc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -74,7 +74,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_smtpsimple.py F [100%] @@ -217,7 +217,7 @@ inspect what is going on and can now run the tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py FF [100%] @@ -710,7 +710,7 @@ Running the above tests results in the following test IDs being used: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 10 items @@ -755,7 +755,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 3 items test_fixture_marks.py::test_data[0] PASSED [ 33%] @@ -800,7 +800,7 @@ Here we declare an ``app`` fixture which receives the previously defined =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] @@ -871,7 +871,7 @@ Let's run the tests in verbose mode and with looking at the print-output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collecting ... collected 8 items test_module.py::test_0[1] SETUP otherarg 1 diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 222ae65ba..0ba19cbba 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -52,7 +52,7 @@ That’s it. You can now execute the test function: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/index.rst b/doc/en/index.rst index 000793d27..3ace95eff 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -30,7 +30,7 @@ To execute it: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 70a35ac44..f704ee98c 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -58,7 +58,7 @@ them in turn: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..F [100%] @@ -125,7 +125,7 @@ Let's run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..x [100%] diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 96af8ee35..549294977 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -335,7 +335,7 @@ Running it with the report-on-xfail option gives this output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR/example, inifile: + rootdir: $REGENDOC_TMPDIR/example collected 7 items xfail_demo.py xxxxxxx [100%] diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index f16b9260c..8583f33b4 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -43,7 +43,7 @@ Running this would result in a passed test except for the last =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_tmp_path.py F [100%] @@ -110,7 +110,7 @@ Running this would result in a passed test except for the last =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_tmpdir.py F [100%] diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 7eb92bf43..05632aef4 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -130,7 +130,7 @@ the ``self.db`` values in the traceback: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 2 items test_unittest_db.py FF [100%] diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 51511673a..d69aa6c07 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -204,7 +204,7 @@ Example: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -256,7 +256,7 @@ More than one character can be used, so for example to only see failed and skipp =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -292,7 +292,7 @@ captured output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 11f73f43e..e82252a92 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -26,7 +26,7 @@ Running pytest now produces this output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR collected 1 item test_show_warnings.py . [100%] From 4621638f0703620ccab416a9b1724746da402463 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 29 Mar 2019 20:29:40 -0300 Subject: [PATCH 95/95] Update CHANGELOG.rst Co-Authored-By: nicoddemus --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3c099b10..533eb9c15 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -173,7 +173,7 @@ Trivial/Internal Changes Feedback is welcome from plugin authors and users alike. -- `#4987 `_: ``Collector.repr_failure`` respects ``--tbstyle``, but only defaults to ``short`` now (with ``auto``). +- `#4987 `_: ``Collector.repr_failure`` respects the ``--tb`` option, but only defaults to ``short`` now (with ``auto``). pytest 4.3.1 (2019-03-11)