Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d88d1827b | ||
|
|
c2e906ec97 | ||
|
|
1ec6805112 | ||
|
|
6befdf8b46 | ||
|
|
4cb838d978 | ||
|
|
023e1c78df | ||
|
|
6ffa347c77 | ||
|
|
2b50911c9d | ||
|
|
a41820fbf0 | ||
|
|
21725e9304 | ||
|
|
48f52b1be0 | ||
|
|
a5b3ad2e45 | ||
|
|
e30f7094f3 | ||
|
|
5197354375 | ||
|
|
5ac4eff09b | ||
|
|
320e41b142 | ||
|
|
70976b04be | ||
|
|
d65f300988 | ||
|
|
948fd7b8b0 | ||
|
|
f2cebce2eb | ||
|
|
a192e6b430 | ||
|
|
f8a2452247 | ||
|
|
56e6bb0ff6 | ||
|
|
017e504a11 | ||
|
|
9871d5ec2d | ||
|
|
642521a9b3 | ||
|
|
0994829afe | ||
|
|
ce1cc3dddb | ||
|
|
65817dd797 | ||
|
|
c31abb1176 | ||
|
|
d4ca634ef6 | ||
|
|
af00367fed | ||
|
|
9b94313b44 | ||
|
|
5404246e64 | ||
|
|
e0038b82f7 | ||
|
|
0fea71a4f5 | ||
|
|
7571f079c8 | ||
|
|
b5d62cdb55 | ||
|
|
cc25256982 | ||
|
|
da04ff52e4 | ||
|
|
d5b5be6fbe | ||
|
|
3b65d190a4 | ||
|
|
ff04a1fb09 | ||
|
|
1f1d4aaf68 | ||
|
|
196a739f58 | ||
|
|
f20eeebde9 | ||
|
|
b17e6cea21 | ||
|
|
233c2a23de | ||
|
|
46ec0ec43a | ||
|
|
a035c89ea7 | ||
|
|
40228fce5a | ||
|
|
f258b75a24 | ||
|
|
22ab737243 | ||
|
|
0d1f142b1c | ||
|
|
8c475a45bb | ||
|
|
e6e40db9c7 | ||
|
|
cc531a1ca9 | ||
|
|
c3acf049bd | ||
|
|
bab1ef5d38 | ||
|
|
b0c0911ba3 | ||
|
|
3b7fbcd47f | ||
|
|
9ef23b686c | ||
|
|
9fe871016d | ||
|
|
e986d06ade | ||
|
|
dcdf86ef5b | ||
|
|
8d0e1a99e4 | ||
|
|
bf19917537 | ||
|
|
777e9e1e17 | ||
|
|
e041823643 | ||
|
|
6dc575f5ee | ||
|
|
0dc6cb298e | ||
|
|
f466105d66 | ||
|
|
ede3a4e850 | ||
|
|
a4ea66cb1f | ||
|
|
65b97c2f41 | ||
|
|
1786ad16a7 | ||
|
|
66ce952da6 | ||
|
|
eee8201e4f | ||
|
|
e690e191fd | ||
|
|
d40cd3ec6b | ||
|
|
f8f4c16020 | ||
|
|
2a45851c9e | ||
|
|
b51ee48f78 | ||
|
|
f30911d3af | ||
|
|
7bb51b8ceb | ||
|
|
f947cb2613 | ||
|
|
b432f1207c | ||
|
|
43c3f59660 | ||
|
|
f694d8d6ad | ||
|
|
e478f66d8b | ||
|
|
c25310d34f | ||
|
|
d67d189d00 | ||
|
|
8187c148d9 | ||
|
|
e773c8ceda | ||
|
|
4f652c9045 | ||
|
|
126bb0760e | ||
|
|
8c059dbc48 | ||
|
|
fd66f69c19 | ||
|
|
63c01d1541 | ||
|
|
c56d7ac40e | ||
|
|
d9c428c1de | ||
|
|
bd9495486b | ||
|
|
33f1ff4e8c | ||
|
|
1d23bef3fb | ||
|
|
661055105c |
@@ -24,12 +24,12 @@ repos:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v1.3.2
|
||||
rev: v1.3.3
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v1.8.0
|
||||
rev: v1.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--keep-percent-format]
|
||||
|
||||
3
AUTHORS
3
AUTHORS
@@ -59,6 +59,7 @@ Danielle Jenkins
|
||||
Dave Hunt
|
||||
David Díaz-Barquero
|
||||
David Mohr
|
||||
David Szotten
|
||||
David Vierra
|
||||
Daw-Ran Liou
|
||||
Denis Kirisov
|
||||
@@ -161,6 +162,7 @@ Miro Hrončok
|
||||
Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Oleg Pidsadnyi
|
||||
Oleg Sushchenko
|
||||
@@ -202,6 +204,7 @@ Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Sven-Hendrik Haase
|
||||
Tadek Teleżyński
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
|
||||
@@ -18,6 +18,72 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 3.10.0 (2018-11-03)
|
||||
==========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2619 <https://github.com/pytest-dev/pytest/issues/2619>`_: Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``.
|
||||
|
||||
This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the
|
||||
existing ``pytest_enter_pdb`` hook.
|
||||
|
||||
|
||||
- `#4147 <https://github.com/pytest-dev/pytest/issues/4147>`_: Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation <https://docs.pytest.org/en/latest/cache.html#stepwise>`__ for more info.
|
||||
|
||||
|
||||
- `#4188 <https://github.com/pytest-dev/pytest/issues/4188>`_: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed.
|
||||
|
||||
|
||||
- `#4225 <https://github.com/pytest-dev/pytest/issues/4225>`_: Improve performance with collection reporting in non-quiet mode with terminals.
|
||||
|
||||
The "collecting …" message is only printed/updated every 0.5s.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2701 <https://github.com/pytest-dev/pytest/issues/2701>`_: Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings.
|
||||
|
||||
|
||||
- `#4046 <https://github.com/pytest-dev/pytest/issues/4046>`_: Fix problems with running tests in package ``__init__.py`` files.
|
||||
|
||||
|
||||
- `#4260 <https://github.com/pytest-dev/pytest/issues/4260>`_: Swallow warnings during anonymous compilation of source.
|
||||
|
||||
|
||||
- `#4262 <https://github.com/pytest-dev/pytest/issues/4262>`_: Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``.
|
||||
|
||||
|
||||
- `#611 <https://github.com/pytest-dev/pytest/issues/611>`_: Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and
|
||||
should not be overwritten as it will lead to internal errors.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#4255 <https://github.com/pytest-dev/pytest/issues/4255>`_: Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#4272 <https://github.com/pytest-dev/pytest/issues/4272>`_: Display cachedir also in non-verbose mode if non-default.
|
||||
|
||||
|
||||
- `#4277 <https://github.com/pytest-dev/pytest/issues/4277>`_: pdb: improve message about output capturing with ``set_trace``.
|
||||
|
||||
Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid
|
||||
confusion.
|
||||
|
||||
|
||||
- `#4279 <https://github.com/pytest-dev/pytest/issues/4279>`_: Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``.
|
||||
|
||||
|
||||
pytest 3.9.3 (2018-10-27)
|
||||
=========================
|
||||
|
||||
@@ -366,7 +432,7 @@ Features
|
||||
the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
|
||||
``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
|
||||
|
||||
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`_ for more info.
|
||||
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`__ for more info.
|
||||
|
||||
|
||||
- `#2908 <https://github.com/pytest-dev/pytest/issues/2908>`_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is
|
||||
@@ -541,7 +607,7 @@ Bug Fixes
|
||||
- `#3473 <https://github.com/pytest-dev/pytest/issues/3473>`_: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.).
|
||||
|
||||
|
||||
- `#3712 <https://github.com/pytest-dev/pytest/issues/3712>`_: Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``.
|
||||
- `#3712 <https://github.com/pytest-dev/pytest/issues/3712>`_: Correctly represent the dimensions of a numpy array when calling ``repr()`` on ``approx()``.
|
||||
|
||||
- `#3742 <https://github.com/pytest-dev/pytest/issues/3742>`_: Fix incompatibility with third party plugins during collection, which produced the error ``object has no attribute '_collectfile'``.
|
||||
|
||||
|
||||
1
doc/4266.bugfix.rst
Normal file
1
doc/4266.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class.
|
||||
@@ -6,6 +6,7 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.10.0
|
||||
release-3.9.3
|
||||
release-3.9.2
|
||||
release-3.9.1
|
||||
|
||||
43
doc/en/announce/release-3.10.0.rst
Normal file
43
doc/en/announce/release-3.10.0.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
pytest-3.10.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.10.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
https://docs.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/latest/
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anders Hovmöller
|
||||
* Andreu Vallbona Plazas
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bernardo Gomes
|
||||
* Brianna Laugher
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* David Szotten
|
||||
* Mick Koch
|
||||
* Niclas Olofsson
|
||||
* Palash Chatterjee
|
||||
* Ronny Pfannschmidt
|
||||
* Sven-Hendrik Haase
|
||||
* Ville Skyttä
|
||||
* William Jamir Silva
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
@@ -244,6 +244,8 @@ You can always peek at the content of the cache using the
|
||||
{'test_caching.py::test_function': True}
|
||||
cache/nodeids contains:
|
||||
['test_caching.py::test_function']
|
||||
cache/stepwise contains:
|
||||
[]
|
||||
example/value contains:
|
||||
42
|
||||
|
||||
@@ -260,3 +262,9 @@ by adding the ``--cache-clear`` option like this::
|
||||
This is recommended for invocations from Continuous Integration
|
||||
servers where isolation and correctness is more important
|
||||
than speed.
|
||||
|
||||
|
||||
Stepwise
|
||||
--------
|
||||
|
||||
As an alternative to ``--lf -x``, especially for cases where you expect a large part of the test suite will fail, ``--sw``, ``--stepwise`` allows you to fix them one at a time. The test suite will run until the first failure and then stop. At the next invocation, tests will continue from the last failing test and then run until the next failing test. You may use the ``--stepwise-skip`` option to ignore one failing test and stop the test execution on the second failing test instead. This is useful if you get stuck on a failing test and just want to ignore it until later.
|
||||
|
||||
@@ -33,6 +33,7 @@ Full pytest documentation
|
||||
reference
|
||||
|
||||
goodpractices
|
||||
flaky
|
||||
pythonpath
|
||||
customize
|
||||
example/index
|
||||
|
||||
@@ -33,7 +33,7 @@ class Python(object):
|
||||
dumpfile = self.picklefile.dirpath("dump.py")
|
||||
dumpfile.write(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
r"""
|
||||
import pickle
|
||||
f = open({!r}, 'wb')
|
||||
s = pickle.dump({!r}, f, protocol=2)
|
||||
@@ -49,7 +49,7 @@ class Python(object):
|
||||
loadfile = self.picklefile.dirpath("load.py")
|
||||
loadfile.write(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
r"""
|
||||
import pickle
|
||||
f = open({!r}, 'rb')
|
||||
obj = pickle.load(f)
|
||||
|
||||
@@ -85,8 +85,9 @@ interesting to just look at the collection tree::
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collected 2 items
|
||||
<Package '$REGENDOC_TMPDIR/nonpython'>
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
<Package '$REGENDOC_TMPDIR/nonpython'>
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
@@ -153,7 +153,7 @@ This makes use of the automatic caching mechanisms of pytest.
|
||||
|
||||
Another good approach is by adding the data files in the ``tests`` folder.
|
||||
There are also community plugins available to help managing this aspect of
|
||||
testing, e.g. `pytest-datadir <https://github.com/gabrielcnr/pytest-datadir>`__
|
||||
testing, e.g. `pytest-datadir <https://pypi.org/project/pytest-datadir/>`__
|
||||
and `pytest-datafiles <https://pypi.org/project/pytest-datafiles/>`__.
|
||||
|
||||
.. _smtpshared:
|
||||
|
||||
125
doc/en/flaky.rst
Normal file
125
doc/en/flaky.rst
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
Flaky tests
|
||||
-----------
|
||||
|
||||
A "flaky" test is one that exhibits intermittent or sporadic failure, that seems to have non-deterministic behaviour. Sometimes it passes, sometimes it fails, and it's not clear why. This page discusses pytest features that can help and other general strategies for identifying, fixing or mitigating them.
|
||||
|
||||
Why flaky tests are a problem
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Flaky tests are particularly troublesome when a continuous integration (CI) server is being used, so that all tests must pass before a new code change can be merged. If the test result is not a reliable signal -- that a test failure means the code change broke the test -- developers can become mistrustful of the test results, which can lead to overlooking genuine failures. It is also a source of wasted time as developers must re-run test suites and investigate spurious failures.
|
||||
|
||||
|
||||
Potential root causes
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
System state
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
|
||||
|
||||
Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering.
|
||||
|
||||
- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
|
||||
- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
|
||||
- Tests that modify global state typically cannot be run in parallel.
|
||||
|
||||
|
||||
Overly strict assertion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Overly strict assertions can cause problems with floating point comparison as well as timing issues. `pytest.approx <https://docs.pytest.org/en/latest/reference.html#pytest-approx>`_ is useful here.
|
||||
|
||||
|
||||
Pytest features
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Xfail strict
|
||||
~~~~~~~~~~~~
|
||||
|
||||
:ref:`pytest.mark.xfail ref` with ``strict=False`` can be used to mark a test so that its failure does not cause the whole build to break. This could be considered like a manual quarantine, and is rather dangerous to use permanently.
|
||||
|
||||
|
||||
PYTEST_CURRENT_TEST
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:ref:`pytest current test env` may be useful for figuring out "which test got stuck".
|
||||
|
||||
|
||||
Plugins
|
||||
~~~~~~~
|
||||
|
||||
Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this:
|
||||
|
||||
* `flaky <https://github.com/box/flaky>`_
|
||||
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
|
||||
* `pytest-rerunfailures <https://github.com/pytest-dev/pytest-rerunfailures>`_
|
||||
* `pytest-replay <https://github.com/ESSS/pytest-replay>`_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs.
|
||||
|
||||
Plugins to deliberately randomize tests can help expose tests with state problems:
|
||||
|
||||
* `pytest-random-order <https://github.com/jbasko/pytest-random-order>`_
|
||||
* `pytest-randomly <https://github.com/pytest-dev/pytest-randomly>`_
|
||||
|
||||
|
||||
Other general strategies
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Split up test suites
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It can be common to split a single test suite into two, such as unit vs integration, and only use the unit test suite as a CI gate. This also helps keep build times manageable as high level tests tend to be slower. However, it means it does become possible for code that breaks the build to be merged, so extra vigilance is needed for monitoring the integration test results.
|
||||
|
||||
|
||||
Video/screenshot on failure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For UI tests these are important for understanding what the state of the UI was when the test failed. pytest-splinter can be used with plugins like pytest-bdd and can `save a screenshot on test failure <https://pytest-splinter.readthedocs.io/en/latest/#automatic-screenshots-on-test-failure>`_, which can help to isolate the cause.
|
||||
|
||||
|
||||
Delete or rewrite the test
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If the functionality is covered by other tests, perhaps the test can be removed. If not, perhaps it can be rewritten at a lower level which will remove the flakiness or make its source more apparent.
|
||||
|
||||
|
||||
Quarantine
|
||||
~~~~~~~~~~
|
||||
|
||||
Mark Lapierre discusses the `Pros and Cons of Quarantined Tests <https://dev.to/mlapierre/pros-and-cons-of-quarantined-tests-2emj>`_ in a post from 2018.
|
||||
|
||||
|
||||
|
||||
CI tools that rerun on failure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/azure/devops/release-notes/2017/dec-11-vsts#identify-flaky-tests>`_ and rerun failed tests.
|
||||
|
||||
|
||||
|
||||
Research
|
||||
^^^^^^^^
|
||||
|
||||
This is a limited list, please submit an issue or pull request to expand it!
|
||||
|
||||
* Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF <http://www.cs.umd.edu/~atif/pubs/gao-icse15.pdf>`__
|
||||
* Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive <https://drive.google.com/file/d/10HdcCQiuQVgW3yYUJD-TSTq1NbYEprl0/view>`__
|
||||
* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF <https://www.jonbell.net/icse18-deflaker.pdf>`__
|
||||
|
||||
|
||||
Resources
|
||||
^^^^^^^^^
|
||||
|
||||
* `Eradicating Non-Determinism in Tests <https://martinfowler.com/articles/nonDeterminism.html>`_ by Martin Fowler, 2011
|
||||
* `No more flaky tests on the Go team <https://www.thoughtworks.com/insights/blog/no-more-flaky-tests-go-team>`_ by Pavan Sudarshan, 2012
|
||||
* `The Build That Cried Broken: Building Trust in your Continuous Integration Tests <https://www.youtube.com/embed/VotJqV4n8ig>`_ talk (video) by `Angie Jones <http://angiejones.tech/>`_ at SeleniumConf Austin 2017
|
||||
* `Test and Code Podcast: Flaky Tests and How to Deal with Them <https://testandcode.com/50>`_ by Brian Okken and Anthony Shaw, 2018
|
||||
* Microsoft:
|
||||
|
||||
* `How we approach testing VSTS to enable continuous delivery <https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/>`_ by Brian Harry MS, 2017
|
||||
* `Eliminating Flaky Tests <https://docs.microsoft.com/en-us/azure/devops/learn/devops-at-microsoft/eliminating-flaky-tests>`_ blog and talk (video) by Munil Shah, 2017
|
||||
|
||||
* Google:
|
||||
|
||||
* `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
|
||||
* `Where do Google's flaky tests come from? <https://docs.google.com/document/d/1mZ0-Kc97DI_F3tf_GBW_NB_aqka-P1jVOsFfufxqUUM/edit#heading=h.ec0r4fypsleh>`_ by Jeff Listfield, 2017
|
||||
@@ -75,7 +75,7 @@ Issues
|
||||
------
|
||||
|
||||
* By using ``request.getfuncargvalue()`` we rely on actual fixture function
|
||||
execution to know what fixtures are involved, due to it's dynamic nature
|
||||
execution to know what fixtures are involved, due to its dynamic nature
|
||||
* More importantly, ``request.getfuncargvalue()`` cannot be combined with
|
||||
parametrized fixtures, such as ``extra_context``
|
||||
* This is very inconvenient if you wish to extend an existing test suite by
|
||||
|
||||
@@ -117,6 +117,7 @@ Add warning filters to marked test items.
|
||||
A *warning specification string*, which is composed of contents of the tuple ``(action, message, category, module, lineno)``
|
||||
as specified in `The Warnings filter <https://docs.python.org/3/library/warnings.html#warning-filter>`_ section of
|
||||
the Python documentation, separated by ``":"``. Optional fields can be omitted.
|
||||
Module names passed for filtering are not regex-escaped.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ Books
|
||||
Talks and blog postings
|
||||
---------------------------------------------
|
||||
|
||||
- pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english <http://talks.apsl.io/testing-pycones-2017/>`_, `video in spanish <https://www.youtube.com/watch?v=K20GeR-lXDk>`_)
|
||||
|
||||
- `Pythonic testing, Igor Starikov (Russian, PyNsk, November 2016)
|
||||
<https://www.youtube.com/watch?v=_92nfdd5nK8>`_.
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ return a result object, with which we can assert the tests' outcomes.
|
||||
result.assert_outcomes(passed=4)
|
||||
|
||||
|
||||
additionally it is possible to copy examples for a example folder before running pytest on it
|
||||
additionally it is possible to copy examples for an example folder before running pytest on it
|
||||
|
||||
.. code:: ini
|
||||
|
||||
@@ -421,21 +421,9 @@ additionally it is possible to copy examples for a example folder before running
|
||||
test_example.py::test_plugin
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead
|
||||
return getattr(object, name, default)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 2 passed, 7 warnings in 0.12 seconds ===================
|
||||
=================== 2 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
|
||||
@@ -8,14 +8,13 @@ import linecache
|
||||
import sys
|
||||
import textwrap
|
||||
import tokenize
|
||||
import warnings
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
from bisect import bisect_right
|
||||
|
||||
import py
|
||||
import six
|
||||
|
||||
cpy_compile = compile
|
||||
|
||||
|
||||
class Source(object):
|
||||
""" an immutable object holding a source code fragment,
|
||||
@@ -161,7 +160,7 @@ class Source(object):
|
||||
filename = base + "%r %s:%d>" % (filename, fn, lineno)
|
||||
source = "\n".join(self.lines) + "\n"
|
||||
try:
|
||||
co = cpy_compile(source, filename, mode, flag)
|
||||
co = compile(source, filename, mode, flag)
|
||||
except SyntaxError:
|
||||
ex = sys.exc_info()[1]
|
||||
# re-represent syntax errors from parsing python strings
|
||||
@@ -195,7 +194,7 @@ def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0):
|
||||
"""
|
||||
if isinstance(source, ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return cpy_compile(source, filename, mode, flags, dont_inherit)
|
||||
return compile(source, filename, mode, flags, dont_inherit)
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
s = Source(source)
|
||||
co = s.compile(filename, mode, flags, _genframe=_genframe)
|
||||
@@ -290,7 +289,11 @@ def get_statement_startend2(lineno, node):
|
||||
def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
|
||||
if astnode is None:
|
||||
content = str(source)
|
||||
astnode = compile(content, "source", "exec", 1024) # 1024 for AST
|
||||
# See #4260:
|
||||
# don't produce duplicate warnings when compiling source to find ast
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
astnode = compile(content, "source", "exec", _AST_FLAG)
|
||||
|
||||
start, end = get_statement_startend2(lineno, astnode)
|
||||
# we need to correct the end:
|
||||
|
||||
@@ -319,7 +319,8 @@ def cache(request):
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
if config.option.verbose:
|
||||
"""Display cachedir with --cache-show and if non-default."""
|
||||
if config.option.verbose or config.getini("cache_dir") != ".pytest_cache":
|
||||
cachedir = config.cache._cachedir
|
||||
# TODO: evaluate generating upward relative paths
|
||||
# starting with .., ../.. if sensible
|
||||
|
||||
@@ -102,6 +102,9 @@ class CaptureManager(object):
|
||||
|
||||
# Global capturing control
|
||||
|
||||
def is_globally_capturing(self):
|
||||
return self._method != "no"
|
||||
|
||||
def start_global_capturing(self):
|
||||
assert self._global_capturing is None
|
||||
self._global_capturing = self._getcapture(self._method)
|
||||
@@ -124,7 +127,7 @@ class CaptureManager(object):
|
||||
def read_global_capture(self):
|
||||
return self._global_capturing.readouterr()
|
||||
|
||||
# Fixture Control (its just forwarding, think about removing this later)
|
||||
# Fixture Control (it's just forwarding, think about removing this later)
|
||||
|
||||
def activate_fixture(self, item):
|
||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
||||
|
||||
@@ -334,6 +334,14 @@ def safe_getattr(object, name, default):
|
||||
return default
|
||||
|
||||
|
||||
def safe_isclass(obj):
|
||||
"""Ignore any exception via isinstance on Python 3."""
|
||||
try:
|
||||
return isclass(obj)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _is_unittest_unexpected_success_a_failure():
|
||||
"""Return if the test suite should fail if an @expectedFailure unittest test PASSES.
|
||||
|
||||
@@ -420,3 +428,16 @@ class FuncargnamesCompatAttr(object):
|
||||
def funcargnames(self):
|
||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||
return self.fixturenames
|
||||
|
||||
|
||||
if six.PY2:
|
||||
|
||||
def lru_cache(*_, **__):
|
||||
def dec(fn):
|
||||
return fn
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
else:
|
||||
from functools import lru_cache # noqa: F401
|
||||
|
||||
@@ -27,6 +27,7 @@ from .findpaths import determine_setup
|
||||
from .findpaths import exists
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest.compat import lru_cache
|
||||
from _pytest.compat import safe_str
|
||||
from _pytest.outcomes import Skipped
|
||||
|
||||
@@ -133,6 +134,7 @@ default_plugins = (
|
||||
"freeze_support",
|
||||
"setuponly",
|
||||
"setupplan",
|
||||
"stepwise",
|
||||
"warnings",
|
||||
"logging",
|
||||
)
|
||||
@@ -212,7 +214,7 @@ class PytestPluginManager(PluginManager):
|
||||
self._conftest_plugins = set()
|
||||
|
||||
# state related to local conftest plugins
|
||||
self._path2confmods = {}
|
||||
self._dirpath2confmods = {}
|
||||
self._conftestpath2mod = {}
|
||||
self._confcutdir = None
|
||||
self._noconftest = False
|
||||
@@ -383,31 +385,35 @@ class PytestPluginManager(PluginManager):
|
||||
if x.check(dir=1):
|
||||
self._getconftestmodules(x)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _getconftestmodules(self, path):
|
||||
if self._noconftest:
|
||||
return []
|
||||
|
||||
try:
|
||||
return self._path2confmods[path]
|
||||
except KeyError:
|
||||
if path.isfile():
|
||||
directory = path.dirpath()
|
||||
else:
|
||||
directory = path
|
||||
# XXX these days we may rather want to use config.rootdir
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir
|
||||
clist = []
|
||||
for parent in directory.realpath().parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
if path.isfile():
|
||||
directory = path.dirpath()
|
||||
else:
|
||||
directory = path
|
||||
|
||||
self._path2confmods[path] = clist
|
||||
return clist
|
||||
if six.PY2: # py2 is not using lru_cache.
|
||||
try:
|
||||
return self._dirpath2confmods[directory]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# XXX these days we may rather want to use config.rootdir
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir
|
||||
clist = []
|
||||
for parent in directory.realpath().parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _rget_with_confmod(self, name, path):
|
||||
modules = self._getconftestmodules(path)
|
||||
@@ -448,8 +454,8 @@ class PytestPluginManager(PluginManager):
|
||||
self._conftest_plugins.add(mod)
|
||||
self._conftestpath2mod[conftestpath] = mod
|
||||
dirpath = conftestpath.dirpath()
|
||||
if dirpath in self._path2confmods:
|
||||
for path, mods in self._path2confmods.items():
|
||||
if dirpath in self._dirpath2confmods:
|
||||
for path, mods in self._dirpath2confmods.items():
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
|
||||
@@ -152,7 +152,7 @@ class ArgumentError(Exception):
|
||||
class Argument(object):
|
||||
"""class that mimics the necessary behaviour of optparse.Option
|
||||
|
||||
its currently a least effort implementation
|
||||
it's currently a least effort implementation
|
||||
and ignoring choices and integer prefixes
|
||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||
"""
|
||||
|
||||
@@ -80,10 +80,54 @@ class pytestPDB(object):
|
||||
capman.suspend_global_capture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
|
||||
if capman and capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
else:
|
||||
tw.sep(">", "PDB set_trace")
|
||||
|
||||
class _PdbWrapper(cls._pdb_cls, object):
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
def do_continue(self, arg):
|
||||
ret = super(_PdbWrapper, self).do_continue(arg)
|
||||
if self._pytest_capman:
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
if self._pytest_capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB continue (IO-capturing resumed)")
|
||||
else:
|
||||
tw.sep(">", "PDB continue")
|
||||
self._pytest_capman.resume_global_capture()
|
||||
cls._pluginmanager.hook.pytest_leave_pdb(
|
||||
config=cls._config, pdb=self
|
||||
)
|
||||
self._continued = True
|
||||
return ret
|
||||
|
||||
do_c = do_cont = do_continue
|
||||
|
||||
def setup(self, f, tb):
|
||||
"""Suspend on setup().
|
||||
|
||||
Needed after do_continue resumed, and entering another
|
||||
breakpoint again.
|
||||
"""
|
||||
ret = super(_PdbWrapper, self).setup(f, tb)
|
||||
if not ret and self._continued:
|
||||
# pdb.setup() returns True if the command wants to exit
|
||||
# from the interaction: do not suspend capturing then.
|
||||
if self._pytest_capman:
|
||||
self._pytest_capman.suspend_global_capture(in_=True)
|
||||
return ret
|
||||
|
||||
_pdb = _PdbWrapper()
|
||||
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
|
||||
else:
|
||||
_pdb = cls._pdb_cls()
|
||||
|
||||
if set_break:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
_pdb.set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke(object):
|
||||
|
||||
@@ -12,6 +12,7 @@ from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
@@ -57,6 +58,10 @@ FIXTURE_FUNCTION_CALL = UnformattedWarning(
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information.",
|
||||
)
|
||||
|
||||
FIXTURE_NAMED_REQUEST = PytestDeprecationWarning(
|
||||
"'request' is a reserved name for fixtures and will raise an error in future versions"
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.",
|
||||
|
||||
@@ -34,6 +34,7 @@ from _pytest.compat import isclass
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL
|
||||
from _pytest.deprecated import FIXTURE_NAMED_REQUEST
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
@@ -618,7 +619,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
subrequest._check_scope(argname, self.scope, scope)
|
||||
|
||||
# clear sys.exc_info before invoking the fixture (python bug?)
|
||||
# if its not explicitly cleared it will leak into the call
|
||||
# if it's not explicitly cleared it will leak into the call
|
||||
exc_clear()
|
||||
try:
|
||||
# call the fixture function
|
||||
@@ -1036,6 +1037,9 @@ class FixtureFunctionMarker(object):
|
||||
|
||||
function = wrap_function_to_warning_if_called_directly(function, self)
|
||||
|
||||
name = self.name or function.__name__
|
||||
if name == "request":
|
||||
warnings.warn(FIXTURE_NAMED_REQUEST)
|
||||
function._pytestfixturefunction = self
|
||||
return function
|
||||
|
||||
@@ -1193,6 +1197,7 @@ class FixtureManager(object):
|
||||
nodeid = p.dirpath().relto(self.config.rootdir)
|
||||
if p.sep != nodes.SEP:
|
||||
nodeid = nodeid.replace(p.sep, nodes.SEP)
|
||||
|
||||
self.parsefactories(plugin, nodeid)
|
||||
|
||||
def _getautousenames(self, nodeid):
|
||||
@@ -1297,11 +1302,18 @@ class FixtureManager(object):
|
||||
nodeid = node_or_obj.nodeid
|
||||
if holderobj in self._holderobjseen:
|
||||
return
|
||||
|
||||
from _pytest.nodes import _CompatProperty
|
||||
|
||||
self._holderobjseen.add(holderobj)
|
||||
autousenames = []
|
||||
for name in dir(holderobj):
|
||||
# The attribute can be an arbitrary descriptor, so the attribute
|
||||
# access below can raise. safe_getatt() ignores such exceptions.
|
||||
maybe_property = safe_getattr(type(holderobj), name, None)
|
||||
if isinstance(maybe_property, _CompatProperty):
|
||||
# deprecated
|
||||
continue
|
||||
obj = safe_getattr(holderobj, name, None)
|
||||
marker = getfixturemarker(obj)
|
||||
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
|
||||
|
||||
@@ -41,10 +41,10 @@ def pytest_namespace():
|
||||
Plugins whose users depend on the current namespace functionality should prepare to migrate to a
|
||||
namespace they actually own.
|
||||
|
||||
To support the migration its suggested to trigger ``DeprecationWarnings`` for objects they put into the
|
||||
To support the migration it's suggested to trigger ``DeprecationWarnings`` for objects they put into the
|
||||
pytest namespace.
|
||||
|
||||
An stopgap measure to avoid the warning is to monkeypatch the ``pytest`` module, but just as the
|
||||
A stopgap measure to avoid the warning is to monkeypatch the ``pytest`` module, but just as the
|
||||
``pytest_namespace`` hook this should be seen as a temporary measure to be removed in future versions after
|
||||
an appropriate transition period.
|
||||
"""
|
||||
@@ -603,9 +603,21 @@ def pytest_exception_interact(node, call, report):
|
||||
"""
|
||||
|
||||
|
||||
def pytest_enter_pdb(config):
|
||||
def pytest_enter_pdb(config, pdb):
|
||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param pdb.Pdb pdb: Pdb instance
|
||||
"""
|
||||
|
||||
|
||||
def pytest_leave_pdb(config, pdb):
|
||||
""" called when leaving pdb (e.g. with continue after pdb.set_trace()).
|
||||
|
||||
Can be used by plugins to take special action just after the python
|
||||
debugger leaves interactive mode.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param pdb.Pdb pdb: Pdb instance
|
||||
"""
|
||||
|
||||
@@ -18,6 +18,7 @@ from _pytest.config import directory_arg
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.pathlib import parts
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
|
||||
@@ -281,15 +282,6 @@ def pytest_ignore_collect(path, config):
|
||||
if _in_venv(path) and not allow_in_venv:
|
||||
return True
|
||||
|
||||
# Skip duplicate paths.
|
||||
keepduplicates = config.getoption("keepduplicates")
|
||||
duplicate_paths = config.pluginmanager._duplicatepaths
|
||||
if not keepduplicates:
|
||||
if path in duplicate_paths:
|
||||
return True
|
||||
else:
|
||||
duplicate_paths.add(path)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -478,8 +470,8 @@ class Session(nodes.FSCollector):
|
||||
return items
|
||||
|
||||
def collect(self):
|
||||
for parts in self._initialparts:
|
||||
arg = "::".join(map(str, parts))
|
||||
for initialpart in self._initialparts:
|
||||
arg = "::".join(map(str, initialpart))
|
||||
self.trace("processing argument", arg)
|
||||
self.trace.root.indent += 1
|
||||
try:
|
||||
@@ -497,7 +489,7 @@ class Session(nodes.FSCollector):
|
||||
|
||||
names = self._parsearg(arg)
|
||||
argpath = names.pop(0).realpath()
|
||||
paths = []
|
||||
paths = set()
|
||||
|
||||
root = self
|
||||
# Start with a Session root, and delve to argpath item (dir or file)
|
||||
@@ -526,21 +518,37 @@ class Session(nodes.FSCollector):
|
||||
# Let the Package collector deal with subnodes, don't collect here.
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
for path in argpath.visit(
|
||||
fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
pkginit = path.dirpath().join("__init__.py")
|
||||
if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
|
||||
for x in root._collectfile(pkginit):
|
||||
yield x
|
||||
paths.append(x.fspath.dirpath())
|
||||
|
||||
if not any(x in path.parts() for x in paths):
|
||||
if six.PY2:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1) and not f.strpath.endswith("*.pyc")
|
||||
|
||||
else:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1)
|
||||
|
||||
seen_dirs = set()
|
||||
for path in argpath.visit(
|
||||
fil=filter_, rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
dirpath = path.dirpath()
|
||||
if dirpath not in seen_dirs:
|
||||
seen_dirs.add(dirpath)
|
||||
pkginit = dirpath.join("__init__.py")
|
||||
if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths):
|
||||
for x in root._collectfile(pkginit):
|
||||
yield x
|
||||
paths.add(x.fspath.dirpath())
|
||||
|
||||
if parts(path.strpath).isdisjoint(paths):
|
||||
for x in root._collectfile(path):
|
||||
if (type(x), x.fspath) in self._node_cache:
|
||||
yield self._node_cache[(type(x), x.fspath)]
|
||||
key = (type(x), x.fspath)
|
||||
if key in self._node_cache:
|
||||
yield self._node_cache[key]
|
||||
else:
|
||||
self._node_cache[(type(x), x.fspath)] = x
|
||||
self._node_cache[key] = x
|
||||
yield x
|
||||
else:
|
||||
assert argpath.check(file=1)
|
||||
@@ -551,7 +559,15 @@ class Session(nodes.FSCollector):
|
||||
col = root._collectfile(argpath)
|
||||
if col:
|
||||
self._node_cache[argpath] = col
|
||||
for y in self.matchnodes(col, names):
|
||||
m = self.matchnodes(col, names)
|
||||
# If __init__.py was the only file requested, then the matched node will be
|
||||
# the corresponding Package, and the first yielded item will be the __init__
|
||||
# Module itself, so just use that. If this special case isn't taken, then all
|
||||
# the files in the package will be yielded.
|
||||
if argpath.basename == "__init__.py":
|
||||
yield next(m[0].collect())
|
||||
return
|
||||
for y in m:
|
||||
yield y
|
||||
|
||||
def _collectfile(self, path):
|
||||
@@ -559,17 +575,29 @@ class Session(nodes.FSCollector):
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return ()
|
||||
|
||||
# Skip duplicate paths.
|
||||
keepduplicates = self.config.getoption("keepduplicates")
|
||||
if not keepduplicates:
|
||||
duplicate_paths = self.config.pluginmanager._duplicatepaths
|
||||
if path in duplicate_paths:
|
||||
return ()
|
||||
else:
|
||||
duplicate_paths.add(path)
|
||||
|
||||
return ihook.pytest_collect_file(path=path, parent=self)
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return
|
||||
def _recurse(self, dirpath):
|
||||
if dirpath.basename == "__pycache__":
|
||||
return False
|
||||
ihook = self.gethookproxy(dirpath.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
|
||||
return False
|
||||
for pat in self._norecursepatterns:
|
||||
if path.check(fnmatch=pat):
|
||||
if dirpath.check(fnmatch=pat):
|
||||
return False
|
||||
ihook = self.gethookproxy(path)
|
||||
ihook.pytest_collect_directory(path=path, parent=self)
|
||||
ihook = self.gethookproxy(dirpath)
|
||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
||||
return True
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
|
||||
@@ -443,7 +443,7 @@ class NodeKeywords(MappingMixin):
|
||||
@attr.s(cmp=False, hash=False)
|
||||
class NodeMarkers(object):
|
||||
"""
|
||||
internal strucutre for storing marks belongong to a node
|
||||
internal structure for storing marks belonging to a node
|
||||
|
||||
..warning::
|
||||
|
||||
|
||||
@@ -230,10 +230,12 @@ class MonkeyPatch(object):
|
||||
if not isinstance(value, str):
|
||||
warnings.warn(
|
||||
pytest.PytestWarning(
|
||||
"Environment variable value {!r} should be str, converted to str implicitly".format(
|
||||
value
|
||||
"Value of environment variable {name} type should be str, but got "
|
||||
"{value!r} (type: {type}); converted to str implicitly".format(
|
||||
name=name, value=value, type=type(value).__name__
|
||||
)
|
||||
)
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
value = str(value)
|
||||
if prepend and name in os.environ:
|
||||
|
||||
@@ -5,7 +5,6 @@ import itertools
|
||||
import operator
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
import uuid
|
||||
from functools import reduce
|
||||
@@ -36,24 +35,17 @@ get_lock_path = operator.methodcaller("joinpath", ".lock")
|
||||
|
||||
def ensure_reset_dir(path):
|
||||
"""
|
||||
ensures the given path is a empty directory
|
||||
ensures the given path is an empty directory
|
||||
"""
|
||||
if path.exists():
|
||||
rmtree(path, force=True)
|
||||
path.mkdir()
|
||||
|
||||
|
||||
def _shutil_rmtree_remove_writable(func, fspath, _):
|
||||
"Clear the readonly bit and reattempt the removal"
|
||||
os.chmod(fspath, stat.S_IWRITE)
|
||||
func(fspath)
|
||||
|
||||
|
||||
def rmtree(path, force=False):
|
||||
if force:
|
||||
# ignore_errors leaves dead folders around
|
||||
# python needs a rm -rf as a followup
|
||||
# the trick with _shutil_rmtree_remove_writable is unreliable
|
||||
# NOTE: ignore_errors might leave dead folders around.
|
||||
# Python needs a rm -rf as a followup.
|
||||
shutil.rmtree(str(path), ignore_errors=True)
|
||||
else:
|
||||
shutil.rmtree(str(path))
|
||||
@@ -106,8 +98,8 @@ else:
|
||||
def _force_symlink(root, target, link_to):
|
||||
"""helper to create the current symlink
|
||||
|
||||
its full of race conditions that are reasonably ok to ignore
|
||||
for the contex of best effort linking to the latest testrun
|
||||
it's full of race conditions that are reasonably ok to ignore
|
||||
for the context of best effort linking to the latest testrun
|
||||
|
||||
the presumption being thatin case of much parallelism
|
||||
the inaccuracy is going to be acceptable
|
||||
@@ -124,7 +116,7 @@ def _force_symlink(root, target, link_to):
|
||||
|
||||
|
||||
def make_numbered_dir(root, prefix):
|
||||
"""create a directory with a increased number as suffix for the given prefix"""
|
||||
"""create a directory with an increased number as suffix for the given prefix"""
|
||||
for i in range(10):
|
||||
# try up to 10 times to create the folder
|
||||
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
@@ -164,7 +156,7 @@ def create_cleanup_lock(p):
|
||||
os.write(fd, spid)
|
||||
os.close(fd)
|
||||
if not lock_path.is_file():
|
||||
raise EnvironmentError("lock path got renamed after sucessfull creation")
|
||||
raise EnvironmentError("lock path got renamed after successful creation")
|
||||
return lock_path
|
||||
|
||||
|
||||
@@ -186,19 +178,29 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register):
|
||||
|
||||
|
||||
def maybe_delete_a_numbered_dir(path):
|
||||
"""removes a numbered directory if its lock can be obtained"""
|
||||
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
|
||||
lock_path = None
|
||||
try:
|
||||
create_cleanup_lock(path)
|
||||
lock_path = create_cleanup_lock(path)
|
||||
parent = path.parent
|
||||
|
||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||
path.rename(garbage)
|
||||
rmtree(garbage, force=True)
|
||||
except (OSError, EnvironmentError):
|
||||
# known races:
|
||||
# * other process did a cleanup at the same time
|
||||
# * deletable folder was found
|
||||
# * process cwd (Windows)
|
||||
return
|
||||
parent = path.parent
|
||||
|
||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||
path.rename(garbage)
|
||||
rmtree(garbage, force=True)
|
||||
finally:
|
||||
# if we created the lock, ensure we remove it even if we failed
|
||||
# to properly remove the numbered dir
|
||||
if lock_path is not None:
|
||||
try:
|
||||
lock_path.unlink()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
|
||||
def ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
@@ -221,7 +223,7 @@ def ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
|
||||
|
||||
def try_cleanup(path, consider_lock_dead_if_created_before):
|
||||
"""tries to cleanup a folder if we can ensure its deletable"""
|
||||
"""tries to cleanup a folder if we can ensure it's deletable"""
|
||||
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
maybe_delete_a_numbered_dir(path)
|
||||
|
||||
@@ -311,3 +313,8 @@ def fnmatch_ex(pattern, path):
|
||||
else:
|
||||
name = six.text_type(path)
|
||||
return fnmatch.fnmatch(name, pattern)
|
||||
|
||||
|
||||
def parts(s):
|
||||
parts = s.split(sep)
|
||||
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
|
||||
|
||||
@@ -33,6 +33,7 @@ from _pytest.compat import NoneType
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import REGEX_TYPE
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.compat import safe_str
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.config import hookimpl
|
||||
@@ -41,6 +42,7 @@ from _pytest.mark.structures import get_unpacked_marks
|
||||
from _pytest.mark.structures import normalize_mark_list
|
||||
from _pytest.mark.structures import transfer_markers
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import parts
|
||||
from _pytest.warning_types import PytestWarning
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
@@ -195,7 +197,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if res is not None:
|
||||
return
|
||||
# nothing was collected elsewhere, let's do it here
|
||||
if isclass(obj):
|
||||
if safe_isclass(obj):
|
||||
if collector.istestclass(obj, name):
|
||||
Class = collector._getcustomclass("Class")
|
||||
outcome.force_result(Class(name, parent=collector))
|
||||
@@ -516,15 +518,17 @@ class Package(Module):
|
||||
self._norecursepatterns = session._norecursepatterns
|
||||
self.fspath = fspath
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
def _recurse(self, dirpath):
|
||||
if dirpath.basename == "__pycache__":
|
||||
return False
|
||||
ihook = self.gethookproxy(dirpath.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=dirpath, config=self.config):
|
||||
return
|
||||
for pat in self._norecursepatterns:
|
||||
if path.check(fnmatch=pat):
|
||||
if dirpath.check(fnmatch=pat):
|
||||
return False
|
||||
ihook = self.gethookproxy(path)
|
||||
ihook.pytest_collect_directory(path=path, parent=self)
|
||||
ihook = self.gethookproxy(dirpath)
|
||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
||||
return True
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
@@ -552,15 +556,6 @@ class Package(Module):
|
||||
return path in self.session._initialpaths
|
||||
|
||||
def collect(self):
|
||||
# XXX: HACK!
|
||||
# Before starting to collect any files from this package we need
|
||||
# to cleanup the duplicate paths added by the session's collect().
|
||||
# Proper fix is to not track these as duplicates in the first place.
|
||||
for path in list(self.session.config.pluginmanager._duplicatepaths):
|
||||
# if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts():
|
||||
if path.dirname.startswith(self.name):
|
||||
self.session.config.pluginmanager._duplicatepaths.remove(path)
|
||||
|
||||
this_path = self.fspath.dirpath()
|
||||
init_module = this_path.join("__init__.py")
|
||||
if init_module.check(file=1) and path_matches_patterns(
|
||||
@@ -569,19 +564,16 @@ class Package(Module):
|
||||
yield Module(init_module, self)
|
||||
pkg_prefixes = set()
|
||||
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
||||
# we will visit our own __init__.py file, in which case we skip it
|
||||
skip = False
|
||||
if path.basename == "__init__.py" and path.dirpath() == this_path:
|
||||
continue
|
||||
# We will visit our own __init__.py file, in which case we skip it.
|
||||
if path.isfile():
|
||||
if path.basename == "__init__.py" and path.dirpath() == this_path:
|
||||
continue
|
||||
|
||||
for pkg_prefix in pkg_prefixes:
|
||||
if (
|
||||
pkg_prefix in path.parts()
|
||||
and pkg_prefix.join("__init__.py") != path
|
||||
):
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
parts_ = parts(path.strpath)
|
||||
if any(
|
||||
pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path
|
||||
for pkg_prefix in pkg_prefixes
|
||||
):
|
||||
continue
|
||||
|
||||
if path.isdir() and path.join("__init__.py").check(file=1):
|
||||
@@ -661,7 +653,7 @@ class Instance(PyCollector):
|
||||
_ALLOW_MARKERS = False # hack, destroy later
|
||||
# instances share the object with their parents in a way
|
||||
# that duplicates markers instances if not taken out
|
||||
# can be removed at node strucutre reorganization time
|
||||
# can be removed at node structure reorganization time
|
||||
|
||||
def _getobj(self):
|
||||
return self.parent.obj()
|
||||
@@ -1343,7 +1335,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
"""
|
||||
|
||||
_genid = None
|
||||
# disable since functions handle it themselfes
|
||||
# disable since functions handle it themselves
|
||||
_ALLOW_MARKERS = False
|
||||
|
||||
def __init__(
|
||||
|
||||
102
src/_pytest/stepwise.py
Normal file
102
src/_pytest/stepwise.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
"--sw",
|
||||
"--stepwise",
|
||||
action="store_true",
|
||||
dest="stepwise",
|
||||
help="exit on test fail and continue from last failing test next time",
|
||||
)
|
||||
group.addoption(
|
||||
"--stepwise-skip",
|
||||
action="store_true",
|
||||
dest="stepwise_skip",
|
||||
help="ignore the first failing test but stop on the next failing test",
|
||||
)
|
||||
|
||||
|
||||
@pytest.hookimpl
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
|
||||
|
||||
|
||||
class StepwisePlugin:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.active = config.getvalue("stepwise")
|
||||
self.session = None
|
||||
|
||||
if self.active:
|
||||
self.lastfailed = config.cache.get("cache/stepwise", None)
|
||||
self.skip = config.getvalue("stepwise_skip")
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
self.session = session
|
||||
|
||||
def pytest_collection_modifyitems(self, session, config, items):
|
||||
if not self.active or not self.lastfailed:
|
||||
return
|
||||
|
||||
already_passed = []
|
||||
found = False
|
||||
|
||||
# Make a list of all tests that have been run before the last failing one.
|
||||
for item in items:
|
||||
if item.nodeid == self.lastfailed:
|
||||
found = True
|
||||
break
|
||||
else:
|
||||
already_passed.append(item)
|
||||
|
||||
# If the previously failed test was not found among the test items,
|
||||
# do not skip any tests.
|
||||
if not found:
|
||||
already_passed = []
|
||||
|
||||
for item in already_passed:
|
||||
items.remove(item)
|
||||
|
||||
config.hook.pytest_deselected(items=already_passed)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if self.active and report.failed:
|
||||
self.session.shouldstop = (
|
||||
"Error when collecting test, stopping test execution."
|
||||
)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
# Skip this hook if plugin is not active or the test is xfailed.
|
||||
if not self.active or "xfail" in report.keywords:
|
||||
return
|
||||
|
||||
if report.failed:
|
||||
if self.skip:
|
||||
# Remove test from the failed ones (if it exists) and unset the skip option
|
||||
# to make sure the following tests will not be skipped.
|
||||
if report.nodeid == self.lastfailed:
|
||||
self.lastfailed = None
|
||||
|
||||
self.skip = False
|
||||
else:
|
||||
# Mark test as the last failing and interrupt the test session.
|
||||
self.lastfailed = report.nodeid
|
||||
self.session.shouldstop = (
|
||||
"Test failed, continuing from this test next run."
|
||||
)
|
||||
|
||||
else:
|
||||
# If the test was actually run and did pass.
|
||||
if report.when == "call":
|
||||
# Remove test from the failed ones, if exists.
|
||||
if report.nodeid == self.lastfailed:
|
||||
self.lastfailed = None
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
if self.active:
|
||||
self.config.cache.set("cache/stepwise", self.lastfailed)
|
||||
else:
|
||||
# Clear the list of failing tests if the plugin is not active.
|
||||
self.config.cache.set("cache/stepwise", [])
|
||||
@@ -246,6 +246,7 @@ class TerminalReporter(object):
|
||||
self.isatty = file.isatty()
|
||||
self._progress_nodeids_reported = set()
|
||||
self._show_progress_info = self._determine_show_progress_info()
|
||||
self._collect_report_last_write = None
|
||||
|
||||
def _determine_show_progress_info(self):
|
||||
"""Return True if we should display progress information based on the current config"""
|
||||
@@ -261,7 +262,7 @@ class TerminalReporter(object):
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, nodeid, res):
|
||||
def write_fspath_result(self, nodeid, res, **markup):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
if self.currentfspath is not None and self._show_progress_info:
|
||||
@@ -270,7 +271,7 @@ class TerminalReporter(object):
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
self._tw.write(fspath + " ")
|
||||
self._tw.write(res)
|
||||
self._tw.write(res, **markup)
|
||||
|
||||
def write_ensure_prefix(self, prefix, extra="", **kwargs):
|
||||
if self.currentfspath != prefix:
|
||||
@@ -384,22 +385,22 @@ class TerminalReporter(object):
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
running_xdist = hasattr(rep, "node")
|
||||
if markup is None:
|
||||
if rep.passed:
|
||||
markup = {"green": True}
|
||||
elif rep.failed:
|
||||
markup = {"red": True}
|
||||
elif rep.skipped:
|
||||
markup = {"yellow": True}
|
||||
else:
|
||||
markup = {}
|
||||
if self.verbosity <= 0:
|
||||
if not running_xdist and self.showfspath:
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
self.write_fspath_result(rep.nodeid, letter, **markup)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
self._tw.write(letter, **markup)
|
||||
else:
|
||||
self._progress_nodeids_reported.add(rep.nodeid)
|
||||
if markup is None:
|
||||
if rep.passed:
|
||||
markup = {"green": True}
|
||||
elif rep.failed:
|
||||
markup = {"red": True}
|
||||
elif rep.skipped:
|
||||
markup = {"yellow": True}
|
||||
else:
|
||||
markup = {}
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
@@ -472,7 +473,11 @@ class TerminalReporter(object):
|
||||
return self._tw.chars_on_current_line
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
if self.isatty:
|
||||
if self.config.option.verbose >= 0:
|
||||
self.write("collecting ... ", bold=True)
|
||||
self._collect_report_last_write = time.time()
|
||||
elif self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
@@ -483,13 +488,19 @@ class TerminalReporter(object):
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.isatty:
|
||||
# self.write_fspath_result(report.nodeid, 'E')
|
||||
self.report_collect()
|
||||
|
||||
def report_collect(self, final=False):
|
||||
if self.config.option.verbose < 0:
|
||||
return
|
||||
|
||||
if not final:
|
||||
# Only write "collecting" report every 0.5s.
|
||||
t = time.time()
|
||||
if self._collect_report_last_write > t - 0.5:
|
||||
return
|
||||
self._collect_report_last_write = t
|
||||
|
||||
errors = len(self.stats.get("error", []))
|
||||
skipped = len(self.stats.get("skipped", []))
|
||||
deselected = len(self.stats.get("deselected", []))
|
||||
|
||||
@@ -6,6 +6,8 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.pytester_example_path("deprecated")
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_yield_tests_deprecation(testdir):
|
||||
@@ -394,3 +396,13 @@ def test_pycollector_makeitem_is_deprecated():
|
||||
with pytest.warns(RemovedInPytest4Warning):
|
||||
collector.makeitem("foo", "bar")
|
||||
assert collector.called
|
||||
|
||||
|
||||
def test_fixture_named_request(testdir):
|
||||
testdir.copy_example()
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*'request' is a reserved name for fixtures and will raise an error in future versions"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def request():
|
||||
pass
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
@@ -1,5 +1,7 @@
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest.outcomes import Failed
|
||||
|
||||
@@ -170,3 +172,25 @@ class TestRaises(object):
|
||||
Failed, match="DID NOT RAISE <class 'raises.ClassLooksIterableException'>"
|
||||
):
|
||||
pytest.raises(ClassLooksIterableException, lambda: None)
|
||||
|
||||
def test_raises_with_raising_dunder_class(self):
|
||||
"""Test current behavior with regard to exceptions via __class__ (#4284)."""
|
||||
|
||||
class CrappyClass(Exception):
|
||||
@property
|
||||
def __class__(self):
|
||||
assert False, "via __class__"
|
||||
|
||||
if six.PY2:
|
||||
with pytest.raises(pytest.fail.Exception) as excinfo:
|
||||
with pytest.raises(CrappyClass()):
|
||||
pass
|
||||
assert "DID NOT RAISE" in excinfo.value.args[0]
|
||||
|
||||
with pytest.raises(CrappyClass) as excinfo:
|
||||
raise CrappyClass()
|
||||
else:
|
||||
with pytest.raises(AssertionError) as excinfo:
|
||||
with pytest.raises(CrappyClass()):
|
||||
pass
|
||||
assert "via __class__" in excinfo.value.args[0]
|
||||
|
||||
@@ -66,7 +66,8 @@ class TestNewAPI(object):
|
||||
)
|
||||
result = testdir.runpytest("-rw")
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*could not create cache path*", "*2 warnings*"])
|
||||
# warnings from nodeids, lastfailed, and stepwise
|
||||
result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"])
|
||||
|
||||
def test_config_cache(self, testdir):
|
||||
testdir.makeconftest(
|
||||
|
||||
@@ -957,6 +957,21 @@ def test_collect_init_tests(testdir):
|
||||
"*<Function 'test_foo'>",
|
||||
]
|
||||
)
|
||||
result = testdir.runpytest("./tests", "--collect-only")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*<Module '__init__.py'>",
|
||||
"*<Function 'test_init'>",
|
||||
"*<Module 'test_foo.py'>",
|
||||
"*<Function 'test_foo'>",
|
||||
]
|
||||
)
|
||||
result = testdir.runpytest("./tests/test_foo.py", "--collect-only")
|
||||
result.stdout.fnmatch_lines(["*<Module 'test_foo.py'>", "*<Function 'test_foo'>"])
|
||||
assert "test_init" not in result.stdout.str()
|
||||
result = testdir.runpytest("./tests/__init__.py", "--collect-only")
|
||||
result.stdout.fnmatch_lines(["*<Module '__init__.py'>", "*<Function 'test_init'>"])
|
||||
assert "test_foo" not in result.stdout.str()
|
||||
|
||||
|
||||
def test_collect_invalid_signature_message(testdir):
|
||||
@@ -977,3 +992,30 @@ def test_collect_invalid_signature_message(testdir):
|
||||
result.stdout.fnmatch_lines(
|
||||
["Could not determine arguments of *.fix *: invalid method signature"]
|
||||
)
|
||||
|
||||
|
||||
def test_collect_handles_raising_on_dunder_class(testdir):
|
||||
"""Handle proxy classes like Django's LazySettings that might raise on
|
||||
``isinstance`` (#4266).
|
||||
"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
class ImproperlyConfigured(Exception):
|
||||
pass
|
||||
|
||||
class RaisesOnGetAttr(object):
|
||||
def raises(self):
|
||||
raise ImproperlyConfigured
|
||||
|
||||
__class__ = property(raises)
|
||||
|
||||
raises = RaisesOnGetAttr()
|
||||
|
||||
|
||||
def test_1():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*1 passed in*"])
|
||||
|
||||
@@ -12,6 +12,7 @@ from _pytest.compat import _PytestWrapper
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.outcomes import OutcomeException
|
||||
|
||||
|
||||
@@ -140,3 +141,14 @@ def test_safe_getattr():
|
||||
helper = ErrorsHelper()
|
||||
assert safe_getattr(helper, "raise_exception", "default") == "default"
|
||||
assert safe_getattr(helper, "raise_fail", "default") == "default"
|
||||
|
||||
|
||||
def test_safe_isclass():
|
||||
assert safe_isclass(type) is True
|
||||
|
||||
class CrappyClass(Exception):
|
||||
@property
|
||||
def __class__(self):
|
||||
assert False, "Should be ignored"
|
||||
|
||||
assert safe_isclass(CrappyClass()) is False
|
||||
|
||||
@@ -49,14 +49,14 @@ class TestConftestValueAccessGlobal(object):
|
||||
|
||||
def test_immediate_initialiation_and_incremental_are_the_same(self, basedir):
|
||||
conftest = PytestPluginManager()
|
||||
len(conftest._path2confmods)
|
||||
len(conftest._dirpath2confmods)
|
||||
conftest._getconftestmodules(basedir)
|
||||
snap1 = len(conftest._path2confmods)
|
||||
# assert len(conftest._path2confmods) == snap1 + 1
|
||||
snap1 = len(conftest._dirpath2confmods)
|
||||
# assert len(conftest._dirpath2confmods) == snap1 + 1
|
||||
conftest._getconftestmodules(basedir.join("adir"))
|
||||
assert len(conftest._path2confmods) == snap1 + 1
|
||||
assert len(conftest._dirpath2confmods) == snap1 + 1
|
||||
conftest._getconftestmodules(basedir.join("b"))
|
||||
assert len(conftest._path2confmods) == snap1 + 2
|
||||
assert len(conftest._dirpath2confmods) == snap1 + 2
|
||||
|
||||
def test_value_access_not_existing(self, basedir):
|
||||
conftest = ConftestWithSetinitial(basedir)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
@@ -226,9 +227,10 @@ class TestEnvironWarnings(object):
|
||||
def test_setenv_non_str_warning(self, monkeypatch):
|
||||
value = 2
|
||||
msg = (
|
||||
"Environment variable value {!r} should be str, converted to str implicitly"
|
||||
"Value of environment variable PYTEST_INTERNAL_MY_VAR type should be str, "
|
||||
"but got 2 (type: int); converted to str implicitly"
|
||||
)
|
||||
with pytest.warns(pytest.PytestWarning, match=msg.format(value)):
|
||||
with pytest.warns(pytest.PytestWarning, match=re.escape(msg)):
|
||||
monkeypatch.setenv(str(self.VAR_NAME), value)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import py
|
||||
|
||||
import pytest
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import get_lock_path
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
|
||||
class TestPort:
|
||||
@@ -66,3 +69,18 @@ class TestPort:
|
||||
)
|
||||
def test_not_matching(self, match, pattern, path):
|
||||
assert not match(pattern, path)
|
||||
|
||||
|
||||
def test_access_denied_during_cleanup(tmp_path, monkeypatch):
|
||||
"""Ensure that deleting a numbered dir does not fail because of OSErrors (#4262)."""
|
||||
path = tmp_path / "temp-1"
|
||||
path.mkdir()
|
||||
|
||||
def renamed_failed(*args):
|
||||
raise OSError("access denied")
|
||||
|
||||
monkeypatch.setattr(Path, "rename", renamed_failed)
|
||||
|
||||
lock_path = get_lock_path(path)
|
||||
maybe_delete_a_numbered_dir(path)
|
||||
assert not lock_path.is_file()
|
||||
@@ -167,6 +167,7 @@ class TestPDB(object):
|
||||
assert "= 1 failed in" in rest
|
||||
assert "def test_1" not in rest
|
||||
assert "Exit: Quitting debugger" in rest
|
||||
assert "PDB continue (IO-capturing resumed)" not in rest
|
||||
self.flush(child)
|
||||
|
||||
@staticmethod
|
||||
@@ -498,18 +499,39 @@ class TestPDB(object):
|
||||
"""
|
||||
)
|
||||
child = testdir.spawn_pytest(str(p1))
|
||||
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
||||
child.expect("test_1")
|
||||
child.expect("x = 3")
|
||||
child.expect("Pdb")
|
||||
child.sendline("c")
|
||||
child.expect(r"PDB continue \(IO-capturing resumed\)")
|
||||
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
||||
child.expect("x = 4")
|
||||
child.expect("Pdb")
|
||||
child.sendeof()
|
||||
child.expect("_ test_1 _")
|
||||
child.expect("def test_1")
|
||||
child.expect("Captured stdout call")
|
||||
rest = child.read().decode("utf8")
|
||||
assert "1 failed" in rest
|
||||
assert "def test_1" in rest
|
||||
assert "hello17" in rest # out is captured
|
||||
assert "hello18" in rest # out is captured
|
||||
assert "1 failed" in rest
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_without_capture(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
def test_1():
|
||||
pytest.set_trace()
|
||||
"""
|
||||
)
|
||||
child = testdir.spawn_pytest("-s %s" % p1)
|
||||
child.expect(r">>> PDB set_trace >>>")
|
||||
child.expect("Pdb")
|
||||
child.sendline("c")
|
||||
child.expect(r">>> PDB continue >>>")
|
||||
child.expect("1 passed")
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_used_outside_test(self, testdir):
|
||||
@@ -550,15 +572,29 @@ class TestPDB(object):
|
||||
["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF
|
||||
)
|
||||
|
||||
def test_enter_pdb_hook_is_called(self, testdir):
|
||||
def test_enter_leave_pdb_hooks_are_called(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_enter_pdb(config):
|
||||
assert config.testing_verification == 'configured'
|
||||
print('enter_pdb_hook')
|
||||
mypdb = None
|
||||
|
||||
def pytest_configure(config):
|
||||
config.testing_verification = 'configured'
|
||||
|
||||
def pytest_enter_pdb(config, pdb):
|
||||
assert config.testing_verification == 'configured'
|
||||
print('enter_pdb_hook')
|
||||
|
||||
global mypdb
|
||||
mypdb = pdb
|
||||
mypdb.set_attribute = "bar"
|
||||
|
||||
def pytest_leave_pdb(config, pdb):
|
||||
assert config.testing_verification == 'configured'
|
||||
print('leave_pdb_hook')
|
||||
|
||||
global mypdb
|
||||
assert mypdb is pdb
|
||||
assert mypdb.set_attribute == "bar"
|
||||
"""
|
||||
)
|
||||
p1 = testdir.makepyfile(
|
||||
@@ -567,11 +603,17 @@ class TestPDB(object):
|
||||
|
||||
def test_foo():
|
||||
pytest.set_trace()
|
||||
assert 0
|
||||
"""
|
||||
)
|
||||
child = testdir.spawn_pytest(str(p1))
|
||||
child.expect("enter_pdb_hook")
|
||||
child.send("c\n")
|
||||
child.sendline("c")
|
||||
child.expect(r"PDB continue \(IO-capturing resumed\)")
|
||||
child.expect("Captured stdout call")
|
||||
rest = child.read().decode("utf8")
|
||||
assert "leave_pdb_hook" in rest
|
||||
assert "1 failed" in rest
|
||||
child.sendeof()
|
||||
self.flush(child)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class TestNewSession(SessionTests):
|
||||
started = reprec.getcalls("pytest_collectstart")
|
||||
finished = reprec.getreports("pytest_collectreport")
|
||||
assert len(started) == len(finished)
|
||||
assert len(started) == 7 # XXX extra TopCollector
|
||||
assert len(started) == 8
|
||||
colfail = [x for x in finished if x.failed]
|
||||
assert len(colfail) == 1
|
||||
|
||||
|
||||
148
testing/test_stepwise.py
Normal file
148
testing/test_stepwise.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stepwise_testdir(testdir):
|
||||
# Rather than having to modify our testfile between tests, we introduce
|
||||
# a flag for wether or not the second test should fail.
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('general')
|
||||
group.addoption('--fail', action='store_true', dest='fail')
|
||||
group.addoption('--fail-last', action='store_true', dest='fail_last')
|
||||
"""
|
||||
)
|
||||
|
||||
# Create a simple test suite.
|
||||
testdir.makepyfile(
|
||||
test_a="""
|
||||
def test_success_before_fail():
|
||||
assert 1
|
||||
|
||||
def test_fail_on_flag(request):
|
||||
assert not request.config.getvalue('fail')
|
||||
|
||||
def test_success_after_fail():
|
||||
assert 1
|
||||
|
||||
def test_fail_last_on_flag(request):
|
||||
assert not request.config.getvalue('fail_last')
|
||||
|
||||
def test_success_after_last_fail():
|
||||
assert 1
|
||||
"""
|
||||
)
|
||||
|
||||
testdir.makepyfile(
|
||||
test_b="""
|
||||
def test_success():
|
||||
assert 1
|
||||
"""
|
||||
)
|
||||
|
||||
return testdir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def error_testdir(testdir):
|
||||
testdir.makepyfile(
|
||||
test_a="""
|
||||
def test_error(nonexisting_fixture):
|
||||
assert 1
|
||||
|
||||
def test_success_after_fail():
|
||||
assert 1
|
||||
"""
|
||||
)
|
||||
|
||||
return testdir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def broken_testdir(testdir):
|
||||
testdir.makepyfile(
|
||||
working_testfile="def test_proper(): assert 1", broken_testfile="foobar"
|
||||
)
|
||||
return testdir
|
||||
|
||||
|
||||
def test_run_without_stepwise(stepwise_testdir):
|
||||
result = stepwise_testdir.runpytest("-v", "--strict", "--fail")
|
||||
|
||||
result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"])
|
||||
result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"])
|
||||
result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"])
|
||||
|
||||
|
||||
def test_fail_and_continue_with_stepwise(stepwise_testdir):
|
||||
# Run the tests with a failing second test.
|
||||
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "--fail")
|
||||
assert not result.stderr.str()
|
||||
|
||||
stdout = result.stdout.str()
|
||||
# Make sure we stop after first failing test.
|
||||
assert "test_success_before_fail PASSED" in stdout
|
||||
assert "test_fail_on_flag FAILED" in stdout
|
||||
assert "test_success_after_fail" not in stdout
|
||||
|
||||
# "Fix" the test that failed in the last run and run it again.
|
||||
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise")
|
||||
assert not result.stderr.str()
|
||||
|
||||
stdout = result.stdout.str()
|
||||
# Make sure the latest failing test runs and then continues.
|
||||
assert "test_success_before_fail" not in stdout
|
||||
assert "test_fail_on_flag PASSED" in stdout
|
||||
assert "test_success_after_fail PASSED" in stdout
|
||||
|
||||
|
||||
def test_run_with_skip_option(stepwise_testdir):
|
||||
result = stepwise_testdir.runpytest(
|
||||
"-v", "--strict", "--stepwise", "--stepwise-skip", "--fail", "--fail-last"
|
||||
)
|
||||
assert not result.stderr.str()
|
||||
|
||||
stdout = result.stdout.str()
|
||||
# Make sure first fail is ignore and second fail stops the test run.
|
||||
assert "test_fail_on_flag FAILED" in stdout
|
||||
assert "test_success_after_fail PASSED" in stdout
|
||||
assert "test_fail_last_on_flag FAILED" in stdout
|
||||
assert "test_success_after_last_fail" not in stdout
|
||||
|
||||
|
||||
def test_fail_on_errors(error_testdir):
|
||||
result = error_testdir.runpytest("-v", "--strict", "--stepwise")
|
||||
|
||||
assert not result.stderr.str()
|
||||
stdout = result.stdout.str()
|
||||
|
||||
assert "test_error ERROR" in stdout
|
||||
assert "test_success_after_fail" not in stdout
|
||||
|
||||
|
||||
def test_change_testfile(stepwise_testdir):
|
||||
result = stepwise_testdir.runpytest(
|
||||
"-v", "--strict", "--stepwise", "--fail", "test_a.py"
|
||||
)
|
||||
assert not result.stderr.str()
|
||||
|
||||
stdout = result.stdout.str()
|
||||
assert "test_fail_on_flag FAILED" in stdout
|
||||
|
||||
# Make sure the second test run starts from the beginning, since the
|
||||
# test to continue from does not exist in testfile_b.
|
||||
result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "test_b.py")
|
||||
assert not result.stderr.str()
|
||||
|
||||
stdout = result.stdout.str()
|
||||
assert "test_success PASSED" in stdout
|
||||
|
||||
|
||||
def test_stop_on_collection_errors(broken_testdir):
|
||||
result = broken_testdir.runpytest(
|
||||
"-v", "--strict", "--stepwise", "working_testfile.py", "broken_testfile.py"
|
||||
)
|
||||
|
||||
stdout = result.stdout.str()
|
||||
assert "errors during collection" in stdout
|
||||
Reference in New Issue
Block a user