diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36ecf79b8..737c6b86b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/ambv/black - rev: 18.9b0 + rev: 19.3b0 hooks: - id: black args: [--safe, --quiet] language_version: python3 - repo: https://github.com/asottile/blacken-docs - rev: v0.3.0 + rev: v0.5.0 hooks: - id: blacken-docs - additional_dependencies: [black==18.9b0] + additional_dependencies: [black==19.3b0] language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 @@ -22,22 +22,22 @@ repos: exclude: _pytest/debugging.py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.0 + rev: 3.7.7 hooks: - id: flake8 language_version: python3 - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/asottile/pyupgrade - rev: v1.11.1 + rev: v1.15.0 hooks: - id: pyupgrade args: [--keep-percent-format] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.2.0 + rev: v1.3.0 hooks: - id: rst-backticks - repo: local diff --git a/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.rst b/CHANGELOG.rst index 533eb9c15..4408c7340 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,24 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.4.1 (2019-04-15) +========================= + +Bug Fixes +--------- + +- `#5031 `_: Environment variables are properly restored when using pytester's ``testdir`` fixture. + + +- `#5039 `_: Fix regression with ``--pdbcls``, which stopped working with local modules in 4.0.0. + + +- `#5092 `_: Produce a warning when unknown keywords are passed to ``pytest.param(...)``. + + +- `#5098 `_: Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used. + + pytest 4.4.0 (2019-03-29) ========================= diff --git a/bench/bench_argcomplete.py b/bench/bench_argcomplete.py index 2b30add08..335733df7 100644 --- a/bench/bench_argcomplete.py +++ b/bench/bench_argcomplete.py @@ -16,4 +16,4 @@ run = 'fc("/d")' if __name__ == "__main__": print(timeit.timeit(run, setup=setup % imports[0], number=count)) - print((timeit.timeit(run, setup=setup % imports[1], number=count))) + print(timeit.timeit(run, setup=setup % imports[1], number=count)) diff --git a/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/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/changelog/5031.bugfix.rst b/changelog/5031.bugfix.rst deleted file mode 100644 index 6ad80b1e3..000000000 --- a/changelog/5031.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Environment variables are properly restored when using pytester's ``testdir`` fixture. diff --git a/changelog/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/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/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/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/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. 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. 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/doc/en/announce/index.rst b/doc/en/announce/index.rst index 7e2554656..c467535bd 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.4.1 release-4.4.0 release-4.3.1 release-4.3.0 diff --git a/doc/en/announce/release-4.4.1.rst b/doc/en/announce/release-4.4.1.rst new file mode 100644 index 000000000..12c0ee779 --- /dev/null +++ b/doc/en/announce/release-4.4.1.rst @@ -0,0 +1,20 @@ +pytest-4.4.1 +======================================= + +pytest 4.4.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 9b26308c6..dec3cd941 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -12,12 +12,15 @@ Asserting with the ``assert`` statement ``pytest`` allows you to use the standard python ``assert`` for verifying expectations and values in Python tests. For example, you can write the -following:: +following: + +.. code-block:: python # content of test_assert1.py def f(): return 3 + def test_function(): assert f() == 4 @@ -30,7 +33,7 @@ you will see the return value of the function call: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert1.py F [100%] @@ -43,7 +46,7 @@ you will see the return value of the function call: E assert 3 == 4 E + where 3 = f() - test_assert1.py:5: AssertionError + test_assert1.py:6: AssertionError ========================= 1 failed in 0.12 seconds ========================= ``pytest`` has support for showing the values of the most common subexpressions @@ -52,7 +55,9 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the idiomatic python constructs without boilerplate code while not losing introspection information. -However, if you specify a message with the assertion like this:: +However, if you specify a message with the assertion like this: + +.. code-block:: python assert a % 2 == 0, "value was odd, should be even" @@ -67,22 +72,29 @@ Assertions about expected exceptions ------------------------------------------ In order to write assertions about raised exceptions, you can use -``pytest.raises`` as a context manager like this:: +``pytest.raises`` as a context manager like this: + +.. code-block:: python import pytest + def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0 -and if you need to have access to the actual exception info you may use:: +and if you need to have access to the actual exception info you may use: + +.. code-block:: python def test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: + def f(): f() + f() - assert 'maximum recursion' in str(excinfo.value) + assert "maximum recursion" in str(excinfo.value) ``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are @@ -90,15 +102,19 @@ the actual exception raised. The main attributes of interest are You can pass a ``match`` keyword parameter to the context-manager to test that a regular expression matches on the string representation of an exception -(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``):: +(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``): + +.. code-block:: python import pytest + def myfunc(): raise ValueError("Exception 123 raised") + def test_match(): - with pytest.raises(ValueError, match=r'.* 123 .*'): + with pytest.raises(ValueError, match=r".* 123 .*"): myfunc() The regexp parameter of the ``match`` method is matched with the ``re.search`` @@ -107,7 +123,9 @@ well. There's an alternate form of the ``pytest.raises`` function where you pass a function that will be executed with the given ``*args`` and ``**kwargs`` and -assert that the given exception is raised:: +assert that the given exception is raised: + +.. code-block:: python pytest.raises(ExpectedException, func, *args, **kwargs) @@ -116,7 +134,9 @@ exception* or *wrong exception*. Note that it is also possible to specify a "raises" argument to ``pytest.mark.xfail``, which checks that the test is failing in a more -specific way than just having any exception raised:: +specific way than just having any exception raised: + +.. code-block:: python @pytest.mark.xfail(raises=IndexError) def test_f(): @@ -148,10 +168,13 @@ Making use of context-sensitive comparisons .. versionadded:: 2.0 ``pytest`` has rich support for providing context-sensitive information -when it encounters comparisons. For example:: +when it encounters comparisons. For example: + +.. code-block:: python # content of test_assert2.py + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -165,7 +188,7 @@ if you run this module: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_assert2.py F [100%] @@ -184,7 +207,7 @@ if you run this module: E '5' E Use -v to get the full diff - test_assert2.py:5: AssertionError + test_assert2.py:6: AssertionError ========================= 1 failed in 0.12 seconds ========================= Special comparisons are done for a number of cases: @@ -205,16 +228,21 @@ the ``pytest_assertrepr_compare`` hook. :noindex: As an example consider adding the following hook in a :ref:`conftest.py ` -file which provides an alternative explanation for ``Foo`` objects:: +file which provides an alternative explanation for ``Foo`` objects: + +.. code-block:: python # content of conftest.py from test_foocompare import Foo + + def pytest_assertrepr_compare(op, left, right): if isinstance(left, Foo) and isinstance(right, Foo) and op == "==": - return ['Comparing Foo instances:', - ' vals: %s != %s' % (left.val, right.val)] + return ["Comparing Foo instances:", " vals: %s != %s" % (left.val, right.val)] -now, given this test module:: +now, given this test module: + +.. code-block:: python # content of test_foocompare.py class Foo(object): @@ -224,6 +252,7 @@ now, given this test module:: def __eq__(self, other): return self.val == other.val + def test_compare(): f1 = Foo(1) f2 = Foo(2) @@ -246,7 +275,7 @@ the conftest file: E assert Comparing Foo instances: E vals: 1 != 2 - test_foocompare.py:11: AssertionError + test_foocompare.py:12: AssertionError 1 failed in 0.12 seconds .. _assert-details: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 8baf88113..a5104f1b0 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -82,7 +82,7 @@ If you then run it with ``--lf``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 50 items / 48 deselected / 2 selected run-last-failure: rerun previous 2 failures @@ -126,7 +126,7 @@ of ``FF`` and dots): =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 50 items run-last-failure: rerun previous 2 failures first @@ -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: @@ -258,9 +258,9 @@ You can always peek at the content of the cache using the =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache - ------------------------------- cache values ------------------------------- + --------------------------- cache 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/doc/en/capture.rst b/doc/en/capture.rst index f0652aa26..8629350a5 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -71,7 +71,7 @@ of the failing function and hide the other one: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .F [100%] diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index db7eb14a4..549ebb00f 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -72,7 +72,7 @@ then you can just invoke ``pytest`` without command line options: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 1 item mymodule.py . [100%] diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index d207ef7e2..6275bbae0 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -9,18 +9,28 @@ Here are some example using the :ref:`mark` mechanism. Marking test functions and selecting them for a run ---------------------------------------------------- -You can "mark" a test function with custom metadata like this:: +You can "mark" a test function with custom metadata like this: + +.. code-block:: python # content of test_server.py import pytest + + @pytest.mark.webtest def test_send_http(): - pass # perform some webtest test for your app + pass # perform some webtest test for your app + + def test_something_quick(): pass + + def test_another(): pass + + class TestClass(object): def test_method(self): pass @@ -35,7 +45,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -50,7 +60,7 @@ Or the inverse, running all tests except the webtest ones: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -72,7 +82,7 @@ tests based on their module, class, method, or function name: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -87,7 +97,7 @@ You can also select on the class: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item test_server.py::TestClass::test_method PASSED [100%] @@ -102,7 +112,7 @@ Or select multiple nodes: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_server.py::TestClass::test_method PASSED [ 50%] @@ -142,7 +152,7 @@ select tests based on their names: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected test_server.py::test_send_http PASSED [100%] @@ -157,7 +167,7 @@ And you can also run all tests except the ones that match the keyword: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected test_server.py::test_something_quick PASSED [ 33%] @@ -174,7 +184,7 @@ Or to select "http" and "quick" tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 2 deselected / 2 selected test_server.py::test_send_http PASSED [ 50%] @@ -257,14 +267,19 @@ Marking whole classes or modules ---------------------------------------------------- You may use ``pytest.mark`` decorators with classes to apply markers to all of -its test methods:: +its test methods: + +.. code-block:: python # content of test_mark_classlevel.py import pytest + + @pytest.mark.webtest class TestClass(object): def test_startup(self): pass + def test_startup_and_more(self): pass @@ -272,17 +287,23 @@ This is equivalent to directly applying the decorator to the two test functions. To remain backward-compatible with Python 2.4 you can also set a -``pytestmark`` attribute on a TestClass like this:: +``pytestmark`` attribute on a TestClass like this: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = pytest.mark.webtest -or if you need to use multiple markers you can use a list:: +or if you need to use multiple markers you can use a list: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] @@ -305,18 +326,19 @@ Marking individual tests when using parametrize When using parametrize, applying a mark will make it apply to each individual test. However it is also possible to -apply a marker to an individual test instance:: +apply a marker to an individual test instance: + +.. code-block:: python import pytest + @pytest.mark.foo - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - pytest.param((1, 3), marks=pytest.mark.bar), - (2, 3), - ]) + @pytest.mark.parametrize( + ("n", "expected"), [(1, 2), pytest.param((1, 3), marks=pytest.mark.bar), (2, 3)] + ) def test_increment(n, expected): - assert n + 1 == expected + assert n + 1 == expected In this example the mark "foo" will apply to each of the three tests, whereas the "bar" mark is only applied to the second test. @@ -332,31 +354,46 @@ Custom marker and command line option to control test runs Plugins can provide custom markers and implement specific behaviour based on it. This is a self-contained example which adds a command line option and a parametrized test function marker to run tests -specifies via named environments:: +specifies via named environments: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_addoption(parser): - parser.addoption("-E", action="store", metavar="NAME", - help="only run tests matching the environment NAME.") + parser.addoption( + "-E", + action="store", + metavar="NAME", + help="only run tests matching the environment NAME.", + ) + def pytest_configure(config): # register an additional marker - config.addinivalue_line("markers", - "env(name): mark test to run only on named environment") + config.addinivalue_line( + "markers", "env(name): mark test to run only on named environment" + ) + def pytest_runtest_setup(item): - envnames = [mark.args[0] for mark in item.iter_markers(name='env')] + envnames = [mark.args[0] for mark in item.iter_markers(name="env")] if envnames: if item.config.getoption("-E") not in envnames: pytest.skip("test requires env in %r" % envnames) -A test file using this local plugin:: +A test file using this local plugin: + +.. code-block:: python # content of test_someenv.py import pytest + + @pytest.mark.env("stage1") def test_basic_db_operation(): pass @@ -370,7 +407,7 @@ the test needs: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py s [100%] @@ -385,7 +422,7 @@ and here is one that specifies exactly the environment needed: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_someenv.py . [100%] @@ -423,25 +460,32 @@ Passing a callable to custom markers .. regendoc:wipe -Below is the config file that will be used in the next examples:: +Below is the config file that will be used in the next examples: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for marker in item.iter_markers(name='my_marker'): + for marker in item.iter_markers(name="my_marker"): print(marker) sys.stdout.flush() A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time. -However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue:: +However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue: + +.. code-block:: python # content of test_custom_marker.py import pytest + def hello_world(*args, **kwargs): - return 'Hello World' + return "Hello World" + @pytest.mark.my_marker.with_args(hello_world) def test_with_args(): @@ -467,12 +511,16 @@ Reading markers which were set from multiple places .. regendoc:wipe If you are heavily using markers in your test suite you may encounter the case where a marker is applied several times to a test function. From plugin -code you can read over all such settings. Example:: +code you can read over all such settings. Example: + +.. code-block:: python # content of test_mark_three_times.py import pytest + pytestmark = pytest.mark.glob("module", x=1) + @pytest.mark.glob("class", x=2) class TestClass(object): @pytest.mark.glob("function", x=3) @@ -480,13 +528,16 @@ code you can read over all such settings. Example:: pass Here we have the marker "glob" applied three times to the same -test function. From a conftest file we can read it like this:: +test function. From a conftest file we can read it like this: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for mark in item.iter_markers(name='glob'): + for mark in item.iter_markers(name="glob"): print("glob args=%s kwargs=%s" % (mark.args, mark.kwargs)) sys.stdout.flush() @@ -510,7 +561,9 @@ Consider you have a test suite which marks tests for particular platforms, namely ``pytest.mark.darwin``, ``pytest.mark.win32`` etc. and you also have tests that run on all platforms and have no specific marker. If you now want to have a way to only run the tests -for your particular platform, you could use the following plugin:: +for your particular platform, you could use the following plugin: + +.. code-block:: python # content of conftest.py # @@ -519,6 +572,7 @@ for your particular platform, you could use the following plugin:: ALL = set("darwin linux win32".split()) + def pytest_runtest_setup(item): supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform @@ -526,24 +580,30 @@ for your particular platform, you could use the following plugin:: pytest.skip("cannot run on platform %s" % (plat)) then tests will be skipped if they were specified for a different platform. -Let's do a little test file to show how this looks like:: +Let's do a little test file to show how this looks like: + +.. code-block:: python # content of test_plat.py import pytest + @pytest.mark.darwin def test_if_apple_is_evil(): pass + @pytest.mark.linux def test_if_linux_works(): pass + @pytest.mark.win32 def test_if_win32_crashes(): pass + def test_runs_everywhere(): pass @@ -555,12 +615,12 @@ then you will see two tests skipped and two executed tests as expected: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] /home/sweet/project/conftest.py:12: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux =================== 2 passed, 2 skipped in 0.12 seconds ==================== @@ -572,7 +632,7 @@ Note that if you specify a platform via the marker-command line option like this =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 3 deselected / 1 selected test_plat.py . [100%] @@ -589,28 +649,38 @@ Automatically adding markers based on test names If you a test suite where test function names indicate a certain type of test, you can implement a hook that automatically defines markers so that you can use the ``-m`` option with it. Let's look -at this test module:: +at this test module: + +.. code-block:: python # content of test_module.py + def test_interface_simple(): assert 0 + def test_interface_complex(): assert 0 + def test_event_simple(): assert 0 + def test_something_else(): assert 0 We want to dynamically define two markers and can do it in a -``conftest.py`` plugin:: +``conftest.py`` plugin: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_collection_modifyitems(items): for item in items: if "interface" in item.nodeid: @@ -626,18 +696,18 @@ We can now use the ``-m option`` to select one set: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 2 deselected / 2 selected test_module.py FF [100%] ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ - test_module.py:3: in test_interface_simple + test_module.py:4: in test_interface_simple assert 0 E assert 0 __________________________ test_interface_complex __________________________ - test_module.py:6: in test_interface_complex + test_module.py:8: in test_interface_complex assert 0 E assert 0 ================== 2 failed, 2 deselected in 0.12 seconds ================== @@ -650,22 +720,22 @@ or to select both "event" and "interface" tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items / 1 deselected / 3 selected test_module.py FFF [100%] ================================= FAILURES ================================= __________________________ test_interface_simple ___________________________ - test_module.py:3: in test_interface_simple + test_module.py:4: in test_interface_simple assert 0 E assert 0 __________________________ test_interface_complex __________________________ - test_module.py:6: in test_interface_complex + test_module.py:8: in test_interface_complex assert 0 E assert 0 ____________________________ test_event_simple _____________________________ - test_module.py:9: in test_event_simple + test_module.py:12: in test_event_simple assert 0 E assert 0 ================== 3 failed, 1 deselected in 0.12 seconds ================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index f6d1e578e..0910071c1 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -31,7 +31,7 @@ now execute the test specification: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/nonpython + rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items test_simple.yml F. [100%] @@ -66,7 +66,7 @@ consulted when reporting in ``verbose`` mode: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/nonpython + rootdir: $REGENDOC_TMPDIR/nonpython collecting ... collected 2 items test_simple.yml::hello FAILED [ 50%] @@ -90,9 +90,9 @@ interesting to just look at the collection tree: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/nonpython + rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items - + diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1b9f657fd..e7dbadf2d 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -146,7 +146,7 @@ objects, they are still using the default pytest representation: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 8 items @@ -205,7 +205,7 @@ this is a fully self-contained example which you can run with: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items test_scenarios.py .... [100%] @@ -220,7 +220,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items @@ -287,7 +287,7 @@ Let's first see how it looks like at collection time: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items @@ -353,7 +353,7 @@ The result of this test will be successful: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item @@ -434,9 +434,9 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ......sss......ssssssssssss [100%] + ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIPPED [15] /home/sweet/project/CWD/multipython.py:30: 'python3.5' not found + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports @@ -488,12 +488,12 @@ If you run this with reporting for skips enabled: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] /home/sweet/project/conftest.py:11: could not import 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' =================== 1 passed, 1 skipped in 0.12 seconds ==================== @@ -515,21 +515,25 @@ Set marks or test ID for individual parametrized test -------------------------------------------------------------------- Use ``pytest.param`` to apply marks or set test ID to individual parametrized test. -For example:: +For example: + +.. code-block:: python # content of test_pytest_param_example.py import pytest - @pytest.mark.parametrize('test_input,expected', [ - ('3+5', 8), - pytest.param('1+7', 8, - marks=pytest.mark.basic), - pytest.param('2+4', 6, - marks=pytest.mark.basic, - id='basic_2+4'), - pytest.param('6*9', 42, - marks=[pytest.mark.basic, pytest.mark.xfail], - id='basic_6*9'), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", + [ + ("3+5", 8), + pytest.param("1+7", 8, marks=pytest.mark.basic), + pytest.param("2+4", 6, marks=pytest.mark.basic, id="basic_2+4"), + pytest.param( + "6*9", 42, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9" + ), + ], + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -546,7 +550,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 17 items / 14 deselected / 3 selected test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%] diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 40eb333b1..02c12f6bc 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -148,7 +148,7 @@ The test collection would look like this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items @@ -210,7 +210,7 @@ You can always peek at the collection tree without running tests like this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 3 items @@ -285,7 +285,7 @@ file will be left out: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2a296dc10..26c6e1b9e 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -1,13 +1,9 @@ - .. _`tbreportdemo`: Demo of Python failure reports with pytest -================================================== +========================================== -Here is a nice run of several tens of failures -and how ``pytest`` presents things (unfortunately -not showing the nice colors here in the HTML that you -get on the terminal - we are working on that): +Here is a nice run of several failures and how ``pytest`` presents things: .. code-block:: pytest @@ -15,7 +11,7 @@ get on the terminal - we are working on that): =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/assertion + rootdir: $REGENDOC_TMPDIR/assertion collected 44 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] @@ -475,7 +471,7 @@ get on the terminal - we are working on that): > assert 1 == 0 E AssertionError - <0-codegen 'abc-123' /home/sweet/project/assertion/failure_demo.py:201>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:201>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a909cd74f..e9fe1f249 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -129,7 +129,7 @@ directory with the above conftest.py: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -190,7 +190,7 @@ and when running it will see a skipped "slow" test: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .s [100%] @@ -207,7 +207,7 @@ Or run it including the ``slow`` marked test: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py .. [100%] @@ -351,7 +351,7 @@ which will add the string to the test header accordingly: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache project deps: mylib-1.1 - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -381,7 +381,7 @@ which will add info only when run with "--v": cachedir: $PYTHON_PREFIX/.pytest_cache info1: did you know that ... did you? - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -394,7 +394,7 @@ and nothing when run plainly: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 0 items ======================= no tests ran in 0.12 seconds ======================= @@ -434,7 +434,7 @@ Now we can profile which test functions execute the slowest: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_some_are_slow.py ... [100%] @@ -509,7 +509,7 @@ If we run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 4 items test_step.py .Fx. [100%] @@ -593,7 +593,7 @@ We can run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 7 items test_step.py .Fx. [ 57%] @@ -603,13 +603,13 @@ We can run this: ================================== ERRORS ================================== _______________________ ERROR at setup of test_root ________________________ - file /home/sweet/project/b/test_error.py, line 1 + file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. - /home/sweet/project/b/test_error.py:1 + $REGENDOC_TMPDIR/b/test_error.py:1 ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ @@ -707,7 +707,7 @@ and run them: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py FF [100%] @@ -811,7 +811,7 @@ and run it: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_module.py Esetting up a test failed! test_module.py::test_setup_fails diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 1de33054e..6cbec6ddc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -74,7 +74,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_smtpsimple.py F [100%] @@ -217,7 +217,7 @@ inspect what is going on and can now run the tests: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_module.py FF [100%] @@ -710,7 +710,7 @@ Running the above tests results in the following test IDs being used: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 10 items @@ -755,7 +755,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 3 items test_fixture_marks.py::test_data[0] PASSED [ 33%] @@ -800,7 +800,7 @@ Here we declare an ``app`` fixture which receives the previously defined =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] @@ -871,7 +871,7 @@ Let's run the tests in verbose mode and with looking at the print-output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collecting ... collected 8 items test_module.py::test_0[1] SETUP otherarg 1 diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 58b0cfca4..0ba19cbba 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -52,7 +52,7 @@ That’s it. You can now execute the test function: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 9462d700f..4dfec7f5e 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -57,14 +57,16 @@ Applying marks to ``@pytest.mark.parametrize`` parameters .. versionchanged:: 3.1 Prior to version 3.1 the supported mechanism for marking values -used the syntax:: +used the syntax: + +.. code-block:: python import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.mark.xfail(("6*9", 42),), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.mark.xfail(("6*9", 42))] + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -105,9 +107,13 @@ Conditions as strings instead of booleans .. versionchanged:: 2.4 Prior to pytest-2.4 the only way to specify skipif/xfail conditions was -to use strings:: +to use strings: + +.. code-block:: python import sys + + @pytest.mark.skipif("sys.version_info >= (3,3)") def test_function(): ... @@ -139,17 +145,20 @@ dictionary which is constructed as follows: expression is applied. The pytest ``config`` object allows you to skip based on a test -configuration value which you might have added:: +configuration value which you might have added: + +.. code-block:: python @pytest.mark.skipif("not config.getvalue('db')") - def test_function(...): + def test_function(): ... -The equivalent with "boolean conditions" is:: +The equivalent with "boolean conditions" is: - @pytest.mark.skipif(not pytest.config.getvalue("db"), - reason="--db was not specified") - def test_function(...): +.. code-block:: python + + @pytest.mark.skipif(not pytest.config.getvalue("db"), reason="--db was not specified") + def test_function(): pass .. note:: @@ -164,12 +173,16 @@ The equivalent with "boolean conditions" is:: .. versionchanged:: 2.4 -Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``:: +Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``: + +.. code-block:: python import pytest + + def test_function(): ... - pytest.set_trace() # invoke PDB debugger and tracing + pytest.set_trace() # invoke PDB debugger and tracing This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly. diff --git a/doc/en/index.rst b/doc/en/index.rst index 32d10040c..3ace95eff 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -30,7 +30,7 @@ To execute it: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_sample.py F [100%] diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index c9bd09d1a..3a2104317 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -36,15 +36,15 @@ pytest enables test parametrization at several levels: The builtin :ref:`pytest.mark.parametrize ref` decorator enables parametrization of arguments for a test function. Here is a typical example of a test function that implements checking that a certain input leads -to an expected output:: +to an expected output: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) + + + @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -58,7 +58,7 @@ them in turn: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..F [100%] @@ -68,17 +68,13 @@ them in turn: test_input = '6*9', expected = 42 - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) + @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): > assert eval(test_input) == expected E AssertionError: assert 54 == 42 E + where 54 = eval('6*9') - test_expectation.py:8: AssertionError + test_expectation.py:6: AssertionError ==================== 1 failed, 2 passed in 0.12 seconds ==================== .. note:: @@ -104,16 +100,18 @@ Note that you could also use the parametrize marker on a class or a module (see :ref:`mark`) which would invoke several functions with the argument sets. It is also possible to mark individual test instances within parametrize, -for example with the builtin ``mark.xfail``:: +for example with the builtin ``mark.xfail``: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.param("6*9", 42, - marks=pytest.mark.xfail), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", + [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -125,7 +123,7 @@ Let's run this: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 3 items test_expectation.py ..x [100%] @@ -140,9 +138,13 @@ example, if they're dynamically generated by some function - the behaviour of pytest is defined by the :confval:`empty_parameter_set_mark` option. To get all combinations of multiple parametrized arguments you can stack -``parametrize`` decorators:: +``parametrize`` decorators: + +.. code-block:: python import pytest + + @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): @@ -166,26 +168,36 @@ parametrization. For example, let's say we want to run a test taking string inputs which we want to set via a new ``pytest`` command line option. Let's first write -a simple test accepting a ``stringinput`` fixture function argument:: +a simple test accepting a ``stringinput`` fixture function argument: + +.. code-block:: python # content of test_strings.py + def test_valid_string(stringinput): assert stringinput.isalpha() Now we add a ``conftest.py`` file containing the addition of a -command line option and the parametrization of our test function:: +command line option and the parametrization of our test function: + +.. code-block:: python # content of conftest.py + def pytest_addoption(parser): - parser.addoption("--stringinput", action="append", default=[], - help="list of stringinputs to pass to test functions") + parser.addoption( + "--stringinput", + action="append", + default=[], + help="list of stringinputs to pass to test functions", + ) + def pytest_generate_tests(metafunc): - if 'stringinput' in metafunc.fixturenames: - metafunc.parametrize("stringinput", - metafunc.config.getoption('stringinput')) + if "stringinput" in metafunc.fixturenames: + metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput")) If we now pass two stringinput values, our test will run twice: @@ -212,7 +224,7 @@ Let's also run with a stringinput that will lead to a failing test: E + where False = () E + where = '!'.isalpha - test_strings.py:3: AssertionError + test_strings.py:4: AssertionError 1 failed in 0.12 seconds As expected our test function fails. @@ -226,7 +238,7 @@ list: $ pytest -q -rs test_strings.py s [100%] ========================= short test summary info ========================== - SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:1 + SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 1 skipped in 0.12 seconds Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 69d7940fc..a2b10f70d 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -84,32 +84,44 @@ It is also possible to skip the whole module using If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on an interpreter earlier than Python3.6:: +when run on an interpreter earlier than Python3.6: + +.. code-block:: python import sys - @pytest.mark.skipif(sys.version_info < (3,6), - reason="requires python3.6 or higher") + + + @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_function(): ... If the condition evaluates to ``True`` during collection, the test function will be skipped, with the specified reason appearing in the summary when using ``-rs``. -You can share ``skipif`` markers between modules. Consider this test module:: +You can share ``skipif`` markers between modules. Consider this test module: + +.. code-block:: python # content of test_mymodule.py import mymodule - minversion = pytest.mark.skipif(mymodule.__versioninfo__ < (1,1), - reason="at least mymodule-1.1 required") + + minversion = pytest.mark.skipif( + mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required" + ) + + @minversion def test_function(): ... -You can import the marker and reuse it in another test module:: +You can import the marker and reuse it in another test module: + +.. code-block:: python # test_myothermodule.py from test_mymodule import minversion + @minversion def test_anotherfunction(): ... @@ -128,12 +140,12 @@ so they are supported mainly for backward compatibility reasons. Skip all test functions of a class or module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can use the ``skipif`` marker (as any other marker) on classes:: +You can use the ``skipif`` marker (as any other marker) on classes: - @pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") +.. code-block:: python + + @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") class TestPosixCalls(object): - def test_function(self): "will not be setup or run under 'win32' platform" @@ -269,10 +281,11 @@ You can change the default value of the ``strict`` parameter using the ~~~~~~~~~~~~~~~~~~~~ As with skipif_ you can also mark your expectation of a failure -on a particular platform:: +on a particular platform: - @pytest.mark.xfail(sys.version_info >= (3,6), - reason="python3.6 api changes") +.. code-block:: python + + @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes") def test_function(): ... @@ -335,7 +348,7 @@ Running it with the report-on-xfail option gives this output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project/example + rootdir: $REGENDOC_TMPDIR/example collected 7 items xfail_demo.py xxxxxxx [100%] diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 8fbd1fe59..8583f33b4 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -43,7 +43,7 @@ Running this would result in a passed test except for the last =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_tmp_path.py F [100%] @@ -110,7 +110,7 @@ Running this would result in a passed test except for the last =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_tmpdir.py F [100%] diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 53ccef599..05632aef4 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -130,7 +130,7 @@ the ``self.db`` values in the traceback: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 2 items test_unittest_db.py FF [100%] diff --git a/doc/en/usage.rst b/doc/en/usage.rst index d6850beda..1b5d1fc80 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -204,7 +204,7 @@ Example: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -227,15 +227,16 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] /home/sweet/project/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error FAILED test_example.py::test_fail - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds -The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". +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: @@ -256,7 +258,7 @@ More than one character can be used, so for example to only see failed and skipp =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -280,8 +282,8 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - SKIPPED [1] /home/sweet/project/test_example.py:23: skipping this test - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -292,7 +294,7 @@ captured output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 6 items test_example.py .FEsxX [100%] @@ -320,7 +322,7 @@ captured output: _________________________________ test_ok __________________________________ --------------------------- Captured stdout call --------------------------- ok - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index c47817c9a..26bd2fdb2 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -6,15 +6,19 @@ Warnings Capture .. versionadded:: 3.1 Starting from version ``3.1``, pytest now automatically catches warnings during test execution -and displays them at the end of the session:: +and displays them at the end of the session: + +.. code-block:: python # content of test_show_warnings.py import warnings + def api_v1(): warnings.warn(UserWarning("api v1, should use functions from v2")) return 1 + def test_one(): assert api_v1() == 1 @@ -26,14 +30,14 @@ Running pytest now produces this output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project + rootdir: $REGENDOC_TMPDIR collected 1 item test_show_warnings.py . [100%] ============================= warnings summary ============================= test_show_warnings.py::test_one - /home/sweet/project/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 + $REGENDOC_TMPDIR/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2 warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html @@ -52,14 +56,14 @@ them into errors: def test_one(): > assert api_v1() == 1 - test_show_warnings.py:8: + test_show_warnings.py:10: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def api_v1(): > warnings.warn(UserWarning("api v1, should use functions from v2")) E UserWarning: api v1, should use functions from v2 - test_show_warnings.py:4: UserWarning + test_show_warnings.py:5: UserWarning 1 failed in 0.12 seconds The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. @@ -195,28 +199,36 @@ Ensuring code triggers a deprecation warning You can also call a global helper for checking that a certain function call triggers a ``DeprecationWarning`` or -``PendingDeprecationWarning``:: +``PendingDeprecationWarning``: + +.. code-block:: python import pytest + def test_global(): pytest.deprecated_call(myfunction, 17) By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide them. If you wish to record them in your own code, use the -command ``warnings.simplefilter('always')``:: +command ``warnings.simplefilter('always')``: + +.. code-block:: python import warnings import pytest + def test_deprecation(recwarn): - warnings.simplefilter('always') + warnings.simplefilter("always") warnings.warn("deprecated", DeprecationWarning) assert len(recwarn) == 1 assert recwarn.pop(DeprecationWarning) -You can also use it as a contextmanager:: +You can also use it as a contextmanager: + +.. code-block:: python def test_global(): with pytest.deprecated_call(): @@ -238,11 +250,14 @@ Asserting warnings with the warns function .. versionadded:: 2.8 You can check that code raises a particular warning using ``pytest.warns``, -which works in a similar manner to :ref:`raises `:: +which works in a similar manner to :ref:`raises `: + +.. code-block:: python import warnings import pytest + def test_warning(): with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) @@ -269,7 +284,9 @@ You can also call ``pytest.warns`` on a function or code string:: The function also returns a list of all raised warnings (as ``warnings.WarningMessage`` objects), which you can query for -additional information:: +additional information: + +.. code-block:: python with pytest.warns(RuntimeWarning) as record: warnings.warn("another warning", RuntimeWarning) @@ -297,7 +314,9 @@ You can record raised warnings either using ``pytest.warns`` or with the ``recwarn`` fixture. To record with ``pytest.warns`` without asserting anything about the warnings, -pass ``None`` as the expected warning type:: +pass ``None`` as the expected warning type: + +.. code-block:: python with pytest.warns(None) as record: warnings.warn("user", UserWarning) @@ -307,10 +326,13 @@ pass ``None`` as the expected warning type:: assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" -The ``recwarn`` fixture will record warnings for the whole function:: +The ``recwarn`` fixture will record warnings for the whole function: + +.. code-block:: python import warnings + def test_hello(recwarn): warnings.warn("hello", UserWarning) assert len(recwarn) == 1 @@ -378,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta ============================= warnings summary ============================= test_pytest_warnings.py:1 - /home/sweet/project/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index e717431e6..d1bac4820 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -433,14 +433,14 @@ additionally it is possible to copy examples for an example folder before runnin =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: /home/sweet/project, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items test_example.py .. [100%] ============================= warnings summary ============================= test_example.py::test_plugin - /home/sweet/project/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time + $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html @@ -528,10 +528,13 @@ a :py:class:`Result ` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). -Here is an example definition of a hook wrapper:: +Here is an example definition of a hook wrapper: + +.. code-block:: python import pytest + @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() @@ -636,10 +639,13 @@ if you depend on a plugin that is not installed, validation will fail and the error message will not make much sense to your users. One approach is to defer the hook implementation to a new plugin instead of -declaring the hook functions directly in your plugin module, for example:: +declaring the hook functions directly in your plugin module, for example: + +.. code-block:: python # contents of myplugin.py + class DeferPlugin(object): """Simple plugin to defer pytest-xdist hook functions.""" @@ -647,8 +653,9 @@ declaring the hook functions directly in your plugin module, for example:: """standard xdist hook function. """ + def pytest_configure(config): - if config.pluginmanager.hasplugin('xdist'): + if config.pluginmanager.hasplugin("xdist"): config.pluginmanager.register(DeferPlugin()) This has the added benefit of allowing you to conditionally install hooks diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index ab01c314c..b53646859 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -285,20 +285,30 @@ 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): - explanation += [ - u"Left contains more items, first extra item: %s" - % saferepr(left[len(right)]) - ] - elif len(left) < len(right): - explanation += [ - u"Right contains more items, first extra item: %s" - % saferepr(right[len(left)]) - ] + len_diff = len_left - len_right + + if len_diff: + if len_diff > 0: + dir_with_more = "Left" + extra = saferepr(left[len_right]) + else: + 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 @@ -319,7 +329,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)] @@ -331,15 +343,23 @@ 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) - if extra_left: - explanation.append(u"Left contains more items:") + extra_left = set_left - set_right + 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 more items:") + extra_right = set_right - set_left + 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/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 246b8dfd8..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 @@ -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/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 diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3743d31ff..a5b9e9c86 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -10,31 +10,18 @@ from doctest import UnexpectedException from _pytest import outcomes from _pytest.config import hookimpl +from _pytest.config.exceptions import UsageError def _validate_usepdb_cls(value): + """Validate syntax of --pdbcls option.""" try: modname, classname = value.split(":") except ValueError: raise argparse.ArgumentTypeError( "{!r} is not in the format 'modname:classname'".format(value) ) - - try: - __import__(modname) - mod = sys.modules[modname] - - # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). - parts = classname.split(".") - pdb_cls = getattr(mod, parts[0]) - for part in parts[1:]: - pdb_cls = getattr(pdb_cls, part) - - return pdb_cls - except Exception as exc: - raise argparse.ArgumentTypeError( - "could not get pdb class for {!r}: {}".format(value, exc) - ) + return (modname, classname) def pytest_addoption(parser): @@ -68,9 +55,28 @@ def pytest_addoption(parser): ) +def _import_pdbcls(modname, classname): + try: + __import__(modname) + mod = sys.modules[modname] + + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) + + return pdb_cls + except Exception as exc: + value = ":".join((modname, classname)) + raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc)) + + def pytest_configure(config): pdb_cls = config.getvalue("usepdb_cls") - if not pdb_cls: + if pdb_cls: + pdb_cls = _import_pdbcls(*pdb_cls) + else: pdb_cls = pdb.Pdb if config.getvalue("trace"): @@ -250,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) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 6134ca77b..fa7e89364 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -93,3 +93,9 @@ PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( "pytest.warns() got unexpected keyword arguments: {args!r}.\n" "This will be an error in future versions.", ) + +PYTEST_PARAM_UNKNOWN_KWARGS = UnformattedWarning( + PytestDeprecationWarning, + "pytest.param() got unexpected keyword arguments: {args!r}.\n" + "This will be an error in future versions.", +) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d6bceba02..902904457 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -853,7 +853,9 @@ class FixtureDef(object): exceptions.append(sys.exc_info()) if exceptions: e = exceptions[0] - del exceptions # ensure we don't keep all frames alive because of the traceback + del ( + exceptions + ) # ensure we don't keep all frames alive because of the traceback six.reraise(*e) finally: diff --git a/src/_pytest/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:") 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) 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/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f3602b2d5..4cae97b71 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -10,6 +10,7 @@ from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET +from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail from _pytest.warning_types import UnknownMarkWarning @@ -61,20 +62,25 @@ def get_empty_parameterset_mark(config, argnames, func): class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): @classmethod - def param(cls, *values, **kw): - marks = kw.pop("marks", ()) + def param(cls, *values, **kwargs): + marks = kwargs.pop("marks", ()) if isinstance(marks, MarkDecorator): marks = (marks,) else: assert isinstance(marks, (tuple, list, set)) - id_ = kw.pop("id", None) + id_ = kwargs.pop("id", None) if id_ is not None: if not isinstance(id_, six.string_types): raise TypeError( "Expected id to be a string, got {}: {!r}".format(type(id_), id_) ) id_ = ascii_escaped(id_) + + if kwargs: + warnings.warn( + PYTEST_PARAM_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=3 + ) return cls(values, marks, id_) @classmethod @@ -298,7 +304,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) @@ -306,7 +312,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/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index f6c134664..3e221d3d9 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -271,6 +271,18 @@ class MonkeyPatch(object): # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 fixup_namespace_packages(str(path)) + # A call to syspathinsert() usually means that the caller wants to + # import some dynamically created files, thus with python3 we + # invalidate its import caches. + # This is especially important when any namespace package is in used, + # since then the mtime based FileFinder cache (that gets created in + # this case already) gets not invalidated when writing the new files + # quickly afterwards. + if sys.version_info >= (3, 3): + from importlib import invalidate_caches + + invalidate_caches() + def chdir(self, path): """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 6c2dfb5ae..f57983918 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -97,8 +97,7 @@ def skip(msg="", **kwargs): __tracebackhide__ = True allow_module_level = kwargs.pop("allow_module_level", False) if kwargs: - keys = [k for k in kwargs.keys()] - raise TypeError("unexpected keyword arguments: {}".format(keys)) + raise TypeError("unexpected keyword arguments: {}".format(sorted(kwargs))) raise Skipped(msg=msg, allow_module_level=allow_module_level) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d474df4b9..413b21824 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -76,8 +76,11 @@ def pytest_configure(config): def raise_on_kwargs(kwargs): - if kwargs: - raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs)))) + __tracebackhide__ = True + if kwargs: # pragma: no branch + raise TypeError( + "Unexpected keyword arguments: {}".format(", ".join(sorted(kwargs))) + ) class LsofFdLeakChecker(object): @@ -309,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 @@ -341,6 +345,15 @@ def testdir(request, tmpdir_factory): return Testdir(request, tmpdir_factory) +@pytest.fixture +def _sys_snapshot(): + snappaths = SysPathsSnapshot() + snapmods = SysModulesSnapshot() + yield + snapmods.restore() + snappaths.restore() + + @pytest.fixture def _config_for_test(): from _pytest.config import get_config @@ -473,6 +486,8 @@ class Testdir(object): """ + CLOSE_STDIN = object + class TimeoutExpired(Exception): pass @@ -613,27 +628,10 @@ class Testdir(object): This is undone automatically when this object dies at the end of each test. """ - from pkg_resources import fixup_namespace_packages - if path is None: path = self.tmpdir - dirname = str(path) - sys.path.insert(0, dirname) - fixup_namespace_packages(dirname) - - # a call to syspathinsert() usually means that the caller wants to - # import some dynamically created files, thus with python3 we - # invalidate its import caches - self._possibly_invalidate_import_caches() - - def _possibly_invalidate_import_caches(self): - # invalidate caches if we can (py33 and above) - try: - from importlib import invalidate_caches - except ImportError: - return - invalidate_caches() + self.monkeypatch.syspath_prepend(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" @@ -801,12 +799,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). @@ -846,7 +847,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: @@ -860,7 +860,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() @@ -872,9 +872,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) @@ -1032,7 +1033,14 @@ class Testdir(object): if colitem.name == name: return colitem - def popen(self, cmdargs, stdout, stderr, **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 @@ -1050,10 +1058,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 Testdir.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 Testdir.CLOSE_STDIN: + popen.stdin.close() + elif isinstance(stdin, bytes): + popen.stdin.write(stdin) return popen @@ -1065,6 +1081,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,6 +1092,7 @@ class Testdir(object): __tracebackhide__ = True timeout = kwargs.pop("timeout", None) + stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) raise_on_kwargs(kwargs) cmdargs = [ @@ -1086,8 +1107,14 @@ class Testdir(object): try: now = time.time() popen = self.popen( - cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") + cmdargs, + stdin=stdin, + stdout=f1, + stderr=f2, + close_fds=(sys.platform != "win32"), ) + if isinstance(stdin, bytes): + popen.stdin.close() def handle_timeout(): __tracebackhide__ = True @@ -1173,9 +1200,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 @@ -1185,7 +1213,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. @@ -1317,7 +1345,7 @@ class LineMatcher(object): raise ValueError("line %r not found in output" % fnline) def _log(self, *args): - self._log_output.append(" ".join((str(x) for x in args))) + self._log_output.append(" ".join(str(x) for x in args)) @property def _log_text(self): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1b643d430..66de85468 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -682,7 +682,7 @@ def raises(expected_exception, *args, **kwargs): match_expr = kwargs.pop("match") if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " - msg += ", ".join(kwargs.keys()) + msg += ", ".join(sorted(kwargs)) raise TypeError(msg) return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index d2df4d21f..41fcb9e6d 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). @@ -328,7 +334,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, diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 28a553e18..eadcf6d12 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -4,8 +4,6 @@ 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 @@ -186,174 +184,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 _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 = wcswidth(line) - ellipsis, len_ellipsis = "...", 3 - 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 = 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: - max_len_msg -= len_ellipsis - 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 - - -def show_simple(terminalreporter, lines, stat): - failed = terminalreporter.stats.get(stat) - if failed: - config = terminalreporter.config - termwidth = terminalreporter.writer.fullwidth - for rep in failed: - line = _get_line_with_reprcrash_message(config, rep, termwidth) - lines.append(line) - - -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..af836658b 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 @@ -81,11 +82,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), or (A)ll. " "Warnings are displayed at all times except when " - "--disable-warnings is set", + "--disable-warnings is set.", ) group._addoption( "--disable-warnings", @@ -140,7 +141,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", ) @@ -164,12 +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 not in reportopts and char != "a": - reportopts += char - elif char == "a": - reportopts = "sxXwEf" + for char in reportchars: + if char == "a": + reportopts = "sxXwEf" + elif char == "A": + reportopts = "sxXwEfpP" + break + elif char not in reportopts: + reportopts += char return reportopts @@ -254,7 +257,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,18 +444,18 @@ 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 = ( + 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 @@ -460,7 +466,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))) @@ -677,8 +683,9 @@ class TerminalReporter(object): self.summary_errors() self.summary_failures() self.summary_warnings() - yield self.summary_passes() + yield + self.short_test_summary() # Display any extra warnings from teardown here (if any). self.summary_warnings() @@ -726,10 +733,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: @@ -820,17 +827,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": @@ -842,10 +854,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 - elif rep.when == "teardown": - msg = "ERROR at teardown of " + msg + else: + msg = "ERROR at %s of %s" % (rep.when, msg) self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) @@ -873,6 +883,150 @@ class TerminalReporter(object): if self.verbosity == -1: self.write_line(msg, **markup) + def short_test_summary(self): + if not self.reportchars: + return + + def show_simple(stat, lines): + failed = self.stats.get(stat, []) + if not failed: + return + termwidth = self.writer.fullwidth + config = self.config + for rep in failed: + line = _get_line_with_reprcrash_message(config, rep, termwidth) + lines.append(line) + + def show_xfailed(lines): + xfailed = self.stats.get("xfailed", []) + for rep in xfailed: + verbose_word = rep._get_verbose_word(self.config) + 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", []) + for rep in xpassed: + 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)) + + def show_skipped(lines): + skipped = self.stats.get("skipped", []) + fskips = _folded_skips(skipped) if skipped else [] + if not fskips: + return + verbose_word = skipped[0]._get_verbose_word(self.config) + 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)) + + REPORTCHAR_ACTIONS = { + "x": show_xfailed, + "X": show_xpassed, + "f": partial(show_simple, "failed"), + "F": partial(show_simple, "failed"), + "s": show_skipped, + "S": show_skipped, + "p": partial(show_simple, "passed"), + "E": partial(show_simple, "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 _get_pos(config, rep): + nodeid = config.cwd_relative_nodeid(rep.nodeid) + return nodeid + + +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 = rep._get_verbose_word(config) + pos = _get_pos(config, rep) + + line = "%s %s" % (verbose_word, pos) + len_line = wcswidth(line) + ellipsis, len_ellipsis = "...", 3 + 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 = 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: + max_len_msg -= len_ellipsis + 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 + + +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 build_summary_stats_line(stats): known_types = ( diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 295dd832c..48a64d827 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -485,7 +485,7 @@ class TestGeneralUsage(object): ["*source code not available*", "E*fixture 'invalid_fixture' not found"] ) - def test_plugins_given_as_strings(self, tmpdir, monkeypatch): + def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot): """test that str values passed to main() as `plugins` arg are interpreted as module names to be imported and registered. #855. diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 4e36fb946..5a4ab8808 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -441,7 +441,7 @@ def test_match_raises_error(testdir): class TestFormattedExcinfo(object): @pytest.fixture - def importasmod(self, request): + def importasmod(self, request, _sys_snapshot): def importasmod(source): source = textwrap.dedent(source) tmpdir = request.getfixturevalue("tmpdir") diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 965838dae..aa56273c4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -410,7 +410,7 @@ def test_deindent(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir): +def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = _pytest._code.Source( diff --git a/testing/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/python/fixtures.py b/testing/python/fixtures.py index e262152e4..48f8028e6 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1071,9 +1071,7 @@ class TestFixtureUsages(object): ) result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines( - ( - "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" - ) + "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) def test_funcarg_parametrized_and_used_twice(self, testdir): 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 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): 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"] ) diff --git a/testing/test_config.py b/testing/test_config.py index 8cd606f45..142616716 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -436,7 +436,7 @@ class TestConfigAPI(object): class TestConfigFromdictargs(object): - def test_basic_behavior(self): + def test_basic_behavior(self, _sys_snapshot): from _pytest.config import Config option_dict = {"verbose": 444, "foo": "bar", "capture": "no"} @@ -450,7 +450,7 @@ class TestConfigFromdictargs(object): assert config.option.capture == "no" assert config.args == args - def test_origargs(self): + def test_origargs(self, _sys_snapshot): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config @@ -1057,7 +1057,7 @@ class TestOverrideIniArgs(object): assert rootdir == tmpdir assert inifile is None - def test_addopts_before_initini(self, monkeypatch, _config_for_test): + def test_addopts_before_initini(self, monkeypatch, _config_for_test, _sys_snapshot): cache_dir = ".custom_cache" monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir) config = _config_for_test @@ -1092,7 +1092,7 @@ class TestOverrideIniArgs(object): ) assert result.ret == _pytest.main.EXIT_USAGEERROR - def test_override_ini_does_not_contain_paths(self, _config_for_test): + def test_override_ini_does_not_contain_paths(self, _config_for_test, _sys_snapshot): """Check that -o no longer swallows all options after it (#3103)""" config = _config_for_test config._preparse(["-o", "cache_dir=/cache", "/some/test/path"]) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 1c4be9816..a0458e595 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -13,17 +13,6 @@ from _pytest.main import EXIT_OK from _pytest.main import EXIT_USAGEERROR -@pytest.fixture(scope="module", params=["global", "inpackage"]) -def basedir(request, tmpdir_factory): - tmpdir = tmpdir_factory.mktemp("basedir", numbered=True) - tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3") - tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") - if request.param == "inpackage": - tmpdir.ensure("adir/__init__.py") - tmpdir.ensure("adir/b/__init__.py") - return tmpdir - - def ConftestWithSetinitial(path): conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) @@ -41,7 +30,19 @@ def conftest_setinitial(conftest, args, confcutdir=None): conftest._set_initial_conftests(Namespace()) +@pytest.mark.usefixtures("_sys_snapshot") class TestConftestValueAccessGlobal(object): + @pytest.fixture(scope="module", params=["global", "inpackage"]) + def basedir(self, request, tmpdir_factory): + tmpdir = tmpdir_factory.mktemp("basedir", numbered=True) + tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3") + tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") + if request.param == "inpackage": + tmpdir.ensure("adir/__init__.py") + tmpdir.ensure("adir/b/__init__.py") + + yield tmpdir + def test_basic_init(self, basedir): conftest = PytestPluginManager() p = basedir.join("adir") @@ -49,10 +50,10 @@ class TestConftestValueAccessGlobal(object): def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = PytestPluginManager() - len(conftest._dirpath2confmods) + assert not len(conftest._dirpath2confmods) conftest._getconftestmodules(basedir) snap1 = len(conftest._dirpath2confmods) - # assert len(conftest._dirpath2confmods) == snap1 + 1 + assert snap1 == 1 conftest._getconftestmodules(basedir.join("adir")) assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._getconftestmodules(basedir.join("b")) @@ -80,7 +81,7 @@ class TestConftestValueAccessGlobal(object): assert path.purebasename.startswith("conftest") -def test_conftest_in_nonpkg_with_init(tmpdir): +def test_conftest_in_nonpkg_with_init(tmpdir, _sys_snapshot): tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3") tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5") tmpdir.ensure("adir-1.0/b/__init__.py") 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( """ diff --git a/testing/test_mark.py b/testing/test_mark.py index cb20658b5..72b96ab51 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,6 +13,7 @@ from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector from _pytest.nodes import Node +from _pytest.warning_types import PytestDeprecationWarning from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: @@ -204,7 +205,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( @@ -991,3 +992,15 @@ def test_pytest_param_id_requires_string(): @pytest.mark.parametrize("s", (None, "hello world")) def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) + + +def test_pytest_param_warning_on_unknown_kwargs(): + with pytest.warns(PytestDeprecationWarning) as warninfo: + # typo, should be marks= + pytest.param(1, 2, mark=pytest.mark.xfail()) + assert warninfo[0].filename == __file__ + msg, = warninfo[0].message.args + assert msg == ( + "pytest.param() got unexpected keyword arguments: ['mark'].\n" + "This will be an error in future versions." + ) 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/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index d43fb6bab..9e45e05c8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -462,3 +462,10 @@ def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): import ns_pkg.world assert ns_pkg.world.check() == "world" + + # Should invalidate caches via importlib.invalidate_caches. + tmpdir = testdir.tmpdir + modules_tmpdir = tmpdir.mkdir("modules_tmpdir") + monkeypatch.syspath_prepend(str(modules_tmpdir)) + modules_tmpdir.join("main_app.py").write("app = True") + from main_app import app # noqa: F401 diff --git a/testing/test_pdb.py b/testing/test_pdb.py index dfb405105..cc2ea15d2 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -import argparse import os import platform import sys @@ -804,13 +803,12 @@ class TestPDB(object): ) def test_pdb_validate_usepdb_cls(self, testdir): - assert _validate_usepdb_cls("os.path:dirname.__name__") == "dirname" + assert _validate_usepdb_cls("os.path:dirname.__name__") == ( + "os.path", + "dirname.__name__", + ) - with pytest.raises( - argparse.ArgumentTypeError, - match=r"^could not get pdb class for 'pdb:DoesNotExist': .*'DoesNotExist'", - ): - _validate_usepdb_cls("pdb:DoesNotExist") + assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist") def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): p1 = testdir.makepyfile("""xxx """) @@ -1136,3 +1134,46 @@ def test_pdb_skip_option(testdir): 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( + """ + def test(): + print("before_settrace") + __import__("pdb").set_trace() + """, + mypdb=""" + class Wrapped: + class MyPdb: + def set_trace(self, *args): + print("settrace_called", args) + + def runcall(self, *args, **kwds): + print("runcall_called", args, kwds) + assert "func" in kwds + """, + ) + result = testdir.runpytest( + str(p1), "--pdbcls=really.invalid:Value", syspathinsert=True + ) + result.stderr.fnmatch_lines( + [ + "ERROR: --pdbcls: could not import 'really.invalid:Value': No module named *really*" + ] + ) + assert result.ret == 4 + + result = testdir.runpytest( + str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True + ) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*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 *"]) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 2e4877463..b76d413b7 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,79 @@ 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, time; time.sleep(1); 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 + + +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 diff --git a/testing/test_runner.py b/testing/test_runner.py index cf335dfad..c52d2ea7c 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. diff --git a/testing/test_skipping.py b/testing/test_skipping.py index f0da0488c..4782e7065 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -7,7 +7,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 @@ -750,40 +749,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 9664d9c7f..ee546a4a1 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -16,6 +16,7 @@ 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 getreportopt @@ -774,11 +775,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): @@ -836,14 +845,23 @@ 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" + def test_terminalreporter_reportopt_addopts(testdir): testdir.makeini("[pytest]\naddopts=-rs") @@ -1530,3 +1548,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 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