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 773e16b1bc0..da4def8f110 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 00000000000..d7d91b899bc --- /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 b286a4f2031..9d309391420 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ '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 00000000000..35d5fffb911 --- /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 00000000000..8b4e019bc13 --- /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 7c7e1132d47..e80969193ab 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 2efb63ae2f2..52360856935 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 b286a4f2031..d9d68c8ae79 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ # 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 4258032b4f1..1da72032bb0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -497,7 +497,7 @@ def consider_pluginarg(self, arg): 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 @@ def _import_plugin_specs(self, spec): 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 @@ def import_plugin(self, modname): 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 d5c4c043a93..8117ee6bcef 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 59771185fa1..17111369b51 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -8,6 +8,7 @@ import textwrap import types +import attr import py import six @@ -108,6 +109,60 @@ def test_option(pytestconfig): 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 1e29b83f1b9..c5c0ca939b5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -5,6 +5,8 @@ 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 6b401aa0bdc..d37728a153a 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -3,6 +3,7 @@ from __future__ import division from __future__ import print_function +import argparse import pdb import sys from doctest import UnexpectedException @@ -12,6 +13,24 @@ 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 43d640614c1..f0cef278885 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -688,6 +688,20 @@ def test_pdb_custom_cls(self, testdir, custom_pdb_calls): 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 301bdedc520..19192c9d907 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -21,6 +21,9 @@ 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 | 41 ++++++++++++++++++++++---------------- testing/test_pdb.py | 17 ++++++++++------ 3 files changed, 39 insertions(+), 23 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 00000000000..274d3991f34 --- /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 d37728a153a..271a590a104 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -12,25 +12,32 @@ 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) - ) +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) + 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 validate_usepdb_cls(value): "--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 f0cef278885..f9a050b109f 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,12 +2,14 @@ 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 @@ def test_pdb_custom_cls_invalid(self, testdir): "*: 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 00000000000..7b532af3ee9 --- /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 fae243a50ca..4aab1a3eb01 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -509,6 +509,7 @@ def __init__(self, request, tmpdir_factory): 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 | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 87b2e5426e2..0c25914bbf1 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -132,24 +132,21 @@ def set(self, key, value): 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) - - 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") - - cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") - if not cachedir_tag_path.is_file(): - cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) + readme_path = self._cachedir / "README.md" + readme_path.write_text(README_CONTENT) + + 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") + 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 b2ad9aae397..fa45dffc129 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -585,11 +585,13 @@ def _compute_fixture_value(self, fixturedef): # 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 @@ def __repr__(self): 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 @@ def finish(self, request): 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 8357991fe17..55dcd8054dd 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -327,6 +327,7 @@ def addfinalizer(self, finalizer, colitem): 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 3d557cec85b..d9a512d7042 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1866,7 +1866,7 @@ def test_finish(): "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 00000000000..44b921ad913 --- /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 d9a512d7042..45c7a17aeab 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -562,6 +562,44 @@ def test_func(something): 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 fa45dffc129..2635d095e2f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -870,7 +870,6 @@ def finish(self, request): 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 55dcd8054dd..8357991fe17 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -327,7 +327,6 @@ def addfinalizer(self, finalizer, colitem): 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 45c7a17aeab..7b328c5b214 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1904,7 +1904,7 @@ def test_finish(): "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 00000000000..6d91fb465c0 --- /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 eda0c0905b8..fc78f4de44e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -586,8 +586,13 @@ def pytest_report_header(self, config): 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 798e8c16a62..abe9549e28c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -567,6 +567,26 @@ def test_passes(): 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 | 14 +++++++------- testing/test_config.py | 6 +++--- testing/test_session.py | 2 +- testing/test_terminal.py | 20 ++++++++++---------- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/changelog/4875.feature.rst b/changelog/4875.feature.rst index 6d91fb465c0..d9fb65ca5bd 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 fc78f4de44e..0868bd751d1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -583,21 +583,21 @@ def _write_report_lines_from_hooks(self, lines): self.write_line(line) def pytest_report_header(self, config): - inifile = "" + line = "rootdir: %s" % config.rootdir + if config.inifile: - inifile = " " + config.rootdir.bestrelpath(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 c5c0ca939b5..3dd707bfa77 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 6b185f76b19..e5eb081d477 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -332,7 +332,7 @@ def test_one(): 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 abe9549e28c..fe67a6aec67 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -570,9 +570,17 @@ def test_passes(): def test_header(self, testdir, request): testdir.tmpdir.join("tests").ensure_dir() testdir.tmpdir.join("gui").ensure_dir() + + # 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:"]) + 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 @@ def test_header(self, testdir, request): ["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 7b328c5b214..1ea37f85c84 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -566,11 +566,11 @@ 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`. + `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 00000000000..a3a08bc1163 --- /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 4aab1a3eb01..d667452d115 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -4,7 +4,6 @@ from __future__ import print_function import codecs -import distutils.spawn import gc import os import platform @@ -151,47 +150,6 @@ def pytest_runtest_protocol(self, item): } -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 00000000000..a1935b46275 --- /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 271a590a104..6dbc0499aa9 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 215015d2721..537c42d0dc0 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 @@ def __init__( 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 @@ def __init__( 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 @@ def _pyfuncitem(self): "(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 6dbc0499aa9..6d51ec59c62 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 537c42d0dc0..e41acfecc12 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1414,11 +1414,6 @@ def __init__( 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 a38af989a96..1105310cf8b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -241,25 +241,20 @@ def getsource(self, astcache=None): 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 6d51ec59c62..5b780d10163 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 00000000000..6f0df93ae44 --- /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 e41acfecc12..d63c2bc2997 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -43,6 +43,7 @@ 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 17111369b51..39bd2fd7733 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 d34cb638cf4..34b6c9d8cb4 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -15,6 +15,7 @@ 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 report_failure(self, out, test, example, got): 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 e7b6b060fd0..09f7c331de9 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -188,6 +188,18 @@ def test_doctest_unexpected_exception(self, testdir): ] ) + 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 85fe6aff01a..d6f6ce82841 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 00000000000..5106e6fb91e --- /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 14c6e9ab6e9..b08dbc7b35b 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 5106e6fb91e..5aef92d76ba 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 b08dbc7b35b..06532dc62a5 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 06532dc62a5..6c2dfb5ae57 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 00000000000..bfc35a4b775 --- /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 810041de7ca..d5faed41820 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -25,6 +25,7 @@ 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 @@ def _match_lines(self, lines2, match_func, match_nickname): 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 675108460d7..08167ec90c6 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -17,6 +17,7 @@ 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_timeout(): ) 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 00000000000..744af129725 --- /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 22eeaa33d61..c4439207341 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -496,6 +496,14 @@ def consider_pluginarg(self, arg): 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 80817932eee..4e0c0ed1e5c 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -346,3 +346,10 @@ def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister( 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 00000000000..846b43f45d2 --- /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 d5faed41820..8f1a7d1d5e5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -469,6 +469,8 @@ def __init__(self, request, tmpdir_factory): 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 2765dfc609d..4748aa58127 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 5b62def2df4..05cb9bd778b 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -470,9 +470,6 @@ def function_1(): ''' """ ) - # 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 7bd319b1a97..867b976b59e 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -91,6 +91,13 @@ def __init__(self, method): 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 @@ def _getcapture(self, method): 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 @@ def resume_fixture(self, item): @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 @@ def close(self): 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 @@ def __init__(self, out=True, err=True, in_=True, Capture=None): 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 91cf8d8cfed..b21c216cdcf 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -566,7 +566,7 @@ def test_hello(capsysbinary): 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 867b976b59e..c6f83454579 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 00000000000..5eb152482c3 --- /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 eabfe88e523..23c7cbdd947 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 @@ def capstderr(self): 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 @@ def __repr__(self): 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 8357991fe17..6ca43884923 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -246,43 +246,7 @@ def __repr__(self): 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 0868bd751d1..3d46ec8968c 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 @@ def pytest_runtest_logstart(self, nodeid, location): 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 @@ def pytest_runtest_logreport(self, report): 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 @@ def mkrel(nodeid): 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 @@ def summary_stats(self): 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 fe67a6aec67..164a33943b5 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -15,6 +15,7 @@ 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 846b43f45d2..00000000000 --- 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 00000000000..1dfbd7e97c1 --- /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 8f1a7d1d5e5..ffcd2982ade 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -29,6 +29,7 @@ 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 @@ def __init__(self, request, tmpdir_factory): 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 @@ def inline_run(self, *args, **kwargs): """ 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 @@ def popen(self, cmdargs, stdout, stderr, **kw): 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 4748aa58127..769e8e8a744 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 00000000000..ade73ce2271 --- /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 c4439207341..b2e4fd774b0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -147,10 +147,15 @@ def directory_arg(path, optname): 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 @@ def cwd_relative_nodeid(self, nodeid): @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 3dd707bfa77..8c2e7a49e23 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,6 +15,7 @@ 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 pytest_addoption(parser): ["*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 3d46ec8968c..291da972353 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -246,7 +246,7 @@ def __init__(self, config, file=None): 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 @@ def pytest_runtest_logfinish(self, nodeid): 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 8c2e7a49e23..50a40a2cb90 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1182,7 +1182,7 @@ def pytest_addoption(parser): 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 291da972353..d15456ee8e7 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -456,8 +456,6 @@ def pytest_runtest_logfinish(self, nodeid): 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 0c25914bbf1..ebba0f9356e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -121,10 +121,12 @@ def set(self, key, value): 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 @@ def set(self, key, value): 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 d15456ee8e7..3537ae445ac 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -26,6 +26,8 @@ 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 @@ def report_collect(self, final=False): 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 164a33943b5..1f3fff8c5c8 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -142,6 +142,31 @@ def test_1(): 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 b2e4fd774b0..2ab1d0790b4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -762,7 +762,7 @@ def _consider_importhook(self, args): 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 d8478d4fc51..3effbfb6ebe 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -548,7 +548,7 @@ def _collect(self, arg): # 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 6ca43884923..1f09f42e8e1 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 3537ae445ac..06604fdf102 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -251,7 +251,7 @@ def _determine_show_progress_info(self): 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 50a40a2cb90..57cbd10eee9 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,6 +15,7 @@ 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 b2e4fd774b0..41519595fd0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -481,7 +481,10 @@ def consider_preparse(self, args): 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 4e0c0ed1e5c..6b44f3e0cc7 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -313,6 +313,9 @@ def test_preparse_args(self, pytestpm): 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 750f93f81d4..c2a6035ab1b 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 712106c94e2..89ed9791f45 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 | 69 ++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index bdfbf823c59..2d57d0494b6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -127,7 +127,7 @@ def test_dont_rewrite_plugin(self, testdir): 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 @@ def f(): 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" - - def test_dont_rewrite_if_hasattr_fails(self): + 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, request): class Y(object): """ A class whos getattr fails, but not with `AttributeError` """ @@ -173,10 +197,16 @@ def __init__(self): 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 @@ def f(): 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 @@ def f(): 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 @@ def __repr__(self): 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 97cac6f6bde..91a09be322b 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 3effbfb6ebe..e08b1fa1dfb 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 35a7232ab0e..082b097ee91 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -884,7 +884,7 @@ def test_always_passes(): 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 bd89d024dce..7c5ee2e4bd8 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 39bd2fd7733..1342a416eb6 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -677,6 +677,8 @@ def test_cmdline_python_package(self, testdir, monkeypatch): 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 00000000000..1c42547c12c --- /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 46d9718da7f..f6c13466433 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -262,10 +262,15 @@ def delenv(self, name, raising=True): 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 0a953d3f1bf..d43fb6babdf 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 bc7462674e3..df6070b8d1d 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -34,8 +34,6 @@ def test_import_duplicate(self, testdir): ) 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 bdfbf823c59..a92ffcf7514 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1335,7 +1335,7 @@ def test_cwd_changed(self, testdir, monkeypatch): # 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 1c42547c12c..40f1de9c13f 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 ffcd2982ade..bc1405176a7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -593,11 +593,16 @@ def syspathinsert(self, path=None): 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 bc1405176a7..f8a79ebc93b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -611,12 +611,10 @@ def syspathinsert(self, path=None): 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 6b44f3e0cc7..10b54c1125d 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -4,7 +4,6 @@ from __future__ import print_function import os -import re import sys import types @@ -165,10 +164,10 @@ def test_traceback(): 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 79224deef8b..6c4d24398d2 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -18,10 +18,10 @@ 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 __repr__(self): 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 cb5f5ccd306..33862799b39 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 e5eb081d477..20079cd1be7 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -68,9 +68,7 @@ def test_raises_doesnt(): 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 @@ def test_one(): pass ) 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 1105310cf8b..79845db7339 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -413,6 +413,7 @@ def from_current(cls, exprinfo=None): 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 3362d46042b..81a87481cda 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -172,6 +172,10 @@ def test_bad_getsource(self): 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 39bd2fd7733..bed7d708077 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 536370f9229..4818379fee3 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 90db8813e9e..4f30157f49a 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -747,7 +747,7 @@ def test_log_file(): """ ) 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 bc7462674e3..860e164b624 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -560,7 +560,7 @@ def test_skip_if(x): """ ) 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 @@ def test_skip(x): """ ) 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 @@ def test_skipif_no_skip(x): """ ) 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 @@ def test_xfail(x): """ ) 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 @@ def test_xfail(x): """ ) 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 @@ def test_passed(x): """ ) 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 @@ def test_something(): ) # 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_real(): 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 1ea37f85c84..70917946e26 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -536,7 +536,7 @@ def test_func(): """ ) 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 @@ def test_func(resource): """ ) 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 @@ def test_request_fixturenames_dynamic_fixture(self, testdir): """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 @@ def test_package(one): 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 @@ def test_browser(browser): ) 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 @@ def test_1(meow): """ ) 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 bdfbf823c59..a331bcc6e68 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -796,7 +796,7 @@ def test_rewrite_warning(self, testdir): ) # 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 @@ def test_foo(self): ) 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 @@ def test_ternary_display(): """ ) 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 @@ def test_ternary_display(): """ ) 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 @@ def test_ternary_display(): """ ) 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 @@ def test_simple_failure(): """ ) 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 @@ def test(): } ) 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 35a7232ab0e..7639d332e5c 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -393,7 +393,7 @@ def test_fail(val): """ ) 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 @@ def test(): """ ) 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 @@ def test(): """ ) 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 @@ def test_bar_2(): """ ) 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 @@ def test_foo_4(): """ ) 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 fa0bad5fca0..c99b843c0d9 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -673,7 +673,7 @@ def test_captured_print(captured_print): ) ) 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 37f7ad89c45..94f57c69d86 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -350,10 +350,10 @@ def pytest_ignore_collect(path, config): 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 @@ def pytest_configure(config): 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 57cbd10eee9..07654e5ad98 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 09f7c331de9..db6c4c2d353 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -968,7 +968,7 @@ def bar(): """ ) 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 6f3d292dd98..2fe49177ae8 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -380,4 +380,4 @@ def test_io(self): """ ) 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 66ae702a44c..72484fb7250 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -640,7 +640,7 @@ def test_hello(): 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_foo(): """ ) 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 33878c8f47d..e5206a44ebf 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -331,7 +331,7 @@ def test_this(): 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 @@ def test_foo(): % 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 @@ def test_foo(): % 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 @@ def test_foo(): ) 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_func(): """ ) 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_func(): """ ) 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_func(): """ ) 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 22a5bf7962b..3b7a4ddc3d0 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -16,7 +16,7 @@ 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 fe33855fa29..a519ec2550b 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -794,7 +794,7 @@ def test_classattr(self): ) ) 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 3effbfb6ebe..4e3700b1e14 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -441,6 +441,15 @@ def __init__(self, config): 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 e5eb081d477..b3aaf571d15 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -126,14 +126,14 @@ def test_implicit_bad_repr1(self): ) 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 76498573694..3360aea9cca 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 90db8813e9e..744a384f7d7 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 1f3fff8c5c8..e8590b1ba21 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -660,7 +660,6 @@ def check(x): ) 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 744a384f7d7..c64c4cb5666 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -67,15 +67,14 @@ def test_foo(): 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 c64c4cb5666..f5cd2a2ce02 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_1(): ) 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_1(): "=* 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 ce02e70cdbf..fcaf59312f9 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -325,7 +325,14 @@ def repr_failure(self, excinfo): 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 37f7ad89c45..b6e0ce4acf9 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -11,6 +11,7 @@ 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 23c7cbdd947..5f0777375c6 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 @@ def head_line(self): 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 00000000000..322995326a9 --- /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 ca2bebabcf5..4ed9deac47a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -140,6 +140,7 @@ def directory_arg(path, optname): "stepwise", "warnings", "logging", + "reports", ) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 0641e3bc5ac..a465923f906 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 5f0777375c6..b4160eb9621 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -404,3 +404,19 @@ def __init__(self, msg): 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 322995326a9..3413a805bf8 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -181,3 +181,53 @@ def test_extended_report_deserialization(self, testdir): 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 b4160eb9621..04cc5269145 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 a465923f906..afcee53979f 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 04cc5269145..74faa3f8c2d 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 00000000000..487a22c7acd --- /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 3413a805bf8..f37ead893a0 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -3,11 +3,12 @@ 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 @@ def test_b(): pass 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 @@ def test_fail(): assert False, 'Expected Message' 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 @@ def test_repr_entry(): 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 @@ def test_repr_entry_native(): 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 @@ def test_xfail_imperative(): 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 @@ def test_collectreport_passed(self, testdir): 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 @@ def test_collectreport_fail(self, testdir): 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 74faa3f8c2d..8ab42478f2e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -11,6 +11,7 @@ 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 @@ def disassembled_report(rep): 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 f37ead893a0..2f9162e101d 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 @@ def test_xdist_longrepr_to_str_issue_241(self, testdir): """ testdir.makepyfile( """ - import os def test_a(): assert False def test_b(): pass """ @@ -35,8 +35,8 @@ def test_xdist_report_longrepr_reprcrash_130(self, testdir): """ 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 @@ def test_extended_report_deserialization(self, testdir): 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 8ab42478f2e..cd7a0b57fc0 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 @@ def _from_json(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) diff --git a/testing/test_reports.py b/testing/test_reports.py index 2f9162e101d..879a9098dbe 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 @@ def test_a(): 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 00000000000..eb79b742a91 --- /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 00000000000..26c93ec18b5 --- /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 a40dfc223fd..8d6a06a4427 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 6326dddbdc4..c7a46cba000 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -151,7 +151,7 @@ def isiterable(obj): 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 isiterable(obj): 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 escape_for_readable_diff(binary_text): 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 ebba0f9356e..ef2039539ac 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 2635d095e2f..d3223a74e6d 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 22db4430102..e395bc435fc 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -389,7 +389,7 @@ def __init__(self, config): 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 1f3fff8c5c8..72af2fa00bb 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -25,15 +25,14 @@ 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 @@ def args(self): @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 @@ def test_func(): """ ) 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 @@ def test_func(): "*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 | 58 ++++++++++++++++++++--------------- testing/test_assertion.py | 11 +++++++ 3 files changed, 46 insertions(+), 24 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 00000000000..259daa8daa6 --- /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 6326dddbdc4..fef0448734c 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -9,6 +9,7 @@ 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,38 +103,45 @@ def _format_lines(lines): basestring = str -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 - left_repr = saferepr(left, maxsize=int(width // 2)) - right_repr = saferepr(right, maxsize=width - len(left_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) - 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 isset(x): + return isinstance(x, (set, frozenset)) - def istext(x): - return isinstance(x, basestring) - def isdict(x): - return isinstance(x, dict) +def isdatacls(obj): + return getattr(obj, "__dataclass_fields__", None) is not None - 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 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 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 + left_repr = saferepr(left, maxsize=int(width // 2)) + right_repr = saferepr(right, maxsize=width - len(left_repr)) + + summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) verbose = config.getoption("verbose") explanation = None @@ -162,6 +170,8 @@ def isiterable(obj): 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 e4fe56c6fdf..330b711afb7 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -11,6 +11,7 @@ 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 f(): "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 00000000000..b8e1ff49477 --- /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 1efa2e7ca74..68e53a31cb4 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 @@ def pytest_sessionstart(self, session): 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 @@ def pytest_collection_modifyitems(self, session, config, items): # 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 @@ def pytest_runtest_logreport(self, report): 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 a28e95edae5..22acafbddfb 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 show_(terminalreporter, lines): 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 cd7a0b57fc0..679d72c9d95 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 879a9098dbe..22d5fce34dd 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 @@ def test_b(): pass def test_collect_report(self, testdir, pytestconfig): testdir.makepyfile( """ - import os def test_a(): assert False def test_b(): pass """ @@ -291,3 +289,25 @@ def test_b(): pass 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 00000000000..8cc816ed625 --- /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 22db4430102..5a31dfc5c39 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -577,8 +577,15 @@ def pytest_sessionfinish(self): 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 52360856935..51511673a30 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 d6f6ce82841..ea6fc5caca3 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 00000000000..37d5138bf93 --- /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 9a237dcd7fd..912ae1898f7 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 5b289c7c86f..8532837a59e 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 8532837a59e..9e439226475 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 912ae1898f7..70a35ac4404 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 00000000000..8d18a50e664 --- /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 887f323f969..39701a39bb0 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 d65d8b9d806..0021dd5d66e 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 f7d8cf6891a..2851dbc16c1 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,6 +8,7 @@ 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) + + p1 = testdir.makepyfile( + """ + import pytest - with pytest.raises(Collector.CollectError, match=expected_errmsg): - get_empty_parameterset_mark(config, ["a"], test_func) + @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 c140757c4fd..3f6055bd815 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -107,6 +107,16 @@ def _getcapture(self, method): 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 @@ def suspend_global_capture(self, in_=False): 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 5b780d10163..abe4f65b656 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -109,7 +109,7 @@ def _init_pdb(cls, *args, **kwargs): 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 @@ def _init_pdb(cls, *args, **kwargs): 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 @@ def do_continue(self, arg): 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 05cb9bd778b..e6b5fecdaf4 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -970,3 +970,52 @@ def test_2(): 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 abe4f65b656..5e859f0e3b6 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -146,22 +146,25 @@ def do_debug(self, arg): 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 e6b5fecdaf4..6d4fd18e8d6 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -576,7 +576,8 @@ def test_1(): 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 6d4fd18e8d6..74d22e7efad 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -604,6 +604,57 @@ def test_1(): 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 3f6055bd815..0e8a693e898 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -145,6 +145,7 @@ def suspend_global_capture(self, in_=False): 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 @@ def resume_fixture(self, item): @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 74d22e7efad..d5cf17ef9ba 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -604,52 +604,93 @@ def test_1(): 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 5e859f0e3b6..bb90d00ca88 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 @@ def _init_pdb(cls, *args, **kwargs): 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 @@ def do_continue(self, arg): 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 @@ def do_continue(self, arg): "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 00000000000..b40e03af5ca --- /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 487a22c7acd..36db733f9fa 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 afcee53979f..65a7f43edbf 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 679d72c9d95..d2df4d21f1e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -409,14 +409,14 @@ def toterminal(self, out): 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 22d5fce34dd..6d2b167f871 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -257,11 +257,11 @@ def test_b(): pass 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 @@ def test_b(): pass 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 @@ def test_a(): pass 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 00000000000..17d2770feee --- /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 a0f16134d1e..fa202447013 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 07654e5ad98..28cc3ab9173 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -46,6 +46,22 @@ def test_getcfg_empty_path(self): """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 00000000000..9ff61652eb4 --- /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 bb90d00ca88..cb1c964c35b 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -176,8 +176,15 @@ def do_continue(self, arg): 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 d5cf17ef9ba..531846e8e13 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -519,7 +519,10 @@ def test_1(): 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 @@ def test_pdb_interaction_continue_recursive(self, testdir): 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 @@ def test_1(): 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 839b4c439c5..c3c099b1061 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 44b921ad913..00000000000 --- 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 6f0df93ae44..00000000000 --- 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 37d5138bf93..00000000000 --- 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 35d5fffb911..00000000000 --- 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 8b4e019bc13..00000000000 --- 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 d7d91b899bc..00000000000 --- 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 a1935b46275..00000000000 --- 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 7b532af3ee9..00000000000 --- 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 274d3991f34..00000000000 --- 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 d9fb65ca5bd..00000000000 --- 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 a3a08bc1163..00000000000 --- 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 116e1b0fd77..00000000000 --- 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 5aef92d76ba..00000000000 --- 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 9600c833b54..00000000000 --- 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 7846775cc94..00000000000 --- 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 5eb152482c3..00000000000 --- 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 8959efacba2..00000000000 --- 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 bfc35a4b775..00000000000 --- 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 744af129725..00000000000 --- 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 b40e03af5ca..00000000000 --- 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 1dfbd7e97c1..00000000000 --- 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 ade73ce2271..00000000000 --- 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 36db733f9fa..00000000000 --- 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 9ff61652eb4..00000000000 --- 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 74799c9b3ed..00000000000 --- 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 26c93ec18b5..00000000000 --- 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 259daa8daa6..00000000000 --- 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 40f1de9c13f..00000000000 --- 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 eb79b742a91..00000000000 --- 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 8cc816ed625..00000000000 --- 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 b8e1ff49477..00000000000 --- 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 8d18a50e664..00000000000 --- 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 17d2770feee..00000000000 --- 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 96123f3fbf4..7e255465629 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 00000000000..4c5bcbc7d35 --- /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 e7e78601b06..996ad02a845 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 8d6a06a4427..fb16140c055 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 89a1e26340a..40202eb76d5 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 f62ec60ca1c..8629350a50a 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 5ec57caeddc..aeb1fdfdea2 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 bf7173ee552..0910071c14a 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 b5d4693ad88..4094a780c77 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 9fcc72ffea5..b769edb6480 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 1c6cf36c9c3..e9fe1f2490f 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 390f8abccbc..6cbec6ddc6e 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 222ae65bae4..0ba19cbba85 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 000793d274d..3ace95effe6 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 70a35ac4404..f704ee98c92 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 96af8ee3527..54929497742 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 f16b9260c36..8583f33b475 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 7eb92bf43db..05632aef4b2 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 51511673a30..d69aa6c0728 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 11f73f43e52..e82252a921c 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 c3c099b1061..533eb9c1522 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)