Compare commits
221 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
527845ef29 | ||
|
|
489e638b4e | ||
|
|
13ee1cffed | ||
|
|
4c148bd0ef | ||
|
|
71a7b3c062 | ||
|
|
ebab1b6c69 | ||
|
|
49773b573f | ||
|
|
32979def7d | ||
|
|
ab00c3e911 | ||
|
|
6e4efccc38 | ||
|
|
269eeec702 | ||
|
|
0e1be01b7a | ||
|
|
aff463a3c4 | ||
|
|
b3247c1d03 | ||
|
|
169635e889 | ||
|
|
cd0b2ace67 | ||
|
|
17a1ed5edf | ||
|
|
a54cd4c2fd | ||
|
|
77de45cce3 | ||
|
|
d550c33cd0 | ||
|
|
a58099022a | ||
|
|
3bc7ced97a | ||
|
|
8979b2a9d7 | ||
|
|
d4c11e58aa | ||
|
|
7f83605c81 | ||
|
|
37b41de779 | ||
|
|
bf2c10c810 | ||
|
|
4285325cb8 | ||
|
|
cbbd606b6c | ||
|
|
a24ca9872f | ||
|
|
b8be339632 | ||
|
|
2aad8c0fce | ||
|
|
15cbd61159 | ||
|
|
2f955e0c99 | ||
|
|
af37778b0d | ||
|
|
3f5e9ea71e | ||
|
|
443275f025 | ||
|
|
8426c57a9e | ||
|
|
30ca9f9d38 | ||
|
|
46d87deb5d | ||
|
|
203508d9f3 | ||
|
|
2c7f94fdb9 | ||
|
|
ff90c9e237 | ||
|
|
b4e8861aa5 | ||
|
|
baa189f5a3 | ||
|
|
113bfb6be8 | ||
|
|
9f4688e549 | ||
|
|
3a9d0b26d5 | ||
|
|
a5e60b6a2d | ||
|
|
0df42b4426 | ||
|
|
0d96a5bf90 | ||
|
|
060f68bd90 | ||
|
|
e5739a3115 | ||
|
|
8a8797df80 | ||
|
|
8994603d46 | ||
|
|
5c0b340a4b | ||
|
|
196dcc37a8 | ||
|
|
0ab57c4139 | ||
|
|
29a7b5e064 | ||
|
|
27ae270159 | ||
|
|
2e40a8b3ca | ||
|
|
18e053546c | ||
|
|
9dbcac9af3 | ||
|
|
4a436572a8 | ||
|
|
97a4967b03 | ||
|
|
8f6a5928f7 | ||
|
|
5d89a93977 | ||
|
|
c53b72fd7b | ||
|
|
6bb739516f | ||
|
|
8d735f3e1d | ||
|
|
aca1b06747 | ||
|
|
8dcd2718aa | ||
|
|
5ad1313b8a | ||
|
|
3b3d237f07 | ||
|
|
c4c968fe69 | ||
|
|
0b6df94b12 | ||
|
|
c3d420bf75 | ||
|
|
ebb4c47155 | ||
|
|
7ea5a22657 | ||
|
|
cd76366d87 | ||
|
|
931e8830ba | ||
|
|
621374679b | ||
|
|
8be1136d03 | ||
|
|
e0b63e34fa | ||
|
|
1fd67c9000 | ||
|
|
e3406e0818 | ||
|
|
dc79116de3 | ||
|
|
8433e2ba04 | ||
|
|
86e1b44230 | ||
|
|
5d3f7d7142 | ||
|
|
648d5d0c6b | ||
|
|
dff597dcd0 | ||
|
|
076fb56f85 | ||
|
|
150537d5e0 | ||
|
|
d8c23fd39b | ||
|
|
b748576358 | ||
|
|
f555a3a76c | ||
|
|
1f4831a23f | ||
|
|
0a0d97aeb5 | ||
|
|
4a3863c2e2 | ||
|
|
d314691fd3 | ||
|
|
01e37fe892 | ||
|
|
abbdb60051 | ||
|
|
5939b336cd | ||
|
|
3d289b803d | ||
|
|
4a704bbb55 | ||
|
|
ee6c9f50a2 | ||
|
|
3181718fe0 | ||
|
|
2674f352e8 | ||
|
|
6fb46a0e79 | ||
|
|
820ea6d68f | ||
|
|
b0032ba2b3 | ||
|
|
cf9b31bd5a | ||
|
|
b68b80aec9 | ||
|
|
bd1d17e8de | ||
|
|
93306f6a5e | ||
|
|
962aede290 | ||
|
|
a8d3d329ec | ||
|
|
b256cd2a6a | ||
|
|
b6b36bc167 | ||
|
|
3dd24f8d21 | ||
|
|
794fb193ba | ||
|
|
d7e1f037d9 | ||
|
|
29ff9301d8 | ||
|
|
f3c666db3c | ||
|
|
b93aa5e35f | ||
|
|
bc66f7e43f | ||
|
|
cb6b851780 | ||
|
|
afb8a4e35d | ||
|
|
06a182386b | ||
|
|
fac07c1b3f | ||
|
|
e8c0ca4f08 | ||
|
|
ac6f257efc | ||
|
|
554cb8d09c | ||
|
|
25b504b4f0 | ||
|
|
0a6e086f9d | ||
|
|
f24c470403 | ||
|
|
52a7ccef57 | ||
|
|
bd2d0d2c3c | ||
|
|
08997279f4 | ||
|
|
205e29d843 | ||
|
|
672c901c70 | ||
|
|
c70efaa0fb | ||
|
|
d370e7788d | ||
|
|
0d83dd1b31 | ||
|
|
ed293ec3e9 | ||
|
|
2f8427bb4e | ||
|
|
94608c6110 | ||
|
|
afc607cfd8 | ||
|
|
30729b7c3c | ||
|
|
dfc5399cd7 | ||
|
|
76489d30f7 | ||
|
|
d6f75d2836 | ||
|
|
d85a3ca19a | ||
|
|
f3c9c6e8a8 | ||
|
|
67bd60d5c6 | ||
|
|
28a93b9eeb | ||
|
|
70461d1ead | ||
|
|
73eccb4c36 | ||
|
|
d7a76a4d07 | ||
|
|
924b5e2e3d | ||
|
|
d87279115d | ||
|
|
0a2735a275 | ||
|
|
c90e76c371 | ||
|
|
370daf0441 | ||
|
|
586ecea6f2 | ||
|
|
db4df5833a | ||
|
|
b17c6e5f89 | ||
|
|
c3f63ac143 | ||
|
|
3862b0b28d | ||
|
|
476d4df1b7 | ||
|
|
52449903c3 | ||
|
|
506c9c91c0 | ||
|
|
38f34e2fa1 | ||
|
|
ba41015ef6 | ||
|
|
ebfc1c49d1 | ||
|
|
45e7734b1a | ||
|
|
8ce6e39b1c | ||
|
|
c8e7d1ae34 | ||
|
|
7b5d4d01ed | ||
|
|
a4f4579f19 | ||
|
|
e4da9bacdf | ||
|
|
1e295535c3 | ||
|
|
3ca1e4b7f0 | ||
|
|
dc19624248 | ||
|
|
f8f1a52ea0 | ||
|
|
5c6d7739bc | ||
|
|
771b5c8852 | ||
|
|
fc5ec5807e | ||
|
|
fc544dc660 | ||
|
|
7792587b3f | ||
|
|
c2cd239d35 | ||
|
|
cb0ba18f53 | ||
|
|
3b85e0c3a9 | ||
|
|
73bc6bacfa | ||
|
|
8e8a953ac6 | ||
|
|
852b96714e | ||
|
|
dd64f1a4a9 | ||
|
|
41a6ec6f31 | ||
|
|
7feab7391d | ||
|
|
f0bfe9de3d | ||
|
|
596937e610 | ||
|
|
57fcd3f57e | ||
|
|
65f5383106 | ||
|
|
964c29cb93 | ||
|
|
38fb6aae78 | ||
|
|
1c5b887dfd | ||
|
|
ba209b5230 | ||
|
|
b62fd79c0c | ||
|
|
655146e522 | ||
|
|
88f2cc9b64 | ||
|
|
ed2bb9d723 | ||
|
|
294729962d | ||
|
|
652936f47f | ||
|
|
1fe2e2cb03 | ||
|
|
e66473853c | ||
|
|
fdd4abb88a | ||
|
|
5085aa2bce | ||
|
|
912330a7e2 | ||
|
|
1f0d06641a | ||
|
|
4c62cd451a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.pytest_cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
|
||||
9
AUTHORS
9
AUTHORS
@@ -3,12 +3,15 @@ merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Aaron Coleman
|
||||
Abdeali JK
|
||||
Abhijeet Kasurde
|
||||
Ahn Ki-Wook
|
||||
Alan Velasco
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
@@ -17,6 +20,7 @@ Anthon van der Neut
|
||||
Anthony Sottile
|
||||
Antony Lee
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Aviv Palivoda
|
||||
Barney Gale
|
||||
@@ -38,6 +42,7 @@ Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Cyrus Maden
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -74,9 +79,11 @@ Grig Gheorghiu
|
||||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Henk-Jaap Wagenaar
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -147,6 +154,7 @@ Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
Ralf Schmitt
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
@@ -177,6 +185,7 @@ Tom Dalton
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
|
||||
181
CHANGELOG.rst
181
CHANGELOG.rst
@@ -8,6 +8,175 @@
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
Pytest 3.4.0 (2018-01-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- All pytest classes now subclass ``object`` for better Python 2/3 compatibility.
|
||||
This should not affect user code except in very rare edge cases. (`#2147
|
||||
<https://github.com/pytest-dev/pytest/issues/2147>`_)
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Introduce ``empty_parameter_set_mark`` ini option to select which mark to
|
||||
apply when ``@pytest.mark.parametrize`` is given an empty set of parameters.
|
||||
Valid options are ``skip`` (default) and ``xfail``. Note that it is planned
|
||||
to change the default to ``xfail`` in future releases as this is considered
|
||||
less error prone. (`#2527
|
||||
<https://github.com/pytest-dev/pytest/issues/2527>`_)
|
||||
|
||||
- **Incompatible change**: after community feedback the `logging
|
||||
<https://docs.pytest.org/en/latest/logging.html>`_ functionality has
|
||||
undergone some changes. Please consult the `logging documentation
|
||||
<https://docs.pytest.org/en/latest/logging.html#incompatible-changes-in-pytest-3-4>`_
|
||||
for details. (`#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_)
|
||||
|
||||
- Console output falls back to "classic" mode when capturing is disabled (``-s``),
|
||||
otherwise the output gets garbled to the point of being useless. (`#3038
|
||||
<https://github.com/pytest-dev/pytest/issues/3038>`_)
|
||||
|
||||
- New `pytest_runtest_logfinish
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_logfinish>`_
|
||||
hook which is called when a test item has finished executing, analogous to
|
||||
`pytest_runtest_logstart
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_start>`_.
|
||||
(`#3101 <https://github.com/pytest-dev/pytest/issues/3101>`_)
|
||||
|
||||
- Improve performance when collecting tests using many fixtures. (`#3107
|
||||
<https://github.com/pytest-dev/pytest/issues/3107>`_)
|
||||
|
||||
- New ``caplog.get_records(when)`` method which provides access to the captured
|
||||
records for the ``"setup"``, ``"call"`` and ``"teardown"``
|
||||
testing stages. (`#3117 <https://github.com/pytest-dev/pytest/issues/3117>`_)
|
||||
|
||||
- New fixture ``record_xml_attribute`` that allows modifying and inserting
|
||||
attributes on the ``<testcase>`` xml node in JUnit reports. (`#3130
|
||||
<https://github.com/pytest-dev/pytest/issues/3130>`_)
|
||||
|
||||
- The default cache directory has been renamed from ``.cache`` to
|
||||
``.pytest_cache`` after community feedback that the name ``.cache`` did not
|
||||
make it clear that it was used by pytest. (`#3138
|
||||
<https://github.com/pytest-dev/pytest/issues/3138>`_)
|
||||
|
||||
- Colorize the levelname column in the live-log output. (`#3142
|
||||
<https://github.com/pytest-dev/pytest/issues/3142>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
|
||||
(`#2022 <https://github.com/pytest-dev/pytest/issues/2022>`_)
|
||||
|
||||
- Fix restoring Python state after in-process pytest runs with the
|
||||
``pytester`` plugin; this may break tests using multiple inprocess
|
||||
pytest runs if later ones depend on earlier ones leaking global interpreter
|
||||
changes. (`#3016 <https://github.com/pytest-dev/pytest/issues/3016>`_)
|
||||
|
||||
- Fix skipping plugin reporting hook when test aborted before plugin setup
|
||||
hook. (`#3074 <https://github.com/pytest-dev/pytest/issues/3074>`_)
|
||||
|
||||
- Fix progress percentage reported when tests fail during teardown. (`#3088
|
||||
<https://github.com/pytest-dev/pytest/issues/3088>`_)
|
||||
|
||||
- **Incompatible change**: ``-o/--override`` option no longer eats all the
|
||||
remaining options, which can lead to surprising behavior: for example,
|
||||
``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py``
|
||||
would be considered as part of the ``-o`` command-line argument. One
|
||||
consequence of this is that now multiple configuration overrides need
|
||||
multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (`#3103
|
||||
<https://github.com/pytest-dev/pytest/issues/3103>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Document hooks (defined with ``historic=True``) which cannot be used with
|
||||
``hookwrapper=True``. (`#2423
|
||||
<https://github.com/pytest-dev/pytest/issues/2423>`_)
|
||||
|
||||
- Clarify that warning capturing doesn't change the warning filter by default.
|
||||
(`#2457 <https://github.com/pytest-dev/pytest/issues/2457>`_)
|
||||
|
||||
- Clarify a possible confusion when using pytest_fixture_setup with fixture
|
||||
functions that return None. (`#2698
|
||||
<https://github.com/pytest-dev/pytest/issues/2698>`_)
|
||||
|
||||
- Fix the wording of a sentence on doctest flags used in pytest. (`#3076
|
||||
<https://github.com/pytest-dev/pytest/issues/3076>`_)
|
||||
|
||||
- Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in
|
||||
the documentation. (`#3092
|
||||
<https://github.com/pytest-dev/pytest/issues/3092>`_)
|
||||
|
||||
- Improve readability (wording, grammar) of Getting Started guide (`#3131
|
||||
<https://github.com/pytest-dev/pytest/issues/3131>`_)
|
||||
|
||||
- Added note that calling pytest.main multiple times from the same process is
|
||||
not recommended because of import caching. (`#3143
|
||||
<https://github.com/pytest-dev/pytest/issues/3143>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Show a simple and easy error when keyword expressions trigger a syntax error
|
||||
(for example, ``"-k foo and import"`` will show an error that you can not use
|
||||
the ``import`` keyword in expressions). (`#2953
|
||||
<https://github.com/pytest-dev/pytest/issues/2953>`_)
|
||||
|
||||
- Change parametrized automatic test id generation to use the ``__name__``
|
||||
attribute of functions instead of the fallback argument name plus counter.
|
||||
(`#2976 <https://github.com/pytest-dev/pytest/issues/2976>`_)
|
||||
|
||||
- Replace py.std with stdlib imports. (`#3067
|
||||
<https://github.com/pytest-dev/pytest/issues/3067>`_)
|
||||
|
||||
- Corrected 'you' to 'your' in logging docs. (`#3129
|
||||
<https://github.com/pytest-dev/pytest/issues/3129>`_)
|
||||
|
||||
|
||||
Pytest 3.3.2 (2017-12-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- pytester: ignore files used to obtain current user metadata in the fd leak
|
||||
detector. (`#2784 <https://github.com/pytest-dev/pytest/issues/2784>`_)
|
||||
|
||||
- Fix **memory leak** where objects returned by fixtures were never destructed
|
||||
by the garbage collector. (`#2981
|
||||
<https://github.com/pytest-dev/pytest/issues/2981>`_)
|
||||
|
||||
- Fix conversion of pyargs to filename to not convert symlinks on Python 2. (`#2985
|
||||
<https://github.com/pytest-dev/pytest/issues/2985>`_)
|
||||
|
||||
- ``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for
|
||||
test modules. (`#2995 <https://github.com/pytest-dev/pytest/issues/2995>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add clarifying note about behavior of multiple parametrized arguments (`#3001
|
||||
<https://github.com/pytest-dev/pytest/issues/3001>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Code cleanup. (`#3015 <https://github.com/pytest-dev/pytest/issues/3015>`_,
|
||||
`#3021 <https://github.com/pytest-dev/pytest/issues/3021>`_)
|
||||
|
||||
- Clean up code by replacing imports and references of `_ast` to `ast`. (`#3018
|
||||
<https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
|
||||
|
||||
Pytest 3.3.1 (2017-12-05)
|
||||
=========================
|
||||
|
||||
@@ -31,7 +200,7 @@ Bug Fixes
|
||||
``TerminalReporter._tw``. This alias was removed by accident in the ``3.3.0``
|
||||
release. (`#2984 <https://github.com/pytest-dev/pytest/issues/2984>`_)
|
||||
|
||||
- The pytest-capturelog plugin is now also blacklisted, avoiding errors when
|
||||
- The ``pytest-capturelog`` plugin is now also blacklisted, avoiding errors when
|
||||
running pytest with it still installed. (`#3004
|
||||
<https://github.com/pytest-dev/pytest/issues/3004>`_)
|
||||
|
||||
@@ -39,14 +208,14 @@ Bug Fixes
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Fix broken link to plugin pytest-localserver. (`#2963
|
||||
- Fix broken link to plugin ``pytest-localserver``. (`#2963
|
||||
<https://github.com/pytest-dev/pytest/issues/2963>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Update github "bugs" link in CONTRIBUTING.rst (`#2949
|
||||
- Update github "bugs" link in ``CONTRIBUTING.rst`` (`#2949
|
||||
<https://github.com/pytest-dev/pytest/issues/2949>`_)
|
||||
|
||||
|
||||
@@ -157,10 +326,10 @@ Bug Fixes
|
||||
<https://github.com/pytest-dev/pytest/issues/2124>`_)
|
||||
|
||||
- If an exception happens while loading a plugin, pytest no longer hides the
|
||||
original traceback. In python2 it will show the original traceback with a new
|
||||
message that explains in which plugin. In python3 it will show 2 canonized
|
||||
original traceback. In Python 2 it will show the original traceback with a new
|
||||
message that explains in which plugin. In Python 3 it will show 2 canonized
|
||||
exceptions, the original exception while loading the plugin in addition to an
|
||||
exception that PyTest throws about loading a plugin. (`#2491
|
||||
exception that pytest throws about loading a plugin. (`#2491
|
||||
<https://github.com/pytest-dev/pytest/issues/2491>`_)
|
||||
|
||||
- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709
|
||||
|
||||
@@ -276,3 +276,15 @@ Here is a simple overview, with pytest-specific bits:
|
||||
base: features # if it's a feature
|
||||
|
||||
|
||||
Joining the Development Team
|
||||
----------------------------
|
||||
|
||||
Anyone who has successfully seen through a pull request which did not
|
||||
require any extra work from the development team to merge will
|
||||
themselves gain commit access if they so wish (if we forget to ask please send a friendly
|
||||
reminder). This does not mean your workflow to contribute changes,
|
||||
everyone goes through the same pull-request-and-review process and
|
||||
no-one merges their own pull requests unless already approved. It does however mean you can
|
||||
participate in the development process more fully since you can merge
|
||||
pull requests from other contributors yourself after having reviewed
|
||||
them.
|
||||
|
||||
@@ -12,7 +12,7 @@ taking a lot of time to make a new one.
|
||||
|
||||
#. Install development dependencies in a virtual environment with::
|
||||
|
||||
pip3 install -r tasks/requirements.txt
|
||||
pip3 install -U -r tasks/requirements.txt
|
||||
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ import os
|
||||
from glob import glob
|
||||
|
||||
|
||||
class FastFilesCompleter:
|
||||
class FastFilesCompleter(object):
|
||||
'Fast file completer class'
|
||||
|
||||
def __init__(self, directories=True):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
import re
|
||||
from weakref import ref
|
||||
@@ -422,7 +424,7 @@ class ExceptionInfo(object):
|
||||
"""
|
||||
if style == 'native':
|
||||
return ReprExceptionInfo(ReprTracebackNative(
|
||||
py.std.traceback.format_exception(
|
||||
traceback.format_exception(
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry,
|
||||
@@ -556,7 +558,7 @@ class FormattedExcinfo(object):
|
||||
# else:
|
||||
# self._line("%-10s =\\" % (name,))
|
||||
# # XXX
|
||||
# py.std.pprint.pprint(value, stream=self.excinfowriter)
|
||||
# pprint.pprint(value, stream=self.excinfowriter)
|
||||
return ReprLocals(lines)
|
||||
|
||||
def repr_traceback_entry(self, entry, excinfo=None):
|
||||
@@ -669,7 +671,7 @@ class FormattedExcinfo(object):
|
||||
else:
|
||||
# fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work
|
||||
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
|
||||
reprtraceback = ReprTracebackNative(traceback.format_exception(type(e), e, None))
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
@@ -886,7 +888,7 @@ def getrawcode(obj, trycall=True):
|
||||
obj = getattr(obj, 'f_code', obj)
|
||||
obj = getattr(obj, '__code__', obj)
|
||||
if trycall and not hasattr(obj, 'co_firstlineno'):
|
||||
if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj):
|
||||
if hasattr(obj, '__call__') and not inspect.isclass(obj):
|
||||
x = getrawcode(obj.__call__, trycall=False)
|
||||
if hasattr(x, 'co_firstlineno'):
|
||||
return x
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
from __future__ import absolute_import, division, generators, print_function
|
||||
|
||||
import ast
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
from bisect import bisect_right
|
||||
import linecache
|
||||
import sys
|
||||
import six
|
||||
import inspect
|
||||
import tokenize
|
||||
import py
|
||||
cpy_compile = compile
|
||||
|
||||
try:
|
||||
import _ast
|
||||
from _ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
except ImportError:
|
||||
_AST_FLAG = 0
|
||||
_ast = None
|
||||
cpy_compile = compile
|
||||
|
||||
|
||||
class Source(object):
|
||||
@@ -195,7 +192,7 @@ class Source(object):
|
||||
if flag & _AST_FLAG:
|
||||
return co
|
||||
lines = [(x + "\n") for x in self.lines]
|
||||
py.std.linecache.cache[filename] = (1, None, lines, filename)
|
||||
linecache.cache[filename] = (1, None, lines, filename)
|
||||
return co
|
||||
|
||||
#
|
||||
@@ -209,7 +206,7 @@ def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag,
|
||||
retrieval of the source code for the code object
|
||||
and any recursively created code objects.
|
||||
"""
|
||||
if _ast is not None and isinstance(source, _ast.AST):
|
||||
if isinstance(source, ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return cpy_compile(source, filename, mode, flags, dont_inherit)
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
@@ -227,8 +224,7 @@ def getfslineno(obj):
|
||||
code = _pytest._code.Code(obj)
|
||||
except TypeError:
|
||||
try:
|
||||
fn = (py.std.inspect.getsourcefile(obj) or
|
||||
py.std.inspect.getfile(obj))
|
||||
fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
|
||||
except TypeError:
|
||||
return "", -1
|
||||
|
||||
@@ -252,7 +248,7 @@ def getfslineno(obj):
|
||||
|
||||
def findsource(obj):
|
||||
try:
|
||||
sourcelines, lineno = py.std.inspect.findsource(obj)
|
||||
sourcelines, lineno = inspect.findsource(obj)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except: # noqa
|
||||
@@ -322,7 +318,7 @@ def get_statement_startend2(lineno, node):
|
||||
# AST's line numbers start indexing at 1
|
||||
values = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
|
||||
if isinstance(x, ast.stmt) or isinstance(x, ast.ExceptHandler):
|
||||
values.append(x.lineno - 1)
|
||||
for name in "finalbody", "orelse":
|
||||
val = getattr(x, name, None)
|
||||
|
||||
@@ -56,7 +56,7 @@ class DummyRewriteHook(object):
|
||||
pass
|
||||
|
||||
|
||||
class AssertionState:
|
||||
class AssertionState(object):
|
||||
"""State for the assertion plugin."""
|
||||
|
||||
def __init__(self, config, mode):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import ast
|
||||
import _ast
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
@@ -180,11 +179,13 @@ class AssertionRewritingHook(object):
|
||||
The named module or package as well as any nested modules will
|
||||
be rewritten on import.
|
||||
"""
|
||||
already_imported = set(names).intersection(set(sys.modules))
|
||||
if already_imported:
|
||||
for name in already_imported:
|
||||
if name not in self._rewritten_names:
|
||||
self._warn_already_imported(name)
|
||||
already_imported = (set(names)
|
||||
.intersection(sys.modules)
|
||||
.difference(self._rewritten_names))
|
||||
for name in already_imported:
|
||||
if not AssertionRewriter.is_rewrite_disabled(
|
||||
sys.modules[name].__doc__ or ""):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
@@ -636,7 +637,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def is_rewrite_disabled(self, docstring):
|
||||
@staticmethod
|
||||
def is_rewrite_disabled(docstring):
|
||||
return "PYTEST_DONT_REWRITE" in docstring
|
||||
|
||||
def variable(self):
|
||||
@@ -914,7 +916,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = "({0})".format(left_expl)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
@@ -925,7 +927,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = "({0})".format(next_expl)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
|
||||
@@ -5,11 +5,7 @@ import pprint
|
||||
import _pytest._code
|
||||
import py
|
||||
import six
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
|
||||
from collections import Sequence
|
||||
|
||||
u = six.text_type
|
||||
|
||||
@@ -113,7 +109,7 @@ def assertrepr_compare(config, op, left, right):
|
||||
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
|
||||
|
||||
def issequence(x):
|
||||
return (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring))
|
||||
return isinstance(x, Sequence) and not isinstance(x, basestring)
|
||||
|
||||
def istext(x):
|
||||
return isinstance(x, basestring)
|
||||
|
||||
@@ -17,7 +17,7 @@ class Cache(object):
|
||||
self.config = config
|
||||
self._cachedir = Cache.cache_dir_from_config(config)
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getvalue("cacheclear"):
|
||||
if config.getoption("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
@@ -98,13 +98,13 @@ class Cache(object):
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
class LFPlugin:
|
||||
class LFPlugin(object):
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
active_keys = 'lf', 'failedfirst'
|
||||
self.active = any(config.getvalue(key) for key in active_keys)
|
||||
self.active = any(config.getoption(key) for key in active_keys)
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
self._previously_failed_count = None
|
||||
|
||||
@@ -114,7 +114,8 @@ class LFPlugin:
|
||||
mode = "run all (no recorded failures)"
|
||||
else:
|
||||
noun = 'failure' if self._previously_failed_count == 1 else 'failures'
|
||||
suffix = " first" if self.config.getvalue("failedfirst") else ""
|
||||
suffix = " first" if self.config.getoption(
|
||||
"failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
@@ -151,7 +152,7 @@ class LFPlugin:
|
||||
# running a subset of all tests with recorded failures outside
|
||||
# of the set of tests currently executing
|
||||
return
|
||||
if self.config.getvalue("lf"):
|
||||
if self.config.getoption("lf"):
|
||||
items[:] = previously_failed
|
||||
config.hook.pytest_deselected(items=previously_passed)
|
||||
else:
|
||||
@@ -159,7 +160,7 @@ class LFPlugin:
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
config = self.config
|
||||
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
|
||||
if config.getoption("cacheshow") or hasattr(config, "slaveinput"):
|
||||
return
|
||||
|
||||
saved_lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
@@ -185,7 +186,7 @@ def pytest_addoption(parser):
|
||||
'--cache-clear', action='store_true', dest="cacheclear",
|
||||
help="remove all cache contents at start of test run.")
|
||||
parser.addini(
|
||||
"cache_dir", default='.cache',
|
||||
"cache_dir", default='.pytest_cache',
|
||||
help="cache directory path.")
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
class CaptureManager:
|
||||
class CaptureManager(object):
|
||||
"""
|
||||
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
|
||||
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
|
||||
@@ -271,7 +271,7 @@ def _install_capture_fixture_on_item(request, capture_class):
|
||||
del request.node._capture_fixture
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
class CaptureFixture(object):
|
||||
def __init__(self, captureclass, request):
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
@@ -416,11 +416,11 @@ class MultiCapture(object):
|
||||
self.err.snap() if self.err is not None else "")
|
||||
|
||||
|
||||
class NoCapture:
|
||||
class NoCapture(object):
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
|
||||
class FDCaptureBinary:
|
||||
class FDCaptureBinary(object):
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces `bytes`
|
||||
@@ -506,7 +506,7 @@ class FDCapture(FDCaptureBinary):
|
||||
return res
|
||||
|
||||
|
||||
class SysCapture:
|
||||
class SysCapture(object):
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
@@ -551,7 +551,7 @@ class SysCaptureBinary(SysCapture):
|
||||
return res
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
class DontReadFromInput(object):
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
so far sent to the screen. This should be configurable, though,
|
||||
|
||||
@@ -60,12 +60,13 @@ def main(args=None, plugins=None):
|
||||
finally:
|
||||
config._ensure_unconfigure()
|
||||
except UsageError as e:
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
sys.stderr.write("ERROR: %s\n" % (msg,))
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return 4
|
||||
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
class cmdline(object): # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
|
||||
@@ -462,7 +463,7 @@ def _get_plugin_specs_as_list(specs):
|
||||
return []
|
||||
|
||||
|
||||
class Parser:
|
||||
class Parser(object):
|
||||
""" Parser for command line arguments and ini-file values.
|
||||
|
||||
:ivar extra_info: dict of generic param -> value to display in case
|
||||
@@ -597,7 +598,7 @@ class ArgumentError(Exception):
|
||||
return self.msg
|
||||
|
||||
|
||||
class Argument:
|
||||
class Argument(object):
|
||||
"""class that mimics the necessary behaviour of optparse.Option
|
||||
|
||||
its currently a least effort implementation
|
||||
@@ -727,7 +728,7 @@ class Argument:
|
||||
return 'Argument({0})'.format(', '.join(args))
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
class OptionGroup(object):
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -858,7 +859,7 @@ class CmdOptions(object):
|
||||
return CmdOptions(self.__dict__)
|
||||
|
||||
|
||||
class Notset:
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
@@ -1187,16 +1188,15 @@ class Config(object):
|
||||
|
||||
def _get_override_ini_value(self, name):
|
||||
value = None
|
||||
# override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and
|
||||
# and -o foo1=bar1 -o foo2=bar2 options
|
||||
# always use the last item if multiple value set for same ini-name,
|
||||
# override_ini is a list of "ini=value" options
|
||||
# always use the last item if multiple values are set for same ini-name,
|
||||
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
|
||||
for ini_config_list in self._override_ini:
|
||||
for ini_config in ini_config_list:
|
||||
try:
|
||||
(key, user_ini_value) = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
for ini_config in self._override_ini:
|
||||
try:
|
||||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
else:
|
||||
if key == name:
|
||||
value = user_ini_value
|
||||
return value
|
||||
|
||||
@@ -40,7 +40,7 @@ def pytest_configure(config):
|
||||
config._cleanup.append(fin)
|
||||
|
||||
|
||||
class pytestPDB:
|
||||
class pytestPDB(object):
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
@@ -62,7 +62,7 @@ class pytestPDB:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke:
|
||||
class PdbInvoke(object):
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
|
||||
@@ -4,7 +4,7 @@ import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
|
||||
import attr
|
||||
import py
|
||||
@@ -26,11 +26,12 @@ from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
import _pytest.python
|
||||
import _pytest.nodes
|
||||
|
||||
scopename2class.update({
|
||||
'class': _pytest.python.Class,
|
||||
'module': _pytest.python.Module,
|
||||
'function': _pytest.main.Item,
|
||||
'function': _pytest.nodes.Item,
|
||||
'session': _pytest.main.Session,
|
||||
})
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
@@ -162,62 +163,51 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
|
||||
def reorder_items(items):
|
||||
argkeys_cache = {}
|
||||
items_by_argkey = {}
|
||||
for scopenum in range(0, scopenum_function):
|
||||
argkeys_cache[scopenum] = d = {}
|
||||
items_by_argkey[scopenum] = item_d = defaultdict(list)
|
||||
for item in items:
|
||||
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
|
||||
if keys:
|
||||
d[item] = keys
|
||||
return reorder_items_atscope(items, set(), argkeys_cache, 0)
|
||||
for key in keys:
|
||||
item_d[key].append(item)
|
||||
items = OrderedDict.fromkeys(items)
|
||||
return list(reorder_items_atscope(items, set(), argkeys_cache, items_by_argkey, 0))
|
||||
|
||||
|
||||
def reorder_items_atscope(items, ignore, argkeys_cache, scopenum):
|
||||
def reorder_items_atscope(items, ignore, argkeys_cache, items_by_argkey, scopenum):
|
||||
if scopenum >= scopenum_function or len(items) < 3:
|
||||
return items
|
||||
items_done = []
|
||||
while 1:
|
||||
items_before, items_same, items_other, newignore = \
|
||||
slice_items(items, ignore, argkeys_cache[scopenum])
|
||||
items_before = reorder_items_atscope(
|
||||
items_before, ignore, argkeys_cache, scopenum + 1)
|
||||
if items_same is None:
|
||||
# nothing to reorder in this scope
|
||||
assert items_other is None
|
||||
return items_done + items_before
|
||||
items_done.extend(items_before)
|
||||
items = items_same + items_other
|
||||
ignore = newignore
|
||||
|
||||
|
||||
def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
# we pick the first item which uses a fixture instance in the
|
||||
# requested scope and which we haven't seen yet. We slice the input
|
||||
# items list into a list of items_nomatch, items_same and
|
||||
# items_other
|
||||
if scoped_argkeys_cache: # do we need to do work at all?
|
||||
it = iter(items)
|
||||
# first find a slicing key
|
||||
for i, item in enumerate(it):
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys is not None:
|
||||
newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
|
||||
if newargkeys: # found a slicing key
|
||||
slicing_argkey, _ = newargkeys.popitem()
|
||||
items_before = items[:i]
|
||||
items_same = [item]
|
||||
items_other = []
|
||||
# now slice the remainder of the list
|
||||
for item in it:
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys and slicing_argkey in argkeys and \
|
||||
slicing_argkey not in ignore:
|
||||
items_same.append(item)
|
||||
else:
|
||||
items_other.append(item)
|
||||
newignore = ignore.copy()
|
||||
newignore.add(slicing_argkey)
|
||||
return (items_before, items_same, items_other, newignore)
|
||||
return items, None, None, None
|
||||
items_deque = deque(items)
|
||||
items_done = OrderedDict()
|
||||
scoped_items_by_argkey = items_by_argkey[scopenum]
|
||||
scoped_argkeys_cache = argkeys_cache[scopenum]
|
||||
while items_deque:
|
||||
no_argkey_group = OrderedDict()
|
||||
slicing_argkey = None
|
||||
while items_deque:
|
||||
item = items_deque.popleft()
|
||||
if item in items_done or item in no_argkey_group:
|
||||
continue
|
||||
argkeys = OrderedDict.fromkeys(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore)
|
||||
if not argkeys:
|
||||
no_argkey_group[item] = None
|
||||
else:
|
||||
slicing_argkey, _ = argkeys.popitem()
|
||||
# we don't have to remove relevant items from later in the deque because they'll just be ignored
|
||||
for i in reversed(scoped_items_by_argkey[slicing_argkey]):
|
||||
if i in items:
|
||||
items_deque.appendleft(i)
|
||||
break
|
||||
if no_argkey_group:
|
||||
no_argkey_group = reorder_items_atscope(
|
||||
no_argkey_group, set(), argkeys_cache, items_by_argkey, scopenum + 1)
|
||||
for item in no_argkey_group:
|
||||
items_done[item] = None
|
||||
ignore.add(slicing_argkey)
|
||||
return items_done
|
||||
|
||||
|
||||
def fillfixtures(function):
|
||||
@@ -246,7 +236,7 @@ def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class FuncFixtureInfo:
|
||||
class FuncFixtureInfo(object):
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
self.names_closure = names_closure
|
||||
@@ -267,7 +257,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
self.fixturename = None
|
||||
#: Scope string, one of "function", "class", "module", "session"
|
||||
self.scope = "function"
|
||||
self._fixture_values = {} # argname -> fixture value
|
||||
self._fixture_defs = {} # argname -> FixtureDef
|
||||
fixtureinfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
@@ -443,15 +432,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
fixturedef = self._getnextfixturedef(argname)
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
class PseudoFixtureDef:
|
||||
class PseudoFixtureDef(object):
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function"
|
||||
return PseudoFixtureDef
|
||||
raise
|
||||
# remove indent to prevent the python3 exception
|
||||
# from leaking into the call
|
||||
result = self._getfixturevalue(fixturedef)
|
||||
self._fixture_values[argname] = result
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
@@ -466,7 +454,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
values.append(fixturedef)
|
||||
current = current._parent_request
|
||||
|
||||
def _getfixturevalue(self, fixturedef):
|
||||
def _compute_fixture_value(self, fixturedef):
|
||||
"""
|
||||
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
||||
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
||||
will be stored into the FixtureDef object itself.
|
||||
|
||||
:param FixtureDef fixturedef:
|
||||
"""
|
||||
# prepare a subrequest object before calling fixture function
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
@@ -515,12 +510,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
exc_clear()
|
||||
try:
|
||||
# call the fixture function
|
||||
val = fixturedef.execute(request=subrequest)
|
||||
fixturedef.execute(request=subrequest)
|
||||
finally:
|
||||
# if fixture function failed it might have registered finalizers
|
||||
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
|
||||
subrequest.node)
|
||||
return val
|
||||
|
||||
def _check_scope(self, argname, invoking_scope, requested_scope):
|
||||
if argname == "request":
|
||||
@@ -573,7 +567,6 @@ class SubRequest(FixtureRequest):
|
||||
self.scope = scope
|
||||
self._fixturedef = fixturedef
|
||||
self._pyfuncitem = request._pyfuncitem
|
||||
self._fixture_values = request._fixture_values
|
||||
self._fixture_defs = request._fixture_defs
|
||||
self._arg2fixturedefs = request._arg2fixturedefs
|
||||
self._arg2index = request._arg2index
|
||||
@@ -715,7 +708,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
return res
|
||||
|
||||
|
||||
class FixtureDef:
|
||||
class FixtureDef(object):
|
||||
""" A container for a factory definition. """
|
||||
|
||||
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
|
||||
@@ -921,7 +914,7 @@ def pytestconfig(request):
|
||||
return request.config
|
||||
|
||||
|
||||
class FixtureManager:
|
||||
class FixtureManager(object):
|
||||
"""
|
||||
pytest fixtures definitions and information is stored and managed
|
||||
from this class.
|
||||
|
||||
@@ -57,9 +57,9 @@ def pytest_addoption(parser):
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
group._addoption(
|
||||
'-o', '--override-ini', nargs='*', dest="override_ini",
|
||||
'-o', '--override-ini', dest="override_ini",
|
||||
action="append",
|
||||
help="override config option with option=value style, e.g. `-o xfail_strict=True`.")
|
||||
help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.')
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
|
||||
@@ -12,22 +12,40 @@ hookspec = HookspecMarker("pytest")
|
||||
@hookspec(historic=True)
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""called at plugin registration time to allow adding new hooks via a call to
|
||||
pluginmanager.add_hookspecs(module_or_class, prefix)."""
|
||||
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
||||
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_namespace():
|
||||
"""
|
||||
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
(**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
return dict of name->object to be made globally available in
|
||||
the pytest namespace. This hook is called at plugin registration
|
||||
time.
|
||||
the pytest namespace.
|
||||
|
||||
This hook is called at plugin registration time.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new pytest plugin got registered. """
|
||||
""" a new pytest plugin got registered.
|
||||
|
||||
:param plugin: the plugin module or instance
|
||||
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
@@ -41,7 +59,7 @@ def pytest_addoption(parser):
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:arg _pytest.config.Parser parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<_pytest.config.Parser.addini>`.
|
||||
@@ -56,8 +74,10 @@ def pytest_addoption(parser):
|
||||
a value read from an ini-style file.
|
||||
|
||||
The config object is passed around on many internal objects via the ``.config``
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
|
||||
via (deprecated) ``pytest.config``.
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@@ -72,14 +92,15 @@ def pytest_configure(config):
|
||||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
|
||||
:arg _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Bootstrapping hooks called for plugins registered early enough:
|
||||
# internal and 3rd party plugins as well as directly
|
||||
# discoverable conftest.py local plugins.
|
||||
# internal and 3rd party plugins.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -87,11 +108,28 @@ def pytest_configure(config):
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
"""(**Deprecated**) modify command line arguments before option parsing.
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version. Consider
|
||||
using :func:`pytest_load_initial_conftests` instead.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -99,12 +137,26 @@ def pytest_cmdline_main(config):
|
||||
""" called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
of command line option parsing.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config early_config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
:param _pytest.config.Parser parser: to add command line options
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -113,18 +165,29 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collection(session):
|
||||
""" perform the collection protocol for the given session.
|
||||
"""Perform the collection protocol for the given session.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
""" called after collection has been performed, may filter or re-order
|
||||
the items in-place."""
|
||||
the items in-place.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param List[_pytest.nodes.Item] items: list of item objects
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
""" called after collection has been performed and modified.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -134,6 +197,9 @@ def pytest_ignore_collect(path, config):
|
||||
more specific hooks.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
@@ -141,12 +207,18 @@ def pytest_ignore_collect(path, config):
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" called before traversing a directory for collection files.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
""" return collection Node or None for the given path. Any new node
|
||||
needs to have the specified ``parent`` as a parent."""
|
||||
needs to have the specified ``parent`` as a parent.
|
||||
|
||||
:param str path: the path to collect
|
||||
"""
|
||||
|
||||
# logging hooks for collection
|
||||
|
||||
@@ -212,7 +284,12 @@ def pytest_make_parametrize_id(config, val, argname):
|
||||
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
|
||||
The parameter name is available as ``argname``, if required.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param val: the parametrized value
|
||||
:param str argname: the automatic parameter name produced by pytest
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
@@ -224,11 +301,14 @@ def pytest_runtestloop(session):
|
||||
""" called for performing the main runtest loop
|
||||
(after collection finished).
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
@@ -250,7 +330,25 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location):
|
||||
""" signal the start of running a single test item. """
|
||||
""" signal the start of running a single test item.
|
||||
|
||||
This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_logfinish(nodeid, location):
|
||||
""" signal the complete finish of running a single test item.
|
||||
|
||||
This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
@@ -293,7 +391,15 @@ def pytest_runtest_logreport(report):
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" performs fixture setup execution.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
:return: The return value of the call to the fixture function
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef, request):
|
||||
@@ -307,15 +413,25 @@ def pytest_fixture_post_finalizer(fixturedef, request):
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
""" before session.main() is called.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
""" whole test run finishes.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param int exitstatus: the status which pytest will return to the system
|
||||
"""
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
""" called before test process is exited.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -329,6 +445,8 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented slightly, the intention is for the first line to be a summary.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -339,7 +457,7 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
def pytest_report_header(config, startdir):
|
||||
""" return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
|
||||
:param config: the pytest config object.
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
|
||||
.. note::
|
||||
@@ -358,7 +476,7 @@ def pytest_report_collectionfinish(config, startdir, items):
|
||||
|
||||
This strings will be displayed after the standard "collected X items" message.
|
||||
|
||||
:param config: the pytest config object.
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
:param items: list of pytest items that are going to be executed; this list should not be modified.
|
||||
"""
|
||||
@@ -379,7 +497,11 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a partilar node/location)."""
|
||||
if the warning is not tied to a particular node/location).
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
@@ -418,6 +540,5 @@ def pytest_enter_pdb(config):
|
||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
@@ -85,6 +85,9 @@ class _NodeReporter(object):
|
||||
def add_property(self, name, value):
|
||||
self.properties.append((str(name), bin_xml_escape(value)))
|
||||
|
||||
def add_attribute(self, name, value):
|
||||
self.attrs[str(name)] = bin_xml_escape(value)
|
||||
|
||||
def make_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
@@ -98,6 +101,7 @@ class _NodeReporter(object):
|
||||
def record_testreport(self, testreport):
|
||||
assert not self.testcase
|
||||
names = mangle_test_address(testreport.nodeid)
|
||||
existing_attrs = self.attrs
|
||||
classnames = names[:-1]
|
||||
if self.xml.prefix:
|
||||
classnames.insert(0, self.xml.prefix)
|
||||
@@ -111,6 +115,7 @@ class _NodeReporter(object):
|
||||
if hasattr(testreport, "url"):
|
||||
attrs["url"] = testreport.url
|
||||
self.attrs = attrs
|
||||
self.attrs.update(existing_attrs) # restore any user-defined attributes
|
||||
|
||||
def to_xml(self):
|
||||
testcase = Junit.testcase(time=self.duration, **self.attrs)
|
||||
@@ -211,6 +216,27 @@ def record_xml_property(request):
|
||||
return add_property_noop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_attribute(request):
|
||||
"""Add extra xml attributes to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded
|
||||
"""
|
||||
request.node.warn(
|
||||
code='C3',
|
||||
message='record_xml_attribute is an experimental feature',
|
||||
)
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
return node_reporter.add_attribute
|
||||
else:
|
||||
def add_attr_noop(name, value):
|
||||
pass
|
||||
|
||||
return add_attr_noop
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption(
|
||||
|
||||
@@ -2,9 +2,10 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
import sys
|
||||
import re
|
||||
import six
|
||||
|
||||
from _pytest.config import create_terminal_writer
|
||||
import pytest
|
||||
import py
|
||||
|
||||
@@ -13,6 +14,58 @@ DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
|
||||
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
|
||||
|
||||
|
||||
class ColoredLevelFormatter(logging.Formatter):
|
||||
"""
|
||||
Colorize the %(levelname)..s part of the log format passed to __init__.
|
||||
"""
|
||||
|
||||
LOGLEVEL_COLOROPTS = {
|
||||
logging.CRITICAL: {'red'},
|
||||
logging.ERROR: {'red', 'bold'},
|
||||
logging.WARNING: {'yellow'},
|
||||
logging.WARN: {'yellow'},
|
||||
logging.INFO: {'green'},
|
||||
logging.DEBUG: {'purple'},
|
||||
logging.NOTSET: set(),
|
||||
}
|
||||
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
|
||||
|
||||
def __init__(self, terminalwriter, *args, **kwargs):
|
||||
super(ColoredLevelFormatter, self).__init__(
|
||||
*args, **kwargs)
|
||||
if six.PY2:
|
||||
self._original_fmt = self._fmt
|
||||
else:
|
||||
self._original_fmt = self._style._fmt
|
||||
self._level_to_fmt_mapping = {}
|
||||
|
||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||
if not levelname_fmt_match:
|
||||
return
|
||||
levelname_fmt = levelname_fmt_match.group()
|
||||
|
||||
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
|
||||
formatted_levelname = levelname_fmt % {
|
||||
'levelname': logging.getLevelName(level)}
|
||||
|
||||
# add ANSI escape sequences around the formatted levelname
|
||||
color_kwargs = {name: True for name in color_opts}
|
||||
colorized_formatted_levelname = terminalwriter.markup(
|
||||
formatted_levelname, **color_kwargs)
|
||||
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
|
||||
colorized_formatted_levelname,
|
||||
self._fmt)
|
||||
|
||||
def format(self, record):
|
||||
fmt = self._level_to_fmt_mapping.get(
|
||||
record.levelno, self._original_fmt)
|
||||
if six.PY2:
|
||||
self._fmt = fmt
|
||||
else:
|
||||
self._style._fmt = fmt
|
||||
return super(ColoredLevelFormatter, self).format(record)
|
||||
|
||||
|
||||
def get_option_ini(config, *names):
|
||||
for name in names:
|
||||
ret = config.getoption(name) # 'default' arg won't work as expected
|
||||
@@ -48,6 +101,9 @@ def pytest_addoption(parser):
|
||||
'--log-date-format',
|
||||
dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help='log date format as used by the logging module.')
|
||||
parser.addini(
|
||||
'log_cli', default=False, type='bool',
|
||||
help='enable log display during test run (also known as "live logging").')
|
||||
add_option_ini(
|
||||
'--log-cli-level',
|
||||
dest='log_cli_level', default=None,
|
||||
@@ -79,39 +135,31 @@ def pytest_addoption(parser):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def logging_using_handler(handler, logger=None):
|
||||
"""Context manager that safely registers a given handler."""
|
||||
logger = logger or logging.getLogger(logger)
|
||||
|
||||
if handler in logger.handlers: # reentrancy
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
yield
|
||||
else:
|
||||
logger.addHandler(handler)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catching_logs(handler, formatter=None,
|
||||
level=logging.NOTSET, logger=None):
|
||||
def catching_logs(handler, formatter=None, level=None):
|
||||
"""Context manager that prepares the whole logging machinery properly."""
|
||||
logger = logger or logging.getLogger(logger)
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
if formatter is not None:
|
||||
handler.setFormatter(formatter)
|
||||
handler.setLevel(level)
|
||||
if level is not None:
|
||||
handler.setLevel(level)
|
||||
|
||||
with logging_using_handler(handler, logger):
|
||||
orig_level = logger.level
|
||||
logger.setLevel(min(orig_level, level))
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
logger.setLevel(orig_level)
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
add_new_handler = handler not in root_logger.handlers
|
||||
|
||||
if add_new_handler:
|
||||
root_logger.addHandler(handler)
|
||||
if level is not None:
|
||||
orig_level = root_logger.level
|
||||
root_logger.setLevel(level)
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
if level is not None:
|
||||
root_logger.setLevel(orig_level)
|
||||
if add_new_handler:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
@@ -134,11 +182,40 @@ class LogCaptureFixture(object):
|
||||
def __init__(self, item):
|
||||
"""Creates a new funcarg."""
|
||||
self._item = item
|
||||
self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
|
||||
|
||||
def _finalize(self):
|
||||
"""Finalizes the fixture.
|
||||
|
||||
This restores the log levels changed by :meth:`set_level`.
|
||||
"""
|
||||
# restore log levels
|
||||
for logger_name, level in self._initial_log_levels.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
return self._item.catch_log_handler
|
||||
|
||||
def get_records(self, when):
|
||||
"""
|
||||
Get the logging records for one of the possible test phases.
|
||||
|
||||
:param str when:
|
||||
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
|
||||
|
||||
:rtype: List[logging.LogRecord]
|
||||
:return: the list of captured records at the given stage
|
||||
|
||||
.. versionadded:: 3.4
|
||||
"""
|
||||
handler = self._item.catch_log_handlers.get(when)
|
||||
if handler:
|
||||
return handler.records
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Returns the log text."""
|
||||
@@ -165,31 +242,31 @@ class LogCaptureFixture(object):
|
||||
self.handler.records = []
|
||||
|
||||
def set_level(self, level, logger=None):
|
||||
"""Sets the level for capturing of logs.
|
||||
"""Sets the level for capturing of logs. The level will be restored to its previous value at the end of
|
||||
the test.
|
||||
|
||||
By default, the level is set on the handler used to capture
|
||||
logs. Specify a logger name to instead set the level of any
|
||||
logger.
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
The levels of the loggers changed by this function will be restored to their initial values at the
|
||||
end of the test.
|
||||
"""
|
||||
if logger is None:
|
||||
logger = self.handler
|
||||
else:
|
||||
logger = logging.getLogger(logger)
|
||||
logger_name = logger
|
||||
logger = logging.getLogger(logger_name)
|
||||
# save the original log-level to restore it during teardown
|
||||
self._initial_log_levels.setdefault(logger_name, logger.level)
|
||||
logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def at_level(self, level, logger=None):
|
||||
"""Context manager that sets the level for capturing of logs.
|
||||
"""Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
|
||||
level is restored to its original value.
|
||||
|
||||
By default, the level is set on the handler used to capture
|
||||
logs. Specify a logger name to instead set the level of any
|
||||
logger.
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
"""
|
||||
if logger is None:
|
||||
logger = self.handler
|
||||
else:
|
||||
logger = logging.getLogger(logger)
|
||||
|
||||
logger = logging.getLogger(logger)
|
||||
orig_level = logger.level
|
||||
logger.setLevel(level)
|
||||
try:
|
||||
@@ -208,7 +285,9 @@ def caplog(request):
|
||||
* caplog.records() -> list of logging.LogRecord instances
|
||||
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
|
||||
"""
|
||||
return LogCaptureFixture(request.node)
|
||||
result = LogCaptureFixture(request.node)
|
||||
yield result
|
||||
result._finalize()
|
||||
|
||||
|
||||
def get_actual_log_level(config, *setting_names):
|
||||
@@ -238,8 +317,12 @@ def get_actual_log_level(config, *setting_names):
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(LoggingPlugin(config),
|
||||
'logging-plugin')
|
||||
config.pluginmanager.register(LoggingPlugin(config), 'logging-plugin')
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _dummy_context_manager():
|
||||
yield
|
||||
|
||||
|
||||
class LoggingPlugin(object):
|
||||
@@ -252,57 +335,52 @@ class LoggingPlugin(object):
|
||||
The formatter can be safely shared across all handlers so
|
||||
create a single one for the entire test session here.
|
||||
"""
|
||||
self.log_cli_level = get_actual_log_level(
|
||||
config, 'log_cli_level', 'log_level') or logging.WARNING
|
||||
self._config = config
|
||||
|
||||
# enable verbose output automatically if live logging is enabled
|
||||
if self._config.getini('log_cli') 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')
|
||||
self.formatter = logging.Formatter(
|
||||
get_option_ini(config, 'log_format'),
|
||||
get_option_ini(config, 'log_date_format'))
|
||||
|
||||
log_cli_handler = logging.StreamHandler(sys.stderr)
|
||||
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 = logging.Formatter(
|
||||
log_cli_format,
|
||||
datefmt=log_cli_date_format)
|
||||
self.log_cli_handler = log_cli_handler # needed for a single unittest
|
||||
self.live_logs = catching_logs(log_cli_handler,
|
||||
formatter=log_cli_formatter,
|
||||
level=self.log_cli_level)
|
||||
self.formatter = logging.Formatter(get_option_ini(config, 'log_format'),
|
||||
get_option_ini(config, 'log_date_format'))
|
||||
self.log_level = get_actual_log_level(config, 'log_level')
|
||||
|
||||
log_file = get_option_ini(config, 'log_file')
|
||||
if log_file:
|
||||
self.log_file_level = get_actual_log_level(
|
||||
config, 'log_file_level') or logging.WARNING
|
||||
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')
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
log_file,
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
mode='w')
|
||||
log_file_formatter = logging.Formatter(
|
||||
log_file_format,
|
||||
datefmt=log_file_date_format)
|
||||
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')
|
||||
log_file_formatter = logging.Formatter(log_file_format, datefmt=log_file_date_format)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
# initialized during pytest_runtestloop
|
||||
self.log_cli_handler = None
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(LogCaptureHandler(),
|
||||
formatter=self.formatter) as log_handler:
|
||||
formatter=self.formatter, level=self.log_level) as log_handler:
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when(when)
|
||||
if not hasattr(item, 'catch_log_handlers'):
|
||||
item.catch_log_handlers = {}
|
||||
item.catch_log_handlers[when] = log_handler
|
||||
item.catch_log_handler = log_handler
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
if when == 'teardown':
|
||||
del item.catch_log_handlers
|
||||
|
||||
if self.print_logs:
|
||||
# Add a captured log section to the report.
|
||||
@@ -324,10 +402,15 @@ class LoggingPlugin(object):
|
||||
with self._runtest_for(item, 'teardown'):
|
||||
yield
|
||||
|
||||
def pytest_runtest_logstart(self):
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.reset()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
with self.live_logs:
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context:
|
||||
if self.log_file_handler is not None:
|
||||
with closing(self.log_file_handler):
|
||||
with catching_logs(self.log_file_handler,
|
||||
@@ -335,3 +418,69 @@ class LoggingPlugin(object):
|
||||
yield # run all the tests
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled.
|
||||
|
||||
This must be done right before starting the loop so we can access the terminal reporter plugin.
|
||||
"""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
|
||||
if self._config.getini('log_cli') 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 = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
|
||||
else:
|
||||
self.live_logs_context = _dummy_context_manager()
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
||||
in each test.
|
||||
|
||||
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
|
||||
and won't appear in the terminal.
|
||||
"""
|
||||
|
||||
def __init__(self, terminal_reporter, capture_manager):
|
||||
"""
|
||||
:param _pytest.terminal.TerminalReporter terminal_reporter:
|
||||
:param _pytest.capture.CaptureManager capture_manager:
|
||||
"""
|
||||
logging.StreamHandler.__init__(self, stream=terminal_reporter)
|
||||
self.capture_manager = capture_manager
|
||||
self.reset()
|
||||
self.set_when(None)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the handler; should be called before the start of each test"""
|
||||
self._first_record_emitted = False
|
||||
|
||||
def set_when(self, when):
|
||||
"""Prepares for the given test phase (setup/call/teardown)"""
|
||||
self._when = when
|
||||
self._section_name_shown = False
|
||||
|
||||
def emit(self, record):
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.suspend_global_capture()
|
||||
try:
|
||||
if not self._first_record_emitted or self._when == 'teardown':
|
||||
self.stream.write('\n')
|
||||
self._first_record_emitted = True
|
||||
if not self._section_name_shown:
|
||||
self.stream.section('live log ' + self._when, sep='-', bold=True)
|
||||
self._section_name_shown = True
|
||||
logging.StreamHandler.emit(self, record)
|
||||
finally:
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.resume_global_capture()
|
||||
|
||||
422
_pytest/main.py
422
_pytest/main.py
@@ -1,8 +1,10 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import six
|
||||
import sys
|
||||
|
||||
@@ -10,16 +12,11 @@ import _pytest
|
||||
from _pytest import nodes
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.config import directory_arg, UsageError, hookimpl
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
@@ -206,7 +203,47 @@ def pytest_ignore_collect(path, config):
|
||||
return False
|
||||
|
||||
|
||||
class FSHookProxy:
|
||||
@contextlib.contextmanager
|
||||
def _patched_find_module():
|
||||
"""Patch bug in pkgutil.ImpImporter.find_module
|
||||
|
||||
When using pkgutil.find_loader on python<3.4 it removes symlinks
|
||||
from the path due to a call to os.path.realpath. This is not consistent
|
||||
with actually doing the import (in these versions, pkgutil and __import__
|
||||
did not share the same underlying code). This can break conftest
|
||||
discovery for pytest where symlinks are involved.
|
||||
|
||||
The only supported python<3.4 by pytest is python 2.7.
|
||||
"""
|
||||
if six.PY2: # python 3.4+ uses importlib instead
|
||||
def find_module_patched(self, fullname, path=None):
|
||||
# Note: we ignore 'path' argument since it is only used via meta_path
|
||||
subname = fullname.split(".")[-1]
|
||||
if subname != fullname and self.path is None:
|
||||
return None
|
||||
if self.path is None:
|
||||
path = None
|
||||
else:
|
||||
# original: path = [os.path.realpath(self.path)]
|
||||
path = [self.path]
|
||||
try:
|
||||
file, filename, etc = pkgutil.imp.find_module(subname,
|
||||
path)
|
||||
except ImportError:
|
||||
return None
|
||||
return pkgutil.ImpLoader(fullname, file, filename, etc)
|
||||
|
||||
old_find_module = pkgutil.ImpImporter.find_module
|
||||
pkgutil.ImpImporter.find_module = find_module_patched
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pkgutil.ImpImporter.find_module = old_find_module
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class FSHookProxy(object):
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
self.pm = pm
|
||||
@@ -218,356 +255,6 @@ class FSHookProxy:
|
||||
return x
|
||||
|
||||
|
||||
class _CompatProperty(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, six.string_types):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, nodes.SEP)
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _check_initialpaths_for_relpath(self):
|
||||
for initialpath in self.session._initialpaths:
|
||||
if self.fspath.common(initialpath) == initialpath:
|
||||
return self.fspath.relto(initialpath.dirname)
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != nodes.SEP:
|
||||
relpath = relpath.replace(os.sep, nodes.SEP)
|
||||
return relpath
|
||||
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
"""
|
||||
Adds a new report section, similar to what's done internally to add stdout and
|
||||
stderr captured output::
|
||||
|
||||
item.add_report_section("call", "stdout", "report section contents")
|
||||
|
||||
:param str when:
|
||||
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
|
||||
:param str key:
|
||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||
``"stderr"`` internally.
|
||||
|
||||
:param str content:
|
||||
The full contents as a string.
|
||||
"""
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
|
||||
class NoMatch(Exception):
|
||||
""" raised if matching cannot locate a matching names. """
|
||||
|
||||
@@ -581,13 +268,14 @@ class Failed(Exception):
|
||||
""" signals an stop as failed test run. """
|
||||
|
||||
|
||||
class Session(FSCollector):
|
||||
class Session(nodes.FSCollector):
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
nodes.FSCollector.__init__(
|
||||
self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False
|
||||
@@ -728,9 +416,10 @@ class Session(FSCollector):
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
import pkgutil
|
||||
|
||||
try:
|
||||
loader = pkgutil.find_loader(x)
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
except ImportError:
|
||||
return x
|
||||
if loader is None:
|
||||
@@ -738,7 +427,8 @@ class Session(FSCollector):
|
||||
# This method is sometimes invoked when AssertionRewritingHook, which
|
||||
# does not define a get_filename method, is already in place:
|
||||
try:
|
||||
path = loader.get_filename(x)
|
||||
with _patched_find_module():
|
||||
path = loader.get_filename(x)
|
||||
except AttributeError:
|
||||
# Retrieve path from AssertionRewritingHook:
|
||||
path = loader.modules[x][0].co_filename
|
||||
@@ -782,11 +472,11 @@ class Session(FSCollector):
|
||||
nextnames = names[1:]
|
||||
resultnodes = []
|
||||
for node in matching:
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
if not names:
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
@@ -808,11 +498,11 @@ class Session(FSCollector):
|
||||
|
||||
def genitems(self, node):
|
||||
self.trace("genitems", node)
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
node.ihook.pytest_itemcollected(item=node)
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result:
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import inspect
|
||||
import keyword
|
||||
import warnings
|
||||
import attr
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from six.moves import map
|
||||
|
||||
from _pytest.config import UsageError
|
||||
from .deprecated import MARK_PARAMETERSET_UNPACKING
|
||||
from .compat import NOTSET, getfslineno
|
||||
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
|
||||
|
||||
def alias(name, warning=None):
|
||||
getter = attrgetter(name)
|
||||
@@ -70,7 +75,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@classmethod
|
||||
def _for_parameterize(cls, argnames, argvalues, function):
|
||||
def _for_parameterize(cls, argnames, argvalues, function, config):
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
@@ -82,10 +87,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
del argvalues
|
||||
|
||||
if not parameters:
|
||||
fs, lineno = getfslineno(function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, function.__name__, fs, lineno)
|
||||
mark = MARK_GEN.skip(reason=reason)
|
||||
mark = get_empty_parameterset_mark(config, argnames, function)
|
||||
parameters.append(ParameterSet(
|
||||
values=(NOTSET,) * len(argnames),
|
||||
marks=[mark],
|
||||
@@ -94,6 +96,20 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
return argnames, parameters
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, function):
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ('', None, 'skip'):
|
||||
mark = MARK_GEN.skip
|
||||
elif requested_mark == 'xfail':
|
||||
mark = MARK_GEN.xfail(run=False)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, function.__name__, fs, lineno)
|
||||
return mark(reason=reason)
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
@@ -133,6 +149,9 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
parser.addini(
|
||||
EMPTY_PARAMETERSET_OPTION,
|
||||
"default marker for empty parametersets")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -222,6 +241,9 @@ class KeywordMapping(object):
|
||||
return False
|
||||
|
||||
|
||||
python_keywords_allowed_list = ["or", "and", "not"]
|
||||
|
||||
|
||||
def matchmark(colitem, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords))
|
||||
@@ -259,7 +281,13 @@ def matchkeyword(colitem, keywordexpr):
|
||||
return mapping[keywordexpr]
|
||||
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
|
||||
return not mapping[keywordexpr[4:]]
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
for kwd in keywordexpr.split():
|
||||
if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
|
||||
raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd))
|
||||
try:
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
except SyntaxError:
|
||||
raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -267,12 +295,19 @@ def pytest_configure(config):
|
||||
if config.option.strict:
|
||||
MARK_GEN._config = config
|
||||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
||||
if empty_parameterset not in ('skip', 'xfail', None, ''):
|
||||
raise UsageError(
|
||||
"{!s} must be one of skip and xfail,"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset))
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
MARK_GEN._config = getattr(config, '_old_mark_config', None)
|
||||
|
||||
|
||||
class MarkGenerator:
|
||||
class MarkGenerator(object):
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def derive_importpath(import_path, raising):
|
||||
return attr, target
|
||||
|
||||
|
||||
class Notset:
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
@@ -96,7 +96,7 @@ class Notset:
|
||||
notset = Notset()
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
class MonkeyPatch(object):
|
||||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
||||
"""
|
||||
|
||||
|
||||
363
_pytest/nodes.py
363
_pytest/nodes.py
@@ -1,5 +1,18 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from collections import MutableMapping as MappingMixin
|
||||
import os
|
||||
|
||||
import six
|
||||
import py
|
||||
import attr
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
SEP = "/"
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
|
||||
def _splitnode(nodeid):
|
||||
"""Split a nodeid into constituent 'parts'.
|
||||
@@ -35,3 +48,353 @@ def ischildnode(baseid, nodeid):
|
||||
if len(node_parts) < len(base_parts):
|
||||
return False
|
||||
return node_parts[:len(base_parts)] == base_parts
|
||||
|
||||
|
||||
@attr.s
|
||||
class _CompatProperty(object):
|
||||
name = attr.ib()
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, six.string_types):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, SEP)
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _check_initialpaths_for_relpath(self):
|
||||
for initialpath in self.session._initialpaths:
|
||||
if self.fspath.common(initialpath) == initialpath:
|
||||
return self.fspath.relto(initialpath.dirname)
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != SEP:
|
||||
relpath = relpath.replace(os.sep, SEP)
|
||||
return relpath
|
||||
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
"""
|
||||
Adds a new report section, similar to what's done internally to add stdout and
|
||||
stderr captured output::
|
||||
|
||||
item.add_report_section("call", "stdout", "report section contents")
|
||||
|
||||
:param str when:
|
||||
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
|
||||
:param str key:
|
||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||
``"stderr"`` internally.
|
||||
|
||||
:param str content:
|
||||
The full contents as a string.
|
||||
"""
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" (disabled by default) support for testing pytest and pytest plugins. """
|
||||
"""(disabled by default) support for testing pytest and pytest plugins."""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import codecs
|
||||
@@ -26,14 +26,18 @@ from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
|
||||
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u'/var/lib/sss/mc/passwd'
|
||||
]
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
# group = parser.getgroup("pytester", "pytester (self-tests) options")
|
||||
parser.addoption('--lsof',
|
||||
action="store_true", dest="lsof", default=False,
|
||||
help=("run FD checks if lsof is available"))
|
||||
|
||||
parser.addoption('--runpytest', default="inprocess", dest="runpytest",
|
||||
choices=("inprocess", "subprocess", ),
|
||||
choices=("inprocess", "subprocess"),
|
||||
help=("run pytest sub runs in tests using an 'inprocess' "
|
||||
"or 'subprocess' (python -m main) method"))
|
||||
|
||||
@@ -67,6 +71,8 @@ class LsofFdLeakChecker(object):
|
||||
fields = line.split('\0')
|
||||
fd = fields[0][1:]
|
||||
filename = fields[1][1:]
|
||||
if filename in IGNORE_PAM:
|
||||
continue
|
||||
if filename.startswith('/'):
|
||||
open_files.append((fd, filename))
|
||||
|
||||
@@ -76,8 +82,8 @@ class LsofFdLeakChecker(object):
|
||||
try:
|
||||
py.process.cmdexec("lsof -v")
|
||||
except (py.process.cmdexec.Error, UnicodeDecodeError):
|
||||
# cmdexec may raise UnicodeDecodeError on Windows systems
|
||||
# with locale other than english:
|
||||
# cmdexec may raise UnicodeDecodeError on Windows systems with
|
||||
# locale other than English:
|
||||
# https://bitbucket.org/pytest-dev/py/issues/66
|
||||
return False
|
||||
else:
|
||||
@@ -132,7 +138,7 @@ def getexecutable(name, cache={}):
|
||||
if "2.5.2" in err:
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
elif popen.returncode != 0:
|
||||
# Handle pyenv's 127.
|
||||
# handle pyenv's 127
|
||||
executable = None
|
||||
cache[name] = executable
|
||||
return executable
|
||||
@@ -157,14 +163,15 @@ def anypython(request):
|
||||
|
||||
@pytest.fixture
|
||||
def _pytest(request):
|
||||
""" Return a helper which offers a gethookrecorder(hook)
|
||||
method which returns a HookRecorder instance which helps
|
||||
to make assertions about called hooks.
|
||||
"""Return a helper which offers a gethookrecorder(hook) method which
|
||||
returns a HookRecorder instance which helps to make assertions about called
|
||||
hooks.
|
||||
|
||||
"""
|
||||
return PytestArg(request)
|
||||
|
||||
|
||||
class PytestArg:
|
||||
class PytestArg(object):
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@@ -179,7 +186,7 @@ def get_public_names(values):
|
||||
return [x for x in values if x[0] != "_"]
|
||||
|
||||
|
||||
class ParsedCall:
|
||||
class ParsedCall(object):
|
||||
def __init__(self, name, kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
self._name = name
|
||||
@@ -190,11 +197,11 @@ class ParsedCall:
|
||||
return "<ParsedCall %r(**%r)>" % (self._name, d)
|
||||
|
||||
|
||||
class HookRecorder:
|
||||
class HookRecorder(object):
|
||||
"""Record all hooks called in a plugin manager.
|
||||
|
||||
This wraps all the hook calls in the plugin manager, recording
|
||||
each call before propagating the normal calls.
|
||||
This wraps all the hook calls in the plugin manager, recording each call
|
||||
before propagating the normal calls.
|
||||
|
||||
"""
|
||||
|
||||
@@ -262,7 +269,7 @@ class HookRecorder:
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
"""return a testreport whose dotted import path matches"""
|
||||
values = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
@@ -336,19 +343,19 @@ def testdir(request, tmpdir_factory):
|
||||
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
|
||||
|
||||
|
||||
class RunResult:
|
||||
class RunResult(object):
|
||||
"""The result of running a command.
|
||||
|
||||
Attributes:
|
||||
|
||||
:ret: The return value.
|
||||
:outlines: List of lines captured from stdout.
|
||||
:errlines: List of lines captures from stderr.
|
||||
:ret: the return value
|
||||
:outlines: list of lines captured from stdout
|
||||
:errlines: list of lines captures from stderr
|
||||
:stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
|
||||
reconstruct stdout or the commonly used
|
||||
``stdout.fnmatch_lines()`` method.
|
||||
:stderrr: :py:class:`LineMatcher` of stderr.
|
||||
:duration: Duration in seconds.
|
||||
reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
|
||||
method
|
||||
:stderr: :py:class:`LineMatcher` of stderr
|
||||
:duration: duration in seconds
|
||||
|
||||
"""
|
||||
|
||||
@@ -361,8 +368,10 @@ class RunResult:
|
||||
self.duration = duration
|
||||
|
||||
def parseoutcomes(self):
|
||||
""" Return a dictionary of outcomestring->num from parsing
|
||||
the terminal output that the test process produced."""
|
||||
"""Return a dictionary of outcomestring->num from parsing the terminal
|
||||
output that the test process produced.
|
||||
|
||||
"""
|
||||
for line in reversed(self.outlines):
|
||||
if 'seconds' in line:
|
||||
outcomes = rex_outcome.findall(line)
|
||||
@@ -374,8 +383,10 @@ class RunResult:
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
|
||||
""" assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run.
|
||||
|
||||
"""
|
||||
d = self.parseoutcomes()
|
||||
obtained = {
|
||||
'passed': d.get('passed', 0),
|
||||
@@ -386,24 +397,50 @@ class RunResult:
|
||||
assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
|
||||
|
||||
|
||||
class Testdir:
|
||||
class CwdSnapshot(object):
|
||||
def __init__(self):
|
||||
self.__saved = os.getcwd()
|
||||
|
||||
def restore(self):
|
||||
os.chdir(self.__saved)
|
||||
|
||||
|
||||
class SysModulesSnapshot(object):
|
||||
def __init__(self, preserve=None):
|
||||
self.__preserve = preserve
|
||||
self.__saved = dict(sys.modules)
|
||||
|
||||
def restore(self):
|
||||
if self.__preserve:
|
||||
self.__saved.update(
|
||||
(k, m) for k, m in sys.modules.items() if self.__preserve(k))
|
||||
sys.modules.clear()
|
||||
sys.modules.update(self.__saved)
|
||||
|
||||
|
||||
class SysPathsSnapshot(object):
|
||||
def __init__(self):
|
||||
self.__saved = list(sys.path), list(sys.meta_path)
|
||||
|
||||
def restore(self):
|
||||
sys.path[:], sys.meta_path[:] = self.__saved
|
||||
|
||||
|
||||
class Testdir(object):
|
||||
"""Temporary test directory with tools to test/run pytest itself.
|
||||
|
||||
This is based on the ``tmpdir`` fixture but provides a number of
|
||||
methods which aid with testing pytest itself. Unless
|
||||
:py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
|
||||
current working directory.
|
||||
This is based on the ``tmpdir`` fixture but provides a number of methods
|
||||
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
|
||||
methods will use :py:attr:`tmpdir` as their current working directory.
|
||||
|
||||
Attributes:
|
||||
|
||||
:tmpdir: The :py:class:`py.path.local` instance of the temporary
|
||||
directory.
|
||||
:tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
|
||||
|
||||
:plugins: A list of plugins to use with :py:meth:`parseconfig` and
|
||||
:py:meth:`runpytest`. Initially this is an empty list but
|
||||
plugins can be added to the list. The type of items to add to
|
||||
the list depend on the method which uses them so refer to them
|
||||
for details.
|
||||
:py:meth:`runpytest`. Initially this is an empty list but plugins can
|
||||
be added to the list. The type of items to add to the list depends on
|
||||
the method using them so refer to them for details.
|
||||
|
||||
"""
|
||||
|
||||
@@ -413,9 +450,10 @@ class Testdir:
|
||||
name = request.function.__name__
|
||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.plugins = []
|
||||
self._savesyspath = (list(sys.path), list(sys.meta_path))
|
||||
self._savemodulekeys = set(sys.modules)
|
||||
self.chdir() # always chdir
|
||||
self._cwd_snapshot = CwdSnapshot()
|
||||
self._sys_path_snapshot = SysPathsSnapshot()
|
||||
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
||||
self.chdir()
|
||||
self.request.addfinalizer(self.finalize)
|
||||
method = self.request.config.getoption("--runpytest")
|
||||
if method == "inprocess":
|
||||
@@ -429,29 +467,22 @@ class Testdir:
|
||||
def finalize(self):
|
||||
"""Clean up global state artifacts.
|
||||
|
||||
Some methods modify the global interpreter state and this
|
||||
tries to clean this up. It does not remove the temporary
|
||||
directory however so it can be looked at after the test run
|
||||
has finished.
|
||||
Some methods modify the global interpreter state and this tries to
|
||||
clean this up. It does not remove the temporary directory however so
|
||||
it can be looked at after the test run has finished.
|
||||
|
||||
"""
|
||||
sys.path[:], sys.meta_path[:] = self._savesyspath
|
||||
if hasattr(self, '_olddir'):
|
||||
self._olddir.chdir()
|
||||
self.delete_loaded_modules()
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
|
||||
def delete_loaded_modules(self):
|
||||
"""Delete modules that have been loaded during a test.
|
||||
|
||||
This allows the interpreter to catch module changes in case
|
||||
the module is re-imported.
|
||||
"""
|
||||
for name in set(sys.modules).difference(self._savemodulekeys):
|
||||
# some zope modules used by twisted-related tests keeps internal
|
||||
# state and can't be deleted; we had some trouble in the past
|
||||
# with zope.interface for example
|
||||
if not name.startswith("zope"):
|
||||
del sys.modules[name]
|
||||
def __take_sys_modules_snapshot(self):
|
||||
# some zope modules used by twisted-related tests keep internal state
|
||||
# and can't be deleted; we had some trouble in the past with
|
||||
# `zope.interface` for example
|
||||
def preserve_module(name):
|
||||
return name.startswith("zope")
|
||||
return SysModulesSnapshot(preserve=preserve_module)
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
|
||||
@@ -466,9 +497,7 @@ class Testdir:
|
||||
This is done automatically upon instantiation.
|
||||
|
||||
"""
|
||||
old = self.tmpdir.chdir()
|
||||
if not hasattr(self, '_olddir'):
|
||||
self._olddir = old
|
||||
self.tmpdir.chdir()
|
||||
|
||||
def _makefile(self, ext, args, kwargs, encoding='utf-8'):
|
||||
items = list(kwargs.items())
|
||||
@@ -495,17 +524,15 @@ class Testdir:
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
"""Create a new file in the testdir.
|
||||
|
||||
ext: The extension the file should use, including the dot.
|
||||
E.g. ".py".
|
||||
ext: The extension the file should use, including the dot, e.g. `.py`.
|
||||
|
||||
args: All args will be treated as strings and joined using
|
||||
newlines. The result will be written as contents to the
|
||||
file. The name of the file will be based on the test
|
||||
function requesting this fixture.
|
||||
args: All args will be treated as strings and joined using newlines.
|
||||
The result will be written as contents to the file. The name of the
|
||||
file will be based on the test function requesting this fixture.
|
||||
E.g. "testdir.makefile('.txt', 'line1', 'line2')"
|
||||
|
||||
kwargs: Each keyword is the name of a file, while the value of
|
||||
it will be written as contents of the file.
|
||||
kwargs: Each keyword is the name of a file, while the value of it will
|
||||
be written as contents of the file.
|
||||
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
|
||||
|
||||
"""
|
||||
@@ -535,14 +562,16 @@ class Testdir:
|
||||
def syspathinsert(self, path=None):
|
||||
"""Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
|
||||
|
||||
This is undone automatically after the test.
|
||||
This is undone automatically when this object dies at the end of each
|
||||
test.
|
||||
|
||||
"""
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
sys.path.insert(0, str(path))
|
||||
# a call to syspathinsert() usually means that the caller
|
||||
# wants to import some dynamically created files.
|
||||
# with python3 we thus invalidate import caches.
|
||||
# a call to syspathinsert() usually means that the caller wants to
|
||||
# import some dynamically created files, thus with python3 we
|
||||
# invalidate its import caches
|
||||
self._possibly_invalidate_import_caches()
|
||||
|
||||
def _possibly_invalidate_import_caches(self):
|
||||
@@ -562,8 +591,8 @@ class Testdir:
|
||||
def mkpydir(self, name):
|
||||
"""Create a new python package.
|
||||
|
||||
This creates a (sub)directory with an empty ``__init__.py``
|
||||
file so that is recognised as a python package.
|
||||
This creates a (sub)directory with an empty ``__init__.py`` file so it
|
||||
gets recognised as a python package.
|
||||
|
||||
"""
|
||||
p = self.mkdir(name)
|
||||
@@ -576,10 +605,10 @@ class Testdir:
|
||||
"""Return the collection node of a file.
|
||||
|
||||
:param config: :py:class:`_pytest.config.Config` instance, see
|
||||
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to
|
||||
create the configuration.
|
||||
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
|
||||
configuration
|
||||
|
||||
:param arg: A :py:class:`py.path.local` instance of the file.
|
||||
:param arg: a :py:class:`py.path.local` instance of the file
|
||||
|
||||
"""
|
||||
session = Session(config)
|
||||
@@ -593,11 +622,10 @@ class Testdir:
|
||||
def getpathnode(self, path):
|
||||
"""Return the collection node of a file.
|
||||
|
||||
This is like :py:meth:`getnode` but uses
|
||||
:py:meth:`parseconfigure` to create the (configured) pytest
|
||||
Config instance.
|
||||
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
|
||||
create the (configured) pytest Config instance.
|
||||
|
||||
:param path: A :py:class:`py.path.local` instance of the file.
|
||||
:param path: a :py:class:`py.path.local` instance of the file
|
||||
|
||||
"""
|
||||
config = self.parseconfigure(path)
|
||||
@@ -611,8 +639,8 @@ class Testdir:
|
||||
def genitems(self, colitems):
|
||||
"""Generate all test items from a collection node.
|
||||
|
||||
This recurses into the collection node and returns a list of
|
||||
all the test items contained within.
|
||||
This recurses into the collection node and returns a list of all the
|
||||
test items contained within.
|
||||
|
||||
"""
|
||||
session = colitems[0].session
|
||||
@@ -624,10 +652,10 @@ class Testdir:
|
||||
def runitem(self, source):
|
||||
"""Run the "test_func" Item.
|
||||
|
||||
The calling test instance (the class which contains the test
|
||||
method) must provide a ``.getrunner()`` method which should
|
||||
return a runner which can run the test protocol for a single
|
||||
item, like e.g. :py:func:`_pytest.runner.runtestprotocol`.
|
||||
The calling test instance (class containing the test method) must
|
||||
provide a ``.getrunner()`` method which should return a runner which
|
||||
can run the test protocol for a single item, e.g.
|
||||
:py:func:`_pytest.runner.runtestprotocol`.
|
||||
|
||||
"""
|
||||
# used from runner functional tests
|
||||
@@ -641,14 +669,14 @@ class Testdir:
|
||||
"""Run a test module in process using ``pytest.main()``.
|
||||
|
||||
This run writes "source" into a temporary file and runs
|
||||
``pytest.main()`` on it, returning a :py:class:`HookRecorder`
|
||||
instance for the result.
|
||||
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
|
||||
for the result.
|
||||
|
||||
:param source: The source code of the test module.
|
||||
:param source: the source code of the test module
|
||||
|
||||
:param cmdlineargs: Any extra command line arguments to use.
|
||||
:param cmdlineargs: any extra command line arguments to use
|
||||
|
||||
:return: :py:class:`HookRecorder` instance of the result.
|
||||
:return: :py:class:`HookRecorder` instance of the result
|
||||
|
||||
"""
|
||||
p = self.makepyfile(source)
|
||||
@@ -658,13 +686,9 @@ class Testdir:
|
||||
def inline_genitems(self, *args):
|
||||
"""Run ``pytest.main(['--collectonly'])`` in-process.
|
||||
|
||||
Returns a tuple of the collected items and a
|
||||
:py:class:`HookRecorder` instance.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
pytest inside the test process itself like
|
||||
:py:meth:`inline_run`. However the return value is a tuple of
|
||||
the collection items and a :py:class:`HookRecorder` instance.
|
||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||
the test process itself like :py:meth:`inline_run`, but returns a
|
||||
tuple of the collected items and a :py:class:`HookRecorder` instance.
|
||||
|
||||
"""
|
||||
rec = self.inline_run("--collect-only", *args)
|
||||
@@ -674,60 +698,78 @@ class Testdir:
|
||||
def inline_run(self, *args, **kwargs):
|
||||
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
pytest inside the test process itself. This means it can
|
||||
return a :py:class:`HookRecorder` instance which gives more
|
||||
detailed results from then run then can be done by matching
|
||||
stdout/stderr from :py:meth:`runpytest`.
|
||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||
the test process itself. This means it can return a
|
||||
:py:class:`HookRecorder` instance which gives more detailed results
|
||||
from that run than can be done by matching stdout/stderr from
|
||||
:py:meth:`runpytest`.
|
||||
|
||||
:param args: Any command line arguments to pass to
|
||||
:py:func:`pytest.main`.
|
||||
:param args: command line arguments to pass to :py:func:`pytest.main`
|
||||
|
||||
:param plugin: (keyword-only) Extra plugin instances the
|
||||
``pytest.main()`` instance should use.
|
||||
:param plugin: (keyword-only) extra plugin instances the
|
||||
``pytest.main()`` instance should use
|
||||
|
||||
:return: a :py:class:`HookRecorder` instance
|
||||
|
||||
:return: A :py:class:`HookRecorder` instance.
|
||||
"""
|
||||
# When running py.test inline any plugins active in the main
|
||||
# test process are already imported. So this disables the
|
||||
# warning which will trigger to say they can no longer be
|
||||
# rewritten, which is fine as they are already rewritten.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
finalizers = []
|
||||
try:
|
||||
# When running py.test inline any plugins active in the main test
|
||||
# process are already imported. So this disables the warning which
|
||||
# will trigger to say they can no longer be rewritten, which is
|
||||
# fine as they have already been rewritten.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
|
||||
def revert():
|
||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
||||
def revert_warn_already_imported():
|
||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
||||
finalizers.append(revert_warn_already_imported)
|
||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
||||
|
||||
self.request.addfinalizer(revert)
|
||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
||||
# Any sys.module or sys.path changes done while running py.test
|
||||
# inline should be reverted after the test run completes to avoid
|
||||
# clashing with later inline tests run within the same pytest test,
|
||||
# e.g. just because they use matching test module names.
|
||||
finalizers.append(self.__take_sys_modules_snapshot().restore)
|
||||
finalizers.append(SysPathsSnapshot().restore)
|
||||
|
||||
rec = []
|
||||
# Important note:
|
||||
# - our tests should not leave any other references/registrations
|
||||
# laying around other than possibly loaded test modules
|
||||
# referenced from sys.modules, as nothing will clean those up
|
||||
# automatically
|
||||
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
rec = []
|
||||
|
||||
plugins = kwargs.get("plugins") or []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
self.delete_loaded_modules()
|
||||
if len(rec) == 1:
|
||||
reprec = rec.pop()
|
||||
else:
|
||||
class reprec:
|
||||
pass
|
||||
reprec.ret = ret
|
||||
class Collect(object):
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
|
||||
# typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing
|
||||
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
|
||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
return reprec
|
||||
plugins = kwargs.get("plugins") or []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
if len(rec) == 1:
|
||||
reprec = rec.pop()
|
||||
else:
|
||||
class reprec(object):
|
||||
pass
|
||||
reprec.ret = ret
|
||||
|
||||
# typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing
|
||||
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
|
||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
return reprec
|
||||
finally:
|
||||
for finalizer in finalizers:
|
||||
finalizer()
|
||||
|
||||
def runpytest_inprocess(self, *args, **kwargs):
|
||||
""" Return result of running pytest in-process, providing a similar
|
||||
interface to what self.runpytest() provides. """
|
||||
"""Return result of running pytest in-process, providing a similar
|
||||
interface to what self.runpytest() provides.
|
||||
|
||||
"""
|
||||
if kwargs.get("syspathinsert"):
|
||||
self.syspathinsert()
|
||||
now = time.time()
|
||||
@@ -738,13 +780,13 @@ class Testdir:
|
||||
reprec = self.inline_run(*args, **kwargs)
|
||||
except SystemExit as e:
|
||||
|
||||
class reprec:
|
||||
class reprec(object):
|
||||
ret = e.args[0]
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
class reprec:
|
||||
class reprec(object):
|
||||
ret = 3
|
||||
finally:
|
||||
out, err = capture.readouterr()
|
||||
@@ -759,7 +801,7 @@ class Testdir:
|
||||
return res
|
||||
|
||||
def runpytest(self, *args, **kwargs):
|
||||
""" Run pytest inline or in a subprocess, depending on the command line
|
||||
"""Run pytest inline or in a subprocess, depending on the command line
|
||||
option "--runpytest" and return a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
@@ -780,13 +822,13 @@ class Testdir:
|
||||
def parseconfig(self, *args):
|
||||
"""Return a new pytest Config instance from given commandline args.
|
||||
|
||||
This invokes the pytest bootstrapping code in _pytest.config
|
||||
to create a new :py:class:`_pytest.core.PluginManager` and
|
||||
call the pytest_cmdline_parse hook to create new
|
||||
This invokes the pytest bootstrapping code in _pytest.config to create
|
||||
a new :py:class:`_pytest.core.PluginManager` and call the
|
||||
pytest_cmdline_parse hook to create a new
|
||||
:py:class:`_pytest.config.Config` instance.
|
||||
|
||||
If :py:attr:`plugins` has been populated they should be plugin
|
||||
modules which will be registered with the PluginManager.
|
||||
If :py:attr:`plugins` has been populated they should be plugin modules
|
||||
to be registered with the PluginManager.
|
||||
|
||||
"""
|
||||
args = self._ensure_basetemp(args)
|
||||
@@ -802,9 +844,8 @@ class Testdir:
|
||||
def parseconfigure(self, *args):
|
||||
"""Return a new pytest configured Config instance.
|
||||
|
||||
This returns a new :py:class:`_pytest.config.Config` instance
|
||||
like :py:meth:`parseconfig`, but also calls the
|
||||
pytest_configure hook.
|
||||
This returns a new :py:class:`_pytest.config.Config` instance like
|
||||
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
||||
|
||||
"""
|
||||
config = self.parseconfig(*args)
|
||||
@@ -815,14 +856,14 @@ class Testdir:
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
"""Return the test item for a test function.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning the test item
|
||||
for the requested function name.
|
||||
This writes the source to a python file and runs pytest's collection on
|
||||
the resulting module, returning the test item for the requested
|
||||
function name.
|
||||
|
||||
:param source: The module source.
|
||||
:param source: the module source
|
||||
|
||||
:param funcname: The name of the test function for which the
|
||||
Item must be returned.
|
||||
:param funcname: the name of the test function for which to return a
|
||||
test item
|
||||
|
||||
"""
|
||||
items = self.getitems(source)
|
||||
@@ -835,9 +876,8 @@ class Testdir:
|
||||
def getitems(self, source):
|
||||
"""Return all test items collected from the module.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning all test items
|
||||
contained within.
|
||||
This writes the source to a python file and runs pytest's collection on
|
||||
the resulting module, returning all test items contained within.
|
||||
|
||||
"""
|
||||
modcol = self.getmodulecol(source)
|
||||
@@ -846,17 +886,17 @@ class Testdir:
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
"""Return the module collection node for ``source``.
|
||||
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile`
|
||||
and then runs the pytest collection on it, returning the
|
||||
collection node for the test module.
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile` and then
|
||||
runs the pytest collection on it, returning the collection node for the
|
||||
test module.
|
||||
|
||||
:param source: The source code of the module to collect.
|
||||
:param source: the source code of the module to collect
|
||||
|
||||
:param configargs: Any extra arguments to pass to
|
||||
:py:meth:`parseconfigure`.
|
||||
:param configargs: any extra arguments to pass to
|
||||
:py:meth:`parseconfigure`
|
||||
|
||||
:param withinit: Whether to also write a ``__init__.py`` file
|
||||
to the temporary directory to ensure it is a package.
|
||||
:param withinit: whether to also write an ``__init__.py`` file to the
|
||||
same directory to ensure it is a package
|
||||
|
||||
"""
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
@@ -871,13 +911,12 @@ class Testdir:
|
||||
def collect_by_name(self, modcol, name):
|
||||
"""Return the collection node for name from the module collection.
|
||||
|
||||
This will search a module collection node for a collection
|
||||
node matching the given name.
|
||||
This will search a module collection node for a collection node
|
||||
matching the given name.
|
||||
|
||||
:param modcol: A module collection node, see
|
||||
:py:meth:`getmodulecol`.
|
||||
:param modcol: a module collection node; see :py:meth:`getmodulecol`
|
||||
|
||||
:param name: The name of the node to return.
|
||||
:param name: the name of the node to return
|
||||
|
||||
"""
|
||||
if modcol not in self._mod_collections:
|
||||
@@ -889,8 +928,8 @@ class Testdir:
|
||||
def popen(self, cmdargs, stdout, stderr, **kw):
|
||||
"""Invoke subprocess.Popen.
|
||||
|
||||
This calls subprocess.Popen making sure the current working
|
||||
directory is the PYTHONPATH.
|
||||
This calls subprocess.Popen making sure the current working directory
|
||||
is in the PYTHONPATH.
|
||||
|
||||
You probably want to use :py:meth:`run` instead.
|
||||
|
||||
@@ -908,8 +947,7 @@ class Testdir:
|
||||
def run(self, *cmdargs):
|
||||
"""Run a command with arguments.
|
||||
|
||||
Run a process using subprocess.Popen saving the stdout and
|
||||
stderr.
|
||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
@@ -952,14 +990,15 @@ class Testdir:
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def _getpytestargs(self):
|
||||
# we cannot use "(sys.executable,script)"
|
||||
# because on windows the script is e.g. a pytest.exe
|
||||
# we cannot use `(sys.executable, script)` because on Windows the
|
||||
# script is e.g. `pytest.exe`
|
||||
return (sys.executable, PYTEST_FULLPATH) # noqa
|
||||
|
||||
def runpython(self, script):
|
||||
"""Run a python script using sys.executable as interpreter.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
return self.run(sys.executable, script)
|
||||
|
||||
@@ -970,25 +1009,18 @@ class Testdir:
|
||||
def runpytest_subprocess(self, *args, **kwargs):
|
||||
"""Run pytest as a subprocess with given arguments.
|
||||
|
||||
Any plugins added to the :py:attr:`plugins` list will added
|
||||
using the ``-p`` command line option. Addtionally
|
||||
``--basetemp`` is used put any temporary files and directories
|
||||
in a numbered directory prefixed with "runpytest-" so they do
|
||||
not conflict with the normal numberd pytest location for
|
||||
temporary files and directories.
|
||||
Any plugins added to the :py:attr:`plugins` list will added using the
|
||||
``-p`` command line option. Additionally ``--basetemp`` is used put
|
||||
any temporary files and directories in a numbered directory prefixed
|
||||
with "runpytest-" so they do not conflict with the normal numbered
|
||||
pytest location for temporary files and directories.
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-",
|
||||
keep=None, rootdir=self.tmpdir)
|
||||
args = ('--basetemp=%s' % p, ) + args
|
||||
# for x in args:
|
||||
# if '--confcutdir' in str(x):
|
||||
# break
|
||||
# else:
|
||||
# pass
|
||||
# args = ('--confcutdir=.',) + args
|
||||
args = ('--basetemp=%s' % p,) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
args = ('-p', plugins[0]) + args
|
||||
@@ -998,8 +1030,8 @@ class Testdir:
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
"""Run pytest using pexpect.
|
||||
|
||||
This makes sure to use the right pytest and sets up the
|
||||
temporary directory locations.
|
||||
This makes sure to use the right pytest and sets up the temporary
|
||||
directory locations.
|
||||
|
||||
The pexpect child is returned.
|
||||
|
||||
@@ -1013,6 +1045,7 @@ class Testdir:
|
||||
"""Run a command using pexpect.
|
||||
|
||||
The pexpect child is returned.
|
||||
|
||||
"""
|
||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
|
||||
@@ -1034,13 +1067,15 @@ def getdecoded(out):
|
||||
py.io.saferepr(out),)
|
||||
|
||||
|
||||
class LineComp:
|
||||
class LineComp(object):
|
||||
def __init__(self):
|
||||
self.stringio = py.io.TextIO()
|
||||
|
||||
def assert_contains_lines(self, lines2):
|
||||
""" assert that lines2 are contained (linearly) in lines1.
|
||||
return a list of extralines found.
|
||||
"""Assert that lines2 are contained (linearly) in lines1.
|
||||
|
||||
Return a list of extralines found.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
val = self.stringio.getvalue()
|
||||
@@ -1050,14 +1085,14 @@ class LineComp:
|
||||
return LineMatcher(lines1).fnmatch_lines(lines2)
|
||||
|
||||
|
||||
class LineMatcher:
|
||||
class LineMatcher(object):
|
||||
"""Flexible matching of text.
|
||||
|
||||
This is a convenience class to test large texts like the output of
|
||||
commands.
|
||||
|
||||
The constructor takes a list of lines without their trailing
|
||||
newlines, i.e. ``text.splitlines()``.
|
||||
The constructor takes a list of lines without their trailing newlines, i.e.
|
||||
``text.splitlines()``.
|
||||
|
||||
"""
|
||||
|
||||
@@ -1077,18 +1112,19 @@ class LineMatcher:
|
||||
return lines2
|
||||
|
||||
def fnmatch_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using ``fnmatch.fnmatch``, in any order.
|
||||
"""Check lines exist in the output using in any order.
|
||||
|
||||
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
|
||||
lines which have to occur in the output, in any order.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order.
|
||||
"""
|
||||
self._match_lines_random(lines2, fnmatch)
|
||||
|
||||
def re_match_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using ``re.match``, in any order.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order.
|
||||
The argument is a list of lines which have to occur in the output, in
|
||||
any order.
|
||||
|
||||
"""
|
||||
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
|
||||
@@ -1096,8 +1132,8 @@ class LineMatcher:
|
||||
def _match_lines_random(self, lines2, match_func):
|
||||
"""Check lines exist in the output.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order. Each line can contain glob whildcards.
|
||||
The argument is a list of lines which have to occur in the output, in
|
||||
any order. Each line can contain glob whildcards.
|
||||
|
||||
"""
|
||||
lines2 = self._getlines(lines2)
|
||||
@@ -1114,6 +1150,7 @@ class LineMatcher:
|
||||
"""Return all lines following the given line in the text.
|
||||
|
||||
The given line can contain glob wildcards.
|
||||
|
||||
"""
|
||||
for i, line in enumerate(self.lines):
|
||||
if fnline == line or fnmatch(line, fnline):
|
||||
@@ -1130,10 +1167,9 @@ class LineMatcher:
|
||||
def fnmatch_lines(self, lines2):
|
||||
"""Search captured text for matching lines using ``fnmatch.fnmatch``.
|
||||
|
||||
The argument is a list of lines which have to match and can
|
||||
use glob wildcards. If they do not match a pytest.fail() is
|
||||
called. The matches and non-matches are also printed on
|
||||
stdout.
|
||||
The argument is a list of lines which have to match and can use glob
|
||||
wildcards. If they do not match a pytest.fail() is called. The
|
||||
matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
self._match_lines(lines2, fnmatch, 'fnmatch')
|
||||
@@ -1144,21 +1180,22 @@ class LineMatcher:
|
||||
The argument is a list of lines which have to match using ``re.match``.
|
||||
If they do not match a pytest.fail() is called.
|
||||
|
||||
The matches and non-matches are also printed on
|
||||
stdout.
|
||||
The matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match')
|
||||
|
||||
def _match_lines(self, lines2, match_func, match_nickname):
|
||||
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
||||
|
||||
:param list[str] lines2: list of string patterns to match. The actual format depends on
|
||||
``match_func``.
|
||||
:param match_func: a callable ``match_func(line, pattern)`` where line is the captured
|
||||
line from stdout/stderr and pattern is the matching pattern.
|
||||
:param list[str] lines2: list of string patterns to match. The actual
|
||||
format depends on ``match_func``
|
||||
:param match_func: a callable ``match_func(line, pattern)`` where line
|
||||
is the captured line from stdout/stderr and pattern is the matching
|
||||
pattern
|
||||
:param str match_nickname: the nickname for the match function that
|
||||
will be logged to stdout when a match occurs
|
||||
|
||||
:param str match_nickname: the nickname for the match function that will be logged
|
||||
to stdout when a match occurs.
|
||||
"""
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
|
||||
@@ -19,7 +19,7 @@ from _pytest.config import hookimpl
|
||||
import _pytest
|
||||
import pluggy
|
||||
from _pytest import fixtures
|
||||
from _pytest import main
|
||||
from _pytest import nodes
|
||||
from _pytest import deprecated
|
||||
from _pytest.compat import (
|
||||
isclass, isfunction, is_generator, ascii_escaped,
|
||||
@@ -30,9 +30,17 @@ from _pytest.compat import (
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.mark import transfer_markers
|
||||
|
||||
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
cutdir2 = py.path.local(_pytest.__file__).dirpath()
|
||||
cutdir3 = py.path.local(py.__file__).dirpath()
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _pluggy_dir.basename == '__init__.py':
|
||||
_pluggy_dir = _pluggy_dir.dirpath()
|
||||
_pytest_dir = py.path.local(_pytest.__file__).dirpath()
|
||||
_py_dir = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
@@ -47,10 +55,10 @@ def filter_traceback(entry):
|
||||
is_generated = '<' in raw_filename and '>' in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to an inexisting file, in which case it will
|
||||
# alsso return a str object. see #1133
|
||||
# entry.path might point to an non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return p != cutdir1 and not p.relto(cutdir2) and not p.relto(cutdir3)
|
||||
return not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
|
||||
|
||||
|
||||
def pyobj_property(name):
|
||||
@@ -261,7 +269,7 @@ class PyobjMixin(PyobjContext):
|
||||
return fspath, lineno, modpath
|
||||
|
||||
|
||||
class PyCollector(PyobjMixin, main.Collector):
|
||||
class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
def funcnamefilter(self, name):
|
||||
return self._matches_prefix_or_glob_option('python_functions', name)
|
||||
@@ -386,7 +394,7 @@ class PyCollector(PyobjMixin, main.Collector):
|
||||
)
|
||||
|
||||
|
||||
class Module(main.File, PyCollector):
|
||||
class Module(nodes.File, PyCollector):
|
||||
""" Collector for test classes and functions. """
|
||||
|
||||
def _getobj(self):
|
||||
@@ -563,7 +571,6 @@ class FunctionMixin(PyobjMixin):
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(path=path)
|
||||
if ntraceback == traceback:
|
||||
# ntraceback = ntraceback.cut(excludepath=cutdir2)
|
||||
ntraceback = ntraceback.filter(filter_traceback)
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
@@ -779,7 +786,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
from _pytest.mark import ParameterSet
|
||||
from py.io import saferepr
|
||||
argnames, parameters = ParameterSet._for_parameterize(
|
||||
argnames, argvalues, self.function)
|
||||
argnames, argvalues, self.function, self.config)
|
||||
del argvalues
|
||||
|
||||
if scope is None:
|
||||
@@ -933,7 +940,7 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
return ascii_escaped(val.pattern)
|
||||
elif enum is not None and isinstance(val, enum.Enum):
|
||||
return str(val)
|
||||
elif isclass(val) and hasattr(val, '__name__'):
|
||||
elif (isclass(val) or isfunction(val)) and hasattr(val, '__name__'):
|
||||
return val.__name__
|
||||
return str(argname) + str(idx)
|
||||
|
||||
@@ -1090,7 +1097,7 @@ def write_docstring(tw, doc):
|
||||
tw.write(INDENT + line + "\n")
|
||||
|
||||
|
||||
class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr):
|
||||
class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
""" a Function Item is responsible for setting up and executing a
|
||||
Python test function.
|
||||
"""
|
||||
|
||||
@@ -60,6 +60,9 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
item.ihook.pytest_runtest_logfinish(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -175,7 +178,7 @@ def call_runtest_hook(item, when, **kwds):
|
||||
return CallInfo(lambda: ihook(item=item, **kwds), when=when)
|
||||
|
||||
|
||||
class CallInfo:
|
||||
class CallInfo(object):
|
||||
""" Result/Exception info a function invocation. """
|
||||
#: None or ExceptionInfo object.
|
||||
excinfo = None
|
||||
|
||||
@@ -261,7 +261,7 @@ def pytest_runtest_makereport(item, call):
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = explanation
|
||||
elif item._skipped_by_mark and rep.skipped and type(rep.longrepr) is tuple:
|
||||
elif getattr(item, '_skipped_by_mark', False) and rep.skipped and type(rep.longrepr) is tuple:
|
||||
# skipped by mark.skipif; change the location of the failure
|
||||
# to point to the item definition, otherwise it will display
|
||||
# the location of where the skip exception was raised within pytest
|
||||
|
||||
@@ -94,7 +94,7 @@ def pytest_report_teststatus(report):
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
class WarningReport:
|
||||
class WarningReport(object):
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
"""
|
||||
@@ -129,7 +129,7 @@ class WarningReport:
|
||||
return None
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
class TerminalReporter(object):
|
||||
def __init__(self, config, file=None):
|
||||
import _pytest.config
|
||||
self.config = config
|
||||
@@ -152,8 +152,18 @@ class TerminalReporter:
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
self._progress_items_reported = 0
|
||||
self._show_progress_info = self.config.getini('console_output_style') == 'progress'
|
||||
self._progress_nodeids_reported = set()
|
||||
self._show_progress_info = self._determine_show_progress_info()
|
||||
|
||||
def _determine_show_progress_info(self):
|
||||
"""Return True if we should display progress information based on the current config"""
|
||||
# do not show progress if we are not capturing output (#3038)
|
||||
if self.config.getoption('capture') == 'no':
|
||||
return False
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption('setupshow'):
|
||||
return False
|
||||
return self.config.getini('console_output_style') == 'progress'
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
@@ -178,7 +188,6 @@ class TerminalReporter:
|
||||
if extra:
|
||||
self._tw.write(extra, **kwargs)
|
||||
self.currentfspath = -2
|
||||
self._write_progress_information_filling_space()
|
||||
|
||||
def ensure_newline(self):
|
||||
if self.currentfspath:
|
||||
@@ -268,14 +277,13 @@ class TerminalReporter:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
running_xdist = hasattr(rep, 'node')
|
||||
self._progress_items_reported += 1
|
||||
if self.verbosity <= 0:
|
||||
if not running_xdist and self.showfspath:
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
self._write_progress_if_past_edge()
|
||||
else:
|
||||
self._progress_nodeids_reported.add(rep.nodeid)
|
||||
if markup is None:
|
||||
if rep.passed:
|
||||
markup = {'green': True}
|
||||
@@ -288,6 +296,8 @@ class TerminalReporter:
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
self.ensure_newline()
|
||||
self._tw.write("[%s]" % rep.node.gateway.id)
|
||||
@@ -299,31 +309,28 @@ class TerminalReporter:
|
||||
self._tw.write(" " + line)
|
||||
self.currentfspath = -2
|
||||
|
||||
def _write_progress_if_past_edge(self):
|
||||
if not self._show_progress_info:
|
||||
return
|
||||
last_item = self._progress_items_reported == self._session.testscollected
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
return
|
||||
|
||||
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + '\n', cyan=True)
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = len(self._progress_nodeids_reported) == self._session.testscollected
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + '\n', cyan=True)
|
||||
|
||||
_PROGRESS_LENGTH = len(' [100%]')
|
||||
|
||||
def _get_progress_information_message(self):
|
||||
collected = self._session.testscollected
|
||||
if collected:
|
||||
progress = self._progress_items_reported * 100 // collected
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return ' [{:3d}%]'.format(progress)
|
||||
return ' [100%]'
|
||||
|
||||
def _write_progress_information_filling_space(self):
|
||||
if not self._show_progress_info:
|
||||
return
|
||||
msg = self._get_progress_information_message()
|
||||
fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1)
|
||||
self.write(fill + msg, cyan=True)
|
||||
|
||||
@@ -8,7 +8,7 @@ import py
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
class TempdirFactory:
|
||||
class TempdirFactory(object):
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
|
||||
@@ -6,6 +6,8 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.4.0
|
||||
release-3.3.2
|
||||
release-3.3.1
|
||||
release-3.3.0
|
||||
release-3.2.5
|
||||
|
||||
28
doc/en/announce/release-3.3.2.rst
Normal file
28
doc/en/announce/release-3.3.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.3.2
|
||||
=======================================
|
||||
|
||||
pytest 3.3.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Antony Lee
|
||||
* Austin
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Floris Bruynooghe
|
||||
* Henk-Jaap Wagenaar
|
||||
* Jurko Gospodnetić
|
||||
* Ronny Pfannschmidt
|
||||
* Srinivas Reddy Thatiparthy
|
||||
* Thomas Hisch
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
52
doc/en/announce/release-3.4.0.rst
Normal file
52
doc/en/announce/release-3.4.0.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-3.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.4.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 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:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Aaron
|
||||
* Alan Velasco
|
||||
* Anders Hovmöller
|
||||
* Andrew Toolan
|
||||
* Anthony Sottile
|
||||
* Aron Coyle
|
||||
* Brian Maissy
|
||||
* Bruno Oliveira
|
||||
* Cyrus Maden
|
||||
* Florian Bruhin
|
||||
* Henk-Jaap Wagenaar
|
||||
* Ian Lesperance
|
||||
* Jon Dufresne
|
||||
* Jurko Gospodnetić
|
||||
* Kate
|
||||
* Kimberly
|
||||
* Per A. Brodtkorb
|
||||
* Pierre-Alexandre Fonta
|
||||
* Raphael Castaneda
|
||||
* Ronny Pfannschmidt
|
||||
* ST John
|
||||
* Segev Finer
|
||||
* Thomas Hisch
|
||||
* Tzu-ping Chung
|
||||
* feuillemorte
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
@@ -15,91 +15,6 @@ We will only remove deprecated functionality in major releases (e.g. if we depre
|
||||
Deprecation Roadmap
|
||||
-------------------
|
||||
|
||||
This page lists deprecated features and when we plan to remove them. It is important to list the feature, the version where it got deprecated and the version we plan to remove it.
|
||||
We track deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
||||
|
||||
Following our deprecation policy, we should aim to keep features for *at least* two minor versions after it was considered deprecated.
|
||||
|
||||
|
||||
Future Releases
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
3.4
|
||||
^^^
|
||||
|
||||
**Old style classes**
|
||||
|
||||
Issue: `#2147 <https://github.com/pytest-dev/pytest/issues/2147>`_.
|
||||
|
||||
Deprecated in ``3.2``.
|
||||
|
||||
4.0
|
||||
^^^
|
||||
|
||||
**Yield tests**
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
**pytest-namespace hook**
|
||||
|
||||
deprecated in ``3.2``.
|
||||
|
||||
**Marks in parameter sets**
|
||||
|
||||
Deprecated in ``3.2``.
|
||||
|
||||
**--result-log**
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
See `#830 <https://github.com/pytest-dev/pytest/issues/830>`_ for more information. Suggested alternative: `pytest-tap <https://pypi.python.org/pypi/pytest-tap>`_.
|
||||
|
||||
**metafunc.addcall**
|
||||
|
||||
Issue: `#2876 <https://github.com/pytest-dev/pytest/issues/2876>`_.
|
||||
|
||||
Deprecated in ``3.3``.
|
||||
|
||||
**pytest_plugins in non-toplevel conftests**
|
||||
|
||||
There is a deep conceptual confusion as ``conftest.py`` files themselves are activated/deactivated based on path, but the plugins they depend on aren't.
|
||||
|
||||
Issue: `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_.
|
||||
|
||||
Not yet officially deprecated.
|
||||
|
||||
**passing a single string to pytest.main()**
|
||||
|
||||
Pass a list of strings to ``pytest.main()`` instead.
|
||||
|
||||
Deprecated in ``3.1``.
|
||||
|
||||
**[pytest] section in setup.cfg**
|
||||
|
||||
Use ``[tool:pytest]`` instead for compatibility with other tools.
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
Past Releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
3.0
|
||||
^^^
|
||||
|
||||
* The following deprecated commandline options were removed:
|
||||
|
||||
* ``--genscript``: no longer supported;
|
||||
* ``--no-assert``: use ``--assert=plain`` instead;
|
||||
* ``--nomagic``: use ``--assert=plain`` instead;
|
||||
* ``--report``: use ``-r`` instead;
|
||||
|
||||
* Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
|
||||
were never documented and a leftover from a pre-virtualenv era. These entry
|
||||
points also created broken entry points in wheels, so removing them also
|
||||
removes a source of confusion for users.
|
||||
|
||||
|
||||
|
||||
3.3
|
||||
^^^
|
||||
|
||||
* Dropped support for EOL Python 2.6 and 3.3.
|
||||
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.
|
||||
|
||||
@@ -116,6 +116,10 @@ You can ask for available builtin or project-custom
|
||||
Add extra xml properties to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded.
|
||||
record_xml_attribute
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded
|
||||
caplog
|
||||
Access and control log capturing.
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ You can always peek at the content of the cache using the
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
cachedir: $REGENDOC_TMPDIR/.cache
|
||||
cachedir: $REGENDOC_TMPDIR/.pytest_cache
|
||||
------------------------------- cache values -------------------------------
|
||||
cache/lastfailed contains:
|
||||
{'test_caching.py::test_function': True}
|
||||
|
||||
@@ -92,14 +92,14 @@ an example test function that performs some output related checks:
|
||||
.. code-block:: python
|
||||
|
||||
def test_myoutput(capsys): # or use "capfd" for fd-level
|
||||
print ("hello")
|
||||
print("hello")
|
||||
sys.stderr.write("world\n")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "hello\n"
|
||||
assert err == "world\n"
|
||||
print ("next")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "next\n"
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
assert captured.err == "world\n"
|
||||
print("next")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "next\n"
|
||||
|
||||
The ``readouterr()`` call snapshots the output so far -
|
||||
and capturing will be continued. After the test
|
||||
@@ -117,6 +117,10 @@ system level output streams (FD1 and FD2).
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
If the code under test writes non-textual data, you can capture this using
|
||||
the ``capsysbinary`` fixture which instead returns ``bytes`` from
|
||||
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only
|
||||
|
||||
@@ -152,11 +152,25 @@ above will show verbose output because ``-v`` overwrites ``-q``.
|
||||
Builtin configuration file options
|
||||
----------------------------------------------
|
||||
|
||||
Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg``
|
||||
file, usually located at the root of your repository. All options must be under a ``[pytest]`` section
|
||||
(``[tool:pytest]`` for ``setup.cfg`` files).
|
||||
|
||||
Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be
|
||||
passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
pytest -o console_output_style=classic -o cache_dir=/tmp/mycache
|
||||
|
||||
|
||||
.. confval:: minversion
|
||||
|
||||
Specifies a minimal pytest version required for running tests.
|
||||
|
||||
minversion = 2.1 # will fail if we run with pytest-2.0
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
minversion = 3.0 # will fail if we run with pytest-2.8
|
||||
|
||||
.. confval:: addopts
|
||||
|
||||
@@ -165,6 +179,7 @@ Builtin configuration file options
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
addopts = --maxfail=2 -rf # exit after 2 failures, report fail info
|
||||
|
||||
@@ -331,3 +346,28 @@ Builtin configuration file options
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
console_output_style = classic
|
||||
|
||||
|
||||
.. confval:: empty_parameter_set_mark
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
Allows to pick the action for empty parametersets in parameterization
|
||||
|
||||
* ``skip`` skips tests with a empty parameterset (default)
|
||||
* ``xfail`` marks tests with a empty parameterset as xfail(run=False)
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
empty_parameter_set_mark = xfail
|
||||
|
||||
.. note::
|
||||
|
||||
The default value of this option is planned to change to ``xfail`` in future releases
|
||||
as this is considered less error prone, see `#3155`_ for more details.
|
||||
|
||||
|
||||
|
||||
.. _`#3155`: https://github.com/pytest-dev/pytest/issues/3155
|
||||
|
||||
@@ -81,9 +81,9 @@ Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported
|
||||
when executing text doctest files.
|
||||
|
||||
The standard ``doctest`` module provides some setting flags to configure the
|
||||
strictness of doctest tests. In pytest You can enable those flags those flags
|
||||
using the configuration file. To make pytest ignore trailing whitespaces and
|
||||
ignore lengthy exception stack traces you can just write:
|
||||
strictness of doctest tests. In pytest, you can enable those flags using the
|
||||
configuration file. To make pytest ignore trailing whitespaces and ignore
|
||||
lengthy exception stack traces you can just write:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
|
||||
@@ -157,12 +157,14 @@ class TestRaises(object):
|
||||
|
||||
# thanks to Matthew Scott for this test
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
import imp
|
||||
import sys
|
||||
src = 'def foo():\n assert 1 == 0\n'
|
||||
name = 'abc-123'
|
||||
module = py.std.imp.new_module(name)
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, 'exec')
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
py.std.sys.modules[name] = module
|
||||
sys.modules[name] = module
|
||||
module.foo()
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
$ pytest -v -m webtest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
@@ -46,7 +46,7 @@ Or the inverse, running all tests except the webtest ones::
|
||||
$ pytest -v -m "not webtest"
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
@@ -67,7 +67,7 @@ tests based on their module, class, method, or function name::
|
||||
$ pytest -v test_server.py::TestClass::test_method
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
|
||||
@@ -80,7 +80,7 @@ You can also select on the class::
|
||||
$ pytest -v test_server.py::TestClass
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
|
||||
@@ -93,7 +93,7 @@ Or select multiple nodes::
|
||||
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
@@ -131,7 +131,7 @@ select tests based on their names::
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
@@ -145,7 +145,7 @@ And you can also run all tests except the ones that match the keyword::
|
||||
$ pytest -k "not send_http" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
@@ -161,7 +161,7 @@ Or to select "http" and "quick" tests::
|
||||
$ pytest -k "http or quick" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
@@ -432,7 +432,7 @@ The output is as follows::
|
||||
|
||||
$ pytest -q -s
|
||||
Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={}
|
||||
. [100%]
|
||||
.
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``.
|
||||
@@ -477,7 +477,7 @@ Let's run this without capturing output and see what we get::
|
||||
glob args=('function',) kwargs={'x': 3}
|
||||
glob args=('class',) kwargs={'x': 2}
|
||||
glob args=('module',) kwargs={'x': 1}
|
||||
. [100%]
|
||||
.
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
marking platform specific tests with pytest
|
||||
|
||||
@@ -60,7 +60,7 @@ consulted when reporting in ``verbose`` mode::
|
||||
nonpython $ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
|
||||
@@ -411,22 +411,24 @@ get on the terminal - we are working on that)::
|
||||
____________________ test_dynamic_compile_shows_nicely _____________________
|
||||
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
import imp
|
||||
import sys
|
||||
src = 'def foo():\n assert 1 == 0\n'
|
||||
name = 'abc-123'
|
||||
module = py.std.imp.new_module(name)
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, 'exec')
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
py.std.sys.modules[name] = module
|
||||
sys.modules[name] = module
|
||||
> module.foo()
|
||||
|
||||
failure_demo.py:166:
|
||||
failure_demo.py:168:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
def foo():
|
||||
> assert 1 == 0
|
||||
E AssertionError
|
||||
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:163>:2: AssertionError
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:165>:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -438,7 +440,7 @@ get on the terminal - we are working on that)::
|
||||
return 43
|
||||
> somefunc(f(), g())
|
||||
|
||||
failure_demo.py:176:
|
||||
failure_demo.py:178:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
failure_demo.py:9: in somefunc
|
||||
otherfunc(x,y)
|
||||
@@ -460,7 +462,7 @@ get on the terminal - we are working on that)::
|
||||
> a,b = l
|
||||
E ValueError: not enough values to unpack (expected 2, got 0)
|
||||
|
||||
failure_demo.py:180: ValueError
|
||||
failure_demo.py:182: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -470,7 +472,7 @@ get on the terminal - we are working on that)::
|
||||
> a,b = l
|
||||
E TypeError: 'int' object is not iterable
|
||||
|
||||
failure_demo.py:184: TypeError
|
||||
failure_demo.py:186: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -483,7 +485,7 @@ get on the terminal - we are working on that)::
|
||||
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:189: AssertionError
|
||||
failure_demo.py:191: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -500,7 +502,7 @@ get on the terminal - we are working on that)::
|
||||
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:196: AssertionError
|
||||
failure_demo.py:198: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -511,7 +513,7 @@ get on the terminal - we are working on that)::
|
||||
E + where False = isinstance(43, float)
|
||||
E + where 43 = globf(42)
|
||||
|
||||
failure_demo.py:199: AssertionError
|
||||
failure_demo.py:201: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -522,7 +524,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 42 != 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
|
||||
|
||||
failure_demo.py:203: AssertionError
|
||||
failure_demo.py:205: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -532,7 +534,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 11 < 5
|
||||
E + where 11 = globf(10)
|
||||
|
||||
failure_demo.py:206: AssertionError
|
||||
failure_demo.py:208: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -543,7 +545,7 @@ get on the terminal - we are working on that)::
|
||||
> assert x == 0
|
||||
E assert 1 == 0
|
||||
|
||||
failure_demo.py:211: AssertionError
|
||||
failure_demo.py:213: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
@@ -557,7 +559,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:222: AssertionError
|
||||
failure_demo.py:224: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
@@ -574,7 +576,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:228: AssertionError
|
||||
failure_demo.py:230: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
@@ -594,7 +596,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
|
||||
|
||||
failure_demo.py:238: AssertionError
|
||||
failure_demo.py:240: AssertionError
|
||||
============================= warnings summary =============================
|
||||
None
|
||||
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
|
||||
@@ -332,7 +332,7 @@ which will add info only when run with "--v"::
|
||||
$ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
@@ -385,8 +385,8 @@ Now we can profile which test functions execute the slowest::
|
||||
test_some_are_slow.py ... [100%]
|
||||
|
||||
========================= slowest 3 test durations =========================
|
||||
0.30s call test_some_are_slow.py::test_funcslow2
|
||||
0.20s call test_some_are_slow.py::test_funcslow1
|
||||
0.58s call test_some_are_slow.py::test_funcslow2
|
||||
0.41s call test_some_are_slow.py::test_funcslow1
|
||||
0.10s call test_some_are_slow.py::test_funcfast
|
||||
========================= 3 passed in 0.12 seconds =========================
|
||||
|
||||
@@ -537,7 +537,7 @@ We can run this::
|
||||
file $REGENDOC_TMPDIR/b/test_error.py, line 1
|
||||
def test_root(db): # no db here, will error out
|
||||
E fixture 'db' not found
|
||||
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory
|
||||
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory
|
||||
> use 'pytest --fixtures [testpath]' for help on them.
|
||||
|
||||
$REGENDOC_TMPDIR/b/test_error.py:1
|
||||
@@ -731,7 +731,7 @@ and run it::
|
||||
|
||||
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
|
||||
Fexecuting test failed test_module.py::test_call_fails
|
||||
F [100%]
|
||||
F
|
||||
|
||||
================================== ERRORS ==================================
|
||||
____________________ ERROR at setup of test_setup_fails ____________________
|
||||
@@ -826,15 +826,20 @@ Instead of freezing the pytest runner as a separate executable, you can make
|
||||
your frozen program work as the pytest runner by some clever
|
||||
argument handling during program startup. This allows you to
|
||||
have a single executable, which is usually more convenient.
|
||||
Please note that the mechanism for plugin discovery used by pytest
|
||||
(setupttools entry points) doesn't work with frozen executables so pytest
|
||||
can't find any third party plugins automatically. To include third party plugins
|
||||
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# contents of app_main.py
|
||||
import sys
|
||||
import pytest_timeout # Third party plugin
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--pytest':
|
||||
import pytest
|
||||
sys.exit(pytest.main(sys.argv[2:]))
|
||||
sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
|
||||
else:
|
||||
# normal application execution: at this point argv can be parsed
|
||||
# by your argument-parsing library of choice as usual
|
||||
@@ -845,3 +850,4 @@ This allows you to execute tests using the frozen
|
||||
application with standard ``pytest`` command-line options::
|
||||
|
||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
||||
|
||||
|
||||
@@ -68,5 +68,5 @@ If you run this without output capturing::
|
||||
.test_method1 called
|
||||
.test other
|
||||
.test_unit1 method called
|
||||
. [100%]
|
||||
.
|
||||
4 passed in 0.12 seconds
|
||||
|
||||
@@ -286,7 +286,7 @@ tests.
|
||||
Let's execute it::
|
||||
|
||||
$ pytest -s -q --tb=no
|
||||
FF [100%]teardown smtp
|
||||
FFteardown smtp
|
||||
|
||||
2 failed in 0.12 seconds
|
||||
|
||||
@@ -391,7 +391,7 @@ We use the ``request.module`` attribute to optionally obtain an
|
||||
again, nothing much has changed::
|
||||
|
||||
$ pytest -s -q --tb=no
|
||||
FF [100%]finalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
|
||||
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
|
||||
|
||||
2 failed in 0.12 seconds
|
||||
|
||||
@@ -612,7 +612,7 @@ Here we declare an ``app`` fixture which receives the previously defined
|
||||
$ pytest -v test_appsetup.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
@@ -681,40 +681,40 @@ Let's run the tests in verbose mode and with looking at the print-output::
|
||||
$ pytest -v -s test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 8 items
|
||||
|
||||
test_module.py::test_0[1] SETUP otherarg 1
|
||||
RUN test0 with otherarg 1
|
||||
PASSED [ 12%] TEARDOWN otherarg 1
|
||||
PASSED TEARDOWN otherarg 1
|
||||
|
||||
test_module.py::test_0[2] SETUP otherarg 2
|
||||
RUN test0 with otherarg 2
|
||||
PASSED [ 25%] TEARDOWN otherarg 2
|
||||
PASSED TEARDOWN otherarg 2
|
||||
|
||||
test_module.py::test_1[mod1] SETUP modarg mod1
|
||||
RUN test1 with modarg mod1
|
||||
PASSED [ 37%]
|
||||
PASSED
|
||||
test_module.py::test_2[1-mod1] SETUP otherarg 1
|
||||
RUN test2 with otherarg 1 and modarg mod1
|
||||
PASSED [ 50%] TEARDOWN otherarg 1
|
||||
PASSED TEARDOWN otherarg 1
|
||||
|
||||
test_module.py::test_2[2-mod1] SETUP otherarg 2
|
||||
RUN test2 with otherarg 2 and modarg mod1
|
||||
PASSED [ 62%] TEARDOWN otherarg 2
|
||||
PASSED TEARDOWN otherarg 2
|
||||
|
||||
test_module.py::test_1[mod2] TEARDOWN modarg mod1
|
||||
SETUP modarg mod2
|
||||
RUN test1 with modarg mod2
|
||||
PASSED [ 75%]
|
||||
PASSED
|
||||
test_module.py::test_2[1-mod2] SETUP otherarg 1
|
||||
RUN test2 with otherarg 1 and modarg mod2
|
||||
PASSED [ 87%] TEARDOWN otherarg 1
|
||||
PASSED TEARDOWN otherarg 1
|
||||
|
||||
test_module.py::test_2[2-mod2] SETUP otherarg 2
|
||||
RUN test2 with otherarg 2 and modarg mod2
|
||||
PASSED [100%] TEARDOWN otherarg 2
|
||||
PASSED TEARDOWN otherarg 2
|
||||
TEARDOWN modarg mod2
|
||||
|
||||
|
||||
|
||||
@@ -7,32 +7,34 @@ Installation and Getting Started
|
||||
|
||||
**PyPI package name**: `pytest <http://pypi.python.org/pypi/pytest>`_
|
||||
|
||||
**dependencies**: `py <http://pypi.python.org/pypi/py>`_,
|
||||
**Dependencies**: `py <http://pypi.python.org/pypi/py>`_,
|
||||
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
|
||||
|
||||
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
|
||||
``pytest`` is a framework that makes building simple and scalable tests easy. Tests are expressive and readable—no boilerplate code required. Get started in minutes with a small unit test or complex functional test for your application or library.
|
||||
|
||||
.. _`getstarted`:
|
||||
.. _installation:
|
||||
.. _`installation`:
|
||||
|
||||
Installation
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
Installation::
|
||||
1. Run the following command in your command line::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
To check your installation has installed the correct version::
|
||||
2. Check that you installed the correct version::
|
||||
|
||||
$ pytest --version
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.5/site-packages/pytest.py
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
Our first test run
|
||||
Create your first test
|
||||
----------------------------------------------------------
|
||||
|
||||
Let's create a first test file with a simple test function::
|
||||
Create a simple test function with just four lines of code::
|
||||
|
||||
# content of test_sample.py
|
||||
def func(x):
|
||||
@@ -41,7 +43,7 @@ Let's create a first test file with a simple test function::
|
||||
def test_answer():
|
||||
assert func(3) == 5
|
||||
|
||||
That's it. You can execute the test function now::
|
||||
That’s it. You can now execute the test function::
|
||||
|
||||
$ pytest
|
||||
=========================== test session starts ============================
|
||||
@@ -62,30 +64,22 @@ That's it. You can execute the test function now::
|
||||
test_sample.py:5: AssertionError
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
We got a failure report because our little ``func(3)`` call did not return ``5``.
|
||||
This test returns a failure report because ``func(3)`` does not return ``5``.
|
||||
|
||||
.. note::
|
||||
|
||||
You can simply use the ``assert`` statement for asserting test
|
||||
expectations. pytest's :ref:`assert introspection` will intelligently
|
||||
report intermediate values of the assert expression freeing
|
||||
you from the need to learn the many names of `JUnit legacy methods`_.
|
||||
You can use the ``assert`` statement to verify test expectations. pytest’s `Advanced assertion introspection <http://docs.python.org/reference/simple_stmts.html#the-assert-statement>`_ will intelligently report intermediate values of the assert expression so you can avoid the many names `of JUnit legacy methods <http://docs.python.org/library/unittest.html#test-cases>`_.
|
||||
|
||||
.. _`JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases
|
||||
|
||||
.. _`assert statement`: http://docs.python.org/reference/simple_stmts.html#the-assert-statement
|
||||
|
||||
Running multiple tests
|
||||
Run multiple tests
|
||||
----------------------------------------------------------
|
||||
|
||||
``pytest`` will run all files in the current directory and its subdirectories of the form test_*.py or \*_test.py. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
|
||||
``pytest`` will run all files of the form test_*.py or \*_test.py in the current directory and its subdirectories. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
|
||||
|
||||
|
||||
Asserting that a certain exception is raised
|
||||
Assert that a certain exception is raised
|
||||
--------------------------------------------------------------
|
||||
|
||||
If you want to assert that some code raises an exception you can
|
||||
use the ``raises`` helper::
|
||||
Use the ``raises`` helper to assert that some code raises an exception::
|
||||
|
||||
# content of test_sysexit.py
|
||||
import pytest
|
||||
@@ -96,18 +90,16 @@ use the ``raises`` helper::
|
||||
with pytest.raises(SystemExit):
|
||||
f()
|
||||
|
||||
Running it with, this time in "quiet" reporting mode::
|
||||
Execute the test function with “quiet” reporting mode::
|
||||
|
||||
$ pytest -q test_sysexit.py
|
||||
. [100%]
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
Grouping multiple tests in a class
|
||||
Group multiple tests in a class
|
||||
--------------------------------------------------------------
|
||||
|
||||
Once you start to have more than a few tests it often makes sense
|
||||
to group tests logically, in classes and modules. Let's write a class
|
||||
containing two tests::
|
||||
Once you develop multiple tests, you may want to group them into a class. pytest makes it easy to create a class containing more than one test::
|
||||
|
||||
# content of test_class.py
|
||||
class TestClass(object):
|
||||
@@ -119,9 +111,7 @@ containing two tests::
|
||||
x = "hello"
|
||||
assert hasattr(x, 'check')
|
||||
|
||||
The two tests are found because of the standard :ref:`test discovery`.
|
||||
There is no need to subclass anything. We can simply
|
||||
run the module by passing its filename::
|
||||
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename::
|
||||
|
||||
$ pytest -q test_class.py
|
||||
.F [100%]
|
||||
@@ -139,26 +129,19 @@ run the module by passing its filename::
|
||||
test_class.py:8: AssertionError
|
||||
1 failed, 1 passed in 0.12 seconds
|
||||
|
||||
The first test passed, the second failed. Again we can easily see
|
||||
the intermediate values used in the assertion, helping us to
|
||||
understand the reason for the failure.
|
||||
The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure.
|
||||
|
||||
Going functional: requesting a unique temporary directory
|
||||
Request a unique temporary directory for functional tests
|
||||
--------------------------------------------------------------
|
||||
|
||||
For functional tests one often needs to create some files
|
||||
and pass them to application objects. pytest provides
|
||||
:ref:`builtinfixtures` which allow to request arbitrary
|
||||
resources, for example a unique temporary directory::
|
||||
``pytest`` provides `Builtin fixtures/function arguments <https://docs.pytest.org/en/latest/builtin.html#builtinfixtures>`_ to request arbitrary resources, like a unique temporary directory::
|
||||
|
||||
# content of test_tmpdir.py
|
||||
def test_needsfiles(tmpdir):
|
||||
print (tmpdir)
|
||||
assert 0
|
||||
|
||||
We list the name ``tmpdir`` in the test function signature and
|
||||
``pytest`` will lookup and call a fixture factory to create the resource
|
||||
before performing the test function call. Let's just run it::
|
||||
List the name ``tmpdir`` in the test function signature and ``pytest`` will lookup and call a fixture factory to create the resource before performing the test function call. Before the test runs, ``pytest`` creates a unique-per-test-invocation temporary directory::
|
||||
|
||||
$ pytest -q test_tmpdir.py
|
||||
F [100%]
|
||||
@@ -177,22 +160,21 @@ before performing the test function call. Let's just run it::
|
||||
PYTEST_TMPDIR/test_needsfiles0
|
||||
1 failed in 0.12 seconds
|
||||
|
||||
Before the test runs, a unique-per-test-invocation temporary directory
|
||||
was created. More info at :ref:`tmpdir handling`.
|
||||
More info on tmpdir handling is available at `Temporary directories and files <tmpdir handling>`_.
|
||||
|
||||
You can find out what kind of builtin :ref:`fixtures` exist by typing::
|
||||
Find out what kind of builtin ```pytest`` fixtures <fixtures>`_ exist with the command::
|
||||
|
||||
pytest --fixtures # shows builtin and custom fixtures
|
||||
|
||||
Where to go next
|
||||
Continue reading
|
||||
-------------------------------------
|
||||
|
||||
Here are a few suggestions where to go next:
|
||||
Check out additional pytest resources to help you customize tests for your unique workflow:
|
||||
|
||||
* :ref:`cmdline` for command line invocation examples
|
||||
* :ref:`good practices <goodpractices>` for virtualenv, test layout
|
||||
* :ref:`existingtestsuite` for working with pre-existing tests
|
||||
* :ref:`fixtures` for providing a functional baseline to your tests
|
||||
* :ref:`plugins` managing and writing plugins
|
||||
* ":ref:`cmdline`" for command line invocation examples
|
||||
* ":ref:`goodpractices`" for virtualenv and test layouts
|
||||
* ":ref:`existingtestsuite`" for working with pre-existing tests
|
||||
* ":ref:`fixtures`" for providing a functional baseline to your tests
|
||||
* ":ref:`plugins`" for managing and writing plugins
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -3,24 +3,11 @@
|
||||
Logging
|
||||
-------
|
||||
|
||||
.. versionadded 3.3.0
|
||||
.. versionadded:: 3.3
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
.. note::
|
||||
|
||||
This feature is a drop-in replacement for the `pytest-catchlog
|
||||
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they will conflict
|
||||
with each other. The backward compatibility API with ``pytest-capturelog``
|
||||
has been dropped when this feature was introduced, so if for that reason you
|
||||
still need ``pytest-catchlog`` you can disable the internal feature by
|
||||
adding to your ``pytest.ini``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts=-p no:logging
|
||||
|
||||
Log messages are captured by default and for each failed test will be shown in
|
||||
the same manner as captured stdout and stderr.
|
||||
pytest captures log messages of level ``WARNING`` or above automatically and displays them in their own section
|
||||
for each failed test in the same manner as captured stdout and stderr.
|
||||
|
||||
Running without options::
|
||||
|
||||
@@ -29,7 +16,7 @@ Running without options::
|
||||
Shows failed tests like so::
|
||||
|
||||
----------------------- Captured stdlog call ----------------------
|
||||
test_reporting.py 26 INFO text going to logger
|
||||
test_reporting.py 26 WARNING text going to logger
|
||||
----------------------- Captured stdout call ----------------------
|
||||
text going to stdout
|
||||
----------------------- Captured stderr call ----------------------
|
||||
@@ -37,11 +24,10 @@ Shows failed tests like so::
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
By default each captured log message shows the module, line number, log level
|
||||
and message. Showing the exact module and line number is useful for testing and
|
||||
debugging. If desired the log format and date format can be specified to
|
||||
anything that the logging module supports.
|
||||
and message.
|
||||
|
||||
Running pytest specifying formatting options::
|
||||
If desired the log and date format can be specified to
|
||||
anything that the logging module supports by passing specific formatting options::
|
||||
|
||||
pytest --log-format="%(asctime)s %(levelname)s %(message)s" \
|
||||
--log-date-format="%Y-%m-%d %H:%M:%S"
|
||||
@@ -49,14 +35,14 @@ Running pytest specifying formatting options::
|
||||
Shows failed tests like so::
|
||||
|
||||
----------------------- Captured stdlog call ----------------------
|
||||
2010-04-10 14:48:44 INFO text going to logger
|
||||
2010-04-10 14:48:44 WARNING text going to logger
|
||||
----------------------- Captured stdout call ----------------------
|
||||
text going to stdout
|
||||
----------------------- Captured stderr call ----------------------
|
||||
text going to stderr
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
These options can also be customized through a configuration file:
|
||||
These options can also be customized through ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -69,7 +55,7 @@ with::
|
||||
|
||||
pytest --no-print-logs
|
||||
|
||||
Or in you ``pytest.ini``:
|
||||
Or in the ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -85,6 +71,10 @@ Shows failed tests in the normal manner as no logs were captured::
|
||||
text going to stderr
|
||||
==================== 2 failed in 0.02 seconds =====================
|
||||
|
||||
|
||||
caplog fixture
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Inside tests it is possible to change the log level for the captured log
|
||||
messages. This is supported by the ``caplog`` fixture::
|
||||
|
||||
@@ -92,7 +82,7 @@ messages. This is supported by the ``caplog`` fixture::
|
||||
caplog.set_level(logging.INFO)
|
||||
pass
|
||||
|
||||
By default the level is set on the handler used to catch the log messages,
|
||||
By default the level is set on the root logger,
|
||||
however as a convenience it is also possible to set the log level of any
|
||||
logger::
|
||||
|
||||
@@ -100,14 +90,16 @@ logger::
|
||||
caplog.set_level(logging.CRITICAL, logger='root.baz')
|
||||
pass
|
||||
|
||||
The log levels set are restored automatically at the end of the test.
|
||||
|
||||
It is also possible to use a context manager to temporarily change the log
|
||||
level::
|
||||
level inside a ``with`` block::
|
||||
|
||||
def test_bar(caplog):
|
||||
with caplog.at_level(logging.INFO):
|
||||
pass
|
||||
|
||||
Again, by default the level of the handler is affected but the level of any
|
||||
Again, by default the level of the root logger is affected but the level of any
|
||||
logger can be changed instead with::
|
||||
|
||||
def test_bar(caplog):
|
||||
@@ -115,7 +107,7 @@ logger can be changed instead with::
|
||||
pass
|
||||
|
||||
Lastly all the logs sent to the logger during the test run are made available on
|
||||
the fixture in the form of both the LogRecord instances and the final log text.
|
||||
the fixture in the form of both the ``logging.LogRecord`` instances and the final log text.
|
||||
This is useful for when you want to assert on the contents of a message::
|
||||
|
||||
def test_baz(caplog):
|
||||
@@ -146,12 +138,41 @@ You can call ``caplog.clear()`` to reset the captured log records in a test::
|
||||
your_test_method()
|
||||
assert ['Foo'] == [rec.message for rec in caplog.records]
|
||||
|
||||
|
||||
The ``caplop.records`` attribute contains records from the current stage only, so
|
||||
inside the ``setup`` phase it contains only setup logs, same with the ``call`` and
|
||||
``teardown`` phases.
|
||||
|
||||
To access logs from other stages, use the ``caplog.get_records(when)`` method. As an example,
|
||||
if you want to make sure that tests which use a certain fixture never log any warnings, you can inspect
|
||||
the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def window(caplog):
|
||||
window = create_window()
|
||||
yield window
|
||||
for when in ('setup', 'call'):
|
||||
messages = [x.message for x in caplog.get_records(when) if x.level == logging.WARNING]
|
||||
if messages:
|
||||
pytest.fail('warning messages encountered during testing: {}'.format(messages))
|
||||
|
||||
|
||||
caplog fixture API
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.logging.LogCaptureFixture
|
||||
:members:
|
||||
|
||||
.. _live_logs:
|
||||
|
||||
Live Logs
|
||||
^^^^^^^^^
|
||||
|
||||
By default, pytest will output any logging records with a level higher or
|
||||
equal to WARNING. In order to actually see these logs in the console you have to
|
||||
disable pytest output capture by passing ``-s``.
|
||||
By setting the :confval:`log_cli` configuration option to ``true``, pytest will output
|
||||
logging records as they are emitted directly into the console.
|
||||
|
||||
You can specify the logging level for which log records with equal or higher
|
||||
level are printed to the console by passing ``--log-cli-level``. This setting
|
||||
@@ -190,3 +211,49 @@ option names are:
|
||||
* ``log_file_level``
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
.. _log_release_notes:
|
||||
|
||||
Release notes
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
This feature was introduced as a drop-in replacement for the `pytest-catchlog
|
||||
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict
|
||||
with each other. The backward compatibility API with ``pytest-capturelog``
|
||||
has been dropped when this feature was introduced, so if for that reason you
|
||||
still need ``pytest-catchlog`` you can disable the internal feature by
|
||||
adding to your ``pytest.ini``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts=-p no:logging
|
||||
|
||||
|
||||
.. _log_changes_3_4:
|
||||
|
||||
Incompatible changes in pytest 3.4
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This feature was introduced in ``3.3`` and some **incompatible changes** have been
|
||||
made in ``3.4`` after community feedback:
|
||||
|
||||
* Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration
|
||||
or ``--log-level`` command-line options. This allows users to configure logger objects themselves.
|
||||
* :ref:`Live Logs <live_logs>` is now disabled by default and can be enabled setting the
|
||||
:confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each
|
||||
test is visible.
|
||||
* :ref:`Live Logs <live_logs>` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option
|
||||
to work.
|
||||
|
||||
If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini``
|
||||
file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
log_cli=true
|
||||
log_level=NOTSET
|
||||
|
||||
More details about the discussion that lead to this changes can be read in
|
||||
issue `#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_.
|
||||
|
||||
@@ -123,8 +123,8 @@ To get all combinations of multiple parametrized arguments you can stack
|
||||
def test_foo(x, y):
|
||||
pass
|
||||
|
||||
This will run the test with the arguments set to ``x=0/y=2``, ``x=0/y=3``, ``x=1/y=2`` and
|
||||
``x=1/y=3``.
|
||||
This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``,
|
||||
``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators.
|
||||
|
||||
.. _`pytest_generate_tests`:
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
|
||||
* `mwlib <http://pypi.python.org/pypi/mwlib>`_ mediawiki parser and utility library
|
||||
* `The Translate Toolkit <http://translate.sourceforge.net/wiki/toolkit/index>`_ for localization and conversion
|
||||
* `execnet <http://codespeak.net/execnet>`_ rapid multi-Python deployment
|
||||
* `pylib <http://py.rtfd.org>`_ cross-platform path, IO, dynamic code library
|
||||
* `pylib <https://py.readthedocs.io>`_ cross-platform path, IO, dynamic code library
|
||||
* `Pacha <http://pacha.cafepais.com/>`_ configuration management in five minutes
|
||||
* `bbfreeze <http://pypi.python.org/pypi/bbfreeze>`_ create standalone executables from Python scripts
|
||||
* `pdb++ <http://bitbucket.org/antocuni/pdb>`_ a fancier version of PDB
|
||||
|
||||
@@ -106,6 +106,4 @@ When distributing tests on the local machine, ``pytest`` takes care to
|
||||
configure a basetemp directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run basetemp directory.
|
||||
|
||||
.. _`py.path.local`: http://py.rtfd.org/en/latest/path.html
|
||||
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
@@ -256,6 +256,66 @@ This will add an extra property ``example_key="1"`` to the generated
|
||||
Also please note that using this feature will break any schema verification.
|
||||
This might be a problem when used with some CI servers.
|
||||
|
||||
record_xml_attribute
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
To add an additional xml attribute to a testcase element, you can use
|
||||
``record_xml_attribute`` fixture. This can also be used to override existing values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_function(record_xml_attribute):
|
||||
record_xml_attribute("assertions", "REQ-1234")
|
||||
record_xml_attribute("classname", "custom_classname")
|
||||
print('hello world')
|
||||
assert True
|
||||
|
||||
Unlike ``record_xml_property``, this will not add a new child element.
|
||||
Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated
|
||||
``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
|
||||
<system-out>
|
||||
hello world
|
||||
</system-out>
|
||||
</testcase>
|
||||
|
||||
.. warning::
|
||||
|
||||
``record_xml_attribute`` is an experimental feature, and its interface might be replaced
|
||||
by something more powerful and general in future versions. The
|
||||
functionality per-se will be kept, however.
|
||||
|
||||
Using this over ``record_xml_property`` can help when using ci tools to parse the xml report.
|
||||
However, some parsers are quite strict about the elements and attributes that are allowed.
|
||||
Many tools use an xsd schema (like the example below) to validate incoming xml.
|
||||
Make sure you are using attribute names that are allowed by your parser.
|
||||
|
||||
Below is the Scheme used by Jenkins to validate the XML report:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<xs:element name="testcase">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
<xs:attribute name="assertions" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="time" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="classname" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="status" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
LogXML: add_global_property
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -389,4 +449,14 @@ hook was invoked::
|
||||
*** test run reporting finishing
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Calling ``pytest.main()`` will result in importing your tests and any modules
|
||||
that they import. Due to the caching mechanism of python's import system,
|
||||
making subsequent calls to ``pytest.main()`` from the same process will not
|
||||
reflect changes to those files between the calls. For this reason, making
|
||||
multiple calls to ``pytest.main()`` from the same process (in order to re-run
|
||||
tests, for example) is not recommended.
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -112,6 +112,12 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
|
||||
pytestmark = pytest.mark.filterwarnings('error')
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Except for these features, pytest does not change the python warning filter; it only captures
|
||||
and displays the warnings which are issued with respect to the currently configured filter,
|
||||
including changes to the filter made by test functions or by the system under test.
|
||||
|
||||
.. note::
|
||||
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are hidden by the standard library
|
||||
|
||||
@@ -583,11 +583,22 @@ pytest hook reference
|
||||
Initialization, command line and configuration hooks
|
||||
----------------------------------------------------
|
||||
|
||||
Bootstrapping hooks
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
|
||||
|
||||
.. autofunction:: pytest_load_initial_conftests
|
||||
.. autofunction:: pytest_cmdline_preparse
|
||||
.. autofunction:: pytest_cmdline_parse
|
||||
.. autofunction:: pytest_addoption
|
||||
.. autofunction:: pytest_cmdline_main
|
||||
|
||||
Initialization hooks
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Initialization hooks called for plugins and ``conftest.py`` files.
|
||||
|
||||
.. autofunction:: pytest_addoption
|
||||
.. autofunction:: pytest_configure
|
||||
.. autofunction:: pytest_unconfigure
|
||||
|
||||
@@ -596,7 +607,10 @@ Generic "runtest" hooks
|
||||
|
||||
All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object.
|
||||
|
||||
.. autofunction:: pytest_runtestloop
|
||||
.. autofunction:: pytest_runtest_protocol
|
||||
.. autofunction:: pytest_runtest_logstart
|
||||
.. autofunction:: pytest_runtest_logfinish
|
||||
.. autofunction:: pytest_runtest_setup
|
||||
.. autofunction:: pytest_runtest_call
|
||||
.. autofunction:: pytest_runtest_teardown
|
||||
@@ -616,6 +630,7 @@ Collection hooks
|
||||
|
||||
``pytest`` calls the following hooks for collecting files and directories:
|
||||
|
||||
.. autofunction:: pytest_collection
|
||||
.. autofunction:: pytest_ignore_collect
|
||||
.. autofunction:: pytest_collect_directory
|
||||
.. autofunction:: pytest_collect_file
|
||||
@@ -680,14 +695,22 @@ Reference of objects involved in hooks
|
||||
.. autoclass:: _pytest.config.Parser()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.main.Node()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.main.Collector()
|
||||
.. autoclass:: _pytest.nodes.Collector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Item()
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.main.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: _pytest.nodes.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import py
|
||||
import textwrap
|
||||
|
||||
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ from _pytest.debugging import pytestPDB as __pytestPDB
|
||||
from _pytest.recwarn import warns, deprecated_call
|
||||
from _pytest.outcomes import fail, skip, importorskip, exit, xfail
|
||||
from _pytest.mark import MARK_GEN as mark, param
|
||||
from _pytest.main import Item, Collector, File, Session
|
||||
from _pytest.main import Session
|
||||
from _pytest.nodes import Item, Collector, File
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.python import (
|
||||
Module, Class, Instance, Function, Generator,
|
||||
|
||||
27
setup.py
27
setup.py
@@ -23,23 +23,34 @@ with open('README.rst') as fd:
|
||||
long_description = fd.read()
|
||||
|
||||
|
||||
def has_environment_marker_support():
|
||||
def get_environment_marker_support_level():
|
||||
"""
|
||||
Tests that setuptools has support for PEP-426 environment marker support.
|
||||
Tests how well setuptools supports PEP-426 environment marker.
|
||||
|
||||
The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2
|
||||
so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142
|
||||
so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350
|
||||
|
||||
The support is later enhanced to allow direct conditional inclusions inside install_requires,
|
||||
which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and
|
||||
again worked since 36.2.2, so we're using that. See:
|
||||
https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2
|
||||
https://github.com/pypa/setuptools/issues/1099
|
||||
|
||||
References:
|
||||
|
||||
* https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies
|
||||
* https://www.python.org/dev/peps/pep-0426/#environment-markers
|
||||
* https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
|
||||
"""
|
||||
try:
|
||||
return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7.2')
|
||||
version = pkg_resources.parse_version(setuptools.__version__)
|
||||
if version >= pkg_resources.parse_version('36.2.2'):
|
||||
return 2
|
||||
if version >= pkg_resources.parse_version('0.7.2'):
|
||||
return 1
|
||||
except Exception as exc:
|
||||
sys.stderr.write("Could not test setuptool's version: %s\n" % exc)
|
||||
return False
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
@@ -54,7 +65,11 @@ def main():
|
||||
# used by tox.ini to test with pluggy master
|
||||
if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ:
|
||||
install_requires.append('pluggy>=0.5,<0.7')
|
||||
if has_environment_marker_support():
|
||||
environment_marker_support_level = get_environment_marker_support_level()
|
||||
if environment_marker_support_level >= 2:
|
||||
install_requires.append('funcsigs;python_version<"3.0"')
|
||||
install_requires.append('colorama;sys_platform=="win32"')
|
||||
elif environment_marker_support_level == 1:
|
||||
extras_require[':python_version<"3.0"'] = ['funcsigs']
|
||||
extras_require[':sys_platform=="win32"'] = ['colorama']
|
||||
else:
|
||||
|
||||
@@ -151,7 +151,7 @@ def publish_release(ctx, version, user, pypi_name):
|
||||
|
||||
@invoke.task(help={
|
||||
'version': 'version being released',
|
||||
'write_out': 'write changes to the actial changelog'
|
||||
'write_out': 'write changes to the actual changelog'
|
||||
})
|
||||
def changelog(ctx, version, write_out=False):
|
||||
if write_out:
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import six
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
@@ -215,8 +218,8 @@ class TestGeneralUsage(object):
|
||||
assert not result.ret
|
||||
|
||||
def test_issue109_sibling_conftests_not_loaded(self, testdir):
|
||||
sub1 = testdir.tmpdir.mkdir("sub1")
|
||||
sub2 = testdir.tmpdir.mkdir("sub2")
|
||||
sub1 = testdir.mkdir("sub1")
|
||||
sub2 = testdir.mkdir("sub2")
|
||||
sub1.join("conftest.py").write("assert 0")
|
||||
result = testdir.runpytest(sub2)
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
@@ -396,7 +399,7 @@ class TestGeneralUsage(object):
|
||||
|
||||
p = tmpdir.join('test_test_plugins_given_as_strings.py')
|
||||
p.write('def test_foo(): pass')
|
||||
mod = py.std.types.ModuleType("myplugin")
|
||||
mod = types.ModuleType("myplugin")
|
||||
monkeypatch.setitem(sys.modules, 'myplugin', mod)
|
||||
assert pytest.main(args=[str(tmpdir)], plugins=['myplugin']) == 0
|
||||
|
||||
@@ -490,17 +493,17 @@ class TestInvocationVariants(object):
|
||||
|
||||
def test_python_minus_m_invocation_ok(self, testdir):
|
||||
p1 = testdir.makepyfile("def test_hello(): pass")
|
||||
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
|
||||
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
|
||||
assert res.ret == 0
|
||||
|
||||
def test_python_minus_m_invocation_fail(self, testdir):
|
||||
p1 = testdir.makepyfile("def test_fail(): 0/0")
|
||||
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
|
||||
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
|
||||
assert res.ret == 1
|
||||
|
||||
def test_python_pytest_package(self, testdir):
|
||||
p1 = testdir.makepyfile("def test_pass(): pass")
|
||||
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
|
||||
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
|
||||
assert res.ret == 0
|
||||
res.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
@@ -533,7 +536,7 @@ class TestInvocationVariants(object):
|
||||
path = testdir.mkpydir("tpkg")
|
||||
path.join("test_hello.py").write('raise ImportError')
|
||||
|
||||
result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello")
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
||||
assert result.ret != 0
|
||||
|
||||
result.stdout.fnmatch_lines([
|
||||
@@ -551,14 +554,14 @@ class TestInvocationVariants(object):
|
||||
result.stdout.fnmatch_lines([
|
||||
"*2 passed*"
|
||||
])
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*1 passed*"
|
||||
])
|
||||
|
||||
def join_pythonpath(what):
|
||||
cur = py.std.os.environ.get('PYTHONPATH')
|
||||
cur = os.environ.get('PYTHONPATH')
|
||||
if cur:
|
||||
return str(what) + os.pathsep + cur
|
||||
return what
|
||||
@@ -575,7 +578,7 @@ class TestInvocationVariants(object):
|
||||
])
|
||||
|
||||
monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir))
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_missing")
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines([
|
||||
"*not*found*test_missing*",
|
||||
@@ -603,11 +606,11 @@ class TestInvocationVariants(object):
|
||||
# The structure of the test directory is now:
|
||||
# .
|
||||
# ├── hello
|
||||
# │ └── ns_pkg
|
||||
# │ ├── __init__.py
|
||||
# │ └── hello
|
||||
# │ ├── __init__.py
|
||||
# │ └── test_hello.py
|
||||
# │ └── ns_pkg
|
||||
# │ ├── __init__.py
|
||||
# │ └── hello
|
||||
# │ ├── __init__.py
|
||||
# │ └── test_hello.py
|
||||
# └── world
|
||||
# └── ns_pkg
|
||||
# ├── __init__.py
|
||||
@@ -616,7 +619,7 @@ class TestInvocationVariants(object):
|
||||
# └── test_world.py
|
||||
|
||||
def join_pythonpath(*dirs):
|
||||
cur = py.std.os.environ.get('PYTHONPATH')
|
||||
cur = os.environ.get('PYTHONPATH')
|
||||
if cur:
|
||||
dirs += (cur,)
|
||||
return os.pathsep.join(str(p) for p in dirs)
|
||||
@@ -624,10 +627,9 @@ class TestInvocationVariants(object):
|
||||
for p in search_path:
|
||||
monkeypatch.syspath_prepend(p)
|
||||
|
||||
os.chdir('world')
|
||||
# mixed module and filenames:
|
||||
os.chdir('world')
|
||||
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
|
||||
testdir.chdir()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_hello.py::test_hello*PASSED*",
|
||||
@@ -638,6 +640,7 @@ class TestInvocationVariants(object):
|
||||
])
|
||||
|
||||
# specify tests within a module
|
||||
testdir.chdir()
|
||||
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
@@ -645,6 +648,76 @@ class TestInvocationVariants(object):
|
||||
"*1 passed*"
|
||||
])
|
||||
|
||||
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
|
||||
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
|
||||
"""
|
||||
test --pyargs option with packages with path containing symlink can
|
||||
have conftest.py in their package (#2985)
|
||||
"""
|
||||
# dummy check that we can actually create symlinks: on Windows `os.symlink` is available,
|
||||
# but normal users require special admin privileges to create symlinks.
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
os.symlink(str(testdir.tmpdir.ensure('tmpfile')), str(testdir.tmpdir.join('tmpfile2')))
|
||||
except OSError as e:
|
||||
pytest.skip(six.text_type(e.args[0]))
|
||||
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)
|
||||
|
||||
search_path = ["lib", os.path.join("local", "lib")]
|
||||
|
||||
dirname = "lib"
|
||||
d = testdir.mkdir(dirname)
|
||||
foo = d.mkdir("foo")
|
||||
foo.ensure("__init__.py")
|
||||
lib = foo.mkdir('bar')
|
||||
lib.ensure("__init__.py")
|
||||
lib.join("test_bar.py"). \
|
||||
write("def test_bar(): pass\n"
|
||||
"def test_other(a_fixture):pass")
|
||||
lib.join("conftest.py"). \
|
||||
write("import pytest\n"
|
||||
"@pytest.fixture\n"
|
||||
"def a_fixture():pass")
|
||||
|
||||
d_local = testdir.mkdir("local")
|
||||
symlink_location = os.path.join(str(d_local), "lib")
|
||||
if six.PY2:
|
||||
os.symlink(str(d), symlink_location)
|
||||
else:
|
||||
os.symlink(str(d), symlink_location, target_is_directory=True)
|
||||
|
||||
# The structure of the test directory is now:
|
||||
# .
|
||||
# ├── local
|
||||
# │ └── lib -> ../lib
|
||||
# └── lib
|
||||
# └── foo
|
||||
# ├── __init__.py
|
||||
# └── bar
|
||||
# ├── __init__.py
|
||||
# ├── conftest.py
|
||||
# └── test_bar.py
|
||||
|
||||
def join_pythonpath(*dirs):
|
||||
cur = os.getenv('PYTHONPATH')
|
||||
if cur:
|
||||
dirs += (cur,)
|
||||
return os.pathsep.join(str(p) for p in dirs)
|
||||
|
||||
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
|
||||
for p in search_path:
|
||||
monkeypatch.syspath_prepend(p)
|
||||
|
||||
# module picked up in symlink-ed directory:
|
||||
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
|
||||
testdir.chdir()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*lib/foo/bar/test_bar.py::test_bar*PASSED*",
|
||||
"*lib/foo/bar/test_bar.py::test_other*PASSED*",
|
||||
"*2 passed*"
|
||||
])
|
||||
|
||||
def test_cmdline_python_package_not_exists(self, testdir):
|
||||
result = testdir.runpytest("--pyargs", "tpkgwhatv")
|
||||
assert result.ret
|
||||
@@ -829,7 +902,7 @@ def test_deferred_hook_checking(testdir):
|
||||
testdir.syspathinsert()
|
||||
testdir.makepyfile(**{
|
||||
'plugin.py': """
|
||||
class Hooks:
|
||||
class Hooks(object):
|
||||
def pytest_my_hook(self, config):
|
||||
pass
|
||||
|
||||
@@ -848,3 +921,46 @@ def test_deferred_hook_checking(testdir):
|
||||
})
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(['* 1 passed *'])
|
||||
|
||||
|
||||
def test_fixture_values_leak(testdir):
|
||||
"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
|
||||
life-times (#2981).
|
||||
"""
|
||||
testdir.makepyfile("""
|
||||
import attr
|
||||
import gc
|
||||
import pytest
|
||||
import weakref
|
||||
|
||||
@attr.s
|
||||
class SomeObj(object):
|
||||
name = attr.ib()
|
||||
|
||||
fix_of_test1_ref = None
|
||||
session_ref = None
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def session_fix():
|
||||
global session_ref
|
||||
obj = SomeObj(name='session-fixture')
|
||||
session_ref = weakref.ref(obj)
|
||||
return obj
|
||||
|
||||
@pytest.fixture
|
||||
def fix(session_fix):
|
||||
global fix_of_test1_ref
|
||||
obj = SomeObj(name='local-fixture')
|
||||
fix_of_test1_ref = weakref.ref(obj)
|
||||
return obj
|
||||
|
||||
def test1(fix):
|
||||
assert fix_of_test1_ref() is fix
|
||||
|
||||
def test2():
|
||||
gc.collect()
|
||||
# fixture "fix" created during test1 must have been destroyed by now
|
||||
assert fix_of_test1_ref() is None
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(['* 2 passed *'])
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
import _pytest
|
||||
import py
|
||||
import pytest
|
||||
@@ -472,7 +474,7 @@ class TestFormattedExcinfo(object):
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
repr = pr.repr_excinfo(excinfo)
|
||||
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
||||
|
||||
def test_repr_many_line_source_not_existing(self):
|
||||
@@ -487,7 +489,7 @@ raise ValueError()
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
repr = pr.repr_excinfo(excinfo)
|
||||
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
||||
|
||||
def test_repr_source_failing_fullsource(self):
|
||||
@@ -542,16 +544,16 @@ raise ValueError()
|
||||
tb = FakeRawTB()
|
||||
excinfo.traceback = Traceback(tb)
|
||||
|
||||
fail = IOError() # noqa
|
||||
fail = IOError()
|
||||
repr = pr.repr_excinfo(excinfo)
|
||||
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
|
||||
|
||||
fail = py.error.ENOENT # noqa
|
||||
repr = pr.repr_excinfo(excinfo)
|
||||
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
|
||||
|
||||
def test_repr_local(self):
|
||||
@@ -738,7 +740,7 @@ raise ValueError()
|
||||
repr = p.repr_excinfo(excinfo)
|
||||
assert repr.reprtraceback
|
||||
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert repr.chain[0][0]
|
||||
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
|
||||
assert repr.reprcrash.path.endswith("mod.py")
|
||||
@@ -758,7 +760,7 @@ raise ValueError()
|
||||
def raiseos():
|
||||
raise OSError(2)
|
||||
|
||||
monkeypatch.setattr(py.std.os, 'getcwd', raiseos)
|
||||
monkeypatch.setattr(os, 'getcwd', raiseos)
|
||||
assert p._makepath(__file__) == __file__
|
||||
p.repr_traceback(excinfo)
|
||||
|
||||
@@ -816,10 +818,10 @@ raise ValueError()
|
||||
for style in ("short", "long", "no"):
|
||||
for showlocals in (True, False):
|
||||
repr = excinfo.getrepr(style=style, showlocals=showlocals)
|
||||
if py.std.sys.version_info[0] < 3:
|
||||
if sys.version_info[0] < 3:
|
||||
assert isinstance(repr, ReprExceptionInfo)
|
||||
assert repr.reprtraceback.style == style
|
||||
if py.std.sys.version_info[0] >= 3:
|
||||
if sys.version_info[0] >= 3:
|
||||
assert isinstance(repr, ExceptionChainRepr)
|
||||
for repr in repr.chain:
|
||||
assert repr[0].style == style
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
# disable flake check on this file because some constructs are strange
|
||||
# or redundant on purpose and can't be disable on a line-by-line basis
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from _pytest._code import Source
|
||||
from _pytest._code.source import _ast
|
||||
from _pytest._code.source import ast
|
||||
|
||||
if _ast is not None:
|
||||
astonly = pytest.mark.nothing
|
||||
else:
|
||||
astonly = pytest.mark.xfail("True", reason="only works with AST-compile")
|
||||
|
||||
astonly = pytest.mark.nothing
|
||||
failsonjython = pytest.mark.xfail("sys.platform.startswith('java')")
|
||||
|
||||
|
||||
@@ -190,9 +188,9 @@ class TestSourceParsingAndCompiling(object):
|
||||
def f():
|
||||
raise ValueError()
|
||||
""")
|
||||
source1 = py.std.inspect.getsource(co1)
|
||||
source1 = inspect.getsource(co1)
|
||||
assert 'KeyError' in source1
|
||||
source2 = py.std.inspect.getsource(co2)
|
||||
source2 = inspect.getsource(co2)
|
||||
assert 'ValueError' in source2
|
||||
|
||||
def test_getstatement(self):
|
||||
@@ -376,7 +374,6 @@ def test_deindent():
|
||||
c = '''while True:
|
||||
pass
|
||||
'''
|
||||
import inspect
|
||||
lines = deindent(inspect.getsource(f).splitlines())
|
||||
assert lines == ["def f():", " c = '''while True:", " pass", "'''"]
|
||||
|
||||
@@ -464,7 +461,7 @@ def test_getfslineno():
|
||||
|
||||
fspath, lineno = getfslineno(A)
|
||||
|
||||
_, A_lineno = py.std.inspect.findsource(A)
|
||||
_, A_lineno = inspect.findsource(A)
|
||||
assert fspath.basename == "test_source.py"
|
||||
assert lineno == A_lineno
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ def test_str_args_deprecated(tmpdir, testdir):
|
||||
warnings.append(message)
|
||||
|
||||
ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()])
|
||||
testdir.delete_loaded_modules()
|
||||
msg = ('passing a string to pytest.main() is deprecated, '
|
||||
'pass a list of arguments instead.')
|
||||
assert msg in warnings
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
sublogger = logging.getLogger(__name__ + '.baz')
|
||||
@@ -26,6 +27,30 @@ def test_change_level(caplog):
|
||||
assert 'CRITICAL' in caplog.text
|
||||
|
||||
|
||||
def test_change_level_undo(testdir):
|
||||
"""Ensure that 'set_level' is undone after the end of the test"""
|
||||
testdir.makepyfile('''
|
||||
import logging
|
||||
|
||||
def test1(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
logging.info('log from ' + 'test1')
|
||||
assert 0
|
||||
|
||||
def test2(caplog):
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
logging.info('log from ' + 'test2')
|
||||
assert 0
|
||||
''')
|
||||
result = testdir.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines([
|
||||
'*log from test1*',
|
||||
'*2 failed in *',
|
||||
])
|
||||
assert 'log from test2' not in result.stdout.str()
|
||||
|
||||
|
||||
def test_with_statement(caplog):
|
||||
with caplog.at_level(logging.INFO):
|
||||
logger.debug('handler DEBUG level')
|
||||
@@ -42,6 +67,7 @@ def test_with_statement(caplog):
|
||||
|
||||
|
||||
def test_log_access(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info('boo %s', 'arg')
|
||||
assert caplog.records[0].levelname == 'INFO'
|
||||
assert caplog.records[0].msg == 'boo %s'
|
||||
@@ -49,6 +75,7 @@ def test_log_access(caplog):
|
||||
|
||||
|
||||
def test_record_tuples(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info('boo %s', 'arg')
|
||||
|
||||
assert caplog.record_tuples == [
|
||||
@@ -57,6 +84,7 @@ def test_record_tuples(caplog):
|
||||
|
||||
|
||||
def test_unicode(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info(u'bū')
|
||||
assert caplog.records[0].levelname == 'INFO'
|
||||
assert caplog.records[0].msg == u'bū'
|
||||
@@ -64,7 +92,29 @@ def test_unicode(caplog):
|
||||
|
||||
|
||||
def test_clear(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info(u'bū')
|
||||
assert len(caplog.records)
|
||||
caplog.clear()
|
||||
assert not len(caplog.records)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logging_during_setup_and_teardown(caplog):
|
||||
caplog.set_level('INFO')
|
||||
logger.info('a_setup_log')
|
||||
yield
|
||||
logger.info('a_teardown_log')
|
||||
assert [x.message for x in caplog.get_records('teardown')] == ['a_teardown_log']
|
||||
|
||||
|
||||
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
|
||||
assert not caplog.records
|
||||
assert not caplog.get_records('call')
|
||||
logger.info('a_call_log')
|
||||
assert [x.message for x in caplog.get_records('call')] == ['a_call_log']
|
||||
|
||||
assert [x.message for x in caplog.get_records('setup')] == ['a_setup_log']
|
||||
|
||||
# This reachers into private API, don't use this type of thing in real tests!
|
||||
assert set(caplog._item.catch_log_handlers.keys()) == {'setup', 'call'}
|
||||
|
||||
29
testing/logging/test_formatter.py
Normal file
29
testing/logging/test_formatter.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
import py.io
|
||||
from _pytest.logging import ColoredLevelFormatter
|
||||
|
||||
|
||||
def test_coloredlogformatter():
|
||||
logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
|
||||
|
||||
record = logging.LogRecord(
|
||||
name='dummy', level=logging.INFO, pathname='dummypath', lineno=10,
|
||||
msg='Test Message', args=(), exc_info=False)
|
||||
|
||||
class ColorConfig(object):
|
||||
class option(object):
|
||||
pass
|
||||
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.hasmarkup = True
|
||||
formatter = ColoredLevelFormatter(tw, logfmt)
|
||||
output = formatter.format(record)
|
||||
assert output == ('dummypath 10 '
|
||||
'\x1b[32mINFO \x1b[0m Test Message')
|
||||
|
||||
tw.hasmarkup = False
|
||||
formatter = ColoredLevelFormatter(tw, logfmt)
|
||||
output = formatter.format(record)
|
||||
assert output == ('dummypath 10 '
|
||||
'INFO Test Message')
|
||||
@@ -1,5 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -35,7 +38,7 @@ def test_messages_logged(testdir):
|
||||
logger.info('text going to logger')
|
||||
assert False
|
||||
''')
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest('--log-level=INFO')
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(['*- Captured *log call -*',
|
||||
'*text going to logger*'])
|
||||
@@ -58,7 +61,7 @@ def test_setup_logging(testdir):
|
||||
logger.info('text going to logger from call')
|
||||
assert False
|
||||
''')
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest('--log-level=INFO')
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(['*- Captured *log setup -*',
|
||||
'*text going to logger from setup*',
|
||||
@@ -79,7 +82,7 @@ def test_teardown_logging(testdir):
|
||||
logger.info('text going to logger from teardown')
|
||||
assert False
|
||||
''')
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest('--log-level=INFO')
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(['*- Captured *log call -*',
|
||||
'*text going to logger from call*',
|
||||
@@ -141,6 +144,30 @@ def test_disable_log_capturing_ini(testdir):
|
||||
result.stdout.fnmatch_lines(['*- Captured *log call -*'])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('enabled', [True, False])
|
||||
def test_log_cli_enabled_disabled(testdir, enabled):
|
||||
msg = 'critical message logged by test'
|
||||
testdir.makepyfile('''
|
||||
import logging
|
||||
def test_log_cli():
|
||||
logging.critical("{}")
|
||||
'''.format(msg))
|
||||
if enabled:
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
log_cli=true
|
||||
''')
|
||||
result = testdir.runpytest()
|
||||
if enabled:
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_log_cli_enabled_disabled.py::test_log_cli ',
|
||||
'test_log_cli_enabled_disabled.py* CRITICAL critical message logged by test',
|
||||
'PASSED*',
|
||||
])
|
||||
else:
|
||||
assert msg not in result.stdout.str()
|
||||
|
||||
|
||||
def test_log_cli_default_level(testdir):
|
||||
# Default log file level
|
||||
testdir.makepyfile('''
|
||||
@@ -148,32 +175,103 @@ def test_log_cli_default_level(testdir):
|
||||
import logging
|
||||
def test_log_cli(request):
|
||||
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||
assert plugin.log_cli_handler.level == logging.WARNING
|
||||
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||
print('PASSED')
|
||||
assert plugin.log_cli_handler.level == logging.NOTSET
|
||||
logging.getLogger('catchlog').info("INFO message won't be shown")
|
||||
logging.getLogger('catchlog').warning("WARNING message will be shown")
|
||||
''')
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
log_cli=true
|
||||
''')
|
||||
|
||||
result = testdir.runpytest('-s')
|
||||
result = testdir.runpytest()
|
||||
|
||||
# fnmatch_lines does an assertion internally
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_log_cli_default_level.py PASSED',
|
||||
'test_log_cli_default_level.py::test_log_cli ',
|
||||
'test_log_cli_default_level.py*WARNING message will be shown*',
|
||||
])
|
||||
result.stderr.fnmatch_lines([
|
||||
"* This log message will be shown"
|
||||
])
|
||||
for line in result.errlines:
|
||||
try:
|
||||
assert "This log message won't be shown" in line
|
||||
pytest.fail("A log message was shown and it shouldn't have been")
|
||||
except AssertionError:
|
||||
continue
|
||||
|
||||
assert "INFO message won't be shown" not in result.stdout.str()
|
||||
# make sure that that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_log_cli_default_level_multiple_tests(testdir, request):
|
||||
"""Ensure we reset the first newline added by the live logger between tests"""
|
||||
filename = request.node.name + '.py'
|
||||
testdir.makepyfile('''
|
||||
import logging
|
||||
|
||||
def test_log_1():
|
||||
logging.warning("log message from test_log_1")
|
||||
|
||||
def test_log_2():
|
||||
logging.warning("log message from test_log_2")
|
||||
''')
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
log_cli=true
|
||||
''')
|
||||
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'{}::test_log_1 '.format(filename),
|
||||
'*WARNING*log message from test_log_1*',
|
||||
'PASSED *50%*',
|
||||
'{}::test_log_2 '.format(filename),
|
||||
'*WARNING*log message from test_log_2*',
|
||||
'PASSED *100%*',
|
||||
'=* 2 passed in *=',
|
||||
])
|
||||
|
||||
|
||||
def test_log_cli_default_level_sections(testdir, request):
|
||||
"""Check that with live logging enable we are printing the correct headers during setup/call/teardown."""
|
||||
filename = request.node.name + '.py'
|
||||
testdir.makepyfile('''
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
@pytest.fixture
|
||||
def fix(request):
|
||||
logging.warning("log message from setup of {}".format(request.node.name))
|
||||
yield
|
||||
logging.warning("log message from teardown of {}".format(request.node.name))
|
||||
|
||||
def test_log_1(fix):
|
||||
logging.warning("log message from test_log_1")
|
||||
|
||||
def test_log_2(fix):
|
||||
logging.warning("log message from test_log_2")
|
||||
''')
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
log_cli=true
|
||||
''')
|
||||
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'{}::test_log_1 '.format(filename),
|
||||
'*-- live log setup --*',
|
||||
'*WARNING*log message from setup of test_log_1*',
|
||||
'*-- live log call --*',
|
||||
'*WARNING*log message from test_log_1*',
|
||||
'PASSED *50%*',
|
||||
'*-- live log teardown --*',
|
||||
'*WARNING*log message from teardown of test_log_1*',
|
||||
|
||||
'{}::test_log_2 '.format(filename),
|
||||
'*-- live log setup --*',
|
||||
'*WARNING*log message from setup of test_log_2*',
|
||||
'*-- live log call --*',
|
||||
'*WARNING*log message from test_log_2*',
|
||||
'PASSED *100%*',
|
||||
'*-- live log teardown --*',
|
||||
'*WARNING*log message from teardown of test_log_2*',
|
||||
'=* 2 passed in *=',
|
||||
])
|
||||
|
||||
|
||||
def test_log_cli_level(testdir):
|
||||
# Default log file level
|
||||
testdir.makepyfile('''
|
||||
@@ -186,22 +284,19 @@ def test_log_cli_level(testdir):
|
||||
logging.getLogger('catchlog').info("This log message will be shown")
|
||||
print('PASSED')
|
||||
''')
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
log_cli=true
|
||||
''')
|
||||
|
||||
result = testdir.runpytest('-s', '--log-cli-level=INFO')
|
||||
|
||||
# fnmatch_lines does an assertion internally
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_log_cli_level.py PASSED',
|
||||
'test_log_cli_level.py*This log message will be shown',
|
||||
'PASSED', # 'PASSED' on its own line because the log message prints a new line
|
||||
])
|
||||
result.stderr.fnmatch_lines([
|
||||
"* This log message will be shown"
|
||||
])
|
||||
for line in result.errlines:
|
||||
try:
|
||||
assert "This log message won't be shown" in line
|
||||
pytest.fail("A log message was shown and it shouldn't have been")
|
||||
except AssertionError:
|
||||
continue
|
||||
assert "This log message won't be shown" not in result.stdout.str()
|
||||
|
||||
# make sure that that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
@@ -210,17 +305,10 @@ def test_log_cli_level(testdir):
|
||||
|
||||
# fnmatch_lines does an assertion internally
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_log_cli_level.py PASSED',
|
||||
'test_log_cli_level.py* This log message will be shown',
|
||||
'PASSED', # 'PASSED' on its own line because the log message prints a new line
|
||||
])
|
||||
result.stderr.fnmatch_lines([
|
||||
"* This log message will be shown"
|
||||
])
|
||||
for line in result.errlines:
|
||||
try:
|
||||
assert "This log message won't be shown" in line
|
||||
pytest.fail("A log message was shown and it shouldn't have been")
|
||||
except AssertionError:
|
||||
continue
|
||||
assert "This log message won't be shown" not in result.stdout.str()
|
||||
|
||||
# make sure that that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
@@ -230,6 +318,7 @@ def test_log_cli_ini_level(testdir):
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_cli=true
|
||||
log_cli_level = INFO
|
||||
""")
|
||||
testdir.makepyfile('''
|
||||
@@ -247,17 +336,10 @@ def test_log_cli_ini_level(testdir):
|
||||
|
||||
# fnmatch_lines does an assertion internally
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_log_cli_ini_level.py PASSED',
|
||||
'test_log_cli_ini_level.py* This log message will be shown',
|
||||
'PASSED', # 'PASSED' on its own line because the log message prints a new line
|
||||
])
|
||||
result.stderr.fnmatch_lines([
|
||||
"* This log message will be shown"
|
||||
])
|
||||
for line in result.errlines:
|
||||
try:
|
||||
assert "This log message won't be shown" in line
|
||||
pytest.fail("A log message was shown and it shouldn't have been")
|
||||
except AssertionError:
|
||||
continue
|
||||
assert "This log message won't be shown" not in result.stdout.str()
|
||||
|
||||
# make sure that that we get a '0' exit code for the testsuite
|
||||
assert result.ret == 0
|
||||
@@ -278,7 +360,7 @@ def test_log_file_cli(testdir):
|
||||
|
||||
log_file = testdir.tmpdir.join('pytest.log').strpath
|
||||
|
||||
result = testdir.runpytest('-s', '--log-file={0}'.format(log_file))
|
||||
result = testdir.runpytest('-s', '--log-file={0}'.format(log_file), '--log-file-level=WARNING')
|
||||
|
||||
# fnmatch_lines does an assertion internally
|
||||
result.stdout.fnmatch_lines([
|
||||
@@ -327,6 +409,16 @@ def test_log_file_cli_level(testdir):
|
||||
assert "This log message won't be shown" not in contents
|
||||
|
||||
|
||||
def test_log_level_not_changed_by_default(testdir):
|
||||
testdir.makepyfile('''
|
||||
import logging
|
||||
def test_log_file():
|
||||
assert logging.getLogger().level == logging.WARNING
|
||||
''')
|
||||
result = testdir.runpytest('-s')
|
||||
result.stdout.fnmatch_lines('* 1 passed in *')
|
||||
|
||||
|
||||
def test_log_file_ini(testdir):
|
||||
log_file = testdir.tmpdir.join('pytest.log').strpath
|
||||
|
||||
@@ -334,6 +426,7 @@ def test_log_file_ini(testdir):
|
||||
"""
|
||||
[pytest]
|
||||
log_file={0}
|
||||
log_file_level=WARNING
|
||||
""".format(log_file))
|
||||
testdir.makepyfile('''
|
||||
import pytest
|
||||
@@ -396,3 +489,53 @@ def test_log_file_ini_level(testdir):
|
||||
contents = rfh.read()
|
||||
assert "This log message will be shown" in contents
|
||||
assert "This log message won't be shown" not in contents
|
||||
|
||||
|
||||
@pytest.mark.parametrize('has_capture_manager', [True, False])
|
||||
def test_live_logging_suspends_capture(has_capture_manager, request):
|
||||
"""Test that capture manager is suspended when we emitting messages for live logging.
|
||||
|
||||
This tests the implementation calls instead of behavior because it is difficult/impossible to do it using
|
||||
``testdir`` facilities because they do their own capturing.
|
||||
|
||||
We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin
|
||||
is installed.
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
from _pytest.capture import CaptureManager
|
||||
from _pytest.logging import _LiveLoggingStreamHandler
|
||||
|
||||
class MockCaptureManager:
|
||||
calls = []
|
||||
|
||||
def suspend_global_capture(self):
|
||||
self.calls.append('suspend_global_capture')
|
||||
|
||||
def resume_global_capture(self):
|
||||
self.calls.append('resume_global_capture')
|
||||
|
||||
# sanity check
|
||||
assert CaptureManager.suspend_capture_item
|
||||
assert CaptureManager.resume_global_capture
|
||||
|
||||
class DummyTerminal(six.StringIO):
|
||||
|
||||
def section(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
out_file = DummyTerminal()
|
||||
capture_manager = MockCaptureManager() if has_capture_manager else None
|
||||
handler = _LiveLoggingStreamHandler(out_file, capture_manager)
|
||||
handler.set_when('call')
|
||||
|
||||
logger = logging.getLogger(__name__ + '.test_live_logging_suspends_capture')
|
||||
logger.addHandler(handler)
|
||||
request.addfinalizer(partial(logger.removeHandler, handler))
|
||||
|
||||
logger.critical('some message')
|
||||
if has_capture_manager:
|
||||
assert MockCaptureManager.calls == ['suspend_global_capture', 'resume_global_capture']
|
||||
else:
|
||||
assert MockCaptureManager.calls == []
|
||||
assert out_file.getvalue() == '\nsome message\n'
|
||||
|
||||
@@ -4,13 +4,9 @@ import sys
|
||||
from textwrap import dedent
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import (
|
||||
Collector,
|
||||
EXIT_NOTESTSCOLLECTED
|
||||
)
|
||||
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||
from _pytest.nodes import Collector
|
||||
|
||||
ignore_parametrized_marks = pytest.mark.filterwarnings('ignore:Applying marks directly to parameters')
|
||||
|
||||
@@ -25,7 +21,7 @@ class TestModule(object):
|
||||
b = testdir.mkdir("b")
|
||||
p = a.ensure("test_whatever.py")
|
||||
p.pyimport()
|
||||
del py.std.sys.modules['test_whatever']
|
||||
del sys.modules['test_whatever']
|
||||
b.ensure("test_whatever.py")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
@@ -754,7 +750,7 @@ class TestSorting(object):
|
||||
|
||||
assert fn1 == fn2
|
||||
assert fn1 != modcol
|
||||
if py.std.sys.version_info < (3, 0):
|
||||
if sys.version_info < (3, 0):
|
||||
assert cmp(fn1, fn2) == 0
|
||||
assert hash(fn1) == hash(fn2)
|
||||
|
||||
@@ -883,10 +879,10 @@ class TestConftestCustomization(object):
|
||||
import sys, os, imp
|
||||
from _pytest.python import Module
|
||||
|
||||
class Loader:
|
||||
class Loader(object):
|
||||
def load_module(self, name):
|
||||
return imp.load_source(name, name + ".narf")
|
||||
class Finder:
|
||||
class Finder(object):
|
||||
def find_module(self, name, path=None):
|
||||
if os.path.exists(name + ".narf"):
|
||||
return Loader()
|
||||
|
||||
@@ -2828,7 +2828,7 @@ class TestShowFixtures(object):
|
||||
def test_show_fixtures_indented_in_class(self, testdir):
|
||||
p = testdir.makepyfile(dedent('''
|
||||
import pytest
|
||||
class TestClass:
|
||||
class TestClass(object):
|
||||
@pytest.fixture
|
||||
def fixture1(self):
|
||||
"""line1
|
||||
|
||||
@@ -14,7 +14,7 @@ PY3 = sys.version_info >= (3, 0)
|
||||
|
||||
|
||||
class TestMetafunc(object):
|
||||
def Metafunc(self, func):
|
||||
def Metafunc(self, func, config=None):
|
||||
# the unit tests of this class check if things work correctly
|
||||
# on the funcarg level, so we don't need a full blown
|
||||
# initiliazation
|
||||
@@ -26,7 +26,7 @@ class TestMetafunc(object):
|
||||
|
||||
names = fixtures.getfuncargnames(func)
|
||||
fixtureinfo = FixtureInfo(names)
|
||||
return python.Metafunc(func, fixtureinfo, None)
|
||||
return python.Metafunc(func, fixtureinfo, config)
|
||||
|
||||
def test_no_funcargs(self, testdir):
|
||||
def function():
|
||||
@@ -156,7 +156,19 @@ class TestMetafunc(object):
|
||||
def test_parametrize_empty_list(self):
|
||||
def func(y):
|
||||
pass
|
||||
metafunc = self.Metafunc(func)
|
||||
|
||||
class MockConfig(object):
|
||||
def getini(self, name):
|
||||
return ''
|
||||
|
||||
@property
|
||||
def hook(self):
|
||||
return self
|
||||
|
||||
def pytest_make_parametrize_id(self, **kw):
|
||||
pass
|
||||
|
||||
metafunc = self.Metafunc(func, MockConfig())
|
||||
metafunc.parametrize("y", [])
|
||||
assert 'skip' == metafunc._calls[0].marks[0].name
|
||||
|
||||
@@ -235,6 +247,25 @@ class TestMetafunc(object):
|
||||
for val, expected in values:
|
||||
assert _idval(val, 'a', 6, None) == expected
|
||||
|
||||
def test_class_or_function_idval(self):
|
||||
"""unittest for the expected behavior to obtain ids for parametrized
|
||||
values that are classes or functions: their __name__.
|
||||
"""
|
||||
from _pytest.python import _idval
|
||||
|
||||
class TestClass(object):
|
||||
pass
|
||||
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
values = [
|
||||
(TestClass, "TestClass"),
|
||||
(test_function, "test_function"),
|
||||
]
|
||||
for val, expected in values:
|
||||
assert _idval(val, 'a', 6, None) == expected
|
||||
|
||||
@pytest.mark.issue250
|
||||
def test_idmaker_autoname(self):
|
||||
from _pytest.python import idmaker
|
||||
@@ -730,7 +761,7 @@ class TestMetafuncFunctional(object):
|
||||
def test_attributes(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
# assumes that generate/provide runs in the same process
|
||||
import py, pytest
|
||||
import sys, pytest
|
||||
def pytest_generate_tests(metafunc):
|
||||
metafunc.addcall(param=metafunc)
|
||||
|
||||
@@ -749,7 +780,7 @@ class TestMetafuncFunctional(object):
|
||||
def test_method(self, metafunc, pytestconfig):
|
||||
assert metafunc.config == pytestconfig
|
||||
assert metafunc.module.__name__ == __name__
|
||||
if py.std.sys.version_info > (3, 0):
|
||||
if sys.version_info > (3, 0):
|
||||
unbound = TestClass.test_method
|
||||
else:
|
||||
unbound = TestClass.test_method.im_func
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
# test for _argcomplete but not specific for any application
|
||||
@@ -23,21 +24,21 @@ def equal_with_bash(prefix, ffc, fc, out=None):
|
||||
|
||||
def _wrapcall(*args, **kargs):
|
||||
try:
|
||||
if py.std.sys.version_info > (2, 7):
|
||||
return py.std.subprocess.check_output(*args, **kargs).decode().splitlines()
|
||||
if sys.version_info > (2, 7):
|
||||
return subprocess.check_output(*args, **kargs).decode().splitlines()
|
||||
if 'stdout' in kargs:
|
||||
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||
process = py.std.subprocess.Popen(
|
||||
stdout=py.std.subprocess.PIPE, *args, **kargs)
|
||||
process = subprocess.Popen(
|
||||
stdout=subprocess.PIPE, *args, **kargs)
|
||||
output, unused_err = process.communicate()
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
cmd = kargs.get("args")
|
||||
if cmd is None:
|
||||
cmd = args[0]
|
||||
raise py.std.subprocess.CalledProcessError(retcode, cmd)
|
||||
raise subprocess.CalledProcessError(retcode, cmd)
|
||||
return output.decode().splitlines()
|
||||
except py.std.subprocess.CalledProcessError:
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
|
||||
@@ -83,7 +84,7 @@ class TestArgComplete(object):
|
||||
ffc = FastFilesCompleter()
|
||||
fc = FilesCompleter()
|
||||
for x in ['/', '/d', '/data', 'qqq', '']:
|
||||
assert equal_with_bash(x, ffc, fc, out=py.std.sys.stdout)
|
||||
assert equal_with_bash(x, ffc, fc, out=sys.stdout)
|
||||
|
||||
@pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
|
||||
def test_remove_dir_prefix(self):
|
||||
@@ -94,4 +95,4 @@ class TestArgComplete(object):
|
||||
ffc = FastFilesCompleter()
|
||||
fc = FilesCompleter()
|
||||
for x in '/usr/'.split():
|
||||
assert not equal_with_bash(x, ffc, fc, out=py.std.sys.stdout)
|
||||
assert not equal_with_bash(x, ffc, fc, out=sys.stdout)
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
import py_compile
|
||||
import stat
|
||||
import sys
|
||||
import textwrap
|
||||
import zipfile
|
||||
import py
|
||||
import pytest
|
||||
@@ -128,6 +129,16 @@ class TestAssertionRewrite(object):
|
||||
assert len(m.body) == 1
|
||||
assert m.body[0].msg is None
|
||||
|
||||
def test_dont_rewrite_plugin(self, testdir):
|
||||
contents = {
|
||||
"conftest.py": "pytest_plugins = 'plugin'; import plugin",
|
||||
"plugin.py": "'PYTEST_DONT_REWRITE'",
|
||||
"test_foo.py": "def test_foo(): pass",
|
||||
}
|
||||
testdir.makepyfile(**contents)
|
||||
result = testdir.runpytest_subprocess()
|
||||
assert "warnings" not in "".join(result.outlines)
|
||||
|
||||
def test_name(self):
|
||||
def f():
|
||||
assert False
|
||||
@@ -901,7 +912,7 @@ class TestAssertionRewriteHookDetails(object):
|
||||
def test_reload_is_same(self, testdir):
|
||||
# A file that will be picked up during collecting.
|
||||
testdir.tmpdir.join("file.py").ensure()
|
||||
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
|
||||
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
|
||||
[pytest]
|
||||
python_files = *.py
|
||||
"""))
|
||||
@@ -987,7 +998,7 @@ class TestIssue2121():
|
||||
def test_simple_failure():
|
||||
assert 1 + 1 == 3
|
||||
""")
|
||||
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
|
||||
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
|
||||
[pytest]
|
||||
python_files = tests/**.py
|
||||
"""))
|
||||
|
||||
30
testing/test_cache.py → testing/test_cacheprovider.py
Executable file → Normal file
30
testing/test_cache.py → testing/test_cacheprovider.py
Executable file → Normal file
@@ -31,7 +31,7 @@ class TestNewAPI(object):
|
||||
|
||||
def test_cache_writefail_cachfile_silent(self, testdir):
|
||||
testdir.makeini("[pytest]")
|
||||
testdir.tmpdir.join('.cache').write('gone wrong')
|
||||
testdir.tmpdir.join('.pytest_cache').write('gone wrong')
|
||||
config = testdir.parseconfigure()
|
||||
cache = config.cache
|
||||
cache.set('test/broken', [])
|
||||
@@ -39,14 +39,14 @@ class TestNewAPI(object):
|
||||
@pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows')
|
||||
def test_cache_writefail_permissions(self, testdir):
|
||||
testdir.makeini("[pytest]")
|
||||
testdir.tmpdir.ensure_dir('.cache').chmod(0)
|
||||
testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0)
|
||||
config = testdir.parseconfigure()
|
||||
cache = config.cache
|
||||
cache.set('test/broken', [])
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows')
|
||||
def test_cache_failure_warns(self, testdir):
|
||||
testdir.tmpdir.ensure_dir('.cache').chmod(0)
|
||||
testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0)
|
||||
testdir.makepyfile("""
|
||||
def test_error():
|
||||
raise Exception
|
||||
@@ -127,7 +127,7 @@ def test_cache_reportheader(testdir):
|
||||
""")
|
||||
result = testdir.runpytest("-v")
|
||||
result.stdout.fnmatch_lines([
|
||||
"cachedir: .cache"
|
||||
"cachedir: .pytest_cache"
|
||||
])
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ class TestLastFailed(object):
|
||||
])
|
||||
|
||||
# Run this again to make sure clear-cache is robust
|
||||
if os.path.isdir('.cache'):
|
||||
shutil.rmtree('.cache')
|
||||
if os.path.isdir('.pytest_cache'):
|
||||
shutil.rmtree('.pytest_cache')
|
||||
result = testdir.runpytest("--lf", "--cache-clear")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*1 failed*2 passed*",
|
||||
@@ -410,8 +410,8 @@ class TestLastFailed(object):
|
||||
def test_lastfailed_collectfailure(self, testdir, monkeypatch):
|
||||
|
||||
testdir.makepyfile(test_maybe="""
|
||||
import py
|
||||
env = py.std.os.environ
|
||||
import os
|
||||
env = os.environ
|
||||
if '1' == env['FAILIMPORT']:
|
||||
raise ImportError('fail')
|
||||
def test_hello():
|
||||
@@ -439,8 +439,8 @@ class TestLastFailed(object):
|
||||
def test_lastfailed_failure_subset(self, testdir, monkeypatch):
|
||||
|
||||
testdir.makepyfile(test_maybe="""
|
||||
import py
|
||||
env = py.std.os.environ
|
||||
import os
|
||||
env = os.environ
|
||||
if '1' == env['FAILIMPORT']:
|
||||
raise ImportError('fail')
|
||||
def test_hello():
|
||||
@@ -448,8 +448,8 @@ class TestLastFailed(object):
|
||||
""")
|
||||
|
||||
testdir.makepyfile(test_maybe2="""
|
||||
import py
|
||||
env = py.std.os.environ
|
||||
import os
|
||||
env = os.environ
|
||||
if '1' == env['FAILIMPORT']:
|
||||
raise ImportError('fail')
|
||||
def test_hello():
|
||||
@@ -495,15 +495,15 @@ class TestLastFailed(object):
|
||||
# Issue #1342
|
||||
testdir.makepyfile(test_empty='')
|
||||
testdir.runpytest('-q', '--lf')
|
||||
assert not os.path.exists('.cache')
|
||||
assert not os.path.exists('.pytest_cache')
|
||||
|
||||
testdir.makepyfile(test_successful='def test_success():\n assert True')
|
||||
testdir.runpytest('-q', '--lf')
|
||||
assert not os.path.exists('.cache')
|
||||
assert not os.path.exists('.pytest_cache')
|
||||
|
||||
testdir.makepyfile(test_errored='def test_error():\n assert False')
|
||||
testdir.runpytest('-q', '--lf')
|
||||
assert os.path.exists('.cache')
|
||||
assert os.path.exists('.pytest_cache')
|
||||
|
||||
def test_xfail_not_considered_failure(self, testdir):
|
||||
testdir.makepyfile('''
|
||||
@@ -1245,7 +1245,7 @@ def test_py36_windowsconsoleio_workaround_non_standard_streams():
|
||||
"""
|
||||
from _pytest.capture import _py36_windowsconsoleio_workaround
|
||||
|
||||
class DummyStream:
|
||||
class DummyStream(object):
|
||||
def write(self, s):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pprint
|
||||
import sys
|
||||
import pytest
|
||||
import py
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv
|
||||
@@ -36,7 +37,7 @@ class TestCollector(object):
|
||||
|
||||
assert fn1 == fn2
|
||||
assert fn1 != modcol
|
||||
if py.std.sys.version_info < (3, 0):
|
||||
if sys.version_info < (3, 0):
|
||||
assert cmp(fn1, fn2) == 0
|
||||
assert hash(fn1) == hash(fn2)
|
||||
|
||||
@@ -128,7 +129,7 @@ class TestCollectFS(object):
|
||||
("activate", "activate.csh", "activate.fish",
|
||||
"Activate", "Activate.bat", "Activate.ps1"))
|
||||
def test_ignored_virtualenvs(self, testdir, fname):
|
||||
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
|
||||
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||
testdir.tmpdir.ensure("virtual", bindir, fname)
|
||||
testfile = testdir.tmpdir.ensure("virtual", "test_invenv.py")
|
||||
testfile.write("def test_hello(): pass")
|
||||
@@ -147,7 +148,7 @@ class TestCollectFS(object):
|
||||
("activate", "activate.csh", "activate.fish",
|
||||
"Activate", "Activate.bat", "Activate.ps1"))
|
||||
def test_ignored_virtualenvs_norecursedirs_precedence(self, testdir, fname):
|
||||
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
|
||||
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||
# norecursedirs takes priority
|
||||
testdir.tmpdir.ensure(".virtual", bindir, fname)
|
||||
testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py")
|
||||
@@ -163,7 +164,7 @@ class TestCollectFS(object):
|
||||
"Activate", "Activate.bat", "Activate.ps1"))
|
||||
def test__in_venv(self, testdir, fname):
|
||||
"""Directly test the virtual env detection function"""
|
||||
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
|
||||
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
|
||||
# no bin/activate, not a virtualenv
|
||||
base_path = testdir.tmpdir.mkdir('venv')
|
||||
assert _in_venv(base_path) is False
|
||||
@@ -436,7 +437,7 @@ class TestSession(object):
|
||||
assert item.name == "test_func"
|
||||
newid = item.nodeid
|
||||
assert newid == id
|
||||
py.std.pprint.pprint(hookrec.calls)
|
||||
pprint.pprint(hookrec.calls)
|
||||
topdir = testdir.tmpdir # noqa
|
||||
hookrec.assert_contains([
|
||||
("pytest_collectstart", "collector.fspath == topdir"),
|
||||
@@ -486,7 +487,7 @@ class TestSession(object):
|
||||
id = p.basename
|
||||
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
py.std.pprint.pprint(hookrec.calls)
|
||||
pprint.pprint(hookrec.calls)
|
||||
assert len(items) == 2
|
||||
hookrec.assert_contains([
|
||||
("pytest_collectstart",
|
||||
@@ -508,7 +509,7 @@ class TestSession(object):
|
||||
|
||||
items, hookrec = testdir.inline_genitems()
|
||||
assert len(items) == 1
|
||||
py.std.pprint.pprint(hookrec.calls)
|
||||
pprint.pprint(hookrec.calls)
|
||||
hookrec.assert_contains([
|
||||
("pytest_collectstart", "collector.fspath == test_aaa"),
|
||||
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||
@@ -529,7 +530,7 @@ class TestSession(object):
|
||||
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
assert len(items) == 2
|
||||
py.std.pprint.pprint(hookrec.calls)
|
||||
pprint.pprint(hookrec.calls)
|
||||
hookrec.assert_contains([
|
||||
("pytest_collectstart", "collector.fspath == test_aaa"),
|
||||
("pytest_pycollect_makeitem", "name == 'test_func'"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import py
|
||||
import textwrap
|
||||
import pytest
|
||||
|
||||
import _pytest._code
|
||||
@@ -57,7 +57,7 @@ class TestParseIni(object):
|
||||
('pytest', 'pytest.ini')],
|
||||
)
|
||||
def test_ini_names(self, testdir, name, section):
|
||||
testdir.tmpdir.join(name).write(py.std.textwrap.dedent("""
|
||||
testdir.tmpdir.join(name).write(textwrap.dedent("""
|
||||
[{section}]
|
||||
minversion = 1.0
|
||||
""".format(section=section)))
|
||||
@@ -66,11 +66,11 @@ class TestParseIni(object):
|
||||
|
||||
def test_toxini_before_lower_pytestini(self, testdir):
|
||||
sub = testdir.tmpdir.mkdir("sub")
|
||||
sub.join("tox.ini").write(py.std.textwrap.dedent("""
|
||||
sub.join("tox.ini").write(textwrap.dedent("""
|
||||
[pytest]
|
||||
minversion = 2.0
|
||||
"""))
|
||||
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
|
||||
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
|
||||
[pytest]
|
||||
minversion = 1.5
|
||||
"""))
|
||||
@@ -731,7 +731,7 @@ class TestRootdir(object):
|
||||
class TestOverrideIniArgs(object):
|
||||
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
|
||||
def test_override_ini_names(self, testdir, name):
|
||||
testdir.tmpdir.join(name).write(py.std.textwrap.dedent("""
|
||||
testdir.tmpdir.join(name).write(textwrap.dedent("""
|
||||
[pytest]
|
||||
custom = 1.0"""))
|
||||
testdir.makeconftest("""
|
||||
@@ -781,16 +781,18 @@ class TestOverrideIniArgs(object):
|
||||
testdir.makeini("""
|
||||
[pytest]
|
||||
custom_option_1=custom_option_1
|
||||
custom_option_2=custom_option_2""")
|
||||
custom_option_2=custom_option_2
|
||||
""")
|
||||
testdir.makepyfile("""
|
||||
def test_multiple_options(pytestconfig):
|
||||
prefix = "custom_option"
|
||||
for x in range(1, 5):
|
||||
ini_value=pytestconfig.getini("%s_%d" % (prefix, x))
|
||||
print('\\nini%d:%s' % (x, ini_value))""")
|
||||
print('\\nini%d:%s' % (x, ini_value))
|
||||
""")
|
||||
result = testdir.runpytest(
|
||||
"--override-ini", 'custom_option_1=fulldir=/tmp/user1',
|
||||
'custom_option_2=url=/tmp/user2?a=b&d=e',
|
||||
'-o', 'custom_option_2=url=/tmp/user2?a=b&d=e',
|
||||
"-o", 'custom_option_3=True',
|
||||
"-o", 'custom_option_4=no', "-s")
|
||||
result.stdout.fnmatch_lines(["ini1:fulldir=/tmp/user1",
|
||||
@@ -853,10 +855,42 @@ class TestOverrideIniArgs(object):
|
||||
assert rootdir == tmpdir
|
||||
assert inifile is None
|
||||
|
||||
def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch):
|
||||
def test_addopts_before_initini(self, monkeypatch):
|
||||
cache_dir = '.custom_cache'
|
||||
monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir)
|
||||
from _pytest.config import get_config
|
||||
config = get_config()
|
||||
config._preparse([], addopts=True)
|
||||
assert config._override_ini == [['cache_dir=%s' % cache_dir]]
|
||||
assert config._override_ini == ['cache_dir=%s' % cache_dir]
|
||||
|
||||
def test_override_ini_does_not_contain_paths(self):
|
||||
"""Check that -o no longer swallows all options after it (#3103)"""
|
||||
from _pytest.config import get_config
|
||||
config = get_config()
|
||||
config._preparse(['-o', 'cache_dir=/cache', '/some/test/path'])
|
||||
assert config._override_ini == ['cache_dir=/cache']
|
||||
|
||||
def test_multiple_override_ini_options(self, testdir, request):
|
||||
"""Ensure a file path following a '-o' option does not generate an error (#3103)"""
|
||||
testdir.makepyfile(**{
|
||||
"conftest.py": """
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('foo', default=None, help='some option')
|
||||
parser.addini('bar', default=None, help='some option')
|
||||
""",
|
||||
"test_foo.py": """
|
||||
def test(pytestconfig):
|
||||
assert pytestconfig.getini('foo') == '1'
|
||||
assert pytestconfig.getini('bar') == '0'
|
||||
""",
|
||||
"test_bar.py": """
|
||||
def test():
|
||||
assert False
|
||||
""",
|
||||
})
|
||||
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 *=',
|
||||
])
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_conftest_in_nonpkg_with_init(tmpdir):
|
||||
|
||||
def test_doubledash_considered(testdir):
|
||||
conf = testdir.mkdir("--option")
|
||||
conf.join("conftest.py").ensure()
|
||||
conf.ensure("conftest.py")
|
||||
conftest = PytestPluginManager()
|
||||
conftest_setinitial(conftest, [conf.basename, conf.basename])
|
||||
values = conftest._getconftestmodules(conf)
|
||||
@@ -232,7 +232,7 @@ def test_fixture_dependency(testdir, monkeypatch):
|
||||
ct1.write("")
|
||||
sub = testdir.mkdir("sub")
|
||||
sub.join("__init__.py").write("")
|
||||
sub.join("conftest.py").write(py.std.textwrap.dedent("""
|
||||
sub.join("conftest.py").write(dedent("""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
@@ -249,7 +249,7 @@ def test_fixture_dependency(testdir, monkeypatch):
|
||||
"""))
|
||||
subsub = sub.mkdir("subsub")
|
||||
subsub.join("__init__.py").write("")
|
||||
subsub.join("test_bar.py").write(py.std.textwrap.dedent("""
|
||||
subsub.join("test_bar.py").write(dedent("""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
@@ -265,16 +265,12 @@ def test_fixture_dependency(testdir, monkeypatch):
|
||||
|
||||
def test_conftest_found_with_double_dash(testdir):
|
||||
sub = testdir.mkdir("sub")
|
||||
sub.join("conftest.py").write(py.std.textwrap.dedent("""
|
||||
sub.join("conftest.py").write(dedent("""
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--hello-world", action="store_true")
|
||||
"""))
|
||||
p = sub.join("test_hello.py")
|
||||
p.write(py.std.textwrap.dedent("""
|
||||
import pytest
|
||||
def test_hello(found):
|
||||
assert found == 1
|
||||
"""))
|
||||
p.write("def test_hello(): pass")
|
||||
result = testdir.runpytest(str(p) + "::test_hello", "-h")
|
||||
result.stdout.fnmatch_lines("""
|
||||
*--hello-world*
|
||||
|
||||
@@ -879,6 +879,27 @@ def test_record_property_same_name(testdir):
|
||||
pnodes[1].assert_attr(name="foo", value="baz")
|
||||
|
||||
|
||||
def test_record_attribute(testdir):
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def other(record_xml_attribute):
|
||||
record_xml_attribute("bar", 1)
|
||||
def test_record(record_xml_attribute, other):
|
||||
record_xml_attribute("foo", "<1");
|
||||
""")
|
||||
result, dom = runandparse(testdir, '-rw')
|
||||
node = dom.find_first_by_tag("testsuite")
|
||||
tnode = node.find_first_by_tag("testcase")
|
||||
tnode.assert_attr(bar="1")
|
||||
tnode.assert_attr(foo="<1")
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_record_attribute.py::test_record',
|
||||
'*record_xml_attribute*experimental*',
|
||||
])
|
||||
|
||||
|
||||
def test_random_report_log_xdist(testdir):
|
||||
"""xdist calls pytest_runtest_logreport as they are executed by the slaves,
|
||||
with nodes from several nodes overlapping, so junitxml must cope with that
|
||||
|
||||
@@ -3,7 +3,10 @@ import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers
|
||||
from _pytest.mark import (
|
||||
MarkGenerator as Mark, ParameterSet, transfer_markers,
|
||||
EMPTY_PARAMETERSET_OPTION,
|
||||
)
|
||||
|
||||
|
||||
class TestMark(object):
|
||||
@@ -344,6 +347,21 @@ def test_keyword_option_parametrize(spec, testdir):
|
||||
assert list(passed) == list(passed_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spec", [
|
||||
("foo or import", "ERROR: Python keyword 'import' not accepted in expressions passed to '-k'"),
|
||||
("foo or", "ERROR: Wrong expression passed to '-k': foo or")
|
||||
])
|
||||
def test_keyword_option_wrong_arguments(spec, testdir, capsys):
|
||||
testdir.makepyfile("""
|
||||
def test_func(arg):
|
||||
pass
|
||||
""")
|
||||
opt, expected_result = spec
|
||||
testdir.inline_run("-k", opt)
|
||||
out = capsys.readouterr().err
|
||||
assert expected_result in out
|
||||
|
||||
|
||||
def test_parametrized_collected_from_command_line(testdir):
|
||||
"""Parametrized test not collected if test named specified
|
||||
in command line issue#649.
|
||||
@@ -876,3 +894,27 @@ class TestMarkDecorator(object):
|
||||
])
|
||||
def test__eq__(self, lhs, rhs, expected):
|
||||
assert (lhs == rhs) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mark', [None, '', 'skip', 'xfail'])
|
||||
def test_parameterset_for_parametrize_marks(testdir, mark):
|
||||
if mark is not None:
|
||||
testdir.makeini(
|
||||
"[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark))
|
||||
|
||||
config = testdir.parseconfig()
|
||||
from _pytest.mark import pytest_configure, get_empty_parameterset_mark
|
||||
pytest_configure(config)
|
||||
result_mark = get_empty_parameterset_mark(config, ['a'], all)
|
||||
if mark in (None, ''):
|
||||
# normalize to the requested name
|
||||
mark = 'skip'
|
||||
assert result_mark.name == mark
|
||||
assert result_mark.kwargs['reason'].startswith("got empty parameter set ")
|
||||
if mark == 'xfail':
|
||||
assert result_mark.kwargs.get('run') is False
|
||||
|
||||
|
||||
def test_parameterset_for_parametrize_bad_markname(testdir):
|
||||
with pytest.raises(pytest.UsageError):
|
||||
test_parameterset_for_parametrize_marks(testdir, 'bad')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import py
|
||||
@@ -189,7 +190,7 @@ class TestParser(object):
|
||||
assert option.no is False
|
||||
|
||||
def test_drop_short_helper(self):
|
||||
parser = py.std.argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter)
|
||||
parser = argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter)
|
||||
parser.add_argument('-t', '--twoword', '--duo', '--two-word', '--two',
|
||||
help='foo').map_long_option = {'two': 'two-word'}
|
||||
# throws error on --deux only!
|
||||
|
||||
@@ -338,7 +338,7 @@ class TestPDB(object):
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_collection_failure_is_shown(self, testdir):
|
||||
p1 = testdir.makepyfile("""xxx """)
|
||||
p1 = testdir.makepyfile("xxx")
|
||||
result = testdir.runpytest_subprocess("--pdb", p1)
|
||||
result.stdout.fnmatch_lines([
|
||||
"*NameError*xxx*",
|
||||
@@ -402,5 +402,4 @@ class TestPDB(object):
|
||||
child = testdir.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))
|
||||
|
||||
child.expect('custom set_trace>')
|
||||
if child.isalive():
|
||||
child.wait()
|
||||
self.flush(child)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# encoding: UTF-8
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pytest
|
||||
import py
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
from _pytest.config import get_config, PytestPluginManager
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED, Session
|
||||
@@ -208,14 +210,14 @@ def test_importplugin_error_message(testdir, pytestpm):
|
||||
|
||||
expected_message = '.*Error importing plugin "qwe": Not possible to import: .'
|
||||
expected_traceback = ".*in test_traceback"
|
||||
assert py.std.re.match(expected_message, str(excinfo.value))
|
||||
assert py.std.re.match(expected_traceback, str(excinfo.traceback[-1]))
|
||||
assert re.match(expected_message, str(excinfo.value))
|
||||
assert re.match(expected_traceback, str(excinfo.traceback[-1]))
|
||||
|
||||
|
||||
class TestPytestPluginManager(object):
|
||||
def test_register_imported_modules(self):
|
||||
pm = PytestPluginManager()
|
||||
mod = py.std.types.ModuleType("x.y.pytest_hello")
|
||||
mod = types.ModuleType("x.y.pytest_hello")
|
||||
pm.register(mod)
|
||||
assert pm.is_registered(mod)
|
||||
values = pm.get_plugins()
|
||||
@@ -226,8 +228,8 @@ class TestPytestPluginManager(object):
|
||||
assert pm.get_plugins() == values
|
||||
|
||||
def test_canonical_import(self, monkeypatch):
|
||||
mod = py.std.types.ModuleType("pytest_xyz")
|
||||
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
|
||||
mod = types.ModuleType("pytest_xyz")
|
||||
monkeypatch.setitem(sys.modules, 'pytest_xyz', mod)
|
||||
pm = PytestPluginManager()
|
||||
pm.import_plugin('pytest_xyz')
|
||||
assert pm.get_plugin('pytest_xyz') == mod
|
||||
@@ -237,7 +239,7 @@ class TestPytestPluginManager(object):
|
||||
testdir.syspathinsert()
|
||||
testdir.makepyfile(pytest_p1="#")
|
||||
testdir.makepyfile(pytest_p2="#")
|
||||
mod = py.std.types.ModuleType("temp")
|
||||
mod = types.ModuleType("temp")
|
||||
mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
|
||||
pytestpm.consider_module(mod)
|
||||
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
|
||||
@@ -245,12 +247,12 @@ class TestPytestPluginManager(object):
|
||||
|
||||
def test_consider_module_import_module(self, testdir):
|
||||
pytestpm = get_config().pluginmanager
|
||||
mod = py.std.types.ModuleType("x")
|
||||
mod = types.ModuleType("x")
|
||||
mod.pytest_plugins = "pytest_a"
|
||||
aplugin = testdir.makepyfile(pytest_a="#")
|
||||
reprec = testdir.make_hook_recorder(pytestpm)
|
||||
# syspath.prepend(aplugin.dirpath())
|
||||
py.std.sys.path.insert(0, str(aplugin.dirpath()))
|
||||
sys.path.insert(0, str(aplugin.dirpath()))
|
||||
pytestpm.consider_module(mod)
|
||||
call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name)
|
||||
assert call.plugin.__name__ == "pytest_a"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pytest
|
||||
import os
|
||||
import py.path
|
||||
import pytest
|
||||
import sys
|
||||
import _pytest.pytester as pytester
|
||||
from _pytest.pytester import HookRecorder
|
||||
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED
|
||||
|
||||
@@ -131,19 +135,244 @@ def test_makepyfile_utf8(testdir):
|
||||
assert u"mixed_encoding = u'São Paulo'".encode('utf-8') in p.read('rb')
|
||||
|
||||
|
||||
def test_inline_run_clean_modules(testdir):
|
||||
test_mod = testdir.makepyfile("def test_foo(): assert True")
|
||||
result = testdir.inline_run(str(test_mod))
|
||||
assert result.ret == EXIT_OK
|
||||
# rewrite module, now test should fail if module was re-imported
|
||||
test_mod.write("def test_foo(): assert False")
|
||||
result2 = testdir.inline_run(str(test_mod))
|
||||
assert result2.ret == EXIT_TESTSFAILED
|
||||
class TestInlineRunModulesCleanup(object):
|
||||
def test_inline_run_test_module_not_cleaned_up(self, testdir):
|
||||
test_mod = testdir.makepyfile("def test_foo(): assert True")
|
||||
result = testdir.inline_run(str(test_mod))
|
||||
assert result.ret == EXIT_OK
|
||||
# rewrite module, now test should fail if module was re-imported
|
||||
test_mod.write("def test_foo(): assert False")
|
||||
result2 = testdir.inline_run(str(test_mod))
|
||||
assert result2.ret == EXIT_TESTSFAILED
|
||||
|
||||
def spy_factory(self):
|
||||
class SysModulesSnapshotSpy(object):
|
||||
instances = []
|
||||
|
||||
def __init__(self, preserve=None):
|
||||
SysModulesSnapshotSpy.instances.append(self)
|
||||
self._spy_restore_count = 0
|
||||
self._spy_preserve = preserve
|
||||
self.__snapshot = SysModulesSnapshot(preserve=preserve)
|
||||
|
||||
def restore(self):
|
||||
self._spy_restore_count += 1
|
||||
return self.__snapshot.restore()
|
||||
return SysModulesSnapshotSpy
|
||||
|
||||
def test_inline_run_taking_and_restoring_a_sys_modules_snapshot(
|
||||
self, testdir, monkeypatch):
|
||||
spy_factory = self.spy_factory()
|
||||
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
|
||||
original = dict(sys.modules)
|
||||
testdir.syspathinsert()
|
||||
testdir.makepyfile(import1="# you son of a silly person")
|
||||
testdir.makepyfile(import2="# my hovercraft is full of eels")
|
||||
test_mod = testdir.makepyfile("""
|
||||
import import1
|
||||
def test_foo(): import import2""")
|
||||
testdir.inline_run(str(test_mod))
|
||||
assert len(spy_factory.instances) == 1
|
||||
spy = spy_factory.instances[0]
|
||||
assert spy._spy_restore_count == 1
|
||||
assert sys.modules == original
|
||||
assert all(sys.modules[x] is original[x] for x in sys.modules)
|
||||
|
||||
def test_inline_run_sys_modules_snapshot_restore_preserving_modules(
|
||||
self, testdir, monkeypatch):
|
||||
spy_factory = self.spy_factory()
|
||||
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
|
||||
test_mod = testdir.makepyfile("def test_foo(): pass")
|
||||
testdir.inline_run(str(test_mod))
|
||||
spy = spy_factory.instances[0]
|
||||
assert not spy._spy_preserve("black_knight")
|
||||
assert spy._spy_preserve("zope")
|
||||
assert spy._spy_preserve("zope.interface")
|
||||
assert spy._spy_preserve("zopelicious")
|
||||
|
||||
def test_external_test_module_imports_not_cleaned_up(self, testdir):
|
||||
testdir.syspathinsert()
|
||||
testdir.makepyfile(imported="data = 'you son of a silly person'")
|
||||
import imported
|
||||
test_mod = testdir.makepyfile("""
|
||||
def test_foo():
|
||||
import imported
|
||||
imported.data = 42""")
|
||||
testdir.inline_run(str(test_mod))
|
||||
assert imported.data == 42
|
||||
|
||||
|
||||
def test_assert_outcomes_after_pytest_erro(testdir):
|
||||
def test_inline_run_clean_sys_paths(testdir):
|
||||
def test_sys_path_change_cleanup(self, testdir):
|
||||
test_path1 = testdir.tmpdir.join("boink1").strpath
|
||||
test_path2 = testdir.tmpdir.join("boink2").strpath
|
||||
test_path3 = testdir.tmpdir.join("boink3").strpath
|
||||
sys.path.append(test_path1)
|
||||
sys.meta_path.append(test_path1)
|
||||
original_path = list(sys.path)
|
||||
original_meta_path = list(sys.meta_path)
|
||||
test_mod = testdir.makepyfile("""
|
||||
import sys
|
||||
sys.path.append({:test_path2})
|
||||
sys.meta_path.append({:test_path2})
|
||||
def test_foo():
|
||||
sys.path.append({:test_path3})
|
||||
sys.meta_path.append({:test_path3})""".format(locals()))
|
||||
testdir.inline_run(str(test_mod))
|
||||
assert sys.path == original_path
|
||||
assert sys.meta_path == original_meta_path
|
||||
|
||||
def spy_factory(self):
|
||||
class SysPathsSnapshotSpy(object):
|
||||
instances = []
|
||||
|
||||
def __init__(self):
|
||||
SysPathsSnapshotSpy.instances.append(self)
|
||||
self._spy_restore_count = 0
|
||||
self.__snapshot = SysPathsSnapshot()
|
||||
|
||||
def restore(self):
|
||||
self._spy_restore_count += 1
|
||||
return self.__snapshot.restore()
|
||||
return SysPathsSnapshotSpy
|
||||
|
||||
def test_inline_run_taking_and_restoring_a_sys_paths_snapshot(
|
||||
self, testdir, monkeypatch):
|
||||
spy_factory = self.spy_factory()
|
||||
monkeypatch.setattr(pytester, "SysPathsSnapshot", spy_factory)
|
||||
test_mod = testdir.makepyfile("def test_foo(): pass")
|
||||
testdir.inline_run(str(test_mod))
|
||||
assert len(spy_factory.instances) == 1
|
||||
spy = spy_factory.instances[0]
|
||||
assert spy._spy_restore_count == 1
|
||||
|
||||
|
||||
def test_assert_outcomes_after_pytest_error(testdir):
|
||||
testdir.makepyfile("def test_foo(): assert True")
|
||||
|
||||
result = testdir.runpytest('--unexpected-argument')
|
||||
with pytest.raises(ValueError, message="Pytest terminal report not found"):
|
||||
result.assert_outcomes(passed=0)
|
||||
|
||||
|
||||
def test_cwd_snapshot(tmpdir):
|
||||
foo = tmpdir.ensure('foo', dir=1)
|
||||
bar = tmpdir.ensure('bar', dir=1)
|
||||
foo.chdir()
|
||||
snapshot = CwdSnapshot()
|
||||
bar.chdir()
|
||||
assert py.path.local() == bar
|
||||
snapshot.restore()
|
||||
assert py.path.local() == foo
|
||||
|
||||
|
||||
class TestSysModulesSnapshot(object):
|
||||
key = 'my-test-module'
|
||||
|
||||
def test_remove_added(self):
|
||||
original = dict(sys.modules)
|
||||
assert self.key not in sys.modules
|
||||
snapshot = SysModulesSnapshot()
|
||||
sys.modules[self.key] = 'something'
|
||||
assert self.key in sys.modules
|
||||
snapshot.restore()
|
||||
assert sys.modules == original
|
||||
|
||||
def test_add_removed(self, monkeypatch):
|
||||
assert self.key not in sys.modules
|
||||
monkeypatch.setitem(sys.modules, self.key, 'something')
|
||||
assert self.key in sys.modules
|
||||
original = dict(sys.modules)
|
||||
snapshot = SysModulesSnapshot()
|
||||
del sys.modules[self.key]
|
||||
assert self.key not in sys.modules
|
||||
snapshot.restore()
|
||||
assert sys.modules == original
|
||||
|
||||
def test_restore_reloaded(self, monkeypatch):
|
||||
assert self.key not in sys.modules
|
||||
monkeypatch.setitem(sys.modules, self.key, 'something')
|
||||
assert self.key in sys.modules
|
||||
original = dict(sys.modules)
|
||||
snapshot = SysModulesSnapshot()
|
||||
sys.modules[self.key] = 'something else'
|
||||
snapshot.restore()
|
||||
assert sys.modules == original
|
||||
|
||||
def test_preserve_modules(self, monkeypatch):
|
||||
key = [self.key + str(i) for i in range(3)]
|
||||
assert not any(k in sys.modules for k in key)
|
||||
for i, k in enumerate(key):
|
||||
monkeypatch.setitem(sys.modules, k, 'something' + str(i))
|
||||
original = dict(sys.modules)
|
||||
|
||||
def preserve(name):
|
||||
return name in (key[0], key[1], 'some-other-key')
|
||||
|
||||
snapshot = SysModulesSnapshot(preserve=preserve)
|
||||
sys.modules[key[0]] = original[key[0]] = 'something else0'
|
||||
sys.modules[key[1]] = original[key[1]] = 'something else1'
|
||||
sys.modules[key[2]] = 'something else2'
|
||||
snapshot.restore()
|
||||
assert sys.modules == original
|
||||
|
||||
def test_preserve_container(self, monkeypatch):
|
||||
original = dict(sys.modules)
|
||||
assert self.key not in original
|
||||
replacement = dict(sys.modules)
|
||||
replacement[self.key] = 'life of brian'
|
||||
snapshot = SysModulesSnapshot()
|
||||
monkeypatch.setattr(sys, 'modules', replacement)
|
||||
snapshot.restore()
|
||||
assert sys.modules is replacement
|
||||
assert sys.modules == original
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path_type', ('path', 'meta_path'))
|
||||
class TestSysPathsSnapshot(object):
|
||||
other_path = {
|
||||
'path': 'meta_path',
|
||||
'meta_path': 'path'}
|
||||
|
||||
@staticmethod
|
||||
def path(n):
|
||||
return 'my-dirty-little-secret-' + str(n)
|
||||
|
||||
def test_restore(self, monkeypatch, path_type):
|
||||
other_path_type = self.other_path[path_type]
|
||||
for i in range(10):
|
||||
assert self.path(i) not in getattr(sys, path_type)
|
||||
sys_path = [self.path(i) for i in range(6)]
|
||||
monkeypatch.setattr(sys, path_type, sys_path)
|
||||
original = list(sys_path)
|
||||
original_other = list(getattr(sys, other_path_type))
|
||||
snapshot = SysPathsSnapshot()
|
||||
transformation = {
|
||||
'source': (0, 1, 2, 3, 4, 5),
|
||||
'target': ( 6, 2, 9, 7, 5, 8)} # noqa: E201
|
||||
assert sys_path == [self.path(x) for x in transformation['source']]
|
||||
sys_path[1] = self.path(6)
|
||||
sys_path[3] = self.path(7)
|
||||
sys_path.append(self.path(8))
|
||||
del sys_path[4]
|
||||
sys_path[3:3] = [self.path(9)]
|
||||
del sys_path[0]
|
||||
assert sys_path == [self.path(x) for x in transformation['target']]
|
||||
snapshot.restore()
|
||||
assert getattr(sys, path_type) is sys_path
|
||||
assert getattr(sys, path_type) == original
|
||||
assert getattr(sys, other_path_type) == original_other
|
||||
|
||||
def test_preserve_container(self, monkeypatch, path_type):
|
||||
other_path_type = self.other_path[path_type]
|
||||
original_data = list(getattr(sys, path_type))
|
||||
original_other = getattr(sys, other_path_type)
|
||||
original_other_data = list(original_other)
|
||||
new = []
|
||||
snapshot = SysPathsSnapshot()
|
||||
monkeypatch.setattr(sys, path_type, new)
|
||||
snapshot.restore()
|
||||
assert getattr(sys, path_type) is new
|
||||
assert getattr(sys, path_type) == original_data
|
||||
assert getattr(sys, other_path_type) is original_other
|
||||
assert getattr(sys, other_path_type) == original_other_data
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import warnings
|
||||
import re
|
||||
import py
|
||||
|
||||
import pytest
|
||||
from _pytest.recwarn import WarningsRecorder
|
||||
@@ -24,9 +23,9 @@ class TestWarningsRecorderChecker(object):
|
||||
rec = WarningsRecorder()
|
||||
with rec:
|
||||
assert not rec.list
|
||||
py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13)
|
||||
warnings.warn_explicit("hello", UserWarning, "xyz", 13)
|
||||
assert len(rec.list) == 1
|
||||
py.std.warnings.warn(DeprecationWarning("hello"))
|
||||
warnings.warn(DeprecationWarning("hello"))
|
||||
assert len(rec.list) == 2
|
||||
warn = rec.pop()
|
||||
assert str(warn.message) == "hello"
|
||||
@@ -64,14 +63,14 @@ class TestDeprecatedCall(object):
|
||||
|
||||
def dep(self, i, j=None):
|
||||
if i == 0:
|
||||
py.std.warnings.warn("is deprecated", DeprecationWarning,
|
||||
stacklevel=1)
|
||||
warnings.warn("is deprecated", DeprecationWarning,
|
||||
stacklevel=1)
|
||||
return 42
|
||||
|
||||
def dep_explicit(self, i):
|
||||
if i == 0:
|
||||
py.std.warnings.warn_explicit("dep_explicit", category=DeprecationWarning,
|
||||
filename="hello", lineno=3)
|
||||
warnings.warn_explicit("dep_explicit", category=DeprecationWarning,
|
||||
filename="hello", lineno=3)
|
||||
|
||||
def test_deprecated_call_raises(self):
|
||||
with pytest.raises(AssertionError) as excinfo:
|
||||
@@ -86,16 +85,16 @@ class TestDeprecatedCall(object):
|
||||
assert ret == 42
|
||||
|
||||
def test_deprecated_call_preserves(self):
|
||||
onceregistry = py.std.warnings.onceregistry.copy()
|
||||
filters = py.std.warnings.filters[:]
|
||||
warn = py.std.warnings.warn
|
||||
warn_explicit = py.std.warnings.warn_explicit
|
||||
onceregistry = warnings.onceregistry.copy()
|
||||
filters = warnings.filters[:]
|
||||
warn = warnings.warn
|
||||
warn_explicit = warnings.warn_explicit
|
||||
self.test_deprecated_call_raises()
|
||||
self.test_deprecated_call()
|
||||
assert onceregistry == py.std.warnings.onceregistry
|
||||
assert filters == py.std.warnings.filters
|
||||
assert warn is py.std.warnings.warn
|
||||
assert warn_explicit is py.std.warnings.warn_explicit
|
||||
assert onceregistry == warnings.onceregistry
|
||||
assert filters == warnings.filters
|
||||
assert warn is warnings.warn
|
||||
assert warn_explicit is warnings.warn_explicit
|
||||
|
||||
def test_deprecated_explicit_call_raises(self):
|
||||
with pytest.raises(AssertionError):
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import Node, Item, FSCollector
|
||||
from _pytest.nodes import Node, Item, FSCollector
|
||||
from _pytest.resultlog import generic_path, ResultLog, \
|
||||
pytest_configure, pytest_unconfigure
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import _pytest._code
|
||||
import inspect
|
||||
import os
|
||||
import py
|
||||
import pytest
|
||||
import sys
|
||||
import types
|
||||
from _pytest import runner, main, outcomes
|
||||
|
||||
|
||||
@@ -32,7 +34,7 @@ class TestSetupState(object):
|
||||
def setup_module(mod):
|
||||
raise ValueError(42)
|
||||
def test_func(): pass
|
||||
""") # noqa
|
||||
""")
|
||||
ss = runner.SetupState()
|
||||
pytest.raises(ValueError, lambda: ss.prepare(item))
|
||||
pytest.raises(ValueError, lambda: ss.prepare(item))
|
||||
@@ -202,6 +204,18 @@ class BaseFunctionalTests(object):
|
||||
""")
|
||||
assert rec.ret == 1
|
||||
|
||||
def test_logstart_logfinish_hooks(self, testdir):
|
||||
rec = testdir.inline_runsource("""
|
||||
import pytest
|
||||
def test_func():
|
||||
pass
|
||||
""")
|
||||
reps = rec.getcalls("pytest_runtest_logstart pytest_runtest_logfinish")
|
||||
assert [x._name for x in reps] == ['pytest_runtest_logstart', 'pytest_runtest_logfinish']
|
||||
for rep in reps:
|
||||
assert rep.nodeid == 'test_logstart_logfinish_hooks.py::test_func'
|
||||
assert rep.location == ('test_logstart_logfinish_hooks.py', 1, 'test_func')
|
||||
|
||||
def test_exact_teardown_issue90(self, testdir):
|
||||
rec = testdir.inline_runsource("""
|
||||
import pytest
|
||||
@@ -392,10 +406,10 @@ reporttypes = [
|
||||
|
||||
@pytest.mark.parametrize('reporttype', reporttypes, ids=[x.__name__ for x in reporttypes])
|
||||
def test_report_extra_parameters(reporttype):
|
||||
if hasattr(py.std.inspect, 'signature'):
|
||||
args = list(py.std.inspect.signature(reporttype.__init__).parameters.keys())[1:]
|
||||
if hasattr(inspect, 'signature'):
|
||||
args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:]
|
||||
else:
|
||||
args = py.std.inspect.getargspec(reporttype.__init__)[0][1:]
|
||||
args = inspect.getargspec(reporttype.__init__)[0][1:]
|
||||
basekw = dict.fromkeys(args, [])
|
||||
report = reporttype(newthing=1, **basekw)
|
||||
assert report.newthing == 1
|
||||
@@ -564,10 +578,10 @@ def test_importorskip(monkeypatch):
|
||||
importorskip("asdlkj")
|
||||
|
||||
try:
|
||||
sys = importorskip("sys") # noqa
|
||||
assert sys == py.std.sys
|
||||
sysmod = importorskip("sys")
|
||||
assert sysmod is sys
|
||||
# path = pytest.importorskip("os.path")
|
||||
# assert path == py.std.os.path
|
||||
# assert path == os.path
|
||||
excinfo = pytest.raises(pytest.skip.Exception, f)
|
||||
path = py.path.local(excinfo.getrepr().reprcrash.path)
|
||||
# check that importorskip reports the actual call
|
||||
@@ -575,7 +589,7 @@ def test_importorskip(monkeypatch):
|
||||
assert path.purebasename == "test_runner"
|
||||
pytest.raises(SyntaxError, "pytest.importorskip('x y z')")
|
||||
pytest.raises(SyntaxError, "pytest.importorskip('x=y')")
|
||||
mod = py.std.types.ModuleType("hello123")
|
||||
mod = types.ModuleType("hello123")
|
||||
mod.__version__ = "1.3"
|
||||
monkeypatch.setitem(sys.modules, "hello123", mod)
|
||||
pytest.raises(pytest.skip.Exception, """
|
||||
@@ -595,7 +609,7 @@ def test_importorskip_imports_last_module_part():
|
||||
|
||||
def test_importorskip_dev_module(monkeypatch):
|
||||
try:
|
||||
mod = py.std.types.ModuleType("mockmodule")
|
||||
mod = types.ModuleType("mockmodule")
|
||||
mod.__version__ = '0.13.0.dev-43290'
|
||||
monkeypatch.setitem(sys.modules, 'mockmodule', mod)
|
||||
mod2 = pytest.importorskip('mockmodule', minversion='0.12.0')
|
||||
|
||||
@@ -589,7 +589,7 @@ class TestSkipif(object):
|
||||
@pytest.mark.skipif("hasattr(os, 'sep')")
|
||||
def test_func():
|
||||
pass
|
||||
""") # noqa
|
||||
""")
|
||||
x = pytest.raises(pytest.skip.Exception, lambda:
|
||||
pytest_runtest_setup(item))
|
||||
assert x.value.msg == "condition: hasattr(os, 'sep')"
|
||||
|
||||
@@ -326,7 +326,7 @@ def test_repr_python_version(monkeypatch):
|
||||
try:
|
||||
monkeypatch.setattr(sys, 'version_info', (2, 5, 1, 'final', 0))
|
||||
assert repr_pythonversion() == "2.5.1-final-0"
|
||||
py.std.sys.version_info = x = (2, 3)
|
||||
sys.version_info = x = (2, 3)
|
||||
assert repr_pythonversion() == str(x)
|
||||
finally:
|
||||
monkeypatch.undo() # do this early as pytest can get confused
|
||||
@@ -475,11 +475,11 @@ class TestTerminalFunctional(object):
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
verinfo = ".".join(map(str, py.std.sys.version_info[:3]))
|
||||
verinfo = ".".join(map(str, sys.version_info[:3]))
|
||||
result.stdout.fnmatch_lines([
|
||||
"*===== test session starts ====*",
|
||||
"platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" % (
|
||||
py.std.sys.platform, verinfo,
|
||||
sys.platform, verinfo,
|
||||
pytest.__version__, py.__version__, pluggy.__version__),
|
||||
"*test_header_trailer_info.py .*",
|
||||
"=* 1 passed*in *.[0-9][0-9] seconds *=",
|
||||
@@ -966,10 +966,10 @@ def test_no_trailing_whitespace_after_inifile_word(testdir):
|
||||
assert 'inifile: tox.ini\n' in result.stdout.str()
|
||||
|
||||
|
||||
class TestProgress:
|
||||
class TestProgress(object):
|
||||
|
||||
@pytest.fixture
|
||||
def many_tests_file(self, testdir):
|
||||
def many_tests_files(self, testdir):
|
||||
testdir.makepyfile(
|
||||
test_bar="""
|
||||
import pytest
|
||||
@@ -1006,7 +1006,7 @@ class TestProgress:
|
||||
'=* 2 passed in *=',
|
||||
])
|
||||
|
||||
def test_normal(self, many_tests_file, testdir):
|
||||
def test_normal(self, many_tests_files, testdir):
|
||||
output = testdir.runpytest()
|
||||
output.stdout.re_match_lines([
|
||||
r'test_bar.py \.{10} \s+ \[ 50%\]',
|
||||
@@ -1014,7 +1014,7 @@ class TestProgress:
|
||||
r'test_foobar.py \.{5} \s+ \[100%\]',
|
||||
])
|
||||
|
||||
def test_verbose(self, many_tests_file, testdir):
|
||||
def test_verbose(self, many_tests_files, testdir):
|
||||
output = testdir.runpytest('-v')
|
||||
output.stdout.re_match_lines([
|
||||
r'test_bar.py::test_bar\[0\] PASSED \s+ \[ 5%\]',
|
||||
@@ -1022,14 +1022,14 @@ class TestProgress:
|
||||
r'test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]',
|
||||
])
|
||||
|
||||
def test_xdist_normal(self, many_tests_file, testdir):
|
||||
def test_xdist_normal(self, many_tests_files, testdir):
|
||||
pytest.importorskip('xdist')
|
||||
output = testdir.runpytest('-n2')
|
||||
output.stdout.re_match_lines([
|
||||
r'\.{20} \s+ \[100%\]',
|
||||
])
|
||||
|
||||
def test_xdist_verbose(self, many_tests_file, testdir):
|
||||
def test_xdist_verbose(self, many_tests_files, testdir):
|
||||
pytest.importorskip('xdist')
|
||||
output = testdir.runpytest('-n2', '-v')
|
||||
output.stdout.re_match_lines_random([
|
||||
@@ -1037,3 +1037,86 @@ class TestProgress:
|
||||
r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]',
|
||||
r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]',
|
||||
])
|
||||
|
||||
def test_capture_no(self, many_tests_files, testdir):
|
||||
output = testdir.runpytest('-s')
|
||||
output.stdout.re_match_lines([
|
||||
r'test_bar.py \.{10}',
|
||||
r'test_foo.py \.{5}',
|
||||
r'test_foobar.py \.{5}',
|
||||
])
|
||||
|
||||
|
||||
class TestProgressWithTeardown(object):
|
||||
"""Ensure we show the correct percentages for tests that fail during teardown (#3088)"""
|
||||
|
||||
@pytest.fixture
|
||||
def contest_with_teardown_fixture(self, testdir):
|
||||
testdir.makeconftest('''
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fail_teardown():
|
||||
yield
|
||||
assert False
|
||||
''')
|
||||
|
||||
@pytest.fixture
|
||||
def many_files(self, testdir, contest_with_teardown_fixture):
|
||||
testdir.makepyfile(
|
||||
test_bar='''
|
||||
import pytest
|
||||
@pytest.mark.parametrize('i', range(5))
|
||||
def test_bar(fail_teardown, i):
|
||||
pass
|
||||
''',
|
||||
test_foo='''
|
||||
import pytest
|
||||
@pytest.mark.parametrize('i', range(15))
|
||||
def test_foo(fail_teardown, i):
|
||||
pass
|
||||
''',
|
||||
)
|
||||
|
||||
def test_teardown_simple(self, testdir, contest_with_teardown_fixture):
|
||||
testdir.makepyfile('''
|
||||
def test_foo(fail_teardown):
|
||||
pass
|
||||
''')
|
||||
output = testdir.runpytest()
|
||||
output.stdout.re_match_lines([
|
||||
r'test_teardown_simple.py \.E\s+\[100%\]',
|
||||
])
|
||||
|
||||
def test_teardown_with_test_also_failing(self, testdir, contest_with_teardown_fixture):
|
||||
testdir.makepyfile('''
|
||||
def test_foo(fail_teardown):
|
||||
assert False
|
||||
''')
|
||||
output = testdir.runpytest()
|
||||
output.stdout.re_match_lines([
|
||||
r'test_teardown_with_test_also_failing.py FE\s+\[100%\]',
|
||||
])
|
||||
|
||||
def test_teardown_many(self, testdir, many_files):
|
||||
output = testdir.runpytest()
|
||||
output.stdout.re_match_lines([
|
||||
r'test_bar.py (\.E){5}\s+\[ 25%\]',
|
||||
r'test_foo.py (\.E){15}\s+\[100%\]',
|
||||
])
|
||||
|
||||
def test_teardown_many_verbose(self, testdir, many_files):
|
||||
output = testdir.runpytest('-v')
|
||||
output.stdout.re_match_lines([
|
||||
r'test_bar.py::test_bar\[0\] PASSED\s+\[ 5%\]',
|
||||
r'test_bar.py::test_bar\[0\] ERROR\s+\[ 5%\]',
|
||||
r'test_bar.py::test_bar\[4\] PASSED\s+\[ 25%\]',
|
||||
r'test_bar.py::test_bar\[4\] ERROR\s+\[ 25%\]',
|
||||
])
|
||||
|
||||
def test_xdist_normal(self, many_files, testdir):
|
||||
pytest.importorskip('xdist')
|
||||
output = testdir.runpytest('-n2')
|
||||
output.stdout.re_match_lines([
|
||||
r'[\.E]{40} \s+ \[100%\]',
|
||||
])
|
||||
|
||||
6
tox.ini
6
tox.ini
@@ -129,6 +129,7 @@ basepython = python
|
||||
changedir = doc/en
|
||||
deps =
|
||||
sphinx
|
||||
attrs
|
||||
PyYAML
|
||||
|
||||
commands =
|
||||
@@ -138,7 +139,7 @@ commands =
|
||||
basepython = python
|
||||
usedevelop = True
|
||||
skipsdist = True
|
||||
# ensure the given pyargs cant mean anytrhing else
|
||||
# ensure the given pyargs can't mean anything else
|
||||
changedir = doc/
|
||||
deps =
|
||||
PyYAML
|
||||
@@ -215,6 +216,9 @@ filterwarnings =
|
||||
ignore:.*type argument to addoption.*:DeprecationWarning
|
||||
# produced by python >=3.5 on execnet (pytest-xdist)
|
||||
ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning
|
||||
# ignore warning about package resolution using __spec__ or __package__
|
||||
# should be a temporary solution, see #3061 for discussion
|
||||
ignore:.*can't resolve package from __spec__ or __package__.*:ImportWarning
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
|
||||
Reference in New Issue
Block a user