From 757ada2fd2d0f9c5e6026f8ab2ef3c1415e1ade8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 3 Apr 2019 21:37:27 +0200 Subject: [PATCH 01/57] pdb: try to import --pdbcls in pytest_configure only Fixes https://github.com/pytest-dev/pytest/issues/5039. --- changelog/5039.bugfix.rst | 1 + src/_pytest/debugging.py | 40 ++++++++++++++++++++--------------- testing/test_pdb.py | 44 ++++++++++++++++++++++++++++++++------- 3 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 changelog/5039.bugfix.rst diff --git a/changelog/5039.bugfix.rst b/changelog/5039.bugfix.rst new file mode 100644 index 000000000..4e173f64e --- /dev/null +++ b/changelog/5039.bugfix.rst @@ -0,0 +1 @@ +Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index cb1c964c3..3bfd5465f 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -10,31 +10,18 @@ from doctest import UnexpectedException from _pytest import outcomes from _pytest.config import hookimpl +from _pytest.config.exceptions import UsageError def _validate_usepdb_cls(value): + """Validate syntax of --pdbcls option.""" 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) - ) + return (modname, classname) def pytest_addoption(parser): @@ -61,9 +48,28 @@ def pytest_addoption(parser): ) +def _import_pdbcls(modname, classname): + 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: + value = ":".join((modname, classname)) + raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc)) + + def pytest_configure(config): pdb_cls = config.getvalue("usepdb_cls") - if not pdb_cls: + if pdb_cls: + pdb_cls = _import_pdbcls(*pdb_cls) + else: pdb_cls = pdb.Pdb if config.getvalue("trace"): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 531846e8e..61f33950d 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -import argparse import os import platform import sys @@ -803,13 +802,12 @@ class TestPDB(object): ) def test_pdb_validate_usepdb_cls(self, testdir): - assert _validate_usepdb_cls("os.path:dirname.__name__") == "dirname" + assert _validate_usepdb_cls("os.path:dirname.__name__") == ( + "os.path", + "dirname.__name__", + ) - with pytest.raises( - argparse.ArgumentTypeError, - match=r"^could not get pdb class for 'pdb:DoesNotExist': .*'DoesNotExist'", - ): - _validate_usepdb_cls("pdb:DoesNotExist") + assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist") def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) @@ -1121,3 +1119,35 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture): assert child.exitstatus == 0 assert "= 1 passed in " in rest assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest + + +def test_pdbcls_via_local_module(testdir): + """It should be imported in pytest_configure or later only.""" + p1 = testdir.makepyfile( + """ + def test(): + print("before_settrace") + __import__("pdb").set_trace() + """, + mypdb=""" + class Wrapped: + class MyPdb: + def set_trace(self, *args): + print("mypdb_called", args) + """, + ) + result = testdir.runpytest( + str(p1), "--pdbcls=really.invalid:Value", syspathinsert=True + ) + result.stderr.fnmatch_lines( + [ + "ERROR: --pdbcls: could not import 'really.invalid:Value': No module named *really*" + ] + ) + assert result.ret == 4 + + result = testdir.runpytest( + str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True + ) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*mypdb_called*", "* 1 passed in *"]) From 5fec793bc7d0e70b8e7a2a5ff384d254204fff05 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 2 Apr 2019 17:21:14 +0200 Subject: [PATCH 02/57] _compare_eq_sequence: display number of extra items --- src/_pytest/assertion/util.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index ab01c314c..231dd040d 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -285,19 +285,22 @@ def _compare_eq_iterable(left, right, verbose=0): def _compare_eq_sequence(left, right, verbose=0): explanation = [] - for i in range(min(len(left), len(right))): + len_left = len(left) + len_right = len(right) + for i in range(min(len_left, len_right)): if left[i] != right[i]: explanation += [u"At index %s diff: %r != %r" % (i, left[i], right[i])] break - if len(left) > len(right): + len_diff = len_left - len_right + if len_diff > 0: explanation += [ - u"Left contains more items, first extra item: %s" - % saferepr(left[len(right)]) + u"Left contains %d more items, first extra item: %s" + % (len_diff, saferepr(left[len_right])) ] - elif len(left) < len(right): + elif len_diff < 0: explanation += [ - u"Right contains more items, first extra item: %s" - % saferepr(right[len(left)]) + u"Right contains %d more items, first extra item: %s" + % (0 - len_diff, saferepr(right[len_left])) ] return explanation From 7f1bf44aa83c4d7d67d5f14fadd2e147e913ed99 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 2 Apr 2019 17:25:14 +0200 Subject: [PATCH 03/57] _compare_eq_dict: display number of different items --- src/_pytest/assertion/util.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 231dd040d..08507a924 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -322,7 +322,9 @@ def _compare_eq_set(left, right, verbose=0): def _compare_eq_dict(left, right, verbose=0): explanation = [] - common = set(left).intersection(set(right)) + set_left = set(left) + set_right = set(right) + common = set_left.intersection(set_right) same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: explanation += [u"Omitting %s identical items, use -vv to show" % len(same)] @@ -334,15 +336,15 @@ def _compare_eq_dict(left, right, verbose=0): explanation += [u"Differing items:"] for k in diff: explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] - extra_left = set(left) - set(right) + extra_left = set_left - set_right if extra_left: - explanation.append(u"Left contains more items:") + explanation.append(u"Left contains %d more items:" % len(extra_left)) explanation.extend( pprint.pformat({k: left[k] for k in extra_left}).splitlines() ) - extra_right = set(right) - set(left) + extra_right = set_right - set_left if extra_right: - explanation.append(u"Right contains more items:") + explanation.append(u"Right contains %d more items:" % len(extra_right)) explanation.extend( pprint.pformat({k: right[k] for k in extra_right}).splitlines() ) From 47d92a0d96e9643402f7d22a73a797ebcde4403e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 17:53:39 +0200 Subject: [PATCH 04/57] Add tests and improve messages --- src/_pytest/assertion/util.py | 43 +++++++++++++++++++++++----------- testing/test_assertion.py | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 08507a924..a62297075 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -292,16 +292,23 @@ def _compare_eq_sequence(left, right, verbose=0): explanation += [u"At index %s diff: %r != %r" % (i, left[i], right[i])] break len_diff = len_left - len_right - if len_diff > 0: - explanation += [ - u"Left contains %d more items, first extra item: %s" - % (len_diff, saferepr(left[len_right])) - ] - elif len_diff < 0: - explanation += [ - u"Right contains %d more items, first extra item: %s" - % (0 - len_diff, saferepr(right[len_left])) - ] + + if len_diff: + if len_diff > 0: + dir_with_more = "Left" + extra = saferepr(left[len_right]) + elif len_diff < 0: + len_diff = 0 - len_diff + dir_with_more = "Right" + extra = saferepr(right[len_left]) + + if len_diff == 1: + explanation += [u"%s contains one more item: %s" % (dir_with_more, extra)] + else: + explanation += [ + u"%s contains %d more items, first extra item: %s" + % (dir_with_more, len_diff, extra) + ] return explanation @@ -337,14 +344,22 @@ def _compare_eq_dict(left, right, verbose=0): for k in diff: explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] extra_left = set_left - set_right - if extra_left: - explanation.append(u"Left contains %d more items:" % len(extra_left)) + len_extra_left = len(extra_left) + if len_extra_left: + explanation.append( + u"Left contains %d more item%s:" + % (len_extra_left, "" if len_extra_left == 1 else "s") + ) explanation.extend( pprint.pformat({k: left[k] for k in extra_left}).splitlines() ) extra_right = set_right - set_left - if extra_right: - explanation.append(u"Right contains %d more items:" % len(extra_right)) + len_extra_right = len(extra_right) + if len_extra_right: + explanation.append( + u"Right contains %d more item%s:" + % (len_extra_right, "" if len_extra_right == 1 else "s") + ) explanation.extend( pprint.pformat({k: right[k] for k in extra_right}).splitlines() ) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 330b711af..8a59b7e8d 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -446,6 +446,50 @@ class TestAssert_reprcompare(object): assert "Omitting" not in lines[1] assert lines[2] == "{'b': 1}" + def test_dict_different_items(self): + lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2) + assert lines == [ + "{'a': 0} == {'b': 1, 'c': 2}", + "Left contains 1 more item:", + "{'a': 0}", + "Right contains 2 more items:", + "{'b': 1, 'c': 2}", + "Full diff:", + "- {'a': 0}", + "+ {'b': 1, 'c': 2}", + ] + lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2) + assert lines == [ + "{'b': 1, 'c': 2} == {'a': 0}", + "Left contains 2 more items:", + "{'b': 1, 'c': 2}", + "Right contains 1 more item:", + "{'a': 0}", + "Full diff:", + "- {'b': 1, 'c': 2}", + "+ {'a': 0}", + ] + + def test_sequence_different_items(self): + lines = callequal((1, 2), (3, 4, 5), verbose=2) + assert lines == [ + "(1, 2) == (3, 4, 5)", + "At index 0 diff: 1 != 3", + "Right contains one more item: 5", + "Full diff:", + "- (1, 2)", + "+ (3, 4, 5)", + ] + lines = callequal((1, 2, 3), (4,), verbose=2) + assert lines == [ + "(1, 2, 3) == (4,)", + "At index 0 diff: 1 != 4", + "Left contains 2 more items, first extra item: 2", + "Full diff:", + "- (1, 2, 3)", + "+ (4,)", + ] + def test_set(self): expl = callequal({0, 1}, {0, 2}) assert len(expl) > 1 From eb5b2e0db5fa0f1c7fe04aa54b0d1e38f506efc3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 3 Apr 2019 15:56:42 +0200 Subject: [PATCH 05/57] Support glob argument with ``--cache-show`` --- changelog/5035.feature.rst | 1 + doc/en/cache.rst | 23 ++++++++++++++++++++--- src/_pytest/cacheprovider.py | 21 +++++++++++++++------ testing/test_cacheprovider.py | 32 ++++++++++++++++++++++++++------ 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 changelog/5035.feature.rst diff --git a/changelog/5035.feature.rst b/changelog/5035.feature.rst new file mode 100644 index 000000000..36211f9f4 --- /dev/null +++ b/changelog/5035.feature.rst @@ -0,0 +1 @@ +The ``--cache-show`` option/action accepts an optional glob to show only matching cache entries. diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 8baf88113..e47dce44c 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -247,7 +247,7 @@ See the :ref:`cache-api` for more details. Inspecting Cache content -------------------------------- +------------------------ You can always peek at the content of the cache using the ``--cache-show`` command line option: @@ -260,7 +260,7 @@ You can always peek at the content of the cache using the cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: /home/sweet/project cachedir: $PYTHON_PREFIX/.pytest_cache - ------------------------------- cache values ------------------------------- + --------------------------- cache values for '*' --------------------------- cache/lastfailed contains: {'test_50.py::test_num[17]': True, 'test_50.py::test_num[25]': True, @@ -277,8 +277,25 @@ You can always peek at the content of the cache using the ======================= no tests ran in 0.12 seconds ======================= +``--cache-show`` takes an optional argument to specify a glob pattern for +filtering: + +.. code-block:: pytest + + $ pytest --cache-show 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: + cachedir: $PYTHON_PREFIX/.pytest_cache + ----------------------- cache values for 'example/*' ----------------------- + example/value contains: + 42 + + ======================= no tests ran in 0.12 seconds ======================= + Clearing Cache content -------------------------------- +---------------------- You can instruct pytest to clear all cache files and values by adding the ``--cache-clear`` option like this: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 246b8dfd8..a1200ce37 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -292,9 +292,13 @@ def pytest_addoption(parser): ) group.addoption( "--cache-show", - action="store_true", + action="append", + nargs="?", dest="cacheshow", - help="show cache contents, don't perform collection or tests", + help=( + "show cache contents, don't perform collection or tests. " + "Optional argument: glob (default: '*')." + ), ) group.addoption( "--cache-clear", @@ -369,11 +373,16 @@ def cacheshow(config, session): if not config.cache._cachedir.is_dir(): tw.line("cache is empty") return 0 + + glob = config.option.cacheshow[0] + if glob is None: + glob = "*" + dummy = object() basedir = config.cache._cachedir vdir = basedir / "v" - tw.sep("-", "cache values") - for valpath in sorted(x for x in vdir.rglob("*") if x.is_file()): + tw.sep("-", "cache values for %r" % glob) + for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): key = valpath.relative_to(vdir) val = config.cache.get(key, dummy) if val is dummy: @@ -385,8 +394,8 @@ def cacheshow(config, session): ddir = basedir / "d" if ddir.is_dir(): - contents = sorted(ddir.rglob("*")) - tw.sep("-", "cache directories") + contents = sorted(ddir.rglob(glob)) + tw.sep("-", "cache directories for %r" % glob) for p in contents: # if p.check(dir=1): # print("%s/" % p.relto(basedir)) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 85603edf4..41e7ffd79 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -196,6 +196,7 @@ def test_cache_show(testdir): """ def pytest_configure(config): config.cache.set("my/name", [1,2,3]) + config.cache.set("my/hello", "world") config.cache.set("other/some", {1:2}) dp = config.cache.makedir("mydb") dp.ensure("hello") @@ -204,20 +205,39 @@ def test_cache_show(testdir): ) result = testdir.runpytest() assert result.ret == 5 # no tests executed + result = testdir.runpytest("--cache-show") - result.stdout.fnmatch_lines_random( + result.stdout.fnmatch_lines( [ "*cachedir:*", - "-*cache values*-", - "*my/name contains:", + "*- cache values for '[*]' -*", + "cache/nodeids contains:", + "my/name contains:", " [1, 2, 3]", - "*other/some contains*", - " {*1*: 2}", - "-*cache directories*-", + "other/some contains:", + " {*'1': 2}", + "*- cache directories for '[*]' -*", "*mydb/hello*length 0*", "*mydb/world*length 0*", ] ) + assert result.ret == 0 + + result = testdir.runpytest("--cache-show", "*/hello") + result.stdout.fnmatch_lines( + [ + "*cachedir:*", + "*- cache values for '[*]/hello' -*", + "my/hello contains:", + " *'world'", + "*- cache directories for '[*]/hello' -*", + "d/mydb/hello*length 0*", + ] + ) + stdout = result.stdout.str() + assert "other/some" not in stdout + assert "d/mydb/world" not in stdout + assert result.ret == 0 class TestLastFailed(object): From 66f743c45a50b72433f850ed761cd6865acd2aec Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 17:07:15 +0200 Subject: [PATCH 06/57] Fix test_conftest when run via pytest-randomly --- testing/test_conftest.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index ac091fed8..931d4d2ac 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -11,17 +11,8 @@ from _pytest.config import PytestPluginManager from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_OK from _pytest.main import EXIT_USAGEERROR - - -@pytest.fixture(scope="module", params=["global", "inpackage"]) -def basedir(request, tmpdir_factory): - tmpdir = tmpdir_factory.mktemp("basedir", numbered=True) - tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3") - tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") - if request.param == "inpackage": - tmpdir.ensure("adir/__init__.py") - tmpdir.ensure("adir/b/__init__.py") - return tmpdir +from _pytest.pytester import SysModulesSnapshot +from _pytest.pytester import SysPathsSnapshot def ConftestWithSetinitial(path): @@ -42,6 +33,26 @@ def conftest_setinitial(conftest, args, confcutdir=None): class TestConftestValueAccessGlobal(object): + @pytest.fixture(scope="module", params=["global", "inpackage"]) + def basedir(self, request, tmpdir_factory): + tmpdir = tmpdir_factory.mktemp("basedir", numbered=True) + tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3") + tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") + if request.param == "inpackage": + tmpdir.ensure("adir/__init__.py") + tmpdir.ensure("adir/b/__init__.py") + + yield tmpdir + + @pytest.fixture(autouse=True) + def restore(self): + """Restore sys.modules to prevent ConftestImportFailure when run randomly.""" + snapmods = SysModulesSnapshot() + snappath = SysPathsSnapshot() + yield + snapmods.restore() + snappath.restore() + def test_basic_init(self, basedir): conftest = PytestPluginManager() p = basedir.join("adir") From 899e74aa14d3757c76b7564689857f58897c2ced Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 10:59:08 +0200 Subject: [PATCH 07/57] tests: harden test_immediate_initialiation_and_incremental_are_the_same --- testing/test_conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 931d4d2ac..2342cb318 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -60,10 +60,10 @@ class TestConftestValueAccessGlobal(object): def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = PytestPluginManager() - len(conftest._dirpath2confmods) + assert not len(conftest._dirpath2confmods) conftest._getconftestmodules(basedir) snap1 = len(conftest._dirpath2confmods) - # assert len(conftest._dirpath2confmods) == snap1 + 1 + assert snap1 == 1 conftest._getconftestmodules(basedir.join("adir")) assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._getconftestmodules(basedir.join("b")) From 8011ff5bda074ca1b6419a768ec32277203106e6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 11:31:02 +0200 Subject: [PATCH 08/57] Add _sys_snapshot fixture and use it with more tests --- src/_pytest/pytester.py | 9 +++++++++ testing/acceptance_test.py | 2 +- testing/code/test_excinfo.py | 2 +- testing/code/test_source.py | 2 +- testing/test_config.py | 8 ++++---- testing/test_conftest.py | 14 ++------------ 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a802a56f0..31cea5923 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -335,6 +335,15 @@ def testdir(request, tmpdir_factory): return Testdir(request, tmpdir_factory) +@pytest.fixture +def _sys_snapshot(): + snappaths = SysPathsSnapshot() + snapmods = SysModulesSnapshot() + yield + snapmods.restore() + snappaths.restore() + + @pytest.fixture def _config_for_test(): from _pytest.config import get_config diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 408fa076e..13a765411 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -485,7 +485,7 @@ class TestGeneralUsage(object): ["*source code not available*", "E*fixture 'invalid_fixture' not found"] ) - def test_plugins_given_as_strings(self, tmpdir, monkeypatch): + def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot): """test that str values passed to main() as `plugins` arg are interpreted as module names to be imported and registered. #855. diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 4e36fb946..5a4ab8808 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -441,7 +441,7 @@ def test_match_raises_error(testdir): class TestFormattedExcinfo(object): @pytest.fixture - def importasmod(self, request): + def importasmod(self, request, _sys_snapshot): def importasmod(source): source = textwrap.dedent(source) tmpdir = request.getfixturevalue("tmpdir") diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 965838dae..aa56273c4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -410,7 +410,7 @@ def test_deindent(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir): +def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = _pytest._code.Source( diff --git a/testing/test_config.py b/testing/test_config.py index c90333a00..68c948a41 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -436,7 +436,7 @@ class TestConfigAPI(object): class TestConfigFromdictargs(object): - def test_basic_behavior(self): + def test_basic_behavior(self, _sys_snapshot): from _pytest.config import Config option_dict = {"verbose": 444, "foo": "bar", "capture": "no"} @@ -450,7 +450,7 @@ class TestConfigFromdictargs(object): assert config.option.capture == "no" assert config.args == args - def test_origargs(self): + def test_origargs(self, _sys_snapshot): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config @@ -1057,7 +1057,7 @@ class TestOverrideIniArgs(object): assert rootdir == tmpdir assert inifile is None - def test_addopts_before_initini(self, monkeypatch, _config_for_test): + def test_addopts_before_initini(self, monkeypatch, _config_for_test, _sys_snapshot): cache_dir = ".custom_cache" monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir) config = _config_for_test @@ -1092,7 +1092,7 @@ class TestOverrideIniArgs(object): ) assert result.ret == _pytest.main.EXIT_USAGEERROR - def test_override_ini_does_not_contain_paths(self, _config_for_test): + def test_override_ini_does_not_contain_paths(self, _config_for_test, _sys_snapshot): """Check that -o no longer swallows all options after it (#3103)""" config = _config_for_test config._preparse(["-o", "cache_dir=/cache", "/some/test/path"]) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 2342cb318..2596dafe9 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -11,8 +11,6 @@ from _pytest.config import PytestPluginManager from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_OK from _pytest.main import EXIT_USAGEERROR -from _pytest.pytester import SysModulesSnapshot -from _pytest.pytester import SysPathsSnapshot def ConftestWithSetinitial(path): @@ -32,6 +30,7 @@ def conftest_setinitial(conftest, args, confcutdir=None): conftest._set_initial_conftests(Namespace()) +@pytest.mark.usefixtures("_sys_snapshot") class TestConftestValueAccessGlobal(object): @pytest.fixture(scope="module", params=["global", "inpackage"]) def basedir(self, request, tmpdir_factory): @@ -44,15 +43,6 @@ class TestConftestValueAccessGlobal(object): yield tmpdir - @pytest.fixture(autouse=True) - def restore(self): - """Restore sys.modules to prevent ConftestImportFailure when run randomly.""" - snapmods = SysModulesSnapshot() - snappath = SysPathsSnapshot() - yield - snapmods.restore() - snappath.restore() - def test_basic_init(self, basedir): conftest = PytestPluginManager() p = basedir.join("adir") @@ -91,7 +81,7 @@ class TestConftestValueAccessGlobal(object): assert path.purebasename.startswith("conftest") -def test_conftest_in_nonpkg_with_init(tmpdir): +def test_conftest_in_nonpkg_with_init(tmpdir, _sys_snapshot): tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3") tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5") tmpdir.ensure("adir-1.0/b/__init__.py") From 0f965e57a23313b0020e1c9737380dbe18b6d088 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 12:12:29 +0200 Subject: [PATCH 09/57] changelog, fix branch coverage --- changelog/5026.feature.rst | 1 + src/_pytest/assertion/util.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/5026.feature.rst diff --git a/changelog/5026.feature.rst b/changelog/5026.feature.rst new file mode 100644 index 000000000..aa0f3cbb3 --- /dev/null +++ b/changelog/5026.feature.rst @@ -0,0 +1 @@ +Assertion failure messages for sequences and dicts contain the number of different items now. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index a62297075..b53646859 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -297,7 +297,7 @@ def _compare_eq_sequence(left, right, verbose=0): if len_diff > 0: dir_with_more = "Left" extra = saferepr(left[len_right]) - elif len_diff < 0: + else: len_diff = 0 - len_diff dir_with_more = "Right" extra = saferepr(right[len_left]) From e20b39d92814a19db57bdace8d7c84bf4f39b557 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 15:13:35 +0200 Subject: [PATCH 10/57] showhelp: move tw.fullwidth out of the loop --- src/_pytest/helpconfig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 8117ee6bc..2b383d264 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -151,13 +151,14 @@ def showhelp(config): ) tw.line() + columns = tw.fullwidth # costly call for name in config._parser._ininames: help, type, default = config._parser._inidict[name] if type is None: type = "string" spec = "%s (%s)" % (name, type) line = " %-24s %s" % (spec, help) - tw.line(line[: tw.fullwidth]) + tw.line(line[:columns]) tw.line() tw.line("environment variables:") From 9ad00714ba2520b694cb826a2757ad860e29d383 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 09:55:56 +0200 Subject: [PATCH 11/57] pytester: allow passing in stdin to run/popen --- changelog/5059.feature.rst | 1 + src/_pytest/pytester.py | 38 +++++++++++++++++++++---- testing/test_pytester.py | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 changelog/5059.feature.rst diff --git a/changelog/5059.feature.rst b/changelog/5059.feature.rst new file mode 100644 index 000000000..4d5d14061 --- /dev/null +++ b/changelog/5059.feature.rst @@ -0,0 +1 @@ +Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d474df4b9..45c88bb3f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -36,6 +36,8 @@ IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] +CLOSE_STDIN = object + def pytest_addoption(parser): parser.addoption( @@ -1032,7 +1034,7 @@ class Testdir(object): if colitem.name == name: return colitem - def popen(self, cmdargs, stdout, stderr, **kw): + def popen(self, cmdargs, stdout, stderr, stdin=CLOSE_STDIN, **kw): """Invoke subprocess.Popen. This calls subprocess.Popen making sure the current working directory @@ -1050,10 +1052,18 @@ class Testdir(object): env["USERPROFILE"] = env["HOME"] kw["env"] = env - popen = subprocess.Popen( - cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, **kw - ) - popen.stdin.close() + if stdin is CLOSE_STDIN: + kw["stdin"] = subprocess.PIPE + elif isinstance(stdin, bytes): + kw["stdin"] = subprocess.PIPE + else: + kw["stdin"] = stdin + + popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + if stdin is CLOSE_STDIN: + popen.stdin.close() + elif isinstance(stdin, bytes): + popen.stdin.write(stdin) return popen @@ -1065,6 +1075,10 @@ class Testdir(object): :param args: the sequence of arguments to pass to `subprocess.Popen()` :param timeout: the period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired` + :param stdin: optional standard input. Bytes are being send, closing + the pipe, otherwise it is passed through to ``popen``. + Defaults to ``CLOSE_STDIN``, which translates to using a pipe + (``subprocess.PIPE``) that gets closed. Returns a :py:class:`RunResult`. @@ -1072,8 +1086,13 @@ class Testdir(object): __tracebackhide__ = True timeout = kwargs.pop("timeout", None) + stdin = kwargs.pop("stdin", CLOSE_STDIN) raise_on_kwargs(kwargs) + popen_kwargs = {"stdin": stdin} + if isinstance(stdin, bytes): + popen_kwargs["stdin"] = subprocess.PIPE + cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1086,8 +1105,15 @@ class Testdir(object): try: now = time.time() popen = self.popen( - cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") + cmdargs, + stdout=f1, + stderr=f2, + close_fds=(sys.platform != "win32"), + **popen_kwargs ) + if isinstance(stdin, bytes): + popen.stdin.write(stdin) + popen.stdin.close() def handle_timeout(): __tracebackhide__ = True diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 2e4877463..86e160ecb 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -4,6 +4,7 @@ from __future__ import division from __future__ import print_function import os +import subprocess import sys import time @@ -482,3 +483,60 @@ def test_pytester_addopts(request, monkeypatch): testdir.finalize() assert os.environ["PYTEST_ADDOPTS"] == "--orig-unused" + + +def test_run_stdin(testdir): + with pytest.raises(testdir.TimeoutExpired): + testdir.run( + sys.executable, + "-c", + "import sys; print(sys.stdin.read())", + stdin=subprocess.PIPE, + timeout=0.1, + ) + + with pytest.raises(testdir.TimeoutExpired): + result = testdir.run( + sys.executable, + "-c", + "import sys, time; time.sleep(1); print(sys.stdin.read())", + stdin=b"input\n2ndline", + timeout=0.1, + ) + + result = testdir.run( + sys.executable, + "-c", + "import sys; print(sys.stdin.read())", + stdin=b"input\n2ndline", + ) + assert result.stdout.lines == ["input", "2ndline"] + assert result.stderr.str() == "" + assert result.ret == 0 + + +def test_popen_stdin_pipe(testdir): + proc = testdir.popen( + [sys.executable, "-c", "import sys; print(sys.stdin.read())"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + ) + stdin = b"input\n2ndline" + stdout, stderr = proc.communicate(input=stdin) + assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] + assert stderr == b"" + assert proc.returncode == 0 + + +def test_popen_stdin_bytes(testdir): + proc = testdir.popen( + [sys.executable, "-c", "import sys; print(sys.stdin.read())"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=b"input\n2ndline", + ) + stdout, stderr = proc.communicate() + assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] + assert stderr == b"" + assert proc.returncode == 0 From 4fca86e2afe57ee2ad3dae9ffe4c4debdb565a59 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 12:09:31 +0200 Subject: [PATCH 12/57] testdir.popen: use kwargs with defaults for stdout/stderr --- changelog/5059.trivial.rst | 1 + src/_pytest/pytester.py | 9 ++++++++- testing/test_pytester.py | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog/5059.trivial.rst diff --git a/changelog/5059.trivial.rst b/changelog/5059.trivial.rst new file mode 100644 index 000000000..bd8035669 --- /dev/null +++ b/changelog/5059.trivial.rst @@ -0,0 +1 @@ +pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``). diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 45c88bb3f..4a89f5515 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1034,7 +1034,14 @@ class Testdir(object): if colitem.name == name: return colitem - def popen(self, cmdargs, stdout, stderr, stdin=CLOSE_STDIN, **kw): + def popen( + self, + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw + ): """Invoke subprocess.Popen. This calls subprocess.Popen making sure the current working directory diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 86e160ecb..f3b5f70e2 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -540,3 +540,22 @@ def test_popen_stdin_bytes(testdir): assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] assert stderr == b"" assert proc.returncode == 0 + + +def test_popen_default_stdin_stderr_and_stdin_None(testdir): + # stdout, stderr default to pipes, + # stdin can be None to not close the pipe, avoiding + # "ValueError: flush of closed file" with `communicate()`. + p1 = testdir.makepyfile( + """ + import sys + print(sys.stdin.read()) # empty + print('stdout') + sys.stderr.write('stderr') + """ + ) + proc = testdir.popen([sys.executable, str(p1)], stdin=None) + stdout, stderr = proc.communicate(b"ignored") + assert stdout.splitlines() == [b"", b"stdout"] + assert stderr.splitlines() == [b"stderr"] + assert proc.returncode == 0 From a7e49e6c07ff163cdd1e646853d2b52e5d535437 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 17:11:26 +0200 Subject: [PATCH 13/57] reportchars: fix/improve help message --- src/_pytest/terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 31c0da46d..fffdd836a 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -81,11 +81,11 @@ def pytest_addoption(parser): dest="reportchars", default="", metavar="chars", - help="show extra test summary info as specified by chars (f)ailed, " - "(E)error, (s)skipped, (x)failed, (X)passed, " - "(p)passed, (P)passed with output, (a)all except pP. " + help="show extra test summary info as specified by chars: (f)ailed, " + "(E)rror, (s)kipped, (x)failed, (X)passed, " + "(p)assed, (P)assed with output, (a)ll except passed (p/P). " "Warnings are displayed at all times except when " - "--disable-warnings is set", + "--disable-warnings is set.", ) group._addoption( "--disable-warnings", From b4b9f788af45906cec62620db4a9522bf0b3d02f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 18:03:06 +0200 Subject: [PATCH 14/57] Support reportchars=A (really all, including passed) --- src/_pytest/terminal.py | 11 +++++++---- testing/test_terminal.py | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index fffdd836a..e8ac6ef6e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -83,7 +83,7 @@ def pytest_addoption(parser): metavar="chars", help="show extra test summary info as specified by chars: (f)ailed, " "(E)rror, (s)kipped, (x)failed, (X)passed, " - "(p)assed, (P)assed with output, (a)ll except passed (p/P). " + "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " "Warnings are displayed at all times except when " "--disable-warnings is set.", ) @@ -166,10 +166,13 @@ def getreportopt(config): reportchars = reportchars.replace("w", "") if reportchars: for char in reportchars: - if char not in reportopts and char != "a": - reportopts += char - elif char == "a": + if char == "a": reportopts = "sxXwEf" + elif char == "A": + reportopts = "sxXwEfpP" + break + elif char not in reportopts: + reportopts += char return reportopts diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d0fdce23e..d730da666 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -838,6 +838,9 @@ def test_getreportopt(): config.option.disable_warnings = False assert getreportopt(config) == "sfxw" + config.option.reportchars = "A" + assert getreportopt(config) == "sxXwEfpP" + def test_terminalreporter_reportopt_addopts(testdir): testdir.makeini("[pytest]\naddopts=-rs") From 50edab8004cf425317af64d2fb188d6f4eaf321f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 18:04:09 +0200 Subject: [PATCH 15/57] Add tests for reportchars=a Ref: https://github.com/pytest-dev/pytest/issues/5066 --- testing/test_terminal.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d730da666..2155935d4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -830,14 +830,20 @@ def test_getreportopt(): config.option.reportchars = "sfxw" assert getreportopt(config) == "sfx" - config.option.reportchars = "sfx" + # Now with --disable-warnings. config.option.disable_warnings = False + config.option.reportchars = "a" + assert getreportopt(config) == "sxXwEf" # NOTE: "w" included! + + config.option.reportchars = "sfx" assert getreportopt(config) == "sfxw" config.option.reportchars = "sfxw" - config.option.disable_warnings = False assert getreportopt(config) == "sfxw" + config.option.reportchars = "a" + assert getreportopt(config) == "sxXwEf" # NOTE: "w" included! + config.option.reportchars = "A" assert getreportopt(config) == "sxXwEfpP" From c70ecd49caf44322c0ebd3a5bd56bf555d006b8c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 18:21:37 +0200 Subject: [PATCH 16/57] cleanup: move terminal summary code to terminal plugin Fixes https://github.com/pytest-dev/pytest/issues/5067. --- src/_pytest/skipping.py | 125 --------------------------------------- src/_pytest/terminal.py | 122 ++++++++++++++++++++++++++++++++++++++ testing/test_skipping.py | 35 ----------- testing/test_terminal.py | 35 +++++++++++ 4 files changed, 157 insertions(+), 160 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 22acafbdd..3f488fad5 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -183,128 +183,3 @@ def pytest_report_teststatus(report): return "xfailed", "x", "XFAIL" elif report.passed: return "xpassed", "X", "XPASS" - - -# called by the terminalreporter instance/plugin - - -def pytest_terminal_summary(terminalreporter): - tr = terminalreporter - if not tr.reportchars: - return - - lines = [] - for char in tr.reportchars: - action = REPORTCHAR_ACTIONS.get(char, lambda tr, lines: None) - action(terminalreporter, lines) - - if lines: - tr._tw.sep("=", "short test summary info") - for line in lines: - tr._tw.line(line) - - -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(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(config, rep) - pos = _get_pos(config, rep) - lines.append("%s %s" % (verbose_word, pos)) - reason = rep.wasxfail - if reason: - lines.append(" " + str(reason)) - - -def show_xpassed(terminalreporter, lines): - xpassed = terminalreporter.stats.get("xpassed") - if xpassed: - config = terminalreporter.config - for rep in xpassed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - reason = rep.wasxfail - lines.append("%s %s %s" % (verbose_word, pos, reason)) - - -def folded_skips(skipped): - d = {} - for event in skipped: - key = event.longrepr - assert len(key) == 3, (event, key) - keywords = getattr(event, "keywords", {}) - # folding reports with global pytestmark variable - # this is workaround, because for now we cannot identify the scope of a skip marker - # TODO: revisit after marks scope would be fixed - if ( - event.when == "setup" - and "skip" in keywords - and "pytestmark" not in keywords - ): - key = (key[0], None, key[2]) - d.setdefault(key, []).append(event) - values = [] - for key, events in d.items(): - values.append((len(events),) + key) - return values - - -def show_skipped(terminalreporter, lines): - tr = terminalreporter - skipped = tr.stats.get("skipped", []) - if skipped: - 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:] - if lineno is not None: - lines.append( - "%s [%d] %s:%d: %s" - % (verbose_word, num, fspath, lineno + 1, reason) - ) - else: - lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) - - -def shower(stat): - def show_(terminalreporter, lines): - return show_simple(terminalreporter, lines, stat) - - return show_ - - -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, - "f": shower("failed"), - "F": shower("failed"), - "s": show_skipped, - "S": show_skipped, - "p": shower("passed"), - "E": shower("error"), -} diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 31c0da46d..8d6abb17a 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -874,6 +874,128 @@ class TerminalReporter(object): self.write_line(msg, **markup) +def pytest_terminal_summary(terminalreporter): + tr = terminalreporter + if not tr.reportchars: + return + + lines = [] + for char in tr.reportchars: + action = REPORTCHAR_ACTIONS.get(char, lambda tr, lines: None) + action(terminalreporter, lines) + + if lines: + tr._tw.sep("=", "short test summary info") + for line in lines: + tr._tw.line(line) + + +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(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(config, rep) + pos = _get_pos(config, rep) + lines.append("%s %s" % (verbose_word, pos)) + reason = rep.wasxfail + if reason: + lines.append(" " + str(reason)) + + +def show_xpassed(terminalreporter, lines): + xpassed = terminalreporter.stats.get("xpassed") + if xpassed: + config = terminalreporter.config + for rep in xpassed: + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) + reason = rep.wasxfail + lines.append("%s %s %s" % (verbose_word, pos, reason)) + + +def folded_skips(skipped): + d = {} + for event in skipped: + key = event.longrepr + assert len(key) == 3, (event, key) + keywords = getattr(event, "keywords", {}) + # folding reports with global pytestmark variable + # this is workaround, because for now we cannot identify the scope of a skip marker + # TODO: revisit after marks scope would be fixed + if ( + event.when == "setup" + and "skip" in keywords + and "pytestmark" not in keywords + ): + key = (key[0], None, key[2]) + d.setdefault(key, []).append(event) + values = [] + for key, events in d.items(): + values.append((len(events),) + key) + return values + + +def show_skipped(terminalreporter, lines): + tr = terminalreporter + skipped = tr.stats.get("skipped", []) + if skipped: + 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:] + if lineno is not None: + lines.append( + "%s [%d] %s:%d: %s" + % (verbose_word, num, fspath, lineno + 1, reason) + ) + else: + lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) + + +def shower(stat): + def show_(terminalreporter, lines): + return show_simple(terminalreporter, lines, stat) + + return show_ + + +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, + "f": shower("failed"), + "F": shower("failed"), + "s": show_skipped, + "S": show_skipped, + "p": shower("passed"), + "E": shower("error"), +} + + def build_summary_stats_line(stats): known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e5206a44e..cef0fe6ee 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -6,7 +6,6 @@ import sys import pytest from _pytest.runner import runtestprotocol -from _pytest.skipping import folded_skips from _pytest.skipping import MarkEvaluator from _pytest.skipping import pytest_runtest_setup @@ -749,40 +748,6 @@ def test_skipif_class(testdir): result.stdout.fnmatch_lines(["*2 skipped*"]) -def test_skip_reasons_folding(): - path = "xyz" - lineno = 3 - message = "justso" - longrepr = (path, lineno, message) - - class X(object): - pass - - ev1 = X() - ev1.when = "execute" - ev1.skipped = True - ev1.longrepr = longrepr - - ev2 = X() - ev2.when = "execute" - ev2.longrepr = longrepr - ev2.skipped = True - - # ev3 might be a collection report - ev3 = X() - ev3.when = "collect" - ev3.longrepr = longrepr - ev3.skipped = True - - values = folded_skips([ev1, ev2, ev3]) - assert len(values) == 1 - num, fspath, lineno, reason = values[0] - assert num == 3 - assert fspath == path - assert lineno == lineno - assert reason == message - - def test_skipped_reasons_functional(testdir): testdir.makepyfile( test_one=""" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d0fdce23e..888104bf3 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -18,6 +18,7 @@ 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 folded_skips from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter @@ -1524,3 +1525,37 @@ class TestProgressWithTeardown(object): monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) output = testdir.runpytest("-n2") output.stdout.re_match_lines([r"[\.E]{40} \s+ \[100%\]"]) + + +def test_skip_reasons_folding(): + path = "xyz" + lineno = 3 + message = "justso" + longrepr = (path, lineno, message) + + class X(object): + pass + + ev1 = X() + ev1.when = "execute" + ev1.skipped = True + ev1.longrepr = longrepr + + ev2 = X() + ev2.when = "execute" + ev2.longrepr = longrepr + ev2.skipped = True + + # ev3 might be a collection report + ev3 = X() + ev3.when = "collect" + ev3.longrepr = longrepr + ev3.skipped = True + + values = folded_skips([ev1, ev2, ev3]) + assert len(values) == 1 + num, fspath, lineno, reason = values[0] + assert num == 3 + assert fspath == path + assert lineno == lineno + assert reason == message From 4c0ba6017d44e6d0beefa6f0ddeff6f97ca25183 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 19:08:59 +0200 Subject: [PATCH 17/57] Add a conftest to prefer faster tests This uses pytest_collection_modifyitems for pytest's own tests to order them, preferring faster ones via quick'n'dirty heuristics only for now. --- testing/conftest.py | 26 ++++++++++++++++++++++++++ testing/test_modimport.py | 2 ++ tox.ini | 1 + 3 files changed, 29 insertions(+) create mode 100644 testing/conftest.py diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 000000000..fb677cd05 --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,26 @@ +def pytest_collection_modifyitems(config, items): + """Prefer faster tests.""" + fast_items = [] + slow_items = [] + neutral_items = [] + + slow_fixturenames = ("testdir",) + + for item in items: + try: + fixtures = item.fixturenames + except AttributeError: + # doctest at least + # (https://github.com/pytest-dev/pytest/issues/5070) + neutral_items.append(item) + else: + if any(x for x in fixtures if x in slow_fixturenames): + slow_items.append(item) + else: + marker = item.get_closest_marker("slow") + if marker: + slow_items.append(item) + else: + fast_items.append(item) + + items[:] = fast_items + neutral_items + slow_items diff --git a/testing/test_modimport.py b/testing/test_modimport.py index 33862799b..3d7a07323 100644 --- a/testing/test_modimport.py +++ b/testing/test_modimport.py @@ -6,6 +6,8 @@ import py import _pytest import pytest +pytestmark = pytest.mark.slow + MODSET = [ x for x in py.path.local(_pytest.__file__).dirpath().visit("*.py") diff --git a/tox.ini b/tox.ini index 16984dd43..3e6745dc5 100644 --- a/tox.ini +++ b/tox.ini @@ -171,6 +171,7 @@ filterwarnings = pytester_example_dir = testing/example_scripts markers = issue + slow [flake8] max-line-length = 120 From 06029d11d337d8777ac3118dd0e96eda6c98125c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 19:36:29 +0200 Subject: [PATCH 18/57] Refactor into TerminalReporter.short_test_summary --- src/_pytest/terminal.py | 187 +++++++++++++++++++-------------------- testing/test_terminal.py | 4 +- 2 files changed, 92 insertions(+), 99 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8d6abb17a..44b874af8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -678,6 +678,7 @@ class TerminalReporter(object): self.summary_failures() self.summary_warnings() yield + self.short_test_summary() self.summary_passes() # Display any extra warnings from teardown here (if any). self.summary_warnings() @@ -873,58 +874,100 @@ class TerminalReporter(object): if self.verbosity == -1: self.write_line(msg, **markup) + def short_test_summary(self): + if not self.reportchars: + return -def pytest_terminal_summary(terminalreporter): - tr = terminalreporter - if not tr.reportchars: - return + def show_simple(lines, stat): + failed = self.stats.get(stat) + if failed: + config = self.config + for rep in failed: + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) + lines.append("%s %s" % (verbose_word, pos)) - lines = [] - for char in tr.reportchars: - action = REPORTCHAR_ACTIONS.get(char, lambda tr, lines: None) - action(terminalreporter, lines) + def show_xfailed(lines): + xfailed = self.stats.get("xfailed") + if xfailed: + config = self.config + for rep in xfailed: + 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)) - if lines: - tr._tw.sep("=", "short test summary info") - for line in lines: - tr._tw.line(line) + def show_xpassed(lines): + xpassed = self.stats.get("xpassed") + if xpassed: + config = self.config + for rep in xpassed: + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) + reason = rep.wasxfail + lines.append("%s %s %s" % (verbose_word, pos, reason)) + + def show_skipped(lines): + skipped = self.stats.get("skipped", []) + if skipped: + fskips = _folded_skips(skipped) + if fskips: + verbose_word = _get_report_str(self.config, report=skipped[0]) + for num, fspath, lineno, reason in fskips: + if reason.startswith("Skipped: "): + reason = reason[9:] + if lineno is not None: + lines.append( + "%s [%d] %s:%d: %s" + % (verbose_word, num, fspath, lineno + 1, reason) + ) + else: + lines.append( + "%s [%d] %s: %s" % (verbose_word, num, fspath, reason) + ) + + def shower(stat): + def show_(lines): + return show_simple(lines, stat) + + return show_ + + 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, + "f": shower("failed"), + "F": shower("failed"), + "s": show_skipped, + "S": show_skipped, + "p": shower("passed"), + "E": shower("error"), + } + + lines = [] + for char in self.reportchars: + action = REPORTCHAR_ACTIONS.get(char) + if action: # skipping e.g. "P" (passed with output) here. + action(lines) + + if lines: + self.write_sep("=", "short test summary info") + for line in lines: + self.write_line(line) -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(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(config, rep) - pos = _get_pos(config, rep) - lines.append("%s %s" % (verbose_word, pos)) - reason = rep.wasxfail - if reason: - lines.append(" " + str(reason)) - - -def show_xpassed(terminalreporter, lines): - xpassed = terminalreporter.stats.get("xpassed") - if xpassed: - config = terminalreporter.config - for rep in xpassed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - reason = rep.wasxfail - lines.append("%s %s %s" % (verbose_word, pos, reason)) - - -def folded_skips(skipped): +def _folded_skips(skipped): d = {} for event in skipped: key = event.longrepr @@ -946,56 +989,6 @@ def folded_skips(skipped): return values -def show_skipped(terminalreporter, lines): - tr = terminalreporter - skipped = tr.stats.get("skipped", []) - if skipped: - 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:] - if lineno is not None: - lines.append( - "%s [%d] %s:%d: %s" - % (verbose_word, num, fspath, lineno + 1, reason) - ) - else: - lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) - - -def shower(stat): - def show_(terminalreporter, lines): - return show_simple(terminalreporter, lines, stat) - - return show_ - - -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, - "f": shower("failed"), - "F": shower("failed"), - "s": show_skipped, - "S": show_skipped, - "p": shower("passed"), - "E": shower("error"), -} - - def build_summary_stats_line(stats): known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 888104bf3..12878d2b3 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -16,9 +16,9 @@ import py import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.reports import BaseReport +from _pytest.terminal import _folded_skips from _pytest.terminal import _plugin_nameversions from _pytest.terminal import build_summary_stats_line -from _pytest.terminal import folded_skips from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter @@ -1552,7 +1552,7 @@ def test_skip_reasons_folding(): ev3.longrepr = longrepr ev3.skipped = True - values = folded_skips([ev1, ev2, ev3]) + values = _folded_skips([ev1, ev2, ev3]) assert len(values) == 1 num, fspath, lineno, reason = values[0] assert num == 3 From d8d835c1f5a516decc7afb37048a2909041d86f0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 19:49:10 +0200 Subject: [PATCH 19/57] minor: use functools.partial --- src/_pytest/terminal.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 44b874af8..8b645f4c9 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -11,6 +11,7 @@ import collections import platform import sys import time +from functools import partial import attr import pluggy @@ -878,7 +879,7 @@ class TerminalReporter(object): if not self.reportchars: return - def show_simple(lines, stat): + def show_simple(stat, lines): failed = self.stats.get(stat) if failed: config = self.config @@ -928,12 +929,6 @@ class TerminalReporter(object): "%s [%d] %s: %s" % (verbose_word, num, fspath, reason) ) - def shower(stat): - def show_(lines): - return show_simple(lines, stat) - - return show_ - def _get_report_str(config, report): _category, _short, verbose = config.hook.pytest_report_teststatus( report=report, config=config @@ -947,12 +942,12 @@ class TerminalReporter(object): REPORTCHAR_ACTIONS = { "x": show_xfailed, "X": show_xpassed, - "f": shower("failed"), - "F": shower("failed"), + "f": partial(show_simple, "failed"), + "F": partial(show_simple, "failed"), "s": show_skipped, "S": show_skipped, - "p": shower("passed"), - "E": shower("error"), + "p": partial(show_simple, "passed"), + "E": partial(show_simple, "error"), } lines = [] From 2662c400ba9463aa9b9c4aaf8778736b1803b2e0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 20:04:31 +0200 Subject: [PATCH 20/57] dedent --- src/_pytest/terminal.py | 74 ++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8b645f4c9..18011b66b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -880,54 +880,46 @@ class TerminalReporter(object): return def show_simple(stat, lines): - failed = self.stats.get(stat) - if failed: - config = self.config - for rep in failed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - lines.append("%s %s" % (verbose_word, pos)) + failed = self.stats.get(stat, []) + for rep in failed: + verbose_word = _get_report_str(self.config, rep) + pos = _get_pos(self.config, rep) + lines.append("%s %s" % (verbose_word, pos)) def show_xfailed(lines): - xfailed = self.stats.get("xfailed") - if xfailed: - config = self.config - for rep in xfailed: - 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)) + xfailed = self.stats.get("xfailed", []) + for rep in xfailed: + verbose_word = _get_report_str(self.config, rep) + pos = _get_pos(self.config, rep) + lines.append("%s %s" % (verbose_word, pos)) + reason = rep.wasxfail + if reason: + lines.append(" " + str(reason)) def show_xpassed(lines): - xpassed = self.stats.get("xpassed") - if xpassed: - config = self.config - for rep in xpassed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - reason = rep.wasxfail - lines.append("%s %s %s" % (verbose_word, pos, reason)) + xpassed = self.stats.get("xpassed", []) + for rep in xpassed: + verbose_word = _get_report_str(self.config, rep) + pos = _get_pos(self.config, rep) + reason = rep.wasxfail + lines.append("%s %s %s" % (verbose_word, pos, reason)) def show_skipped(lines): skipped = self.stats.get("skipped", []) - if skipped: - fskips = _folded_skips(skipped) - if fskips: - verbose_word = _get_report_str(self.config, report=skipped[0]) - for num, fspath, lineno, reason in fskips: - if reason.startswith("Skipped: "): - reason = reason[9:] - if lineno is not None: - lines.append( - "%s [%d] %s:%d: %s" - % (verbose_word, num, fspath, lineno + 1, reason) - ) - else: - lines.append( - "%s [%d] %s: %s" % (verbose_word, num, fspath, reason) - ) + fskips = _folded_skips(skipped) if skipped else [] + if not fskips: + return + verbose_word = _get_report_str(self.config, report=skipped[0]) + for num, fspath, lineno, reason in fskips: + if reason.startswith("Skipped: "): + reason = reason[9:] + if lineno is not None: + lines.append( + "%s [%d] %s:%d: %s" + % (verbose_word, num, fspath, lineno + 1, reason) + ) + else: + lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) def _get_report_str(config, report): _category, _short, verbose = config.hook.pytest_report_teststatus( From ff5e98c654e618d4ed56b86879ddb5e26253031e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 8 Apr 2019 01:59:30 +0200 Subject: [PATCH 21/57] Change noqa comment to pragma --- 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 76ec072dc..a5b9e9c86 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -256,7 +256,7 @@ def _test_pytest_function(pyfuncitem): _pdb = pytestPDB._init_pdb() testfunction = pyfuncitem.obj pyfuncitem.obj = _pdb.runcall - if "func" in pyfuncitem._fixtureinfo.argnames: # noqa + if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch raise ValueError("--trace can't be used with a fixture named func!") pyfuncitem.funcargs["func"] = testfunction new_list = list(pyfuncitem._fixtureinfo.argnames) From 4fb7a91a5e2c93c0c2fc29af29f22b57782e7bf1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 8 Apr 2019 02:00:28 +0200 Subject: [PATCH 22/57] pdb: add test for --trace with --pdbcls Ensures that https://github.com/pytest-dev/pytest/issues/4111 is fixed, which happened in 92a2884b as a byproduct. --- testing/test_pdb.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 77e421af1..cc2ea15d2 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1148,7 +1148,11 @@ def test_pdbcls_via_local_module(testdir): class Wrapped: class MyPdb: def set_trace(self, *args): - print("mypdb_called", args) + print("settrace_called", args) + + def runcall(self, *args, **kwds): + print("runcall_called", args, kwds) + assert "func" in kwds """, ) result = testdir.runpytest( @@ -1165,4 +1169,11 @@ def test_pdbcls_via_local_module(testdir): str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True ) assert result.ret == 0 - result.stdout.fnmatch_lines(["*mypdb_called*", "* 1 passed in *"]) + result.stdout.fnmatch_lines(["*settrace_called*", "* 1 passed in *"]) + + # Ensure that it also works with --trace. + result = testdir.runpytest( + str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", "--trace", syspathinsert=True + ) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*runcall_called*", "* 1 passed in *"]) From b6b7185b7bffcc4b71f752ea283d34c252b2eb52 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 8 Apr 2019 04:32:06 +0200 Subject: [PATCH 23/57] terminal: console_output_style: document "count" with help --- src/_pytest/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 31c0da46d..2d62bee13 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -140,7 +140,7 @@ def pytest_addoption(parser): parser.addini( "console_output_style", - help="console output: classic or with additional progress information (classic|progress).", + help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").', default="progress", ) From a70e5f119eed02907ace69956074753da34bc139 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 8 Apr 2019 04:33:45 +0200 Subject: [PATCH 24/57] terminal: store console_output_style in _show_progress_info Avoids ini lookups. --- src/_pytest/terminal.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 31c0da46d..c4c12e5ab 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -254,7 +254,10 @@ class TerminalReporter(object): # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow", False): return False - return self.config.getini("console_output_style") in ("progress", "count") + cfg = self.config.getini("console_output_style") + if cfg in ("progress", "count"): + return cfg + return False @property def verbosity(self): @@ -438,13 +441,13 @@ class TerminalReporter(object): self.currentfspath = -2 def pytest_runtest_logfinish(self, nodeid): - if self.config.getini("console_output_style") == "count": - num_tests = self._session.testscollected - progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests))) - else: - progress_length = len(" [100%]") - if self.verbosity <= 0 and self._show_progress_info: + if self._show_progress_info == "count": + num_tests = self._session.testscollected + progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests))) + else: + progress_length = len(" [100%]") + self._progress_nodeids_reported.add(nodeid) last_item = ( len(self._progress_nodeids_reported) == self._session.testscollected @@ -460,7 +463,7 @@ class TerminalReporter(object): def _get_progress_information_message(self): collected = self._session.testscollected - if self.config.getini("console_output_style") == "count": + if self._show_progress_info == "count": if collected: progress = self._progress_nodeids_reported counter_format = "{{:{}d}}".format(len(str(collected))) From c36a90531a413f9415e950c17fe5d36a0ca052ed Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:01:15 +0200 Subject: [PATCH 25/57] Move CLOSE_STDIN to class --- src/_pytest/pytester.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4a89f5515..21f8ed26b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -36,8 +36,6 @@ IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] -CLOSE_STDIN = object - def pytest_addoption(parser): parser.addoption( @@ -475,6 +473,8 @@ class Testdir(object): """ + CLOSE_STDIN = object + class TimeoutExpired(Exception): pass @@ -1059,7 +1059,7 @@ class Testdir(object): env["USERPROFILE"] = env["HOME"] kw["env"] = env - if stdin is CLOSE_STDIN: + if stdin is Testdir.CLOSE_STDIN: kw["stdin"] = subprocess.PIPE elif isinstance(stdin, bytes): kw["stdin"] = subprocess.PIPE @@ -1067,7 +1067,7 @@ class Testdir(object): kw["stdin"] = stdin popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - if stdin is CLOSE_STDIN: + if stdin is Testdir.CLOSE_STDIN: popen.stdin.close() elif isinstance(stdin, bytes): popen.stdin.write(stdin) @@ -1093,7 +1093,7 @@ class Testdir(object): __tracebackhide__ = True timeout = kwargs.pop("timeout", None) - stdin = kwargs.pop("stdin", CLOSE_STDIN) + stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) raise_on_kwargs(kwargs) popen_kwargs = {"stdin": stdin} From ec46864922d2aab4fb266ae8317d553ec9ef3d9b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:02:38 +0200 Subject: [PATCH 26/57] run: pass through stdin, just close then --- src/_pytest/pytester.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 21f8ed26b..6dc9031df 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1096,10 +1096,6 @@ class Testdir(object): stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) raise_on_kwargs(kwargs) - popen_kwargs = {"stdin": stdin} - if isinstance(stdin, bytes): - popen_kwargs["stdin"] = subprocess.PIPE - cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1113,13 +1109,12 @@ class Testdir(object): now = time.time() popen = self.popen( cmdargs, + stdin=stdin, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32"), - **popen_kwargs ) if isinstance(stdin, bytes): - popen.stdin.write(stdin) popen.stdin.close() def handle_timeout(): From b84f826fc8a4f305309d699b3bc2f1dbd3d9672d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:03:49 +0200 Subject: [PATCH 27/57] test_run_stdin: add sleep --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index f3b5f70e2..b76d413b7 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -490,7 +490,7 @@ def test_run_stdin(testdir): testdir.run( sys.executable, "-c", - "import sys; print(sys.stdin.read())", + "import sys, time; time.sleep(1); print(sys.stdin.read())", stdin=subprocess.PIPE, timeout=0.1, ) From dde27a2305d2e2cd80fd27ae0885bfa3f9206822 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 21:39:11 +0200 Subject: [PATCH 28/57] changelog [ci skip] --- changelog/5069.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5069.trivial.rst diff --git a/changelog/5069.trivial.rst b/changelog/5069.trivial.rst new file mode 100644 index 000000000..dd6abd8b8 --- /dev/null +++ b/changelog/5069.trivial.rst @@ -0,0 +1 @@ +The code for the short test summary in the terminal was moved to the terminal plugin. From 5d9d12a6bec2c4b30d456382afe11ad44b203891 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 21:36:42 +0200 Subject: [PATCH 29/57] pytester: improve/fix kwargs validation --- src/_pytest/pytester.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6dc9031df..2c6ff2020 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -76,8 +76,11 @@ def pytest_configure(config): def raise_on_kwargs(kwargs): + __tracebackhide__ = True if kwargs: - raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs)))) + raise TypeError( + "Unexpected keyword arguments: {}".format(", ".join(sorted(kwargs))) + ) class LsofFdLeakChecker(object): @@ -803,12 +806,15 @@ class Testdir(object): :param args: command line arguments to pass to :py:func:`pytest.main` - :param plugin: (keyword-only) extra plugin instances the + :param plugins: (keyword-only) extra plugin instances the ``pytest.main()`` instance should use :return: a :py:class:`HookRecorder` instance - """ + plugins = kwargs.pop("plugins", []) + no_reraise_ctrlc = kwargs.pop("no_reraise_ctrlc", None) + raise_on_kwargs(kwargs) + finalizers = [] try: # Do not load user config (during runs only). @@ -848,7 +854,6 @@ class Testdir(object): def pytest_configure(x, config): rec.append(self.make_hook_recorder(config.pluginmanager)) - plugins = kwargs.get("plugins") or [] plugins.append(Collect()) ret = pytest.main(list(args), plugins=plugins) if len(rec) == 1: @@ -862,7 +867,7 @@ class Testdir(object): # typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing - if ret == EXIT_INTERRUPTED and not kwargs.get("no_reraise_ctrlc"): + if ret == EXIT_INTERRUPTED and not no_reraise_ctrlc: calls = reprec.getcalls("pytest_keyboard_interrupt") if calls and calls[-1].excinfo.type == KeyboardInterrupt: raise KeyboardInterrupt() @@ -874,9 +879,10 @@ class Testdir(object): def runpytest_inprocess(self, *args, **kwargs): """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides. - """ - if kwargs.get("syspathinsert"): + syspathinsert = kwargs.pop("syspathinsert", False) + + if syspathinsert: self.syspathinsert() now = time.time() capture = MultiCapture(Capture=SysCapture) @@ -1201,9 +1207,10 @@ class Testdir(object): :py:class:`Testdir.TimeoutExpired` Returns a :py:class:`RunResult`. - """ __tracebackhide__ = True + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir @@ -1213,7 +1220,7 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args, timeout=kwargs.get("timeout")) + return self.run(*args, timeout=timeout) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. From 148f2fc72cae5eb4af2e1ecf0ccd95aebec03b33 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 21:56:44 +0200 Subject: [PATCH 30/57] Fix test_error_during_readouterr: syspathinsert is unused --- testing/test_capture.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 1b34ab583..c3881128f 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -819,15 +819,15 @@ def test_error_during_readouterr(testdir): testdir.makepyfile( pytest_xyz=""" from _pytest.capture import FDCapture + def bad_snap(self): raise Exception('boom') + assert FDCapture.snap FDCapture.snap = bad_snap """ ) - result = testdir.runpytest_subprocess( - "-p", "pytest_xyz", "--version", syspathinsert=True - ) + result = testdir.runpytest_subprocess("-p", "pytest_xyz", "--version") result.stderr.fnmatch_lines( ["*in bad_snap", " raise Exception('boom')", "Exception: boom"] ) From 12133d4eb7480c490b3770988da37bac375633c3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 23:15:25 +0200 Subject: [PATCH 31/57] changelog [ci skip] --- changelog/5082.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5082.trivial.rst diff --git a/changelog/5082.trivial.rst b/changelog/5082.trivial.rst new file mode 100644 index 000000000..edd23a28f --- /dev/null +++ b/changelog/5082.trivial.rst @@ -0,0 +1 @@ +Improved validation of kwargs for various methods in the pytester plugin. From 1f66e3b0d01e463228df5a129e542d44b868506a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 11 Apr 2019 11:38:55 +0200 Subject: [PATCH 32/57] docs: tbreportdemo: remove obsolete comment --- doc/en/example/reportingdemo.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2a296dc10..2a2364c27 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -1,13 +1,9 @@ - .. _`tbreportdemo`: Demo of Python failure reports with pytest -================================================== +========================================== -Here is a nice run of several tens of failures -and how ``pytest`` presents things (unfortunately -not showing the nice colors here in the HTML that you -get on the terminal - we are working on that): +Here is a nice run of several failures and how ``pytest`` presents things: .. code-block:: pytest From 42e60d935adb5292e9ae87387a5a61745bd34ab1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 11 Apr 2019 11:44:04 +0200 Subject: [PATCH 33/57] doc/changelog --- changelog/5068.feature.rst | 1 + doc/en/usage.rst | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog/5068.feature.rst diff --git a/changelog/5068.feature.rst b/changelog/5068.feature.rst new file mode 100644 index 000000000..bceebffc1 --- /dev/null +++ b/changelog/5068.feature.rst @@ -0,0 +1 @@ +The ``-r`` option learnt about ``A`` to display all reports (including passed ones) in the short test summary. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index d6850beda..1c49f81d6 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -235,7 +235,8 @@ Example: FAILED test_example.py::test_fail = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = -The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". +The ``-r`` options accepts a number of characters after it, with ``a`` used +above meaning "all except passes". Here is the full list of available characters that can be used: @@ -247,6 +248,7 @@ Here is the full list of available characters that can be used: - ``p`` - passed - ``P`` - passed with output - ``a`` - all except ``pP`` + - ``A`` - all More than one character can be used, so for example to only see failed and skipped tests, you can execute: From a37d1df0896018a7bf41cb62ff4d93821e783507 Mon Sep 17 00:00:00 2001 From: Samuel Searles-Bryant Date: Wed, 10 Apr 2019 23:07:57 +0100 Subject: [PATCH 34/57] Show XFail reason as part of JUnitXML message field Fixes #4907 --- AUTHORS | 1 + changelog/4907.feature.rst | 1 + src/_pytest/junitxml.py | 9 ++++++++- testing/test_junitxml.py | 20 +++++++++++++++++++- 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 changelog/4907.feature.rst diff --git a/AUTHORS b/AUTHORS index ea6fc5cac..41e30ffbc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -208,6 +208,7 @@ Ross Lawley Russel Winder Ryan Wooden Samuel Dion-Girardeau +Samuel Searles-Bryant Samuele Pedroni Sankt Petersbug Segev Finer diff --git a/changelog/4907.feature.rst b/changelog/4907.feature.rst new file mode 100644 index 000000000..48bece401 --- /dev/null +++ b/changelog/4907.feature.rst @@ -0,0 +1 @@ +Show XFail reason as part of JUnitXML message field. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 122e0c7ce..c2b277b8a 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -252,7 +252,14 @@ class _NodeReporter(object): def append_skipped(self, report): if hasattr(report, "wasxfail"): - self._add_simple(Junit.skipped, "expected test failure", report.wasxfail) + xfailreason = report.wasxfail + if xfailreason.startswith("reason: "): + xfailreason = xfailreason[8:] + self.append( + Junit.skipped( + "", type="pytest.xfail", message=bin_xml_escape(xfailreason) + ) + ) else: filename, lineno, skipreason = report.longrepr if skipreason.startswith("Skipped: "): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 769e8e8a7..82e984785 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -485,9 +485,27 @@ class TestPython(object): tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_function", name="test_xfail") fnode = tnode.find_first_by_tag("skipped") - fnode.assert_attr(message="expected test failure") + fnode.assert_attr(type="pytest.xfail", message="42") # assert "ValueError" in fnode.toxml() + def test_xfailure_marker(self, testdir): + testdir.makepyfile( + """ + import pytest + @pytest.mark.xfail(reason="42") + def test_xfail(): + assert False + """ + ) + result, dom = runandparse(testdir) + assert not result.ret + node = dom.find_first_by_tag("testsuite") + node.assert_attr(skipped=1, tests=1) + tnode = node.find_first_by_tag("testcase") + tnode.assert_attr(classname="test_xfailure_marker", name="test_xfail") + fnode = tnode.find_first_by_tag("skipped") + fnode.assert_attr(type="pytest.xfail", message="42") + def test_xfail_captures_output_once(self, testdir): testdir.makepyfile( """ From 8449294e5d23d6d8543548b0c65eeaf1d5cd9fa3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 12 Apr 2019 04:50:26 -0700 Subject: [PATCH 35/57] blacken-docs more code samples in docs --- doc/en/assert.rst | 57 +++++++++++---- doc/en/example/markers.rst | 130 +++++++++++++++++++++++++-------- doc/en/example/parametrize.rst | 28 ++++--- doc/en/historical-notes.rst | 43 +++++++---- doc/en/parametrize.rst | 58 +++++++++------ doc/en/skipping.rst | 41 +++++++---- doc/en/warnings.rst | 40 +++++++--- doc/en/writing_plugins.rst | 13 +++- 8 files changed, 292 insertions(+), 118 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 9b26308c6..67ecb5348 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -12,12 +12,15 @@ Asserting with the ``assert`` statement ``pytest`` allows you to use the standard python ``assert`` for verifying expectations and values in Python tests. For example, you can write the -following:: +following: + +.. code-block:: python # content of test_assert1.py def f(): return 3 + def test_function(): assert f() == 4 @@ -52,7 +55,9 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the idiomatic python constructs without boilerplate code while not losing introspection information. -However, if you specify a message with the assertion like this:: +However, if you specify a message with the assertion like this: + +.. code-block:: python assert a % 2 == 0, "value was odd, should be even" @@ -67,22 +72,29 @@ Assertions about expected exceptions ------------------------------------------ In order to write assertions about raised exceptions, you can use -``pytest.raises`` as a context manager like this:: +``pytest.raises`` as a context manager like this: + +.. code-block:: python import pytest + def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0 -and if you need to have access to the actual exception info you may use:: +and if you need to have access to the actual exception info you may use: + +.. code-block:: python def test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: + def f(): f() + f() - assert 'maximum recursion' in str(excinfo.value) + assert "maximum recursion" in str(excinfo.value) ``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are @@ -90,15 +102,19 @@ the actual exception raised. The main attributes of interest are You can pass a ``match`` keyword parameter to the context-manager to test that a regular expression matches on the string representation of an exception -(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``):: +(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``): + +.. code-block:: python import pytest + def myfunc(): raise ValueError("Exception 123 raised") + def test_match(): - with pytest.raises(ValueError, match=r'.* 123 .*'): + with pytest.raises(ValueError, match=r".* 123 .*"): myfunc() The regexp parameter of the ``match`` method is matched with the ``re.search`` @@ -107,7 +123,9 @@ well. There's an alternate form of the ``pytest.raises`` function where you pass a function that will be executed with the given ``*args`` and ``**kwargs`` and -assert that the given exception is raised:: +assert that the given exception is raised: + +.. code-block:: python pytest.raises(ExpectedException, func, *args, **kwargs) @@ -116,7 +134,9 @@ exception* or *wrong exception*. Note that it is also possible to specify a "raises" argument to ``pytest.mark.xfail``, which checks that the test is failing in a more -specific way than just having any exception raised:: +specific way than just having any exception raised: + +.. code-block:: python @pytest.mark.xfail(raises=IndexError) def test_f(): @@ -148,10 +168,13 @@ Making use of context-sensitive comparisons .. versionadded:: 2.0 ``pytest`` has rich support for providing context-sensitive information -when it encounters comparisons. For example:: +when it encounters comparisons. For example: + +.. code-block:: python # content of test_assert2.py + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -205,16 +228,21 @@ the ``pytest_assertrepr_compare`` hook. :noindex: As an example consider adding the following hook in a :ref:`conftest.py ` -file which provides an alternative explanation for ``Foo`` objects:: +file which provides an alternative explanation for ``Foo`` objects: + +.. code-block:: python # content of conftest.py from test_foocompare import Foo + + def pytest_assertrepr_compare(op, left, right): if isinstance(left, Foo) and isinstance(right, Foo) and op == "==": - return ['Comparing Foo instances:', - ' vals: %s != %s' % (left.val, right.val)] + return ["Comparing Foo instances:", " vals: %s != %s" % (left.val, right.val)] -now, given this test module:: +now, given this test module: + +.. code-block:: python # content of test_foocompare.py class Foo(object): @@ -224,6 +252,7 @@ now, given this test module:: def __eq__(self, other): return self.val == other.val + def test_compare(): f1 = Foo(1) f2 = Foo(2) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index d207ef7e2..f5a37bdb3 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -9,18 +9,28 @@ Here are some example using the :ref:`mark` mechanism. Marking test functions and selecting them for a run ---------------------------------------------------- -You can "mark" a test function with custom metadata like this:: +You can "mark" a test function with custom metadata like this: + +.. code-block:: python # content of test_server.py import pytest + + @pytest.mark.webtest def test_send_http(): - pass # perform some webtest test for your app + pass # perform some webtest test for your app + + def test_something_quick(): pass + + def test_another(): pass + + class TestClass(object): def test_method(self): pass @@ -257,14 +267,19 @@ Marking whole classes or modules ---------------------------------------------------- You may use ``pytest.mark`` decorators with classes to apply markers to all of -its test methods:: +its test methods: + +.. code-block:: python # content of test_mark_classlevel.py import pytest + + @pytest.mark.webtest class TestClass(object): def test_startup(self): pass + def test_startup_and_more(self): pass @@ -272,17 +287,23 @@ This is equivalent to directly applying the decorator to the two test functions. To remain backward-compatible with Python 2.4 you can also set a -``pytestmark`` attribute on a TestClass like this:: +``pytestmark`` attribute on a TestClass like this: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = pytest.mark.webtest -or if you need to use multiple markers you can use a list:: +or if you need to use multiple markers you can use a list: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] @@ -305,18 +326,19 @@ Marking individual tests when using parametrize When using parametrize, applying a mark will make it apply to each individual test. However it is also possible to -apply a marker to an individual test instance:: +apply a marker to an individual test instance: + +.. code-block:: python import pytest + @pytest.mark.foo - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - pytest.param((1, 3), marks=pytest.mark.bar), - (2, 3), - ]) + @pytest.mark.parametrize( + ("n", "expected"), [(1, 2), pytest.param((1, 3), marks=pytest.mark.bar), (2, 3)] + ) def test_increment(n, expected): - assert n + 1 == expected + assert n + 1 == expected In this example the mark "foo" will apply to each of the three tests, whereas the "bar" mark is only applied to the second test. @@ -332,31 +354,46 @@ Custom marker and command line option to control test runs Plugins can provide custom markers and implement specific behaviour based on it. This is a self-contained example which adds a command line option and a parametrized test function marker to run tests -specifies via named environments:: +specifies via named environments: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_addoption(parser): - parser.addoption("-E", action="store", metavar="NAME", - help="only run tests matching the environment NAME.") + parser.addoption( + "-E", + action="store", + metavar="NAME", + help="only run tests matching the environment NAME.", + ) + def pytest_configure(config): # register an additional marker - config.addinivalue_line("markers", - "env(name): mark test to run only on named environment") + config.addinivalue_line( + "markers", "env(name): mark test to run only on named environment" + ) + def pytest_runtest_setup(item): - envnames = [mark.args[0] for mark in item.iter_markers(name='env')] + envnames = [mark.args[0] for mark in item.iter_markers(name="env")] if envnames: if item.config.getoption("-E") not in envnames: pytest.skip("test requires env in %r" % envnames) -A test file using this local plugin:: +A test file using this local plugin: + +.. code-block:: python # content of test_someenv.py import pytest + + @pytest.mark.env("stage1") def test_basic_db_operation(): pass @@ -423,25 +460,32 @@ Passing a callable to custom markers .. regendoc:wipe -Below is the config file that will be used in the next examples:: +Below is the config file that will be used in the next examples: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for marker in item.iter_markers(name='my_marker'): + for marker in item.iter_markers(name="my_marker"): print(marker) sys.stdout.flush() A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time. -However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue:: +However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue: + +.. code-block:: python # content of test_custom_marker.py import pytest + def hello_world(*args, **kwargs): - return 'Hello World' + return "Hello World" + @pytest.mark.my_marker.with_args(hello_world) def test_with_args(): @@ -467,12 +511,16 @@ Reading markers which were set from multiple places .. regendoc:wipe If you are heavily using markers in your test suite you may encounter the case where a marker is applied several times to a test function. From plugin -code you can read over all such settings. Example:: +code you can read over all such settings. Example: + +.. code-block:: python # content of test_mark_three_times.py import pytest + pytestmark = pytest.mark.glob("module", x=1) + @pytest.mark.glob("class", x=2) class TestClass(object): @pytest.mark.glob("function", x=3) @@ -480,13 +528,16 @@ code you can read over all such settings. Example:: pass Here we have the marker "glob" applied three times to the same -test function. From a conftest file we can read it like this:: +test function. From a conftest file we can read it like this: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for mark in item.iter_markers(name='glob'): + for mark in item.iter_markers(name="glob"): print("glob args=%s kwargs=%s" % (mark.args, mark.kwargs)) sys.stdout.flush() @@ -510,7 +561,9 @@ Consider you have a test suite which marks tests for particular platforms, namely ``pytest.mark.darwin``, ``pytest.mark.win32`` etc. and you also have tests that run on all platforms and have no specific marker. If you now want to have a way to only run the tests -for your particular platform, you could use the following plugin:: +for your particular platform, you could use the following plugin: + +.. code-block:: python # content of conftest.py # @@ -519,6 +572,7 @@ for your particular platform, you could use the following plugin:: ALL = set("darwin linux win32".split()) + def pytest_runtest_setup(item): supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform @@ -526,24 +580,30 @@ for your particular platform, you could use the following plugin:: pytest.skip("cannot run on platform %s" % (plat)) then tests will be skipped if they were specified for a different platform. -Let's do a little test file to show how this looks like:: +Let's do a little test file to show how this looks like: + +.. code-block:: python # content of test_plat.py import pytest + @pytest.mark.darwin def test_if_apple_is_evil(): pass + @pytest.mark.linux def test_if_linux_works(): pass + @pytest.mark.win32 def test_if_win32_crashes(): pass + def test_runs_everywhere(): pass @@ -589,28 +649,38 @@ Automatically adding markers based on test names If you a test suite where test function names indicate a certain type of test, you can implement a hook that automatically defines markers so that you can use the ``-m`` option with it. Let's look -at this test module:: +at this test module: + +.. code-block:: python # content of test_module.py + def test_interface_simple(): assert 0 + def test_interface_complex(): assert 0 + def test_event_simple(): assert 0 + def test_something_else(): assert 0 We want to dynamically define two markers and can do it in a -``conftest.py`` plugin:: +``conftest.py`` plugin: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_collection_modifyitems(items): for item in items: if "interface" in item.nodeid: diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1b9f657fd..19de27af6 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -515,21 +515,25 @@ Set marks or test ID for individual parametrized test -------------------------------------------------------------------- Use ``pytest.param`` to apply marks or set test ID to individual parametrized test. -For example:: +For example: + +.. code-block:: python # content of test_pytest_param_example.py import pytest - @pytest.mark.parametrize('test_input,expected', [ - ('3+5', 8), - pytest.param('1+7', 8, - marks=pytest.mark.basic), - pytest.param('2+4', 6, - marks=pytest.mark.basic, - id='basic_2+4'), - pytest.param('6*9', 42, - marks=[pytest.mark.basic, pytest.mark.xfail], - id='basic_6*9'), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", + [ + ("3+5", 8), + pytest.param("1+7", 8, marks=pytest.mark.basic), + pytest.param("2+4", 6, marks=pytest.mark.basic, id="basic_2+4"), + pytest.param( + "6*9", 42, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9" + ), + ], + ) def test_eval(test_input, expected): assert eval(test_input) == expected diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 9462d700f..4dfec7f5e 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -57,14 +57,16 @@ Applying marks to ``@pytest.mark.parametrize`` parameters .. versionchanged:: 3.1 Prior to version 3.1 the supported mechanism for marking values -used the syntax:: +used the syntax: + +.. code-block:: python import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.mark.xfail(("6*9", 42),), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.mark.xfail(("6*9", 42))] + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -105,9 +107,13 @@ Conditions as strings instead of booleans .. versionchanged:: 2.4 Prior to pytest-2.4 the only way to specify skipif/xfail conditions was -to use strings:: +to use strings: + +.. code-block:: python import sys + + @pytest.mark.skipif("sys.version_info >= (3,3)") def test_function(): ... @@ -139,17 +145,20 @@ dictionary which is constructed as follows: expression is applied. The pytest ``config`` object allows you to skip based on a test -configuration value which you might have added:: +configuration value which you might have added: + +.. code-block:: python @pytest.mark.skipif("not config.getvalue('db')") - def test_function(...): + def test_function(): ... -The equivalent with "boolean conditions" is:: +The equivalent with "boolean conditions" is: - @pytest.mark.skipif(not pytest.config.getvalue("db"), - reason="--db was not specified") - def test_function(...): +.. code-block:: python + + @pytest.mark.skipif(not pytest.config.getvalue("db"), reason="--db was not specified") + def test_function(): pass .. note:: @@ -164,12 +173,16 @@ The equivalent with "boolean conditions" is:: .. versionchanged:: 2.4 -Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``:: +Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``: + +.. code-block:: python import pytest + + def test_function(): ... - pytest.set_trace() # invoke PDB debugger and tracing + pytest.set_trace() # invoke PDB debugger and tracing This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index c9bd09d1a..51481b227 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -36,15 +36,15 @@ pytest enables test parametrization at several levels: The builtin :ref:`pytest.mark.parametrize ref` decorator enables parametrization of arguments for a test function. Here is a typical example of a test function that implements checking that a certain input leads -to an expected output:: +to an expected output: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) + + + @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -104,16 +104,18 @@ Note that you could also use the parametrize marker on a class or a module (see :ref:`mark`) which would invoke several functions with the argument sets. It is also possible to mark individual test instances within parametrize, -for example with the builtin ``mark.xfail``:: +for example with the builtin ``mark.xfail``: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.param("6*9", 42, - marks=pytest.mark.xfail), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", + [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -140,9 +142,13 @@ example, if they're dynamically generated by some function - the behaviour of pytest is defined by the :confval:`empty_parameter_set_mark` option. To get all combinations of multiple parametrized arguments you can stack -``parametrize`` decorators:: +``parametrize`` decorators: + +.. code-block:: python import pytest + + @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): @@ -166,26 +172,36 @@ parametrization. For example, let's say we want to run a test taking string inputs which we want to set via a new ``pytest`` command line option. Let's first write -a simple test accepting a ``stringinput`` fixture function argument:: +a simple test accepting a ``stringinput`` fixture function argument: + +.. code-block:: python # content of test_strings.py + def test_valid_string(stringinput): assert stringinput.isalpha() Now we add a ``conftest.py`` file containing the addition of a -command line option and the parametrization of our test function:: +command line option and the parametrization of our test function: + +.. code-block:: python # content of conftest.py + def pytest_addoption(parser): - parser.addoption("--stringinput", action="append", default=[], - help="list of stringinputs to pass to test functions") + parser.addoption( + "--stringinput", + action="append", + default=[], + help="list of stringinputs to pass to test functions", + ) + def pytest_generate_tests(metafunc): - if 'stringinput' in metafunc.fixturenames: - metafunc.parametrize("stringinput", - metafunc.config.getoption('stringinput')) + if "stringinput" in metafunc.fixturenames: + metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput")) If we now pass two stringinput values, our test will run twice: diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 69d7940fc..871e6c3f0 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -84,32 +84,44 @@ It is also possible to skip the whole module using If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on an interpreter earlier than Python3.6:: +when run on an interpreter earlier than Python3.6: + +.. code-block:: python import sys - @pytest.mark.skipif(sys.version_info < (3,6), - reason="requires python3.6 or higher") + + + @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_function(): ... If the condition evaluates to ``True`` during collection, the test function will be skipped, with the specified reason appearing in the summary when using ``-rs``. -You can share ``skipif`` markers between modules. Consider this test module:: +You can share ``skipif`` markers between modules. Consider this test module: + +.. code-block:: python # content of test_mymodule.py import mymodule - minversion = pytest.mark.skipif(mymodule.__versioninfo__ < (1,1), - reason="at least mymodule-1.1 required") + + minversion = pytest.mark.skipif( + mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required" + ) + + @minversion def test_function(): ... -You can import the marker and reuse it in another test module:: +You can import the marker and reuse it in another test module: + +.. code-block:: python # test_myothermodule.py from test_mymodule import minversion + @minversion def test_anotherfunction(): ... @@ -128,12 +140,12 @@ so they are supported mainly for backward compatibility reasons. Skip all test functions of a class or module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can use the ``skipif`` marker (as any other marker) on classes:: +You can use the ``skipif`` marker (as any other marker) on classes: - @pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") +.. code-block:: python + + @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") class TestPosixCalls(object): - def test_function(self): "will not be setup or run under 'win32' platform" @@ -269,10 +281,11 @@ You can change the default value of the ``strict`` parameter using the ~~~~~~~~~~~~~~~~~~~~ As with skipif_ you can also mark your expectation of a failure -on a particular platform:: +on a particular platform: - @pytest.mark.xfail(sys.version_info >= (3,6), - reason="python3.6 api changes") +.. code-block:: python + + @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes") def test_function(): ... diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index c47817c9a..322cf631c 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -6,15 +6,19 @@ Warnings Capture .. versionadded:: 3.1 Starting from version ``3.1``, pytest now automatically catches warnings during test execution -and displays them at the end of the session:: +and displays them at the end of the session: + +.. code-block:: python # content of test_show_warnings.py import warnings + def api_v1(): warnings.warn(UserWarning("api v1, should use functions from v2")) return 1 + def test_one(): assert api_v1() == 1 @@ -195,28 +199,36 @@ Ensuring code triggers a deprecation warning You can also call a global helper for checking that a certain function call triggers a ``DeprecationWarning`` or -``PendingDeprecationWarning``:: +``PendingDeprecationWarning``: + +.. code-block:: python import pytest + def test_global(): pytest.deprecated_call(myfunction, 17) By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide them. If you wish to record them in your own code, use the -command ``warnings.simplefilter('always')``:: +command ``warnings.simplefilter('always')``: + +.. code-block:: python import warnings import pytest + def test_deprecation(recwarn): - warnings.simplefilter('always') + warnings.simplefilter("always") warnings.warn("deprecated", DeprecationWarning) assert len(recwarn) == 1 assert recwarn.pop(DeprecationWarning) -You can also use it as a contextmanager:: +You can also use it as a contextmanager: + +.. code-block:: python def test_global(): with pytest.deprecated_call(): @@ -238,11 +250,14 @@ Asserting warnings with the warns function .. versionadded:: 2.8 You can check that code raises a particular warning using ``pytest.warns``, -which works in a similar manner to :ref:`raises `:: +which works in a similar manner to :ref:`raises `: + +.. code-block:: python import warnings import pytest + def test_warning(): with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) @@ -269,7 +284,9 @@ You can also call ``pytest.warns`` on a function or code string:: The function also returns a list of all raised warnings (as ``warnings.WarningMessage`` objects), which you can query for -additional information:: +additional information: + +.. code-block:: python with pytest.warns(RuntimeWarning) as record: warnings.warn("another warning", RuntimeWarning) @@ -297,7 +314,9 @@ You can record raised warnings either using ``pytest.warns`` or with the ``recwarn`` fixture. To record with ``pytest.warns`` without asserting anything about the warnings, -pass ``None`` as the expected warning type:: +pass ``None`` as the expected warning type: + +.. code-block:: python with pytest.warns(None) as record: warnings.warn("user", UserWarning) @@ -307,10 +326,13 @@ pass ``None`` as the expected warning type:: assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" -The ``recwarn`` fixture will record warnings for the whole function:: +The ``recwarn`` fixture will record warnings for the whole function: + +.. code-block:: python import warnings + def test_hello(recwarn): warnings.warn("hello", UserWarning) assert len(recwarn) == 1 diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 245bfc4ef..330470fc0 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -509,10 +509,13 @@ a :py:class:`Result ` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). -Here is an example definition of a hook wrapper:: +Here is an example definition of a hook wrapper: + +.. code-block:: python import pytest + @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() @@ -617,10 +620,13 @@ if you depend on a plugin that is not installed, validation will fail and the error message will not make much sense to your users. One approach is to defer the hook implementation to a new plugin instead of -declaring the hook functions directly in your plugin module, for example:: +declaring the hook functions directly in your plugin module, for example: + +.. code-block:: python # contents of myplugin.py + class DeferPlugin(object): """Simple plugin to defer pytest-xdist hook functions.""" @@ -628,8 +634,9 @@ declaring the hook functions directly in your plugin module, for example:: """standard xdist hook function. """ + def pytest_configure(config): - if config.pluginmanager.hasplugin('xdist'): + if config.pluginmanager.hasplugin("xdist"): config.pluginmanager.register(DeferPlugin()) This has the added benefit of allowing you to conditionally install hooks From da2e092163eb1c595fa286af0d801a9682f877a4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 12 Apr 2019 04:52:47 -0700 Subject: [PATCH 36/57] pre-commit autoupdate --- .pre-commit-config.yaml | 14 +++++++------- bench/bench_argcomplete.py | 2 +- src/_pytest/fixtures.py | 4 +++- src/_pytest/pytester.py | 2 +- testing/python/fixtures.py | 4 +--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36ecf79b8..737c6b86b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/ambv/black - rev: 18.9b0 + rev: 19.3b0 hooks: - id: black args: [--safe, --quiet] language_version: python3 - repo: https://github.com/asottile/blacken-docs - rev: v0.3.0 + rev: v0.5.0 hooks: - id: blacken-docs - additional_dependencies: [black==18.9b0] + additional_dependencies: [black==19.3b0] language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 @@ -22,22 +22,22 @@ repos: exclude: _pytest/debugging.py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.0 + rev: 3.7.7 hooks: - id: flake8 language_version: python3 - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/asottile/pyupgrade - rev: v1.11.1 + rev: v1.15.0 hooks: - id: pyupgrade args: [--keep-percent-format] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.2.0 + rev: v1.3.0 hooks: - id: rst-backticks - repo: local diff --git a/bench/bench_argcomplete.py b/bench/bench_argcomplete.py index 2b30add08..335733df7 100644 --- a/bench/bench_argcomplete.py +++ b/bench/bench_argcomplete.py @@ -16,4 +16,4 @@ run = 'fc("/d")' if __name__ == "__main__": print(timeit.timeit(run, setup=setup % imports[0], number=count)) - print((timeit.timeit(run, setup=setup % imports[1], number=count))) + print(timeit.timeit(run, setup=setup % imports[1], number=count)) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0bb525a45..f16561267 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -853,7 +853,9 @@ class FixtureDef(object): exceptions.append(sys.exc_info()) if exceptions: e = exceptions[0] - del exceptions # ensure we don't keep all frames alive because of the traceback + del ( + exceptions + ) # ensure we don't keep all frames alive because of the traceback six.reraise(*e) finally: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 31cea5923..131876910 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1320,7 +1320,7 @@ class LineMatcher(object): raise ValueError("line %r not found in output" % fnline) def _log(self, *args): - self._log_output.append(" ".join((str(x) for x in args))) + self._log_output.append(" ".join(str(x) for x in args)) @property def _log_text(self): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d67d6f4a2..f3393cf65 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1071,9 +1071,7 @@ class TestFixtureUsages(object): ) result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines( - ( - "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" - ) + "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) def test_funcarg_parametrized_and_used_twice(self, testdir): From 43e7401c918c03f8ac758a2ab6284ae0287260c2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 12 Apr 2019 08:04:27 -0700 Subject: [PATCH 37/57] Produce a warning when unknown arguments are passed to pytest.param() --- changelog/5092.bugfix.rst | 1 + src/_pytest/deprecated.py | 6 ++++++ src/_pytest/mark/structures.py | 12 +++++++++--- src/_pytest/outcomes.py | 3 +-- src/_pytest/python_api.py | 2 +- testing/test_mark.py | 13 +++++++++++++ 6 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 changelog/5092.bugfix.rst diff --git a/changelog/5092.bugfix.rst b/changelog/5092.bugfix.rst new file mode 100644 index 000000000..3fd29677c --- /dev/null +++ b/changelog/5092.bugfix.rst @@ -0,0 +1 @@ +Produce a warning when unknown keywords are passed to ``pytest.param(...)``. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 6134ca77b..fa7e89364 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -93,3 +93,9 @@ PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( "pytest.warns() got unexpected keyword arguments: {args!r}.\n" "This will be an error in future versions.", ) + +PYTEST_PARAM_UNKNOWN_KWARGS = UnformattedWarning( + PytestDeprecationWarning, + "pytest.param() got unexpected keyword arguments: {args!r}.\n" + "This will be an error in future versions.", +) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0021dd5d6..59a4ba20f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -10,6 +10,7 @@ from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET +from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -60,20 +61,25 @@ def get_empty_parameterset_mark(config, argnames, func): class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): @classmethod - def param(cls, *values, **kw): - marks = kw.pop("marks", ()) + def param(cls, *values, **kwargs): + marks = kwargs.pop("marks", ()) if isinstance(marks, MarkDecorator): marks = (marks,) else: assert isinstance(marks, (tuple, list, set)) - id_ = kw.pop("id", None) + id_ = kwargs.pop("id", None) if id_ is not None: if not isinstance(id_, six.string_types): raise TypeError( "Expected id to be a string, got {}: {!r}".format(type(id_), id_) ) id_ = ascii_escaped(id_) + + if kwargs: + warnings.warn( + PYTEST_PARAM_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=3 + ) return cls(values, marks, id_) @classmethod diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 6c2dfb5ae..f57983918 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -97,8 +97,7 @@ def skip(msg="", **kwargs): __tracebackhide__ = True allow_module_level = kwargs.pop("allow_module_level", False) if kwargs: - keys = [k for k in kwargs.keys()] - raise TypeError("unexpected keyword arguments: {}".format(keys)) + raise TypeError("unexpected keyword arguments: {}".format(sorted(kwargs))) raise Skipped(msg=msg, allow_module_level=allow_module_level) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1b643d430..66de85468 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -682,7 +682,7 @@ def raises(expected_exception, *args, **kwargs): match_expr = kwargs.pop("match") if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " - msg += ", ".join(kwargs.keys()) + msg += ", ".join(sorted(kwargs)) raise TypeError(msg) return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): diff --git a/testing/test_mark.py b/testing/test_mark.py index 2851dbc16..2e9eea092 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,6 +13,7 @@ from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector from _pytest.nodes import Node +from _pytest.warning_types import PytestDeprecationWarning from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: @@ -991,3 +992,15 @@ def test_pytest_param_id_requires_string(): @pytest.mark.parametrize("s", (None, "hello world")) def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) + + +def test_pytest_param_warning_on_unknown_kwargs(): + with pytest.warns(PytestDeprecationWarning) as warninfo: + # typo, should be marks= + pytest.param(1, 2, mark=pytest.mark.xfail()) + assert warninfo[0].filename == __file__ + msg, = warninfo[0].message.args + assert msg == ( + "pytest.param() got unexpected keyword arguments: ['mark'].\n" + "This will be an error in future versions." + ) From 8fd5a658eb7d36e6816d65ee8f81ebef79c1af14 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 12 Apr 2019 17:27:36 +0200 Subject: [PATCH 38/57] monkeypatch.syspath_prepend: invalidate import cache This was done with testdir only, and uses the fixed monkeypatch method there now. --- changelog/5098.bugfix.rst | 1 + src/_pytest/monkeypatch.py | 12 ++++++++++++ src/_pytest/pytester.py | 19 +------------------ testing/test_monkeypatch.py | 7 +++++++ 4 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 changelog/5098.bugfix.rst diff --git a/changelog/5098.bugfix.rst b/changelog/5098.bugfix.rst new file mode 100644 index 000000000..f0104b24e --- /dev/null +++ b/changelog/5098.bugfix.rst @@ -0,0 +1 @@ +Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index f6c134664..3e221d3d9 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -271,6 +271,18 @@ class MonkeyPatch(object): # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 fixup_namespace_packages(str(path)) + # A call to syspathinsert() usually means that the caller wants to + # import some dynamically created files, thus with python3 we + # invalidate its import caches. + # This is especially important when any namespace package is in used, + # since then the mtime based FileFinder cache (that gets created in + # this case already) gets not invalidated when writing the new files + # quickly afterwards. + if sys.version_info >= (3, 3): + from importlib import invalidate_caches + + invalidate_caches() + 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/src/_pytest/pytester.py b/src/_pytest/pytester.py index 131876910..d90ef1ce7 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -616,27 +616,10 @@ class Testdir(object): This is undone automatically when this object dies at the end of each test. """ - from pkg_resources import fixup_namespace_packages - if path is None: path = self.tmpdir - 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 - self._possibly_invalidate_import_caches() - - def _possibly_invalidate_import_caches(self): - # invalidate caches if we can (py33 and above) - try: - from importlib import invalidate_caches - except ImportError: - return - invalidate_caches() + self.monkeypatch.syspath_prepend(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index d43fb6bab..9e45e05c8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -462,3 +462,10 @@ def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): import ns_pkg.world assert ns_pkg.world.check() == "world" + + # Should invalidate caches via importlib.invalidate_caches. + tmpdir = testdir.tmpdir + modules_tmpdir = tmpdir.mkdir("modules_tmpdir") + monkeypatch.syspath_prepend(str(modules_tmpdir)) + modules_tmpdir.join("main_app.py").write("app = True") + from main_app import app # noqa: F401 From 1dd5f088faed8248f06b3ef59b8e38b0c249e4c3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 21:55:15 +0200 Subject: [PATCH 39/57] test_pytest_exit_returncode: ignore ResourceWarnings Fixes https://github.com/pytest-dev/pytest/issues/5088. --- testing/test_runner.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index cf335dfad..ec144dc0e 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -581,7 +581,14 @@ def test_pytest_exit_returncode(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"]) - assert result.stderr.lines == [""] + # Assert no output on stderr, except for unreliable ResourceWarnings. + # (https://github.com/pytest-dev/pytest/issues/5088) + assert [ + x + for x in result.stderr.lines + if not x.startswith("Exception ignored in:") + and not x.startswith("ResourceWarning") + ] == [""] assert result.ret == 99 # It prints to stderr also in case of exit during pytest_sessionstart. From 1da8ce65a62b855214903eaad9c031d784d1bbb6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 22:58:19 +0200 Subject: [PATCH 40/57] pytester: raise_on_kwargs: ignore branch coverage --- src/_pytest/pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 40b014dbd..1cf6515a0 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -77,7 +77,7 @@ def pytest_configure(config): def raise_on_kwargs(kwargs): __tracebackhide__ = True - if kwargs: + if kwargs: # pragma: no branch raise TypeError( "Unexpected keyword arguments: {}".format(", ".join(sorted(kwargs))) ) From f3dbe5a3084bd0386c07691f684652fb19b3d614 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 22:58:41 +0200 Subject: [PATCH 41/57] pytester: listoutcomes: assert instead of implicit if --- src/_pytest/pytester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1cf6515a0..532d799f2 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -312,7 +312,8 @@ class HookRecorder(object): passed.append(rep) elif rep.skipped: skipped.append(rep) - elif rep.failed: + else: + assert rep.failed, "Unexpected outcome: {!r}".format(rep) failed.append(rep) return passed, skipped, failed From fd0b3e2e8bab00693d3b5bcea47e5aa94eb42f7c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:15:31 +0200 Subject: [PATCH 42/57] getreportopt: remove needless if --- src/_pytest/terminal.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..c2d0d0ce1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -165,15 +165,14 @@ def getreportopt(config): reportchars += "w" elif config.option.disable_warnings and "w" in reportchars: reportchars = reportchars.replace("w", "") - if reportchars: - for char in reportchars: - if char == "a": - reportopts = "sxXwEf" - elif char == "A": - reportopts = "sxXwEfpP" - break - elif char not in reportopts: - reportopts += char + for char in reportchars: + if char == "a": + reportopts = "sxXwEf" + elif char == "A": + reportopts = "sxXwEfpP" + break + elif char not in reportopts: + reportopts += char return reportopts From cc78a533aee7e40d8412262552d3e86cba37f409 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:17:09 +0200 Subject: [PATCH 43/57] terminal: summary_errors: replace if with assert --- src/_pytest/terminal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index c2d0d0ce1..fe3996238 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -851,7 +851,8 @@ class TerminalReporter(object): msg = "ERROR collecting " + msg elif rep.when == "setup": msg = "ERROR at setup of " + msg - elif rep.when == "teardown": + else: + assert rep.when == "teardown", "Unexpected rep: %r" % (rep,) msg = "ERROR at teardown of " + msg self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) From f1f1862b1950f90ccca4e20fbd73bf4e3c6f56bf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:26:56 +0200 Subject: [PATCH 44/57] Update testing/test_runner.py --- testing/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index ec144dc0e..c52d2ea7c 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -581,7 +581,7 @@ def test_pytest_exit_returncode(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"]) - # Assert no output on stderr, except for unreliable ResourceWarnings. + # Assert no output on stderr, except for unreliable ResourceWarnings. # (https://github.com/pytest-dev/pytest/issues/5088) assert [ x From 1d137fd2fecfcaf3527f331eca56f9c77df12cd5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 15:03:22 +0200 Subject: [PATCH 45/57] minor: LFPlugin: de-indent code by returning if not active --- src/_pytest/cacheprovider.py | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index a1200ce37..63503ed2e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -179,45 +179,45 @@ class LFPlugin(object): self.lastfailed[report.nodeid] = True def pytest_collection_modifyitems(self, session, config, items): - if self.active: - if self.lastfailed: - previously_failed = [] - previously_passed = [] - for item in items: - if item.nodeid in self.lastfailed: - previously_failed.append(item) - else: - previously_passed.append(item) - self._previously_failed_count = len(previously_failed) + if not self.active: + return - if not previously_failed: - # Running a subset of all tests with recorded failures - # only outside of it. - self._report_status = "%d known failures not in selected tests" % ( - len(self.lastfailed), - ) + if self.lastfailed: + previously_failed = [] + previously_passed = [] + for item in items: + if item.nodeid in self.lastfailed: + previously_failed.append(item) else: - if self.config.getoption("lf"): - items[:] = previously_failed - config.hook.pytest_deselected(items=previously_passed) - else: # --failedfirst - items[:] = previously_failed + previously_passed + previously_passed.append(item) + self._previously_failed_count = len(previously_failed) - noun = ( - "failure" if self._previously_failed_count == 1 else "failures" - ) - suffix = " first" if self.config.getoption("failedfirst") else "" - self._report_status = "rerun previous {count} {noun}{suffix}".format( - count=self._previously_failed_count, suffix=suffix, noun=noun - ) + if not previously_failed: + # Running a subset of all tests with recorded failures + # only outside of it. + self._report_status = "%d known failures not in selected tests" % ( + len(self.lastfailed), + ) else: - self._report_status = "no previously failed tests, " - if self.config.getoption("last_failed_no_failures") == "none": - self._report_status += "deselecting all items." - config.hook.pytest_deselected(items=items) - items[:] = [] - else: - self._report_status += "not deselecting items." + if self.config.getoption("lf"): + items[:] = previously_failed + config.hook.pytest_deselected(items=previously_passed) + else: # --failedfirst + items[:] = previously_failed + previously_passed + + noun = "failure" if self._previously_failed_count == 1 else "failures" + suffix = " first" if self.config.getoption("failedfirst") else "" + self._report_status = "rerun previous {count} {noun}{suffix}".format( + count=self._previously_failed_count, suffix=suffix, noun=noun + ) + else: + self._report_status = "no previously failed tests, " + if self.config.getoption("last_failed_no_failures") == "none": + self._report_status += "deselecting all items." + config.hook.pytest_deselected(items=items) + items[:] = [] + else: + self._report_status += "not deselecting items." def pytest_sessionfinish(self, session): config = self.config From bd1a2e6435d770e9e5d007f1329941ab3cd1db92 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 8 Apr 2019 12:42:30 +0200 Subject: [PATCH 46/57] fix typo --- src/_pytest/mark/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f3602b2d5..cda16c7ad 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -298,7 +298,7 @@ class MarkGenerator(object): for line in self._config.getini("markers"): # example lines: "skipif(condition): skip the given test if..." # or "hypothesis: tests which use Hypothesis", so to get the - # marker name we we split on both `:` and `(`. + # marker name we split on both `:` and `(`. marker = line.split(":")[0].split("(")[0].strip() self._markers.add(marker) From 992e7f7771646ecd80c10c11ae50666bccc6437e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 13 Apr 2019 18:51:27 +0200 Subject: [PATCH 47/57] rename variable --- src/_pytest/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..46b624888 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -453,10 +453,10 @@ class TerminalReporter(object): progress_length = len(" [100%]") self._progress_nodeids_reported.add(nodeid) - last_item = ( + is_last_item = ( len(self._progress_nodeids_reported) == self._session.testscollected ) - if last_item: + if is_last_item: self._write_progress_information_filling_space() else: w = self._width_of_current_line From e804e419bc5bd3ee2115b49c958488c53ffe9863 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:49:24 +0200 Subject: [PATCH 48/57] remove unnecessary newline --- src/_pytest/config/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4542f06ab..d77561f85 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -282,7 +282,6 @@ class PytestPluginManager(PluginManager): known_marks = {m.name for m in getattr(method, "pytestmark", [])} for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): - opts.setdefault(name, hasattr(method, name) or name in known_marks) return opts From c43a9c83eec4b9f996c53a465ffb4f2b69c19e7e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:50:14 +0200 Subject: [PATCH 49/57] doc: pytest_deselected: not only via keywords --- src/_pytest/hookspec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 5a3eb282d..25c2c3cbc 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -227,7 +227,7 @@ def pytest_collectreport(report): def pytest_deselected(items): - """ called for test items deselected by keyword. """ + """ called for test items deselected, e.g. by keyword. """ @hookspec(firstresult=True) From b2be6c1a30a35554800141a9006660a738d1ad28 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 05:47:35 +0200 Subject: [PATCH 50/57] TestReport: use class name in repr --- src/_pytest/reports.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index d2df4d21f..97699cfc1 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -328,7 +328,8 @@ class TestReport(BaseReport): self.__dict__.update(extra) def __repr__(self): - return "" % ( + return "<%s %r when=%r outcome=%r>" % ( + self.__class__.__name__, self.nodeid, self.when, self.outcome, From 20c624efcfba7566b4c0dce2e031ea13c0e6dfab Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 05:48:35 +0200 Subject: [PATCH 51/57] terminal: revisit summary_failures - get the list of reports for teardown sections only once - do not check option in loop --- src/_pytest/terminal.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..2d14a7bdc 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -828,17 +828,22 @@ class TerminalReporter(object): if not reports: return self.write_sep("=", "FAILURES") - for rep in reports: - if self.config.option.tbstyle == "line": + if self.config.option.tbstyle == "line": + for rep in reports: line = self._getcrashline(rep) self.write_line(line) - else: + else: + teardown_sections = {} + for report in self.getreports(""): + if report.when == "teardown": + teardown_sections.setdefault(report.nodeid, []).append(report) + + for rep in reports: msg = self._getfailureheadline(rep) self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) - for report in self.getreports(""): - if report.nodeid == rep.nodeid and report.when == "teardown": - self.print_teardown_sections(report) + for report in teardown_sections.get(rep.nodeid, []): + self.print_teardown_sections(report) def summary_errors(self): if self.config.option.tbstyle != "no": From cc005af47ee31bc1364b29fef7de72a65889ad99 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 10:15:37 +0200 Subject: [PATCH 52/57] Fix error message with unregistered markers --- src/_pytest/mark/structures.py | 2 +- testing/test_mark.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f3602b2d5..5b88bceda 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -306,7 +306,7 @@ class MarkGenerator(object): # then it really is time to issue a warning or an error. if name not in self._markers: if self._config.option.strict: - fail("{!r} not a registered marker".format(name), pytrace=False) + fail("{!r} is not a registered marker".format(name), pytrace=False) else: warnings.warn( "Unknown pytest.mark.%s - is this a typo? You can register " diff --git a/testing/test_mark.py b/testing/test_mark.py index cb20658b5..e3a9f0c4c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -204,7 +204,7 @@ def test_strict_prohibits_unregistered_markers(testdir): ) result = testdir.runpytest("--strict") assert result.ret != 0 - result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"]) + result.stdout.fnmatch_lines(["'unregisteredmark' is not a registered marker"]) @pytest.mark.parametrize( From 006dc30476b7ed5924f8f7e8bb97de06a9b573e6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Apr 2019 14:24:17 +0000 Subject: [PATCH 53/57] Preparing release version 4.4.1 --- CHANGELOG.rst | 18 +++++++++++++ changelog/5031.bugfix.rst | 1 - changelog/5039.bugfix.rst | 1 - changelog/5092.bugfix.rst | 1 - changelog/5098.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.4.1.rst | 20 +++++++++++++++ doc/en/assert.rst | 10 ++++---- doc/en/cache.rst | 6 ++--- doc/en/capture.rst | 2 +- doc/en/doctest.rst | 2 +- doc/en/example/markers.rst | 40 ++++++++++++++--------------- doc/en/example/nonpython.rst | 8 +++--- doc/en/example/parametrize.rst | 20 +++++++-------- doc/en/example/pythoncollection.rst | 6 ++--- doc/en/example/reportingdemo.rst | 4 +-- doc/en/example/simple.rst | 26 +++++++++---------- doc/en/fixture.rst | 12 ++++----- doc/en/getting-started.rst | 2 +- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 16 +++++------- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 4 +-- doc/en/unittest.rst | 2 +- doc/en/usage.rst | 16 ++++++------ doc/en/warnings.rst | 10 ++++---- doc/en/writing_plugins.rst | 4 +-- 27 files changed, 134 insertions(+), 103 deletions(-) delete mode 100644 changelog/5031.bugfix.rst delete mode 100644 changelog/5039.bugfix.rst delete mode 100644 changelog/5092.bugfix.rst delete mode 100644 changelog/5098.bugfix.rst create mode 100644 doc/en/announce/release-4.4.1.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 533eb9c15..4408c7340 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,24 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.4.1 (2019-04-15) +========================= + +Bug Fixes +--------- + +- `#5031 `_: Environment variables are properly restored when using pytester's ``testdir`` fixture. + + +- `#5039 `_: Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0. + + +- `#5092 `_: Produce a warning when unknown keywords are passed to ``pytest.param(...)``. + + +- `#5098 `_: Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used. + + pytest 4.4.0 (2019-03-29) ========================= diff --git a/changelog/5031.bugfix.rst b/changelog/5031.bugfix.rst deleted file mode 100644 index 6ad80b1e3..000000000 --- a/changelog/5031.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Environment variables are properly restored when using pytester's ``testdir`` fixture. diff --git a/changelog/5039.bugfix.rst b/changelog/5039.bugfix.rst deleted file mode 100644 index 4e173f64e..000000000 --- a/changelog/5039.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0. diff --git a/changelog/5092.bugfix.rst b/changelog/5092.bugfix.rst deleted file mode 100644 index 3fd29677c..000000000 --- a/changelog/5092.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Produce a warning when unknown keywords are passed to ``pytest.param(...)``. diff --git a/changelog/5098.bugfix.rst b/changelog/5098.bugfix.rst deleted file mode 100644 index f0104b24e..000000000 --- a/changelog/5098.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 7e2554656..c467535bd 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.4.1 release-4.4.0 release-4.3.1 release-4.3.0 diff --git a/doc/en/announce/release-4.4.1.rst b/doc/en/announce/release-4.4.1.rst new file mode 100644 index 000000000..12c0ee779 --- /dev/null +++ b/doc/en/announce/release-4.4.1.rst @@ -0,0 +1,20 @@ +pytest-4.4.1 +======================================= + +pytest 4.4.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 67ecb5348..dec3cd941 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -33,7 +33,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert1.py F [100%] @@ -46,7 +46,7 @@ you will see the return value of the function call: E assert 3 == 4 E + where 3 = f() - test_assert1.py:5: AssertionError + test_assert1.py:6: AssertionError ========================= 1 failed in 0.12 seconds ========================= ``pytest`` has support for showing the values of the most common subexpressions @@ -188,7 +188,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert2.py F [100%] @@ -207,7 +207,7 @@ if you run this module: E '5' E Use -v to get the full diff - test_assert2.py:5: AssertionError + test_assert2.py:6: AssertionError ========================= 1 failed in 0.12 seconds ========================= Special comparisons are done for a number of cases: @@ -275,7 +275,7 @@ the conftest file: E assert Comparing Foo instances: E vals: 1 != 2 - test_foocompare.py:11: AssertionError + test_foocompare.py:12: AssertionError 1 failed in 0.12 seconds .. _assert-details: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 8baf88113..4286fc40f 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: /home/sweet/project + 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 50 items run-last-failure: rerun previous 2 failures first @@ -258,7 +258,7 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache ------------------------------- cache values ------------------------------- cache/lastfailed contains: diff --git a/doc/en/capture.rst b/doc/en/capture.rst index f0652aa26..8629350a5 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -71,7 +71,7 @@ of the failing function and hide the other one: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .F [100%] diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index db7eb14a4..549ebb00f 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -72,7 +72,7 @@ then you can just invoke ``pytest`` without command line options: =========================== 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: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 1 item mymodule.py . [100%] diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index f5a37bdb3..6275bbae0 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -45,7 +45,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -60,7 +60,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -82,7 +82,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -97,7 +97,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -112,7 +112,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_server.py::TestClass::test_method PASSED [ 50%] @@ -152,7 +152,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -167,7 +167,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -184,7 +184,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 2 deselected / 2 selected test_server.py::test_send_http PASSED [ 50%] @@ -407,7 +407,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py s [100%] @@ -422,7 +422,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py . [100%] @@ -615,12 +615,12 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] /home/sweet/project/conftest.py:12: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux =================== 2 passed, 2 skipped in 0.12 seconds ==================== @@ -632,7 +632,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 3 deselected / 1 selected test_plat.py . [100%] @@ -696,18 +696,18 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 2 deselected / 2 selected test_module.py FF [100%] ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ - test_module.py:3: in test_interface_simple + test_module.py:4: in test_interface_simple assert 0 E assert 0 __________________________ test_interface_complex __________________________ - test_module.py:6: in test_interface_complex + test_module.py:8: in test_interface_complex assert 0 E assert 0 ================== 2 failed, 2 deselected in 0.12 seconds ================== @@ -720,22 +720,22 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 1 deselected / 3 selected test_module.py FFF [100%] ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ - test_module.py:3: in test_interface_simple + test_module.py:4: in test_interface_simple assert 0 E assert 0 __________________________ test_interface_complex __________________________ - test_module.py:6: in test_interface_complex + test_module.py:8: in test_interface_complex assert 0 E assert 0 ____________________________ test_event_simple _____________________________ - test_module.py:9: in test_event_simple + test_module.py:12: in test_event_simple assert 0 E assert 0 ================== 3 failed, 1 deselected in 0.12 seconds ================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index f6d1e578e..0910071c1 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -31,7 +31,7 @@ now execute the test specification: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/nonpython + 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: /home/sweet/project/nonpython + rootdir: $REGENDOC_TMPDIR/nonpython collecting ... collected 2 items test_simple.yml::hello FAILED [ 50%] @@ -90,9 +90,9 @@ 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: /home/sweet/project/nonpython + rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items - + diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 19de27af6..e7dbadf2d 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item @@ -434,9 +434,9 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ......sss......ssssssssssss [100%] + ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIPPED [15] /home/sweet/project/CWD/multipython.py:30: 'python3.5' not found + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports @@ -488,12 +488,12 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] /home/sweet/project/conftest.py:11: could not import 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' =================== 1 passed, 1 skipped in 0.12 seconds ==================== @@ -550,7 +550,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: /home/sweet/project + 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/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 40eb333b1..02c12f6bc 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -148,7 +148,7 @@ The test collection would look 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: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items @@ -210,7 +210,7 @@ You can always peek at the collection tree without running tests 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: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 3 items @@ -285,7 +285,7 @@ file will be left out: =========================== 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: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2a2364c27..26c6e1b9e 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -11,7 +11,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: =========================== 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: /home/sweet/project/assertion + rootdir: $REGENDOC_TMPDIR/assertion collected 44 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] @@ -471,7 +471,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 == 0 E AssertionError - <0-codegen 'abc-123' /home/sweet/project/assertion/failure_demo.py:201>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:201>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a909cd74f..e9fe1f249 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -129,7 +129,7 @@ directory with the above conftest.py: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 7 items test_step.py .Fx. [ 57%] @@ -603,13 +603,13 @@ We can run this: ================================== ERRORS ================================== _______________________ ERROR at setup of test_root ________________________ - file /home/sweet/project/b/test_error.py, line 1 + file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. - /home/sweet/project/b/test_error.py:1 + $REGENDOC_TMPDIR/b/test_error.py:1 ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ @@ -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: /home/sweet/project + 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: /home/sweet/project + 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 1de33054e..6cbec6ddc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -74,7 +74,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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: /home/sweet/project + 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 58b0cfca4..0ba19cbba 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -52,7 +52,7 @@ That’s it. You can now execute the test function: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/index.rst b/doc/en/index.rst index 32d10040c..3ace95eff 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -30,7 +30,7 @@ To execute it: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 51481b227..3a2104317 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..F [100%] @@ -68,17 +68,13 @@ them in turn: test_input = '6*9', expected = 42 - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) + @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): > assert eval(test_input) == expected E AssertionError: assert 54 == 42 E + where 54 = eval('6*9') - test_expectation.py:8: AssertionError + test_expectation.py:6: AssertionError ==================== 1 failed, 2 passed in 0.12 seconds ==================== .. note:: @@ -127,7 +123,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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..x [100%] @@ -228,7 +224,7 @@ Let's also run with a stringinput that will lead to a failing test: E + where False = () E + where = '!'.isalpha - test_strings.py:3: AssertionError + test_strings.py:4: AssertionError 1 failed in 0.12 seconds As expected our test function fails. @@ -242,7 +238,7 @@ list: $ pytest -q -rs test_strings.py s [100%] ========================= short test summary info ========================== - SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:1 + SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 1 skipped in 0.12 seconds Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 871e6c3f0..a2b10f70d 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -348,7 +348,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: /home/sweet/project/example + 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 8fbd1fe59..8583f33b4 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -43,7 +43,7 @@ Running this would result in a passed test except for the last =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_tmpdir.py F [100%] diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 53ccef599..05632aef4 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -130,7 +130,7 @@ the ``self.db`` values in the traceback: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + 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 d6850beda..d69aa6c07 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -204,7 +204,7 @@ Example: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -227,13 +227,13 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] /home/sweet/project/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error FAILED test_example.py::test_fail - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -280,8 +280,8 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - SKIPPED [1] /home/sweet/project/test_example.py:23: skipping this test - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -320,7 +320,7 @@ captured output: _________________________________ test_ok __________________________________ --------------------------- Captured stdout call --------------------------- ok - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 322cf631c..26bd2fdb2 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -30,14 +30,14 @@ 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: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_show_warnings.py . [100%] ============================= warnings summary ============================= test_show_warnings.py::test_one - /home/sweet/project/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 + $REGENDOC_TMPDIR/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2 warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html @@ -56,14 +56,14 @@ them into errors: def test_one(): > assert api_v1() == 1 - test_show_warnings.py:8: + test_show_warnings.py:10: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def api_v1(): > warnings.warn(UserWarning("api v1, should use functions from v2")) E UserWarning: api v1, should use functions from v2 - test_show_warnings.py:4: UserWarning + test_show_warnings.py:5: UserWarning 1 failed in 0.12 seconds The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. @@ -400,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta ============================= warnings summary ============================= test_pytest_warnings.py:1 - /home/sweet/project/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 330470fc0..16543b7b8 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -414,14 +414,14 @@ additionally it is possible to copy examples for an example folder before runnin =========================== 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: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items test_example.py .. [100%] ============================= warnings summary ============================= test_example.py::test_plugin - /home/sweet/project/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time + $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html From ea79eb5c3ff96e5dad7719650784580cd31fa0ad Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 15:13:15 +0200 Subject: [PATCH 54/57] terminal summary: display passes after warnings This displays passes (with output, `-rP`) before the short summary, and before any other output from other plugins also. --- changelog/5108.feature.rst | 1 + src/_pytest/terminal.py | 2 +- testing/test_terminal.py | 14 +++++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 changelog/5108.feature.rst diff --git a/changelog/5108.feature.rst b/changelog/5108.feature.rst new file mode 100644 index 000000000..3b66ce5bf --- /dev/null +++ b/changelog/5108.feature.rst @@ -0,0 +1 @@ +The short test summary is displayed after passes with output (``-rP``). diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..b8b37835f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -684,9 +684,9 @@ class TerminalReporter(object): self.summary_errors() self.summary_failures() self.summary_warnings() + self.summary_passes() yield self.short_test_summary() - self.summary_passes() # Display any extra warnings from teardown here (if any). self.summary_warnings() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index feacc242d..de1da0249 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -769,11 +769,19 @@ def test_pass_output_reporting(testdir): assert "test_pass_has_output" not in s assert "Four score and seven years ago..." not in s assert "test_pass_no_output" not in s - result = testdir.runpytest("-rP") + result = testdir.runpytest("-rPp") result.stdout.fnmatch_lines( - ["*test_pass_has_output*", "Four score and seven years ago..."] + [ + "*= PASSES =*", + "*_ test_pass_has_output _*", + "*- Captured stdout call -*", + "Four score and seven years ago...", + "*= short test summary info =*", + "PASSED test_pass_output_reporting.py::test_pass_has_output", + "PASSED test_pass_output_reporting.py::test_pass_no_output", + "*= 2 passed in *", + ] ) - assert "test_pass_no_output" not in result.stdout.str() def test_color_yes(testdir): From eb13530560da34318e96c7c375e1cdf75a9e8ee1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 17:04:34 +0200 Subject: [PATCH 55/57] _getfailureheadline: get head_line property only once --- src/_pytest/terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..f33e8ccd8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -734,10 +734,10 @@ class TerminalReporter(object): return res + " " def _getfailureheadline(self, rep): - if rep.head_line: - return rep.head_line - else: - return "test session" # XXX? + head_line = rep.head_line + if head_line: + return head_line + return "test session" # XXX? def _getcrashline(self, rep): try: From 9374114370ad509169745548ffcc1176d430daf4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 07:23:40 +0200 Subject: [PATCH 56/57] terminal/reports: add/use _get_verbose_word method --- src/_pytest/reports.py | 6 ++++++ src/_pytest/terminal.py | 14 ++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index d2df4d21f..4d32f04b6 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -148,6 +148,12 @@ class BaseReport(object): fspath, lineno, domain = self.location return domain + def _get_verbose_word(self, config): + _category, _short, verbose = config.hook.pytest_report_teststatus( + report=self, config=config + ) + return verbose + def _to_json(self): """ This was originally the serialize_report() function from xdist (ca03269). diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..adc9a92f5 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -888,14 +888,14 @@ class TerminalReporter(object): def show_simple(stat, lines): failed = self.stats.get(stat, []) for rep in failed: - verbose_word = _get_report_str(self.config, rep) + verbose_word = rep._get_verbose_word(self.config) pos = _get_pos(self.config, rep) lines.append("%s %s" % (verbose_word, pos)) def show_xfailed(lines): xfailed = self.stats.get("xfailed", []) for rep in xfailed: - verbose_word = _get_report_str(self.config, rep) + verbose_word = rep._get_verbose_word(self.config) pos = _get_pos(self.config, rep) lines.append("%s %s" % (verbose_word, pos)) reason = rep.wasxfail @@ -905,7 +905,7 @@ class TerminalReporter(object): def show_xpassed(lines): xpassed = self.stats.get("xpassed", []) for rep in xpassed: - verbose_word = _get_report_str(self.config, rep) + verbose_word = rep._get_verbose_word(self.config) pos = _get_pos(self.config, rep) reason = rep.wasxfail lines.append("%s %s %s" % (verbose_word, pos, reason)) @@ -915,7 +915,7 @@ class TerminalReporter(object): fskips = _folded_skips(skipped) if skipped else [] if not fskips: return - verbose_word = _get_report_str(self.config, report=skipped[0]) + verbose_word = skipped[0]._get_verbose_word(self.config) for num, fspath, lineno, reason in fskips: if reason.startswith("Skipped: "): reason = reason[9:] @@ -927,12 +927,6 @@ class TerminalReporter(object): else: lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) - 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 From 7412df092040bbb1f29bdc6de32068a3761c2b9a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 15 Apr 2019 22:53:31 +0200 Subject: [PATCH 57/57] fixup! terminal: summary_errors: replace if with assert --- src/_pytest/terminal.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index fe3996238..a238b38f5 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -849,11 +849,8 @@ class TerminalReporter(object): msg = self._getfailureheadline(rep) if rep.when == "collect": msg = "ERROR collecting " + msg - elif rep.when == "setup": - msg = "ERROR at setup of " + msg else: - assert rep.when == "teardown", "Unexpected rep: %r" % (rep,) - msg = "ERROR at teardown of " + msg + msg = "ERROR at %s of %s" % (rep.when, msg) self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep)