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:
- python: '3.8-dev'
env: TOXENV=py38-xdist
# temporary until pytest 4.6 is released
- env: TOXENV=py27-pluggymaster-xdist
python: '2.7'
- env: TOXENV=py37-pluggymaster-xdist
# Temporary (https://github.com/pytest-dev/pytest/pull/5334).
- env: TOXENV=pypy3-xdist
python: 'pypy3'
before_script:
- |

View File

@ -18,6 +18,84 @@ with advance notice in the **Deprecations** section of releases.
.. 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)
=========================

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
release-4.6.0
release-4.5.0
release-4.4.2
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
...sss...sssssssss...sss... [100%]
========================= 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
Indirect parametrization of optional implementations/imports
@ -494,7 +494,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%]
========================= 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 ====================
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
E assert (3 * 2) < 6
failure_demo.py:20: AssertionError
failure_demo.py:21: AssertionError
_________________________ TestFailing.test_simple __________________________
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 + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef>()
failure_demo.py:31: AssertionError
failure_demo.py:32: AssertionError
____________________ TestFailing.test_simple_multiline _____________________
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):
> otherfunc_multi(42, 6 * 9)
failure_demo.py:34:
failure_demo.py:35:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
a = 42, b = 54
@ -60,7 +60,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b
E assert 42 == 54
failure_demo.py:15: AssertionError
failure_demo.py:16: AssertionError
___________________________ TestFailing.test_not ___________________________
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 + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef>()
failure_demo.py:40: AssertionError
failure_demo.py:41: AssertionError
_________________ TestSpecialisedExplanations.test_eq_text _________________
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 + eggs
failure_demo.py:45: AssertionError
failure_demo.py:46: AssertionError
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
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 ? ^
failure_demo.py:48: AssertionError
failure_demo.py:49: AssertionError
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
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 bar
failure_demo.py:51: AssertionError
failure_demo.py:52: AssertionError
______________ TestSpecialisedExplanations.test_eq_long_text _______________
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 ? ^
failure_demo.py:56: AssertionError
failure_demo.py:57: AssertionError
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
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 ...Full output truncated (7 lines hidden), use '-vv' to show
failure_demo.py:61: AssertionError
failure_demo.py:62: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________
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 Use -v to get the full diff
failure_demo.py:64: AssertionError
failure_demo.py:65: AssertionError
______________ TestSpecialisedExplanations.test_eq_list_long _______________
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 Use -v to get the full diff
failure_demo.py:69: AssertionError
failure_demo.py:70: AssertionError
_________________ TestSpecialisedExplanations.test_eq_dict _________________
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 ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:72: AssertionError
failure_demo.py:73: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________
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 ...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 ______________
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 Use -v to get the full diff
failure_demo.py:78: AssertionError
failure_demo.py:79: AssertionError
_________________ TestSpecialisedExplanations.test_in_list _________________
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]
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 __________
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 ...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 ____________
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 ? +++
failure_demo.py:89: AssertionError
failure_demo.py:90: AssertionError
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
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 ? +++
failure_demo.py:93: AssertionError
failure_demo.py:94: AssertionError
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
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 ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
failure_demo.py:97: AssertionError
failure_demo.py:98: AssertionError
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
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 b: 'b' != 'c'
failure_demo.py:109: AssertionError
failure_demo.py:110: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________
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 b: 'b' != 'c'
failure_demo.py:121: AssertionError
failure_demo.py:122: AssertionError
______________________________ 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 + 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 __________________________
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 <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 __________________________
def test_attribute_failure():
@ -364,7 +364,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
i = Foo()
> assert i.b == 2
failure_demo.py:147:
failure_demo.py:148:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
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")
E Exception: Failed to get attrib
failure_demo.py:142: Exception
failure_demo.py:143: Exception
_________________________ 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 + 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 __________________________
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)
E ValueError: invalid literal for int() with base 10: 'qwe'
failure_demo.py:167: ValueError
failure_demo.py:168: ValueError
______________________ TestRaises.test_raises_doesnt _______________________
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")
E Failed: DID NOT RAISE <class 'OSError'>
failure_demo.py:170: Failed
failure_demo.py:171: Failed
__________________________ TestRaises.test_raise ___________________________
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")
E ValueError: demo error
failure_demo.py:173: ValueError
failure_demo.py:174: ValueError
________________________ TestRaises.test_tupleerror ________________________
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
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 ______
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()
E TypeError: 'int' object is not iterable
failure_demo.py:181: TypeError
failure_demo.py:182: TypeError
--------------------------- Captured stdout call ---------------------------
items is [1, 2, 3]
________________________ TestRaises.test_some_error ________________________
@ -449,7 +449,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> if namenotexi: # NOQA
E NameError: name 'namenotexi' is not defined
failure_demo.py:184: NameError
failure_demo.py:185: NameError
____________________ 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
> module.foo()
failure_demo.py:202:
failure_demo.py:203:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def foo():
> assert 1 == 0
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 _____________________
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())
failure_demo.py:213:
failure_demo.py:214:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
failure_demo.py:11: in somefunc
failure_demo.py:12: in somefunc
otherfunc(x, y)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
@ -497,7 +497,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert a == b
E assert 44 == 43
failure_demo.py:7: AssertionError
failure_demo.py:8: AssertionError
___________________ TestMoreErrors.test_z1_unpack_error ____________________
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
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 _____________________
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
E TypeError: 'int' object is not iterable
failure_demo.py:221: TypeError
failure_demo.py:222: TypeError
______________________ TestMoreErrors.test_startswith ______________________
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 <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 ___________________
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 + 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 ______________________
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 43 = globf(42)
failure_demo.py:238: AssertionError
failure_demo.py:239: AssertionError
_______________________ TestMoreErrors.test_instance _______________________
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 + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
failure_demo.py:242: AssertionError
failure_demo.py:243: AssertionError
_______________________ TestMoreErrors.test_compare ________________________
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 + where 11 = globf(10)
failure_demo.py:245: AssertionError
failure_demo.py:246: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________
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
E assert 1 == 0
failure_demo.py:250: AssertionError
failure_demo.py:251: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________
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 + 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 ____________________
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 + 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 ___________________
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 + 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 =========================

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
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
.. versionadded:: 4.2
@ -1102,10 +1118,35 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
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
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

View File

@ -400,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta
============================= warnings summary =============================
test_pytest_warnings.py:1
$REGENDOC_TMPDIR/test_pytest_warnings.py:1: 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:
-- Docs: https://docs.pytest.org/en/latest/warnings.html

View File

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

View File

@ -68,7 +68,6 @@ class AssertionRewritingHook(object):
self.session = None
self.modules = {}
self._rewritten_names = set()
self._register_with_pkg_resources()
self._must_rewrite = set()
# 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)
@ -319,24 +318,6 @@ class AssertionRewritingHook(object):
tp = desc[2]
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):
"""Optional PEP302 get_data API.
"""
@ -972,6 +953,8 @@ warn_explicit(
"""
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)
arg_expls = []
new_args = []
@ -995,6 +978,27 @@ warn_explicit(
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, 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):
# From Python 3.5, a Starred node can appear in a function call
res, expl = self.visit(starred.value)
@ -1005,6 +1009,8 @@ warn_explicit(
"""
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)
arg_expls = []
new_args = []

View File

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

View File

@ -13,8 +13,10 @@ import sys
import types
import warnings
import importlib_metadata
import py
import six
from packaging.version import Version
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@ -788,25 +790,17 @@ class Config(object):
modules or packages in the distribution package for
all pytest plugins.
"""
import pkg_resources
self.pluginmanager.rewrite_hook = hook
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# We don't autoload from setuptools entry points, no need to continue.
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 = (
entry.split(",")[0]
for entrypoint in pkg_resources.iter_entry_points("pytest11")
for metadata in metadata_files
for entry in entrypoint.dist._get_metadata(metadata)
str(file)
for dist in importlib_metadata.distributions()
if any(ep.group == "pytest11" for ep in dist.entry_points)
for file in dist.files
)
for name in _iter_rewritable_modules(package_files):
@ -875,11 +869,10 @@ class Config(object):
def _checkversion(self):
import pytest
from pkg_resources import parse_version
minver = self.inicfg.get("minversion", None)
if minver:
if parse_version(minver) > parse_version(pytest.__version__):
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'"
% (

View File

@ -81,6 +81,7 @@ class pytestPDB(object):
_config = None
_saved = []
_recursive_debug = 0
_wrapped_pdb_cls = None
@classmethod
def _is_capturing(cls, capman):
@ -89,43 +90,138 @@ class pytestPDB(object):
return False
@classmethod
def _import_pdb_cls(cls):
def _import_pdb_cls(cls, capman):
if not cls._config:
# Happens when using pytest.set_trace outside of a test.
return pdb.Pdb
pdb_cls = cls._config.getvalue("usepdb_cls")
if not pdb_cls:
return pdb.Pdb
usepdb_cls = cls._config.getvalue("usepdb_cls")
modname, classname = pdb_cls
if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
return cls._wrapped_pdb_cls[1]
try:
__import__(modname)
mod = sys.modules[modname]
if usepdb_cls:
modname, classname = usepdb_cls
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
parts = classname.split(".")
pdb_cls = getattr(mod, parts[0])
for part in parts[1:]:
pdb_cls = getattr(pdb_cls, part)
try:
__import__(modname)
mod = sys.modules[modname]
return pdb_cls
except Exception as exc:
value = ":".join((modname, classname))
raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc))
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
parts = classname.split(".")
pdb_cls = getattr(mod, parts[0])
for part in parts[1:]:
pdb_cls = getattr(pdb_cls, part)
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
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. """
import _pytest.config
if cls._pluginmanager is not None:
capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend(in_=True)
else:
capman = None
if capman:
capman.suspend(in_=True)
if cls._config:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if cls._recursive_debug == 0:
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
@ -133,112 +229,28 @@ class pytestPDB(object):
tw.sep(">", header)
else:
capturing = cls._is_capturing(capman)
if capturing:
if capturing == "global":
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(
">",
"PDB set_trace (IO-capturing turned off for %s)"
% capturing,
)
if capturing == "global":
tw.sep(">", "PDB %s (IO-capturing turned off)" % (method,))
elif capturing:
tw.sep(
">",
"PDB %s (IO-capturing turned off for %s)"
% (method, capturing),
)
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):
_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)
if cls._pluginmanager:
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
else:
pdb_cls = cls._import_pdb_cls()
_pdb = pdb_cls(**kwargs)
return _pdb
@classmethod
def set_trace(cls, *args, **kwargs):
"""Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
frame = sys._getframe().f_back
_pdb = cls._init_pdb(*args, **kwargs)
_pdb = cls._init_pdb("set_trace", *args, **kwargs)
_pdb.set_trace(frame)
@ -265,7 +277,7 @@ class PdbTrace(object):
def _test_pytest_function(pyfuncitem):
_pdb = pytestPDB._init_pdb()
_pdb = pytestPDB._init_pdb("runcall")
testfunction = pyfuncitem.obj
pyfuncitem.obj = _pdb.runcall
if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch
@ -315,7 +327,7 @@ def _postmortem_traceback(excinfo):
def post_mortem(t):
p = pytestPDB._init_pdb()
p = pytestPDB._init_pdb("post_mortem")
p.reset()
p.interaction(None, t)
if p.quitting:

View File

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

View File

@ -142,24 +142,48 @@ def pytest_cmdline_main(config):
def showhelp(config):
import textwrap
reporter = config.pluginmanager.get_plugin("terminalreporter")
tw = reporter._tw
tw.write(config._parser.optparser.format_help())
tw.line()
tw.line()
tw.line(
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
)
tw.line()
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:
help, type, default = config._parser._inidict[name]
if type is None:
type = "string"
spec = "%s (%s)" % (name, type)
line = " %-24s %s" % (spec, help)
tw.line(line[:columns])
spec = "%s (%s):" % (name, type)
tw.write(" %s" % spec)
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("environment variables:")

View File

@ -167,6 +167,9 @@ class _NodeReporter(object):
self.append(node)
def write_captured_output(self, report):
if not self.xml.log_passing_tests and report.passed:
return
content_out = report.capstdout
content_log = report.caplog
content_err = report.capstderr
@ -414,6 +417,12 @@ def pytest_addoption(parser):
"one of no|system-out|system-err",
default="no",
) # 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(
"junit_duration_report",
"Duration time to report: one of total|call",
@ -437,6 +446,7 @@ def pytest_configure(config):
config.getini("junit_logging"),
config.getini("junit_duration_report"),
config.getini("junit_family"),
config.getini("junit_log_passing_tests"),
)
config.pluginmanager.register(config._xml)
@ -472,12 +482,14 @@ class LogXML(object):
logging="no",
report_duration="total",
family="xunit1",
log_passing_tests=True,
):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile))
self.prefix = prefix
self.suite_name = suite_name
self.logging = logging
self.log_passing_tests = log_passing_tests
self.report_duration = report_duration
self.family = family
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_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):
@ -72,6 +77,36 @@ class ColoredLevelFormatter(logging.Formatter):
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):
for name in names:
ret = config.getoption(name) # 'default' arg won't work as expected
@ -257,8 +292,8 @@ class LogCaptureFixture(object):
@property
def text(self):
"""Returns the log text."""
return self.handler.stream.getvalue()
"""Returns the formatted log text."""
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
@property
def records(self):
@ -394,7 +429,7 @@ class LoggingPlugin(object):
config.option.verbose = 1
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_date_format"),
)
@ -428,6 +463,22 @@ class LoggingPlugin(object):
if self._log_cli_enabled():
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):
config = self._config
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
@ -438,23 +489,12 @@ class LoggingPlugin(object):
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
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(
config, "log_cli_date_format", "log_date_format"
log_cli_formatter = self._create_formatter(
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")
self.log_cli_handler = log_cli_handler
self.live_logs_context = lambda: catching_logs(

View File

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

View File

@ -153,15 +153,6 @@ class LsofFdLeakChecker(object):
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
@ -517,6 +508,10 @@ class Testdir(object):
# Discard outer pytest options.
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):
return "<Testdir %r>" % (self.tmpdir,)
@ -813,8 +808,8 @@ class Testdir(object):
try:
# Do not load user config (during runs only).
mp_run = MonkeyPatch()
mp_run.setenv("HOME", str(self.tmpdir))
mp_run.setenv("USERPROFILE", str(self.tmpdir))
for k, v in self._env_run_update.items():
mp_run.setenv(k, v)
finalizers.append(mp_run.undo)
# When running pytest inline any plugins active in the main test
@ -1054,9 +1049,7 @@ class Testdir(object):
env["PYTHONPATH"] = os.pathsep.join(
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
)
# Do not load user config.
env["HOME"] = str(self.tmpdir)
env["USERPROFILE"] = env["HOME"]
env.update(self._env_run_update)
kw["env"] = env
if stdin is Testdir.CLOSE_STDIN:
@ -1242,7 +1235,12 @@ class Testdir(object):
if sys.platform.startswith("freebsd"):
pytest.xfail("pexpect does not work reliably on freebsd")
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)
child.timeout = expect_timeout
return child

View File

@ -31,7 +31,6 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import isclass
from _pytest.compat import isfunction
from _pytest.compat import NoneType
from _pytest.compat import NOTSET
from _pytest.compat import REGEX_TYPE
from _pytest.compat import safe_getattr
@ -1194,7 +1193,7 @@ def _idval(val, argname, idx, idfn, item, config):
if isinstance(val, STRING_TYPES):
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)
elif isinstance(val, REGEX_TYPE):
return ascii_escaped(val.pattern)

View File

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

View File

@ -60,29 +60,29 @@ class TempPathFactory(object):
def getbasetemp(self):
""" return base temporary directory. """
if self._basetemp is 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:
if self._basetemp is not None:
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
class TempdirFactory(object):

View File

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

View File

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

View File

@ -2,7 +2,9 @@
import logging
import py.io
import six
import pytest
from _pytest.logging import ColoredLevelFormatter
@ -35,3 +37,31 @@ def test_coloredlogformatter():
formatter = ColoredLevelFormatter(tw, logfmt)
output = formatter.format(record)
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:
content = rfh.read()
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):
"""Test that we match files correctly when they are marked for rewriting (#2939)."""
contents = {
"conftest.py": """
"conftest.py": """\
pytest_plugins = "ham"
""",
"ham.py": "",
"hamster.py": "",
"test_foo.py": """
"test_foo.py": """\
def test_foo(pytestconfig):
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
@ -153,14 +153,13 @@ class TestImportHookInstallation(object):
assert result.ret == 0
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
@pytest.mark.parametrize("plugin_state", ["development", "installed"])
def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch):
def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch):
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
# Make sure the hook is installed early enough so that plugins
# installed via setuptools are rewritten.
testdir.tmpdir.join("hampkg").ensure(dir=1)
contents = {
"hampkg/__init__.py": """
"hampkg/__init__.py": """\
import pytest
@pytest.fixture
@ -169,7 +168,7 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value
return check
""",
"spamplugin.py": """
"spamplugin.py": """\
import pytest
from hampkg import check_first2
@ -179,46 +178,31 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value
return check
""",
"mainwrapper.py": """
import pytest, pkg_resources
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 []
"mainwrapper.py": """\
import pytest, importlib_metadata
class DummyEntryPoint(object):
name = 'spam'
module_name = 'spam.py'
attrs = ()
extras = None
dist = DummyDistInfo()
group = 'pytest11'
def load(self, require=True, *args, **kwargs):
def load(self):
import spamplugin
return spamplugin
def iter_entry_points(group, name=None):
yield DummyEntryPoint()
class DummyDistInfo(object):
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()
""".format(
plugin_state=plugin_state
),
"test_foo.py": """
""",
"test_foo.py": """\
def test(check_first):
check_first([10, 30], 30)
@ -605,7 +589,10 @@ class TestAssert_reprcompare(object):
return "\xff"
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):
assert util.format_explanation("λ")

View File

@ -656,6 +656,12 @@ class TestAssertionRewrite(object):
else:
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 f():
class A(object):
@ -671,6 +677,53 @@ class TestAssertionRewrite(object):
assert "UnicodeDecodeError" 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):
def test_pycache_is_a_file(self, testdir):

View File

@ -6,7 +6,7 @@ from __future__ import print_function
import sys
import textwrap
import attr
import importlib_metadata
import _pytest._code
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):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None):
assert group == "pytest11"
class EntryPoint(object):
name = "mytestplugin"
group = "pytest11"
class Dist(object):
project_name = "spam"
version = "1.0"
def load(self):
class PseudoPlugin(object):
x = 42
def _get_metadata(self, name):
return ["foo.txt,sha256=abc,123"]
return PseudoPlugin()
class EntryPoint(object):
name = "mytestplugin"
dist = Dist()
class Dist(object):
files = ()
entry_points = (EntryPoint(),)
def load(self):
class PseudoPlugin(object):
x = 42
def my_dists():
return (Dist,)
return PseudoPlugin()
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
testdir.makeconftest(
"""
pytest_plugins = "mytestplugin",
@ -570,60 +564,50 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
def test_setuptools_importerror_issue1479(testdir, monkeypatch):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None):
assert group == "pytest11"
class DummyEntryPoint(object):
name = "mytestplugin"
group = "pytest11"
class Dist(object):
project_name = "spam"
version = "1.0"
def load(self):
raise ImportError("Don't hide me!")
def _get_metadata(self, name):
return ["foo.txt,sha256=abc,123"]
class Distribution(object):
version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object):
name = "mytestplugin"
dist = Dist()
def distributions():
return (Distribution(),)
def load(self):
raise ImportError("Don't hide me!")
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
with pytest.raises(ImportError):
testdir.parseconfig()
@pytest.mark.parametrize("block_it", [True, False])
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)
plugin_module_placeholder = object()
def my_iter(group, name=None):
assert group == "pytest11"
class DummyEntryPoint(object):
name = "mytestplugin"
group = "pytest11"
class Dist(object):
project_name = "spam"
version = "1.0"
def load(self):
return plugin_module_placeholder
def _get_metadata(self, name):
return ["foo.txt,sha256=abc,123"]
class Distribution(object):
version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object):
name = "mytestplugin"
dist = Dist()
def distributions():
return (Distribution(),)
def load(self):
return plugin_module_placeholder
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
args = ("-p", "no:mytestplugin") if block_it else ()
config = testdir.parseconfig(*args)
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)]
)
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):
name = "mytestplugin"
project_name = name = "mytestplugin"
group = "pytest11"
version = "1.0"
@property
def project_name(self):
return self.name
def load(self):
return sys.modules[self.name]
@property
def dist(self):
return self
def _get_metadata(self, *args):
return []
class Distribution(object):
entry_points = (DummyEntryPoint(),)
files = ()
class PseudoPlugin(object):
x = 42
def distributions():
return (Distribution(),)
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())
config = testdir.parseconfig(*parse_args)
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 print_function
import pkg_resources
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"]
import importlib_metadata
def test_pytest_entry_points_are_identical():
entryMap = pkg_resources.get_entry_map("pytest")["console_scripts"]
assert entryMap["pytest"].module_name == entryMap["py.test"].module_name
dist = importlib_metadata.distribution("pytest")
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")
assert "1 <> 2" in snode.text
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):
@classmethod
def set_trace(cls, *args, **kwargs):
# Init _PdbWrapper to handle capturing.
_pdb = cls._init_pdb(*args, **kwargs)
# Init PytestPdbWrapper to handle capturing.
_pdb = cls._init_pdb("set_trace", *args, **kwargs)
# Mock out pdb.Pdb.do_continue.
import pdb
pdb.Pdb.do_continue = lambda self, arg: None
print("=== SET_TRACE ===")
print("===" + " SET_TRACE ===")
assert input() == "debug set_trace()"
# Simulate _PdbWrapper.do_debug
# Simulate PytestPdbWrapper.do_debug
cls._recursive_debug += 1
print("ENTERING RECURSIVE DEBUGGER")
print("=== SET_TRACE_2 ===")
print("===" + " SET_TRACE_2 ===")
assert input() == "c"
_pdb.do_continue("")
print("=== SET_TRACE_3 ===")
print("===" + " SET_TRACE_3 ===")
# Simulate _PdbWrapper.do_debug
# Simulate PytestPdbWrapper.do_debug
print("LEAVING RECURSIVE DEBUGGER")
cls._recursive_debug -= 1
print("=== SET_TRACE_4 ===")
print("===" + " SET_TRACE_4 ===")
assert input() == "c"
_pdb.do_continue("")
def do_continue(self, arg):
print("=== do_continue")
# _PdbWrapper.do_continue("")
monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest)
@ -677,7 +676,7 @@ class TestPDB(object):
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 ===")
before = child.before.decode("utf8")
if not capture_arg:
@ -827,7 +826,7 @@ class TestPDB(object):
result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"])
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(
custom_pdb="""
class CustomPdb(object):
@ -1037,24 +1036,28 @@ def test_trace_after_runpytest(testdir):
from _pytest.debugging import pytestPDB
def test_outer(testdir):
from _pytest.debugging import pytestPDB
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):
assert len(pytestPDB._saved) == 2
result = testdir.runpytest("-s", "-k", "test_inner")
assert result.ret == 0
assert len(pytestPDB._saved) == 1
"""
)
child = testdir.spawn_pytest("-p pytester %s -k test_outer" % p1)
child.expect(r"\(Pdb")
child.sendline("c")
rest = child.read().decode("utf8")
TestPDB.flush(child)
assert child.exitstatus == 0, rest
result = testdir.runpytest_subprocess("-s", "-p", "pytester", str(p1))
result.stdout.fnmatch_lines(["test_inner_end"])
assert result.ret == 0
def test_quit_with_swallowed_SystemExit(testdir):
@ -1139,14 +1142,14 @@ def test_pdbcls_via_local_module(testdir):
p1 = testdir.makepyfile(
"""
def test():
print("before_settrace")
print("before_set_trace")
__import__("pdb").set_trace()
""",
mypdb="""
class Wrapped:
class MyPdb:
def set_trace(self, *args):
print("settrace_called", args)
print("set_trace_called", args)
def runcall(self, *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
)
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.
result = testdir.runpytest(
@ -1202,3 +1205,33 @@ def test_raises_bdbquit_with_eoferror(testdir):
result = testdir.runpytest(str(p1))
result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"])
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 stderr.splitlines() == [b"stderr"]
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:
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")
reports = reprec.getreports("pytest_collectreport")
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",
]
)
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!
config.option.reportchars = "A"
assert getreportopt(config) == "sxXwEfpP"
assert getreportopt(config) == "PpsxXwEf"
def test_terminalreporter_reportopt_addopts(testdir):