diff --git a/.travis.yml b/.travis.yml index c1f7ad357..5de40f3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,8 +43,7 @@ jobs: python: 'pypy3' - env: TOXENV=py35-xdist - dist: trusty - python: '3.5.0' + python: '3.5' # Coverage for: # - pytester's LsofFdLeakChecker diff --git a/AUTHORS b/AUTHORS index 6d1a2a816..9f6ee048d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Andras Tim Andrea Cimatoribus Andreas Zeidler Andrey Paramonov +Andrzej Klajnert Andrzej Ostrowski Andy Freeland Anthon van der Neut @@ -55,6 +56,7 @@ Charnjit SiNGH (CCSJ) Chris Lamb Christian Boelsen Christian Fetzer +Christian Neumüller Christian Theunert Christian Tismer Christopher Gilling @@ -96,6 +98,7 @@ Feng Ma Florian Bruhin Floris Bruynooghe Gabriel Reis +Gene Wood George Kussumoto Georgy Dyuldin Graham Horler @@ -210,6 +213,7 @@ Raphael Castaneda Raphael Pierzina Raquel Alegre Ravi Chandra +Robert Holt Roberto Polli Roland Puntaier Romain Dorgueil diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f9637248..4270d4df3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,32 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.1.2 (2019-08-30) +========================= + +Bug Fixes +--------- + +- `#2270 `_: Fixed ``self`` reference in function-scoped fixtures defined plugin classes: previously ``self`` + would be a reference to a *test* class, not the *plugin* class. + + +- `#570 `_: Fixed long standing issue where fixture scope was not respected when indirect fixtures were used during + parametrization. + + +- `#5782 `_: Fix decoding error when printing an error response from ``--pastebin``. + + +- `#5786 `_: Chained exceptions in test and collection reports are now correctly serialized, allowing plugins like + ``pytest-xdist`` to display them properly. + + +- `#5792 `_: Windows: Fix error that occurs in certain circumstances when loading + ``conftest.py`` from a working directory that has casing other than the one stored + in the filesystem (e.g., ``c:\test`` instead of ``C:\test``). + + pytest 5.1.1 (2019-08-20) ========================= diff --git a/changelog/5782.bugfix.rst b/changelog/5782.bugfix.rst deleted file mode 100644 index e961d8fb5..000000000 --- a/changelog/5782.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix decoding error when printing an error response from ``--pastebin``. diff --git a/changelog/5807.bugfix.rst b/changelog/5807.bugfix.rst new file mode 100644 index 000000000..bfa0ffbcf --- /dev/null +++ b/changelog/5807.bugfix.rst @@ -0,0 +1 @@ +Fix pypy3.6 (nightly) on windows. diff --git a/changelog/5811.bugfix.rst b/changelog/5811.bugfix.rst new file mode 100644 index 000000000..a052eb03a --- /dev/null +++ b/changelog/5811.bugfix.rst @@ -0,0 +1 @@ +Handle ``--fulltrace`` correctly with ``pytest.raises``. diff --git a/changelog/5819.bugfix.rst b/changelog/5819.bugfix.rst new file mode 100644 index 000000000..aa953429a --- /dev/null +++ b/changelog/5819.bugfix.rst @@ -0,0 +1,2 @@ +Windows: Fix regression with conftest whose qualified name contains uppercase +characters (introduced by #5792). diff --git a/doc/en/Makefile b/doc/en/Makefile index 341a5cb85..bfeb96c8c 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -16,7 +16,7 @@ REGENDOC_ARGS := \ --normalize "/[ \t]+\n/\n/" \ --normalize "~\$$REGENDOC_TMPDIR~/home/sweet/project~" \ --normalize "~/path/to/example~/home/sweet/project~" \ - --normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \ + --normalize "/in \d+.\d+s ==/in 0.12s ==/" \ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ --normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \ --normalize "@(This is pytest version )(\d+)\\.[^ ,]+@\1\2.x.y@" \ diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 84a41d2bf..81f977ef1 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.1.2 release-5.1.1 release-5.1.0 release-5.0.1 diff --git a/doc/en/announce/release-5.1.2.rst b/doc/en/announce/release-5.1.2.rst new file mode 100644 index 000000000..ac6e00581 --- /dev/null +++ b/doc/en/announce/release-5.1.2.rst @@ -0,0 +1,23 @@ +pytest-5.1.2 +======================================= + +pytest 5.1.2 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: + +* Andrzej Klajnert +* Anthony Sottile +* Bruno Oliveira +* Christian Neumüller +* Robert Holt +* linchiwei123 + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 16de77898..04ee9c0f6 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -47,7 +47,7 @@ you will see the return value of the function call: E + where 3 = f() test_assert1.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= ``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -208,7 +208,7 @@ if you run this module: E Use -v to get the full diff test_assert2.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= Special comparisons are done for a number of cases: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 384be5daf..38e00cc0c 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -75,7 +75,7 @@ If you run this for the first time you will see two failures: E Failed: bad luck test_50.py:7: Failed - 2 failed, 48 passed in 0.08s + 2 failed, 48 passed in 0.07s If you then run it with ``--lf``: @@ -114,7 +114,7 @@ If you then run it with ``--lf``: E Failed: bad luck test_50.py:7: Failed - ===================== 2 failed, 48 deselected in 0.02s ===================== + ===================== 2 failed, 48 deselected in 0.12s ===================== You have run only the two failing tests from the last run, while the 48 passing tests have not been run ("deselected"). @@ -158,7 +158,7 @@ of ``FF`` and dots): E Failed: bad luck test_50.py:7: Failed - ======================= 2 failed, 48 passed in 0.07s ======================= + ======================= 2 failed, 48 passed in 0.12s ======================= .. _`config.cache`: @@ -283,7 +283,7 @@ You can always peek at the content of the cache using the example/value contains: 42 - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== ``--cache-show`` takes an optional argument to specify a glob pattern for filtering: @@ -300,7 +300,7 @@ filtering: example/value contains: 42 - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== Clearing Cache content ---------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 2a9de0be3..3e744e764 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -91,7 +91,7 @@ of the failing function and hide the other one: test_module.py:12: AssertionError -------------------------- Captured stdout setup --------------------------- setting up - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 77217e9d2..3471463a3 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -107,8 +107,8 @@ check for ini-files as follows: # first look for pytest.ini files path/pytest.ini - path/setup.cfg # must also contain [tool:pytest] section to match path/tox.ini # must also contain [pytest] section to match + path/setup.cfg # must also contain [tool:pytest] section to match pytest.ini ... # all the way down to the root diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 7ecfe7e56..9ffd5285a 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -36,7 +36,7 @@ then you can just invoke ``pytest`` directly: test_example.txt . [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= By default, pytest will collect ``test*.txt`` files looking for doctest directives, but you can pass additional globs using the ``--doctest-glob`` option (multi-allowed). @@ -66,7 +66,7 @@ and functions, including from test modules: mymodule.py . [ 50%] test_example.txt . [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= You can make these changes permanent in your project by putting them into a pytest.ini file like this: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index f5acd296f..bb2fbb22a 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -52,7 +52,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: test_server.py::test_send_http PASSED [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== Or the inverse, running all tests except the webtest ones: @@ -69,7 +69,7 @@ Or the inverse, running all tests except the webtest ones: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ===================== 3 passed, 1 deselected in 0.01s ====================== + ===================== 3 passed, 1 deselected in 0.12s ====================== Selecting tests based on their node ID -------------------------------------- @@ -89,7 +89,7 @@ tests based on their module, class, method, or function name: test_server.py::TestClass::test_method PASSED [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= You can also select on the class: @@ -104,7 +104,7 @@ You can also select on the class: test_server.py::TestClass::test_method PASSED [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= Or select multiple nodes: @@ -120,7 +120,7 @@ Or select multiple nodes: test_server.py::TestClass::test_method PASSED [ 50%] test_server.py::test_send_http PASSED [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= .. _node-id: @@ -159,7 +159,7 @@ select tests based on their names: test_server.py::test_send_http PASSED [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== And you can also run all tests except the ones that match the keyword: @@ -176,7 +176,7 @@ And you can also run all tests except the ones that match the keyword: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ===================== 3 passed, 1 deselected in 0.01s ====================== + ===================== 3 passed, 1 deselected in 0.12s ====================== Or to select "http" and "quick" tests: @@ -192,7 +192,7 @@ Or to select "http" and "quick" tests: test_server.py::test_send_http PASSED [ 50%] test_server.py::test_something_quick PASSED [100%] - ===================== 2 passed, 2 deselected in 0.01s ====================== + ===================== 2 passed, 2 deselected in 0.12s ====================== .. note:: @@ -413,7 +413,7 @@ the test needs: test_someenv.py s [100%] - ============================ 1 skipped in 0.00s ============================ + ============================ 1 skipped in 0.12s ============================ and here is one that specifies exactly the environment needed: @@ -428,7 +428,7 @@ and here is one that specifies exactly the environment needed: test_someenv.py . [100%] - ============================ 1 passed in 0.01s ============================= + ============================ 1 passed in 0.12s ============================= The ``--markers`` option always gives you a list of available markers: @@ -499,7 +499,7 @@ The output is as follows: $ pytest -q -s Mark(name='my_marker', args=(,), kwargs={}) . - 1 passed in 0.00s + 1 passed in 0.01s We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. @@ -551,7 +551,7 @@ Let's run this without capturing output and see what we get: glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . - 1 passed in 0.01s + 1 passed in 0.02s marking platform specific tests with pytest -------------------------------------------------------------- @@ -623,7 +623,7 @@ then you will see two tests skipped and two executed tests as expected: ========================= short test summary info ========================== SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - ======================= 2 passed, 2 skipped in 0.01s ======================= + ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -638,7 +638,7 @@ Note that if you specify a platform via the marker-command line option like this test_plat.py . [100%] - ===================== 1 passed, 3 deselected in 0.01s ====================== + ===================== 1 passed, 3 deselected in 0.12s ====================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. @@ -711,7 +711,7 @@ We can now use the ``-m option`` to select one set: test_module.py:8: in test_interface_complex assert 0 E assert 0 - ===================== 2 failed, 2 deselected in 0.02s ====================== + ===================== 2 failed, 2 deselected in 0.12s ====================== or to select both "event" and "interface" tests: @@ -739,4 +739,4 @@ or to select both "event" and "interface" tests: test_module.py:12: in test_event_simple assert 0 E assert 0 - ===================== 3 failed, 1 deselected in 0.03s ====================== + ===================== 3 failed, 1 deselected in 0.12s ====================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 6699de749..28b20800e 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -41,7 +41,7 @@ now execute the test specification: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== .. regendoc:wipe @@ -77,7 +77,7 @@ consulted when reporting in ``verbose`` mode: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======================= 1 failed, 1 passed in 0.02s ======================== + ======================= 1 failed, 1 passed in 0.12s ======================== .. regendoc:wipe @@ -97,4 +97,4 @@ interesting to just look at the collection tree: - ========================== no tests ran in 0.02s =========================== + ========================== no tests ran in 0.12s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index cf99ea472..19cdcc293 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -172,7 +172,7 @@ objects, they are still using the default pytest representation: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -229,7 +229,7 @@ this is a fully self-contained example which you can run with: test_scenarios.py .... [100%] - ============================ 4 passed in 0.01s ============================= + ============================ 4 passed in 0.12s ============================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function: @@ -248,7 +248,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -323,7 +323,7 @@ Let's first see how it looks like at collection time: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== And then when we run the test: @@ -394,7 +394,7 @@ The result of this test will be successful: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. regendoc:wipe @@ -475,10 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss......sss...... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - 12 passed, 15 skipped in 0.62s + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found + 3 passed, 24 skipped in 0.24s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -547,7 +548,7 @@ If you run this with reporting for skips enabled: ========================= short test summary info ========================== SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' - ======================= 1 passed, 1 skipped in 0.01s ======================= + ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: @@ -609,7 +610,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - =============== 2 passed, 15 deselected, 1 xfailed in 0.08s ================ + =============== 2 passed, 15 deselected, 1 xfailed in 0.12s ================ As the result: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index a718de400..d8261a949 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -158,7 +158,7 @@ The test collection would look like this: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== You can check for multiple glob patterns by adding a space between the patterns: @@ -221,7 +221,7 @@ You can always peek at the collection tree without running tests like this: - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. _customizing-test-collection: @@ -297,7 +297,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to ``collect_ignore_glob``. diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index c024b8616..1c06782f6 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -650,4 +650,4 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a failure_demo.py:282: AssertionError - ============================ 44 failed in 0.26s ============================ + ============================ 44 failed in 0.12s ============================ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index fea73f4e9..913769c35 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -132,7 +132,7 @@ directory with the above conftest.py: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. _`excontrolskip`: @@ -201,7 +201,7 @@ and when running it will see a skipped "slow" test: ========================= short test summary info ========================== SKIPPED [1] test_module.py:8: need --runslow option to run - ======================= 1 passed, 1 skipped in 0.01s ======================= + ======================= 1 passed, 1 skipped in 0.12s ======================= Or run it including the ``slow`` marked test: @@ -216,7 +216,7 @@ Or run it including the ``slow`` marked test: test_module.py .. [100%] - ============================ 2 passed in 0.01s ============================= + ============================ 2 passed in 0.12s ============================= Writing well integrated assertion helpers -------------------------------------------------- @@ -358,7 +358,7 @@ which will add the string to the test header accordingly: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== .. regendoc:wipe @@ -388,7 +388,7 @@ which will add info only when run with "--v": rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== and nothing when run plainly: @@ -401,7 +401,7 @@ and nothing when run plainly: rootdir: $REGENDOC_TMPDIR collected 0 items - ========================== no tests ran in 0.00s =========================== + ========================== no tests ran in 0.12s =========================== profiling test duration -------------------------- @@ -447,7 +447,7 @@ Now we can profile which test functions execute the slowest: 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ============================ 3 passed in 0.61s ============================= + ============================ 3 passed in 0.12s ============================= incremental testing - test steps --------------------------------------------------- @@ -531,7 +531,7 @@ If we run this: ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion reason: previous test failed (test_modification) - ================== 1 failed, 2 passed, 1 xfailed in 0.03s ================== + ================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -644,7 +644,7 @@ We can run this: E assert 0 a/test_db2.py:2: AssertionError - ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.05s ============== + ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ============== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -733,7 +733,7 @@ and run them: E assert 0 test_module.py:6: AssertionError - ============================ 2 failed in 0.02s ============================= + ============================ 2 failed in 0.12s ============================= you will have a "failures" file which contains the failing test ids: @@ -848,7 +848,7 @@ and run it: E assert 0 test_module.py:19: AssertionError - ======================== 2 failed, 1 error in 0.02s ======================== + ======================== 2 failed, 1 error in 0.12s ======================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 91b5aca85..3558c79c4 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -96,7 +96,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: E assert 0 test_smtpsimple.py:14: AssertionError - ============================ 1 failed in 0.18s ============================= + ============================ 1 failed in 0.12s ============================= In the failure traceback we see that the test function was called with a ``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -258,7 +258,7 @@ inspect what is going on and can now run the tests: E assert 0 test_module.py:13: AssertionError - ============================ 2 failed in 0.20s ============================= + ============================ 2 failed in 0.12s ============================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp_connection`` object was passed into the @@ -361,7 +361,7 @@ Let's execute it: $ pytest -s -q --tb=no FFteardown smtp - 2 failed in 0.20s + 2 failed in 0.79s We see that the ``smtp_connection`` instance is finalized after the two tests finished execution. Note that if we decorated our fixture @@ -515,7 +515,7 @@ again, nothing much has changed: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - 2 failed in 0.21s + 2 failed in 0.77s Let's quickly create another test module that actually sets the server URL in its module namespace: @@ -692,7 +692,7 @@ So let's just do another run: test_module.py:13: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing - 4 failed in 0.89s + 4 failed in 1.69s We see that our two test functions each ran twice, against the different ``smtp_connection`` instances. Note also, that with the ``mail.python.org`` @@ -771,7 +771,7 @@ Running the above tests results in the following test IDs being used: - ========================== no tests ran in 0.01s =========================== + ========================== no tests ran in 0.12s =========================== .. _`fixture-parametrize-marks`: @@ -812,7 +812,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED [100%] - ======================= 2 passed, 1 skipped in 0.01s ======================= + ======================= 2 passed, 1 skipped in 0.12s ======================= .. _`interdependent fixtures`: @@ -861,7 +861,7 @@ Here we declare an ``app`` fixture which receives the previously defined test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%] - ============================ 2 passed in 0.44s ============================= + ============================ 2 passed in 0.12s ============================= Due to the parametrization of ``smtp_connection``, the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -971,7 +971,7 @@ Let's run the tests in verbose mode and with looking at the print-output: TEARDOWN modarg mod2 - ============================ 8 passed in 0.01s ============================= + ============================ 8 passed in 0.12s ============================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 38a361818..455e93622 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -69,7 +69,7 @@ That’s it. You can now execute the test function: E + where 4 = func(3) test_sample.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= This test returns a failure report because ``func(3)`` does not return ``5``. @@ -108,7 +108,7 @@ Execute the test function with “quiet” reporting mode: $ pytest -q test_sysexit.py . [100%] - 1 passed in 0.00s + 1 passed in 0.01s Group multiple tests in a class -------------------------------------------------------------- diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index 0aa0dc97c..4da9d1bca 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -88,7 +88,7 @@ This has the following benefits: .. note:: - See :ref:`pythonpath` for more information about the difference between calling ``pytest`` and + See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and ``python -m pytest``. Note that using this scheme your test files must have **unique names**, because diff --git a/doc/en/index.rst b/doc/en/index.rst index 65b4631cd..cb66f9dd3 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -44,7 +44,7 @@ To execute it: E + where 4 = inc(3) test_sample.py:6: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 85f233be3..e02ff2e53 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -75,7 +75,7 @@ them in turn: E + where 54 = eval('6*9') test_expectation.py:6: AssertionError - ======================= 1 failed, 2 passed in 0.02s ======================== + ======================= 1 failed, 2 passed in 0.12s ======================== .. note:: @@ -128,7 +128,7 @@ Let's run this: test_expectation.py ..x [100%] - ======================= 2 passed, 1 xfailed in 0.02s ======================= + ======================= 2 passed, 1 xfailed in 0.12s ======================= The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. diff --git a/doc/en/pythonpath.rst b/doc/en/pythonpath.rst index 113c95c30..0054acc59 100644 --- a/doc/en/pythonpath.rst +++ b/doc/en/pythonpath.rst @@ -72,6 +72,8 @@ imported in the global import namespace. This is also discussed in details in :ref:`test discovery`. +.. _`pytest vs python -m pytest`: + Invoking ``pytest`` versus ``python -m pytest`` ----------------------------------------------- diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 991050c23..b6df27f9c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -59,7 +59,7 @@ pytest.raises **Tutorial**: :ref:`assertraises`. -.. autofunction:: pytest.raises(expected_exception: Exception, [match], [message]) +.. autofunction:: pytest.raises(expected_exception: Exception, [match]) :with: excinfo pytest.deprecated_call diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index d271b0b2a..9ef1e8a36 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -371,7 +371,7 @@ Running it with the report-on-xfail option gives this output: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - ============================ 7 xfailed in 0.05s ============================ + ============================ 7 xfailed in 0.12s ============================ .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index c231e76a1..b9faef4dc 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -64,7 +64,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmp_path.py:13: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= .. _`tmp_path_factory example`: @@ -133,7 +133,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmpdir.py:9: AssertionError - ============================ 1 failed in 0.02s ============================= + ============================ 1 failed in 0.12s ============================= .. _`tmpdir factory example`: diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 4f0a279a2..dc55cc0ed 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -166,7 +166,7 @@ the ``self.db`` values in the traceback: E assert 0 test_unittest_db.py:13: AssertionError - ============================ 2 failed in 0.02s ============================= + ============================ 2 failed in 0.12s ============================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 78702ea86..777c14be2 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -247,7 +247,7 @@ Example: XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error - assert 0 FAILED test_example.py::test_fail - assert 0 - == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -297,7 +297,7 @@ More than one character can be used, so for example to only see failed and skipp ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test - == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -336,7 +336,7 @@ captured output: ok ========================= short test summary info ========================== PASSED test_example.py::test_ok - == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.03s === + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index b8a2df270..90245a0d4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 1 passed, 1 warnings in 0.00s ======================= + ====================== 1 passed, 1 warnings in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f89aaefba..e4e58b32c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -789,7 +789,11 @@ def _py36_windowsconsoleio_workaround(stream): See https://github.com/pytest-dev/py/issues/103 """ - if not sys.platform.startswith("win32") or sys.version_info[:2] < (3, 6): + if ( + not sys.platform.startswith("win32") + or sys.version_info[:2] < (3, 6) + or hasattr(sys, "pypy_version_info") + ): return # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d547f033d..3164c81ba 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -431,10 +431,7 @@ class PytestPluginManager(PluginManager): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - # Use realpath to avoid loading the same conftest twice - # with build systems that create build directories containing - # symlinks to actual files. - mod = self._importconftest(conftestpath.realpath()) + mod = self._importconftest(conftestpath) clist.append(mod) self._dirpath2confmods[directory] = clist return clist @@ -449,8 +446,14 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest(self, conftestpath): + # Use a resolved Path object as key to avoid loading the same conftest twice + # with build systems that create build directories containing + # symlinks to actual files. + # Using Path().resolve() is better than py.path.realpath because + # it resolves to the correct path/drive in case-insensitive file systems (#5792) + key = Path(str(conftestpath)).resolve() try: - return self._conftestpath2mod[conftestpath] + return self._conftestpath2mod[key] except KeyError: pkgpath = conftestpath.pypkgpath() if pkgpath is None: @@ -467,7 +470,7 @@ class PytestPluginManager(PluginManager): raise ConftestImportFailure(conftestpath, sys.exc_info()) self._conftest_plugins.add(mod) - self._conftestpath2mod[conftestpath] = mod + self._conftestpath2mod[key] = mod dirpath = conftestpath.dirpath() if dirpath in self._dirpath2confmods: for path, mods in self._dirpath2confmods.items(): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d5f9ad2d3..e78ed011e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -859,7 +859,7 @@ class FixtureDef: if argname != "request": fixturedef.addfinalizer(functools.partial(self.finish, request=request)) - my_cache_key = request.param_index + my_cache_key = self.cache_key(request) cached_result = getattr(self, "cached_result", None) if cached_result is not None: result, cache_key, err = cached_result @@ -877,6 +877,9 @@ class FixtureDef: hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) + def cache_key(self, request): + return request.param_index if not hasattr(request, "param") else request.param + def __repr__(self): return "".format( self.argname, self.scope, self.baseid @@ -897,6 +900,12 @@ def resolve_fixture_function(fixturedef, request): # request.instance so that code working with "fixturedef" behaves # as expected. if request.instance is not None: + # handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270 + if hasattr(fixturefunc, "__self__") and not isinstance( + request.instance, fixturefunc.__self__.__class__ + ): + return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: fixturefunc = fixturefunc.__get__(request.instance) @@ -913,7 +922,7 @@ def pytest_fixture_setup(fixturedef, request): kwargs[argname] = result fixturefunc = resolve_fixture_function(fixturedef, request) - my_cache_key = request.param_index + my_cache_key = fixturedef.cache_key(request) try: result = call_fixture_func(fixturefunc, request, kwargs) except TEST_OUTCOME: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index b1bbc2943..e6dee1547 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -263,7 +263,6 @@ class Node: fm = self.session._fixturemanager if excinfo.errisinstance(fm.FixtureLookupError): return excinfo.value.formatrepr() - tbfilter = True if self.config.getoption("fulltrace", False): style = "long" else: @@ -271,7 +270,6 @@ class Node: self._prunetraceback(excinfo) if len(excinfo.traceback) == 0: excinfo.traceback = tb - tbfilter = False # prunetraceback already does it if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -297,7 +295,7 @@ class Node: abspath=abspath, showlocals=self.config.getoption("showlocals", False), style=style, - tbfilter=tbfilter, + tbfilter=False, # pruned already, or in --fulltrace mode. truncate_locals=truncate_locals, ) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 732408323..d8030b926 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -4,6 +4,7 @@ from typing import Union import py +from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprEntry from _pytest._code.code import ReprEntryNative @@ -161,46 +162,7 @@ class BaseReport: Experimental method. """ - - def disassembled_report(rep): - reprtraceback = rep.longrepr.reprtraceback.__dict__.copy() - reprcrash = rep.longrepr.reprcrash.__dict__.copy() - - new_entries = [] - for entry in reprtraceback["reprentries"]: - entry_data = { - "type": type(entry).__name__, - "data": entry.__dict__.copy(), - } - for key, value in entry_data["data"].items(): - if hasattr(value, "__dict__"): - entry_data["data"][key] = value.__dict__.copy() - new_entries.append(entry_data) - - reprtraceback["reprentries"] = new_entries - - return { - "reprcrash": reprcrash, - "reprtraceback": reprtraceback, - "sections": rep.longrepr.sections, - } - - d = self.__dict__.copy() - if hasattr(self.longrepr, "toterminal"): - if hasattr(self.longrepr, "reprtraceback") and hasattr( - self.longrepr, "reprcrash" - ): - d["longrepr"] = disassembled_report(self) - else: - d["longrepr"] = str(self.longrepr) - else: - d["longrepr"] = self.longrepr - for name in d: - if isinstance(d[name], (py.path.local, Path)): - d[name] = str(d[name]) - elif name == "result": - d[name] = None # for now - return d + return _report_to_json(self) @classmethod def _from_json(cls, reportdict): @@ -212,54 +174,8 @@ class BaseReport: Experimental method. """ - if reportdict["longrepr"]: - if ( - "reprcrash" in reportdict["longrepr"] - and "reprtraceback" in reportdict["longrepr"] - ): - - reprtraceback = reportdict["longrepr"]["reprtraceback"] - reprcrash = reportdict["longrepr"]["reprcrash"] - - unserialized_entries = [] - for entry_data in reprtraceback["reprentries"]: - data = entry_data["data"] - entry_type = entry_data["type"] - if entry_type == "ReprEntry": - reprfuncargs = None - reprfileloc = None - reprlocals = None - if data["reprfuncargs"]: - reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) - if data["reprfileloc"]: - reprfileloc = ReprFileLocation(**data["reprfileloc"]) - if data["reprlocals"]: - reprlocals = ReprLocals(data["reprlocals"]["lines"]) - - reprentry = ReprEntry( - lines=data["lines"], - reprfuncargs=reprfuncargs, - reprlocals=reprlocals, - filelocrepr=reprfileloc, - style=data["style"], - ) # type: Union[ReprEntry, ReprEntryNative] - elif entry_type == "ReprEntryNative": - reprentry = ReprEntryNative(data["lines"]) - else: - _report_unserialization_failure(entry_type, cls, reportdict) - unserialized_entries.append(reprentry) - reprtraceback["reprentries"] = unserialized_entries - - exception_info = ReprExceptionInfo( - reprtraceback=ReprTraceback(**reprtraceback), - reprcrash=ReprFileLocation(**reprcrash), - ) - - for section in reportdict["longrepr"]["sections"]: - exception_info.addsection(*section) - reportdict["longrepr"] = exception_info - - return cls(**reportdict) + kwargs = _report_kwargs_from_json(reportdict) + return cls(**kwargs) def _report_unserialization_failure(type_name, report_class, reportdict): @@ -425,3 +341,142 @@ def pytest_report_from_serializable(data): assert False, "Unknown report_type unserialize data: {}".format( data["_report_type"] ) + + +def _report_to_json(report): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Returns the contents of this report as a dict of builtin entries, suitable for + serialization. + """ + + def serialize_repr_entry(entry): + entry_data = {"type": type(entry).__name__, "data": entry.__dict__.copy()} + for key, value in entry_data["data"].items(): + if hasattr(value, "__dict__"): + entry_data["data"][key] = value.__dict__.copy() + return entry_data + + def serialize_repr_traceback(reprtraceback): + result = reprtraceback.__dict__.copy() + result["reprentries"] = [ + serialize_repr_entry(x) for x in reprtraceback.reprentries + ] + return result + + def serialize_repr_crash(reprcrash): + return reprcrash.__dict__.copy() + + def serialize_longrepr(rep): + result = { + "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), + "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), + "sections": rep.longrepr.sections, + } + if isinstance(rep.longrepr, ExceptionChainRepr): + result["chain"] = [] + for repr_traceback, repr_crash, description in rep.longrepr.chain: + result["chain"].append( + ( + serialize_repr_traceback(repr_traceback), + serialize_repr_crash(repr_crash), + description, + ) + ) + else: + result["chain"] = None + return result + + d = report.__dict__.copy() + if hasattr(report.longrepr, "toterminal"): + if hasattr(report.longrepr, "reprtraceback") and hasattr( + report.longrepr, "reprcrash" + ): + d["longrepr"] = serialize_longrepr(report) + else: + d["longrepr"] = str(report.longrepr) + else: + d["longrepr"] = report.longrepr + for name in d: + if isinstance(d[name], (py.path.local, Path)): + d[name] = str(d[name]) + elif name == "result": + d[name] = None # for now + return d + + +def _report_kwargs_from_json(reportdict): + """ + This was originally the serialize_report() function from xdist (ca03269). + + Returns **kwargs that can be used to construct a TestReport or CollectReport instance. + """ + + def deserialize_repr_entry(entry_data): + data = entry_data["data"] + entry_type = entry_data["type"] + if entry_type == "ReprEntry": + reprfuncargs = None + reprfileloc = None + reprlocals = None + if data["reprfuncargs"]: + reprfuncargs = ReprFuncArgs(**data["reprfuncargs"]) + if data["reprfileloc"]: + reprfileloc = ReprFileLocation(**data["reprfileloc"]) + if data["reprlocals"]: + reprlocals = ReprLocals(data["reprlocals"]["lines"]) + + reprentry = ReprEntry( + lines=data["lines"], + reprfuncargs=reprfuncargs, + reprlocals=reprlocals, + filelocrepr=reprfileloc, + style=data["style"], + ) # type: Union[ReprEntry, ReprEntryNative] + elif entry_type == "ReprEntryNative": + reprentry = ReprEntryNative(data["lines"]) + else: + _report_unserialization_failure(entry_type, TestReport, reportdict) + return reprentry + + def deserialize_repr_traceback(repr_traceback_dict): + repr_traceback_dict["reprentries"] = [ + deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"] + ] + return ReprTraceback(**repr_traceback_dict) + + def deserialize_repr_crash(repr_crash_dict): + return ReprFileLocation(**repr_crash_dict) + + if ( + reportdict["longrepr"] + and "reprcrash" in reportdict["longrepr"] + and "reprtraceback" in reportdict["longrepr"] + ): + + reprtraceback = deserialize_repr_traceback( + reportdict["longrepr"]["reprtraceback"] + ) + reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"]) + if reportdict["longrepr"]["chain"]: + chain = [] + for repr_traceback_data, repr_crash_data, description in reportdict[ + "longrepr" + ]["chain"]: + chain.append( + ( + deserialize_repr_traceback(repr_traceback_data), + deserialize_repr_crash(repr_crash_data), + description, + ) + ) + exception_info = ExceptionChainRepr(chain) + else: + exception_info = ReprExceptionInfo(reprtraceback, reprcrash) + + for section in reportdict["longrepr"]["sections"]: + exception_info.addsection(*section) + reportdict["longrepr"] = exception_info + + return reportdict diff --git a/testing/code/test_code.py b/testing/code/test_code.py index d45795967..2f55720b4 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,8 +1,6 @@ import sys from unittest import mock -from test_excinfo import TWMock - import _pytest._code import pytest @@ -168,17 +166,15 @@ class TestTracebackEntry: class TestReprFuncArgs: - def test_not_raise_exception_with_mixed_encoding(self): + def test_not_raise_exception_with_mixed_encoding(self, tw_mock): from _pytest._code.code import ReprFuncArgs - tw = TWMock() - args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] r = ReprFuncArgs(args) - r.toterminal(tw) + r.toterminal(tw_mock) assert ( - tw.lines[0] + tw_mock.lines[0] == r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'" ) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 7742b4da9..5673b811b 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -31,33 +31,6 @@ def limited_recursion_depth(): sys.setrecursionlimit(before) -class TWMock: - WRITE = object() - - def __init__(self): - self.lines = [] - self.is_writing = False - - def sep(self, sep, line=None): - self.lines.append((sep, line)) - - def write(self, msg, **kw): - self.lines.append((TWMock.WRITE, msg)) - - def line(self, line, **kw): - self.lines.append(line) - - def markup(self, text, **kw): - return text - - def get_write_msg(self, idx): - flag, msg = self.lines[idx] - assert flag == TWMock.WRITE - return msg - - fullwidth = 80 - - def test_excinfo_simple() -> None: try: raise ValueError @@ -426,6 +399,13 @@ def test_match_raises_error(testdir): result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"]) + assert "__tracebackhide__ = True" not in result.stdout.str() + + result = testdir.runpytest("--fulltrace") + assert result.ret != 0 + result.stdout.fnmatch_lines( + ["*__tracebackhide__ = True*", "*AssertionError*Pattern*[123]*not found*"] + ) class TestFormattedExcinfo: @@ -658,7 +638,7 @@ raise ValueError() assert loc.lineno == 3 # assert loc.message == "ValueError: hello" - def test_repr_tracebackentry_lines2(self, importasmod): + def test_repr_tracebackentry_lines2(self, importasmod, tw_mock): mod = importasmod( """ def func1(m, x, y, z): @@ -678,13 +658,12 @@ raise ValueError() p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "m = " + repr("m" * 90) - assert tw.lines[1] == "x = 5, y = 13" - assert tw.lines[2] == "z = " + repr("z" * 120) + repr_entry.toterminal(tw_mock) + assert tw_mock.lines[0] == "m = " + repr("m" * 90) + assert tw_mock.lines[1] == "x = 5, y = 13" + assert tw_mock.lines[2] == "z = " + repr("z" * 120) - def test_repr_tracebackentry_lines_var_kw_args(self, importasmod): + def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock): mod = importasmod( """ def func1(x, *y, **z): @@ -703,9 +682,8 @@ raise ValueError() p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" + repr_entry.toterminal(tw_mock) + assert tw_mock.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" def test_repr_tracebackentry_short(self, importasmod): mod = importasmod( @@ -842,7 +820,7 @@ raise ValueError() assert p._makepath(__file__) == __file__ p.repr_traceback(excinfo) - def test_repr_excinfo_addouterr(self, importasmod): + def test_repr_excinfo_addouterr(self, importasmod, tw_mock): mod = importasmod( """ def entry(): @@ -852,10 +830,9 @@ raise ValueError() excinfo = pytest.raises(ValueError, mod.entry) repr = excinfo.getrepr() repr.addsection("title", "content") - twmock = TWMock() - repr.toterminal(twmock) - assert twmock.lines[-1] == "content" - assert twmock.lines[-2] == ("-", "title") + repr.toterminal(tw_mock) + assert tw_mock.lines[-1] == "content" + assert tw_mock.lines[-2] == ("-", "title") def test_repr_excinfo_reprcrash(self, importasmod): mod = importasmod( @@ -920,7 +897,7 @@ raise ValueError() x = str(MyRepr()) assert x == "я" - def test_toterminal_long(self, importasmod): + def test_toterminal_long(self, importasmod, tw_mock): mod = importasmod( """ def g(x): @@ -932,27 +909,26 @@ raise ValueError() excinfo = pytest.raises(ValueError, mod.f) excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == " def f():" - assert tw.lines[1] == "> g(3)" - assert tw.lines[2] == "" - line = tw.get_write_msg(3) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == " def f():" + assert tw_mock.lines[1] == "> g(3)" + assert tw_mock.lines[2] == "" + line = tw_mock.get_write_msg(3) assert line.endswith("mod.py") - assert tw.lines[4] == (":5: ") - assert tw.lines[5] == ("_ ", None) - assert tw.lines[6] == "" - assert tw.lines[7] == " def g(x):" - assert tw.lines[8] == "> raise ValueError(x)" - assert tw.lines[9] == "E ValueError: 3" - assert tw.lines[10] == "" - line = tw.get_write_msg(11) + assert tw_mock.lines[4] == (":5: ") + assert tw_mock.lines[5] == ("_ ", None) + assert tw_mock.lines[6] == "" + assert tw_mock.lines[7] == " def g(x):" + assert tw_mock.lines[8] == "> raise ValueError(x)" + assert tw_mock.lines[9] == "E ValueError: 3" + assert tw_mock.lines[10] == "" + line = tw_mock.get_write_msg(11) assert line.endswith("mod.py") - assert tw.lines[12] == ":3: ValueError" + assert tw_mock.lines[12] == ":3: ValueError" - def test_toterminal_long_missing_source(self, importasmod, tmpdir): + def test_toterminal_long_missing_source(self, importasmod, tmpdir, tw_mock): mod = importasmod( """ def g(x): @@ -965,25 +941,24 @@ raise ValueError() tmpdir.join("mod.py").remove() excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - line = tw.get_write_msg(2) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == "> ???" + assert tw_mock.lines[1] == "" + line = tw_mock.get_write_msg(2) assert line.endswith("mod.py") - assert tw.lines[3] == ":5: " - assert tw.lines[4] == ("_ ", None) - assert tw.lines[5] == "" - assert tw.lines[6] == "> ???" - assert tw.lines[7] == "E ValueError: 3" - assert tw.lines[8] == "" - line = tw.get_write_msg(9) + assert tw_mock.lines[3] == ":5: " + assert tw_mock.lines[4] == ("_ ", None) + assert tw_mock.lines[5] == "" + assert tw_mock.lines[6] == "> ???" + assert tw_mock.lines[7] == "E ValueError: 3" + assert tw_mock.lines[8] == "" + line = tw_mock.get_write_msg(9) assert line.endswith("mod.py") - assert tw.lines[10] == ":3: ValueError" + assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): + def test_toterminal_long_incomplete_source(self, importasmod, tmpdir, tw_mock): mod = importasmod( """ def g(x): @@ -996,25 +971,24 @@ raise ValueError() tmpdir.join("mod.py").write("asdf") excinfo.traceback = excinfo.traceback.filter() repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - line = tw.get_write_msg(2) + repr.toterminal(tw_mock) + assert tw_mock.lines[0] == "" + tw_mock.lines.pop(0) + assert tw_mock.lines[0] == "> ???" + assert tw_mock.lines[1] == "" + line = tw_mock.get_write_msg(2) assert line.endswith("mod.py") - assert tw.lines[3] == ":5: " - assert tw.lines[4] == ("_ ", None) - assert tw.lines[5] == "" - assert tw.lines[6] == "> ???" - assert tw.lines[7] == "E ValueError: 3" - assert tw.lines[8] == "" - line = tw.get_write_msg(9) + assert tw_mock.lines[3] == ":5: " + assert tw_mock.lines[4] == ("_ ", None) + assert tw_mock.lines[5] == "" + assert tw_mock.lines[6] == "> ???" + assert tw_mock.lines[7] == "E ValueError: 3" + assert tw_mock.lines[8] == "" + line = tw_mock.get_write_msg(9) assert line.endswith("mod.py") - assert tw.lines[10] == ":3: ValueError" + assert tw_mock.lines[10] == ":3: ValueError" - def test_toterminal_long_filenames(self, importasmod): + def test_toterminal_long_filenames(self, importasmod, tw_mock): mod = importasmod( """ def f(): @@ -1022,23 +996,22 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = TWMock() path = py.path.local(mod.__file__) old = path.dirpath().chdir() try: repr = excinfo.getrepr(abspath=False) - repr.toterminal(tw) + repr.toterminal(tw_mock) x = py.path.local().bestrelpath(path) if len(x) < len(str(path)): - msg = tw.get_write_msg(-2) + msg = tw_mock.get_write_msg(-2) assert msg == "mod.py" - assert tw.lines[-1] == ":3: ValueError" + assert tw_mock.lines[-1] == ":3: ValueError" repr = excinfo.getrepr(abspath=True) - repr.toterminal(tw) - msg = tw.get_write_msg(-2) + repr.toterminal(tw_mock) + msg = tw_mock.get_write_msg(-2) assert msg == path - line = tw.lines[-1] + line = tw_mock.lines[-1] assert line == ":3: ValueError" finally: old.chdir() @@ -1073,7 +1046,7 @@ raise ValueError() repr.toterminal(tw) assert tw.stringio.getvalue() - def test_traceback_repr_style(self, importasmod): + def test_traceback_repr_style(self, importasmod, tw_mock): mod = importasmod( """ def f(): @@ -1091,35 +1064,34 @@ raise ValueError() excinfo.traceback[1].set_repr_style("short") excinfo.traceback[2].set_repr_style("short") r = excinfo.getrepr(style="long") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == "> g()" - assert tw.lines[3] == "" - msg = tw.get_write_msg(4) + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == "> g()" + assert tw_mock.lines[3] == "" + msg = tw_mock.get_write_msg(4) assert msg.endswith("mod.py") - assert tw.lines[5] == ":3: " - assert tw.lines[6] == ("_ ", None) - tw.get_write_msg(7) - assert tw.lines[8].endswith("in g") - assert tw.lines[9] == " h()" - tw.get_write_msg(10) - assert tw.lines[11].endswith("in h") - assert tw.lines[12] == " i()" - assert tw.lines[13] == ("_ ", None) - assert tw.lines[14] == "" - assert tw.lines[15] == " def i():" - assert tw.lines[16] == "> raise ValueError()" - assert tw.lines[17] == "E ValueError" - assert tw.lines[18] == "" - msg = tw.get_write_msg(19) + assert tw_mock.lines[5] == ":3: " + assert tw_mock.lines[6] == ("_ ", None) + tw_mock.get_write_msg(7) + assert tw_mock.lines[8].endswith("in g") + assert tw_mock.lines[9] == " h()" + tw_mock.get_write_msg(10) + assert tw_mock.lines[11].endswith("in h") + assert tw_mock.lines[12] == " i()" + assert tw_mock.lines[13] == ("_ ", None) + assert tw_mock.lines[14] == "" + assert tw_mock.lines[15] == " def i():" + assert tw_mock.lines[16] == "> raise ValueError()" + assert tw_mock.lines[17] == "E ValueError" + assert tw_mock.lines[18] == "" + msg = tw_mock.get_write_msg(19) msg.endswith("mod.py") - assert tw.lines[20] == ":9: ValueError" + assert tw_mock.lines[20] == ":9: ValueError" - def test_exc_chain_repr(self, importasmod): + def test_exc_chain_repr(self, importasmod, tw_mock): mod = importasmod( """ class Err(Exception): @@ -1140,72 +1112,71 @@ raise ValueError() ) excinfo = pytest.raises(AttributeError, mod.f) r = excinfo.getrepr(style="long") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == " try:" - assert tw.lines[3] == "> g()" - assert tw.lines[4] == "" - line = tw.get_write_msg(5) + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == " try:" + assert tw_mock.lines[3] == "> g()" + assert tw_mock.lines[4] == "" + line = tw_mock.get_write_msg(5) assert line.endswith("mod.py") - assert tw.lines[6] == ":6: " - assert tw.lines[7] == ("_ ", None) - assert tw.lines[8] == "" - assert tw.lines[9] == " def g():" - assert tw.lines[10] == "> raise ValueError()" - assert tw.lines[11] == "E ValueError" - assert tw.lines[12] == "" - line = tw.get_write_msg(13) + assert tw_mock.lines[6] == ":6: " + assert tw_mock.lines[7] == ("_ ", None) + assert tw_mock.lines[8] == "" + assert tw_mock.lines[9] == " def g():" + assert tw_mock.lines[10] == "> raise ValueError()" + assert tw_mock.lines[11] == "E ValueError" + assert tw_mock.lines[12] == "" + line = tw_mock.get_write_msg(13) assert line.endswith("mod.py") - assert tw.lines[14] == ":12: ValueError" - assert tw.lines[15] == "" + assert tw_mock.lines[14] == ":12: ValueError" + assert tw_mock.lines[15] == "" assert ( - tw.lines[16] + tw_mock.lines[16] == "The above exception was the direct cause of the following exception:" ) - assert tw.lines[17] == "" - assert tw.lines[18] == " def f():" - assert tw.lines[19] == " try:" - assert tw.lines[20] == " g()" - assert tw.lines[21] == " except Exception as e:" - assert tw.lines[22] == "> raise Err() from e" - assert tw.lines[23] == "E test_exc_chain_repr0.mod.Err" - assert tw.lines[24] == "" - line = tw.get_write_msg(25) + assert tw_mock.lines[17] == "" + assert tw_mock.lines[18] == " def f():" + assert tw_mock.lines[19] == " try:" + assert tw_mock.lines[20] == " g()" + assert tw_mock.lines[21] == " except Exception as e:" + assert tw_mock.lines[22] == "> raise Err() from e" + assert tw_mock.lines[23] == "E test_exc_chain_repr0.mod.Err" + assert tw_mock.lines[24] == "" + line = tw_mock.get_write_msg(25) assert line.endswith("mod.py") - assert tw.lines[26] == ":8: Err" - assert tw.lines[27] == "" + assert tw_mock.lines[26] == ":8: Err" + assert tw_mock.lines[27] == "" assert ( - tw.lines[28] + tw_mock.lines[28] == "During handling of the above exception, another exception occurred:" ) - assert tw.lines[29] == "" - assert tw.lines[30] == " def f():" - assert tw.lines[31] == " try:" - assert tw.lines[32] == " g()" - assert tw.lines[33] == " except Exception as e:" - assert tw.lines[34] == " raise Err() from e" - assert tw.lines[35] == " finally:" - assert tw.lines[36] == "> h()" - assert tw.lines[37] == "" - line = tw.get_write_msg(38) + assert tw_mock.lines[29] == "" + assert tw_mock.lines[30] == " def f():" + assert tw_mock.lines[31] == " try:" + assert tw_mock.lines[32] == " g()" + assert tw_mock.lines[33] == " except Exception as e:" + assert tw_mock.lines[34] == " raise Err() from e" + assert tw_mock.lines[35] == " finally:" + assert tw_mock.lines[36] == "> h()" + assert tw_mock.lines[37] == "" + line = tw_mock.get_write_msg(38) assert line.endswith("mod.py") - assert tw.lines[39] == ":10: " - assert tw.lines[40] == ("_ ", None) - assert tw.lines[41] == "" - assert tw.lines[42] == " def h():" - assert tw.lines[43] == "> raise AttributeError()" - assert tw.lines[44] == "E AttributeError" - assert tw.lines[45] == "" - line = tw.get_write_msg(46) + assert tw_mock.lines[39] == ":10: " + assert tw_mock.lines[40] == ("_ ", None) + assert tw_mock.lines[41] == "" + assert tw_mock.lines[42] == " def h():" + assert tw_mock.lines[43] == "> raise AttributeError()" + assert tw_mock.lines[44] == "E AttributeError" + assert tw_mock.lines[45] == "" + line = tw_mock.get_write_msg(46) assert line.endswith("mod.py") - assert tw.lines[47] == ":15: AttributeError" + assert tw_mock.lines[47] == ":15: AttributeError" @pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"]) - def test_exc_repr_chain_suppression(self, importasmod, mode): + def test_exc_repr_chain_suppression(self, importasmod, mode, tw_mock): """Check that exc repr does not show chained exceptions in Python 3. - When the exception is raised with "from None" - Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr(). @@ -1226,24 +1197,23 @@ raise ValueError() ) excinfo = pytest.raises(AttributeError, mod.f) r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: + r.toterminal(tw_mock) + for line in tw_mock.lines: print(line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == " try:" - assert tw.lines[3] == " g()" - assert tw.lines[4] == " except Exception:" - assert tw.lines[5] == "> raise AttributeError(){}".format( + assert tw_mock.lines[0] == "" + assert tw_mock.lines[1] == " def f():" + assert tw_mock.lines[2] == " try:" + assert tw_mock.lines[3] == " g()" + assert tw_mock.lines[4] == " except Exception:" + assert tw_mock.lines[5] == "> raise AttributeError(){}".format( raise_suffix ) - assert tw.lines[6] == "E AttributeError" - assert tw.lines[7] == "" - line = tw.get_write_msg(8) + assert tw_mock.lines[6] == "E AttributeError" + assert tw_mock.lines[7] == "" + line = tw_mock.get_write_msg(8) assert line.endswith("mod.py") - assert tw.lines[9] == ":6: AttributeError" - assert len(tw.lines) == 10 + assert tw_mock.lines[9] == ":6: AttributeError" + assert len(tw_mock.lines) == 10 @pytest.mark.parametrize( "reason, description", @@ -1304,7 +1274,7 @@ raise ValueError() ] ) - def test_exc_chain_repr_cycle(self, importasmod): + def test_exc_chain_repr_cycle(self, importasmod, tw_mock): mod = importasmod( """ class Err(Exception): @@ -1325,9 +1295,8 @@ raise ValueError() ) excinfo = pytest.raises(ZeroDivisionError, mod.unreraise) r = excinfo.getrepr(style="short") - tw = TWMock() - r.toterminal(tw) - out = "\n".join(line for line in tw.lines if isinstance(line, str)) + r.toterminal(tw_mock) + out = "\n".join(line for line in tw_mock.lines if isinstance(line, str)) expected_out = textwrap.dedent( """\ :13: in unreraise diff --git a/testing/conftest.py b/testing/conftest.py index 635e7a614..d7f94ce45 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -55,3 +55,36 @@ def pytest_collection_modifyitems(config, items): items[:] = fast_items + neutral_items + slow_items + slowest_items yield + + +@pytest.fixture +def tw_mock(): + """Returns a mock terminal writer""" + + class TWMock: + WRITE = object() + + def __init__(self): + self.lines = [] + self.is_writing = False + + def sep(self, sep, line=None): + self.lines.append((sep, line)) + + def write(self, msg, **kw): + self.lines.append((TWMock.WRITE, msg)) + + def line(self, line, **kw): + self.lines.append(line) + + def markup(self, text, **kw): + return text + + def get_write_msg(self, idx): + flag, msg = self.lines[idx] + assert flag == TWMock.WRITE + return msg + + fullwidth = 80 + + return TWMock() diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 1f383e752..43a05e7bb 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -449,7 +449,8 @@ class TestFillFixtures: "*ERROR at setup of test_lookup_error*", " def test_lookup_error(unknown):*", "E fixture 'unknown' not found", - "> available fixtures:*a_fixture,*b_fixture,*c_fixture,*d_fixture*monkeypatch,*", # sorted + "> available fixtures:*a_fixture,*b_fixture,*c_fixture,*d_fixture*monkeypatch,*", + # sorted "> use 'py*test --fixtures *' for help on them.", "*1 error*", ] @@ -3945,6 +3946,38 @@ class TestScopeOrdering: reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + def test_class_fixture_self_instance(self, testdir): + """Check that plugin classes which implement fixtures receive the plugin instance + as self (see #2270). + """ + testdir.makeconftest( + """ + import pytest + + def pytest_configure(config): + config.pluginmanager.register(MyPlugin()) + + class MyPlugin(): + def __init__(self): + self.arg = 1 + + @pytest.fixture(scope='function') + def myfix(self): + assert isinstance(self, MyPlugin) + return self.arg + """ + ) + + testdir.makepyfile( + """ + class TestClass(object): + def test_1(self, myfix): + assert myfix == 1 + """ + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_call_fixture_function_error(): """Check if an error is raised if a fixture function is called directly (#4545)""" @@ -4009,3 +4042,55 @@ def test_fixture_named_request(testdir): " *test_fixture_named_request.py:5", ] ) + + +def test_indirect_fixture_does_not_break_scope(testdir): + """Ensure that fixture scope is respected when using indirect fixtures (#570)""" + testdir.makepyfile( + """ + import pytest + + instantiated = [] + + @pytest.fixture(scope="session") + def fixture_1(request): + instantiated.append(("fixture_1", request.param)) + + + @pytest.fixture(scope="session") + def fixture_2(request): + instantiated.append(("fixture_2", request.param)) + + + scenarios = [ + ("A", "a1"), + ("A", "a2"), + ("B", "b1"), + ("B", "b2"), + ("C", "c1"), + ("C", "c2"), + ] + + @pytest.mark.parametrize( + "fixture_1,fixture_2", scenarios, indirect=["fixture_1", "fixture_2"] + ) + def test_create_fixtures(fixture_1, fixture_2): + pass + + + def test_check_fixture_instantiations(): + assert instantiated == [ + ('fixture_1', 'A'), + ('fixture_2', 'a1'), + ('fixture_2', 'a2'), + ('fixture_1', 'B'), + ('fixture_2', 'b1'), + ('fixture_2', 'b2'), + ('fixture_1', 'C'), + ('fixture_2', 'c1'), + ('fixture_2', 'c2'), + ] + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=7) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 447416f10..3f08ee381 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,6 @@ +import os import textwrap +from pathlib import Path import py @@ -163,11 +165,12 @@ def test_setinitial_conftest_subdirs(testdir, name): subconftest = sub.ensure("conftest.py") conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) + key = Path(str(subconftest)).resolve() if name not in ("whatever", ".dotdir"): - assert subconftest in conftest._conftestpath2mod + assert key in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: - assert subconftest not in conftest._conftestpath2mod + assert key not in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 0 @@ -275,6 +278,31 @@ def test_conftest_symlink_files(testdir): assert result.ret == ExitCode.OK +@pytest.mark.skipif( + os.path.normcase("x") != os.path.normcase("X"), + reason="only relevant for case insensitive file systems", +) +def test_conftest_badcase(testdir): + """Check conftest.py loading when directory casing is wrong (#5792).""" + testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") + source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} + testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + + testdir.tmpdir.join("jenkinsroot/test").chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + +def test_conftest_uppercase(testdir): + """Check conftest.py whose qualified name contains uppercase characters (#5819)""" + source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""} + testdir.makepyfile(**source) + + testdir.tmpdir.chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 8d327cbb3..3c56f40e1 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -853,7 +853,7 @@ class TestDebuggingBreakpoints: Test that supports breakpoint global marks on Python 3.7+ and not on CPython 3.5, 2.7 """ - if sys.version_info.major == 3 and sys.version_info.minor >= 7: + if sys.version_info >= (3, 7): assert SUPPORTS_BREAKPOINT_BUILTIN is True if sys.version_info.major == 3 and sys.version_info.minor == 5: assert SUPPORTS_BREAKPOINT_BUILTIN is False diff --git a/testing/test_reports.py b/testing/test_reports.py index b8b1a5406..8bac0243a 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,4 +1,5 @@ import pytest +from _pytest._code.code import ExceptionChainRepr from _pytest.pathlib import Path from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -220,8 +221,8 @@ class TestReportSerialization: assert data["path1"] == str(testdir.tmpdir) assert data["path2"] == str(testdir.tmpdir) - def test_unserialization_failure(self, testdir): - """Check handling of failure during unserialization of report types.""" + def test_deserialization_failure(self, testdir): + """Check handling of failure during deserialization of report types.""" testdir.makepyfile( """ def test_a(): @@ -242,6 +243,75 @@ class TestReportSerialization: ): TestReport._from_json(data) + @pytest.mark.parametrize("report_class", [TestReport, CollectReport]) + def test_chained_exceptions(self, testdir, tw_mock, report_class): + """Check serialization/deserialization of report objects containing chained exceptions (#5786)""" + testdir.makepyfile( + """ + def foo(): + raise ValueError('value error') + def test_a(): + try: + foo() + except ValueError as e: + raise RuntimeError('runtime error') from e + if {error_during_import}: + test_a() + """.format( + error_during_import=report_class is CollectReport + ) + ) + + reprec = testdir.inline_run() + if report_class is TestReport: + reports = reprec.getreports("pytest_runtest_logreport") + # we have 3 reports: setup/call/teardown + assert len(reports) == 3 + # get the call report + report = reports[1] + else: + assert report_class is CollectReport + # two collection reports: session and test file + reports = reprec.getreports("pytest_collectreport") + assert len(reports) == 2 + report = reports[1] + + def check_longrepr(longrepr): + """Check the attributes of the given longrepr object according to the test file. + + We can get away with testing both CollectReport and TestReport with this function because + the longrepr objects are very similar. + """ + assert isinstance(longrepr, ExceptionChainRepr) + assert longrepr.sections == [("title", "contents", "=")] + assert len(longrepr.chain) == 2 + entry1, entry2 = longrepr.chain + tb1, fileloc1, desc1 = entry1 + tb2, fileloc2, desc2 = entry2 + + assert "ValueError('value error')" in str(tb1) + assert "RuntimeError('runtime error')" in str(tb2) + + assert ( + desc1 + == "The above exception was the direct cause of the following exception:" + ) + assert desc2 is None + + assert report.failed + assert len(report.sections) == 0 + report.longrepr.addsection("title", "contents", "=") + check_longrepr(report.longrepr) + + data = report._to_json() + loaded_report = report_class._from_json(data) + check_longrepr(loaded_report.longrepr) + + # make sure we don't blow up on ``toterminal`` call; we don't test the actual output because it is very + # brittle and hard to maintain, but we can assume it is correct because ``toterminal`` is already tested + # elsewhere and we do check the contents of the longrepr object after loading it. + loaded_report.longrepr.toterminal(tw_mock) + class TestHooks: """Test that the hooks are working correctly for plugins"""