Merge pull request #5350 from asottile/release-4.6.0

Release 4.6.0
This commit is contained in:
Anthony Sottile 2019-06-01 11:10:57 -07:00 committed by GitHub
commit 917195ea8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 866 additions and 474 deletions

View File

@ -112,11 +112,9 @@ matrix:
allow_failures: allow_failures:
- python: '3.8-dev' - python: '3.8-dev'
env: TOXENV=py38-xdist env: TOXENV=py38-xdist
# temporary until pytest 4.6 is released # Temporary (https://github.com/pytest-dev/pytest/pull/5334).
- env: TOXENV=py27-pluggymaster-xdist - env: TOXENV=pypy3-xdist
python: '2.7' python: 'pypy3'
- env: TOXENV=py37-pluggymaster-xdist
before_script: before_script:
- | - |

View File

@ -18,6 +18,84 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 4.6.0 (2019-05-31)
=========================
Important
---------
The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**.
For more details, see our `Python 2.7 and 3.4 support plan <https://docs.pytest.org/en/latest/py27-py34-deprecation.html>`__.
Features
--------
- `#4559 <https://github.com/pytest-dev/pytest/issues/4559>`_: Added the ``junit_log_passing_tests`` ini value which can be used to enable or disable logging of passing test output in the Junit XML file.
- `#4956 <https://github.com/pytest-dev/pytest/issues/4956>`_: pytester's ``testdir.spawn`` uses ``tmpdir`` as HOME/USERPROFILE directory.
- `#5062 <https://github.com/pytest-dev/pytest/issues/5062>`_: Unroll calls to ``all`` to full for-loops with assertion rewriting for better failure messages, especially when using Generator Expressions.
- `#5063 <https://github.com/pytest-dev/pytest/issues/5063>`_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
- `#5091 <https://github.com/pytest-dev/pytest/issues/5091>`_: The output for ini options in ``--help`` has been improved.
- `#5269 <https://github.com/pytest-dev/pytest/issues/5269>`_: ``pytest.importorskip`` includes the ``ImportError`` now in the default ``reason``.
- `#5311 <https://github.com/pytest-dev/pytest/issues/5311>`_: Captured logs that are output for each failing test are formatted using the
ColoredLevelFormatter.
- `#5312 <https://github.com/pytest-dev/pytest/issues/5312>`_: Improved formatting of multiline log messages in Python 3.
Bug Fixes
---------
- `#2064 <https://github.com/pytest-dev/pytest/issues/2064>`_: The debugging plugin imports the wrapped ``Pdb`` class (``--pdbcls``) on-demand now.
- `#4908 <https://github.com/pytest-dev/pytest/issues/4908>`_: The ``pytest_enter_pdb`` hook gets called with post-mortem (``--pdb``).
- `#5036 <https://github.com/pytest-dev/pytest/issues/5036>`_: Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.
- `#5256 <https://github.com/pytest-dev/pytest/issues/5256>`_: Handle internal error due to a lone surrogate unicode character not being representable in Jython.
- `#5257 <https://github.com/pytest-dev/pytest/issues/5257>`_: Ensure that ``sys.stdout.mode`` does not include ``'b'`` as it is a text stream.
- `#5278 <https://github.com/pytest-dev/pytest/issues/5278>`_: Pytest's internal python plugin can be disabled using ``-p no:python`` again.
- `#5286 <https://github.com/pytest-dev/pytest/issues/5286>`_: Fix issue with ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option not working when using a list of test IDs in parametrized tests.
- `#5330 <https://github.com/pytest-dev/pytest/issues/5330>`_: Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.
- `#5333 <https://github.com/pytest-dev/pytest/issues/5333>`_: Fix regression in 4.5.0 with ``--lf`` not re-running all tests with known failures from non-selected tests.
Improved Documentation
----------------------
- `#5250 <https://github.com/pytest-dev/pytest/issues/5250>`_: Expand docs on use of ``setenv`` and ``delenv`` with ``monkeypatch``.
pytest 4.5.0 (2019-05-11) pytest 4.5.0 (2019-05-11)
========================= =========================

View File

@ -1 +0,0 @@
The debugging plugin imports the wrapped ``Pdb`` class (``--pdbcls``) on-demand now.

View File

@ -1 +0,0 @@
The ``pytest_enter_pdb`` hook gets called with post-mortem (``--pdb``).

View File

@ -1 +0,0 @@
Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.

View File

@ -1 +0,0 @@
Expand docs on use of ``setenv`` and ``delenv`` with ``monkeypatch``.

View File

@ -1 +0,0 @@
Handle internal error due to a lone surrogate unicode character not being representable in Jython.

View File

@ -1 +0,0 @@
Ensure that ``sys.stdout.mode`` does not include ``'b'`` as it is a text stream.

View File

@ -1 +0,0 @@
Pytest's internal python plugin can be disabled using ``-p no:python`` again.

View File

@ -1 +0,0 @@
Fix issue with ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option doesn't work when using a list of test IDs in parametrized tests.

View File

@ -1,2 +0,0 @@
Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.

View File

@ -1 +0,0 @@
Fix regression with ``--lf`` not re-running all tests with known failures from non-selected tests.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-4.6.0
release-4.5.0 release-4.5.0
release-4.4.2 release-4.4.2
release-4.4.1 release-4.4.1

View File

@ -0,0 +1,43 @@
pytest-4.6.0
=======================================
The pytest team is proud to announce the 4.6.0 release!
pytest is a mature Python testing tool with more than a 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
to take a look at the CHANGELOG:
https://docs.pytest.org/en/latest/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/latest/
As usual, you can upgrade from pypi via:
pip install -U pytest
Thanks to all who contributed to this release, among them:
* Akiomi Kamakura
* Anthony Sottile
* Bruno Oliveira
* Daniel Hahler
* David Röthlisberger
* Evan Kepner
* Jeffrey Rackauckas
* MyComputer
* Nikita Krokosh
* Raul Tambre
* Thomas Hisch
* Tim Hoffmann
* Tomer Keren
* Victor Maryama
* danielx123
* oleg-yegorov
Happy testing,
The Pytest Development Team

View File

@ -436,7 +436,7 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py . $ pytest -rs -q multipython.py
...sss...sssssssss...sss... [100%] ...sss...sssssssss...sss... [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found
12 passed, 15 skipped in 0.12 seconds 12 passed, 15 skipped in 0.12 seconds
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
@ -494,7 +494,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%] test_module.py .s [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2': No module named 'opt2'
=================== 1 passed, 1 skipped in 0.12 seconds ==================== =================== 1 passed, 1 skipped in 0.12 seconds ====================
You'll see that we don't have an ``opt2`` module and thus the second test run You'll see that we don't have an ``opt2`` module and thus the second test run

View File

@ -26,7 +26,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert param1 * 2 < param2 > assert param1 * 2 < param2
E assert (3 * 2) < 6 E assert (3 * 2) < 6
failure_demo.py:20: AssertionError failure_demo.py:21: AssertionError
_________________________ TestFailing.test_simple __________________________ _________________________ TestFailing.test_simple __________________________
self = <failure_demo.TestFailing object at 0xdeadbeef> self = <failure_demo.TestFailing object at 0xdeadbeef>
@ -43,7 +43,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + where 42 = <function TestFailing.test_simple.<locals>.f at 0xdeadbeef>() E + where 42 = <function TestFailing.test_simple.<locals>.f at 0xdeadbeef>()
E + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef>() E + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef>()
failure_demo.py:31: AssertionError failure_demo.py:32: AssertionError
____________________ TestFailing.test_simple_multiline _____________________ ____________________ TestFailing.test_simple_multiline _____________________
self = <failure_demo.TestFailing object at 0xdeadbeef> self = <failure_demo.TestFailing object at 0xdeadbeef>
@ -51,7 +51,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_simple_multiline(self): def test_simple_multiline(self):
> otherfunc_multi(42, 6 * 9) > otherfunc_multi(42, 6 * 9)
failure_demo.py:34: failure_demo.py:35:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
a = 42, b = 54 a = 42, b = 54
@ -60,7 +60,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b > assert a == b
E assert 42 == 54 E assert 42 == 54
failure_demo.py:15: AssertionError failure_demo.py:16: AssertionError
___________________________ TestFailing.test_not ___________________________ ___________________________ TestFailing.test_not ___________________________
self = <failure_demo.TestFailing object at 0xdeadbeef> self = <failure_demo.TestFailing object at 0xdeadbeef>
@ -73,7 +73,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert not 42 E assert not 42
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef>() E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef>()
failure_demo.py:40: AssertionError failure_demo.py:41: AssertionError
_________________ TestSpecialisedExplanations.test_eq_text _________________ _________________ TestSpecialisedExplanations.test_eq_text _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E - spam E - spam
E + eggs E + eggs
failure_demo.py:45: AssertionError failure_demo.py:46: AssertionError
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________ _____________ TestSpecialisedExplanations.test_eq_similar_text _____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -97,7 +97,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + foo 2 bar E + foo 2 bar
E ? ^ E ? ^
failure_demo.py:48: AssertionError failure_demo.py:49: AssertionError
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -110,7 +110,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + eggs E + eggs
E bar E bar
failure_demo.py:51: AssertionError failure_demo.py:52: AssertionError
______________ TestSpecialisedExplanations.test_eq_long_text _______________ ______________ TestSpecialisedExplanations.test_eq_long_text _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -127,7 +127,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + 1111111111b222222222 E + 1111111111b222222222
E ? ^ E ? ^
failure_demo.py:56: AssertionError failure_demo.py:57: AssertionError
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -147,7 +147,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E E
E ...Full output truncated (7 lines hidden), use '-vv' to show E ...Full output truncated (7 lines hidden), use '-vv' to show
failure_demo.py:61: AssertionError failure_demo.py:62: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________ _________________ TestSpecialisedExplanations.test_eq_list _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -158,7 +158,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E At index 2 diff: 2 != 3 E At index 2 diff: 2 != 3
E Use -v to get the full diff E Use -v to get the full diff
failure_demo.py:64: AssertionError failure_demo.py:65: AssertionError
______________ TestSpecialisedExplanations.test_eq_list_long _______________ ______________ TestSpecialisedExplanations.test_eq_list_long _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -171,7 +171,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E At index 100 diff: 1 != 2 E At index 100 diff: 1 != 2
E Use -v to get the full diff E Use -v to get the full diff
failure_demo.py:69: AssertionError failure_demo.py:70: AssertionError
_________________ TestSpecialisedExplanations.test_eq_dict _________________ _________________ TestSpecialisedExplanations.test_eq_dict _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -189,7 +189,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E E
E ...Full output truncated (2 lines hidden), use '-vv' to show E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:72: AssertionError failure_demo.py:73: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________ _________________ TestSpecialisedExplanations.test_eq_set __________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E E
E ...Full output truncated (2 lines hidden), use '-vv' to show E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:75: AssertionError failure_demo.py:76: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________ _____________ TestSpecialisedExplanations.test_eq_longer_list ______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Right contains one more item: 3 E Right contains one more item: 3
E Use -v to get the full diff E Use -v to get the full diff
failure_demo.py:78: AssertionError failure_demo.py:79: AssertionError
_________________ TestSpecialisedExplanations.test_in_list _________________ _________________ TestSpecialisedExplanations.test_in_list _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert 1 in [0, 2, 3, 4, 5] > assert 1 in [0, 2, 3, 4, 5]
E assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5]
failure_demo.py:81: AssertionError failure_demo.py:82: AssertionError
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________ __________ TestSpecialisedExplanations.test_not_in_text_multiline __________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -246,7 +246,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E E
E ...Full output truncated (2 lines hidden), use '-vv' to show E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:85: AssertionError failure_demo.py:86: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________ ___________ TestSpecialisedExplanations.test_not_in_text_single ____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -259,7 +259,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E single foo line E single foo line
E ? +++ E ? +++
failure_demo.py:89: AssertionError failure_demo.py:90: AssertionError
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________ _________ TestSpecialisedExplanations.test_not_in_text_single_long _________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -272,7 +272,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail
E ? +++ E ? +++
failure_demo.py:93: AssertionError failure_demo.py:94: AssertionError
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -285,7 +285,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail
E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
failure_demo.py:97: AssertionError failure_demo.py:98: AssertionError
______________ TestSpecialisedExplanations.test_eq_dataclass _______________ ______________ TestSpecialisedExplanations.test_eq_dataclass _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -306,7 +306,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Differing attributes: E Differing attributes:
E b: 'b' != 'c' E b: 'b' != 'c'
failure_demo.py:109: AssertionError failure_demo.py:110: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________ ________________ TestSpecialisedExplanations.test_eq_attrs _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef> self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
@ -327,7 +327,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Differing attributes: E Differing attributes:
E b: 'b' != 'c' E b: 'b' != 'c'
failure_demo.py:121: AssertionError failure_demo.py:122: AssertionError
______________________________ test_attribute ______________________________ ______________________________ test_attribute ______________________________
def test_attribute(): def test_attribute():
@ -339,7 +339,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 1 == 2 E assert 1 == 2
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef>.b E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef>.b
failure_demo.py:129: AssertionError failure_demo.py:130: AssertionError
_________________________ test_attribute_instance __________________________ _________________________ test_attribute_instance __________________________
def test_attribute_instance(): def test_attribute_instance():
@ -351,7 +351,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef>.b E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef>.b
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>() E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
failure_demo.py:136: AssertionError failure_demo.py:137: AssertionError
__________________________ test_attribute_failure __________________________ __________________________ test_attribute_failure __________________________
def test_attribute_failure(): def test_attribute_failure():
@ -364,7 +364,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
i = Foo() i = Foo()
> assert i.b == 2 > assert i.b == 2
failure_demo.py:147: failure_demo.py:148:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef> self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef>
@ -373,7 +373,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> raise Exception("Failed to get attrib") > raise Exception("Failed to get attrib")
E Exception: Failed to get attrib E Exception: Failed to get attrib
failure_demo.py:142: Exception failure_demo.py:143: Exception
_________________________ test_attribute_multiple __________________________ _________________________ test_attribute_multiple __________________________
def test_attribute_multiple(): def test_attribute_multiple():
@ -390,7 +390,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef>.b E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef>.b
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>() E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
failure_demo.py:157: AssertionError failure_demo.py:158: AssertionError
__________________________ TestRaises.test_raises __________________________ __________________________ TestRaises.test_raises __________________________
self = <failure_demo.TestRaises object at 0xdeadbeef> self = <failure_demo.TestRaises object at 0xdeadbeef>
@ -400,7 +400,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> raises(TypeError, int, s) > raises(TypeError, int, s)
E ValueError: invalid literal for int() with base 10: 'qwe' E ValueError: invalid literal for int() with base 10: 'qwe'
failure_demo.py:167: ValueError failure_demo.py:168: ValueError
______________________ TestRaises.test_raises_doesnt _______________________ ______________________ TestRaises.test_raises_doesnt _______________________
self = <failure_demo.TestRaises object at 0xdeadbeef> self = <failure_demo.TestRaises object at 0xdeadbeef>
@ -409,7 +409,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> raises(IOError, int, "3") > raises(IOError, int, "3")
E Failed: DID NOT RAISE <class 'OSError'> E Failed: DID NOT RAISE <class 'OSError'>
failure_demo.py:170: Failed failure_demo.py:171: Failed
__________________________ TestRaises.test_raise ___________________________ __________________________ TestRaises.test_raise ___________________________
self = <failure_demo.TestRaises object at 0xdeadbeef> self = <failure_demo.TestRaises object at 0xdeadbeef>
@ -418,7 +418,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> raise ValueError("demo error") > raise ValueError("demo error")
E ValueError: demo error E ValueError: demo error
failure_demo.py:173: ValueError failure_demo.py:174: ValueError
________________________ TestRaises.test_tupleerror ________________________ ________________________ TestRaises.test_tupleerror ________________________
self = <failure_demo.TestRaises object at 0xdeadbeef> self = <failure_demo.TestRaises object at 0xdeadbeef>
@ -427,7 +427,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> a, b = [1] # NOQA > a, b = [1] # NOQA
E ValueError: not enough values to unpack (expected 2, got 1) E ValueError: not enough values to unpack (expected 2, got 1)
failure_demo.py:176: ValueError failure_demo.py:177: ValueError
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
self = <failure_demo.TestRaises object at 0xdeadbeef> self = <failure_demo.TestRaises object at 0xdeadbeef>
@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> a, b = items.pop() > a, b = items.pop()
E TypeError: 'int' object is not iterable E TypeError: 'int' object is not iterable
failure_demo.py:181: TypeError failure_demo.py:182: TypeError
--------------------------- Captured stdout call --------------------------- --------------------------- Captured stdout call ---------------------------
items is [1, 2, 3] items is [1, 2, 3]
________________________ TestRaises.test_some_error ________________________ ________________________ TestRaises.test_some_error ________________________
@ -449,7 +449,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> if namenotexi: # NOQA > if namenotexi: # NOQA
E NameError: name 'namenotexi' is not defined E NameError: name 'namenotexi' is not defined
failure_demo.py:184: NameError failure_demo.py:185: NameError
____________________ test_dynamic_compile_shows_nicely _____________________ ____________________ test_dynamic_compile_shows_nicely _____________________
def test_dynamic_compile_shows_nicely(): def test_dynamic_compile_shows_nicely():
@ -464,14 +464,14 @@ Here is a nice run of several failures and how ``pytest`` presents things:
sys.modules[name] = module sys.modules[name] = module
> module.foo() > module.foo()
failure_demo.py:202: failure_demo.py:203:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def foo(): def foo():
> assert 1 == 0 > assert 1 == 0
E AssertionError E AssertionError
<0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:199>:2: AssertionError <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:200>:2: AssertionError
____________________ TestMoreErrors.test_complex_error _____________________ ____________________ TestMoreErrors.test_complex_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -485,9 +485,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> somefunc(f(), g()) > somefunc(f(), g())
failure_demo.py:213: failure_demo.py:214:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
failure_demo.py:11: in somefunc failure_demo.py:12: in somefunc
otherfunc(x, y) otherfunc(x, y)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
@ -497,7 +497,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b > assert a == b
E assert 44 == 43 E assert 44 == 43
failure_demo.py:7: AssertionError failure_demo.py:8: AssertionError
___________________ TestMoreErrors.test_z1_unpack_error ____________________ ___________________ TestMoreErrors.test_z1_unpack_error ____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -507,7 +507,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> a, b = items > a, b = items
E ValueError: not enough values to unpack (expected 2, got 0) E ValueError: not enough values to unpack (expected 2, got 0)
failure_demo.py:217: ValueError failure_demo.py:218: ValueError
____________________ TestMoreErrors.test_z2_type_error _____________________ ____________________ TestMoreErrors.test_z2_type_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -517,7 +517,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> a, b = items > a, b = items
E TypeError: 'int' object is not iterable E TypeError: 'int' object is not iterable
failure_demo.py:221: TypeError failure_demo.py:222: TypeError
______________________ TestMoreErrors.test_startswith ______________________ ______________________ TestMoreErrors.test_startswith ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -530,7 +530,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + where False = <built-in method startswith of str object at 0xdeadbeef>('456') E + where False = <built-in method startswith of str object at 0xdeadbeef>('456')
E + where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith E + where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith
failure_demo.py:226: AssertionError failure_demo.py:227: AssertionError
__________________ TestMoreErrors.test_startswith_nested ___________________ __________________ TestMoreErrors.test_startswith_nested ___________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -549,7 +549,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>() E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>() E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>()
failure_demo.py:235: AssertionError failure_demo.py:236: AssertionError
_____________________ TestMoreErrors.test_global_func ______________________ _____________________ TestMoreErrors.test_global_func ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -560,7 +560,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E + where False = isinstance(43, float) E + where False = isinstance(43, float)
E + where 43 = globf(42) E + where 43 = globf(42)
failure_demo.py:238: AssertionError failure_demo.py:239: AssertionError
_______________________ TestMoreErrors.test_instance _______________________ _______________________ TestMoreErrors.test_instance _______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -571,7 +571,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 42 != 42 E assert 42 != 42
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
failure_demo.py:242: AssertionError failure_demo.py:243: AssertionError
_______________________ TestMoreErrors.test_compare ________________________ _______________________ TestMoreErrors.test_compare ________________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -581,7 +581,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 11 < 5 E assert 11 < 5
E + where 11 = globf(10) E + where 11 = globf(10)
failure_demo.py:245: AssertionError failure_demo.py:246: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________ _____________________ TestMoreErrors.test_try_finally ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef> self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert x == 0 > assert x == 0
E assert 1 == 0 E assert 1 == 0
failure_demo.py:250: AssertionError failure_demo.py:251: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________ ___________________ TestCustomAssertMsg.test_single_line ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@ -607,7 +607,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 1 == 2 E assert 1 == 2
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
failure_demo.py:261: AssertionError failure_demo.py:262: AssertionError
____________________ TestCustomAssertMsg.test_multiline ____________________ ____________________ TestCustomAssertMsg.test_multiline ____________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@ -626,7 +626,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 1 == 2 E assert 1 == 2
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
failure_demo.py:268: AssertionError failure_demo.py:269: AssertionError
___________________ TestCustomAssertMsg.test_custom_repr ___________________ ___________________ TestCustomAssertMsg.test_custom_repr ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef> self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@ -648,5 +648,5 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E assert 1 == 2 E assert 1 == 2
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
failure_demo.py:281: AssertionError failure_demo.py:282: AssertionError
======================== 44 failed in 0.12 seconds ========================= ======================== 44 failed in 0.12 seconds =========================

View File

@ -1087,6 +1087,22 @@ passed multiple times. The expected format is ``name=value``. For example::
This tells pytest to ignore deprecation warnings and turn all other warnings This tells pytest to ignore deprecation warnings and turn all other warnings
into errors. For more information please refer to :ref:`warnings`. into errors. For more information please refer to :ref:`warnings`.
.. confval:: junit_duration_report
.. versionadded:: 4.1
Configures how durations are recorded into the JUnit XML report:
* ``total`` (the default): duration times reported include setup, call, and teardown times.
* ``call``: duration times reported include only call times, excluding setup and teardown.
.. code-block:: ini
[pytest]
junit_duration_report = call
.. confval:: junit_family .. confval:: junit_family
.. versionadded:: 4.2 .. versionadded:: 4.2
@ -1102,10 +1118,35 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest] [pytest]
junit_family = xunit2 junit_family = xunit2
.. confval:: junit_logging
.. versionadded:: 3.5
Configures if stdout/stderr should be written to the JUnit XML file. Valid values are
``system-out``, ``system-err``, and ``no`` (the default).
.. code-block:: ini
[pytest]
junit_logging = system-out
.. confval:: junit_log_passing_tests
.. versionadded:: 4.6
If ``junit_logging != "no"``, configures if the captured output should be written
to the JUnit XML file for **passing** tests. Default is ``True``.
.. code-block:: ini
[pytest]
junit_log_passing_tests = False
.. confval:: junit_suite_name .. confval:: junit_suite_name
To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file: To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file:
.. code-block:: ini .. code-block:: ini

View File

@ -400,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta
============================= warnings summary ============================= ============================= warnings summary =============================
test_pytest_warnings.py:1 test_pytest_warnings.py:1
$REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py)
class Test: class Test:
-- Docs: https://docs.pytest.org/en/latest/warnings.html -- Docs: https://docs.pytest.org/en/latest/warnings.html

View File

@ -6,7 +6,7 @@ from setuptools import setup
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [
"py>=1.5.0", "py>=1.5.0",
"six>=1.10.0", "six>=1.10.0",
"setuptools", "packaging",
"attrs>=17.4.0", "attrs>=17.4.0",
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"', 'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
'more-itertools>=4.0.0;python_version>"2.7"', 'more-itertools>=4.0.0;python_version>"2.7"',
@ -14,7 +14,8 @@ INSTALL_REQUIRES = [
'funcsigs>=1.0;python_version<"3.0"', 'funcsigs>=1.0;python_version<"3.0"',
'pathlib2>=2.2.0;python_version<"3.6"', 'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"', 'colorama;sys_platform=="win32"',
"pluggy==0.11", # temporary until 4.6 is released "pluggy>=0.12,<1.0",
"importlib-metadata>=0.12",
"wcwidth", "wcwidth",
] ]

View File

@ -68,7 +68,6 @@ class AssertionRewritingHook(object):
self.session = None self.session = None
self.modules = {} self.modules = {}
self._rewritten_names = set() self._rewritten_names = set()
self._register_with_pkg_resources()
self._must_rewrite = set() self._must_rewrite = set()
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
# which might result in infinite recursion (#3506) # which might result in infinite recursion (#3506)
@ -319,24 +318,6 @@ class AssertionRewritingHook(object):
tp = desc[2] tp = desc[2]
return tp == imp.PKG_DIRECTORY return tp == imp.PKG_DIRECTORY
@classmethod
def _register_with_pkg_resources(cls):
"""
Ensure package resources can be loaded from this loader. May be called
multiple times, as the operation is idempotent.
"""
try:
import pkg_resources
# access an attribute in case a deferred importer is present
pkg_resources.__name__
except ImportError:
return
# Since pytest tests are always located in the file system, the
# DefaultProvider is appropriate.
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
def get_data(self, pathname): def get_data(self, pathname):
"""Optional PEP302 get_data API. """Optional PEP302 get_data API.
""" """
@ -972,6 +953,8 @@ warn_explicit(
""" """
visit `ast.Call` nodes on Python3.5 and after visit `ast.Call` nodes on Python3.5 and after
""" """
if isinstance(call.func, ast.Name) and call.func.id == "all":
return self._visit_all(call)
new_func, func_expl = self.visit(call.func) new_func, func_expl = self.visit(call.func)
arg_expls = [] arg_expls = []
new_args = [] new_args = []
@ -995,6 +978,27 @@ warn_explicit(
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
return res, outer_expl return res, outer_expl
def _visit_all(self, call):
"""Special rewrite for the builtin all function, see #5062"""
if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)):
return
gen_exp = call.args[0]
assertion_module = ast.Module(
body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)]
)
AssertionRewriter(module_path=None, config=None).run(assertion_module)
for_loop = ast.For(
iter=gen_exp.generators[0].iter,
target=gen_exp.generators[0].target,
body=assertion_module.body,
orelse=[],
)
self.statements.append(for_loop)
return (
ast.Num(n=1),
"",
) # Return an empty expression, all the asserts are in the for_loop
def visit_Starred(self, starred): def visit_Starred(self, starred):
# From Python 3.5, a Starred node can appear in a function call # From Python 3.5, a Starred node can appear in a function call
res, expl = self.visit(starred.value) res, expl = self.visit(starred.value)
@ -1005,6 +1009,8 @@ warn_explicit(
""" """
visit `ast.Call nodes on 3.4 and below` visit `ast.Call nodes on 3.4 and below`
""" """
if isinstance(call.func, ast.Name) and call.func.id == "all":
return self._visit_all(call)
new_func, func_expl = self.visit(call.func) new_func, func_expl = self.visit(call.func)
arg_expls = [] arg_expls = []
new_args = [] new_args = []

View File

@ -37,7 +37,6 @@ if _PY3:
else: else:
from funcsigs import signature, Parameter as Parameter from funcsigs import signature, Parameter as Parameter
NoneType = type(None)
NOTSET = object() NOTSET = object()
PY35 = sys.version_info[:2] >= (3, 5) PY35 = sys.version_info[:2] >= (3, 5)

View File

@ -13,8 +13,10 @@ import sys
import types import types
import warnings import warnings
import importlib_metadata
import py import py
import six import six
from packaging.version import Version
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import PluginManager from pluggy import PluginManager
@ -788,25 +790,17 @@ class Config(object):
modules or packages in the distribution package for modules or packages in the distribution package for
all pytest plugins. all pytest plugins.
""" """
import pkg_resources
self.pluginmanager.rewrite_hook = hook self.pluginmanager.rewrite_hook = hook
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# We don't autoload from setuptools entry points, no need to continue. # We don't autoload from setuptools entry points, no need to continue.
return return
# 'RECORD' available for plugins installed normally (pip install)
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
# so it shouldn't be an issue
metadata_files = "RECORD", "SOURCES.txt"
package_files = ( package_files = (
entry.split(",")[0] str(file)
for entrypoint in pkg_resources.iter_entry_points("pytest11") for dist in importlib_metadata.distributions()
for metadata in metadata_files if any(ep.group == "pytest11" for ep in dist.entry_points)
for entry in entrypoint.dist._get_metadata(metadata) for file in dist.files
) )
for name in _iter_rewritable_modules(package_files): for name in _iter_rewritable_modules(package_files):
@ -875,11 +869,10 @@ class Config(object):
def _checkversion(self): def _checkversion(self):
import pytest import pytest
from pkg_resources import parse_version
minver = self.inicfg.get("minversion", None) minver = self.inicfg.get("minversion", None)
if minver: if minver:
if parse_version(minver) > parse_version(pytest.__version__): if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError( raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'" "%s:%d: requires pytest-%s, actual pytest-%s'"
% ( % (

View File

@ -81,6 +81,7 @@ class pytestPDB(object):
_config = None _config = None
_saved = [] _saved = []
_recursive_debug = 0 _recursive_debug = 0
_wrapped_pdb_cls = None
@classmethod @classmethod
def _is_capturing(cls, capman): def _is_capturing(cls, capman):
@ -89,43 +90,138 @@ class pytestPDB(object):
return False return False
@classmethod @classmethod
def _import_pdb_cls(cls): def _import_pdb_cls(cls, capman):
if not cls._config: if not cls._config:
# Happens when using pytest.set_trace outside of a test. # Happens when using pytest.set_trace outside of a test.
return pdb.Pdb return pdb.Pdb
pdb_cls = cls._config.getvalue("usepdb_cls") usepdb_cls = cls._config.getvalue("usepdb_cls")
if not pdb_cls:
return pdb.Pdb
modname, classname = pdb_cls if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
return cls._wrapped_pdb_cls[1]
try: if usepdb_cls:
__import__(modname) modname, classname = usepdb_cls
mod = sys.modules[modname]
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). try:
parts = classname.split(".") __import__(modname)
pdb_cls = getattr(mod, parts[0]) mod = sys.modules[modname]
for part in parts[1:]:
pdb_cls = getattr(pdb_cls, part)
return pdb_cls # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
except Exception as exc: parts = classname.split(".")
value = ":".join((modname, classname)) pdb_cls = getattr(mod, parts[0])
raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc)) for part in parts[1:]:
pdb_cls = getattr(pdb_cls, part)
except Exception as exc:
value = ":".join((modname, classname))
raise UsageError(
"--pdbcls: could not import {!r}: {}".format(value, exc)
)
else:
pdb_cls = pdb.Pdb
wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
return wrapped_cls
@classmethod @classmethod
def _init_pdb(cls, *args, **kwargs): def _get_pdb_wrapper_class(cls, pdb_cls, capman):
import _pytest.config
class PytestPdbWrapper(pdb_cls, object):
_pytest_capman = capman
_continued = False
def do_debug(self, arg):
cls._recursive_debug += 1
ret = super(PytestPdbWrapper, self).do_debug(arg)
cls._recursive_debug -= 1
return ret
def do_continue(self, arg):
ret = super(PytestPdbWrapper, self).do_continue(arg)
if cls._recursive_debug == 0:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
capman = self._pytest_capman
capturing = pytestPDB._is_capturing(capman)
if capturing:
if capturing == "global":
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(
">",
"PDB continue (IO-capturing resumed for %s)"
% capturing,
)
capman.resume()
else:
tw.sep(">", "PDB continue")
cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
self._continued = True
return ret
do_c = do_cont = do_continue
def do_quit(self, arg):
"""Raise Exit outcome when quit command is used in pdb.
This is a bit of a hack - it would be better if BdbQuit
could be handled, but this would require to wrap the
whole pytest run, and adjust the report etc.
"""
ret = super(PytestPdbWrapper, self).do_quit(arg)
if cls._recursive_debug == 0:
outcomes.exit("Quitting debugger")
return ret
do_q = do_quit
do_exit = do_quit
def setup(self, f, tb):
"""Suspend on setup().
Needed after do_continue resumed, and entering another
breakpoint again.
"""
ret = super(PytestPdbWrapper, self).setup(f, tb)
if not ret and self._continued:
# pdb.setup() returns True if the command wants to exit
# from the interaction: do not suspend capturing then.
if self._pytest_capman:
self._pytest_capman.suspend_global_capture(in_=True)
return ret
def get_stack(self, f, t):
stack, i = super(PytestPdbWrapper, self).get_stack(f, t)
if f is None:
# Find last non-hidden frame.
i = max(0, len(stack) - 1)
while i and stack[i][0].f_locals.get("__tracebackhide__", False):
i -= 1
return stack, i
return PytestPdbWrapper
@classmethod
def _init_pdb(cls, method, *args, **kwargs):
""" Initialize PDB debugging, dropping any IO capturing. """ """ Initialize PDB debugging, dropping any IO capturing. """
import _pytest.config import _pytest.config
if cls._pluginmanager is not None: if cls._pluginmanager is not None:
capman = cls._pluginmanager.getplugin("capturemanager") capman = cls._pluginmanager.getplugin("capturemanager")
if capman: else:
capman.suspend(in_=True) capman = None
if capman:
capman.suspend(in_=True)
if cls._config:
tw = _pytest.config.create_terminal_writer(cls._config) tw = _pytest.config.create_terminal_writer(cls._config)
tw.line() tw.line()
if cls._recursive_debug == 0: if cls._recursive_debug == 0:
# Handle header similar to pdb.set_trace in py37+. # Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None) header = kwargs.pop("header", None)
@ -133,112 +229,28 @@ class pytestPDB(object):
tw.sep(">", header) tw.sep(">", header)
else: else:
capturing = cls._is_capturing(capman) capturing = cls._is_capturing(capman)
if capturing: if capturing == "global":
if capturing == "global": tw.sep(">", "PDB %s (IO-capturing turned off)" % (method,))
tw.sep(">", "PDB set_trace (IO-capturing turned off)") elif capturing:
else: tw.sep(
tw.sep( ">",
">", "PDB %s (IO-capturing turned off for %s)"
"PDB set_trace (IO-capturing turned off for %s)" % (method, capturing),
% capturing, )
)
else: else:
tw.sep(">", "PDB set_trace") tw.sep(">", "PDB %s" % (method,))
pdb_cls = cls._import_pdb_cls() _pdb = cls._import_pdb_cls(capman)(**kwargs)
class PytestPdbWrapper(pdb_cls, object): if cls._pluginmanager:
_pytest_capman = capman
_continued = False
def do_debug(self, arg):
cls._recursive_debug += 1
ret = super(PytestPdbWrapper, self).do_debug(arg)
cls._recursive_debug -= 1
return ret
def do_continue(self, arg):
ret = super(PytestPdbWrapper, self).do_continue(arg)
if cls._recursive_debug == 0:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
capman = self._pytest_capman
capturing = pytestPDB._is_capturing(capman)
if capturing:
if capturing == "global":
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(
">",
"PDB continue (IO-capturing resumed for %s)"
% capturing,
)
capman.resume()
else:
tw.sep(">", "PDB continue")
cls._pluginmanager.hook.pytest_leave_pdb(
config=cls._config, pdb=self
)
self._continued = True
return ret
do_c = do_cont = do_continue
def do_quit(self, arg):
"""Raise Exit outcome when quit command is used in pdb.
This is a bit of a hack - it would be better if BdbQuit
could be handled, but this would require to wrap the
whole pytest run, and adjust the report etc.
"""
ret = super(PytestPdbWrapper, self).do_quit(arg)
if cls._recursive_debug == 0:
outcomes.exit("Quitting debugger")
return ret
do_q = do_quit
do_exit = do_quit
def setup(self, f, tb):
"""Suspend on setup().
Needed after do_continue resumed, and entering another
breakpoint again.
"""
ret = super(PytestPdbWrapper, self).setup(f, tb)
if not ret and self._continued:
# pdb.setup() returns True if the command wants to exit
# from the interaction: do not suspend capturing then.
if self._pytest_capman:
self._pytest_capman.suspend_global_capture(in_=True)
return ret
def get_stack(self, f, t):
stack, i = super(PytestPdbWrapper, self).get_stack(f, t)
if f is None:
# Find last non-hidden frame.
i = max(0, len(stack) - 1)
while i and stack[i][0].f_locals.get(
"__tracebackhide__", False
):
i -= 1
return stack, i
_pdb = PytestPdbWrapper(**kwargs)
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
else:
pdb_cls = cls._import_pdb_cls()
_pdb = pdb_cls(**kwargs)
return _pdb return _pdb
@classmethod @classmethod
def set_trace(cls, *args, **kwargs): def set_trace(cls, *args, **kwargs):
"""Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
frame = sys._getframe().f_back frame = sys._getframe().f_back
_pdb = cls._init_pdb(*args, **kwargs) _pdb = cls._init_pdb("set_trace", *args, **kwargs)
_pdb.set_trace(frame) _pdb.set_trace(frame)
@ -265,7 +277,7 @@ class PdbTrace(object):
def _test_pytest_function(pyfuncitem): def _test_pytest_function(pyfuncitem):
_pdb = pytestPDB._init_pdb() _pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
pyfuncitem.obj = _pdb.runcall pyfuncitem.obj = _pdb.runcall
if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch
@ -315,7 +327,7 @@ def _postmortem_traceback(excinfo):
def post_mortem(t): def post_mortem(t):
p = pytestPDB._init_pdb() p = pytestPDB._init_pdb("post_mortem")
p.reset() p.reset()
p.interaction(None, t) p.interaction(None, t)
if p.quitting: if p.quitting:

View File

@ -854,11 +854,9 @@ class FixtureDef(object):
exceptions.append(sys.exc_info()) exceptions.append(sys.exc_info())
if exceptions: if exceptions:
e = exceptions[0] e = exceptions[0]
del ( # Ensure to not keep frame references through traceback.
exceptions del exceptions
) # ensure we don't keep all frames alive because of the traceback
six.reraise(*e) six.reraise(*e)
finally: finally:
hook = self._fixturemanager.session.gethookproxy(request.node.fspath) hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)

View File

@ -142,24 +142,48 @@ def pytest_cmdline_main(config):
def showhelp(config): def showhelp(config):
import textwrap
reporter = config.pluginmanager.get_plugin("terminalreporter") reporter = config.pluginmanager.get_plugin("terminalreporter")
tw = reporter._tw tw = reporter._tw
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()
tw.line()
tw.line( tw.line(
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:" "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
) )
tw.line() tw.line()
columns = tw.fullwidth # costly call columns = tw.fullwidth # costly call
indent_len = 24 # based on argparse's max_help_position=24
indent = " " * indent_len
for name in config._parser._ininames: for name in config._parser._ininames:
help, type, default = config._parser._inidict[name] help, type, default = config._parser._inidict[name]
if type is None: if type is None:
type = "string" type = "string"
spec = "%s (%s)" % (name, type) spec = "%s (%s):" % (name, type)
line = " %-24s %s" % (spec, help) tw.write(" %s" % spec)
tw.line(line[:columns]) spec_len = len(spec)
if spec_len > (indent_len - 3):
# Display help starting at a new line.
tw.line()
helplines = textwrap.wrap(
help,
columns,
initial_indent=indent,
subsequent_indent=indent,
break_on_hyphens=False,
)
for line in helplines:
tw.line(line)
else:
# Display help starting after the spec, following lines indented.
tw.write(" " * (indent_len - spec_len - 2))
wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)
tw.line(wrapped[0])
for line in wrapped[1:]:
tw.line(indent + line)
tw.line() tw.line()
tw.line("environment variables:") tw.line("environment variables:")

View File

@ -167,6 +167,9 @@ class _NodeReporter(object):
self.append(node) self.append(node)
def write_captured_output(self, report): def write_captured_output(self, report):
if not self.xml.log_passing_tests and report.passed:
return
content_out = report.capstdout content_out = report.capstdout
content_log = report.caplog content_log = report.caplog
content_err = report.capstderr content_err = report.capstderr
@ -414,6 +417,12 @@ def pytest_addoption(parser):
"one of no|system-out|system-err", "one of no|system-out|system-err",
default="no", default="no",
) # choices=['no', 'stdout', 'stderr']) ) # choices=['no', 'stdout', 'stderr'])
parser.addini(
"junit_log_passing_tests",
"Capture log information for passing tests to JUnit report: ",
type="bool",
default=True,
)
parser.addini( parser.addini(
"junit_duration_report", "junit_duration_report",
"Duration time to report: one of total|call", "Duration time to report: one of total|call",
@ -437,6 +446,7 @@ def pytest_configure(config):
config.getini("junit_logging"), config.getini("junit_logging"),
config.getini("junit_duration_report"), config.getini("junit_duration_report"),
config.getini("junit_family"), config.getini("junit_family"),
config.getini("junit_log_passing_tests"),
) )
config.pluginmanager.register(config._xml) config.pluginmanager.register(config._xml)
@ -472,12 +482,14 @@ class LogXML(object):
logging="no", logging="no",
report_duration="total", report_duration="total",
family="xunit1", family="xunit1",
log_passing_tests=True,
): ):
logfile = os.path.expanduser(os.path.expandvars(logfile)) logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile))
self.prefix = prefix self.prefix = prefix
self.suite_name = suite_name self.suite_name = suite_name
self.logging = logging self.logging = logging
self.log_passing_tests = log_passing_tests
self.report_duration = report_duration self.report_duration = report_duration
self.family = family self.family = family
self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0)

View File

@ -18,6 +18,11 @@ from _pytest.pathlib import Path
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
def _remove_ansi_escape_sequences(text):
return _ANSI_ESCAPE_SEQ.sub("", text)
class ColoredLevelFormatter(logging.Formatter): class ColoredLevelFormatter(logging.Formatter):
@ -72,6 +77,36 @@ class ColoredLevelFormatter(logging.Formatter):
return super(ColoredLevelFormatter, self).format(record) return super(ColoredLevelFormatter, self).format(record)
if not six.PY2:
# Formatter classes don't support format styles in PY2
class PercentStyleMultiline(logging.PercentStyle):
"""A logging style with special support for multiline messages.
If the message of a record consists of multiple lines, this style
formats the message as if each line were logged separately.
"""
@staticmethod
def _update_message(record_dict, message):
tmp = record_dict.copy()
tmp["message"] = message
return tmp
def format(self, record):
if "\n" in record.message:
lines = record.message.splitlines()
formatted = self._fmt % self._update_message(record.__dict__, lines[0])
# TODO optimize this by introducing an option that tells the
# logging framework that the indentation doesn't
# change. This allows to compute the indentation only once.
indentation = _remove_ansi_escape_sequences(formatted).find(lines[0])
lines[0] = formatted
return ("\n" + " " * indentation).join(lines)
else:
return self._fmt % record.__dict__
def get_option_ini(config, *names): def get_option_ini(config, *names):
for name in names: for name in names:
ret = config.getoption(name) # 'default' arg won't work as expected ret = config.getoption(name) # 'default' arg won't work as expected
@ -257,8 +292,8 @@ class LogCaptureFixture(object):
@property @property
def text(self): def text(self):
"""Returns the log text.""" """Returns the formatted log text."""
return self.handler.stream.getvalue() return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
@property @property
def records(self): def records(self):
@ -394,7 +429,7 @@ class LoggingPlugin(object):
config.option.verbose = 1 config.option.verbose = 1
self.print_logs = get_option_ini(config, "log_print") self.print_logs = get_option_ini(config, "log_print")
self.formatter = logging.Formatter( self.formatter = self._create_formatter(
get_option_ini(config, "log_format"), get_option_ini(config, "log_format"),
get_option_ini(config, "log_date_format"), get_option_ini(config, "log_date_format"),
) )
@ -428,6 +463,22 @@ class LoggingPlugin(object):
if self._log_cli_enabled(): if self._log_cli_enabled():
self._setup_cli_logging() self._setup_cli_logging()
def _create_formatter(self, log_format, log_date_format):
# color option doesn't exist if terminal plugin is disabled
color = getattr(self._config.option, "color", "no")
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
log_format
):
formatter = ColoredLevelFormatter(
create_terminal_writer(self._config), log_format, log_date_format
)
else:
formatter = logging.Formatter(log_format, log_date_format)
if not six.PY2:
formatter._style = PercentStyleMultiline(formatter._style._fmt)
return formatter
def _setup_cli_logging(self): def _setup_cli_logging(self):
config = self._config config = self._config
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
@ -438,23 +489,12 @@ class LoggingPlugin(object):
capture_manager = config.pluginmanager.get_plugin("capturemanager") capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works. # if capturemanager plugin is disabled, live logging still works.
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
log_cli_format = get_option_ini(config, "log_cli_format", "log_format")
log_cli_date_format = get_option_ini( log_cli_formatter = self._create_formatter(
config, "log_cli_date_format", "log_date_format" get_option_ini(config, "log_cli_format", "log_format"),
get_option_ini(config, "log_cli_date_format", "log_date_format"),
) )
if (
config.option.color != "no"
and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
):
log_cli_formatter = ColoredLevelFormatter(
create_terminal_writer(config),
log_cli_format,
datefmt=log_cli_date_format,
)
else:
log_cli_formatter = logging.Formatter(
log_cli_format, datefmt=log_cli_date_format
)
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
self.log_cli_handler = log_cli_handler self.log_cli_handler = log_cli_handler
self.live_logs_context = lambda: catching_logs( self.live_logs_context = lambda: catching_logs(

View File

@ -9,6 +9,8 @@ from __future__ import print_function
import sys import sys
from packaging.version import Version
class OutcomeException(BaseException): class OutcomeException(BaseException):
""" OutcomeException and its subclass instances indicate and """ OutcomeException and its subclass instances indicate and
@ -155,7 +157,7 @@ def importorskip(modname, minversion=None, reason=None):
__tracebackhide__ = True __tracebackhide__ = True
compile(modname, "", "eval") # to catch syntaxerrors compile(modname, "", "eval") # to catch syntaxerrors
should_skip = False import_exc = None
with warnings.catch_warnings(): with warnings.catch_warnings():
# make sure to ignore ImportWarnings that might happen because # make sure to ignore ImportWarnings that might happen because
@ -164,27 +166,19 @@ def importorskip(modname, minversion=None, reason=None):
warnings.simplefilter("ignore") warnings.simplefilter("ignore")
try: try:
__import__(modname) __import__(modname)
except ImportError: except ImportError as exc:
# Do not raise chained exception here(#1485) # Do not raise chained exception here(#1485)
should_skip = True import_exc = exc
if should_skip: if import_exc:
if reason is None: if reason is None:
reason = "could not import %r" % (modname,) reason = "could not import %r: %s" % (modname, import_exc)
raise Skipped(reason, allow_module_level=True) raise Skipped(reason, allow_module_level=True)
mod = sys.modules[modname] mod = sys.modules[modname]
if minversion is None: if minversion is None:
return mod return mod
verattr = getattr(mod, "__version__", None) verattr = getattr(mod, "__version__", None)
if minversion is not None: if minversion is not None:
try: if verattr is None or Version(verattr) < Version(minversion):
from pkg_resources import parse_version as pv
except ImportError:
raise Skipped(
"we have a required version for %r but can not import "
"pkg_resources to parse version strings." % (modname,),
allow_module_level=True,
)
if verattr is None or pv(verattr) < pv(minversion):
raise Skipped( raise Skipped(
"module %r has __version__ %r, required is: %r" "module %r has __version__ %r, required is: %r"
% (modname, verattr, minversion), % (modname, verattr, minversion),

View File

@ -153,15 +153,6 @@ class LsofFdLeakChecker(object):
item.warn(pytest.PytestWarning("\n".join(error))) item.warn(pytest.PytestWarning("\n".join(error)))
# XXX copied from execnet's conftest.py - needs to be merged
winpymap = {
"python2.7": r"C:\Python27\python.exe",
"python3.4": r"C:\Python34\python.exe",
"python3.5": r"C:\Python35\python.exe",
"python3.6": r"C:\Python36\python.exe",
}
# used at least by pytest-xdist plugin # used at least by pytest-xdist plugin
@ -517,6 +508,10 @@ class Testdir(object):
# Discard outer pytest options. # Discard outer pytest options.
mp.delenv("PYTEST_ADDOPTS", raising=False) mp.delenv("PYTEST_ADDOPTS", raising=False)
# Environment (updates) for inner runs.
tmphome = str(self.tmpdir)
self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome}
def __repr__(self): def __repr__(self):
return "<Testdir %r>" % (self.tmpdir,) return "<Testdir %r>" % (self.tmpdir,)
@ -813,8 +808,8 @@ class Testdir(object):
try: try:
# Do not load user config (during runs only). # Do not load user config (during runs only).
mp_run = MonkeyPatch() mp_run = MonkeyPatch()
mp_run.setenv("HOME", str(self.tmpdir)) for k, v in self._env_run_update.items():
mp_run.setenv("USERPROFILE", str(self.tmpdir)) mp_run.setenv(k, v)
finalizers.append(mp_run.undo) finalizers.append(mp_run.undo)
# When running pytest inline any plugins active in the main test # When running pytest inline any plugins active in the main test
@ -1054,9 +1049,7 @@ class Testdir(object):
env["PYTHONPATH"] = os.pathsep.join( env["PYTHONPATH"] = os.pathsep.join(
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
) )
# Do not load user config. env.update(self._env_run_update)
env["HOME"] = str(self.tmpdir)
env["USERPROFILE"] = env["HOME"]
kw["env"] = env kw["env"] = env
if stdin is Testdir.CLOSE_STDIN: if stdin is Testdir.CLOSE_STDIN:
@ -1242,7 +1235,12 @@ class Testdir(object):
if sys.platform.startswith("freebsd"): if sys.platform.startswith("freebsd"):
pytest.xfail("pexpect does not work reliably on freebsd") pytest.xfail("pexpect does not work reliably on freebsd")
logfile = self.tmpdir.join("spawn.out").open("wb") logfile = self.tmpdir.join("spawn.out").open("wb")
child = pexpect.spawn(cmd, logfile=logfile)
# Do not load user config.
env = os.environ.copy()
env.update(self._env_run_update)
child = pexpect.spawn(cmd, logfile=logfile, env=env)
self.request.addfinalizer(logfile.close) self.request.addfinalizer(logfile.close)
child.timeout = expect_timeout child.timeout = expect_timeout
return child return child

View File

@ -31,7 +31,6 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import isclass from _pytest.compat import isclass
from _pytest.compat import isfunction from _pytest.compat import isfunction
from _pytest.compat import NoneType
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import REGEX_TYPE from _pytest.compat import REGEX_TYPE
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
@ -1194,7 +1193,7 @@ def _idval(val, argname, idx, idfn, item, config):
if isinstance(val, STRING_TYPES): if isinstance(val, STRING_TYPES):
return _ascii_escaped_by_config(val, config) return _ascii_escaped_by_config(val, config)
elif isinstance(val, (float, int, bool, NoneType)): elif val is None or isinstance(val, (float, int, bool)):
return str(val) return str(val)
elif isinstance(val, REGEX_TYPE): elif isinstance(val, REGEX_TYPE):
return ascii_escaped(val.pattern) return ascii_escaped(val.pattern)

View File

@ -170,7 +170,7 @@ def getreportopt(config):
if char == "a": if char == "a":
reportopts = "sxXwEf" reportopts = "sxXwEf"
elif char == "A": elif char == "A":
reportopts = "sxXwEfpP" reportopts = "PpsxXwEf"
break break
elif char not in reportopts: elif char not in reportopts:
reportopts += char reportopts += char

View File

@ -60,29 +60,29 @@ class TempPathFactory(object):
def getbasetemp(self): def getbasetemp(self):
""" return base temporary directory. """ """ return base temporary directory. """
if self._basetemp is None: if self._basetemp is not None:
if self._given_basetemp is not None:
basetemp = self._given_basetemp
ensure_reset_dir(basetemp)
basetemp = basetemp.resolve()
else:
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
temproot = Path(from_env or tempfile.gettempdir()).resolve()
user = get_user() or "unknown"
# use a sub-directory in the temproot to speed-up
# make_numbered_dir() call
rootdir = temproot.joinpath("pytest-of-{}".format(user))
rootdir.mkdir(exist_ok=True)
basetemp = make_numbered_dir_with_cleanup(
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
)
assert basetemp is not None
self._basetemp = t = basetemp
self._trace("new basetemp", t)
return t
else:
return self._basetemp return self._basetemp
if self._given_basetemp is not None:
basetemp = self._given_basetemp
ensure_reset_dir(basetemp)
basetemp = basetemp.resolve()
else:
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
temproot = Path(from_env or tempfile.gettempdir()).resolve()
user = get_user() or "unknown"
# use a sub-directory in the temproot to speed-up
# make_numbered_dir() call
rootdir = temproot.joinpath("pytest-of-{}".format(user))
rootdir.mkdir(exist_ok=True)
basetemp = make_numbered_dir_with_cleanup(
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
)
assert basetemp is not None, basetemp
self._basetemp = t = basetemp
self._trace("new basetemp", t)
return t
@attr.s @attr.s
class TempdirFactory(object): class TempdirFactory(object):

View File

@ -9,6 +9,7 @@ import textwrap
import types import types
import attr import attr
import importlib_metadata
import py import py
import six import six
@ -111,8 +112,6 @@ class TestGeneralUsage(object):
@pytest.mark.parametrize("load_cov_early", [True, False]) @pytest.mark.parametrize("load_cov_early", [True, False])
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
pkg_resources = pytest.importorskip("pkg_resources")
testdir.makepyfile(mytestplugin1_module="") testdir.makepyfile(mytestplugin1_module="")
testdir.makepyfile(mytestplugin2_module="") testdir.makepyfile(mytestplugin2_module="")
testdir.makepyfile(mycov_module="") testdir.makepyfile(mycov_module="")
@ -124,38 +123,28 @@ class TestGeneralUsage(object):
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = attr.ib() name = attr.ib()
module = attr.ib() module = attr.ib()
version = "1.0" group = "pytest11"
@property
def project_name(self):
return self.name
def load(self): def load(self):
__import__(self.module) __import__(self.module)
loaded.append(self.name) loaded.append(self.name)
return sys.modules[self.module] return sys.modules[self.module]
@property
def dist(self):
return self
def _get_metadata(self, *args):
return []
entry_points = [ entry_points = [
DummyEntryPoint("myplugin1", "mytestplugin1_module"), DummyEntryPoint("myplugin1", "mytestplugin1_module"),
DummyEntryPoint("myplugin2", "mytestplugin2_module"), DummyEntryPoint("myplugin2", "mytestplugin2_module"),
DummyEntryPoint("mycov", "mycov_module"), DummyEntryPoint("mycov", "mycov_module"),
] ]
def my_iter(group, name=None): @attr.s
assert group == "pytest11" class DummyDist(object):
for ep in entry_points: entry_points = attr.ib()
if name is not None and ep.name != name: files = ()
continue
yield ep
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) def my_dists():
return (DummyDist(entry_points),)
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
params = ("-p", "mycov") if load_cov_early else () params = ("-p", "mycov") if load_cov_early else ()
testdir.runpytest_inprocess(*params) testdir.runpytest_inprocess(*params)
if load_cov_early: if load_cov_early:

View File

@ -30,6 +30,7 @@ def pytest_collection_modifyitems(config, items):
slowest_items.append(item) slowest_items.append(item)
else: else:
slow_items.append(item) slow_items.append(item)
item.add_marker(pytest.mark.slow)
else: else:
marker = item.get_closest_marker("slow") marker = item.get_closest_marker("slow")
if marker: if marker:

View File

@ -2,7 +2,9 @@
import logging import logging
import py.io import py.io
import six
import pytest
from _pytest.logging import ColoredLevelFormatter from _pytest.logging import ColoredLevelFormatter
@ -35,3 +37,31 @@ def test_coloredlogformatter():
formatter = ColoredLevelFormatter(tw, logfmt) formatter = ColoredLevelFormatter(tw, logfmt)
output = formatter.format(record) output = formatter.format(record)
assert output == ("dummypath 10 INFO Test Message") assert output == ("dummypath 10 INFO Test Message")
@pytest.mark.skipif(
six.PY2, reason="Formatter classes don't support format styles in PY2"
)
def test_multiline_message():
from _pytest.logging import PercentStyleMultiline
logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
record = logging.LogRecord(
name="dummy",
level=logging.INFO,
pathname="dummypath",
lineno=10,
msg="Test Message line1\nline2",
args=(),
exc_info=False,
)
# this is called by logging.Formatter.format
record.message = record.getMessage()
style = PercentStyleMultiline(logfmt)
output = style.format(record)
assert output == (
"dummypath 10 INFO Test Message line1\n"
" line2"
)

View File

@ -1084,3 +1084,48 @@ def test_log_set_path(testdir):
with open(os.path.join(report_dir_base, "test_second"), "r") as rfh: with open(os.path.join(report_dir_base, "test_second"), "r") as rfh:
content = rfh.read() content = rfh.read()
assert "message from test 2" in content assert "message from test 2" in content
def test_colored_captured_log(testdir):
"""
Test that the level names of captured log messages of a failing test are
colored.
"""
testdir.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.info('text going to logger from call')
assert False
"""
)
result = testdir.runpytest("--log-level=INFO", "--color=yes")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*-- Captured log call --*",
"\x1b[32mINFO \x1b[0m*text going to logger from call",
]
)
def test_colored_ansi_esc_caplogtext(testdir):
"""
Make sure that caplog.text does not contain ANSI escape sequences.
"""
testdir.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo(caplog):
logger.info('text going to logger from call')
assert '\x1b' not in caplog.text
"""
)
result = testdir.runpytest("--log-level=INFO", "--color=yes")
assert result.ret == 0

View File

@ -137,12 +137,12 @@ class TestImportHookInstallation(object):
def test_pytest_plugins_rewrite_module_names_correctly(self, testdir): def test_pytest_plugins_rewrite_module_names_correctly(self, testdir):
"""Test that we match files correctly when they are marked for rewriting (#2939).""" """Test that we match files correctly when they are marked for rewriting (#2939)."""
contents = { contents = {
"conftest.py": """ "conftest.py": """\
pytest_plugins = "ham" pytest_plugins = "ham"
""", """,
"ham.py": "", "ham.py": "",
"hamster.py": "", "hamster.py": "",
"test_foo.py": """ "test_foo.py": """\
def test_foo(pytestconfig): def test_foo(pytestconfig):
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
@ -153,14 +153,13 @@ class TestImportHookInstallation(object):
assert result.ret == 0 assert result.ret == 0
@pytest.mark.parametrize("mode", ["plain", "rewrite"]) @pytest.mark.parametrize("mode", ["plain", "rewrite"])
@pytest.mark.parametrize("plugin_state", ["development", "installed"]) def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch):
def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch):
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
# Make sure the hook is installed early enough so that plugins # Make sure the hook is installed early enough so that plugins
# installed via setuptools are rewritten. # installed via setuptools are rewritten.
testdir.tmpdir.join("hampkg").ensure(dir=1) testdir.tmpdir.join("hampkg").ensure(dir=1)
contents = { contents = {
"hampkg/__init__.py": """ "hampkg/__init__.py": """\
import pytest import pytest
@pytest.fixture @pytest.fixture
@ -169,7 +168,7 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value assert values.pop(0) == value
return check return check
""", """,
"spamplugin.py": """ "spamplugin.py": """\
import pytest import pytest
from hampkg import check_first2 from hampkg import check_first2
@ -179,46 +178,31 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value assert values.pop(0) == value
return check return check
""", """,
"mainwrapper.py": """ "mainwrapper.py": """\
import pytest, pkg_resources import pytest, importlib_metadata
plugin_state = "{plugin_state}"
class DummyDistInfo(object):
project_name = 'spam'
version = '1.0'
def _get_metadata(self, name):
# 'RECORD' meta-data only available in installed plugins
if name == 'RECORD' and plugin_state == "installed":
return ['spamplugin.py,sha256=abc,123',
'hampkg/__init__.py,sha256=abc,123']
# 'SOURCES.txt' meta-data only available for plugins in development mode
elif name == 'SOURCES.txt' and plugin_state == "development":
return ['spamplugin.py',
'hampkg/__init__.py']
return []
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = 'spam' name = 'spam'
module_name = 'spam.py' module_name = 'spam.py'
attrs = () group = 'pytest11'
extras = None
dist = DummyDistInfo()
def load(self, require=True, *args, **kwargs): def load(self):
import spamplugin import spamplugin
return spamplugin return spamplugin
def iter_entry_points(group, name=None): class DummyDistInfo(object):
yield DummyEntryPoint() version = '1.0'
files = ('spamplugin.py', 'hampkg/__init__.py')
entry_points = (DummyEntryPoint(),)
metadata = {'name': 'foo'}
pkg_resources.iter_entry_points = iter_entry_points def distributions():
return (DummyDistInfo(),)
importlib_metadata.distributions = distributions
pytest.main() pytest.main()
""".format( """,
plugin_state=plugin_state "test_foo.py": """\
),
"test_foo.py": """
def test(check_first): def test(check_first):
check_first([10, 30], 30) check_first([10, 30], 30)
@ -605,7 +589,10 @@ class TestAssert_reprcompare(object):
return "\xff" return "\xff"
expl = callequal(A(), "1") expl = callequal(A(), "1")
assert expl if PY3:
assert expl == ["ÿ == '1'", "+ 1"]
else:
assert expl == [u"\ufffd == '1'", u"+ 1"]
def test_format_nonascii_explanation(self): def test_format_nonascii_explanation(self):
assert util.format_explanation("λ") assert util.format_explanation("λ")

View File

@ -656,6 +656,12 @@ class TestAssertionRewrite(object):
else: else:
assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"]
def test_unroll_expression(self):
def f():
assert all(x == 1 for x in range(10))
assert "0 == 1" in getmsg(f)
def test_custom_repr_non_ascii(self): def test_custom_repr_non_ascii(self):
def f(): def f():
class A(object): class A(object):
@ -671,6 +677,53 @@ class TestAssertionRewrite(object):
assert "UnicodeDecodeError" not in msg assert "UnicodeDecodeError" not in msg
assert "UnicodeEncodeError" not in msg assert "UnicodeEncodeError" not in msg
def test_unroll_generator(self, testdir):
testdir.makepyfile(
"""
def check_even(num):
if num % 2 == 0:
return True
return False
def test_generator():
odd_list = list(range(1,9,2))
assert all(check_even(num) for num in odd_list)"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
def test_unroll_list_comprehension(self, testdir):
testdir.makepyfile(
"""
def check_even(num):
if num % 2 == 0:
return True
return False
def test_list_comprehension():
odd_list = list(range(1,9,2))
assert all([check_even(num) for num in odd_list])"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
def test_for_loop(self, testdir):
testdir.makepyfile(
"""
def check_even(num):
if num % 2 == 0:
return True
return False
def test_for_loop():
odd_list = list(range(1,9,2))
for num in odd_list:
assert check_even(num)
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"])
class TestRewriteOnImport(object): class TestRewriteOnImport(object):
def test_pycache_is_a_file(self, testdir): def test_pycache_is_a_file(self, testdir):

View File

@ -6,7 +6,7 @@ from __future__ import print_function
import sys import sys
import textwrap import textwrap
import attr import importlib_metadata
import _pytest._code import _pytest._code
import pytest import pytest
@ -532,32 +532,26 @@ def test_options_on_small_file_do_not_blow_up(testdir):
def test_preparse_ordering_with_setuptools(testdir, monkeypatch): def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None): class EntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" class PseudoPlugin(object):
version = "1.0" x = 42
def _get_metadata(self, name): return PseudoPlugin()
return ["foo.txt,sha256=abc,123"]
class EntryPoint(object): class Dist(object):
name = "mytestplugin" files = ()
dist = Dist() entry_points = (EntryPoint(),)
def load(self): def my_dists():
class PseudoPlugin(object): return (Dist,)
x = 42
return PseudoPlugin() monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
testdir.makeconftest( testdir.makeconftest(
""" """
pytest_plugins = "mytestplugin", pytest_plugins = "mytestplugin",
@ -570,60 +564,50 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
def test_setuptools_importerror_issue1479(testdir, monkeypatch): def test_setuptools_importerror_issue1479(testdir, monkeypatch):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None): class DummyEntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" raise ImportError("Don't hide me!")
version = "1.0"
def _get_metadata(self, name): class Distribution(object):
return ["foo.txt,sha256=abc,123"] version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object): def distributions():
name = "mytestplugin" return (Distribution(),)
dist = Dist()
def load(self): monkeypatch.setattr(importlib_metadata, "distributions", distributions)
raise ImportError("Don't hide me!")
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
with pytest.raises(ImportError): with pytest.raises(ImportError):
testdir.parseconfig() testdir.parseconfig()
@pytest.mark.parametrize("block_it", [True, False]) @pytest.mark.parametrize("block_it", [True, False])
def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
plugin_module_placeholder = object() plugin_module_placeholder = object()
def my_iter(group, name=None): class DummyEntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" return plugin_module_placeholder
version = "1.0"
def _get_metadata(self, name): class Distribution(object):
return ["foo.txt,sha256=abc,123"] version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object): def distributions():
name = "mytestplugin" return (Distribution(),)
dist = Dist()
def load(self): monkeypatch.setattr(importlib_metadata, "distributions", distributions)
return plugin_module_placeholder
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
args = ("-p", "no:mytestplugin") if block_it else () args = ("-p", "no:mytestplugin") if block_it else ()
config = testdir.parseconfig(*args) config = testdir.parseconfig(*args)
config.pluginmanager.import_plugin("mytestplugin") config.pluginmanager.import_plugin("mytestplugin")
@ -640,37 +624,26 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block
"parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)] "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
) )
def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load):
pkg_resources = pytest.importorskip("pkg_resources")
def my_iter(group, name=None):
assert group == "pytest11"
assert name == "mytestplugin"
return iter([DummyEntryPoint()])
@attr.s
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = "mytestplugin" project_name = name = "mytestplugin"
group = "pytest11"
version = "1.0" version = "1.0"
@property
def project_name(self):
return self.name
def load(self): def load(self):
return sys.modules[self.name] return sys.modules[self.name]
@property class Distribution(object):
def dist(self): entry_points = (DummyEntryPoint(),)
return self files = ()
def _get_metadata(self, *args):
return []
class PseudoPlugin(object): class PseudoPlugin(object):
x = 42 x = 42
def distributions():
return (Distribution(),)
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) monkeypatch.setattr(importlib_metadata, "distributions", distributions)
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin())
config = testdir.parseconfig(*parse_args) config = testdir.parseconfig(*parse_args)
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None

View File

@ -3,16 +3,10 @@ from __future__ import absolute_import
from __future__ import division from __future__ import division
from __future__ import print_function from __future__ import print_function
import pkg_resources import importlib_metadata
import pytest
@pytest.mark.parametrize("entrypoint", ["py.test", "pytest"])
def test_entry_point_exist(entrypoint):
assert entrypoint in pkg_resources.get_entry_map("pytest")["console_scripts"]
def test_pytest_entry_points_are_identical(): def test_pytest_entry_points_are_identical():
entryMap = pkg_resources.get_entry_map("pytest")["console_scripts"] dist = importlib_metadata.distribution("pytest")
assert entryMap["pytest"].module_name == entryMap["py.test"].module_name entry_map = {ep.name: ep for ep in dist.entry_points}
assert entry_map["pytest"].value == entry_map["py.test"].value

View File

@ -1332,3 +1332,30 @@ def test_escaped_skipreason_issue3533(testdir):
snode = node.find_first_by_tag("skipped") snode = node.find_first_by_tag("skipped")
assert "1 <> 2" in snode.text assert "1 <> 2" in snode.text
snode.assert_attr(message="1 <> 2") snode.assert_attr(message="1 <> 2")
def test_logging_passing_tests_disabled_does_not_log_test_output(testdir):
testdir.makeini(
"""
[pytest]
junit_log_passing_tests=False
junit_logging=system-out
"""
)
testdir.makepyfile(
"""
import pytest
import logging
import sys
def test_func():
sys.stdout.write('This is stdout')
sys.stderr.write('This is stderr')
logging.warning('hello')
"""
)
result, dom = runandparse(testdir)
assert result.ret == 0
node = dom.find_first_by_tag("testcase")
assert len(node.find_by_tag("system-err")) == 0
assert len(node.find_by_tag("system-out")) == 0

View File

@ -638,36 +638,35 @@ class TestPDB(object):
class pytestPDBTest(_pytest.debugging.pytestPDB): class pytestPDBTest(_pytest.debugging.pytestPDB):
@classmethod @classmethod
def set_trace(cls, *args, **kwargs): def set_trace(cls, *args, **kwargs):
# Init _PdbWrapper to handle capturing. # Init PytestPdbWrapper to handle capturing.
_pdb = cls._init_pdb(*args, **kwargs) _pdb = cls._init_pdb("set_trace", *args, **kwargs)
# Mock out pdb.Pdb.do_continue. # Mock out pdb.Pdb.do_continue.
import pdb import pdb
pdb.Pdb.do_continue = lambda self, arg: None pdb.Pdb.do_continue = lambda self, arg: None
print("=== SET_TRACE ===") print("===" + " SET_TRACE ===")
assert input() == "debug set_trace()" assert input() == "debug set_trace()"
# Simulate _PdbWrapper.do_debug # Simulate PytestPdbWrapper.do_debug
cls._recursive_debug += 1 cls._recursive_debug += 1
print("ENTERING RECURSIVE DEBUGGER") print("ENTERING RECURSIVE DEBUGGER")
print("=== SET_TRACE_2 ===") print("===" + " SET_TRACE_2 ===")
assert input() == "c" assert input() == "c"
_pdb.do_continue("") _pdb.do_continue("")
print("=== SET_TRACE_3 ===") print("===" + " SET_TRACE_3 ===")
# Simulate _PdbWrapper.do_debug # Simulate PytestPdbWrapper.do_debug
print("LEAVING RECURSIVE DEBUGGER") print("LEAVING RECURSIVE DEBUGGER")
cls._recursive_debug -= 1 cls._recursive_debug -= 1
print("=== SET_TRACE_4 ===") print("===" + " SET_TRACE_4 ===")
assert input() == "c" assert input() == "c"
_pdb.do_continue("") _pdb.do_continue("")
def do_continue(self, arg): def do_continue(self, arg):
print("=== do_continue") print("=== do_continue")
# _PdbWrapper.do_continue("")
monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest) monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest)
@ -677,7 +676,7 @@ class TestPDB(object):
set_trace() set_trace()
""" """
) )
child = testdir.spawn_pytest("%s %s" % (p1, capture_arg)) child = testdir.spawn_pytest("--tb=short %s %s" % (p1, capture_arg))
child.expect("=== SET_TRACE ===") child.expect("=== SET_TRACE ===")
before = child.before.decode("utf8") before = child.before.decode("utf8")
if not capture_arg: if not capture_arg:
@ -827,7 +826,7 @@ class TestPDB(object):
result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"])
assert custom_pdb_calls == [] assert custom_pdb_calls == []
def test_pdb_custom_cls_with_settrace(self, testdir, monkeypatch): def test_pdb_custom_cls_with_set_trace(self, testdir, monkeypatch):
testdir.makepyfile( testdir.makepyfile(
custom_pdb=""" custom_pdb="""
class CustomPdb(object): class CustomPdb(object):
@ -1037,24 +1036,28 @@ def test_trace_after_runpytest(testdir):
from _pytest.debugging import pytestPDB from _pytest.debugging import pytestPDB
def test_outer(testdir): def test_outer(testdir):
from _pytest.debugging import pytestPDB
assert len(pytestPDB._saved) == 1 assert len(pytestPDB._saved) == 1
testdir.runpytest("-k test_inner") testdir.makepyfile(
\"""
from _pytest.debugging import pytestPDB
__import__('pdb').set_trace() def test_inner():
assert len(pytestPDB._saved) == 2
print()
print("test_inner_" + "end")
\"""
)
def test_inner(testdir): result = testdir.runpytest("-s", "-k", "test_inner")
assert len(pytestPDB._saved) == 2 assert result.ret == 0
assert len(pytestPDB._saved) == 1
""" """
) )
child = testdir.spawn_pytest("-p pytester %s -k test_outer" % p1) result = testdir.runpytest_subprocess("-s", "-p", "pytester", str(p1))
child.expect(r"\(Pdb") result.stdout.fnmatch_lines(["test_inner_end"])
child.sendline("c") assert result.ret == 0
rest = child.read().decode("utf8")
TestPDB.flush(child)
assert child.exitstatus == 0, rest
def test_quit_with_swallowed_SystemExit(testdir): def test_quit_with_swallowed_SystemExit(testdir):
@ -1139,14 +1142,14 @@ def test_pdbcls_via_local_module(testdir):
p1 = testdir.makepyfile( p1 = testdir.makepyfile(
""" """
def test(): def test():
print("before_settrace") print("before_set_trace")
__import__("pdb").set_trace() __import__("pdb").set_trace()
""", """,
mypdb=""" mypdb="""
class Wrapped: class Wrapped:
class MyPdb: class MyPdb:
def set_trace(self, *args): def set_trace(self, *args):
print("settrace_called", args) print("set_trace_called", args)
def runcall(self, *args, **kwds): def runcall(self, *args, **kwds):
print("runcall_called", args, kwds) print("runcall_called", args, kwds)
@ -1168,7 +1171,7 @@ def test_pdbcls_via_local_module(testdir):
str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True
) )
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["*settrace_called*", "* 1 passed in *"]) result.stdout.fnmatch_lines(["*set_trace_called*", "* 1 passed in *"])
# Ensure that it also works with --trace. # Ensure that it also works with --trace.
result = testdir.runpytest( result = testdir.runpytest(
@ -1202,3 +1205,33 @@ def test_raises_bdbquit_with_eoferror(testdir):
result = testdir.runpytest(str(p1)) result = testdir.runpytest(str(p1))
result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"]) result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"])
assert result.ret == 1 assert result.ret == 1
def test_pdb_wrapper_class_is_reused(testdir):
p1 = testdir.makepyfile(
"""
def test():
__import__("pdb").set_trace()
__import__("pdb").set_trace()
import mypdb
instances = mypdb.instances
assert len(instances) == 2
assert instances[0].__class__ is instances[1].__class__
""",
mypdb="""
instances = []
class MyPdb:
def __init__(self, *args, **kwargs):
instances.append(self)
def set_trace(self, *args):
print("set_trace_called", args)
""",
)
result = testdir.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True)
assert result.ret == 0
result.stdout.fnmatch_lines(
["*set_trace_called*", "*set_trace_called*", "* 1 passed in *"]
)

View File

@ -559,3 +559,29 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir):
assert stdout.splitlines() == [b"", b"stdout"] assert stdout.splitlines() == [b"", b"stdout"]
assert stderr.splitlines() == [b"stderr"] assert stderr.splitlines() == [b"stderr"]
assert proc.returncode == 0 assert proc.returncode == 0
def test_spawn_uses_tmphome(testdir):
import os
tmphome = str(testdir.tmpdir)
# Does use HOME only during run.
assert os.environ.get("HOME") != tmphome
testdir._env_run_update["CUSTOMENV"] = "42"
p1 = testdir.makepyfile(
"""
import os
def test():
assert os.environ["HOME"] == {tmphome!r}
assert os.environ["CUSTOMENV"] == "42"
""".format(
tmphome=tmphome
)
)
child = testdir.spawn_pytest(str(p1))
out = child.read()
assert child.wait() == 0, out.decode("utf8")

View File

@ -172,7 +172,7 @@ class SessionTests(object):
) )
try: try:
reprec = testdir.inline_run(testdir.tmpdir) reprec = testdir.inline_run(testdir.tmpdir)
except pytest.skip.Exception: # pragma: no covers except pytest.skip.Exception: # pragma: no cover
pytest.fail("wrong skipped caught") pytest.fail("wrong skipped caught")
reports = reprec.getreports("pytest_collectreport") reports = reprec.getreports("pytest_collectreport")
assert len(reports) == 1 assert len(reports) == 1

View File

@ -1177,3 +1177,11 @@ def test_summary_list_after_errors(testdir):
"FAILED test_summary_list_after_errors.py::test_fail - assert 0", "FAILED test_summary_list_after_errors.py::test_fail - assert 0",
] ]
) )
def test_importorskip():
with pytest.raises(
pytest.skip.Exception,
match="^could not import 'doesnotexist': No module named .*",
):
pytest.importorskip("doesnotexist")

View File

@ -893,7 +893,7 @@ def test_getreportopt():
assert getreportopt(config) == "sxXwEf" # NOTE: "w" included! assert getreportopt(config) == "sxXwEf" # NOTE: "w" included!
config.option.reportchars = "A" config.option.reportchars = "A"
assert getreportopt(config) == "sxXwEfpP" assert getreportopt(config) == "PpsxXwEf"
def test_terminalreporter_reportopt_addopts(testdir): def test_terminalreporter_reportopt_addopts(testdir):