From adebfd0a847feac0be580c638e39aad1976c497a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 28 Feb 2019 14:18:16 +0100 Subject: [PATCH 001/104] pdb: add option to skip `pdb.set_trace()` --- changelog/4854.feature.rst | 5 +++++ src/_pytest/debugging.py | 10 ++++++++++ testing/test_pdb.py | 14 ++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 changelog/4854.feature.rst diff --git a/changelog/4854.feature.rst b/changelog/4854.feature.rst new file mode 100644 index 000000000..c48f08da2 --- /dev/null +++ b/changelog/4854.feature.rst @@ -0,0 +1,5 @@ +The ``--pdb-skip`` option can now be used to ignore calls to +``pdb.set_trace()`` (and ``pytest.set_trace()``). + +This is meant to help while debugging, where you want to use e.g. ``--pdb`` or +``--trace`` only, or just run the tests again without any interruption. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index bb90d00ca..3ad83bb1c 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -59,6 +59,13 @@ def pytest_addoption(parser): action="store_true", help="Immediately break when running each test.", ) + group._addoption( + "--pdb-skip", + "--pdb-ignore-set_trace", + dest="pdb_ignore_set_trace", + action="store_true", + help="Ignore calls to pdb.set_trace().", + ) def pytest_configure(config): @@ -202,6 +209,9 @@ class pytestPDB(object): @classmethod def set_trace(cls, *args, **kwargs): """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" + if pytestPDB._config: # Might not be available when called directly. + if pytestPDB._config.getoption("pdb_ignore_set_trace"): + return frame = sys._getframe().f_back _pdb = cls._init_pdb(*args, **kwargs) _pdb.set_trace(frame) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index d5cf17ef9..1dd31d439 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -10,6 +10,7 @@ import sys import _pytest._code import pytest from _pytest.debugging import _validate_usepdb_cls +from _pytest.main import EXIT_NOTESTSCOLLECTED try: breakpoint @@ -1112,3 +1113,16 @@ 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_pdb_skip_option(testdir): + p = testdir.makepyfile( + """ + print("before_set_trace") + __import__('pdb').set_trace() + print("after_set_trace") + """ + ) + result = testdir.runpytest_inprocess("--pdb-ignore-set_trace", "-s", p) + assert result.ret == EXIT_NOTESTSCOLLECTED + result.stdout.fnmatch_lines(["*before_set_trace*", "*after_set_trace*"]) From 38d687f7c795ecb57111eb9a57173579a1122696 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 31 Mar 2019 14:22:30 +1100 Subject: [PATCH 002/104] Fix typos in comments --- src/_pytest/mark/structures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0021dd5d6..5186e7545 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -129,7 +129,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): ) else: # empty parameter set (likely computed at runtime): create a single - # parameter set with NOSET values, with the "empty parameter set" mark applied to it + # parameter set with NOTSET values, with the "empty parameter set" mark applied to it mark = get_empty_parameterset_mark(config, argnames, func) parameters.append( ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) @@ -152,7 +152,7 @@ class Mark(object): :type other: Mark :rtype: Mark - combines by appending aargs and merging the mappings + combines by appending args and merging the mappings """ assert self.name == other.name return Mark( From ba1fc02a9b11ccfeb6e58e9c807431b6b30ba05c Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 31 Mar 2019 14:22:30 +1100 Subject: [PATCH 003/104] Register mark used by pytester --- src/_pytest/pytester.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 7fc5eaf64..4afadc5a5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -68,6 +68,12 @@ def pytest_configure(config): if checker.matching_platform(): config.pluginmanager.register(checker) + config.addinivalue_line( + "markers", + "pytester_example_path(*path_segments): join the given path " + "segments to `pytester_example_dir` for this test.", + ) + def raise_on_kwargs(kwargs): if kwargs: From bcc08ffe4df58dd8104db2570b0ed040d4420eb7 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 31 Mar 2019 14:22:30 +1100 Subject: [PATCH 004/104] More docs on registering marks --- changelog/4935.doc.rst | 1 + doc/en/mark.rst | 17 ++++++++++------- doc/en/writing_plugins.rst | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 changelog/4935.doc.rst diff --git a/changelog/4935.doc.rst b/changelog/4935.doc.rst new file mode 100644 index 000000000..ac948b568 --- /dev/null +++ b/changelog/4935.doc.rst @@ -0,0 +1 @@ +Expand docs on registering marks and the effect of ``--strict``. diff --git a/doc/en/mark.rst b/doc/en/mark.rst index e841a6780..6b9de7e6d 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -26,12 +26,10 @@ which also serve as documentation. :ref:`fixtures `. -Raising errors on unknown marks: --strict ------------------------------------------ +.. _unknown-marks: -When the ``--strict`` command-line flag is passed, any unknown marks applied -with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. -Marks defined or added by pytest or by a plugin will not trigger an error. +Raising errors on unknown marks +------------------------------- Marks can be registered in ``pytest.ini`` like this: @@ -42,8 +40,10 @@ Marks can be registered in ``pytest.ini`` like this: slow serial -This can be used to prevent users mistyping mark names by accident. Test suites that want to enforce this -should add ``--strict`` to ``addopts``: +When the ``--strict`` command-line flag is passed, any unknown marks applied +with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. +Marks added by pytest or by a plugin instead of the decorator will not trigger +this error. To enforce a limited set of markers, add ``--strict`` to ``addopts``: .. code-block:: ini @@ -53,6 +53,9 @@ should add ``--strict`` to ``addopts``: slow serial +Third-party plugins should always :ref:`register their markers ` +so that they appear in pytest's help text and do not emit warnings. + .. _marker-revamp: diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index bc1bcda0e..8684a0431 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -286,6 +286,26 @@ the plugin manager like this: If you want to look at the names of existing plugins, use the ``--trace-config`` option. + +.. _registering-markers: + +Registering custom markers +-------------------------- + +If your plugin uses any markers, you should register them so that they appear in +pytest's help text and do not :ref:`cause spurious warnings `. +For example, the following plugin would register ``cool_marker`` and +``mark_with`` for all users: + +.. code-block:: python + + def pytest_configure(config): + config.addinivalue_line("markers", "cool_marker: this one is for cool tests.") + config.addinivalue_line( + "markers", "mark_with(arg, arg2): this marker takes arguments." + ) + + Testing plugins --------------- From 00810b9b2a6a4483e7dc8121df76df32f85cdaf7 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 31 Mar 2019 14:22:30 +1100 Subject: [PATCH 005/104] Register "issue" mark for self-tests --- testing/python/fixtures.py | 8 ++++---- testing/python/integration.py | 2 +- testing/python/metafunc.py | 36 +++++++++++++++++------------------ testing/test_capture.py | 2 +- testing/test_conftest.py | 2 +- testing/test_mark.py | 4 ++-- tox.ini | 2 ++ 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d67d6f4a2..e262152e4 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1927,7 +1927,7 @@ class TestAutouseManagement(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - @pytest.mark.issue226 + @pytest.mark.issue(226) @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first(self, testdir, param1, param2): @@ -2709,7 +2709,7 @@ class TestFixtureMarker(object): reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=5) - @pytest.mark.issue246 + @pytest.mark.issue(246) @pytest.mark.parametrize("scope", ["session", "function", "module"]) def test_finalizer_order_on_parametrization(self, scope, testdir): testdir.makepyfile( @@ -2746,7 +2746,7 @@ class TestFixtureMarker(object): reprec = testdir.inline_run("-lvs") reprec.assertoutcome(passed=3) - @pytest.mark.issue396 + @pytest.mark.issue(396) def test_class_scope_parametrization_ordering(self, testdir): testdir.makepyfile( """ @@ -2867,7 +2867,7 @@ class TestFixtureMarker(object): res = testdir.runpytest("-v") res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"]) - @pytest.mark.issue920 + @pytest.mark.issue(920) def test_deterministic_fixture_collection(self, testdir, monkeypatch): testdir.makepyfile( """ diff --git a/testing/python/integration.py b/testing/python/integration.py index 79de048c3..3c6eecbe1 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -393,7 +393,7 @@ class TestNoselikeTestAttribute(object): assert not call.items -@pytest.mark.issue351 +@pytest.mark.issue(351) class TestParameterize(object): def test_idfn_marker(self, testdir): testdir.makepyfile( diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index fa22966d8..c3429753e 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -159,7 +159,7 @@ class TestMetafunc(object): ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ) - @pytest.mark.issue510 + @pytest.mark.issue(510) def test_parametrize_empty_list(self): def func(y): pass @@ -262,7 +262,7 @@ class TestMetafunc(object): for val, expected in values: assert _idval(val, "a", 6, None, item=None, config=None) == expected - @pytest.mark.issue250 + @pytest.mark.issue(250) def test_idmaker_autoname(self): from _pytest.python import idmaker @@ -356,7 +356,7 @@ class TestMetafunc(object): result = idmaker(("a", "b"), [pytest.param(e.one, e.two)]) assert result == ["Foo.one-Foo.two"] - @pytest.mark.issue351 + @pytest.mark.issue(351) def test_idmaker_idfn(self): from _pytest.python import idmaker @@ -375,7 +375,7 @@ class TestMetafunc(object): ) assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"] - @pytest.mark.issue351 + @pytest.mark.issue(351) def test_idmaker_idfn_unique_names(self): from _pytest.python import idmaker @@ -459,7 +459,7 @@ class TestMetafunc(object): ) assert result == ["a0", "a1", "b0", "c", "b1"] - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect(self): def func(x, y): pass @@ -473,7 +473,7 @@ class TestMetafunc(object): assert metafunc._calls[0].params == dict(x=1, y=2) assert metafunc._calls[1].params == dict(x=1, y=3) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_list(self): def func(x, y): pass @@ -483,7 +483,7 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == dict(y="b") assert metafunc._calls[0].params == dict(x="a") - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_list_all(self): def func(x, y): pass @@ -493,7 +493,7 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == {} assert metafunc._calls[0].params == dict(x="a", y="b") - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_list_empty(self): def func(x, y): pass @@ -503,7 +503,7 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].params == {} - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_list_functional(self, testdir): """ Test parametrization with 'indirect' parameter applied on @@ -532,7 +532,7 @@ class TestMetafunc(object): result = testdir.runpytest("-v") result.stdout.fnmatch_lines(["*test_simple*a-b*", "*1 passed*"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_list_error(self, testdir): def func(x, y): pass @@ -541,7 +541,7 @@ class TestMetafunc(object): with pytest.raises(pytest.fail.Exception): metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_uses_no_fixture_error_indirect_false(self, testdir): """The 'uses no fixture' error tells the user at collection time that the parametrize data they've set up doesn't correspond to the @@ -560,7 +560,7 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no argument 'y'*"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_uses_no_fixture_error_indirect_true(self, testdir): testdir.makepyfile( """ @@ -580,7 +580,7 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_uses_no_fixture_error_indirect_string(self, testdir): testdir.makepyfile( """ @@ -597,7 +597,7 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_indirect_uses_no_fixture_error_indirect_list(self, testdir): testdir.makepyfile( """ @@ -614,7 +614,7 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue714 + @pytest.mark.issue(714) def test_parametrize_argument_not_in_indirect_list(self, testdir): testdir.makepyfile( """ @@ -1201,7 +1201,7 @@ class TestMetafuncFunctional(object): reprec = testdir.runpytest() reprec.assert_outcomes(passed=4) - @pytest.mark.issue463 + @pytest.mark.issue(463) @pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"]) def test_parametrize_misspelling(self, testdir, attr): testdir.makepyfile( @@ -1386,7 +1386,7 @@ class TestMetafuncFunctionalAuto(object): assert output.count("preparing foo-3") == 1 -@pytest.mark.issue308 +@pytest.mark.issue(308) class TestMarkersWithParametrization(object): def test_simple_mark(self, testdir): s = """ @@ -1575,7 +1575,7 @@ class TestMarkersWithParametrization(object): reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) reprec.assertoutcome(passed=2, skipped=2) - @pytest.mark.issue290 + @pytest.mark.issue(290) def test_parametrize_ID_generation_string_int_works(self, testdir): testdir.makepyfile( """ diff --git a/testing/test_capture.py b/testing/test_capture.py index 796d05b47..1b34ab583 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -605,7 +605,7 @@ class TestCaptureFixture(object): result.stdout.fnmatch_lines(["*KeyboardInterrupt*"]) assert result.ret == 2 - @pytest.mark.issue14 + @pytest.mark.issue(14) def test_capture_and_logging(self, testdir): p = testdir.makepyfile( """\ diff --git a/testing/test_conftest.py b/testing/test_conftest.py index ac091fed8..1c4be9816 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -490,7 +490,7 @@ class TestConftestVisibility(object): ("snc", ".", 1), ], ) - @pytest.mark.issue616 + @pytest.mark.issue(616) def test_parsefactories_relative_node_ids( self, testdir, chdir, testarg, expect_ntests_passed ): diff --git a/testing/test_mark.py b/testing/test_mark.py index 2851dbc16..cb20658b5 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -448,7 +448,7 @@ class TestFunctional(object): items, rec = testdir.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b"), test_bar=("a",)) - @pytest.mark.issue568 + @pytest.mark.issue(568) def test_mark_should_not_pass_to_siebling_class(self, testdir): p = testdir.makepyfile( """ @@ -651,7 +651,7 @@ class TestFunctional(object): markers = {m.name for m in items[name].iter_markers()} assert markers == set(expected_markers) - @pytest.mark.issue1540 + @pytest.mark.issue(1540) @pytest.mark.filterwarnings("ignore") def test_mark_from_parameters(self, testdir): testdir.makepyfile( diff --git a/tox.ini b/tox.ini index 4f24150b7..2b87199c8 100644 --- a/tox.ini +++ b/tox.ini @@ -166,6 +166,8 @@ filterwarnings = # Do not cause SyntaxError for invalid escape sequences in py37. default:invalid escape sequence:DeprecationWarning pytester_example_dir = testing/example_scripts +markers = + issue [flake8] max-line-length = 120 From deade370b9d151d0587eb1f1fa8de93a15d2dbfa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 1 Apr 2019 07:13:45 +1100 Subject: [PATCH 006/104] Update doc/en/mark.rst Co-Authored-By: Zac-HD --- doc/en/mark.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 6b9de7e6d..0ce9cb31b 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -43,7 +43,7 @@ Marks can be registered in ``pytest.ini`` like this: When the ``--strict`` command-line flag is passed, any unknown marks applied with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. Marks added by pytest or by a plugin instead of the decorator will not trigger -this error. To enforce a limited set of markers, add ``--strict`` to ``addopts``: +this error. To enforce validation of markers, add ``--strict`` to ``addopts``: .. code-block:: ini From 9121138a1b469b97d544c38967a8859d8112ed14 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 1 Apr 2019 10:40:18 +1100 Subject: [PATCH 007/104] Emit warning for unknown marks --- changelog/4826.feature.rst | 2 ++ doc/en/mark.rst | 5 ++++- src/_pytest/mark/__init__.py | 3 +-- src/_pytest/mark/structures.py | 38 +++++++++++++++++++++------------- 4 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 changelog/4826.feature.rst diff --git a/changelog/4826.feature.rst b/changelog/4826.feature.rst new file mode 100644 index 000000000..2afcba1ad --- /dev/null +++ b/changelog/4826.feature.rst @@ -0,0 +1,2 @@ +A warning is now emitted when unknown marks are used as a decorator. +This is often due to a typo, which can lead to silently broken tests. diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 0ce9cb31b..5801dccde 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -31,7 +31,10 @@ which also serve as documentation. Raising errors on unknown marks ------------------------------- -Marks can be registered in ``pytest.ini`` like this: +Unknown marks applied with the ``@pytest.mark.name_of_the_mark`` decorator +will always emit a warning, in order to avoid silently doing something +surprising due to mis-typed names. You can disable the warning for custom +marks by registering them in ``pytest.ini`` like this: .. code-block:: ini diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index e7f08e163..ef81784f4 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -147,8 +147,7 @@ def pytest_collection_modifyitems(items, config): def pytest_configure(config): config._old_mark_config = MARK_GEN._config - if config.option.strict: - MARK_GEN._config = config + MARK_GEN._config = config empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5186e7545..dfb358052 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -11,6 +11,7 @@ from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET from _pytest.outcomes import fail +from _pytest.warning_types import PytestWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -283,28 +284,37 @@ class MarkGenerator(object): on the ``test_function`` object. """ _config = None + _markers = set() def __getattr__(self, name): if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") + if self._config is not None: - self._check(name) + self._update_markers(name) + if name not in self._markers: + warnings.warn( + "Unknown mark %r. You can register custom marks to avoid this " + "warning, without risking typos that break your tests. See " + "https://docs.pytest.org/en/latest/mark.html for details." % name, + PytestWarning, + ) + if self._config.option.strict: + fail("{!r} not a registered marker".format(name), pytrace=False) + return MarkDecorator(Mark(name, (), {})) - def _check(self, name): - try: - if name in self._markers: - return - except AttributeError: - pass - self._markers = values = set() - for line in self._config.getini("markers"): - marker = line.split(":", 1)[0] - marker = marker.rstrip() - x = marker.split("(", 1)[0] - values.add(x) + def _update_markers(self, name): + # We store a set of registered markers as a performance optimisation, + # but more could be added to `self._config` by other plugins at runtime. + # If we see an unknown marker, we therefore update the set and try again! if name not in self._markers: - fail("{!r} not a registered marker".format(name), pytrace=False) + 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 = line.split(":")[0].split("(")[0].strip() + self._markers.add(marker) MARK_GEN = MarkGenerator() From cda9ce198ad10d7d82addf55c23e0c8f2e7fbb6d Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 1 Apr 2019 10:52:43 +1100 Subject: [PATCH 008/104] Register marks from self-tests --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tox.ini b/tox.ini index 2b87199c8..131bacae6 100644 --- a/tox.ini +++ b/tox.ini @@ -168,6 +168,12 @@ filterwarnings = pytester_example_dir = testing/example_scripts markers = issue + nothing + foo + bar + baz + xyz + XYZ [flake8] max-line-length = 120 From 4f6c67658c95ae45f4b5bdc5967aa9faab38640d Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 1 Apr 2019 12:38:33 +1100 Subject: [PATCH 009/104] Use mark-specific warning type So that we can ignore it in self-tests. --- src/_pytest/mark/structures.py | 10 +++++----- src/_pytest/warning_types.py | 9 +++++++++ tox.ini | 8 ++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index dfb358052..d96c0b126 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -11,7 +11,7 @@ from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET from _pytest.outcomes import fail -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import UnknownMarkWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -294,10 +294,10 @@ class MarkGenerator(object): self._update_markers(name) if name not in self._markers: warnings.warn( - "Unknown mark %r. You can register custom marks to avoid this " - "warning, without risking typos that break your tests. See " - "https://docs.pytest.org/en/latest/mark.html for details." % name, - PytestWarning, + "Unknown pytest.mark.%s - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/latest/mark.html" % name, + UnknownMarkWarning, ) if self._config.option.strict: fail("{!r} not a registered marker".format(name), pytrace=False) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 55e1f037a..ffc6e69d6 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -9,6 +9,15 @@ class PytestWarning(UserWarning): """ +class UnknownMarkWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted on use of unknown markers. + See https://docs.pytest.org/en/latest/mark.html for details. + """ + + class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """ Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`. diff --git a/tox.ini b/tox.ini index 131bacae6..d0dd95ea3 100644 --- a/tox.ini +++ b/tox.ini @@ -165,15 +165,11 @@ filterwarnings = ignore::pytest.PytestExperimentalApiWarning # Do not cause SyntaxError for invalid escape sequences in py37. default:invalid escape sequence:DeprecationWarning + # ignore use of unregistered marks, because we use many to test the implementation + ignore::_pytest.warning_types.UnknownMarkWarning pytester_example_dir = testing/example_scripts markers = issue - nothing - foo - bar - baz - xyz - XYZ [flake8] max-line-length = 120 From cab4069f42e9bd2debfc8f71b8805a334194a9cf Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 2 Apr 2019 12:31:42 +1100 Subject: [PATCH 010/104] Clarify mark.__getattr__ --- src/_pytest/mark/structures.py | 39 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index d96c0b126..f3602b2d5 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -291,31 +291,32 @@ class MarkGenerator(object): raise AttributeError("Marker name must NOT start with underscore") if self._config is not None: - self._update_markers(name) + # We store a set of markers as a performance optimisation - if a mark + # name is in the set we definitely know it, but a mark may be known and + # not in the set. We therefore start by updating the set! + if name not in self._markers: + 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 = line.split(":")[0].split("(")[0].strip() + self._markers.add(marker) + + # If the name is not in the set of known marks after updating, + # then it really is time to issue a warning or an error. if name not in self._markers: - warnings.warn( - "Unknown pytest.mark.%s - is this a typo? You can register " - "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/latest/mark.html" % name, - UnknownMarkWarning, - ) if self._config.option.strict: fail("{!r} not a registered marker".format(name), pytrace=False) + else: + warnings.warn( + "Unknown pytest.mark.%s - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/latest/mark.html" % name, + UnknownMarkWarning, + ) return MarkDecorator(Mark(name, (), {})) - def _update_markers(self, name): - # We store a set of registered markers as a performance optimisation, - # but more could be added to `self._config` by other plugins at runtime. - # If we see an unknown marker, we therefore update the set and try again! - if name not in self._markers: - 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 = line.split(":")[0].split("(")[0].strip() - self._markers.add(marker) - MARK_GEN = MarkGenerator() From 08ded2927a76a06f51813e8794b48386dd971c94 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Mar 2019 01:27:25 +0100 Subject: [PATCH 011/104] capture: do not set logging.raiseExceptions = False Ref: https://github.com/pytest-dev/pytest/issues/4942 --- changelog/4942.trivial.rst | 1 + src/_pytest/capture.py | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) create mode 100644 changelog/4942.trivial.rst diff --git a/changelog/4942.trivial.rst b/changelog/4942.trivial.rst new file mode 100644 index 000000000..87dba6b8c --- /dev/null +++ b/changelog/4942.trivial.rst @@ -0,0 +1 @@ +``logging.raiseExceptions`` is not set to ``False`` anymore. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 0e8a693e8..95d6e363e 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -56,13 +56,6 @@ def pytest_load_initial_conftests(early_config, parser, args): # make sure that capturemanager is properly reset at final shutdown early_config.add_cleanup(capman.stop_global_capturing) - # make sure logging does not raise exceptions at the end - def silence_logging_at_shutdown(): - if "logging" in sys.modules: - sys.modules["logging"].raiseExceptions = False - - early_config.add_cleanup(silence_logging_at_shutdown) - # finally trigger conftest loading but while capturing (issue93) capman.start_global_capturing() outcome = yield From 8c734dfc2f98e617ca7796ab0af74abdb747a34c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 27 Mar 2019 19:10:33 +0100 Subject: [PATCH 012/104] Split out list of essential plugins Fixes https://github.com/pytest-dev/pytest/issues/4976. --- src/_pytest/config/__init__.py | 14 ++++++++++---- src/_pytest/terminal.py | 1 + testing/test_config.py | 24 +++++++++--------------- testing/test_pluginmanager.py | 4 ++++ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4ed9deac4..4542f06ab 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -112,13 +112,18 @@ def directory_arg(path, optname): return path -default_plugins = ( +# Plugins that cannot be disabled via "-p no:X" currently. +essential_plugins = ( "mark", "main", - "terminal", "runner", "python", "fixtures", + "helpconfig", # Provides -p. +) + +default_plugins = essential_plugins + ( + "terminal", # Has essential options, but xdist uses -pno:terminal. "debugging", "unittest", "capture", @@ -127,7 +132,6 @@ default_plugins = ( "monkeypatch", "recwarn", "pastebin", - "helpconfig", "nose", "assertion", "junitxml", @@ -143,7 +147,6 @@ default_plugins = ( "reports", ) - builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") @@ -496,6 +499,9 @@ class PytestPluginManager(PluginManager): def consider_pluginarg(self, arg): if arg.startswith("no:"): name = arg[3:] + if name in essential_plugins: + raise UsageError("plugin %s cannot be disabled" % name) + # PR #4304 : remove stepwise if cacheprovider is blocked if name == "cacheprovider": self.set_blocked("stepwise") diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 06604fdf1..31c0da46d 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -173,6 +173,7 @@ def getreportopt(config): return reportopts +@pytest.hookimpl(trylast=True) # after _pytest.runner def pytest_report_teststatus(report): if report.passed: letter = "." diff --git a/testing/test_config.py b/testing/test_config.py index c90333a00..8cd606f45 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1205,20 +1205,12 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): [ x for x in _pytest.config.default_plugins - if x - not in [ - "fixtures", - "helpconfig", # Provides -p. - "main", - "mark", - "python", - "runner", - "terminal", # works in OK case (no output), but not with failures. - ] + if x not in _pytest.config.essential_plugins ], ) def test_config_blocked_default_plugins(testdir, plugin): if plugin == "debugging": + # Fixed in xdist master (after 1.27.0). # https://github.com/pytest-dev/pytest-xdist/pull/422 try: import xdist # noqa: F401 @@ -1230,9 +1222,11 @@ def test_config_blocked_default_plugins(testdir, plugin): p = testdir.makepyfile("def test(): pass") result = testdir.runpytest(str(p), "-pno:%s" % plugin) assert result.ret == EXIT_OK - result.stdout.fnmatch_lines(["* 1 passed in *"]) + if plugin != "terminal": + result.stdout.fnmatch_lines(["* 1 passed in *"]) - p = testdir.makepyfile("def test(): assert 0") - result = testdir.runpytest(str(p), "-pno:%s" % plugin) - assert result.ret == EXIT_TESTSFAILED - result.stdout.fnmatch_lines(["* 1 failed in *"]) + if plugin != "terminal": # fails to report due to its options being used elsewhere. + p = testdir.makepyfile("def test(): assert 0") + result = testdir.runpytest(str(p), "-pno:%s" % plugin) + assert result.ret == EXIT_TESTSFAILED + result.stdout.fnmatch_lines(["* 1 failed in *"]) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 614572ae4..12990c867 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -9,6 +9,7 @@ import types import pytest from _pytest.config import PytestPluginManager +from _pytest.config.exceptions import UsageError from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import Session @@ -314,6 +315,9 @@ class TestPytestPluginManagerBootstrapming(object): # Handles -p without following arg (when used without argparse). pytestpm.consider_preparse(["-p"]) + with pytest.raises(UsageError, match="^plugin main cannot be disabled$"): + pytestpm.consider_preparse(["-p", "no:main"]) + def test_plugin_prevent_register(self, pytestpm): pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) l1 = pytestpm.get_plugins() From cc90bcce4c3d6bc55aeb85a6450020cb4a2e49f2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 3 Apr 2019 04:07:42 +0200 Subject: [PATCH 013/104] wrap_session: restore old behavior for initstate=1 --- src/_pytest/main.py | 7 +++++-- testing/test_pdb.py | 3 ++- testing/test_runner.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 19553ca0e..df4a7a956 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -214,10 +214,13 @@ def wrap_session(config, doit): except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() exitstatus = EXIT_INTERRUPTED - if initstate <= 2 and isinstance(excinfo.value, exit.Exception): - sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg)) + if isinstance(excinfo.value, exit.Exception): if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode + if initstate < 2: + sys.stderr.write( + "{}: {}\n".format(excinfo.typename, excinfo.value.msg) + ) config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except: # noqa diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 531846e8e..f524f06a2 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1015,7 +1015,8 @@ class TestTraceOption: rest = child.read().decode("utf8") assert "2 passed in" in rest assert "reading from stdin while output" not in rest - assert "Exit: Quitting debugger" in child.before.decode("utf8") + # Only printed once - not on stderr. + assert "Exit: Quitting debugger" not in child.before.decode("utf8") TestPDB.flush(child) diff --git a/testing/test_runner.py b/testing/test_runner.py index 72484fb72..cf335dfad 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -580,8 +580,24 @@ def test_pytest_exit_returncode(testdir): """ ) result = testdir.runpytest() + result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"]) + assert result.stderr.lines == [""] assert result.ret == 99 + # It prints to stderr also in case of exit during pytest_sessionstart. + testdir.makeconftest( + """ + import pytest + + def pytest_sessionstart(): + pytest.exit("during_sessionstart", 98) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"]) + assert result.stderr.lines == ["Exit: during_sessionstart", ""] + assert result.ret == 98 + def test_pytest_fail_notrace_runtest(testdir): """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run.""" From 9434541090d41caf6ac6ca2030ca8653a65ef74b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 2 Apr 2019 17:36:52 +0200 Subject: [PATCH 014/104] doc: mention that pytest.fixture's param is in request.param --- src/_pytest/fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0bb525a45..d6bceba02 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1021,6 +1021,7 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None): :arg params: an optional list of parameters which will cause multiple invocations of the fixture function and all of the tests using it. + The current parameter is available in ``request.param``. :arg autouse: if True, the fixture func is activated for all tests that can see it. If False (the default) then an explicit From db34bf01b635decb537bc3868b9221734aac430d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 3 Apr 2019 00:22:09 +0200 Subject: [PATCH 015/104] doc: minor whitespace, punctuation --- doc/en/mark.rst | 6 ++---- doc/en/writing_plugins.rst | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 5801dccde..ad9aaca4b 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -1,9 +1,7 @@ - .. _mark: Marking test functions with attributes -================================================================= - +====================================== By using the ``pytest.mark`` helper you can easily set metadata on your test functions. There are @@ -164,4 +162,4 @@ More details can be found in the `original PR Date: Wed, 3 Apr 2019 00:23:10 +0200 Subject: [PATCH 016/104] minor: check_interactive_exception: use Skipped --- src/_pytest/nose.py | 3 ++- src/_pytest/runner.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index 13dda68e7..492388260 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -7,6 +7,7 @@ import sys import six +import pytest from _pytest import python from _pytest import runner from _pytest import unittest @@ -26,7 +27,7 @@ def pytest_runtest_makereport(item, call): if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): # let's substitute the excinfo with a pytest.skip one call2 = runner.CallInfo.from_call( - lambda: runner.skip(six.text_type(call.excinfo.value)), call.when + lambda: pytest.skip(six.text_type(call.excinfo.value)), call.when ) call.excinfo = call2.excinfo diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 1f09f42e8..7fb343d4e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -16,7 +16,6 @@ from .reports import CollectReport from .reports import TestReport from _pytest._code.code import ExceptionInfo from _pytest.outcomes import Exit -from _pytest.outcomes import skip from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME @@ -183,7 +182,7 @@ def call_and_report(item, when, log=True, **kwds): def check_interactive_exception(call, report): return call.excinfo and not ( hasattr(report, "wasxfail") - or call.excinfo.errisinstance(skip.Exception) + or call.excinfo.errisinstance(Skipped) or call.excinfo.errisinstance(bdb.BdbQuit) ) From 1f5a61e4efe09f727c00ce503387921147be81b8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 3 Apr 2019 15:18:29 +0200 Subject: [PATCH 017/104] run-last-failure: improve reporting --- changelog/5034.feature.rst | 1 + src/_pytest/cacheprovider.py | 51 ++++++++++++++++++++--------------- testing/test_cacheprovider.py | 26 +++++++++++++++--- 3 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 changelog/5034.feature.rst diff --git a/changelog/5034.feature.rst b/changelog/5034.feature.rst new file mode 100644 index 000000000..6ae2def3f --- /dev/null +++ b/changelog/5034.feature.rst @@ -0,0 +1 @@ +Improve reporting with ``--lf`` and ``--ff`` (run-last-failure). diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index ef2039539..246b8dfd8 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -157,18 +157,11 @@ class LFPlugin(object): self.active = any(config.getoption(key) for key in active_keys) self.lastfailed = config.cache.get("cache/lastfailed", {}) self._previously_failed_count = None - self._no_failures_behavior = self.config.getoption("last_failed_no_failures") + self._report_status = None def pytest_report_collectionfinish(self): if self.active and self.config.getoption("verbose") >= 0: - if not self._previously_failed_count: - return None - noun = "failure" if self._previously_failed_count == 1 else "failures" - suffix = " first" if self.config.getoption("failedfirst") else "" - mode = "rerun previous {count} {noun}{suffix}".format( - count=self._previously_failed_count, suffix=suffix, noun=noun - ) - return "run-last-failure: %s" % mode + return "run-last-failure: %s" % self._report_status def pytest_runtest_logreport(self, report): if (report.when == "call" and report.passed) or report.skipped: @@ -196,18 +189,35 @@ class LFPlugin(object): else: previously_passed.append(item) self._previously_failed_count = len(previously_failed) + if not previously_failed: - # running a subset of all tests with recorded failures outside - # of the set of tests currently executing - return - if self.config.getoption("lf"): - items[:] = previously_failed - config.hook.pytest_deselected(items=previously_passed) + # 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: - items[:] = previously_failed + previously_passed - elif self._no_failures_behavior == "none": - config.hook.pytest_deselected(items=items) - 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 @@ -303,8 +313,7 @@ def pytest_addoption(parser): dest="last_failed_no_failures", choices=("all", "none"), default="all", - help="change the behavior when no test failed in the last run or no " - "information about the last failures was found in the cache", + help="which tests to run with no previously (known) failures.", ) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index a1b4a86b5..85603edf4 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -10,6 +10,7 @@ import textwrap import py import pytest +from _pytest.main import EXIT_NOTESTSCOLLECTED pytest_plugins = ("pytester",) @@ -251,7 +252,13 @@ class TestLastFailed(object): result = testdir.runpytest("--lf") result.stdout.fnmatch_lines(["*2 passed*1 desel*"]) result = testdir.runpytest("--lf") - result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) + result.stdout.fnmatch_lines( + [ + "collected 3 items", + "run-last-failure: no previously failed tests, not deselecting items.", + "*1 failed*2 passed*", + ] + ) result = testdir.runpytest("--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) @@ -425,7 +432,13 @@ class TestLastFailed(object): ) result = testdir.runpytest(test_a, "--lf") - result.stdout.fnmatch_lines(["collected 2 items", "*2 passed in*"]) + result.stdout.fnmatch_lines( + [ + "collected 2 items", + "run-last-failure: 2 known failures not in selected tests", + "*2 passed in*", + ] + ) result = testdir.runpytest(test_b, "--lf") result.stdout.fnmatch_lines( @@ -721,7 +734,14 @@ class TestLastFailed(object): result = testdir.runpytest("--lf", "--lfnf", "all") result.stdout.fnmatch_lines(["*2 passed*"]) result = testdir.runpytest("--lf", "--lfnf", "none") - result.stdout.fnmatch_lines(["*2 desel*"]) + result.stdout.fnmatch_lines( + [ + "collected 2 items / 2 deselected", + "run-last-failure: no previously failed tests, deselecting all items.", + "* 2 deselected in *", + ] + ) + assert result.ret == EXIT_NOTESTSCOLLECTED def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): testdir.makepyfile( From 5fec793bc7d0e70b8e7a2a5ff384d254204fff05 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 2 Apr 2019 17:21:14 +0200 Subject: [PATCH 018/104] _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 019/104] _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 020/104] 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 021/104] 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 3d0ecd03ed554442b43b1eb4c04d8bab2a99f4f4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 29 Mar 2019 17:59:02 +0100 Subject: [PATCH 022/104] Display message from reprcrash in short test summary This is useful to see common patterns easily, but also for single failures already. --- src/_pytest/skipping.py | 20 +++++++++++++++++++- testing/acceptance_test.py | 4 +++- testing/test_skipping.py | 2 +- testing/test_terminal.py | 12 +++++++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 22acafbdd..9abed5b1f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -211,7 +211,25 @@ def show_simple(terminalreporter, lines, stat): for rep in failed: verbose_word = _get_report_str(config, rep) pos = _get_pos(config, rep) - lines.append("%s %s" % (verbose_word, pos)) + + line = "%s %s" % (verbose_word, pos) + try: + msg = rep.longrepr.reprcrash.message + except AttributeError: + pass + else: + # Only use the first line. + # Might be worth having a short_message property, which + # could default to this behavior. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + max_len = terminalreporter.writer.fullwidth - len(line) - 2 + if len(msg) > max_len: + msg = msg[: (max_len - 1)] + "…" + line += ": %s" % msg + + lines.append(line) def show_xfailed(terminalreporter, lines): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 408fa076e..295dd832c 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -865,7 +865,9 @@ class TestInvocationVariants(object): _fail, _sep, testid = line.partition(" ") break result = testdir.runpytest(testid, "-rf") - result.stdout.fnmatch_lines([line, "*1 failed*"]) + result.stdout.fnmatch_lines( + ["FAILED test_doctest_id.txt::test_doctest_id.txt", "*1 failed*"] + ) def test_core_backward_compatibility(self): """Test backward compatibility for get_plugin_manager function. See #787.""" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e5206a44e..fb0cf60e0 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1208,6 +1208,6 @@ def test_summary_list_after_errors(testdir): [ "=* FAILURES *=", "*= short test summary info =*", - "FAILED test_summary_list_after_errors.py::test_fail", + "FAILED test_summary_list_after_errors.py::test_fail: assert 0", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index d0fdce23e..9b221366f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -726,12 +726,18 @@ class TestTerminalFunctional(object): result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"]) -def test_fail_extra_reporting(testdir): - testdir.makepyfile("def test_this(): assert 0") +def test_fail_extra_reporting(testdir, monkeypatch): + monkeypatch.setenv("COLUMNS", "80") + testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100") result = testdir.runpytest() assert "short test summary" not in result.stdout.str() result = testdir.runpytest("-rf") - result.stdout.fnmatch_lines(["*test summary*", "FAIL*test_fail_extra_reporting*"]) + result.stdout.fnmatch_lines( + [ + "*test summary*", + "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedthis…", + ] + ) def test_fail_reporting_on_pass(testdir): From 37ecca3ba9b1715735ec745b975f75327c3261b7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 4 Apr 2019 22:13:28 +0200 Subject: [PATCH 023/104] factor out _get_line_with_reprcrash_message --- changelog/5013.feature.rst | 1 + src/_pytest/skipping.py | 58 +++++++++++++++++++++++++------------- testing/test_skipping.py | 44 +++++++++++++++++++++++++++++ testing/test_terminal.py | 2 +- 4 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 changelog/5013.feature.rst diff --git a/changelog/5013.feature.rst b/changelog/5013.feature.rst new file mode 100644 index 000000000..08f82efeb --- /dev/null +++ b/changelog/5013.feature.rst @@ -0,0 +1 @@ +Messages from crash reports are displayed within test summaries now, truncated to the terminal width. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 9abed5b1f..e72eecccf 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -204,31 +204,49 @@ def pytest_terminal_summary(terminalreporter): tr._tw.line(line) +def _get_line_with_reprcrash_message(config, rep, termwidth): + """Get summary line for a report, trying to add reprcrash message.""" + verbose_word = _get_report_str(config, rep) + pos = _get_pos(config, rep) + + line = "%s %s" % (verbose_word, pos) + + len_line = len(line) + ellipsis = "..." + len_ellipsis = len(ellipsis) + + if len_line > termwidth - len_ellipsis: + # No space for an additional message. + return line + + try: + msg = rep.longrepr.reprcrash.message + except AttributeError: + pass + else: + # Only use the first line. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + len_msg = len(msg) + + sep = ": " + len_sep = len(sep) + max_len = termwidth - len_line - len_sep + if max_len >= len_ellipsis: + if len_msg > max_len: + msg = msg[: (max_len - len_ellipsis)] + ellipsis + line += sep + msg + return line + + def show_simple(terminalreporter, lines, stat): failed = terminalreporter.stats.get(stat) if failed: config = terminalreporter.config + termwidth = terminalreporter.writer.fullwidth for rep in failed: - verbose_word = _get_report_str(config, rep) - pos = _get_pos(config, rep) - - line = "%s %s" % (verbose_word, pos) - try: - msg = rep.longrepr.reprcrash.message - except AttributeError: - pass - else: - # Only use the first line. - # Might be worth having a short_message property, which - # could default to this behavior. - i = msg.find("\n") - if i != -1: - msg = msg[:i] - max_len = terminalreporter.writer.fullwidth - len(line) - 2 - if len(msg) > max_len: - msg = msg[: (max_len - 1)] + "…" - line += ": %s" % msg - + line = _get_line_with_reprcrash_message(config, rep, termwidth) lines.append(line) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index fb0cf60e0..93dd2a97d 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1211,3 +1211,47 @@ def test_summary_list_after_errors(testdir): "FAILED test_summary_list_after_errors.py::test_fail: assert 0", ] ) + + +def test_line_with_reprcrash(monkeypatch): + import _pytest.skipping + from _pytest.skipping import _get_line_with_reprcrash_message + + def mock_get_report_str(*args): + return "FAILED" + + def mock_get_pos(*args): + return "some::nodeid" + + monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) + monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) + + class config: + pass + + class rep: + pass + + f = _get_line_with_reprcrash_message + assert f(config, rep, 80) == "FAILED some::nodeid" + + class rep: + class longrepr: + class reprcrash: + message = "msg" + + assert f(config, rep, 80) == "FAILED some::nodeid: msg" + assert f(config, rep, 3) == "FAILED some::nodeid" + + assert f(config, rep, 23) == "FAILED some::nodeid" + assert f(config, rep, 24) == "FAILED some::nodeid: msg" + + rep.longrepr.reprcrash.message = "some longer message" + assert f(config, rep, 23) == "FAILED some::nodeid" + assert f(config, rep, 24) == "FAILED some::nodeid: ..." + assert f(config, rep, 25) == "FAILED some::nodeid: s..." + + rep.longrepr.reprcrash.message = "some\nmessage" + assert f(config, rep, 24) == "FAILED some::nodeid: ..." + assert f(config, rep, 25) == "FAILED some::nodeid: some" + assert f(config, rep, 80) == "FAILED some::nodeid: some" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 9b221366f..c465fc903 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -735,7 +735,7 @@ def test_fail_extra_reporting(testdir, monkeypatch): result.stdout.fnmatch_lines( [ "*test summary*", - "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedthis…", + "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedth...", ] ) From 0f965e57a23313b0020e1c9737380dbe18b6d088 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 12:12:29 +0200 Subject: [PATCH 024/104] 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 159704421ebef2b2b0e40d1a8074b09f2d0f8a0f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 12:17:08 +0200 Subject: [PATCH 025/104] change separator to hyphen --- src/_pytest/skipping.py | 16 ++++++---------- testing/test_skipping.py | 20 ++++++++++---------- testing/test_terminal.py | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index e72eecccf..f7f085303 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -210,11 +210,8 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): pos = _get_pos(config, rep) line = "%s %s" % (verbose_word, pos) - len_line = len(line) - ellipsis = "..." - len_ellipsis = len(ellipsis) - + ellipsis, len_ellipsis = "...", 3 if len_line > termwidth - len_ellipsis: # No space for an additional message. return line @@ -230,12 +227,11 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): msg = msg[:i] len_msg = len(msg) - sep = ": " - len_sep = len(sep) - max_len = termwidth - len_line - len_sep - if max_len >= len_ellipsis: - if len_msg > max_len: - msg = msg[: (max_len - len_ellipsis)] + ellipsis + sep, len_sep = " - ", 3 + max_len_msg = termwidth - len_line - len_sep + if max_len_msg >= len_ellipsis: + if len_msg > max_len_msg: + msg = msg[: (max_len_msg - len_ellipsis)] + ellipsis line += sep + msg return line diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 93dd2a97d..4121f0c28 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1208,7 +1208,7 @@ def test_summary_list_after_errors(testdir): [ "=* FAILURES *=", "*= short test summary info =*", - "FAILED test_summary_list_after_errors.py::test_fail: assert 0", + "FAILED test_summary_list_after_errors.py::test_fail - assert 0", ] ) @@ -1240,18 +1240,18 @@ def test_line_with_reprcrash(monkeypatch): class reprcrash: message = "msg" - assert f(config, rep, 80) == "FAILED some::nodeid: msg" + assert f(config, rep, 80) == "FAILED some::nodeid - msg" assert f(config, rep, 3) == "FAILED some::nodeid" - assert f(config, rep, 23) == "FAILED some::nodeid" - assert f(config, rep, 24) == "FAILED some::nodeid: msg" + assert f(config, rep, 24) == "FAILED some::nodeid" + assert f(config, rep, 25) == "FAILED some::nodeid - msg" rep.longrepr.reprcrash.message = "some longer message" - assert f(config, rep, 23) == "FAILED some::nodeid" - assert f(config, rep, 24) == "FAILED some::nodeid: ..." - assert f(config, rep, 25) == "FAILED some::nodeid: s..." + assert f(config, rep, 24) == "FAILED some::nodeid" + assert f(config, rep, 25) == "FAILED some::nodeid - ..." + assert f(config, rep, 26) == "FAILED some::nodeid - s..." rep.longrepr.reprcrash.message = "some\nmessage" - assert f(config, rep, 24) == "FAILED some::nodeid: ..." - assert f(config, rep, 25) == "FAILED some::nodeid: some" - assert f(config, rep, 80) == "FAILED some::nodeid: some" + assert f(config, rep, 25) == "FAILED some::nodeid - ..." + assert f(config, rep, 26) == "FAILED some::nodeid - some" + assert f(config, rep, 80) == "FAILED some::nodeid - some" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c465fc903..9664d9c7f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -735,7 +735,7 @@ def test_fail_extra_reporting(testdir, monkeypatch): result.stdout.fnmatch_lines( [ "*test summary*", - "FAILED test_fail_extra_reporting.py::test_this: AssertionError: this_failedth...", + "FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...", ] ) From e20b39d92814a19db57bdace8d7c84bf4f39b557 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 15:13:35 +0200 Subject: [PATCH 026/104] 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 f599172addeba19d992086187c309df416887f00 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 16:08:11 +0200 Subject: [PATCH 027/104] =?UTF-8?q?test=20with=20=F0=9F=98=84=20in=20messa?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- testing/test_skipping.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 4121f0c28..e167d4477 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1255,3 +1255,11 @@ def test_line_with_reprcrash(monkeypatch): assert f(config, rep, 25) == "FAILED some::nodeid - ..." assert f(config, rep, 26) == "FAILED some::nodeid - some" assert f(config, rep, 80) == "FAILED some::nodeid - some" + + # Test unicode safety. + rep.longrepr.reprcrash.message = "😄😄😄😄😄\n2nd line" + assert f(config, rep, 26) == "FAILED some::nodeid - 😄..." + # XXX: this is actually wrong - since the character uses two terminal + # cells. + rep.longrepr.reprcrash.message = "😄😄😄😄\n2nd line" + assert f(config, rep, 26) == "FAILED some::nodeid - 😄😄😄😄" From df377b589f5345b6cc5774f803a656fd0bdc9bf8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 5 Apr 2019 17:43:11 +0200 Subject: [PATCH 028/104] use wcwidth --- setup.py | 1 + src/_pytest/skipping.py | 12 +++++-- testing/test_skipping.py | 75 ++++++++++++++++++++++++---------------- 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/setup.py b/setup.py index a924d4aba..795a8c75f 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ INSTALL_REQUIRES = [ 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.9", + "wcwidth", ] diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index f7f085303..1013f4889 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -206,11 +206,13 @@ def pytest_terminal_summary(terminalreporter): def _get_line_with_reprcrash_message(config, rep, termwidth): """Get summary line for a report, trying to add reprcrash message.""" + from wcwidth import wcswidth + verbose_word = _get_report_str(config, rep) pos = _get_pos(config, rep) line = "%s %s" % (verbose_word, pos) - len_line = len(line) + len_line = wcswidth(line) ellipsis, len_ellipsis = "...", 3 if len_line > termwidth - len_ellipsis: # No space for an additional message. @@ -225,13 +227,17 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): i = msg.find("\n") if i != -1: msg = msg[:i] - len_msg = len(msg) + len_msg = wcswidth(msg) sep, len_sep = " - ", 3 max_len_msg = termwidth - len_line - len_sep if max_len_msg >= len_ellipsis: if len_msg > max_len_msg: - msg = msg[: (max_len_msg - len_ellipsis)] + ellipsis + max_len_msg -= len_ellipsis + msg = msg[:max_len_msg] + while wcswidth(msg) > max_len_msg: + msg = msg[:-1] + msg += ellipsis line += sep + msg return line diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e167d4477..952877d8e 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1216,50 +1216,67 @@ def test_summary_list_after_errors(testdir): def test_line_with_reprcrash(monkeypatch): import _pytest.skipping from _pytest.skipping import _get_line_with_reprcrash_message + from wcwidth import wcswidth + + mocked_verbose_word = "FAILED" def mock_get_report_str(*args): - return "FAILED" - - def mock_get_pos(*args): - return "some::nodeid" + return mocked_verbose_word monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) + + mocked_pos = "some::nodeid" + + def mock_get_pos(*args): + return mocked_pos + monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) class config: pass - class rep: - pass - - f = _get_line_with_reprcrash_message - assert f(config, rep, 80) == "FAILED some::nodeid" - class rep: class longrepr: class reprcrash: - message = "msg" + pass - assert f(config, rep, 80) == "FAILED some::nodeid - msg" - assert f(config, rep, 3) == "FAILED some::nodeid" + def check(msg, width, expected): + if msg: + rep.longrepr.reprcrash.message = msg + actual = _get_line_with_reprcrash_message(config, rep, width) - assert f(config, rep, 24) == "FAILED some::nodeid" - assert f(config, rep, 25) == "FAILED some::nodeid - msg" + assert actual == expected + if actual != "%s %s" % (mocked_verbose_word, mocked_pos): + assert len(actual) <= width + assert wcswidth(actual) <= width - rep.longrepr.reprcrash.message = "some longer message" - assert f(config, rep, 24) == "FAILED some::nodeid" - assert f(config, rep, 25) == "FAILED some::nodeid - ..." - assert f(config, rep, 26) == "FAILED some::nodeid - s..." + # AttributeError with message + check(None, 80, "FAILED some::nodeid") - rep.longrepr.reprcrash.message = "some\nmessage" - assert f(config, rep, 25) == "FAILED some::nodeid - ..." - assert f(config, rep, 26) == "FAILED some::nodeid - some" - assert f(config, rep, 80) == "FAILED some::nodeid - some" + check("msg", 80, "FAILED some::nodeid - msg") + check("msg", 3, "FAILED some::nodeid") + + check("msg", 24, "FAILED some::nodeid") + check("msg", 25, "FAILED some::nodeid - msg") + + check("some longer msg", 24, "FAILED some::nodeid") + check("some longer msg", 25, "FAILED some::nodeid - ...") + check("some longer msg", 26, "FAILED some::nodeid - s...") + + check("some\nmessage", 25, "FAILED some::nodeid - ...") + check("some\nmessage", 26, "FAILED some::nodeid - some") + check("some\nmessage", 80, "FAILED some::nodeid - some") # Test unicode safety. - rep.longrepr.reprcrash.message = "😄😄😄😄😄\n2nd line" - assert f(config, rep, 26) == "FAILED some::nodeid - 😄..." - # XXX: this is actually wrong - since the character uses two terminal - # cells. - rep.longrepr.reprcrash.message = "😄😄😄😄\n2nd line" - assert f(config, rep, 26) == "FAILED some::nodeid - 😄😄😄😄" + check("😄😄😄😄😄\n2nd line", 25, "FAILED some::nodeid - ...") + check("😄😄😄😄😄\n2nd line", 26, "FAILED some::nodeid - ...") + check("😄😄😄😄😄\n2nd line", 27, "FAILED some::nodeid - 😄...") + check("😄😄😄😄😄\n2nd line", 28, "FAILED some::nodeid - 😄...") + check("😄😄😄😄😄\n2nd line", 29, "FAILED some::nodeid - 😄😄...") + + mocked_pos = "nodeid::😄::withunicode" + check("😄😄😄😄😄\n2nd line", 29, "FAILED nodeid::😄::withunicode") + check("😄😄😄😄😄\n2nd line", 40, "FAILED nodeid::😄::withunicode - 😄😄...") + check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...") + check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...") + check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄") From 9ad00714ba2520b694cb826a2757ad860e29d383 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 09:55:56 +0200 Subject: [PATCH 029/104] 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 030/104] 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 2ebb69b50a761a041a8d9b96207c0e74c49db798 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 15:00:12 +0200 Subject: [PATCH 031/104] py2 fixes --- testing/test_skipping.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 952877d8e..421531cc4 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,3 +1,4 @@ +# coding=utf8 from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -1268,15 +1269,17 @@ def test_line_with_reprcrash(monkeypatch): check("some\nmessage", 80, "FAILED some::nodeid - some") # Test unicode safety. - check("😄😄😄😄😄\n2nd line", 25, "FAILED some::nodeid - ...") - check("😄😄😄😄😄\n2nd line", 26, "FAILED some::nodeid - ...") - check("😄😄😄😄😄\n2nd line", 27, "FAILED some::nodeid - 😄...") - check("😄😄😄😄😄\n2nd line", 28, "FAILED some::nodeid - 😄...") - check("😄😄😄😄😄\n2nd line", 29, "FAILED some::nodeid - 😄😄...") + check(u"😄😄😄😄😄\n2nd line", 25, u"FAILED some::nodeid - ...") + check(u"😄😄😄😄😄\n2nd line", 26, u"FAILED some::nodeid - ...") + check(u"😄😄😄😄😄\n2nd line", 27, u"FAILED some::nodeid - 😄...") + check(u"😄😄😄😄😄\n2nd line", 28, u"FAILED some::nodeid - 😄...") + check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED some::nodeid - 😄😄...") - mocked_pos = "nodeid::😄::withunicode" - check("😄😄😄😄😄\n2nd line", 29, "FAILED nodeid::😄::withunicode") - check("😄😄😄😄😄\n2nd line", 40, "FAILED nodeid::😄::withunicode - 😄😄...") - check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...") - check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...") - check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄") + # NOTE: constructed, not sure if this is supported. + # It would fail if not using u"" in Python 2 for mocked_pos. + mocked_pos = u"nodeid::😄::withunicode" + check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED nodeid::😄::withunicode") + check(u"😄😄😄😄😄\n2nd line", 40, u"FAILED nodeid::😄::withunicode - 😄😄...") + check(u"😄😄😄😄😄\n2nd line", 41, u"FAILED nodeid::😄::withunicode - 😄😄...") + check(u"😄😄😄😄😄\n2nd line", 42, u"FAILED nodeid::😄::withunicode - 😄😄😄...") + check(u"😄😄😄😄😄\n2nd line", 80, u"FAILED nodeid::😄::withunicode - 😄😄😄😄😄") From 2b1ae8a66d07c2a0dcedf79125fe4dadf2df7abc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 15:00:23 +0200 Subject: [PATCH 032/104] __tracebackhide__ for check --- testing/test_skipping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 421531cc4..f0da0488c 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1242,6 +1242,7 @@ def test_line_with_reprcrash(monkeypatch): pass def check(msg, width, expected): + __tracebackhide__ = True if msg: rep.longrepr.reprcrash.message = msg actual = _get_line_with_reprcrash_message(config, rep, width) From a7e49e6c07ff163cdd1e646853d2b52e5d535437 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 7 Apr 2019 17:11:26 +0200 Subject: [PATCH 033/104] 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 034/104] 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 035/104] 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 036/104] 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 037/104] 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 038/104] 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 039/104] 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 040/104] 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 041/104] 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 042/104] 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 043/104] 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 044/104] 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 045/104] 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 046/104] 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 047/104] 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 048/104] 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 049/104] 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 050/104] 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 051/104] 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 42e60d935adb5292e9ae87387a5a61745bd34ab1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 11 Apr 2019 11:44:04 +0200 Subject: [PATCH 052/104] 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 053/104] 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 14d3d9187fde788b715fa358a9d4519a9a4edfbf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 Apr 2019 19:01:21 -0300 Subject: [PATCH 054/104] Remove partial unicode characters from summary messages in Python 2 --- src/_pytest/skipping.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 1013f4889..28a553e18 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,8 +1,11 @@ +# coding=utf8 """ support for skip/xfail functions and markers. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import six + from _pytest.config import hookimpl from _pytest.mark.evaluate import MarkEvaluator from _pytest.outcomes import fail @@ -237,6 +240,14 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): msg = msg[:max_len_msg] while wcswidth(msg) > max_len_msg: msg = msg[:-1] + if six.PY2: + # on python 2 systems with narrow unicode compilation, trying to + # get a single character out of a multi-byte unicode character such as + # u'😄' will result in a High Surrogate (U+D83D) character, which is + # rendered as u'�'; in this case we just strip that character out as it + # serves no purpose being rendered + while msg.endswith(u"\uD83D"): + msg = msg[:-1] msg += ellipsis line += sep + msg return line From 6371243c107d6aaf989b3c85d091e4d1041ec576 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 15:21:01 +0200 Subject: [PATCH 055/104] summary_passes: use bold green for report headers --- 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 2d7132259..7183ac157 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -806,7 +806,7 @@ class TerminalReporter(object): for rep in reports: if rep.sections: msg = self._getfailureheadline(rep) - self.write_sep("_", msg) + self.write_sep("_", msg, green=True, bold=True) self._outrep_summary(rep) def print_teardown_sections(self, rep): From 6a73714b00279647297f599ff57855ec2b12efd0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 19:00:46 +0200 Subject: [PATCH 056/104] deselect_by_keyword: skip without expression There is no need to iterate over all items always, if `-k` is not specified. --- src/_pytest/mark/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index ef81784f4..e98dc5c37 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -100,6 +100,9 @@ pytest_cmdline_main.tryfirst = True def deselect_by_keyword(items, config): keywordexpr = config.option.keyword.lstrip() + if not keywordexpr: + return + if keywordexpr.startswith("-"): keywordexpr = "not " + keywordexpr[1:] selectuntil = False From 1dd5f088faed8248f06b3ef59b8e38b0c249e4c3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 21:55:15 +0200 Subject: [PATCH 057/104] 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 058/104] 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 059/104] 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 060/104] 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 061/104] 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 062/104] 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 063/104] 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 064/104] 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 065/104] 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 066/104] 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 067/104] 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 068/104] 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 069/104] 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 070/104] 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 ea79eb5c3ff96e5dad7719650784580cd31fa0ad Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 15:13:15 +0200 Subject: [PATCH 071/104] 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 072/104] _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 073/104] 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 074/104] 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) From adb8edbae12bd5c8e2c3b8ef196a7d838d1b9e3e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 17 Apr 2019 14:40:55 +0200 Subject: [PATCH 075/104] assertion rewriting: use actual helper name This makes it easier / possible to grep. --- src/_pytest/assertion/rewrite.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 18506d2e1..7cb1acb2e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -744,12 +744,12 @@ class AssertionRewriter(ast.NodeVisitor): def display(self, expr): """Call saferepr on the expression.""" - return self.helper("saferepr", expr) + return self.helper("_saferepr", expr) def helper(self, name, *args): """Call a helper in this module.""" py_name = ast.Name("@pytest_ar", ast.Load()) - attr = ast.Attribute(py_name, "_" + name, ast.Load()) + attr = ast.Attribute(py_name, name, ast.Load()) return ast_Call(attr, list(args), []) def builtin(self, name): @@ -849,14 +849,14 @@ class AssertionRewriter(ast.NodeVisitor): negation = ast.UnaryOp(ast.Not(), top_condition) self.statements.append(ast.If(negation, body, [])) if assert_.msg: - assertmsg = self.helper("format_assertmsg", assert_.msg) + assertmsg = self.helper("_format_assertmsg", assert_.msg) explanation = "\n>assert " + explanation else: assertmsg = ast.Str("") explanation = "assert " + explanation template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) msg = self.pop_format_context(template) - fmt = self.helper("format_explanation", msg) + fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) exc = ast_Call(err_name, [fmt], []) if sys.version_info[0] >= 3: @@ -906,7 +906,7 @@ warn_explicit( # _should_repr_global_name() thinks it's acceptable. locs = ast_Call(self.builtin("locals"), [], []) inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) - dorepr = self.helper("should_repr_global_name", name) + dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) return name, self.explanation_param(expr) @@ -942,7 +942,7 @@ warn_explicit( self.statements = body = inner self.statements = save self.on_failure = fail_save - expl_template = self.helper("format_boolop", expl_list, ast.Num(is_or)) + expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) @@ -1067,7 +1067,7 @@ warn_explicit( left_res, left_expl = next_res, next_expl # Use pytest.assertion.util._reprcompare if that's available. expl_call = self.helper( - "call_reprcompare", + "_call_reprcompare", ast.Tuple(syms, ast.Load()), ast.Tuple(load_names, ast.Load()), ast.Tuple(expls, ast.Load()), From c3178a176dda482f912da4d5c99c9396e7a8e9db Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 17 Apr 2019 15:30:34 +0200 Subject: [PATCH 076/104] move test --- testing/test_skipping.py | 72 ---------------------------------------- testing/test_terminal.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 4782e7065..fb0822f8f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1177,75 +1177,3 @@ def test_summary_list_after_errors(testdir): "FAILED test_summary_list_after_errors.py::test_fail - assert 0", ] ) - - -def test_line_with_reprcrash(monkeypatch): - import _pytest.skipping - from _pytest.skipping import _get_line_with_reprcrash_message - from wcwidth import wcswidth - - mocked_verbose_word = "FAILED" - - def mock_get_report_str(*args): - return mocked_verbose_word - - monkeypatch.setattr(_pytest.skipping, "_get_report_str", mock_get_report_str) - - mocked_pos = "some::nodeid" - - def mock_get_pos(*args): - return mocked_pos - - monkeypatch.setattr(_pytest.skipping, "_get_pos", mock_get_pos) - - class config: - pass - - class rep: - class longrepr: - class reprcrash: - pass - - def check(msg, width, expected): - __tracebackhide__ = True - if msg: - rep.longrepr.reprcrash.message = msg - actual = _get_line_with_reprcrash_message(config, rep, width) - - assert actual == expected - if actual != "%s %s" % (mocked_verbose_word, mocked_pos): - assert len(actual) <= width - assert wcswidth(actual) <= width - - # AttributeError with message - check(None, 80, "FAILED some::nodeid") - - check("msg", 80, "FAILED some::nodeid - msg") - check("msg", 3, "FAILED some::nodeid") - - check("msg", 24, "FAILED some::nodeid") - check("msg", 25, "FAILED some::nodeid - msg") - - check("some longer msg", 24, "FAILED some::nodeid") - check("some longer msg", 25, "FAILED some::nodeid - ...") - check("some longer msg", 26, "FAILED some::nodeid - s...") - - check("some\nmessage", 25, "FAILED some::nodeid - ...") - check("some\nmessage", 26, "FAILED some::nodeid - some") - check("some\nmessage", 80, "FAILED some::nodeid - some") - - # Test unicode safety. - check(u"😄😄😄😄😄\n2nd line", 25, u"FAILED some::nodeid - ...") - check(u"😄😄😄😄😄\n2nd line", 26, u"FAILED some::nodeid - ...") - check(u"😄😄😄😄😄\n2nd line", 27, u"FAILED some::nodeid - 😄...") - check(u"😄😄😄😄😄\n2nd line", 28, u"FAILED some::nodeid - 😄...") - check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED some::nodeid - 😄😄...") - - # NOTE: constructed, not sure if this is supported. - # It would fail if not using u"" in Python 2 for mocked_pos. - mocked_pos = u"nodeid::😄::withunicode" - check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED nodeid::😄::withunicode") - check(u"😄😄😄😄😄\n2nd line", 40, u"FAILED nodeid::😄::withunicode - 😄😄...") - check(u"😄😄😄😄😄\n2nd line", 41, u"FAILED nodeid::😄::withunicode - 😄😄...") - check(u"😄😄😄😄😄\n2nd line", 42, u"FAILED nodeid::😄::withunicode - 😄😄😄...") - check(u"😄😄😄😄😄\n2nd line", 80, u"FAILED nodeid::😄::withunicode - 😄😄😄😄😄") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index ee546a4a1..cf0faf5c3 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -17,6 +17,7 @@ import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.reports import BaseReport from _pytest.terminal import _folded_skips +from _pytest.terminal import _get_line_with_reprcrash_message from _pytest.terminal import _plugin_nameversions from _pytest.terminal import build_summary_stats_line from _pytest.terminal import getreportopt @@ -1582,3 +1583,72 @@ def test_skip_reasons_folding(): assert fspath == path assert lineno == lineno assert reason == message + + +def test_line_with_reprcrash(monkeypatch): + import _pytest.terminal + from wcwidth import wcswidth + + mocked_verbose_word = "FAILED" + + mocked_pos = "some::nodeid" + + def mock_get_pos(*args): + return mocked_pos + + monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos) + + class config: + pass + + class rep: + def _get_verbose_word(self, *args): + return mocked_verbose_word + + class longrepr: + class reprcrash: + pass + + def check(msg, width, expected): + __tracebackhide__ = True + if msg: + rep.longrepr.reprcrash.message = msg + actual = _get_line_with_reprcrash_message(config, rep, width) + + assert actual == expected + if actual != "%s %s" % (mocked_verbose_word, mocked_pos): + assert len(actual) <= width + assert wcswidth(actual) <= width + + # AttributeError with message + check(None, 80, "FAILED some::nodeid") + + check("msg", 80, "FAILED some::nodeid - msg") + check("msg", 3, "FAILED some::nodeid") + + check("msg", 24, "FAILED some::nodeid") + check("msg", 25, "FAILED some::nodeid - msg") + + check("some longer msg", 24, "FAILED some::nodeid") + check("some longer msg", 25, "FAILED some::nodeid - ...") + check("some longer msg", 26, "FAILED some::nodeid - s...") + + check("some\nmessage", 25, "FAILED some::nodeid - ...") + check("some\nmessage", 26, "FAILED some::nodeid - some") + check("some\nmessage", 80, "FAILED some::nodeid - some") + + # Test unicode safety. + check(u"😄😄😄😄😄\n2nd line", 25, u"FAILED some::nodeid - ...") + check(u"😄😄😄😄😄\n2nd line", 26, u"FAILED some::nodeid - ...") + check(u"😄😄😄😄😄\n2nd line", 27, u"FAILED some::nodeid - 😄...") + check(u"😄😄😄😄😄\n2nd line", 28, u"FAILED some::nodeid - 😄...") + check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED some::nodeid - 😄😄...") + + # NOTE: constructed, not sure if this is supported. + # It would fail if not using u"" in Python 2 for mocked_pos. + mocked_pos = u"nodeid::😄::withunicode" + check(u"😄😄😄😄😄\n2nd line", 29, u"FAILED nodeid::😄::withunicode") + check(u"😄😄😄😄😄\n2nd line", 40, u"FAILED nodeid::😄::withunicode - 😄😄...") + check(u"😄😄😄😄😄\n2nd line", 41, u"FAILED nodeid::😄::withunicode - 😄😄...") + check(u"😄😄😄😄😄\n2nd line", 42, u"FAILED nodeid::😄::withunicode - 😄😄😄...") + check(u"😄😄😄😄😄\n2nd line", 80, u"FAILED nodeid::😄::withunicode - 😄😄😄😄😄") From 649d23c8a8cf86895830d256be4f6ee1f0f8d1e4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 18 Apr 2019 23:18:59 +0200 Subject: [PATCH 077/104] pytest_sessionfinish: preset exitstatus with UsageErrors Previously it would be 0. Setting it to the expected outcome (EXIT_USAGEERROR) here already helps `pytest_sessionfinish` hooks. --- src/_pytest/main.py | 1 + testing/acceptance_test.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index df4a7a956..64e7110b4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -208,6 +208,7 @@ def wrap_session(config, doit): initstate = 2 session.exitstatus = doit(config, session) or 0 except UsageError: + session.exitstatus = EXIT_USAGEERROR raise except Failed: session.exitstatus = EXIT_TESTSFAILED diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 13a765411..4fb7bc02b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -428,9 +428,20 @@ class TestGeneralUsage(object): assert result.ret == 4 # usage error only if item not found def test_report_all_failed_collections_initargs(self, testdir): + testdir.makeconftest( + """ + from _pytest.main import EXIT_USAGEERROR + + def pytest_sessionfinish(exitstatus): + assert exitstatus == EXIT_USAGEERROR + print("pytest_sessionfinish_called") + """ + ) testdir.makepyfile(test_a="def", test_b="def") result = testdir.runpytest("test_a.py::a", "test_b.py::b") result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"]) + result.stdout.fnmatch_lines(["pytest_sessionfinish_called"]) + assert result.ret == EXIT_USAGEERROR @pytest.mark.usefixtures("recwarn") def test_namespace_import_doesnt_confuse_import_hook(self, testdir): From 4749dca764bf8c82f20e7bb86f44265cfa488dc9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 18 Apr 2019 23:54:16 +0200 Subject: [PATCH 078/104] changelog [ci skip] --- changelog/5144.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5144.bugfix.rst diff --git a/changelog/5144.bugfix.rst b/changelog/5144.bugfix.rst new file mode 100644 index 000000000..c8c270288 --- /dev/null +++ b/changelog/5144.bugfix.rst @@ -0,0 +1 @@ +With usage errors ``exitstatus`` is set to ``EXIT_USAGEERROR`` in the ``pytest_sessionfinish`` hook now as expected. From 698c4e75fd6e5346cb8a90b61fa228ce79406e32 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 23:07:29 +0200 Subject: [PATCH 079/104] capture: track current state in _state attributes This is meant for debugging, and making assertions later. --- src/_pytest/capture.py | 37 +++++++++++++++++++++++++++++++++---- testing/test_capture.py | 9 ++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 95d6e363e..25eab7fdf 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -456,6 +456,7 @@ CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) class MultiCapture(object): out = err = in_ = None + _state = None def __init__(self, out=True, err=True, in_=True, Capture=None): if in_: @@ -466,9 +467,16 @@ class MultiCapture(object): self.err = Capture(2) def __repr__(self): - return "" % (self.out, self.err, self.in_) + return "" % ( + self.out, + self.err, + self.in_, + self._state, + getattr(self, "_in_suspended", ""), + ) def start_capturing(self): + self._state = "started" if self.in_: self.in_.start() if self.out: @@ -486,6 +494,7 @@ class MultiCapture(object): return out, err def suspend_capturing(self, in_=False): + self._state = "suspended" if self.out: self.out.suspend() if self.err: @@ -495,6 +504,7 @@ class MultiCapture(object): self._in_suspended = True def resume_capturing(self): + self._state = "resumed" if self.out: self.out.resume() if self.err: @@ -505,9 +515,9 @@ class MultiCapture(object): def stop_capturing(self): """ stop capturing and reset capturing streams """ - if hasattr(self, "_reset"): + if self._state == "stopped": raise ValueError("was already stopped") - self._reset = True + self._state = "stopped" if self.out: self.out.done() if self.err: @@ -535,6 +545,7 @@ class FDCaptureBinary(object): """ EMPTY_BUFFER = b"" + _state = None def __init__(self, targetfd, tmpfile=None): self.targetfd = targetfd @@ -561,9 +572,10 @@ class FDCaptureBinary(object): self.tmpfile_fd = tmpfile.fileno() def __repr__(self): - return "" % ( + return "" % ( self.targetfd, getattr(self, "targetfd_save", None), + self._state, ) def start(self): @@ -574,6 +586,7 @@ class FDCaptureBinary(object): raise ValueError("saved filedescriptor not valid anymore") os.dup2(self.tmpfile_fd, self.targetfd) self.syscapture.start() + self._state = "started" def snap(self): self.tmpfile.seek(0) @@ -590,14 +603,17 @@ class FDCaptureBinary(object): os.close(targetfd_save) self.syscapture.done() _attempt_to_close_capture_file(self.tmpfile) + self._state = "done" def suspend(self): self.syscapture.suspend() os.dup2(self.targetfd_save, self.targetfd) + self._state = "suspended" def resume(self): self.syscapture.resume() os.dup2(self.tmpfile_fd, self.targetfd) + self._state = "resumed" def writeorg(self, data): """ write to original file descriptor. """ @@ -625,6 +641,7 @@ class FDCapture(FDCaptureBinary): class SysCapture(object): EMPTY_BUFFER = str() + _state = None def __init__(self, fd, tmpfile=None): name = patchsysdict[fd] @@ -637,8 +654,17 @@ class SysCapture(object): tmpfile = CaptureIO() self.tmpfile = tmpfile + def __repr__(self): + return "" % ( + self.name, + self._old, + self.tmpfile, + self._state, + ) + def start(self): setattr(sys, self.name, self.tmpfile) + self._state = "started" def snap(self): res = self.tmpfile.getvalue() @@ -650,12 +676,15 @@ class SysCapture(object): setattr(sys, self.name, self._old) del self._old _attempt_to_close_capture_file(self.tmpfile) + self._state = "done" def suspend(self): setattr(sys, self.name, self._old) + self._state = "suspended" def resume(self): setattr(sys, self.name, self.tmpfile) + self._state = "resumed" def writeorg(self, data): self._old.write(data) diff --git a/testing/test_capture.py b/testing/test_capture.py index c3881128f..2b450c189 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1243,25 +1243,24 @@ class TestStdCaptureFDinvalidFD(object): from _pytest import capture def StdCaptureFD(out=True, err=True, in_=True): - return capture.MultiCapture(out, err, in_, - Capture=capture.FDCapture) + return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture) def test_stdout(): os.close(1) cap = StdCaptureFD(out=True, err=False, in_=False) - assert repr(cap.out) == "" + assert repr(cap.out) == "" cap.stop_capturing() def test_stderr(): os.close(2) cap = StdCaptureFD(out=False, err=True, in_=False) - assert repr(cap.err) == "" + assert repr(cap.err) == "" cap.stop_capturing() def test_stdin(): os.close(0) cap = StdCaptureFD(out=False, err=False, in_=True) - assert repr(cap.in_) == "" + assert repr(cap.in_) == "" cap.stop_capturing() """ ) From f75f7c192586fd856dad10673cd6d1f8cce03f75 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 19 Apr 2019 01:23:08 +0200 Subject: [PATCH 080/104] conftest: use a hookwrapper with sorting faster tests first --- testing/conftest.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/testing/conftest.py b/testing/conftest.py index fb677cd05..4582c7d90 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,5 +1,13 @@ +import pytest + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems(config, items): - """Prefer faster tests.""" + """Prefer faster tests. + + Use a hookwrapper to do this in the beginning, so e.g. --ff still works + correctly. + """ fast_items = [] slow_items = [] neutral_items = [] @@ -24,3 +32,5 @@ def pytest_collection_modifyitems(config, items): fast_items.append(item) items[:] = fast_items + neutral_items + slow_items + + yield From 0bf363472ed23c42674bc41528fd482a7b73f8d8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 22 Apr 2019 02:04:07 +0200 Subject: [PATCH 081/104] Use config_invocation_dir for startdirs `Session.startdir` and `TerminalReporter.startdir` appear to be redundant given `Config.invocation_dir`. Keep them for backward compatibility reasons, but use `config.invocation_dir` for them. --- src/_pytest/main.py | 2 +- src/_pytest/terminal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index df4a7a956..291fb73a4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -434,7 +434,7 @@ class Session(nodes.FSCollector): self.shouldfail = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") - self.startdir = py.path.local() + self.startdir = config.invocation_dir self._initialpaths = frozenset() # Keep track of any collected nodes in here, so we don't duplicate fixtures self._node_cache = {} diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8d2d65da5..93267a6b8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -234,7 +234,7 @@ class TerminalReporter(object): self._showfspath = None self.stats = {} - self.startdir = py.path.local() + self.startdir = config.invocation_dir if file is None: file = sys.stdout self._tw = _pytest.config.create_terminal_writer(config, file) From 308b733b9d387b487104ed850063824e9a41bcfa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 27 Apr 2019 02:25:38 +0200 Subject: [PATCH 082/104] Revert "Merge pull request #4854 from blueyed/pdb-skip" This reverts commit e88aa957aed7ee9dc3649e88052f548f5ffd0911, reversing changes made to 1410d3dc9a61001f71b74ea800b203e92a25d689. I do not think it warrants an option anymore, and there is a way to achieve this via `--pdbcls` if needed. --- changelog/4854.feature.rst | 5 ----- src/_pytest/debugging.py | 10 ---------- testing/test_pdb.py | 14 -------------- 3 files changed, 29 deletions(-) delete mode 100644 changelog/4854.feature.rst diff --git a/changelog/4854.feature.rst b/changelog/4854.feature.rst deleted file mode 100644 index c48f08da2..000000000 --- a/changelog/4854.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -The ``--pdb-skip`` option can now be used to ignore calls to -``pdb.set_trace()`` (and ``pytest.set_trace()``). - -This is meant to help while debugging, where you want to use e.g. ``--pdb`` or -``--trace`` only, or just run the tests again without any interruption. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index a5b9e9c86..d8a3adc08 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -46,13 +46,6 @@ def pytest_addoption(parser): action="store_true", help="Immediately break when running each test.", ) - group._addoption( - "--pdb-skip", - "--pdb-ignore-set_trace", - dest="pdb_ignore_set_trace", - action="store_true", - help="Ignore calls to pdb.set_trace().", - ) def _import_pdbcls(modname, classname): @@ -222,9 +215,6 @@ class pytestPDB(object): @classmethod def set_trace(cls, *args, **kwargs): """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" - if pytestPDB._config: # Might not be available when called directly. - if pytestPDB._config.getoption("pdb_ignore_set_trace"): - return frame = sys._getframe().f_back _pdb = cls._init_pdb(*args, **kwargs) _pdb.set_trace(frame) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index cc2ea15d2..0a72a2907 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -9,7 +9,6 @@ import sys import _pytest._code import pytest from _pytest.debugging import _validate_usepdb_cls -from _pytest.main import EXIT_NOTESTSCOLLECTED try: breakpoint @@ -1123,19 +1122,6 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture): assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest -def test_pdb_skip_option(testdir): - p = testdir.makepyfile( - """ - print("before_set_trace") - __import__('pdb').set_trace() - print("after_set_trace") - """ - ) - result = testdir.runpytest_inprocess("--pdb-ignore-set_trace", "-s", p) - assert result.ret == EXIT_NOTESTSCOLLECTED - result.stdout.fnmatch_lines(["*before_set_trace*", "*after_set_trace*"]) - - def test_pdbcls_via_local_module(testdir): """It should be imported in pytest_configure or later only.""" p1 = testdir.makepyfile( From 65133018f3e86f60b5d8bfe13493920a415810dd Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 27 Apr 2019 03:39:00 +0200 Subject: [PATCH 083/104] Terminal plugin is not semi-essential anymore Thanks to https://github.com/pytest-dev/pytest/pull/5138. --- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2617a4589..1a2edf4f8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -123,7 +123,7 @@ essential_plugins = ( ) default_plugins = essential_plugins + ( - "terminal", # Has essential options, but xdist uses -pno:terminal. + "terminal", "debugging", "unittest", "capture", diff --git a/testing/test_config.py b/testing/test_config.py index f5ebdad5a..ecb8fd403 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1232,8 +1232,10 @@ def test_config_blocked_default_plugins(testdir, plugin): if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 passed in *"]) - if plugin != "terminal": # fails to report due to its options being used elsewhere. - p = testdir.makepyfile("def test(): assert 0") - result = testdir.runpytest(str(p), "-pno:%s" % plugin) - assert result.ret == EXIT_TESTSFAILED + p = testdir.makepyfile("def test(): assert 0") + result = testdir.runpytest(str(p), "-pno:%s" % plugin) + assert result.ret == EXIT_TESTSFAILED + if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) + else: + assert result.stdout.lines == [""] From 8532e991a53732a241f0d2b4b1f254a6db597b69 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:06:01 -0300 Subject: [PATCH 084/104] Publish UnknownMarkWarning as part of the public API and docs --- doc/en/warnings.rst | 2 ++ src/_pytest/mark/structures.py | 4 ++-- src/_pytest/warning_types.py | 2 +- src/pytest.py | 2 ++ tox.ini | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 26bd2fdb2..54740b970 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -420,3 +420,5 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.RemovedInPytest4Warning .. autoclass:: pytest.PytestExperimentalApiWarning + +.. autoclass:: pytest.PytestUnknownMarkWarning diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 4cae97b71..b4ff6e988 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -12,7 +12,7 @@ from ..compat import MappingMixin from ..compat import NOTSET from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail -from _pytest.warning_types import UnknownMarkWarning +from _pytest.warning_types import PytestUnknownMarkWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -318,7 +318,7 @@ class MarkGenerator(object): "Unknown pytest.mark.%s - is this a typo? You can register " "custom marks to avoid this warning - for details, see " "https://docs.pytest.org/en/latest/mark.html" % name, - UnknownMarkWarning, + PytestUnknownMarkWarning, ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index ffc6e69d6..70f41a2b2 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -9,7 +9,7 @@ class PytestWarning(UserWarning): """ -class UnknownMarkWarning(PytestWarning): +class PytestUnknownMarkWarning(PytestWarning): """ Bases: :class:`PytestWarning`. diff --git a/src/pytest.py b/src/pytest.py index c0010f166..2be72ce77 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -37,6 +37,7 @@ from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning from _pytest.warning_types import RemovedInPytest4Warning @@ -72,6 +73,7 @@ __all__ = [ "raises", "register_assert_rewrite", "RemovedInPytest4Warning", + "PytestUnknownMarkWarning", "Session", "set_trace", "skip", diff --git a/tox.ini b/tox.ini index e297b7099..3c1ca65b7 100644 --- a/tox.ini +++ b/tox.ini @@ -169,7 +169,7 @@ filterwarnings = # Do not cause SyntaxError for invalid escape sequences in py37. default:invalid escape sequence:DeprecationWarning # ignore use of unregistered marks, because we use many to test the implementation - ignore::_pytest.warning_types.UnknownMarkWarning + ignore::_pytest.warning_types.PytestUnknownMarkWarning pytester_example_dir = testing/example_scripts markers = issue From 53cd7fd2ea2cba07820009a4cfba1e09fa35fcfa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:38:25 -0300 Subject: [PATCH 085/104] Introduce new warning subclasses Fix #5177 --- doc/en/warnings.rst | 14 ++++++- src/_pytest/assertion/rewrite.py | 16 +++++--- src/_pytest/cacheprovider.py | 4 +- src/_pytest/config/__init__.py | 8 ++-- src/_pytest/junitxml.py | 6 ++- src/_pytest/python.py | 13 ++++--- src/_pytest/warning_types.py | 64 ++++++++++++++++++++++++++------ src/pytest.py | 12 +++++- testing/test_warnings.py | 2 +- 9 files changed, 104 insertions(+), 35 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 54740b970..83d2d6b15 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -415,10 +415,20 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.PytestWarning -.. autoclass:: pytest.PytestDeprecationWarning +.. autoclass:: pytest.PytestAssertRewriteWarning -.. autoclass:: pytest.RemovedInPytest4Warning +.. autoclass:: pytest.PytestCacheWarning + +.. autoclass:: pytest.PytestCollectionWarning + +.. autoclass:: pytest.PytestConfigWarning + +.. autoclass:: pytest.PytestDeprecationWarning .. autoclass:: pytest.PytestExperimentalApiWarning +.. autoclass:: pytest.PytestUnhandledCoroutineWarning + .. autoclass:: pytest.PytestUnknownMarkWarning + +.. autoclass:: pytest.RemovedInPytest4Warning diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 7cb1acb2e..24d96b722 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -268,11 +268,13 @@ class AssertionRewritingHook(object): self._marked_for_rewrite_cache.clear() def _warn_already_imported(self, name): - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning("Module already imported so cannot be rewritten: %s" % name), + PytestAssertRewriteWarning( + "Module already imported so cannot be rewritten: %s" % name + ), self.config.hook, stacklevel=5, ) @@ -819,11 +821,13 @@ class AssertionRewriter(ast.NodeVisitor): """ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestAssertRewriteWarning import warnings warnings.warn_explicit( - PytestWarning("assertion is always true, perhaps remove parentheses?"), + PytestAssertRewriteWarning( + "assertion is always true, perhaps remove parentheses?" + ), category=None, filename=str(self.module_path), lineno=assert_.lineno, @@ -887,10 +891,10 @@ class AssertionRewriter(ast.NodeVisitor): val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) send_warning = ast.parse( """ -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestAssertRewriteWarning from warnings import warn_explicit warn_explicit( - PytestWarning('asserting the value None, please use "assert is None"'), + PytestAssertRewriteWarning('asserting the value None, please use "assert is None"'), category=None, filename={filename!r}, lineno={lineno}, diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 63503ed2e..1b7001c0a 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -60,10 +60,10 @@ class Cache(object): def warn(self, fmt, **args): from _pytest.warnings import _issue_warning_captured - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestCacheWarning _issue_warning_captured( - PytestWarning(fmt.format(**args) if args else fmt), + PytestCacheWarning(fmt.format(**args) if args else fmt), self._config.hook, stacklevel=3, ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1a2edf4f8..03769b815 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -32,7 +32,7 @@ from _pytest.compat import lru_cache from _pytest.compat import safe_str from _pytest.outcomes import fail from _pytest.outcomes import Skipped -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -307,7 +307,7 @@ class PytestPluginManager(PluginManager): def register(self, plugin, name=None): if name in ["pytest_catchlog", "pytest_capturelog"]: warnings.warn( - PytestWarning( + PytestConfigWarning( "{} plugin has been merged into the core, " "please remove it from your requirements.".format( name.replace("_", "-") @@ -574,7 +574,7 @@ class PytestPluginManager(PluginManager): from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning("skipped plugin %r: %s" % (modname, e.msg)), + PytestConfigWarning("skipped plugin %r: %s" % (modname, e.msg)), self.hook, stacklevel=1, ) @@ -863,7 +863,7 @@ class Config(object): from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestWarning( + PytestConfigWarning( "could not load initial conftests: {}".format(e.path) ), self.hook, diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index c2b277b8a..3a5f31735 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -307,9 +307,11 @@ def record_xml_attribute(request): The fixture is callable with ``(name, value)``, with value being automatically xml-encoded """ - from _pytest.warning_types import PytestWarning + from _pytest.warning_types import PytestExperimentalApiWarning, PytestWarning - request.node.warn(PytestWarning("record_xml_attribute is an experimental feature")) + request.node.warn( + PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") + ) # Declare noop def add_attr_noop(name, value): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 135f5bff9..377357846 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -45,7 +45,8 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.pathlib import parts -from _pytest.warning_types import PytestWarning +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestUnhandledCoroutineWarning def pyobj_property(name): @@ -171,7 +172,7 @@ def pytest_pyfunc_call(pyfuncitem): msg += " - pytest-asyncio\n" msg += " - pytest-trio\n" msg += " - pytest-tornasync" - warnings.warn(PytestWarning(msg.format(pyfuncitem.nodeid))) + warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) skip(msg="coroutine function and no async plugin installed (see warnings)") funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} @@ -221,7 +222,7 @@ def pytest_pycollect_makeitem(collector, name, obj): if not (isfunction(obj) or isfunction(get_real_func(obj))): filename, lineno = getfslineno(obj) warnings.warn_explicit( - message=PytestWarning( + message=PytestCollectionWarning( "cannot collect %r because it is not a function." % name ), category=None, @@ -233,7 +234,7 @@ def pytest_pycollect_makeitem(collector, name, obj): res = Function(name, parent=collector) reason = deprecated.YIELD_TESTS.format(name=name) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) - res.warn(PytestWarning(reason)) + res.warn(PytestCollectionWarning(reason)) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res) @@ -721,7 +722,7 @@ class Class(PyCollector): return [] if hasinit(self.obj): self.warn( - PytestWarning( + PytestCollectionWarning( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__ ) @@ -729,7 +730,7 @@ class Class(PyCollector): return [] elif hasnew(self.obj): self.warn( - PytestWarning( + PytestCollectionWarning( "cannot collect test class %r because it has a " "__new__ constructor" % self.obj.__name__ ) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 70f41a2b2..2777aabea 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -9,12 +9,35 @@ class PytestWarning(UserWarning): """ -class PytestUnknownMarkWarning(PytestWarning): +class PytestAssertRewriteWarning(PytestWarning): """ Bases: :class:`PytestWarning`. - Warning emitted on use of unknown markers. - See https://docs.pytest.org/en/latest/mark.html for details. + Warning emitted by the pytest assert rewrite module. + """ + + +class PytestCacheWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted by the cache plugin in various situations. + """ + + +class PytestConfigWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted for configuration issues. + """ + + +class PytestCollectionWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted when pytest is not able to collect a file or symbol in a module. """ @@ -26,14 +49,6 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """ -class RemovedInPytest4Warning(PytestDeprecationWarning): - """ - Bases: :class:`pytest.PytestDeprecationWarning`. - - Warning class for features scheduled to be removed in pytest 4.0. - """ - - class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """ Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. @@ -51,6 +66,33 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): ) +class PytestUnhandledCoroutineWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted when pytest encounters a test function which is a coroutine, + but it was not handled by any async-aware plugin. Coroutine test functions + are not natively supported. + """ + + +class PytestUnknownMarkWarning(PytestWarning): + """ + Bases: :class:`PytestWarning`. + + Warning emitted on use of unknown markers. + See https://docs.pytest.org/en/latest/mark.html for details. + """ + + +class RemovedInPytest4Warning(PytestDeprecationWarning): + """ + Bases: :class:`pytest.PytestDeprecationWarning`. + + Warning class for features scheduled to be removed in pytest 4.0. + """ + + @attr.s class UnformattedWarning(object): """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. diff --git a/src/pytest.py b/src/pytest.py index 2be72ce77..a6376843d 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -35,8 +35,13 @@ from _pytest.python_api import approx from _pytest.python_api import raises from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns +from _pytest.warning_types import PytestAssertRewriteWarning +from _pytest.warning_types import PytestCacheWarning +from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning from _pytest.warning_types import RemovedInPytest4Warning @@ -67,13 +72,18 @@ __all__ = [ "Module", "Package", "param", + "PytestAssertRewriteWarning", + "PytestCacheWarning", + "PytestCollectionWarning", + "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", + "PytestUnhandledCoroutineWarning", + "PytestUnknownMarkWarning", "PytestWarning", "raises", "register_assert_rewrite", "RemovedInPytest4Warning", - "PytestUnknownMarkWarning", "Session", "set_trace", "skip", diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 929ae8d60..a24289f57 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -630,7 +630,7 @@ def test_removed_in_pytest4_warning_as_error(testdir, change_default): class TestAssertionWarnings: @staticmethod def assert_result_warns(result, msg): - result.stdout.fnmatch_lines(["*PytestWarning: %s*" % msg]) + result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg]) def test_tuple_warning(self, testdir): testdir.makepyfile( From 915ecb0dac19f05f7a392161203539076bce7d5c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 10:41:44 -0300 Subject: [PATCH 086/104] Add CHANGELOG for #5177 --- changelog/5177.feature.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 changelog/5177.feature.rst diff --git a/changelog/5177.feature.rst b/changelog/5177.feature.rst new file mode 100644 index 000000000..a5b4ab111 --- /dev/null +++ b/changelog/5177.feature.rst @@ -0,0 +1,14 @@ +Introduce new specific warning ``PytestWarning`` subclasses to make it easier to filter warnings based on the class, rather than on the message. The new subclasses are: + + +* ``PytestAssertRewriteWarning`` + +* ``PytestCacheWarning`` + +* ``PytestCollectionWarning`` + +* ``PytestConfigWarning`` + +* ``PytestUnhandledCoroutineWarning`` + +* ``PytestUnknownMarkWarning`` From 08734bdd18ec4b11aeea0cf7e46fcbf4e68ee9ad Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 28 Apr 2019 16:23:38 -0300 Subject: [PATCH 087/104] --lf now skips colletion of files without failed tests Fix #5172 --- changelog/5172.feature.rst | 2 ++ src/_pytest/cacheprovider.py | 41 ++++++++++++++++++++++-- testing/test_cacheprovider.py | 60 ++++++++++++++++++++++++++++++++--- 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 changelog/5172.feature.rst diff --git a/changelog/5172.feature.rst b/changelog/5172.feature.rst new file mode 100644 index 000000000..85b55f922 --- /dev/null +++ b/changelog/5172.feature.rst @@ -0,0 +1,2 @@ +The ``--last-failed`` (``--lf``) option got smarter and will now skip entire files if all tests +of that test file have passed in previous runs, greatly speeding up collection. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 63503ed2e..df02f4d54 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -158,6 +158,33 @@ class LFPlugin(object): self.lastfailed = config.cache.get("cache/lastfailed", {}) self._previously_failed_count = None self._report_status = None + self._skipped_files = 0 # count skipped files during collection due to --lf + + def last_failed_paths(self): + """Returns a set with all Paths()s of the previously failed nodeids (cached). + """ + result = getattr(self, "_last_failed_paths", None) + if result is None: + rootpath = Path(self.config.rootdir) + result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} + self._last_failed_paths = result + return result + + def pytest_ignore_collect(self, path): + """ + Ignore this file path if we are in --lf mode and it is not in the list of + previously failed files. + """ + if ( + self.active + and self.config.getoption("lf") + and path.isfile() + and self.lastfailed + ): + skip_it = Path(path) not in self.last_failed_paths() + if skip_it: + self._skipped_files += 1 + return skip_it def pytest_report_collectionfinish(self): if self.active and self.config.getoption("verbose") >= 0: @@ -206,9 +233,19 @@ class LFPlugin(object): items[:] = previously_failed + previously_passed noun = "failure" if self._previously_failed_count == 1 else "failures" + if self._skipped_files > 0: + files_noun = "file" if self._skipped_files == 1 else "files" + skipped_files_msg = " (skipped {files} {files_noun})".format( + files=self._skipped_files, files_noun=files_noun + ) + else: + skipped_files_msg = "" 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 + self._report_status = "rerun previous {count} {noun}{suffix}{skipped_files}".format( + count=self._previously_failed_count, + suffix=suffix, + noun=noun, + skipped_files=skipped_files_msg, ) else: self._report_status = "no previously failed tests, " diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 41e7ffd79..02c758424 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -445,9 +445,9 @@ class TestLastFailed(object): result = testdir.runpytest("--lf") result.stdout.fnmatch_lines( [ - "collected 4 items / 2 deselected / 2 selected", - "run-last-failure: rerun previous 2 failures", - "*2 failed, 2 deselected in*", + "collected 2 items", + "run-last-failure: rerun previous 2 failures (skipped 1 file)", + "*2 failed in*", ] ) @@ -718,7 +718,7 @@ class TestLastFailed(object): assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] result = testdir.runpytest("--last-failed") - result.stdout.fnmatch_lines(["*1 failed, 3 deselected*"]) + result.stdout.fnmatch_lines(["*1 failed, 1 deselected*"]) assert self.get_cached_last_failed(testdir) == ["test_foo.py::test_foo_4"] # 3. fix test_foo_4, run only test_foo.py @@ -779,6 +779,58 @@ class TestLastFailed(object): result = testdir.runpytest("--lf", "--cache-clear", "--lfnf", "none") result.stdout.fnmatch_lines(["*2 desel*"]) + def test_lastfailed_skip_collection(self, testdir): + """ + Test --lf behavior regarding skipping collection of files that are not marked as + failed in the cache (#5172). + """ + testdir.makepyfile( + **{ + "pkg1/test_1.py": """ + import pytest + + @pytest.mark.parametrize('i', range(3)) + def test_1(i): pass + """, + "pkg2/test_2.py": """ + import pytest + + @pytest.mark.parametrize('i', range(5)) + def test_1(i): + assert i not in (1, 3) + """, + } + ) + # first run: collects 8 items (test_1: 3, test_2: 5) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["collected 8 items", "*2 failed*6 passed*"]) + # second run: collects only 5 items from test_2, because all tests from test_1 have passed + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines( + [ + "collected 5 items / 3 deselected / 2 selected", + "run-last-failure: rerun previous 2 failures (skipped 1 file)", + "*2 failed*3 deselected*", + ] + ) + + # add another file and check if message is correct when skipping more than 1 file + testdir.makepyfile( + **{ + "pkg1/test_3.py": """ + def test_3(): pass + """ + } + ) + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines( + [ + "collected 5 items / 3 deselected / 2 selected", + "run-last-failure: rerun previous 2 failures (skipped 2 files)", + "*2 failed*3 deselected*", + ] + ) + class TestNewFirst(object): def test_newfirst_usecase(self, testdir): From ff5317a7f38eecf0d38f3b1130f465d27722db69 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 19:54:18 +0200 Subject: [PATCH 088/104] terminal: use pytest_collection_finish for reporting --- changelog/5113.bugfix.rst | 1 + src/_pytest/terminal.py | 6 ++---- testing/test_terminal.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 changelog/5113.bugfix.rst diff --git a/changelog/5113.bugfix.rst b/changelog/5113.bugfix.rst new file mode 100644 index 000000000..713b48967 --- /dev/null +++ b/changelog/5113.bugfix.rst @@ -0,0 +1 @@ +Deselected items from plugins using ``pytest_collect_modifyitems`` as a hookwrapper are correctly reported now. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..25c42c34c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -553,10 +553,6 @@ class TerminalReporter(object): else: self.write_line(line) - @pytest.hookimpl(trylast=True) - def pytest_collection_modifyitems(self): - self.report_collect(True) - @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): self._session = session @@ -609,6 +605,8 @@ class TerminalReporter(object): return result def pytest_collection_finish(self, session): + self.report_collect(True) + if self.config.getoption("collectonly"): self._printcollecteditems(session.items) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index feacc242d..47becf00a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -506,6 +506,37 @@ class TestTerminalFunctional(object): ) assert result.ret == 0 + def test_deselected_with_hookwrapper(self, testdir): + testpath = testdir.makeconftest( + """ + import pytest + + @pytest.hookimpl(hookwrapper=True) + def pytest_collection_modifyitems(config, items): + yield + deselected = items.pop() + config.hook.pytest_deselected(items=[deselected]) + """ + ) + testpath = testdir.makepyfile( + """ + def test_one(): + pass + def test_two(): + pass + def test_three(): + pass + """ + ) + result = testdir.runpytest(testpath) + result.stdout.fnmatch_lines( + [ + "collected 3 items / 1 deselected / 2 selected", + "*= 2 passed, 1 deselected in*", + ] + ) + assert result.ret == 0 + def test_show_deselected_items_using_markexpr_before_test_execution(self, testdir): testdir.makepyfile( test_show_deselected=""" From 02053bf556c9580f77c3dd8f5530e31efed6f0ee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 29 Apr 2019 05:45:21 +0200 Subject: [PATCH 089/104] debugging: rename internal wrapper for pdb.Pdb This is useful/clearer in case of errors / tracebacks - i.e. you see clearly that it is coming from pytest. --- src/_pytest/debugging.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index d8a3adc08..7138fd2e1 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -143,18 +143,18 @@ class pytestPDB(object): else: tw.sep(">", "PDB set_trace") - class _PdbWrapper(cls._pdb_cls, object): + class PytestPdbWrapper(cls._pdb_cls, object): _pytest_capman = capman _continued = False def do_debug(self, arg): cls._recursive_debug += 1 - ret = super(_PdbWrapper, self).do_debug(arg) + ret = super(PytestPdbWrapper, self).do_debug(arg) cls._recursive_debug -= 1 return ret def do_continue(self, arg): - ret = super(_PdbWrapper, self).do_continue(arg) + ret = super(PytestPdbWrapper, self).do_continue(arg) if cls._recursive_debug == 0: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() @@ -188,7 +188,7 @@ class pytestPDB(object): could be handled, but this would require to wrap the whole pytest run, and adjust the report etc. """ - super(_PdbWrapper, self).set_quit() + super(PytestPdbWrapper, self).set_quit() if cls._recursive_debug == 0: outcomes.exit("Quitting debugger") @@ -198,7 +198,7 @@ class pytestPDB(object): Needed after do_continue resumed, and entering another breakpoint again. """ - ret = super(_PdbWrapper, self).setup(f, tb) + ret = super(PytestPdbWrapper, self).setup(f, tb) if not ret and self._continued: # pdb.setup() returns True if the command wants to exit # from the interaction: do not suspend capturing then. @@ -206,7 +206,7 @@ class pytestPDB(object): self._pytest_capman.suspend_global_capture(in_=True) return ret - _pdb = _PdbWrapper(**kwargs) + _pdb = PytestPdbWrapper(**kwargs) cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) else: _pdb = cls._pdb_cls(**kwargs) From 8f23e19bcb71998342f2d522b531707024dfcbf0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 May 2019 15:33:43 -0300 Subject: [PATCH 090/104] Emit a warning for record_property when used with xunit2 "property" elements cannot be children of "testsuite" according to the schema, so it is incompatible with xunit2 Related to #5202 --- changelog/5202.trivial.rst | 4 +++ src/_pytest/junitxml.py | 31 +++++++++++++++-------- testing/test_junitxml.py | 50 +++++++++++++++++++++++++++----------- 3 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 changelog/5202.trivial.rst diff --git a/changelog/5202.trivial.rst b/changelog/5202.trivial.rst new file mode 100644 index 000000000..2eaaf0ca4 --- /dev/null +++ b/changelog/5202.trivial.rst @@ -0,0 +1,4 @@ +``record_property`` now emits a ``PytestWarning`` when used with ``junit_family=xunit2``: the fixture generates +``property`` tags as children of ``testcase``, which is not permitted according to the most +`recent schema `__. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 3a5f31735..f1b7763e2 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -281,6 +281,21 @@ class _NodeReporter(object): self.to_xml = lambda: py.xml.raw(data) +def _warn_incompatibility_with_xunit2(request, fixture_name): + """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions""" + from _pytest.warning_types import PytestWarning + + xml = getattr(request.config, "_xml", None) + if xml is not None and xml.family not in ("xunit1", "legacy"): + request.node.warn( + PytestWarning( + "{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format( + fixture_name=fixture_name, family=xml.family + ) + ) + ) + + @pytest.fixture def record_property(request): """Add an extra properties the calling test. @@ -294,6 +309,7 @@ def record_property(request): def test_function(record_property): record_property("example_key", 1) """ + _warn_incompatibility_with_xunit2(request, "record_property") def append_property(name, value): request.node.user_properties.append((name, value)) @@ -307,27 +323,22 @@ def record_xml_attribute(request): The fixture is callable with ``(name, value)``, with value being automatically xml-encoded """ - from _pytest.warning_types import PytestExperimentalApiWarning, PytestWarning + from _pytest.warning_types import PytestExperimentalApiWarning request.node.warn( PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") ) + _warn_incompatibility_with_xunit2(request, "record_xml_attribute") + # Declare noop def add_attr_noop(name, value): pass attr_func = add_attr_noop - xml = getattr(request.config, "_xml", None) - if xml is not None and xml.family != "xunit1": - request.node.warn( - PytestWarning( - "record_xml_attribute is incompatible with junit_family: " - "%s (use: legacy|xunit1)" % xml.family - ) - ) - elif xml is not None: + xml = getattr(request.config, "_xml", None) + if xml is not None: node_reporter = xml.node_reporter(request.node.nodeid) attr_func = node_reporter.add_attribute diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 82e984785..a32eab2ec 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -993,6 +993,20 @@ def test_record_property_same_name(testdir): pnodes[1].assert_attr(name="foo", value="baz") +@pytest.mark.parametrize("fixture_name", ["record_property", "record_xml_attribute"]) +def test_record_fixtures_without_junitxml(testdir, fixture_name): + testdir.makepyfile( + """ + def test_record({fixture_name}): + {fixture_name}("foo", "bar") + """.format( + fixture_name=fixture_name + ) + ) + result = testdir.runpytest() + assert result.ret == 0 + + @pytest.mark.filterwarnings("default") def test_record_attribute(testdir): testdir.makeini( @@ -1023,8 +1037,9 @@ def test_record_attribute(testdir): @pytest.mark.filterwarnings("default") -def test_record_attribute_xunit2(testdir): - """Ensure record_xml_attribute drops values when outside of legacy family +@pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"]) +def test_record_fixtures_xunit2(testdir, fixture_name): + """Ensure record_xml_attribute and record_property drop values when outside of legacy family """ testdir.makeini( """ @@ -1037,21 +1052,28 @@ def test_record_attribute_xunit2(testdir): import pytest @pytest.fixture - def other(record_xml_attribute): - record_xml_attribute("bar", 1) - def test_record(record_xml_attribute, other): - record_xml_attribute("foo", "<1"); - """ + def other({fixture_name}): + {fixture_name}("bar", 1) + def test_record({fixture_name}, other): + {fixture_name}("foo", "<1"); + """.format( + fixture_name=fixture_name + ) ) result, dom = runandparse(testdir, "-rw") - result.stdout.fnmatch_lines( - [ - "*test_record_attribute_xunit2.py:6:*record_xml_attribute is an experimental feature", - "*test_record_attribute_xunit2.py:6:*record_xml_attribute is incompatible with " - "junit_family: xunit2 (use: legacy|xunit1)", - ] - ) + expected_lines = [] + if fixture_name == "record_xml_attribute": + expected_lines.append( + "*test_record_fixtures_xunit2.py:6:*record_xml_attribute is an experimental feature" + ) + expected_lines = [ + "*test_record_fixtures_xunit2.py:6:*{fixture_name} is incompatible " + "with junit_family 'xunit2' (use 'legacy' or 'xunit1')".format( + fixture_name=fixture_name + ) + ] + result.stdout.fnmatch_lines(expected_lines) def test_random_report_log_xdist(testdir, monkeypatch): From 0e8a8f94f6967dc549dd3def4b64bd07106e4547 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 5 May 2019 09:14:07 -0300 Subject: [PATCH 091/104] Add encoding header to test_terminal.py --- testing/test_terminal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cf0faf5c3..35981b568 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """ terminal reporting of the full testing process. """ From 32a5e80a6d805e3cefb67b201f2e960b5cdd82f9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 5 May 2019 09:33:37 -0300 Subject: [PATCH 092/104] Add encoding: header and fix rep mock in test_line_with_reprcrash on py27 --- src/_pytest/terminal.py | 1 + testing/test_terminal.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index af836658b..5330d81cb 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1,3 +1,4 @@ +# encoding: utf-8 """ terminal reporting of the full testing process. This is a good source for looking at the various reporting hooks. diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 35981b568..da5d9ca44 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1599,10 +1599,10 @@ def test_line_with_reprcrash(monkeypatch): monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos) - class config: + class config(object): pass - class rep: + class rep(object): def _get_verbose_word(self, *args): return mocked_verbose_word @@ -1614,7 +1614,7 @@ def test_line_with_reprcrash(monkeypatch): __tracebackhide__ = True if msg: rep.longrepr.reprcrash.message = msg - actual = _get_line_with_reprcrash_message(config, rep, width) + actual = _get_line_with_reprcrash_message(config, rep(), width) assert actual == expected if actual != "%s %s" % (mocked_verbose_word, mocked_pos): From 6d040370edc62e4981985d092f9402c462da6c6a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 6 May 2019 19:33:48 -0300 Subject: [PATCH 093/104] Show fixture scopes with ``--fixtures``, except for "function" scope Fix #5220 --- changelog/5220.feature.rst | 1 + src/_pytest/python.py | 10 ++++++---- testing/python/fixtures.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 changelog/5220.feature.rst diff --git a/changelog/5220.feature.rst b/changelog/5220.feature.rst new file mode 100644 index 000000000..cf535afa0 --- /dev/null +++ b/changelog/5220.feature.rst @@ -0,0 +1 @@ +``--fixtures`` now also shows fixture scope for scopes other than ``"function"``. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 377357846..18d909855 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1342,17 +1342,19 @@ def _showfixtures_main(config, session): currentmodule = module if verbose <= 0 and argname[0] == "_": continue + tw.write(argname, green=True) + if fixturedef.scope != "function": + tw.write(" [%s scope]" % fixturedef.scope, cyan=True) if verbose > 0: - funcargspec = "%s -- %s" % (argname, bestrel) - else: - funcargspec = argname - tw.line(funcargspec, green=True) + tw.write(" -- %s" % bestrel, yellow=True) + tw.write("\n") loc = getlocation(fixturedef.func, curdir) doc = fixturedef.func.__doc__ or "" if doc: write_docstring(tw, doc) else: tw.line(" %s: no docstring available" % (loc,), red=True) + tw.line() def write_docstring(tw, doc, indent=" "): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 48f8028e6..41627680d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3037,11 +3037,25 @@ class TestShowFixtures(object): def test_show_fixtures(self, testdir): result = testdir.runpytest("--fixtures") - result.stdout.fnmatch_lines(["*tmpdir*", "*temporary directory*"]) + result.stdout.fnmatch_lines( + [ + "tmpdir_factory [[]session scope[]]", + "*for the test session*", + "tmpdir", + "*temporary directory*", + ] + ) def test_show_fixtures_verbose(self, testdir): result = testdir.runpytest("--fixtures", "-v") - result.stdout.fnmatch_lines(["*tmpdir*--*tmpdir.py*", "*temporary directory*"]) + result.stdout.fnmatch_lines( + [ + "tmpdir_factory [[]session scope[]] -- *tmpdir.py*", + "*for the test session*", + "tmpdir -- *tmpdir.py*", + "*temporary directory*", + ] + ) def test_show_fixtures_testmodule(self, testdir): p = testdir.makepyfile( From c04767f9469c58984bcfd004c1e2995bc36bbc0d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 May 2019 15:20:00 -0300 Subject: [PATCH 094/104] Use msg.rstrip() as suggested in review --- src/_pytest/terminal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 5330d81cb..91526d00f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1000,8 +1000,7 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): # u'😄' will result in a High Surrogate (U+D83D) character, which is # rendered as u'�'; in this case we just strip that character out as it # serves no purpose being rendered - while msg.endswith(u"\uD83D"): - msg = msg[:-1] + msg = msg.rstrip(u"\uD83D") msg += ellipsis line += sep + msg return line From f339147d120452f0dcc7a524822bc6ad53696142 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 May 2019 19:34:57 -0300 Subject: [PATCH 095/104] Add CHANGELOG entry about depending on wcwidth --- changelog/5013.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5013.trivial.rst diff --git a/changelog/5013.trivial.rst b/changelog/5013.trivial.rst new file mode 100644 index 000000000..fff4eaf3f --- /dev/null +++ b/changelog/5013.trivial.rst @@ -0,0 +1 @@ +pytest now depends on `wcwidth `__ to properly track unicode character sizes for more precise terminal output. From 7e08e094730a66d97cfec4aa8d07ed167d1c20ee Mon Sep 17 00:00:00 2001 From: Pulkit Goyal <7895pulkit@gmail.com> Date: Tue, 7 May 2019 23:22:48 +0300 Subject: [PATCH 096/104] logging: improve default logging format (issue5214) We improve the following things in the logging format: * Show module name instead of just the filename * show level of logging as the first thing * show lineno attached to module:file details Thanks to @blueyed who suggested this on the github issue. It's my first contribution and I have added myself to AUTHORS. I also added to a changelog file. --- AUTHORS | 1 + changelog/5214.feature.rst | 10 ++++++++++ src/_pytest/logging.py | 2 +- testing/logging/test_reporting.py | 14 +++++++------- 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 changelog/5214.feature.rst diff --git a/AUTHORS b/AUTHORS index 42420f676..a5bb34039 100644 --- a/AUTHORS +++ b/AUTHORS @@ -194,6 +194,7 @@ Paweł Adamczak Pedro Algarvio Pieter Mulder Piotr Banaszkiewicz +Pulkit Goyal Punyashloka Biswal Quentin Pradet Ralf Schmitt diff --git a/changelog/5214.feature.rst b/changelog/5214.feature.rst new file mode 100644 index 000000000..422a4dd85 --- /dev/null +++ b/changelog/5214.feature.rst @@ -0,0 +1,10 @@ +The default logging format has been changed to improve readability. Here is an +example of a previous logging message:: + + test_log_cli_enabled_disabled.py 3 CRITICAL critical message logged by test + +This has now become:: + + CRITICAL root:test_log_cli_enabled_disabled.py:3 critical message logged by test + +The formatting can be changed through the `log_format `__ configuration option. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 757cb2797..08670d2b2 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -15,7 +15,7 @@ from _pytest.compat import dummy_context_manager from _pytest.config import create_terminal_writer from _pytest.pathlib import Path -DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" +DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index aed203b70..77cf71b43 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -248,7 +248,7 @@ def test_log_cli_enabled_disabled(testdir, enabled): [ "test_log_cli_enabled_disabled.py::test_log_cli ", "*-- live log call --*", - "test_log_cli_enabled_disabled.py* CRITICAL critical message logged by test", + "CRITICAL *test_log_cli_enabled_disabled.py* critical message logged by test", "PASSED*", ] ) @@ -282,7 +282,7 @@ def test_log_cli_default_level(testdir): result.stdout.fnmatch_lines( [ "test_log_cli_default_level.py::test_log_cli ", - "test_log_cli_default_level.py*WARNING message will be shown*", + "WARNING*test_log_cli_default_level.py* message will be shown*", ] ) assert "INFO message won't be shown" not in result.stdout.str() @@ -523,7 +523,7 @@ def test_sections_single_new_line_after_test_outcome(testdir, request): ) assert ( re.search( - r"(.+)live log teardown(.+)\n(.+)WARNING(.+)\n(.+)WARNING(.+)", + r"(.+)live log teardown(.+)\nWARNING(.+)\nWARNING(.+)", result.stdout.str(), re.MULTILINE, ) @@ -531,7 +531,7 @@ def test_sections_single_new_line_after_test_outcome(testdir, request): ) assert ( re.search( - r"(.+)live log finish(.+)\n(.+)WARNING(.+)\n(.+)WARNING(.+)", + r"(.+)live log finish(.+)\nWARNING(.+)\nWARNING(.+)", result.stdout.str(), re.MULTILINE, ) @@ -565,7 +565,7 @@ def test_log_cli_level(testdir): # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ - "test_log_cli_level.py*This log message will be shown", + "*test_log_cli_level.py*This log message will be shown", "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) @@ -579,7 +579,7 @@ def test_log_cli_level(testdir): # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ - "test_log_cli_level.py* This log message will be shown", + "*test_log_cli_level.py* This log message will be shown", "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) @@ -615,7 +615,7 @@ def test_log_cli_ini_level(testdir): # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ - "test_log_cli_ini_level.py* This log message will be shown", + "*test_log_cli_ini_level.py* This log message will be shown", "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) From 80c5f6e60989384fd30b226201e7d5be3e6679b6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 May 2019 21:46:26 +0000 Subject: [PATCH 097/104] Ignore PytestUnknownMark warnings when regen docs A lot of our examples use custom markers to make a point and showcase features, which generates a lot of warnings --- doc/en/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/Makefile b/doc/en/Makefile index 3f4bd9dfe..341a5cb85 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -43,7 +43,7 @@ clean: regen: REGENDOC_FILES:=*.rst */*.rst regen: - PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS=-pno:hypothesis COLUMNS=76 regendoc --update ${REGENDOC_FILES} ${REGENDOC_ARGS} + PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS="-pno:hypothesis -Wignore::pytest.PytestUnknownMarkWarning" COLUMNS=76 regendoc --update ${REGENDOC_FILES} ${REGENDOC_ARGS} html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html From 5d7686951ceb635ffba3e416f3cc3e57a96bfd55 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 May 2019 21:50:08 +0000 Subject: [PATCH 098/104] Run regendoc --- doc/en/builtin.rst | 24 ++++++++++++++++++++---- doc/en/cache.rst | 2 +- doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 2 +- doc/en/example/reportingdemo.rst | 6 +++--- doc/en/example/simple.rst | 2 +- doc/en/skipping.rst | 2 +- doc/en/usage.rst | 16 ++++++++-------- doc/en/warnings.rst | 2 +- 9 files changed, 37 insertions(+), 21 deletions(-) diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index fb16140c0..c7d6b271f 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -27,33 +27,39 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a name of your plugin or application to avoid clashes with other cache users. Values can be any object handled by the json stdlib module. + capsys Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + capsysbinary Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. + capfd Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + capfdbinary Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. - doctest_namespace + + doctest_namespace [session scope] Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. - pytestconfig + + pytestconfig [session scope] Session-scoped fixture that returns the :class:`_pytest.config.Config` object. Example:: @@ -61,6 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_foo(pytestconfig): if pytestconfig.getoption("verbose") > 0: ... + record_property Add an extra properties the calling test. User properties become part of the test report and are available to the @@ -72,10 +79,12 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_function(record_property): record_property("example_key", 1) + record_xml_attribute Add extra xml attributes to the tag for the calling test. The fixture is callable with ``(name, value)``, with value being automatically xml-encoded + caplog Access and control log capturing. @@ -85,6 +94,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.records -> list of logging.LogRecord instances * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string + monkeypatch The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -102,15 +112,19 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a test function or fixture has finished. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. + recwarn Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See http://docs.python.org/library/warnings.html for information on warning categories. - tmpdir_factory + + tmpdir_factory [session scope] Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. - tmp_path_factory + + tmp_path_factory [session scope] Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. + tmpdir Return a temporary directory path object which is unique to each test function invocation, @@ -119,6 +133,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html + tmp_path Return a temporary directory path object which is unique to each test function invocation, @@ -130,6 +145,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a in python < 3.6 this is a pathlib2.Path + no tests ran in 0.12 seconds You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like:: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 4df29e21d..27a844910 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -286,7 +286,7 @@ filtering: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: + rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache ----------------------- cache values for 'example/*' ----------------------- example/value contains: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 991adb50c..be08e4f24 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -619,9 +619,9 @@ then you will see two tests skipped and two executed tests as expected: collected 4 items test_plat.py s.s. [100%] + ========================= short test summary info ========================== SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - =================== 2 passed, 2 skipped in 0.12 seconds ==================== Note that if you specify a platform via the marker-command line option like this: diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index e7dbadf2d..c3f825297 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -492,9 +492,9 @@ If you run this with reporting for skips enabled: collected 2 items test_module.py .s [100%] + ========================= short test summary info ========================== SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' - =================== 1 passed, 1 skipped in 0.12 seconds ==================== You'll see that we don't have an ``opt2`` module and thus the second test run diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 26c6e1b9e..e3c74abf7 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -182,9 +182,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Omitting 1 identical items, use -vv to show E Differing items: E {'b': 1} != {'b': 2} - E Left contains more items: + E Left contains 1 more item: E {'c': 0} - E Right contains more items: + E Right contains 1 more item: E {'d': 0}... E E ...Full output truncated (2 lines hidden), use '-vv' to show @@ -215,7 +215,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_longer_list(self): > assert [1, 2] == [1, 2, 3] E assert [1, 2] == [1, 2, 3] - E Right contains more items, first extra item: 3 + E Right contains one more item: 3 E Use -v to get the full diff failure_demo.py:80: AssertionError diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index ff422c585..1c7c10570 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -194,9 +194,9 @@ and when running it will see a skipped "slow" test: collected 2 items test_module.py .s [100%] + ========================= short test summary info ========================== SKIPPED [1] test_module.py:8: need --runslow option to run - =================== 1 passed, 1 skipped in 0.12 seconds ==================== Or run it including the ``slow`` marked test: diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index d8d4dd1c1..8d0c499b3 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -352,6 +352,7 @@ Running it with the report-on-xfail option gives this output: collected 7 items xfail_demo.py xxxxxxx [100%] + ========================= short test summary info ========================== XFAIL xfail_demo.py::test_hello XFAIL xfail_demo.py::test_hello2 @@ -365,7 +366,6 @@ Running it with the report-on-xfail option gives this output: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - ======================== 7 xfailed in 0.12 seconds ========================= .. _`skip/xfail with parametrize`: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 465979613..9c5d4e250 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -231,9 +231,9 @@ Example: 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 + ERROR test_example.py::test_error - assert 0 + FAILED test_example.py::test_fail - assert 0 + = 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". @@ -281,9 +281,9 @@ 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 + FAILED test_example.py::test_fail - assert 0 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 + = 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: @@ -316,13 +316,13 @@ captured output: E assert 0 test_example.py:14: AssertionError - ========================= short test summary info ========================== - PASSED test_example.py::test_ok ================================== PASSES ================================== _________________________________ test_ok __________________________________ --------------------------- Captured stdout call --------------------------- ok - 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds + ========================= short test summary info ========================== + PASSED test_example.py::test_ok + = 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 832d169ca..dd10d94f4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -400,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta ============================= warnings summary ============================= test_pytest_warnings.py:1 - $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html From 73b74c74c9674947c8383f657c3a78b1d73ffe1b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 May 2019 00:06:36 +0200 Subject: [PATCH 099/104] pdb: only use outcomes.exit via do_quit Fixes https://github.com/pytest-dev/pytest/issues/5235. --- changelog/5235.bugfix.rst | 1 + src/_pytest/debugging.py | 10 ++++++++-- testing/test_pdb.py | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 changelog/5235.bugfix.rst diff --git a/changelog/5235.bugfix.rst b/changelog/5235.bugfix.rst new file mode 100644 index 000000000..87597a589 --- /dev/null +++ b/changelog/5235.bugfix.rst @@ -0,0 +1 @@ +``outcome.exit`` is not used with ``EOF`` in the pdb wrapper anymore, but only with ``quit``. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 7138fd2e1..52c6536f4 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -181,17 +181,23 @@ class pytestPDB(object): do_c = do_cont = do_continue - def set_quit(self): + def do_quit(self, arg): """Raise Exit outcome when quit command is used in pdb. This is a bit of a hack - it would be better if BdbQuit could be handled, but this would require to wrap the whole pytest run, and adjust the report etc. """ - super(PytestPdbWrapper, self).set_quit() + ret = super(PytestPdbWrapper, self).do_quit(arg) + if cls._recursive_debug == 0: outcomes.exit("Quitting debugger") + return ret + + do_q = do_quit + do_exit = do_quit + def setup(self, f, tb): """Suspend on setup(). diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 0a72a2907..3b21bacd9 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -6,6 +6,8 @@ import os import platform import sys +import six + import _pytest._code import pytest from _pytest.debugging import _validate_usepdb_cls @@ -395,7 +397,7 @@ class TestPDB(object): child = testdir.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") - child.sendeof() + child.sendline("q") rest = child.read().decode("utf8") assert "no tests ran" in rest assert "reading from stdin while output" not in rest @@ -957,7 +959,7 @@ class TestDebuggingBreakpoints(object): child = testdir.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") - child.sendeof() + child.sendline("quit") rest = child.read().decode("utf8") assert "Quitting debugger" in rest assert "reading from stdin while output" not in rest @@ -1163,3 +1165,29 @@ def test_pdbcls_via_local_module(testdir): ) assert result.ret == 0 result.stdout.fnmatch_lines(["*runcall_called*", "* 1 passed in *"]) + + +def test_raises_bdbquit_with_eoferror(testdir): + """It is not guaranteed that DontReadFromInput's read is called.""" + if six.PY2: + builtin_module = "__builtin__" + input_func = "raw_input" + else: + builtin_module = "builtins" + input_func = "input" + p1 = testdir.makepyfile( + """ + def input_without_read(*args, **kwargs): + raise EOFError() + + def test(monkeypatch): + import {builtin_module} + monkeypatch.setattr({builtin_module}, {input_func!r}, input_without_read) + __import__('pdb').set_trace() + """.format( + builtin_module=builtin_module, input_func=input_func + ) + ) + result = testdir.runpytest(str(p1)) + result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"]) + assert result.ret == 1 From 685ca96c71d21611bb695a966121815ce1a44e5e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 27 Apr 2019 10:52:12 -0300 Subject: [PATCH 100/104] Change ``--strict`` to ``--strict-markers``, preserving the old one Fix #5023 --- changelog/5023.feature.rst | 1 + doc/en/example/markers.rst | 2 +- doc/en/mark.rst | 6 +++--- doc/en/reference.rst | 11 ++++++++--- src/_pytest/main.py | 8 ++------ src/_pytest/mark/structures.py | 7 +++++-- testing/test_mark.py | 13 ++++++++----- testing/test_stepwise.py | 29 +++++++++++++++++++++-------- testing/test_warnings.py | 2 +- 9 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 changelog/5023.feature.rst diff --git a/changelog/5023.feature.rst b/changelog/5023.feature.rst new file mode 100644 index 000000000..a4c67fe68 --- /dev/null +++ b/changelog/5023.feature.rst @@ -0,0 +1 @@ +``--strict`` is now called ``--strict-markers`` as it is more explicit about what it does. The old name still works for backward compatibility. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index be08e4f24..f143e448c 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -259,7 +259,7 @@ For an example on how to add and work with markers from a plugin, see * Asking for existing markers via ``pytest --markers`` gives good output * Typos in function markers are treated as an error if you use - the ``--strict`` option. + the ``--strict-markers`` option. .. _`scoped-marking`: diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 14509c70b..05ffdb36c 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -41,15 +41,15 @@ marks by registering them in ``pytest.ini`` like this: slow serial -When the ``--strict`` command-line flag is passed, any unknown marks applied +When the ``--strict-markers`` command-line flag is passed, any unknown marks applied with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. Marks added by pytest or by a plugin instead of the decorator will not trigger -this error. To enforce validation of markers, add ``--strict`` to ``addopts``: +this error. To enforce validation of markers, add ``--strict-markers`` to ``addopts``: .. code-block:: ini [pytest] - addopts = --strict + addopts = --strict-markers markers = slow serial diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f39f2a6e0..4b76bdaf7 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1261,19 +1261,24 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: markers - When the ``--strict`` command-line argument is used, only known markers - + When the ``--strict-markers`` command-line argument is used, only known markers - defined in code by core pytest or some plugin - are allowed. - You can list additional markers in this setting to add them to the whitelist. - You can list one marker name per line, indented from the option name. + You can list additional markers in this setting to add them to the whitelist, + in which case you probably want to add ``--strict-markers`` to ``addopts`` + to avoid future regressions: .. code-block:: ini [pytest] + addopts = --strict-markers markers = slow serial + **Note**: This option was previously called ``--strict``, which is now an + alias preserved for backward compatibility. + .. confval:: minversion Specifies a minimal pytest version required for running tests. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4f5a87bcd..96ead8509 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -47,11 +47,6 @@ def pytest_addoption(parser): type="args", default=[], ) - # parser.addini("dirpatterns", - # "patterns specifying possible locations of test files", - # type="linelist", default=["**/test_*.txt", - # "**/test_*.py", "**/*_test.py"] - # ) group = parser.getgroup("general", "running and selection options") group._addoption( "-x", @@ -71,9 +66,10 @@ def pytest_addoption(parser): help="exit after first num failures or errors.", ) group._addoption( + "--strict-markers", "--strict", action="store_true", - help="marks not registered in configuration file raise errors.", + help="markers not registered in the `markers` section of the configuration file raise errors.", ) group._addoption( "-c", diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b4ff6e988..a734ae1d4 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -311,8 +311,11 @@ class MarkGenerator(object): # If the name is not in the set of known marks after updating, # 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} is not a registered marker".format(name), pytrace=False) + if self._config.option.strict_markers: + fail( + "{!r} not found in `markers` configuration option".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 72b96ab51..294a1ee6b 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -130,7 +130,7 @@ def test_ini_markers_whitespace(testdir): assert True """ ) - rec = testdir.inline_run("--strict", "-m", "a1") + rec = testdir.inline_run("--strict-markers", "-m", "a1") rec.assertoutcome(passed=1) @@ -150,7 +150,7 @@ def test_marker_without_description(testdir): ) ftdir = testdir.mkdir("ft1_dummy") testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) - rec = testdir.runpytest("--strict") + rec = testdir.runpytest("--strict-markers") rec.assert_outcomes() @@ -194,7 +194,8 @@ def test_mark_on_pseudo_function(testdir): reprec.assertoutcome(passed=1) -def test_strict_prohibits_unregistered_markers(testdir): +@pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"]) +def test_strict_prohibits_unregistered_markers(testdir, option_name): testdir.makepyfile( """ import pytest @@ -203,9 +204,11 @@ def test_strict_prohibits_unregistered_markers(testdir): pass """ ) - result = testdir.runpytest("--strict") + result = testdir.runpytest(option_name) assert result.ret != 0 - result.stdout.fnmatch_lines(["'unregisteredmark' is not a registered marker"]) + result.stdout.fnmatch_lines( + ["'unregisteredmark' not found in `markers` configuration option"] + ) @pytest.mark.parametrize( diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index b85839925..2202bbf1b 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -76,7 +76,7 @@ def broken_testdir(testdir): def test_run_without_stepwise(stepwise_testdir): - result = stepwise_testdir.runpytest("-v", "--strict", "--fail") + result = stepwise_testdir.runpytest("-v", "--strict-markers", "--fail") result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"]) result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"]) @@ -85,7 +85,9 @@ def test_run_without_stepwise(stepwise_testdir): def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. - result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "--fail") + result = stepwise_testdir.runpytest( + "-v", "--strict-markers", "--stepwise", "--fail" + ) assert not result.stderr.str() stdout = result.stdout.str() @@ -95,7 +97,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): assert "test_success_after_fail" not in stdout # "Fix" the test that failed in the last run and run it again. - result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise") + result = stepwise_testdir.runpytest("-v", "--strict-markers", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() @@ -107,7 +109,12 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): def test_run_with_skip_option(stepwise_testdir): result = stepwise_testdir.runpytest( - "-v", "--strict", "--stepwise", "--stepwise-skip", "--fail", "--fail-last" + "-v", + "--strict-markers", + "--stepwise", + "--stepwise-skip", + "--fail", + "--fail-last", ) assert not result.stderr.str() @@ -120,7 +127,7 @@ def test_run_with_skip_option(stepwise_testdir): def test_fail_on_errors(error_testdir): - result = error_testdir.runpytest("-v", "--strict", "--stepwise") + result = error_testdir.runpytest("-v", "--strict-markers", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() @@ -131,7 +138,7 @@ def test_fail_on_errors(error_testdir): def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest( - "-v", "--strict", "--stepwise", "--fail", "test_a.py" + "-v", "--strict-markers", "--stepwise", "--fail", "test_a.py" ) assert not result.stderr.str() @@ -140,7 +147,9 @@ def test_change_testfile(stepwise_testdir): # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. - result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "test_b.py") + result = stepwise_testdir.runpytest( + "-v", "--strict-markers", "--stepwise", "test_b.py" + ) assert not result.stderr.str() stdout = result.stdout.str() @@ -149,7 +158,11 @@ def test_change_testfile(stepwise_testdir): def test_stop_on_collection_errors(broken_testdir): result = broken_testdir.runpytest( - "-v", "--strict", "--stepwise", "working_testfile.py", "broken_testfile.py" + "-v", + "--strict-markers", + "--stepwise", + "working_testfile.py", + "broken_testfile.py", ) stdout = result.stdout.str() diff --git a/testing/test_warnings.py b/testing/test_warnings.py index a24289f57..0daa466b7 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -302,7 +302,7 @@ def test_filterwarnings_mark_registration(testdir): pass """ ) - result = testdir.runpytest("--strict") + result = testdir.runpytest("--strict-markers") assert result.ret == 0 From f1183c242275eafd5ad5e594bd631f694c18833a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 27 Apr 2019 11:25:37 -0300 Subject: [PATCH 101/104] Remove the 'issue' marker from test suite It doesn't seem to add much value (why would one execute tests based on that marker?), plus using the docstring for that encourages one to write a more descriptive message about the test --- testing/python/fixtures.py | 9 ++++--- testing/python/integration.py | 3 ++- testing/python/metafunc.py | 44 +++++++++++++++++++++-------------- testing/test_capture.py | 2 +- testing/test_conftest.py | 2 +- testing/test_mark.py | 6 ++--- testing/test_recwarn.py | 2 +- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 2 +- tox.ini | 1 - 10 files changed, 40 insertions(+), 33 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 41627680d..18ede4006 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1925,10 +1925,10 @@ class TestAutouseManagement(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - @pytest.mark.issue(226) @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first(self, testdir, param1, param2): + """#226""" testdir.makepyfile( """ import pytest @@ -2707,9 +2707,9 @@ class TestFixtureMarker(object): reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=5) - @pytest.mark.issue(246) @pytest.mark.parametrize("scope", ["session", "function", "module"]) def test_finalizer_order_on_parametrization(self, scope, testdir): + """#246""" testdir.makepyfile( """ import pytest @@ -2744,8 +2744,8 @@ class TestFixtureMarker(object): reprec = testdir.inline_run("-lvs") reprec.assertoutcome(passed=3) - @pytest.mark.issue(396) def test_class_scope_parametrization_ordering(self, testdir): + """#396""" testdir.makepyfile( """ import pytest @@ -2865,8 +2865,8 @@ class TestFixtureMarker(object): res = testdir.runpytest("-v") res.stdout.fnmatch_lines(["*test_foo*alpha*", "*test_foo*beta*"]) - @pytest.mark.issue(920) def test_deterministic_fixture_collection(self, testdir, monkeypatch): + """#920""" testdir.makepyfile( """ import pytest @@ -3649,7 +3649,6 @@ class TestScopeOrdering(object): """Class of tests that ensure fixtures are ordered based on their scopes (#2405)""" @pytest.mark.parametrize("variant", ["mark", "autouse"]) - @pytest.mark.issue(github="#2405") def test_func_closure_module_auto(self, testdir, variant, monkeypatch): """Semantically identical to the example posted in #2405 when ``use_mark=True``""" monkeypatch.setenv("FIXTURE_ACTIVATION_VARIANT", variant) diff --git a/testing/python/integration.py b/testing/python/integration.py index 3c6eecbe1..a62747014 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -393,8 +393,9 @@ class TestNoselikeTestAttribute(object): assert not call.items -@pytest.mark.issue(351) class TestParameterize(object): + """#351""" + def test_idfn_marker(self, testdir): testdir.makepyfile( """ diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c3429753e..29f18da36 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -159,8 +159,9 @@ class TestMetafunc(object): ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ) - @pytest.mark.issue(510) def test_parametrize_empty_list(self): + """#510""" + def func(y): pass @@ -262,8 +263,8 @@ class TestMetafunc(object): for val, expected in values: assert _idval(val, "a", 6, None, item=None, config=None) == expected - @pytest.mark.issue(250) def test_idmaker_autoname(self): + """#250""" from _pytest.python import idmaker result = idmaker( @@ -356,8 +357,8 @@ class TestMetafunc(object): result = idmaker(("a", "b"), [pytest.param(e.one, e.two)]) assert result == ["Foo.one-Foo.two"] - @pytest.mark.issue(351) def test_idmaker_idfn(self): + """#351""" from _pytest.python import idmaker def ids(val): @@ -375,8 +376,8 @@ class TestMetafunc(object): ) assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"] - @pytest.mark.issue(351) def test_idmaker_idfn_unique_names(self): + """#351""" from _pytest.python import idmaker def ids(val): @@ -459,8 +460,9 @@ class TestMetafunc(object): ) assert result == ["a0", "a1", "b0", "c", "b1"] - @pytest.mark.issue(714) def test_parametrize_indirect(self): + """#714""" + def func(x, y): pass @@ -473,8 +475,9 @@ class TestMetafunc(object): assert metafunc._calls[0].params == dict(x=1, y=2) assert metafunc._calls[1].params == dict(x=1, y=3) - @pytest.mark.issue(714) def test_parametrize_indirect_list(self): + """#714""" + def func(x, y): pass @@ -483,8 +486,9 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == dict(y="b") assert metafunc._calls[0].params == dict(x="a") - @pytest.mark.issue(714) def test_parametrize_indirect_list_all(self): + """#714""" + def func(x, y): pass @@ -493,8 +497,9 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == {} assert metafunc._calls[0].params == dict(x="a", y="b") - @pytest.mark.issue(714) def test_parametrize_indirect_list_empty(self): + """#714""" + def func(x, y): pass @@ -503,9 +508,9 @@ class TestMetafunc(object): assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].params == {} - @pytest.mark.issue(714) def test_parametrize_indirect_list_functional(self, testdir): """ + #714 Test parametrization with 'indirect' parameter applied on particular arguments. As y is is direct, its value should be used directly rather than being passed to the fixture @@ -532,8 +537,9 @@ class TestMetafunc(object): result = testdir.runpytest("-v") result.stdout.fnmatch_lines(["*test_simple*a-b*", "*1 passed*"]) - @pytest.mark.issue(714) def test_parametrize_indirect_list_error(self, testdir): + """#714""" + def func(x, y): pass @@ -541,12 +547,13 @@ class TestMetafunc(object): with pytest.raises(pytest.fail.Exception): metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) - @pytest.mark.issue(714) def test_parametrize_uses_no_fixture_error_indirect_false(self, testdir): """The 'uses no fixture' error tells the user at collection time that the parametrize data they've set up doesn't correspond to the fixtures in their test function, rather than silently ignoring this and letting the test potentially pass. + + #714 """ testdir.makepyfile( """ @@ -560,8 +567,8 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no argument 'y'*"]) - @pytest.mark.issue(714) def test_parametrize_uses_no_fixture_error_indirect_true(self, testdir): + """#714""" testdir.makepyfile( """ import pytest @@ -580,8 +587,8 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue(714) def test_parametrize_indirect_uses_no_fixture_error_indirect_string(self, testdir): + """#714""" testdir.makepyfile( """ import pytest @@ -597,8 +604,8 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue(714) def test_parametrize_indirect_uses_no_fixture_error_indirect_list(self, testdir): + """#714""" testdir.makepyfile( """ import pytest @@ -614,8 +621,8 @@ class TestMetafunc(object): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*uses no fixture 'y'*"]) - @pytest.mark.issue(714) def test_parametrize_argument_not_in_indirect_list(self, testdir): + """#714""" testdir.makepyfile( """ import pytest @@ -1201,9 +1208,9 @@ class TestMetafuncFunctional(object): reprec = testdir.runpytest() reprec.assert_outcomes(passed=4) - @pytest.mark.issue(463) @pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"]) def test_parametrize_misspelling(self, testdir, attr): + """#463""" testdir.makepyfile( """ import pytest @@ -1386,8 +1393,9 @@ class TestMetafuncFunctionalAuto(object): assert output.count("preparing foo-3") == 1 -@pytest.mark.issue(308) class TestMarkersWithParametrization(object): + """#308""" + def test_simple_mark(self, testdir): s = """ import pytest @@ -1575,8 +1583,8 @@ class TestMarkersWithParametrization(object): reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) reprec.assertoutcome(passed=2, skipped=2) - @pytest.mark.issue(290) def test_parametrize_ID_generation_string_int_works(self, testdir): + """#290""" testdir.makepyfile( """ import pytest diff --git a/testing/test_capture.py b/testing/test_capture.py index 2b450c189..5d80eb63d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -605,8 +605,8 @@ class TestCaptureFixture(object): result.stdout.fnmatch_lines(["*KeyboardInterrupt*"]) assert result.ret == 2 - @pytest.mark.issue(14) def test_capture_and_logging(self, testdir): + """#14""" p = testdir.makepyfile( """\ import logging diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a0458e595..acb9b52b4 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -491,10 +491,10 @@ class TestConftestVisibility(object): ("snc", ".", 1), ], ) - @pytest.mark.issue(616) def test_parsefactories_relative_node_ids( self, testdir, chdir, testarg, expect_ntests_passed ): + """#616""" dirs = self._setup_tree(testdir) print("pytest run in cwd: %s" % (dirs[chdir].relto(testdir.tmpdir))) print("pytestarg : %s" % (testarg)) diff --git a/testing/test_mark.py b/testing/test_mark.py index 294a1ee6b..03cd2f78c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -452,8 +452,8 @@ class TestFunctional(object): items, rec = testdir.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b"), test_bar=("a",)) - @pytest.mark.issue(568) def test_mark_should_not_pass_to_siebling_class(self, testdir): + """#568""" p = testdir.makepyfile( """ import pytest @@ -655,9 +655,9 @@ class TestFunctional(object): markers = {m.name for m in items[name].iter_markers()} assert markers == set(expected_markers) - @pytest.mark.issue(1540) @pytest.mark.filterwarnings("ignore") def test_mark_from_parameters(self, testdir): + """#1540""" testdir.makepyfile( """ import pytest @@ -943,9 +943,9 @@ def test_addmarker_order(): assert extracted == ["c", "a", "b"] -@pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/3605") @pytest.mark.filterwarnings("ignore") def test_markers_from_parametrize(testdir): + """#3605""" testdir.makepyfile( """ from __future__ import print_function diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 9bf6a2ffb..982e246d7 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -47,8 +47,8 @@ class TestWarningsRecorderChecker(object): assert values is rec.list pytest.raises(AssertionError, rec.pop) - @pytest.mark.issue(4243) def test_warn_stacklevel(self): + """#4243""" rec = WarningsRecorder() with rec: warnings.warn("test", DeprecationWarning, 2) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 3b7a4ddc3..c49f7930d 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -58,8 +58,8 @@ class TestTempdirHandler(object): assert tmp2.relto(t.getbasetemp()).startswith("this") assert tmp2 != tmp - @pytest.mark.issue(4425) def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): + """#4425""" from _pytest.tmpdir import TempPathFactory monkeypatch.chdir(tmp_path) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index a519ec255..78250dae3 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -930,11 +930,11 @@ def test_class_method_containing_test_issue1558(testdir): reprec.assertoutcome(passed=1) -@pytest.mark.issue(3498) @pytest.mark.parametrize( "base", ["six.moves.builtins.object", "unittest.TestCase", "unittest2.TestCase"] ) def test_usefixtures_marker_on_unittest(base, testdir): + """#3498""" module = base.rsplit(".", 1)[0] pytest.importorskip(module) testdir.makepyfile( diff --git a/tox.ini b/tox.ini index 3c1ca65b7..e9517b63c 100644 --- a/tox.ini +++ b/tox.ini @@ -172,7 +172,6 @@ filterwarnings = ignore::_pytest.warning_types.PytestUnknownMarkWarning pytester_example_dir = testing/example_scripts markers = - issue slow [flake8] From 0594dba5ce76628902dfd3ee107c6c2d10c7d73a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 27 Apr 2019 11:43:36 -0300 Subject: [PATCH 102/104] Remove unused markers and enable --strict-markers --- changelog/5023.feature.rst | 6 +++++- doc/en/reference.rst | 7 ++----- testing/code/test_excinfo.py | 2 -- testing/code/test_source.py | 4 ---- testing/test_mark.py | 16 ++++++++-------- tox.ini | 7 ++++++- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/changelog/5023.feature.rst b/changelog/5023.feature.rst index a4c67fe68..348e2f1c3 100644 --- a/changelog/5023.feature.rst +++ b/changelog/5023.feature.rst @@ -1 +1,5 @@ -``--strict`` is now called ``--strict-markers`` as it is more explicit about what it does. The old name still works for backward compatibility. +New flag ``--strict-markers`` that triggers an error when unknown markers (e.g. those not registered using the `markers option`_ in the configuration file) are used in the test suite. + +The existing ``--strict`` option has the same behavior currently, but can be augmented in the future for additional checks. + +.. _`markers option`: https://docs.pytest.org/en/latest/reference.html#confval-markers diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 4b76bdaf7..33e4412ca 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1261,8 +1261,8 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: markers - When the ``--strict-markers`` command-line argument is used, only known markers - - defined in code by core pytest or some plugin - are allowed. + When the ``--strict-markers`` or ``--strict`` command-line arguments are used, + only known markers - defined in code by core pytest or some plugin - are allowed. You can list additional markers in this setting to add them to the whitelist, in which case you probably want to add ``--strict-markers`` to ``addopts`` @@ -1276,9 +1276,6 @@ passed multiple times. The expected format is ``name=value``. For example:: slow serial - **Note**: This option was previously called ``--strict``, which is now an - alias preserved for backward compatibility. - .. confval:: minversion Specifies a minimal pytest version required for running tests. diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 92e9395c7..a76797301 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -11,7 +11,6 @@ import textwrap import py import six from six.moves import queue -from test_source import astonly import _pytest import pytest @@ -147,7 +146,6 @@ class TestTraceback_f_g_h(object): assert s.startswith("def f():") assert s.endswith("raise ValueError") - @astonly @failsonjython def test_traceback_entry_getsource_in_construct(self): source = _pytest._code.Source( diff --git a/testing/code/test_source.py b/testing/code/test_source.py index aa56273c4..8d400a289 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -16,7 +16,6 @@ import _pytest._code import pytest from _pytest._code import Source -astonly = pytest.mark.nothing failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") @@ -227,7 +226,6 @@ class TestSourceParsingAndCompiling(object): s = source.getstatement(1) assert s == str(source) - @astonly def test_getstatementrange_within_constructs(self): source = Source( """\ @@ -630,7 +628,6 @@ x = 3 class TestTry(object): - pytestmark = astonly source = """\ try: raise ValueError @@ -675,7 +672,6 @@ finally: class TestIf(object): - pytestmark = astonly source = """\ if 1: y = 3 diff --git a/testing/test_mark.py b/testing/test_mark.py index 03cd2f78c..b2f893438 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -44,11 +44,11 @@ class TestMark(object): class SomeClass(object): pass - assert pytest.mark.fun(some_function) is some_function - assert pytest.mark.fun.with_args(some_function) is not some_function + assert pytest.mark.foo(some_function) is some_function + assert pytest.mark.foo.with_args(some_function) is not some_function - assert pytest.mark.fun(SomeClass) is SomeClass - assert pytest.mark.fun.with_args(SomeClass) is not SomeClass + assert pytest.mark.foo(SomeClass) is SomeClass + assert pytest.mark.foo.with_args(SomeClass) is not SomeClass def test_pytest_mark_name_starts_with_underscore(self): mark = Mark() @@ -936,11 +936,11 @@ def test_mark_expressions_no_smear(testdir): def test_addmarker_order(): node = Node("Test", config=mock.Mock(), session=mock.Mock(), nodeid="Test") - node.add_marker("a") - node.add_marker("b") - node.add_marker("c", append=False) + node.add_marker("foo") + node.add_marker("bar") + node.add_marker("baz", append=False) extracted = [x.name for x in node.iter_markers()] - assert extracted == ["c", "a", "b"] + assert extracted == ["baz", "foo", "bar"] @pytest.mark.filterwarnings("ignore") diff --git a/tox.ini b/tox.ini index e9517b63c..fb4134d17 100644 --- a/tox.ini +++ b/tox.ini @@ -141,7 +141,7 @@ commands = python scripts/release.py {posargs} [pytest] minversion = 2.0 -addopts = -ra -p pytester +addopts = -ra -p pytester --strict-markers rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance @@ -172,6 +172,11 @@ filterwarnings = ignore::_pytest.warning_types.PytestUnknownMarkWarning pytester_example_dir = testing/example_scripts markers = + # dummy markers for testing + foo + bar + baz + # conftest.py reorders tests moving slow ones to the end of the list slow [flake8] From 73bbff2b7459e2523973ac90dcbf15c3b4e1684a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 May 2019 16:30:16 -0300 Subject: [PATCH 103/104] Introduce record_testsuite_property fixture This exposes the functionality introduced in fa6acdc as a session-scoped fixture. Plugins that want to remain compatible with the `xunit2` standard should use this fixture instead of `record_property`. Fix #5202 --- changelog/5202.feature.rst | 5 ++++ doc/en/reference.rst | 8 ++++++ doc/en/usage.rst | 59 +++++++++++++++++--------------------- src/_pytest/junitxml.py | 44 +++++++++++++++++++++++++++- testing/test_junitxml.py | 47 ++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 34 deletions(-) create mode 100644 changelog/5202.feature.rst diff --git a/changelog/5202.feature.rst b/changelog/5202.feature.rst new file mode 100644 index 000000000..82b718d9c --- /dev/null +++ b/changelog/5202.feature.rst @@ -0,0 +1,5 @@ +New ``record_testsuite_property`` session-scoped fixture allows users to log ```` tags at the ``testsuite`` +level with the ``junitxml`` plugin. + +The generated XML is compatible with the latest xunit standard, contrary to +the properties recorded by ``record_property`` and ``record_xml_attribute``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f39f2a6e0..437d6694a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -424,6 +424,14 @@ record_property .. autofunction:: _pytest.junitxml.record_property() + +record_testsuite_property +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Tutorial**: :ref:`record_testsuite_property example`. + +.. autofunction:: _pytest.junitxml.record_testsuite_property() + caplog ~~~~~~ diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 9c5d4e250..acf736f21 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -458,13 +458,6 @@ instead, configure the ``junit_duration_report`` option like this: record_property ^^^^^^^^^^^^^^^ - - - - Fixture renamed from ``record_xml_property`` to ``record_property`` as user - properties are now available to all reporters. - ``record_xml_property`` is now deprecated. - If you want to log additional information for a test, you can use the ``record_property`` fixture: @@ -522,9 +515,7 @@ Will result in: .. warning:: - ``record_property`` is an experimental feature and may change in the future. - - Also please note that using this feature will break any schema verification. + Please note that using this feature will break schema verifications for the latest JUnitXML schema. This might be a problem when used with some CI servers. record_xml_attribute @@ -587,43 +578,45 @@ Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generat -LogXML: add_global_property -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. warning:: + Please note that using this feature will break schema verifications for the latest JUnitXML schema. + This might be a problem when used with some CI servers. +.. _record_testsuite_property example: -If you want to add a properties node in the testsuite level, which may contains properties that are relevant -to all testcases you can use ``LogXML.add_global_properties`` +record_testsuite_property +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 4.5 + +If you want to add a properties node at the test-suite level, which may contains properties +that are relevant to all tests, you can use the ``record_testsuite_property`` session-scoped fixture: + +The ``record_testsuite_property`` session-scoped fixture can be used to add properties relevant +to all tests. .. code-block:: python import pytest - @pytest.fixture(scope="session") - def log_global_env_facts(f): - - if pytest.config.pluginmanager.hasplugin("junitxml"): - my_junit = getattr(pytest.config, "_xml", None) - - my_junit.add_global_property("ARCH", "PPC") - my_junit.add_global_property("STORAGE_TYPE", "CEPH") - - - @pytest.mark.usefixtures(log_global_env_facts.__name__) - def start_and_prepare_env(): - pass + @pytest.fixture(scope="session", autouse=True) + def log_global_env_facts(record_testsuite_property): + record_testsuite_property("ARCH", "PPC") + record_testsuite_property("STORAGE_TYPE", "CEPH") class TestMe(object): def test_foo(self): assert True -This will add a property node below the testsuite node to the generated xml: +The fixture is a callable which receives ``name`` and ``value`` of a ```` tag +added at the test-suite level of the generated xml: .. code-block:: xml - + @@ -631,11 +624,11 @@ This will add a property node below the testsuite node to the generated xml: -.. warning:: +``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + +The generated XML is compatible with the latest ``xunit`` standard, contrary to `record_property`_ +and `record_xml_attribute`_. - This is an experimental feature, and its interface might be replaced - by something more powerful and general in future versions. The - functionality per-se will be kept. Creating resultlog format files ---------------------------------------------------- diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index f1b7763e2..e3c98c37e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -345,6 +345,45 @@ def record_xml_attribute(request): return attr_func +def _check_record_param_type(param, v): + """Used by record_testsuite_property to check that the given parameter name is of the proper + type""" + __tracebackhide__ = True + if not isinstance(v, six.string_types): + msg = "{param} parameter needs to be a string, but {g} given" + raise TypeError(msg.format(param=param, g=type(v).__name__)) + + +@pytest.fixture(scope="session") +def record_testsuite_property(request): + """ + Records a new ```` tag as child of the root ````. This is suitable to + writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. + + This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: + + .. code-block:: python + + def test_foo(record_testsuite_property): + record_testsuite_property("ARCH", "PPC") + record_testsuite_property("STORAGE_TYPE", "CEPH") + + ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + """ + + __tracebackhide__ = True + + def record_func(name, value): + """noop function in case --junitxml was not passed in the command-line""" + __tracebackhide__ = True + _check_record_param_type("name", name) + + xml = getattr(request.config, "_xml", None) + if xml is not None: + record_func = xml.add_global_property # noqa + return record_func + + def pytest_addoption(parser): group = parser.getgroup("terminal reporting") group.addoption( @@ -444,6 +483,7 @@ class LogXML(object): self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters_ordered = [] self.global_properties = [] + # List of reports that failed on call but teardown is pending. self.open_reports = [] self.cnt_double_fail_tests = 0 @@ -632,7 +672,9 @@ class LogXML(object): terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) def add_global_property(self, name, value): - self.global_properties.append((str(name), bin_xml_escape(value))) + __tracebackhide__ = True + _check_record_param_type("name", name) + self.global_properties.append((name, bin_xml_escape(value))) def _get_global_properties_node(self): """Return a Junit node containing custom properties, if any. diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a32eab2ec..cca0143a2 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1243,6 +1243,53 @@ def test_url_property(testdir): ), "The URL did not get written to the xml" +def test_record_testsuite_property(testdir): + testdir.makepyfile( + """ + def test_func1(record_testsuite_property): + record_testsuite_property("stats", "all good") + + def test_func2(record_testsuite_property): + record_testsuite_property("stats", 10) + """ + ) + result, dom = runandparse(testdir) + assert result.ret == 0 + node = dom.find_first_by_tag("testsuite") + properties_node = node.find_first_by_tag("properties") + p1_node = properties_node.find_nth_by_tag("property", 0) + p2_node = properties_node.find_nth_by_tag("property", 1) + p1_node.assert_attr(name="stats", value="all good") + p2_node.assert_attr(name="stats", value="10") + + +def test_record_testsuite_property_junit_disabled(testdir): + testdir.makepyfile( + """ + def test_func1(record_testsuite_property): + record_testsuite_property("stats", "all good") + """ + ) + result = testdir.runpytest() + assert result.ret == 0 + + +@pytest.mark.parametrize("junit", [True, False]) +def test_record_testsuite_property_type_checking(testdir, junit): + testdir.makepyfile( + """ + def test_func1(record_testsuite_property): + record_testsuite_property(1, 2) + """ + ) + args = ("--junitxml=tests.xml",) if junit else () + result = testdir.runpytest(*args) + assert result.ret == 1 + result.stdout.fnmatch_lines( + ["*TypeError: name parameter needs to be a string, but int given"] + ) + + @pytest.mark.parametrize("suite_name", ["my_suite", ""]) def test_set_suite_name(testdir, suite_name): if suite_name: From 63fe547d9f97a78c63a91e139d3a17c15afe7e84 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 11 May 2019 16:35:32 +0000 Subject: [PATCH 104/104] Preparing release version 4.5.0 --- CHANGELOG.rst | 133 ++++++++++++++++++++++++++++++ changelog/4826.feature.rst | 2 - changelog/4907.feature.rst | 1 - changelog/4935.doc.rst | 1 - changelog/4942.trivial.rst | 1 - changelog/5013.feature.rst | 1 - changelog/5013.trivial.rst | 1 - changelog/5023.feature.rst | 5 -- changelog/5026.feature.rst | 1 - changelog/5034.feature.rst | 1 - changelog/5035.feature.rst | 1 - changelog/5059.feature.rst | 1 - changelog/5059.trivial.rst | 1 - changelog/5068.feature.rst | 1 - changelog/5069.trivial.rst | 1 - changelog/5082.trivial.rst | 1 - changelog/5108.feature.rst | 1 - changelog/5113.bugfix.rst | 1 - changelog/5144.bugfix.rst | 1 - changelog/5172.feature.rst | 2 - changelog/5177.feature.rst | 14 ---- changelog/5202.feature.rst | 5 -- changelog/5202.trivial.rst | 4 - changelog/5214.feature.rst | 10 --- changelog/5220.feature.rst | 1 - changelog/5235.bugfix.rst | 1 - changelog/5239.trivial.rst | 3 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.5.0.rst | 35 ++++++++ doc/en/builtin.rst | 14 ++++ doc/en/example/simple.rst | 2 +- 31 files changed, 184 insertions(+), 64 deletions(-) delete mode 100644 changelog/4826.feature.rst delete mode 100644 changelog/4907.feature.rst delete mode 100644 changelog/4935.doc.rst delete mode 100644 changelog/4942.trivial.rst delete mode 100644 changelog/5013.feature.rst delete mode 100644 changelog/5013.trivial.rst delete mode 100644 changelog/5023.feature.rst delete mode 100644 changelog/5026.feature.rst delete mode 100644 changelog/5034.feature.rst delete mode 100644 changelog/5035.feature.rst delete mode 100644 changelog/5059.feature.rst delete mode 100644 changelog/5059.trivial.rst delete mode 100644 changelog/5068.feature.rst delete mode 100644 changelog/5069.trivial.rst delete mode 100644 changelog/5082.trivial.rst delete mode 100644 changelog/5108.feature.rst delete mode 100644 changelog/5113.bugfix.rst delete mode 100644 changelog/5144.bugfix.rst delete mode 100644 changelog/5172.feature.rst delete mode 100644 changelog/5177.feature.rst delete mode 100644 changelog/5202.feature.rst delete mode 100644 changelog/5202.trivial.rst delete mode 100644 changelog/5214.feature.rst delete mode 100644 changelog/5220.feature.rst delete mode 100644 changelog/5235.bugfix.rst delete mode 100644 changelog/5239.trivial.rst create mode 100644 doc/en/announce/release-4.5.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e035a1a4d..013e891d8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,139 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.5.0 (2019-05-11) +========================= + +Features +-------- + +- `#4826 `_: A warning is now emitted when unknown marks are used as a decorator. + This is often due to a typo, which can lead to silently broken tests. + + +- `#4907 `_: Show XFail reason as part of JUnitXML message field. + + +- `#5013 `_: Messages from crash reports are displayed within test summaries now, truncated to the terminal width. + + +- `#5023 `_: New flag ``--strict-markers`` that triggers an error when unknown markers (e.g. those not registered using the `markers option`_ in the configuration file) are used in the test suite. + + The existing ``--strict`` option has the same behavior currently, but can be augmented in the future for additional checks. + + .. _`markers option`: https://docs.pytest.org/en/latest/reference.html#confval-markers + + +- `#5026 `_: Assertion failure messages for sequences and dicts contain the number of different items now. + + +- `#5034 `_: Improve reporting with ``--lf`` and ``--ff`` (run-last-failure). + + +- `#5035 `_: The ``--cache-show`` option/action accepts an optional glob to show only matching cache entries. + + +- `#5059 `_: Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``. + + +- `#5068 `_: The ``-r`` option learnt about ``A`` to display all reports (including passed ones) in the short test summary. + + +- `#5108 `_: The short test summary is displayed after passes with output (``-rP``). + + +- `#5172 `_: The ``--last-failed`` (``--lf``) option got smarter and will now skip entire files if all tests + of that test file have passed in previous runs, greatly speeding up collection. + + +- `#5177 `_: Introduce new specific warning ``PytestWarning`` subclasses to make it easier to filter warnings based on the class, rather than on the message. The new subclasses are: + + + * ``PytestAssertRewriteWarning`` + + * ``PytestCacheWarning`` + + * ``PytestCollectionWarning`` + + * ``PytestConfigWarning`` + + * ``PytestUnhandledCoroutineWarning`` + + * ``PytestUnknownMarkWarning`` + + +- `#5202 `_: New ``record_testsuite_property`` session-scoped fixture allows users to log ```` tags at the ``testsuite`` + level with the ``junitxml`` plugin. + + The generated XML is compatible with the latest xunit standard, contrary to + the properties recorded by ``record_property`` and ``record_xml_attribute``. + + +- `#5214 `_: The default logging format has been changed to improve readability. Here is an + example of a previous logging message:: + + test_log_cli_enabled_disabled.py 3 CRITICAL critical message logged by test + + This has now become:: + + CRITICAL root:test_log_cli_enabled_disabled.py:3 critical message logged by test + + The formatting can be changed through the `log_format `__ configuration option. + + +- `#5220 `_: ``--fixtures`` now also shows fixture scope for scopes other than ``"function"``. + + + +Bug Fixes +--------- + +- `#5113 `_: Deselected items from plugins using ``pytest_collect_modifyitems`` as a hookwrapper are correctly reported now. + + +- `#5144 `_: With usage errors ``exitstatus`` is set to ``EXIT_USAGEERROR`` in the ``pytest_sessionfinish`` hook now as expected. + + +- `#5235 `_: ``outcome.exit`` is not used with ``EOF`` in the pdb wrapper anymore, but only with ``quit``. + + + +Improved Documentation +---------------------- + +- `#4935 `_: Expand docs on registering marks and the effect of ``--strict``. + + + +Trivial/Internal Changes +------------------------ + +- `#4942 `_: ``logging.raiseExceptions`` is not set to ``False`` anymore. + + +- `#5013 `_: pytest now depends on `wcwidth `__ to properly track unicode character sizes for more precise terminal output. + + +- `#5059 `_: pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``). + + +- `#5069 `_: The code for the short test summary in the terminal was moved to the terminal plugin. + + +- `#5082 `_: Improved validation of kwargs for various methods in the pytester plugin. + + +- `#5202 `_: ``record_property`` now emits a ``PytestWarning`` when used with ``junit_family=xunit2``: the fixture generates + ``property`` tags as children of ``testcase``, which is not permitted according to the most + `recent schema `__. + + +- `#5239 `_: Pin ``pluggy`` to ``< 1.0`` so we don't update to ``1.0`` automatically when + it gets released: there are planned breaking changes, and we want to ensure + pytest properly supports ``pluggy 1.0``. + + pytest 4.4.2 (2019-05-08) ========================= diff --git a/changelog/4826.feature.rst b/changelog/4826.feature.rst deleted file mode 100644 index 2afcba1ad..000000000 --- a/changelog/4826.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -A warning is now emitted when unknown marks are used as a decorator. -This is often due to a typo, which can lead to silently broken tests. diff --git a/changelog/4907.feature.rst b/changelog/4907.feature.rst deleted file mode 100644 index 48bece401..000000000 --- a/changelog/4907.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Show XFail reason as part of JUnitXML message field. diff --git a/changelog/4935.doc.rst b/changelog/4935.doc.rst deleted file mode 100644 index ac948b568..000000000 --- a/changelog/4935.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Expand docs on registering marks and the effect of ``--strict``. diff --git a/changelog/4942.trivial.rst b/changelog/4942.trivial.rst deleted file mode 100644 index 87dba6b8c..000000000 --- a/changelog/4942.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``logging.raiseExceptions`` is not set to ``False`` anymore. diff --git a/changelog/5013.feature.rst b/changelog/5013.feature.rst deleted file mode 100644 index 08f82efeb..000000000 --- a/changelog/5013.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Messages from crash reports are displayed within test summaries now, truncated to the terminal width. diff --git a/changelog/5013.trivial.rst b/changelog/5013.trivial.rst deleted file mode 100644 index fff4eaf3f..000000000 --- a/changelog/5013.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -pytest now depends on `wcwidth `__ to properly track unicode character sizes for more precise terminal output. diff --git a/changelog/5023.feature.rst b/changelog/5023.feature.rst deleted file mode 100644 index 348e2f1c3..000000000 --- a/changelog/5023.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -New flag ``--strict-markers`` that triggers an error when unknown markers (e.g. those not registered using the `markers option`_ in the configuration file) are used in the test suite. - -The existing ``--strict`` option has the same behavior currently, but can be augmented in the future for additional checks. - -.. _`markers option`: https://docs.pytest.org/en/latest/reference.html#confval-markers diff --git a/changelog/5026.feature.rst b/changelog/5026.feature.rst deleted file mode 100644 index aa0f3cbb3..000000000 --- a/changelog/5026.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Assertion failure messages for sequences and dicts contain the number of different items now. diff --git a/changelog/5034.feature.rst b/changelog/5034.feature.rst deleted file mode 100644 index 6ae2def3f..000000000 --- a/changelog/5034.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Improve reporting with ``--lf`` and ``--ff`` (run-last-failure). diff --git a/changelog/5035.feature.rst b/changelog/5035.feature.rst deleted file mode 100644 index 36211f9f4..000000000 --- a/changelog/5035.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The ``--cache-show`` option/action accepts an optional glob to show only matching cache entries. diff --git a/changelog/5059.feature.rst b/changelog/5059.feature.rst deleted file mode 100644 index 4d5d14061..000000000 --- a/changelog/5059.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``. diff --git a/changelog/5059.trivial.rst b/changelog/5059.trivial.rst deleted file mode 100644 index bd8035669..000000000 --- a/changelog/5059.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``). diff --git a/changelog/5068.feature.rst b/changelog/5068.feature.rst deleted file mode 100644 index bceebffc1..000000000 --- a/changelog/5068.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The ``-r`` option learnt about ``A`` to display all reports (including passed ones) in the short test summary. diff --git a/changelog/5069.trivial.rst b/changelog/5069.trivial.rst deleted file mode 100644 index dd6abd8b8..000000000 --- a/changelog/5069.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -The code for the short test summary in the terminal was moved to the terminal plugin. diff --git a/changelog/5082.trivial.rst b/changelog/5082.trivial.rst deleted file mode 100644 index edd23a28f..000000000 --- a/changelog/5082.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Improved validation of kwargs for various methods in the pytester plugin. diff --git a/changelog/5108.feature.rst b/changelog/5108.feature.rst deleted file mode 100644 index 3b66ce5bf..000000000 --- a/changelog/5108.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The short test summary is displayed after passes with output (``-rP``). diff --git a/changelog/5113.bugfix.rst b/changelog/5113.bugfix.rst deleted file mode 100644 index 713b48967..000000000 --- a/changelog/5113.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Deselected items from plugins using ``pytest_collect_modifyitems`` as a hookwrapper are correctly reported now. diff --git a/changelog/5144.bugfix.rst b/changelog/5144.bugfix.rst deleted file mode 100644 index c8c270288..000000000 --- a/changelog/5144.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -With usage errors ``exitstatus`` is set to ``EXIT_USAGEERROR`` in the ``pytest_sessionfinish`` hook now as expected. diff --git a/changelog/5172.feature.rst b/changelog/5172.feature.rst deleted file mode 100644 index 85b55f922..000000000 --- a/changelog/5172.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``--last-failed`` (``--lf``) option got smarter and will now skip entire files if all tests -of that test file have passed in previous runs, greatly speeding up collection. diff --git a/changelog/5177.feature.rst b/changelog/5177.feature.rst deleted file mode 100644 index a5b4ab111..000000000 --- a/changelog/5177.feature.rst +++ /dev/null @@ -1,14 +0,0 @@ -Introduce new specific warning ``PytestWarning`` subclasses to make it easier to filter warnings based on the class, rather than on the message. The new subclasses are: - - -* ``PytestAssertRewriteWarning`` - -* ``PytestCacheWarning`` - -* ``PytestCollectionWarning`` - -* ``PytestConfigWarning`` - -* ``PytestUnhandledCoroutineWarning`` - -* ``PytestUnknownMarkWarning`` diff --git a/changelog/5202.feature.rst b/changelog/5202.feature.rst deleted file mode 100644 index 82b718d9c..000000000 --- a/changelog/5202.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -New ``record_testsuite_property`` session-scoped fixture allows users to log ```` tags at the ``testsuite`` -level with the ``junitxml`` plugin. - -The generated XML is compatible with the latest xunit standard, contrary to -the properties recorded by ``record_property`` and ``record_xml_attribute``. diff --git a/changelog/5202.trivial.rst b/changelog/5202.trivial.rst deleted file mode 100644 index 2eaaf0ca4..000000000 --- a/changelog/5202.trivial.rst +++ /dev/null @@ -1,4 +0,0 @@ -``record_property`` now emits a ``PytestWarning`` when used with ``junit_family=xunit2``: the fixture generates -``property`` tags as children of ``testcase``, which is not permitted according to the most -`recent schema `__. diff --git a/changelog/5214.feature.rst b/changelog/5214.feature.rst deleted file mode 100644 index 422a4dd85..000000000 --- a/changelog/5214.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -The default logging format has been changed to improve readability. Here is an -example of a previous logging message:: - - test_log_cli_enabled_disabled.py 3 CRITICAL critical message logged by test - -This has now become:: - - CRITICAL root:test_log_cli_enabled_disabled.py:3 critical message logged by test - -The formatting can be changed through the `log_format `__ configuration option. diff --git a/changelog/5220.feature.rst b/changelog/5220.feature.rst deleted file mode 100644 index cf535afa0..000000000 --- a/changelog/5220.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``--fixtures`` now also shows fixture scope for scopes other than ``"function"``. diff --git a/changelog/5235.bugfix.rst b/changelog/5235.bugfix.rst deleted file mode 100644 index 87597a589..000000000 --- a/changelog/5235.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``outcome.exit`` is not used with ``EOF`` in the pdb wrapper anymore, but only with ``quit``. diff --git a/changelog/5239.trivial.rst b/changelog/5239.trivial.rst deleted file mode 100644 index 5bd7389f3..000000000 --- a/changelog/5239.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -Pin ``pluggy`` to ``< 1.0`` so we don't update to ``1.0`` automatically when -it gets released: there are planned breaking changes, and we want to ensure -pytest properly supports ``pluggy 1.0``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 179e40abe..8b5ca7b0a 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.5.0 release-4.4.2 release-4.4.1 release-4.4.0 diff --git a/doc/en/announce/release-4.5.0.rst b/doc/en/announce/release-4.5.0.rst new file mode 100644 index 000000000..084579ac4 --- /dev/null +++ b/doc/en/announce/release-4.5.0.rst @@ -0,0 +1,35 @@ +pytest-4.5.0 +======================================= + +The pytest team is proud to announce the 4.5.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Floris Bruynooghe +* Pulkit Goyal +* Samuel Searles-Bryant +* Zac Hatfield-Dodds +* Zac-HD + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index c7d6b271f..a4c20695c 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -85,6 +85,20 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a The fixture is callable with ``(name, value)``, with value being automatically xml-encoded + record_testsuite_property [session scope] + Records a new ```` tag as child of the root ````. This is suitable to + writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. + + This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: + + .. code-block:: python + + def test_foo(record_testsuite_property): + record_testsuite_property("ARCH", "PPC") + record_testsuite_property("STORAGE_TYPE", "CEPH") + + ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + caplog Access and control log capturing. diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1c7c10570..140f4b840 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -606,7 +606,7 @@ We can run this: 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 + > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. $REGENDOC_TMPDIR/b/test_error.py:1