Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff015f6308 | ||
|
|
31c869b4c4 | ||
|
|
0395996756 | ||
|
|
986dd84375 | ||
|
|
a36e986920 | ||
|
|
68dc433bf5 | ||
|
|
e59fc730f8 | ||
|
|
4f9e835472 | ||
|
|
498b994eb4 | ||
|
|
40eef6da0c | ||
|
|
71373b04b0 | ||
|
|
6fb7269979 | ||
|
|
3460105def | ||
|
|
e3824d23bc | ||
|
|
80ad448590 | ||
|
|
59f69dd9e7 | ||
|
|
2106515f6d | ||
|
|
66b4709830 | ||
|
|
0b1f813c38 | ||
|
|
0b6960446e | ||
|
|
4bc2f96c93 | ||
|
|
0e4750d837 | ||
|
|
40cec637d7 | ||
|
|
407d4a0cf0 | ||
|
|
c84ae0bb7a | ||
|
|
1dbf440194 | ||
|
|
afaaa7e411 | ||
|
|
7b91952645 | ||
|
|
8726be27a6 | ||
|
|
e26c5bda6e | ||
|
|
f672b7e39e | ||
|
|
f0e6bf7604 | ||
|
|
f729d5d4ee | ||
|
|
04a941c818 | ||
|
|
215d537624 | ||
|
|
f63fbf8114 | ||
|
|
b595587031 | ||
|
|
82cc3d8cc2 | ||
|
|
e20e376881 | ||
|
|
8052d01a37 | ||
|
|
d03444db4a | ||
|
|
f9c1329dab | ||
|
|
a8003286b5 | ||
|
|
747a8ae3a6 | ||
|
|
b759ebdb93 | ||
|
|
b41632e9a8 | ||
|
|
b4be228330 | ||
|
|
31c948184a | ||
|
|
67dd10de26 | ||
|
|
f13935da53 | ||
|
|
dc8af18a0e | ||
|
|
7bee359459 | ||
|
|
61b9246afe | ||
|
|
9feb4941f4 | ||
|
|
237f690f8b | ||
|
|
386e801a5a | ||
|
|
5cf05ce149 | ||
|
|
aee67bb1a7 | ||
|
|
5e2d740829 | ||
|
|
82b8ec37fc | ||
|
|
f73fa47b1f | ||
|
|
fd1684e70b | ||
|
|
19501028ca | ||
|
|
ed01dc6567 | ||
|
|
3a366f451a | ||
|
|
7f6108beb1 | ||
|
|
76b0660f47 | ||
|
|
fc8800c71f | ||
|
|
75a12b9d2b | ||
|
|
9bcbf552d6 | ||
|
|
32c6d4f603 | ||
|
|
b4b2f58eab | ||
|
|
a131cd6c3b | ||
|
|
9c03196e79 | ||
|
|
8b92d10fb3 | ||
|
|
8e220f0e6f | ||
|
|
e191a65ebb | ||
|
|
5ca81596bb | ||
|
|
64e8185ff7 | ||
|
|
7bb504b807 | ||
|
|
9be069f899 | ||
|
|
913a2da6e5 | ||
|
|
ea732464aa | ||
|
|
ddbea29c12 | ||
|
|
4c7ddb8d9b | ||
|
|
a1fcd6e445 | ||
|
|
7b8fd0cc12 | ||
|
|
391dc549c0 | ||
|
|
526f4a95cc | ||
|
|
4cd268dc5d | ||
|
|
59e6fb94b5 | ||
|
|
2f083504ee | ||
|
|
3384ffc6eb | ||
|
|
e276bd3332 | ||
|
|
7445b5345f | ||
|
|
429485e621 | ||
|
|
52d497570b | ||
|
|
1876a928d3 | ||
|
|
2dc2a19db5 | ||
|
|
0c5e717f43 | ||
|
|
54af0f4c65 | ||
|
|
678dfaa6eb | ||
|
|
fc5d4654e5 | ||
|
|
19c93d16d1 | ||
|
|
0ce8b910ca | ||
|
|
c780d1fa7c | ||
|
|
584c052da4 | ||
|
|
315374008b | ||
|
|
a9457345ee | ||
|
|
726e165932 | ||
|
|
2264db7f4a | ||
|
|
4e93dc2c97 | ||
|
|
8003d8d279 | ||
|
|
f0ecb25acd | ||
|
|
7ec1a1407a | ||
|
|
7dbe40092d | ||
|
|
e53563ebbe | ||
|
|
c2c9b27771 | ||
|
|
c3d7340542 | ||
|
|
2461a43e00 | ||
|
|
7dcd9bf5ad | ||
|
|
8a1afe4213 | ||
|
|
fd4289dae0 | ||
|
|
977adf1354 | ||
|
|
c1fe07276c | ||
|
|
c166b80a8c | ||
|
|
afe9fd5ffd |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ coverage.xml
|
||||
.project
|
||||
.settings
|
||||
.vscode
|
||||
|
||||
# generated by pip
|
||||
pip-wheel-metadata/
|
||||
|
||||
25
.travis.yml
25
.travis.yml
@@ -1,4 +1,3 @@
|
||||
sudo: false
|
||||
language: python
|
||||
dist: xenial
|
||||
stages:
|
||||
@@ -17,23 +16,28 @@ env:
|
||||
# Specialized factors for py27.
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py27-pluggymaster
|
||||
# Specialized factors for py37.
|
||||
- TOXENV=py37-pexpect,py37-trial,py37-numpy
|
||||
- TOXENV=py37-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py37-pluggymaster
|
||||
- TOXENV=py37-freeze PYTEST_NO_COVERAGE=1
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: '3.8-dev'
|
||||
env: TOXENV=py38-xdist
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Coverage tracking is slow with pypy, skip it.
|
||||
- env: TOXENV=pypy PYTEST_NO_COVERAGE=1
|
||||
python: 'pypy-5.4'
|
||||
dist: trusty
|
||||
- env: TOXENV=py34
|
||||
- env: TOXENV=py34-xdist
|
||||
python: '3.4'
|
||||
- env: TOXENV=py35
|
||||
- env: TOXENV=py35-xdist
|
||||
python: '3.5'
|
||||
- env: TOXENV=py36
|
||||
- env: TOXENV=py36-xdist
|
||||
python: '3.6'
|
||||
- env: TOXENV=py37
|
||||
- &test-macos
|
||||
@@ -43,15 +47,20 @@ jobs:
|
||||
sudo: required
|
||||
install:
|
||||
- python -m pip install --pre tox
|
||||
env: TOXENV=py27
|
||||
env: TOXENV=py27-xdist
|
||||
- <<: *test-macos
|
||||
env: TOXENV=py37
|
||||
env: TOXENV=py37-xdist
|
||||
before_install:
|
||||
- brew update
|
||||
- brew upgrade python
|
||||
- brew unlink python
|
||||
- brew link python
|
||||
|
||||
# Jobs only run via Travis cron jobs (currently daily).
|
||||
- env: TOXENV=py38-xdist
|
||||
python: '3.8-dev'
|
||||
if: type = cron
|
||||
|
||||
- stage: baseline
|
||||
env: TOXENV=py27-pexpect,py27-trial,py27-numpy
|
||||
- env: TOXENV=py37-xdist
|
||||
|
||||
4
AUTHORS
4
AUTHORS
@@ -16,6 +16,7 @@ Allan Feldman
|
||||
Aly Sivji
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Mitzki
|
||||
Andras Tim
|
||||
Andrea Cimatoribus
|
||||
Andreas Zeidler
|
||||
@@ -27,6 +28,7 @@ Anthony Shaw
|
||||
Anthony Sottile
|
||||
Anton Lodder
|
||||
Antony Lee
|
||||
Arel Cordero
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
@@ -50,6 +52,7 @@ Charles Cloud
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
Christian Boelsen
|
||||
Christian Fetzer
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
@@ -173,6 +176,7 @@ Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
Nicholas Devenish
|
||||
Nicholas Murphy
|
||||
Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Oleg Pidsadnyi
|
||||
|
||||
103
CHANGELOG.rst
103
CHANGELOG.rst
@@ -18,13 +18,111 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 4.3.0 (2019-02-16)
|
||||
=========================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#4724 <https://github.com/pytest-dev/pytest/issues/4724>`_: ``pytest.warns()`` now emits a warning when it receives unknown keyword arguments.
|
||||
|
||||
This will be changed into an error in the future.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2753 <https://github.com/pytest-dev/pytest/issues/2753>`_: Usage errors from argparse are mapped to pytest's ``UsageError``.
|
||||
|
||||
|
||||
- `#3711 <https://github.com/pytest-dev/pytest/issues/3711>`_: Add the ``--ignore-glob`` parameter to exclude test-modules with Unix shell-style wildcards.
|
||||
Add the ``collect_ignore_glob`` for ``conftest.py`` to exclude test-modules with Unix shell-style wildcards.
|
||||
|
||||
|
||||
- `#4698 <https://github.com/pytest-dev/pytest/issues/4698>`_: The warning about Python 2.7 and 3.4 not being supported in pytest 5.0 has been removed.
|
||||
|
||||
In the end it was considered to be more
|
||||
of a nuisance than actual utility and users of those Python versions shouldn't have problems as ``pip`` will not
|
||||
install pytest 5.0 on those interpreters.
|
||||
|
||||
|
||||
- `#4707 <https://github.com/pytest-dev/pytest/issues/4707>`_: With the help of new ``set_log_path()`` method there is a way to set ``log_file`` paths from hooks.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#4651 <https://github.com/pytest-dev/pytest/issues/4651>`_: ``--help`` and ``--version`` are handled with ``UsageError``.
|
||||
|
||||
|
||||
- `#4782 <https://github.com/pytest-dev/pytest/issues/4782>`_: Fix ``AssertionError`` with collection of broken symlinks with packages.
|
||||
|
||||
|
||||
pytest 4.2.1 (2019-02-12)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2895 <https://github.com/pytest-dev/pytest/issues/2895>`_: The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``.
|
||||
|
||||
|
||||
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module.
|
||||
|
||||
|
||||
- `#4347 <https://github.com/pytest-dev/pytest/issues/4347>`_: Fix output capturing when using pdb++ with recursive debugging.
|
||||
|
||||
|
||||
- `#4592 <https://github.com/pytest-dev/pytest/issues/4592>`_: Fix handling of ``collect_ignore`` via parent ``conftest.py``.
|
||||
|
||||
|
||||
- `#4700 <https://github.com/pytest-dev/pytest/issues/4700>`_: Fix regression where ``setUpClass`` would always be called in subclasses even if all tests
|
||||
were skipped by a ``unittest.skip()`` decorator applied in the subclass.
|
||||
|
||||
|
||||
- `#4739 <https://github.com/pytest-dev/pytest/issues/4739>`_: Fix ``parametrize(... ids=<function>)`` when the function returns non-strings.
|
||||
|
||||
|
||||
- `#4745 <https://github.com/pytest-dev/pytest/issues/4745>`_: Fix/improve collection of args when passing in ``__init__.py`` and a test file.
|
||||
|
||||
|
||||
- `#4770 <https://github.com/pytest-dev/pytest/issues/4770>`_: ``more_itertools`` is now constrained to <6.0.0 when required for Python 2.7 compatibility.
|
||||
|
||||
|
||||
- `#526 <https://github.com/pytest-dev/pytest/issues/526>`_: Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins.
|
||||
|
||||
|
||||
- `#4324 <https://github.com/pytest-dev/pytest/issues/4324>`_: Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.
|
||||
|
||||
|
||||
- `#4709 <https://github.com/pytest-dev/pytest/issues/4709>`_: Document how to customize test failure messages when using
|
||||
``pytest.warns``.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#4741 <https://github.com/pytest-dev/pytest/issues/4741>`_: Some verbosity related attributes of the TerminalReporter plugin are now
|
||||
read only properties.
|
||||
|
||||
|
||||
pytest 4.2.0 (2019-01-30)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#3094 <https://github.com/pytest-dev/pytest/issues/3094>`_: `Class xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
|
||||
- `#3094 <https://github.com/pytest-dev/pytest/issues/3094>`_: `Classic xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
|
||||
now obey the scope of *autouse* fixtures.
|
||||
|
||||
This fixes a number of surprising issues like ``setup_method`` being called before session-scoped
|
||||
@@ -96,6 +194,9 @@ Trivial/Internal Changes
|
||||
- `#4657 <https://github.com/pytest-dev/pytest/issues/4657>`_: Copy saferepr from pylib
|
||||
|
||||
|
||||
- `#4668 <https://github.com/pytest-dev/pytest/issues/4668>`_: The verbose word for expected failures in the teststatus report changes from ``xfail`` to ``XFAIL`` to be consistent with other test outcomes.
|
||||
|
||||
|
||||
pytest 4.1.1 (2019-01-12)
|
||||
=========================
|
||||
|
||||
|
||||
10
appveyor.yml
10
appveyor.yml
@@ -2,22 +2,18 @@ environment:
|
||||
matrix:
|
||||
- TOXENV: "py37-xdist"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27"
|
||||
- TOXENV: "py37"
|
||||
- TOXENV: "linting,docs,doctesting"
|
||||
- TOXENV: "py36"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py34-xdist"
|
||||
- TOXENV: "py35-xdist"
|
||||
- TOXENV: "py36-xdist"
|
||||
- TOXENV: "pypy"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
# Specialized factors for py27.
|
||||
- TOXENV: "py27-trial,py27-numpy,py27-nobyte"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
# Specialized factors for py37.
|
||||
- TOXENV: "py37-trial,py37-numpy"
|
||||
- TOXENV: "py37-pluggymaster"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py37-freeze"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
|
||||
|
||||
45
azure-pipelines.yml
Normal file
45
azure-pipelines.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
trigger:
|
||||
- master
|
||||
- features
|
||||
|
||||
variables:
|
||||
PYTEST_ADDOPTS: "--junitxml=build/test-results/$(tox.env).xml"
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Test'
|
||||
pool:
|
||||
vmImage: "vs2017-win2016"
|
||||
strategy:
|
||||
matrix:
|
||||
py27:
|
||||
python.version: '2.7'
|
||||
tox.env: 'py27'
|
||||
py35:
|
||||
python.version: '3.5'
|
||||
tox.env: 'py35'
|
||||
py36:
|
||||
python.version: '3.6'
|
||||
tox.env: 'py36'
|
||||
py37:
|
||||
python.version: '3.7'
|
||||
tox.env: 'py37'
|
||||
maxParallel: 4
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '$(python.version)'
|
||||
architecture: 'x64'
|
||||
|
||||
- script: python -m pip install --upgrade pip && pip install tox
|
||||
displayName: 'Install tox'
|
||||
|
||||
- script: python -m tox -e $(tox.env)
|
||||
displayName: 'Run tests'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: 'build/test-results/$(tox.env).xml'
|
||||
testRunTitle: '$(tox.env)'
|
||||
condition: succeededOrFailed()
|
||||
@@ -6,6 +6,8 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-4.3.0
|
||||
release-4.2.1
|
||||
release-4.2.0
|
||||
release-4.1.1
|
||||
release-4.1.0
|
||||
|
||||
30
doc/en/announce/release-4.2.1.rst
Normal file
30
doc/en/announce/release-4.2.1.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
pytest-4.2.1
|
||||
=======================================
|
||||
|
||||
pytest 4.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Arel Cordero
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Holger Kohr
|
||||
* Kevin J. Foley
|
||||
* Nick Murphy
|
||||
* Paweł Stradomski
|
||||
* Raphael Pierzina
|
||||
* Ronny Pfannschmidt
|
||||
* Sam Brightman
|
||||
* Thomas Hisch
|
||||
* Zac Hatfield-Dodds
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
36
doc/en/announce/release-4.3.0.rst
Normal file
36
doc/en/announce/release-4.3.0.rst
Normal file
@@ -0,0 +1,36 @@
|
||||
pytest-4.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 4.3.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:
|
||||
|
||||
* Andras Mitzki
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Christian Fetzer
|
||||
* Daniel Hahler
|
||||
* Grygorii Iermolenko
|
||||
* R. Alex Matevish
|
||||
* Ronny Pfannschmidt
|
||||
* cclauss
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
@@ -88,23 +88,30 @@ and if you need to have access to the actual exception info you may use::
|
||||
the actual exception raised. The main attributes of interest are
|
||||
``.type``, ``.value`` and ``.traceback``.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
You can pass a ``match`` keyword parameter to the context-manager to test
|
||||
that a regular expression matches on the string representation of an exception
|
||||
(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``)::
|
||||
|
||||
In the context manager form you may use the keyword argument
|
||||
``message`` to specify a custom failure message::
|
||||
import pytest
|
||||
|
||||
>>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
|
||||
... pass
|
||||
... Failed: Expecting ZeroDivisionError
|
||||
def myfunc():
|
||||
raise ValueError("Exception 123 raised")
|
||||
|
||||
If you want to write test code that works on Python 2.4 as well,
|
||||
you may also use two other ways to test for an expected exception::
|
||||
def test_match():
|
||||
with pytest.raises(ValueError, match=r'.* 123 .*'):
|
||||
myfunc()
|
||||
|
||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
function, so in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
There's an alternate form of the ``pytest.raises`` function where you pass
|
||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
||||
assert that the given exception is raised::
|
||||
|
||||
pytest.raises(ExpectedException, func, *args, **kwargs)
|
||||
|
||||
which will execute the specified function with args and kwargs and
|
||||
assert that the given ``ExpectedException`` is raised. The reporter will
|
||||
provide you with helpful output in case of failures such as *no
|
||||
The reporter will provide you with helpful output in case of failures such as *no
|
||||
exception* or *wrong exception*.
|
||||
|
||||
Note that it is also possible to specify a "raises" argument to
|
||||
@@ -121,23 +128,6 @@ exceptions your own code is deliberately raising, whereas using
|
||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
||||
or bugs in dependencies.
|
||||
|
||||
Also, the context manager form accepts a ``match`` keyword parameter to test
|
||||
that a regular expression matches on the string representation of an exception
|
||||
(like the ``TestCase.assertRaisesRegexp`` method from ``unittest``)::
|
||||
|
||||
import pytest
|
||||
|
||||
def myfunc():
|
||||
raise ValueError("Exception 123 raised")
|
||||
|
||||
def test_match():
|
||||
with pytest.raises(ValueError, match=r'.* 123 .*'):
|
||||
myfunc()
|
||||
|
||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
function. So in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
|
||||
.. _`assertwarns`:
|
||||
|
||||
@@ -205,8 +195,8 @@ Special comparisons are done for a number of cases:
|
||||
|
||||
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
|
||||
|
||||
Defining your own assertion comparison
|
||||
----------------------------------------------
|
||||
Defining your own explanation for failed assertions
|
||||
---------------------------------------------------
|
||||
|
||||
It is possible to add your own detailed explanations by implementing
|
||||
the ``pytest_assertrepr_compare`` hook.
|
||||
|
||||
@@ -436,10 +436,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
.. code-block:: pytest
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
...sss...sssssssss...sss... [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found
|
||||
12 passed, 15 skipped in 0.12 seconds
|
||||
........................... [100%]
|
||||
27 passed in 0.12 seconds
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
@@ -565,3 +563,50 @@ As the result:
|
||||
- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing.
|
||||
- The test ``test_eval[basic_2+4]`` passed.
|
||||
- The test ``test_eval[basic_6*9]`` was expected to fail and did fail.
|
||||
|
||||
.. _`parametrizing_conditional_raising`:
|
||||
|
||||
Parametrizing conditional raising
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Use :func:`pytest.raises` with the
|
||||
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
|
||||
in which some tests raise exceptions and others do not.
|
||||
|
||||
It is helpful to define a no-op context manager ``does_not_raise`` to serve
|
||||
as a complement to ``raises``. For example::
|
||||
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
"""Test how much I know division."""
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
|
||||
If you're only supporting Python 3.7+, you can simply use ``nullcontext``
|
||||
to define ``does_not_raise``::
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
|
||||
Or, if you're supporting Python 3.3+ you can use::
|
||||
|
||||
from contextlib import ExitStack as does_not_raise
|
||||
|
||||
Or, if desired, you can ``pip install contextlib2`` and use::
|
||||
|
||||
from contextlib2 import ExitStack as does_not_raise
|
||||
|
||||
@@ -41,6 +41,9 @@ you will see that ``pytest`` only collects test-modules, which do not match the
|
||||
|
||||
========================= 5 passed in 0.02 seconds =========================
|
||||
|
||||
The ``--ignore-glob`` option allows to ignore test file paths based on Unix shell-style wildcards.
|
||||
If you want to exclude test-modules that end with ``_01.py``, execute ``pytest`` with ``--ignore-glob='*_01.py'``.
|
||||
|
||||
Deselect tests during test collection
|
||||
-------------------------------------
|
||||
|
||||
@@ -266,3 +269,17 @@ file will be left out:
|
||||
collected 0 items
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
It's also possible to ignore files based on Unix shell-style wildcards by adding
|
||||
patterns to ``collect_ignore_glob``.
|
||||
|
||||
The following example ``conftest.py`` ignores the file ``setup.py`` and in
|
||||
addition all files that end with ``*_py2.py`` when executed with a Python 3
|
||||
interpreter::
|
||||
|
||||
# content of conftest.py
|
||||
import sys
|
||||
|
||||
collect_ignore = ["setup.py"]
|
||||
if sys.version_info[0] > 2:
|
||||
collect_ignore_glob = ["*_py2.py"]
|
||||
|
||||
@@ -198,6 +198,9 @@ option names are:
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||
is considered **experimental**.
|
||||
|
||||
.. _log_release_notes:
|
||||
|
||||
Release notes
|
||||
|
||||
@@ -84,6 +84,11 @@ will be loaded as well.
|
||||
:ref:`full explanation <requiring plugins in non-root conftests>`
|
||||
in the Writing plugins section.
|
||||
|
||||
.. note::
|
||||
The name ``pytest_plugins`` is reserved and should not be used as a
|
||||
name for a custom plugin module.
|
||||
|
||||
|
||||
.. _`findpluginname`:
|
||||
|
||||
Finding out which plugins are active
|
||||
|
||||
@@ -797,6 +797,33 @@ Special Variables
|
||||
pytest treats some global variables in a special manner when defined in a test module.
|
||||
|
||||
|
||||
collect_ignore
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`customizing-test-collection`
|
||||
|
||||
Can be declared in *conftest.py files* to exclude test directories or modules.
|
||||
Needs to be ``list[str]``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collect_ignore = ["setup.py"]
|
||||
|
||||
|
||||
collect_ignore_glob
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`customizing-test-collection`
|
||||
|
||||
Can be declared in *conftest.py files* to exclude test directories or modules
|
||||
with Unix shell-style wildcards. Needs to be ``list[str]`` where ``str`` can
|
||||
contain glob patterns.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collect_ignore_glob = ["*_ignore.py"]
|
||||
|
||||
|
||||
pytest_plugins
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1015,6 +1042,20 @@ 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_family
|
||||
|
||||
.. versionadded:: 4.2
|
||||
|
||||
Configures the format of the generated JUnit XML file. The possible options are:
|
||||
|
||||
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**.
|
||||
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__,
|
||||
which should be more compatible with latest Jenkins versions.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
junit_family = xunit2
|
||||
|
||||
.. confval:: junit_suite_name
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ Talks and blog postings
|
||||
|
||||
- pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english <http://talks.apsl.io/testing-pycones-2017/>`_, `video in spanish <https://www.youtube.com/watch?v=K20GeR-lXDk>`_)
|
||||
|
||||
- `pytest advanced, Andrew Svetlov (Russian, PyCon Russia, 2016)
|
||||
<https://www.youtube.com/watch?v=7KgihdKTWY4>`_.
|
||||
|
||||
- `Pythonic testing, Igor Starikov (Russian, PyNsk, November 2016)
|
||||
<https://www.youtube.com/watch?v=_92nfdd5nK8>`_.
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ You can also use it as a contextmanager::
|
||||
.. _warns:
|
||||
|
||||
Asserting warnings with the warns function
|
||||
-----------------------------------------------
|
||||
------------------------------------------
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
@@ -291,7 +291,7 @@ Alternatively, you can examine raised warnings in detail using the
|
||||
.. _recwarn:
|
||||
|
||||
Recording warnings
|
||||
------------------------
|
||||
------------------
|
||||
|
||||
You can record raised warnings either using ``pytest.warns`` or with
|
||||
the ``recwarn`` fixture.
|
||||
@@ -329,6 +329,26 @@ warnings, or index into it to get a particular recorded warning.
|
||||
|
||||
Full API: :class:`WarningsRecorder`.
|
||||
|
||||
.. _custom_failure_messages:
|
||||
|
||||
Custom failure messages
|
||||
-----------------------
|
||||
|
||||
Recording warnings provides an opportunity to produce custom test
|
||||
failure messages for when no warnings are issued or other conditions
|
||||
are met.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test():
|
||||
with pytest.warns(Warning) as record:
|
||||
f()
|
||||
if not record:
|
||||
pytest.fail("Expected a warning!")
|
||||
|
||||
If no warnings are issued when calling ``f``, then ``not record`` will
|
||||
evaluate to ``True``. You can then call ``pytest.fail`` with a
|
||||
custom error message.
|
||||
|
||||
.. _internal-warnings:
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ requires = [
|
||||
"setuptools-scm",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.towncrier]
|
||||
package = "pytest"
|
||||
|
||||
3
setup.py
3
setup.py
@@ -10,7 +10,8 @@ INSTALL_REQUIRES = [
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
"more-itertools>=4.0.0",
|
||||
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
|
||||
'more-itertools>=4.0.0;python_version>"2.7"',
|
||||
"atomicwrites>=1.0",
|
||||
'funcsigs;python_version<"3.0"',
|
||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||
|
||||
@@ -408,7 +408,10 @@ class PytestPluginManager(PluginManager):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
# Use realpath to avoid loading the same conftest twice
|
||||
# with build systems that create build directories containing
|
||||
# symlinks to actual files.
|
||||
mod = self._importconftest(conftestpath.realpath())
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
@@ -559,8 +562,8 @@ def _get_plugin_specs_as_list(specs):
|
||||
which case it is returned as a list. Specs can also be `None` in which case an
|
||||
empty list is returned.
|
||||
"""
|
||||
if specs is not None:
|
||||
if isinstance(specs, str):
|
||||
if specs is not None and not isinstance(specs, types.ModuleType):
|
||||
if isinstance(specs, six.string_types):
|
||||
specs = specs.split(",") if specs else []
|
||||
if not isinstance(specs, (list, tuple)):
|
||||
raise UsageError(
|
||||
@@ -648,8 +651,27 @@ class Config(object):
|
||||
return self.pluginmanager.get_plugin("terminalreporter")._tw
|
||||
|
||||
def pytest_cmdline_parse(self, pluginmanager, args):
|
||||
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
|
||||
self.parse(args)
|
||||
try:
|
||||
self.parse(args)
|
||||
except UsageError:
|
||||
|
||||
# Handle --version and --help here in a minimal fashion.
|
||||
# This gets done via helpconfig normally, but its
|
||||
# pytest_cmdline_main is not called in case of errors.
|
||||
if getattr(self.option, "version", False) or "--version" in args:
|
||||
from _pytest.helpconfig import showversion
|
||||
|
||||
showversion(self)
|
||||
elif (
|
||||
getattr(self.option, "help", False) or "--help" in args or "-h" in args
|
||||
):
|
||||
self._parser._getparser().print_help()
|
||||
sys.stdout.write(
|
||||
"\nNOTE: displaying only minimal help due to UsageError.\n\n"
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
return self
|
||||
|
||||
def notify_exception(self, excinfo, option=None):
|
||||
@@ -760,21 +782,32 @@ class Config(object):
|
||||
for name in _iter_rewritable_modules(package_files):
|
||||
hook.mark_rewrite(name)
|
||||
|
||||
def _validate_args(self, args):
|
||||
def _validate_args(self, args, via):
|
||||
"""Validate known args."""
|
||||
self._parser.parse_known_and_unknown_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
self._parser._config_source_hint = via
|
||||
try:
|
||||
self._parser.parse_known_and_unknown_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
finally:
|
||||
del self._parser._config_source_hint
|
||||
|
||||
return args
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
if addopts:
|
||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||
if len(env_addopts):
|
||||
args[:] = self._validate_args(shlex.split(env_addopts)) + args
|
||||
args[:] = (
|
||||
self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
|
||||
+ args
|
||||
)
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = self._validate_args(self.getini("addopts")) + args
|
||||
args[:] = (
|
||||
self._validate_args(self.getini("addopts"), "via addopts config") + args
|
||||
)
|
||||
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import argparse
|
||||
import sys as _sys
|
||||
import warnings
|
||||
from gettext import gettext as _
|
||||
|
||||
import py
|
||||
import six
|
||||
|
||||
from ..main import EXIT_USAGEERROR
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
@@ -337,14 +335,13 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
self.extra_info = extra_info
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
"""Transform argparse error message into UsageError."""
|
||||
msg = "%s: error: %s" % (self.prog, message)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
Overrides the method in parent class to change exit code"""
|
||||
self.print_usage(_sys.stderr)
|
||||
args = {"prog": self.prog, "message": message}
|
||||
self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args)
|
||||
if hasattr(self._parser, "_config_source_hint"):
|
||||
msg = "%s (%s)" % (msg, self._parser._config_source_hint)
|
||||
|
||||
raise UsageError(self.format_usage() + msg)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""allow splitting of positional arguments"""
|
||||
|
||||
@@ -75,6 +75,7 @@ class pytestPDB(object):
|
||||
_config = None
|
||||
_pdb_cls = pdb.Pdb
|
||||
_saved = []
|
||||
_recursive_debug = 0
|
||||
|
||||
@classmethod
|
||||
def _init_pdb(cls, *args, **kwargs):
|
||||
@@ -87,29 +88,37 @@ class pytestPDB(object):
|
||||
capman.suspend_global_capture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
# Handle header similar to pdb.set_trace in py37+.
|
||||
header = kwargs.pop("header", None)
|
||||
if header is not None:
|
||||
tw.sep(">", header)
|
||||
elif capman and capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
else:
|
||||
tw.sep(">", "PDB set_trace")
|
||||
if cls._recursive_debug == 0:
|
||||
# Handle header similar to pdb.set_trace in py37+.
|
||||
header = kwargs.pop("header", None)
|
||||
if header is not None:
|
||||
tw.sep(">", header)
|
||||
elif capman and capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
else:
|
||||
tw.sep(">", "PDB set_trace")
|
||||
|
||||
class _PdbWrapper(cls._pdb_cls, object):
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
def do_debug(self, arg):
|
||||
cls._recursive_debug += 1
|
||||
ret = super(_PdbWrapper, self).do_debug(arg)
|
||||
cls._recursive_debug -= 1
|
||||
return ret
|
||||
|
||||
def do_continue(self, arg):
|
||||
ret = super(_PdbWrapper, self).do_continue(arg)
|
||||
if self._pytest_capman:
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
if self._pytest_capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB continue (IO-capturing resumed)")
|
||||
else:
|
||||
tw.sep(">", "PDB continue")
|
||||
self._pytest_capman.resume_global_capture()
|
||||
if cls._recursive_debug == 0:
|
||||
if self._pytest_capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB continue (IO-capturing resumed)")
|
||||
else:
|
||||
tw.sep(">", "PDB continue")
|
||||
self._pytest_capman.resume_global_capture()
|
||||
cls._pluginmanager.hook.pytest_leave_pdb(
|
||||
config=cls._config, pdb=self
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import print_function
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
|
||||
YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored"
|
||||
@@ -87,3 +88,9 @@ PYTEST_LOGWARNING = PytestDeprecationWarning(
|
||||
"pytest_logwarning is deprecated, no longer being called, and will be removed soon\n"
|
||||
"please use pytest_warning_captured instead"
|
||||
)
|
||||
|
||||
PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"pytest.warns() got unexpected keyword arguments: {args!r}.\n"
|
||||
"This will be an error in future versions.",
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import print_function
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import sys
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
@@ -13,7 +14,6 @@ from collections import OrderedDict
|
||||
import attr
|
||||
import py
|
||||
import six
|
||||
from more_itertools import flatten
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
@@ -1109,7 +1109,7 @@ class FixtureManager(object):
|
||||
argnames = getfuncargnames(func, cls=cls)
|
||||
else:
|
||||
argnames = ()
|
||||
usefixtures = flatten(
|
||||
usefixtures = itertools.chain.from_iterable(
|
||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||
)
|
||||
initialnames = tuple(usefixtures) + argnames
|
||||
|
||||
@@ -118,16 +118,20 @@ def pytest_cmdline_parse():
|
||||
config.add_cleanup(unset_tracing)
|
||||
|
||||
|
||||
def showversion(config):
|
||||
p = py.path.local(pytest.__file__)
|
||||
sys.stderr.write(
|
||||
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
|
||||
)
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
for line in plugininfo:
|
||||
sys.stderr.write(line + "\n")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.version:
|
||||
p = py.path.local(pytest.__file__)
|
||||
sys.stderr.write(
|
||||
"This is pytest version %s, imported from %s\n" % (pytest.__version__, p)
|
||||
)
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
for line in plugininfo:
|
||||
sys.stderr.write(line + "\n")
|
||||
showversion(config)
|
||||
return 0
|
||||
elif config.option.help:
|
||||
config._do_configure()
|
||||
|
||||
@@ -13,6 +13,7 @@ import six
|
||||
import pytest
|
||||
from _pytest.compat import dummy_context_manager
|
||||
from _pytest.config import create_terminal_writer
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
|
||||
@@ -370,6 +371,8 @@ def get_actual_log_level(config, *setting_names):
|
||||
)
|
||||
|
||||
|
||||
# run after terminalreporter/capturemanager are configured
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
|
||||
|
||||
@@ -388,8 +391,6 @@ class LoggingPlugin(object):
|
||||
|
||||
# enable verbose output automatically if live logging is enabled
|
||||
if self._log_cli_enabled() and not config.getoption("verbose"):
|
||||
# sanity check: terminal reporter should not have been loaded at this point
|
||||
assert self._config.pluginmanager.get_plugin("terminalreporter") is None
|
||||
config.option.verbose = 1
|
||||
|
||||
self.print_logs = get_option_ini(config, "log_print")
|
||||
@@ -399,27 +400,88 @@ class LoggingPlugin(object):
|
||||
)
|
||||
self.log_level = get_actual_log_level(config, "log_level")
|
||||
|
||||
self.log_file_level = get_actual_log_level(config, "log_file_level")
|
||||
self.log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||
self.log_file_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
)
|
||||
self.log_file_formatter = logging.Formatter(
|
||||
self.log_file_format, datefmt=self.log_file_date_format
|
||||
)
|
||||
|
||||
log_file = get_option_ini(config, "log_file")
|
||||
if log_file:
|
||||
self.log_file_level = get_actual_log_level(config, "log_file_level")
|
||||
|
||||
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||
log_file_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
)
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
log_file, mode="w", encoding="UTF-8"
|
||||
)
|
||||
log_file_formatter = logging.Formatter(
|
||||
log_file_format, datefmt=log_file_date_format
|
||||
)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
self.log_file_handler.setFormatter(self.log_file_formatter)
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
self.log_cli_handler = None
|
||||
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager.
|
||||
|
||||
if self._log_cli_enabled():
|
||||
self._setup_cli_logging()
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
config = self._config
|
||||
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||
if terminal_reporter is None:
|
||||
# terminal reporter is disabled e.g. by pytest-xdist.
|
||||
return
|
||||
|
||||
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"
|
||||
)
|
||||
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(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
|
||||
def set_log_path(self, fname):
|
||||
"""Public method, which can set filename parameter for
|
||||
Logging.FileHandler(). Also creates parent directory if
|
||||
it does not exist.
|
||||
|
||||
.. warning::
|
||||
Please considered as an experimental API.
|
||||
"""
|
||||
fname = Path(fname)
|
||||
|
||||
if not fname.is_absolute():
|
||||
fname = Path(self._config.rootdir, fname)
|
||||
|
||||
if not fname.parent.exists():
|
||||
fname.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
str(fname), mode="w", encoding="UTF-8"
|
||||
)
|
||||
self.log_file_handler.setFormatter(self.log_file_formatter)
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
"""Return True if log_cli should be considered enabled, either explicitly
|
||||
or because --log-cli-level was given in the command-line.
|
||||
@@ -430,10 +492,6 @@ class LoggingPlugin(object):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self):
|
||||
# This has to be called before the first log message is logged,
|
||||
# so we can access the terminal reporter plugin.
|
||||
self._setup_cli_logging()
|
||||
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
@@ -446,6 +504,15 @@ class LoggingPlugin(object):
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
with self._runtest_for_main(item, when):
|
||||
if self.log_file_handler is not None:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for_main(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(
|
||||
LogCaptureHandler(), formatter=self.formatter, level=self.log_level
|
||||
@@ -513,7 +580,6 @@ class LoggingPlugin(object):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionstart(self):
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("sessionstart")
|
||||
@@ -533,46 +599,6 @@ class LoggingPlugin(object):
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled."""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
|
||||
if self._log_cli_enabled() and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
|
||||
log_cli_handler = _LiveLoggingStreamHandler(
|
||||
terminal_reporter, capture_manager
|
||||
)
|
||||
log_cli_format = get_option_ini(
|
||||
self._config, "log_cli_format", "log_format"
|
||||
)
|
||||
log_cli_date_format = get_option_ini(
|
||||
self._config, "log_cli_date_format", "log_date_format"
|
||||
)
|
||||
if (
|
||||
self._config.option.color != "no"
|
||||
and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
|
||||
):
|
||||
log_cli_formatter = ColoredLevelFormatter(
|
||||
create_terminal_writer(self._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(
|
||||
self._config, "log_cli_level", "log_level"
|
||||
)
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import contextlib
|
||||
import fnmatch
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
@@ -117,6 +118,12 @@ def pytest_addoption(parser):
|
||||
metavar="path",
|
||||
help="ignore path during collection (multi-allowed).",
|
||||
)
|
||||
group.addoption(
|
||||
"--ignore-glob",
|
||||
action="append",
|
||||
metavar="path",
|
||||
help="ignore path pattern during collection (multi-allowed).",
|
||||
)
|
||||
group.addoption(
|
||||
"--deselect",
|
||||
action="append",
|
||||
@@ -296,6 +303,20 @@ def pytest_ignore_collect(path, config):
|
||||
if py.path.local(path) in ignore_paths:
|
||||
return True
|
||||
|
||||
ignore_globs = config._getconftest_pathlist(
|
||||
"collect_ignore_glob", path=path.dirpath()
|
||||
)
|
||||
ignore_globs = ignore_globs or []
|
||||
excludeglobopt = config.getoption("ignore_glob")
|
||||
if excludeglobopt:
|
||||
ignore_globs.extend([py.path.local(x) for x in excludeglobopt])
|
||||
|
||||
if any(
|
||||
fnmatch.fnmatch(six.text_type(path), six.text_type(glob))
|
||||
for glob in ignore_globs
|
||||
):
|
||||
return True
|
||||
|
||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||
if not allow_in_venv and _in_venv(path):
|
||||
return True
|
||||
@@ -550,19 +571,9 @@ class Session(nodes.FSCollector):
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
|
||||
if six.PY2:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1) and not f.strpath.endswith("*.pyc")
|
||||
|
||||
else:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1)
|
||||
|
||||
seen_dirs = set()
|
||||
for path in argpath.visit(
|
||||
fil=filter_, rec=self._recurse, bf=True, sort=True
|
||||
fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
dirpath = path.dirpath()
|
||||
if dirpath not in seen_dirs:
|
||||
@@ -592,7 +603,7 @@ class Session(nodes.FSCollector):
|
||||
col = self._node_cache[argpath]
|
||||
else:
|
||||
collect_root = self._pkg_roots.get(argpath.dirname, self)
|
||||
col = collect_root._collectfile(argpath)
|
||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||
if col:
|
||||
self._node_cache[argpath] = col
|
||||
m = self.matchnodes(col, names)
|
||||
@@ -607,6 +618,12 @@ class Session(nodes.FSCollector):
|
||||
yield y
|
||||
|
||||
def _collectfile(self, path, handle_dupes=True):
|
||||
assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % (
|
||||
path,
|
||||
path.isdir(),
|
||||
path.exists(),
|
||||
path.islink(),
|
||||
)
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
@@ -636,6 +653,18 @@ class Session(nodes.FSCollector):
|
||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
||||
return True
|
||||
|
||||
if six.PY2:
|
||||
|
||||
@staticmethod
|
||||
def _visit_filter(f):
|
||||
return f.check(file=1) and not f.strpath.endswith("*.pyc")
|
||||
|
||||
else:
|
||||
|
||||
@staticmethod
|
||||
def _visit_filter(f):
|
||||
return f.check(file=1)
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
"""Convert a dotted module name to path."""
|
||||
try:
|
||||
|
||||
@@ -113,11 +113,12 @@ class Node(object):
|
||||
|
||||
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
|
||||
|
||||
Example usage::
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn(PytestWarning("some message"))
|
||||
|
||||
"""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ class LsofFdLeakChecker(object):
|
||||
|
||||
def _exec_lsof(self):
|
||||
pid = os.getpid()
|
||||
return subprocess.check_output(("lsof", "-Ffn0", "-p", str(pid))).decode()
|
||||
# py3: use subprocess.DEVNULL directly.
|
||||
with open(os.devnull, "wb") as devnull:
|
||||
return subprocess.check_output(
|
||||
("lsof", "-Ffn0", "-p", str(pid)), stderr=devnull
|
||||
).decode()
|
||||
|
||||
def _parse_lsof_output(self, out):
|
||||
def isopen(line):
|
||||
@@ -398,6 +402,12 @@ class RunResult(object):
|
||||
self.stderr = LineMatcher(errlines)
|
||||
self.duration = duration
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<RunResult ret=%r len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
|
||||
% (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
|
||||
)
|
||||
|
||||
def parseoutcomes(self):
|
||||
"""Return a dictionary of outcomestring->num from parsing the terminal
|
||||
output that the test process produced.
|
||||
|
||||
@@ -599,6 +599,12 @@ class Package(Module):
|
||||
return proxy
|
||||
|
||||
def _collectfile(self, path, handle_dupes=True):
|
||||
assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % (
|
||||
path,
|
||||
path.isdir(),
|
||||
path.exists(),
|
||||
path.islink(),
|
||||
)
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
@@ -631,7 +637,8 @@ class Package(Module):
|
||||
pkg_prefixes = set()
|
||||
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
||||
# We will visit our own __init__.py file, in which case we skip it.
|
||||
if path.isfile():
|
||||
is_file = path.isfile()
|
||||
if is_file:
|
||||
if path.basename == "__init__.py" and path.dirpath() == this_path:
|
||||
continue
|
||||
|
||||
@@ -642,12 +649,15 @@ class Package(Module):
|
||||
):
|
||||
continue
|
||||
|
||||
if path.isdir() and path.join("__init__.py").check(file=1):
|
||||
if is_file:
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
elif not path.isdir():
|
||||
# Broken symlink or invalid/missing file.
|
||||
continue
|
||||
elif path.join("__init__.py").check(file=1):
|
||||
pkg_prefixes.add(path)
|
||||
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
|
||||
|
||||
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
|
||||
"""
|
||||
@@ -1144,9 +1154,10 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
|
||||
def _idval(val, argname, idx, idfn, item, config):
|
||||
if idfn:
|
||||
s = None
|
||||
try:
|
||||
s = idfn(val)
|
||||
generated_id = idfn(val)
|
||||
if generated_id is not None:
|
||||
val = generated_id
|
||||
except Exception as e:
|
||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
||||
msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n"
|
||||
@@ -1154,10 +1165,7 @@ def _idval(val, argname, idx, idfn, item, config):
|
||||
# we only append the exception type and message because on Python 2 reraise does nothing
|
||||
msg += " {}: {}\n".format(type(e).__name__, e)
|
||||
six.raise_from(ValueError(msg), e)
|
||||
if s:
|
||||
return ascii_escaped(s)
|
||||
|
||||
if config:
|
||||
elif config:
|
||||
hook_id = config.hook.pytest_make_parametrize_id(
|
||||
config=config, val=val, argname=argname
|
||||
)
|
||||
|
||||
@@ -621,6 +621,14 @@ def raises(expected_exception, *args, **kwargs):
|
||||
...
|
||||
>>> assert exc_info.type is ValueError
|
||||
|
||||
**Using with** ``pytest.mark.parametrize``
|
||||
|
||||
When using :ref:`pytest.mark.parametrize ref`
|
||||
it is possible to parametrize tests such that
|
||||
some runs raise an exception and others do not.
|
||||
|
||||
See :ref:`parametrizing_conditional_raising` for an example.
|
||||
|
||||
**Legacy form**
|
||||
|
||||
It is possible to specify a callable by passing a to-be-called lambda::
|
||||
|
||||
@@ -11,6 +11,7 @@ import warnings
|
||||
import six
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS
|
||||
from _pytest.deprecated import WARNS_EXEC
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.outcomes import fail
|
||||
@@ -84,10 +85,12 @@ def warns(expected_warning, *args, **kwargs):
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
match_expr = None
|
||||
if not args:
|
||||
if "match" in kwargs:
|
||||
match_expr = kwargs.pop("match")
|
||||
match_expr = kwargs.pop("match", None)
|
||||
if kwargs:
|
||||
warnings.warn(
|
||||
PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2
|
||||
)
|
||||
return WarningsChecker(expected_warning, match_expr=match_expr)
|
||||
elif isinstance(args[0], str):
|
||||
warnings.warn(WARNS_EXEC, stacklevel=2)
|
||||
@@ -97,12 +100,12 @@ def warns(expected_warning, *args, **kwargs):
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(kwargs)
|
||||
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
with WarningsChecker(expected_warning):
|
||||
code = _pytest._code.Source(code).compile()
|
||||
six.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
with WarningsChecker(expected_warning):
|
||||
return func(*args[1:], **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -222,12 +222,9 @@ class TerminalReporter(object):
|
||||
import _pytest.config
|
||||
|
||||
self.config = config
|
||||
self.verbosity = self.config.option.verbose
|
||||
self.showheader = self.verbosity >= 0
|
||||
self.showfspath = self.verbosity >= 0
|
||||
self.showlongtestinfo = self.verbosity > 0
|
||||
self._numcollected = 0
|
||||
self._session = None
|
||||
self._showfspath = None
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = py.path.local()
|
||||
@@ -255,13 +252,37 @@ class TerminalReporter(object):
|
||||
return False
|
||||
return self.config.getini("console_output_style") in ("progress", "count")
|
||||
|
||||
@property
|
||||
def verbosity(self):
|
||||
return self.config.option.verbose
|
||||
|
||||
@property
|
||||
def showheader(self):
|
||||
return self.verbosity >= 0
|
||||
|
||||
@property
|
||||
def showfspath(self):
|
||||
if self._showfspath is None:
|
||||
return self.verbosity >= 0
|
||||
return self._showfspath
|
||||
|
||||
@showfspath.setter
|
||||
def showfspath(self, value):
|
||||
self._showfspath = value
|
||||
|
||||
@property
|
||||
def showlongtestinfo(self):
|
||||
return self.verbosity > 0
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, nodeid, res, **markup):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
# NOTE: explicitly check for None to work around py bug, and for less
|
||||
# overhead in general (https://github.com/pytest-dev/py/pull/207).
|
||||
if self.currentfspath is None or fspath != self.currentfspath:
|
||||
if self.currentfspath is not None and self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
self.currentfspath = fspath
|
||||
@@ -574,19 +595,20 @@ class TerminalReporter(object):
|
||||
return lines
|
||||
|
||||
def pytest_collection_finish(self, session):
|
||||
if self.config.option.collectonly:
|
||||
if self.config.getoption("collectonly"):
|
||||
self._printcollecteditems(session.items)
|
||||
if self.stats.get("failed"):
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self.stats.get("failed"):
|
||||
rep.toterminal(self._tw)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
lines = self.config.hook.pytest_report_collectionfinish(
|
||||
config=self.config, startdir=self.startdir, items=session.items
|
||||
)
|
||||
self._write_report_lines_from_hooks(lines)
|
||||
|
||||
if self.config.getoption("collectonly"):
|
||||
if self.stats.get("failed"):
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self.stats.get("failed"):
|
||||
rep.toterminal(self._tw)
|
||||
|
||||
def _printcollecteditems(self, items):
|
||||
# to print out items and their parent collectors
|
||||
# we take care to leave out Instances aka ()
|
||||
@@ -652,7 +674,6 @@ class TerminalReporter(object):
|
||||
self.summary_passes()
|
||||
# Display any extra warnings from teardown here (if any).
|
||||
self.summary_warnings()
|
||||
self.summary_deprecated_python()
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
|
||||
@@ -774,20 +795,6 @@ class TerminalReporter(object):
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def summary_deprecated_python(self):
|
||||
if sys.version_info[:2] <= (3, 4) and self.verbosity >= 0:
|
||||
self.write_sep("=", "deprecated python version", yellow=True, bold=False)
|
||||
using_version = ".".join(str(x) for x in sys.version_info[:3])
|
||||
self.line(
|
||||
"You are using Python {}, which will no longer be supported in pytest 5.0".format(
|
||||
using_version
|
||||
),
|
||||
yellow=True,
|
||||
bold=False,
|
||||
)
|
||||
self.line("For more information, please read:")
|
||||
self.line(" https://docs.pytest.org/en/latest/py27-py34-deprecation.html")
|
||||
|
||||
def print_teardown_sections(self, rep):
|
||||
showcapture = self.config.option.showcapture
|
||||
if showcapture == "no":
|
||||
|
||||
@@ -31,7 +31,7 @@ class TempPathFactory(object):
|
||||
# using os.path.abspath() to get absolute path instead of resolve() as it
|
||||
# does not work the same in all platforms (see #4427)
|
||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
|
||||
convert=attr.converters.optional(
|
||||
converter=attr.converters.optional(
|
||||
lambda p: Path(os.path.abspath(six.text_type(p)))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -87,6 +87,9 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
|
||||
|
||||
@pytest.fixture(scope=scope, autouse=True)
|
||||
def fixture(self, request):
|
||||
if getattr(self, "__unittest_skip__", None):
|
||||
reason = self.__unittest_skip_why__
|
||||
pytest.skip(reason)
|
||||
if setup is not None:
|
||||
if pass_self:
|
||||
setup(self, request.function)
|
||||
|
||||
@@ -854,9 +854,7 @@ class TestDurations(object):
|
||||
result = testdir.runpytest("--durations=2")
|
||||
assert result.ret == 0
|
||||
lines = result.stdout.get_lines_after("*slowest*durations*")
|
||||
# account for the "deprecated python version" header
|
||||
index = 2 if sys.version_info[:2] > (3, 4) else 6
|
||||
assert "4 passed" in lines[index]
|
||||
assert "4 passed" in lines[2]
|
||||
|
||||
def test_calls_showall(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
@@ -969,6 +967,20 @@ def test_import_plugin_unicode_name(testdir):
|
||||
assert r.ret == 0
|
||||
|
||||
|
||||
def test_pytest_plugins_as_module(testdir):
|
||||
"""Do not raise an error if pytest_plugins attribute is a module (#3899)"""
|
||||
testdir.makepyfile(
|
||||
**{
|
||||
"__init__.py": "",
|
||||
"pytest_plugins.py": "",
|
||||
"conftest.py": "from . import pytest_plugins",
|
||||
"test_foo.py": "def test(): pass",
|
||||
}
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("* 1 passed in *")
|
||||
|
||||
|
||||
def test_deferred_hook_checking(testdir):
|
||||
"""
|
||||
Check hooks as late as possible (#1821).
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
|
||||
|
||||
pytestmark = pytest.mark.pytester_example_path("deprecated")
|
||||
@@ -222,19 +222,9 @@ def test_fixture_named_request(testdir):
|
||||
)
|
||||
|
||||
|
||||
def test_python_deprecation(testdir):
|
||||
result = testdir.runpytest()
|
||||
python_ver = ".".join(str(x) for x in sys.version_info[:3])
|
||||
msg = "You are using Python {}, which will no longer be supported in pytest 5.0".format(
|
||||
python_ver
|
||||
)
|
||||
if sys.version_info[:2] <= (3, 4):
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
msg,
|
||||
"For more information, please read:",
|
||||
" https://docs.pytest.org/en/latest/py27-py34-deprecation.html",
|
||||
]
|
||||
)
|
||||
else:
|
||||
assert msg not in result.stdout.str()
|
||||
def test_pytest_warns_unknown_kwargs():
|
||||
with pytest.warns(
|
||||
PytestDeprecationWarning,
|
||||
match=r"pytest.warns\(\) got unexpected keyword arguments: \['foo'\]",
|
||||
):
|
||||
pytest.warns(UserWarning, foo="hello")
|
||||
|
||||
13
testing/example_scripts/unittest/test_setup_skip.py
Normal file
13
testing/example_scripts/unittest/test_setup_skip.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class."""
|
||||
import unittest
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Test(Base):
|
||||
def test_foo(self):
|
||||
assert 0
|
||||
14
testing/example_scripts/unittest/test_setup_skip_class.py
Normal file
14
testing/example_scripts/unittest/test_setup_skip_class.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class."""
|
||||
import unittest
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Test(Base):
|
||||
def test_foo(self):
|
||||
assert 0
|
||||
12
testing/example_scripts/unittest/test_setup_skip_module.py
Normal file
12
testing/example_scripts/unittest/test_setup_skip_module.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""setUpModule is always called, even if all tests in the module are skipped"""
|
||||
import unittest
|
||||
|
||||
|
||||
def setUpModule():
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Base(unittest.TestCase):
|
||||
def test(self):
|
||||
assert 0
|
||||
@@ -1002,3 +1002,51 @@ def test_log_in_hooks(testdir):
|
||||
assert "sessionstart" in contents
|
||||
assert "runtestloop" in contents
|
||||
assert "sessionfinish" in contents
|
||||
|
||||
|
||||
def test_log_set_path(testdir):
|
||||
report_dir_base = testdir.tmpdir.strpath
|
||||
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_file_level = DEBUG
|
||||
log_cli=true
|
||||
"""
|
||||
)
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_setup(item):
|
||||
config = item.config
|
||||
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
||||
report_file = os.path.join({}, item._request.node.name)
|
||||
logging_plugin.set_log_path(report_file)
|
||||
yield
|
||||
""".format(
|
||||
repr(report_dir_base)
|
||||
)
|
||||
)
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger("testcase-logger")
|
||||
def test_first():
|
||||
logger.info("message from test 1")
|
||||
assert True
|
||||
|
||||
def test_second():
|
||||
logger.debug("message from test 2")
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
testdir.runpytest()
|
||||
with open(os.path.join(report_dir_base, "test_first"), "r") as rfh:
|
||||
content = rfh.read()
|
||||
assert "message from test 1" in content
|
||||
|
||||
with open(os.path.join(report_dir_base, "test_second"), "r") as rfh:
|
||||
content = rfh.read()
|
||||
assert "message from test 2" in content
|
||||
|
||||
@@ -418,6 +418,21 @@ class TestMetafunc(object):
|
||||
]
|
||||
)
|
||||
|
||||
def test_parametrize_ids_returns_non_string(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""\
|
||||
import pytest
|
||||
|
||||
def ids(d):
|
||||
return d
|
||||
|
||||
@pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids)
|
||||
def test(arg):
|
||||
assert arg
|
||||
"""
|
||||
)
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_idmaker_with_ids(self):
|
||||
from _pytest.python import idmaker
|
||||
|
||||
|
||||
@@ -94,6 +94,54 @@ class TestRaises(object):
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||
|
||||
def test_does_not_raise(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
'''Test how much I know division.'''
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*4 passed*"])
|
||||
|
||||
def test_does_not_raise_does_raise(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(0, does_not_raise()),
|
||||
(1, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
'''Test how much I know division.'''
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||
|
||||
def test_noclass(self):
|
||||
with pytest.raises(TypeError):
|
||||
pytest.raises("wrong", lambda: None)
|
||||
|
||||
@@ -68,38 +68,16 @@ def getmsg(f, extra_ns=None, must_pass=False):
|
||||
pytest.fail("function didn't raise at all")
|
||||
|
||||
|
||||
def adjust_body_for_new_docstring_in_module_node(m):
|
||||
"""Module docstrings in 3.8 are part of Module node.
|
||||
This was briefly in 3.7 as well but got reverted in beta 5.
|
||||
|
||||
It's not in the body so we remove it so the following body items have
|
||||
the same indexes on all Python versions:
|
||||
|
||||
TODO:
|
||||
|
||||
We have a complicated sys.version_info if in here to ease testing on
|
||||
various Python 3.7 versions, but we should remove the 3.7 check after
|
||||
3.7 is released as stable to make this check more straightforward.
|
||||
"""
|
||||
if sys.version_info < (3, 8) and not (
|
||||
(3, 7) <= sys.version_info <= (3, 7, 0, "beta", 4)
|
||||
):
|
||||
assert len(m.body) > 1
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
del m.body[0]
|
||||
|
||||
|
||||
class TestAssertionRewrite(object):
|
||||
def test_place_initial_imports(self):
|
||||
s = """'Doc string'\nother = stuff"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
for imp in m.body[0:2]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[2], ast.Assign)
|
||||
assert isinstance(m.body[3], ast.Assign)
|
||||
s = """from __future__ import division\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
@@ -110,24 +88,24 @@ class TestAssertionRewrite(object):
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
s = """'doc string'\nfrom __future__ import division"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
s = """'doc string'\nfrom __future__ import division\nother"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 3
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
assert isinstance(m.body[4], ast.Expr)
|
||||
s = """from . import relative\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
for imp in m.body[0:2]:
|
||||
for imp in m.body[:2]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 1
|
||||
assert imp.col_offset == 0
|
||||
@@ -136,9 +114,8 @@ class TestAssertionRewrite(object):
|
||||
def test_dont_rewrite(self):
|
||||
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert len(m.body) == 1
|
||||
assert m.body[0].msg is None
|
||||
assert len(m.body) == 2
|
||||
assert m.body[1].msg is None
|
||||
|
||||
def test_dont_rewrite_plugin(self, testdir):
|
||||
contents = {
|
||||
@@ -1331,10 +1308,17 @@ class TestEarlyRewriteBailout(object):
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
|
||||
)
|
||||
def test_cwd_changed(self, testdir):
|
||||
def test_cwd_changed(self, testdir, monkeypatch):
|
||||
# Setup conditions for py's fspath trying to import pathlib on py34
|
||||
# always (previously triggered via xdist only).
|
||||
# Ref: https://github.com/pytest-dev/py/pull/207
|
||||
monkeypatch.setattr(sys, "path", [""] + sys.path)
|
||||
if "pathlib" in sys.modules:
|
||||
del sys.modules["pathlib"]
|
||||
|
||||
testdir.makepyfile(
|
||||
**{
|
||||
"test_bar.py": """
|
||||
"test_setup_nonexisting_cwd.py": """
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -1343,7 +1327,7 @@ class TestEarlyRewriteBailout(object):
|
||||
os.chdir(d)
|
||||
shutil.rmtree(d)
|
||||
""",
|
||||
"test_foo.py": """
|
||||
"test_test.py": """
|
||||
def test():
|
||||
pass
|
||||
""",
|
||||
|
||||
@@ -374,6 +374,26 @@ class TestCustomConftests(object):
|
||||
assert result.ret == 0
|
||||
assert "passed" in result.stdout.str()
|
||||
|
||||
def test_collectignoreglob_exclude_on_option(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
collect_ignore_glob = ['*w*l[dt]*']
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--XX", action="store_true", default=False)
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("XX"):
|
||||
collect_ignore_glob[:] = []
|
||||
"""
|
||||
)
|
||||
testdir.makepyfile(test_world="def test_hello(): pass")
|
||||
testdir.makepyfile(test_welt="def test_hallo(): pass")
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
result.stdout.fnmatch_lines("*collected 0 items*")
|
||||
result = testdir.runpytest("--XX")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines("*2 passed*")
|
||||
|
||||
def test_pytest_fs_collect_hooks_are_seen(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
@@ -1144,3 +1164,72 @@ def test_collect_symlink_out_of_tree(testdir):
|
||||
]
|
||||
)
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_collectignore_via_conftest(testdir, monkeypatch):
|
||||
"""collect_ignore in parent conftest skips importing child (issue #4592)."""
|
||||
tests = testdir.mkpydir("tests")
|
||||
tests.ensure("conftest.py").write("collect_ignore = ['ignore_me']")
|
||||
|
||||
ignore_me = tests.mkdir("ignore_me")
|
||||
ignore_me.ensure("__init__.py")
|
||||
ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'")
|
||||
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def test_collect_pkg_init_and_file_in_args(testdir):
|
||||
subdir = testdir.mkdir("sub")
|
||||
init = subdir.ensure("__init__.py")
|
||||
init.write("def test_init(): pass")
|
||||
p = subdir.ensure("test_file.py")
|
||||
p.write("def test_file(): pass")
|
||||
|
||||
# NOTE: without "-o python_files=*.py" this collects test_file.py twice.
|
||||
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895)
|
||||
# initially (causing a RecursionError).
|
||||
result = testdir.runpytest("-v", str(init), str(p))
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"*2 passed in*",
|
||||
]
|
||||
)
|
||||
|
||||
result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init), str(p))
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"sub/__init__.py::test_init PASSED*",
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"*2 passed in*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(py.path.local, "mksymlinkto"),
|
||||
reason="symlink not available on this platform",
|
||||
)
|
||||
@pytest.mark.parametrize("use_pkg", (True, False))
|
||||
def test_collect_sub_with_symlinks(use_pkg, testdir):
|
||||
sub = testdir.mkdir("sub")
|
||||
if use_pkg:
|
||||
sub.ensure("__init__.py")
|
||||
sub.ensure("test_file.py").write("def test_file(): pass")
|
||||
|
||||
# Create a broken symlink.
|
||||
sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py")
|
||||
|
||||
# Symlink that gets collected.
|
||||
sub.join("test_symlink.py").mksymlinkto("test_file.py")
|
||||
|
||||
result = testdir.runpytest("-v", str(sub))
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"sub/test_symlink.py::test_file PASSED*",
|
||||
"*2 passed in*",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -8,10 +8,12 @@ import textwrap
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest.config import _iter_rewritable_modules
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.config.findpaths import determine_setup
|
||||
from _pytest.config.findpaths import get_common_ancestor
|
||||
from _pytest.config.findpaths import getcfg
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||
from _pytest.main import EXIT_USAGEERROR
|
||||
|
||||
|
||||
class TestParseIni(object):
|
||||
@@ -1031,9 +1033,12 @@ class TestOverrideIniArgs(object):
|
||||
|
||||
monkeypatch.setenv("PYTEST_ADDOPTS", "-o")
|
||||
config = get_config()
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
with pytest.raises(UsageError) as excinfo:
|
||||
config._preparse(["cache_dir=ignored"], addopts=True)
|
||||
assert excinfo.value.args[0] == _pytest.main.EXIT_USAGEERROR
|
||||
assert (
|
||||
"error: argument -o/--override-ini: expected one argument (via PYTEST_ADDOPTS)"
|
||||
in excinfo.value.args[0]
|
||||
)
|
||||
|
||||
def test_addopts_from_ini_not_concatenated(self, testdir):
|
||||
"""addopts from ini should not take values from normal args (#4265)."""
|
||||
@@ -1046,7 +1051,7 @@ class TestOverrideIniArgs(object):
|
||||
result = testdir.runpytest("cache_dir=ignored")
|
||||
result.stderr.fnmatch_lines(
|
||||
[
|
||||
"%s: error: argument -o/--override-ini: expected one argument"
|
||||
"%s: error: argument -o/--override-ini: expected one argument (via addopts config)"
|
||||
% (testdir.request.config._parser.optparser.prog,)
|
||||
]
|
||||
)
|
||||
@@ -1083,3 +1088,68 @@ class TestOverrideIniArgs(object):
|
||||
result = testdir.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py")
|
||||
assert "ERROR:" not in result.stderr.str()
|
||||
result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="])
|
||||
|
||||
|
||||
def test_help_via_addopts(testdir):
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
addopts = --unknown-option-should-allow-for-help --help
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"usage: *",
|
||||
"positional arguments:",
|
||||
# Displays full/default help.
|
||||
"to see available markers type: pytest --markers",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_help_and_version_after_argument_error(testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def validate(arg):
|
||||
raise argparse.ArgumentTypeError("argerror")
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('cov')
|
||||
group.addoption(
|
||||
"--invalid-option-should-allow-for-help",
|
||||
type=validate,
|
||||
)
|
||||
"""
|
||||
)
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
addopts = --invalid-option-should-allow-for-help
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--help")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"usage: *",
|
||||
"positional arguments:",
|
||||
"NOTE: displaying only minimal help due to UsageError.",
|
||||
]
|
||||
)
|
||||
result.stderr.fnmatch_lines(
|
||||
[
|
||||
"ERROR: usage: *",
|
||||
"%s: error: argument --invalid-option-should-allow-for-help: expected one argument"
|
||||
% (testdir.request.config._parser.optparser.prog,),
|
||||
]
|
||||
)
|
||||
# Does not display full/default help.
|
||||
assert "to see available markers type: pytest --markers" not in result.stdout.lines
|
||||
assert result.ret == EXIT_USAGEERROR
|
||||
|
||||
result = testdir.runpytest("--version")
|
||||
result.stderr.fnmatch_lines(
|
||||
["*pytest*{}*imported from*".format(pytest.__version__)]
|
||||
)
|
||||
assert result.ret == EXIT_USAGEERROR
|
||||
|
||||
@@ -244,6 +244,42 @@ def test_conftest_symlink(testdir):
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(py.path.local, "mksymlinkto"),
|
||||
reason="symlink not available on this platform",
|
||||
)
|
||||
def test_conftest_symlink_files(testdir):
|
||||
"""Check conftest.py loading when running in directory with symlinks."""
|
||||
real = testdir.tmpdir.mkdir("real")
|
||||
source = {
|
||||
"app/test_foo.py": "def test1(fixture): pass",
|
||||
"app/__init__.py": "",
|
||||
"app/conftest.py": textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
print("conftest_loaded")
|
||||
|
||||
@pytest.fixture
|
||||
def fixture():
|
||||
print("fixture_used")
|
||||
"""
|
||||
),
|
||||
}
|
||||
testdir.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
|
||||
|
||||
# Create a build directory that contains symlinks to actual files
|
||||
# but doesn't symlink actual directories.
|
||||
build = testdir.tmpdir.mkdir("build")
|
||||
build.mkdir("app")
|
||||
for f in source:
|
||||
build.join(f).mksymlinkto(real.join(f))
|
||||
build.chdir()
|
||||
result = testdir.runpytest("-vs", "app/test_foo.py")
|
||||
result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"])
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
|
||||
def test_no_conftest(testdir):
|
||||
testdir.makeconftest("assert 0")
|
||||
result = testdir.runpytest("--noconftest")
|
||||
|
||||
@@ -11,6 +11,7 @@ import py
|
||||
|
||||
import pytest
|
||||
from _pytest.config import argparsing as parseopt
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -19,11 +20,9 @@ def parser():
|
||||
|
||||
|
||||
class TestParser(object):
|
||||
def test_no_help_by_default(self, capsys):
|
||||
def test_no_help_by_default(self):
|
||||
parser = parseopt.Parser(usage="xyz")
|
||||
pytest.raises(SystemExit, lambda: parser.parse(["-h"]))
|
||||
out, err = capsys.readouterr()
|
||||
assert err.find("error: unrecognized arguments") != -1
|
||||
pytest.raises(UsageError, lambda: parser.parse(["-h"]))
|
||||
|
||||
def test_custom_prog(self, parser):
|
||||
"""Custom prog can be set for `argparse.ArgumentParser`."""
|
||||
|
||||
@@ -513,6 +513,76 @@ class TestPDB(object):
|
||||
assert "1 failed" in rest
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_interaction_continue_recursive(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
mytest="""
|
||||
import pdb
|
||||
import pytest
|
||||
|
||||
count_continue = 0
|
||||
|
||||
# Simulates pdbpp, which injects Pdb into do_debug, and uses
|
||||
# self.__class__ in do_continue.
|
||||
class CustomPdb(pdb.Pdb, object):
|
||||
def do_debug(self, arg):
|
||||
import sys
|
||||
import types
|
||||
|
||||
newglobals = {
|
||||
'Pdb': self.__class__, # NOTE: different with pdb.Pdb
|
||||
'sys': sys,
|
||||
}
|
||||
if sys.version_info < (3, ):
|
||||
do_debug_func = pdb.Pdb.do_debug.im_func
|
||||
else:
|
||||
do_debug_func = pdb.Pdb.do_debug
|
||||
|
||||
orig_do_debug = types.FunctionType(
|
||||
do_debug_func.__code__, newglobals,
|
||||
do_debug_func.__name__, do_debug_func.__defaults__,
|
||||
)
|
||||
return orig_do_debug(self, arg)
|
||||
do_debug.__doc__ = pdb.Pdb.do_debug.__doc__
|
||||
|
||||
def do_continue(self, *args, **kwargs):
|
||||
global count_continue
|
||||
count_continue += 1
|
||||
return super(CustomPdb, self).do_continue(*args, **kwargs)
|
||||
|
||||
def foo():
|
||||
print("print_from_foo")
|
||||
|
||||
def test_1():
|
||||
i = 0
|
||||
print("hello17")
|
||||
pytest.set_trace()
|
||||
x = 3
|
||||
print("hello18")
|
||||
|
||||
assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
|
||||
pytest.fail("expected_failure")
|
||||
"""
|
||||
)
|
||||
child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
|
||||
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
||||
child.expect(r"\n\(Pdb")
|
||||
child.sendline("debug foo()")
|
||||
child.expect("ENTERING RECURSIVE DEBUGGER")
|
||||
child.expect(r"\n\(\(Pdb")
|
||||
child.sendline("c")
|
||||
child.expect("LEAVING RECURSIVE DEBUGGER")
|
||||
assert b"PDB continue" not in child.before
|
||||
assert b"print_from_foo" in child.before
|
||||
child.sendline("c")
|
||||
child.expect(r"PDB continue \(IO-capturing resumed\)")
|
||||
rest = child.read().decode("utf8")
|
||||
assert "hello17" in rest # out is captured
|
||||
assert "hello18" in rest # out is captured
|
||||
assert "1 failed" in rest
|
||||
assert "Failed: expected_failure" in rest
|
||||
assert "AssertionError: unexpected_failure" not in rest
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_without_capture(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -127,6 +127,17 @@ def test_runresult_assertion_on_xpassed(testdir):
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_runresult_repr():
|
||||
from _pytest.pytester import RunResult
|
||||
|
||||
assert (
|
||||
repr(
|
||||
RunResult(ret="ret", outlines=[""], errlines=["some", "errors"], duration=1)
|
||||
)
|
||||
== "<RunResult ret='ret' len(stdout.lines)=1 len(stderr.lines)=2 duration=1.00s>"
|
||||
)
|
||||
|
||||
|
||||
def test_xpassed_with_strict_is_considered_a_failure(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -253,6 +253,21 @@ def test_exclude(testdir):
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
|
||||
def test_exclude_glob(testdir):
|
||||
hellodir = testdir.mkdir("hello")
|
||||
hellodir.join("test_hello.py").write("x y syntaxerror")
|
||||
hello2dir = testdir.mkdir("hello2")
|
||||
hello2dir.join("test_hello2.py").write("x y syntaxerror")
|
||||
hello3dir = testdir.mkdir("hallo3")
|
||||
hello3dir.join("test_hello3.py").write("x y syntaxerror")
|
||||
subdir = testdir.mkdir("sub")
|
||||
subdir.join("test_hello4.py").write("x y syntaxerror")
|
||||
testdir.makepyfile(test_ok="def test_pass(): pass")
|
||||
result = testdir.runpytest("--ignore-glob=*h[ea]llo*")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
|
||||
def test_deselect(testdir):
|
||||
testdir.makepyfile(
|
||||
test_a="""
|
||||
|
||||
@@ -649,7 +649,10 @@ class TestTerminalFunctional(object):
|
||||
assert "===" not in s
|
||||
assert "passed" not in s
|
||||
|
||||
def test_report_collectionfinish_hook(self, testdir):
|
||||
@pytest.mark.parametrize(
|
||||
"params", [(), ("--collect-only",)], ids=["no-params", "collect-only"]
|
||||
)
|
||||
def test_report_collectionfinish_hook(self, testdir, params):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_report_collectionfinish(config, startdir, items):
|
||||
@@ -664,7 +667,7 @@ class TestTerminalFunctional(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest(*params)
|
||||
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
||||
|
||||
|
||||
|
||||
@@ -1026,3 +1026,18 @@ def test_error_message_with_parametrized_fixtures(testdir):
|
||||
"*Function type: TestCaseFunction",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, expected_outcome",
|
||||
[
|
||||
("test_setup_skip.py", "1 skipped"),
|
||||
("test_setup_skip_class.py", "1 skipped"),
|
||||
("test_setup_skip_module.py", "1 error"),
|
||||
],
|
||||
)
|
||||
def test_setup_inheritance_skipping(testdir, test_name, expected_outcome):
|
||||
"""Issue #4700"""
|
||||
testdir.copy_example("unittest/{}".format(test_name))
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("* {} in *".format(expected_outcome))
|
||||
|
||||
48
tox.ini
48
tox.ini
@@ -1,5 +1,6 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
isolated_build = True
|
||||
minversion = 3.5.3
|
||||
distshare = {homedir}/.tox/distshare
|
||||
# make sure to update environment list in travis.yml and appveyor.yml
|
||||
envlist =
|
||||
@@ -9,6 +10,7 @@ envlist =
|
||||
py35
|
||||
py36
|
||||
py37
|
||||
py38
|
||||
pypy
|
||||
{py27,py37}-{pexpect,xdist,trial,numpy,pluggymaster}
|
||||
py27-nobyte
|
||||
@@ -18,18 +20,22 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest --lsof {posargs}
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {env:_PYTEST_TOX_ARGS:} {posargs}
|
||||
coverage: coverage combine
|
||||
coverage: coverage report
|
||||
passenv = USER USERNAME COVERAGE_* TRAVIS
|
||||
passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS
|
||||
setenv =
|
||||
# configuration if a user runs tox with a "coverage" factor, for example "tox -e py37-coverage"
|
||||
_PYTEST_TOX_ARGS=--lsof
|
||||
# Configuration to run with coverage similar to Travis/Appveyor, e.g.
|
||||
# "tox -e py37-coverage".
|
||||
coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m
|
||||
coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
coverage: COVERAGE_FILE={toxinidir}/.coverage
|
||||
coverage: COVERAGE_PROCESS_START={toxinidir}/.coveragerc
|
||||
xdist: _PYTEST_TOX_ARGS={env:_PYTEST_TOX_ARGS:-n auto}
|
||||
extras = testing
|
||||
deps =
|
||||
xdist: pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
|
||||
[testenv:py27-subprocess]
|
||||
@@ -47,27 +53,11 @@ basepython = python3
|
||||
deps = pre-commit>=1.11.0
|
||||
commands = pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
[testenv:py27-xdist]
|
||||
extras = testing
|
||||
deps =
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest -n auto {posargs}
|
||||
|
||||
[testenv:py37-xdist]
|
||||
# NOTE: copied from above due to https://github.com/tox-dev/tox/issues/706.
|
||||
extras = testing
|
||||
deps =
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands = {[testenv:py27-xdist]commands}
|
||||
|
||||
[testenv:py27-pexpect]
|
||||
platform = linux|darwin
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pexpect
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_pdb.py testing/test_terminal.py testing/test_unittest.py}
|
||||
|
||||
@@ -79,8 +69,8 @@ commands = {[testenv:py27-pexpect]commands}
|
||||
[testenv:py27-nobyte]
|
||||
extras = testing
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
distribute = true
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
@@ -90,8 +80,8 @@ commands =
|
||||
|
||||
[testenv:py27-trial]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
twisted
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_unittest.py}
|
||||
|
||||
@@ -101,8 +91,8 @@ commands = {[testenv:py27-trial]commands}
|
||||
|
||||
[testenv:py27-numpy]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
numpy
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands=
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/python/approx.py}
|
||||
|
||||
@@ -114,11 +104,13 @@ commands = {[testenv:py27-numpy]commands}
|
||||
setenv=
|
||||
{[testenv]setenv}
|
||||
_PYTEST_SETUP_SKIP_PLUGGY_DEP=1
|
||||
# NOTE: using env instead of "{[testenv]deps}", because of https://github.com/tox-dev/tox/issues/706.
|
||||
_PYTEST_TOX_EXTRA_DEP=git+https://github.com/pytest-dev/pluggy.git@master
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
git+https://github.com/pytest-dev/pluggy.git@master
|
||||
|
||||
[testenv:py37-pluggymaster]
|
||||
setenv = {[testenv:py27-pluggymaster]setenv}
|
||||
deps = {[testenv:py27-pluggymaster]deps}
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3
|
||||
@@ -134,8 +126,8 @@ commands =
|
||||
basepython = python3
|
||||
skipsdist = True
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyYAML
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest doc/en
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest
|
||||
@@ -165,7 +157,9 @@ commands =
|
||||
|
||||
[testenv:py37-freeze]
|
||||
changedir = testing/freeze
|
||||
# Disable PEP 517 with pip, which does not work with PyInstaller currently.
|
||||
deps =
|
||||
--no-use-pep517
|
||||
pyinstaller
|
||||
commands =
|
||||
{envpython} create_executable.py
|
||||
|
||||
Reference in New Issue
Block a user