Merge features into master after 5.3 (#6236)
Merge features into master after 5.3
This commit is contained in:
commit
7e5ad31428
|
|
@ -1,7 +1,7 @@
|
||||||
exclude: doc/en/example/py2py3/test_py2.py
|
exclude: doc/en/example/py2py3/test_py2.py
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 19.3b0
|
rev: 19.10b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
|
|
@ -42,7 +42,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.720
|
rev: v0.740
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
|
||||||
27
.travis.yml
27
.travis.yml
|
|
@ -23,10 +23,13 @@ install:
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
# OSX tests - first (in test stage), since they are the slower ones.
|
# OSX tests - first (in test stage), since they are the slower ones.
|
||||||
|
# Coverage for:
|
||||||
|
# - osx
|
||||||
|
# - verbose=1
|
||||||
- os: osx
|
- os: osx
|
||||||
osx_image: xcode10.1
|
osx_image: xcode10.1
|
||||||
language: generic
|
language: generic
|
||||||
env: TOXENV=py37-xdist PYTEST_COVERAGE=1
|
env: TOXENV=py37-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=-v
|
||||||
before_install:
|
before_install:
|
||||||
- which python3
|
- which python3
|
||||||
- python3 -V
|
- python3 -V
|
||||||
|
|
@ -36,8 +39,13 @@ jobs:
|
||||||
|
|
||||||
# Full run of latest supported version, without xdist.
|
# Full run of latest supported version, without xdist.
|
||||||
# Coverage for:
|
# Coverage for:
|
||||||
|
# - pytester's LsofFdLeakChecker
|
||||||
|
# - TestArgComplete (linux only)
|
||||||
|
# - numpy
|
||||||
|
# - old attrs
|
||||||
|
# - verbose=0
|
||||||
# - test_sys_breakpoint_interception (via pexpect).
|
# - test_sys_breakpoint_interception (via pexpect).
|
||||||
- env: TOXENV=py37-pexpect PYTEST_COVERAGE=1
|
- env: TOXENV=py37-lsof-numpy-oldattrs-pexpect-twisted PYTEST_COVERAGE=1 PYTEST_ADDOPTS=
|
||||||
python: '3.7'
|
python: '3.7'
|
||||||
|
|
||||||
# Coverage tracking is slow with pypy, skip it.
|
# Coverage tracking is slow with pypy, skip it.
|
||||||
|
|
@ -47,14 +55,6 @@ jobs:
|
||||||
- env: TOXENV=py35-xdist
|
- env: TOXENV=py35-xdist
|
||||||
python: '3.5'
|
python: '3.5'
|
||||||
|
|
||||||
# Coverage for:
|
|
||||||
# - pytester's LsofFdLeakChecker
|
|
||||||
# - TestArgComplete (linux only)
|
|
||||||
# - numpy
|
|
||||||
# - old attrs
|
|
||||||
# Empty PYTEST_ADDOPTS to run this non-verbose.
|
|
||||||
- env: TOXENV=py37-lsof-oldattrs-numpy-twisted-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=
|
|
||||||
|
|
||||||
# Specialized factors for py37.
|
# Specialized factors for py37.
|
||||||
- env: TOXENV=py37-pluggymaster-xdist
|
- env: TOXENV=py37-pluggymaster-xdist
|
||||||
- env: TOXENV=py37-freeze
|
- env: TOXENV=py37-freeze
|
||||||
|
|
@ -125,3 +125,10 @@ notifications:
|
||||||
skip_join: true
|
skip_join: true
|
||||||
email:
|
email:
|
||||||
- pytest-commit@python.org
|
- pytest-commit@python.org
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- features
|
||||||
|
- 4.6-maintenance
|
||||||
|
- /^\d+(\.\d+)+$/
|
||||||
|
|
|
||||||
3
AUTHORS
3
AUTHORS
|
|
@ -104,6 +104,7 @@ George Kussumoto
|
||||||
Georgy Dyuldin
|
Georgy Dyuldin
|
||||||
Graham Horler
|
Graham Horler
|
||||||
Greg Price
|
Greg Price
|
||||||
|
Gregory Lee
|
||||||
Grig Gheorghiu
|
Grig Gheorghiu
|
||||||
Grigorii Eremeev (budulianin)
|
Grigorii Eremeev (budulianin)
|
||||||
Guido Wesdorp
|
Guido Wesdorp
|
||||||
|
|
@ -162,6 +163,7 @@ Manuel Krebber
|
||||||
Marc Schlaich
|
Marc Schlaich
|
||||||
Marcelo Duarte Trevisani
|
Marcelo Duarte Trevisani
|
||||||
Marcin Bachry
|
Marcin Bachry
|
||||||
|
Marco Gorelli
|
||||||
Mark Abramowitz
|
Mark Abramowitz
|
||||||
Markus Unterwaditzer
|
Markus Unterwaditzer
|
||||||
Martijn Faassen
|
Martijn Faassen
|
||||||
|
|
@ -179,6 +181,7 @@ Michael Aquilina
|
||||||
Michael Birtwell
|
Michael Birtwell
|
||||||
Michael Droettboom
|
Michael Droettboom
|
||||||
Michael Goerz
|
Michael Goerz
|
||||||
|
Michael Krebs
|
||||||
Michael Seifert
|
Michael Seifert
|
||||||
Michal Wajszczuk
|
Michal Wajszczuk
|
||||||
Mihai Capotă
|
Mihai Capotă
|
||||||
|
|
|
||||||
202
CHANGELOG.rst
202
CHANGELOG.rst
|
|
@ -18,6 +18,208 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 5.3.0 (2019-11-19)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Deprecations
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#6179 <https://github.com/pytest-dev/pytest/issues/6179>`_: The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given
|
||||||
|
that this is the version supported by default in modern tools that manipulate this type of file.
|
||||||
|
|
||||||
|
In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option
|
||||||
|
is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``.
|
||||||
|
|
||||||
|
For more information, `see the docs <https://docs.pytest.org/en/latest/deprecations.html#junit-family-default-value-change-to-xunit2>`__.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#4488 <https://github.com/pytest-dev/pytest/issues/4488>`_: The pytest team has created the `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__
|
||||||
|
plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes.
|
||||||
|
|
||||||
|
Each line of the report log contains a self contained JSON object corresponding to a testing event,
|
||||||
|
such as a collection or a test result report. The file is guaranteed to be flushed after writing
|
||||||
|
each line, so systems can read and process events in real-time.
|
||||||
|
|
||||||
|
The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed
|
||||||
|
in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and
|
||||||
|
provide feedback.
|
||||||
|
|
||||||
|
|
||||||
|
- `#4730 <https://github.com/pytest-dev/pytest/issues/4730>`_: When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism.
|
||||||
|
|
||||||
|
This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5515 <https://github.com/pytest-dev/pytest/issues/5515>`_: Allow selective auto-indentation of multiline log messages.
|
||||||
|
|
||||||
|
Adds command line option ``--log-auto-indent``, config option
|
||||||
|
``log_auto_indent`` and support for per-entry configuration of
|
||||||
|
indentation behavior on calls to ``logging.log()``.
|
||||||
|
|
||||||
|
Alters the default for auto-indention from ``on`` to ``off``. This
|
||||||
|
restores the older behavior that existed prior to v4.6.0. This
|
||||||
|
reversion to earlier behavior was done because it is better to
|
||||||
|
activate new features that may lead to broken tests explicitly
|
||||||
|
rather than implicitly.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5914 <https://github.com/pytest-dev/pytest/issues/5914>`_: ``pytester`` learned two new functions, `no_fnmatch_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.no_fnmatch_line>`_ and
|
||||||
|
`no_re_match_line <https://docs.pytest.org/en/latest/reference.html#_pytest.pytester.LineMatcher.no_re_match_line>`_.
|
||||||
|
|
||||||
|
The functions are used to ensure the captured text *does not* match the given
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
The previous idiom was to use ``re.match``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
assert re.match(pat, result.stdout.str()) is None
|
||||||
|
|
||||||
|
Or the ``in`` operator:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
assert text in result.stdout.str()
|
||||||
|
|
||||||
|
But the new functions produce best output on failure.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6057 <https://github.com/pytest-dev/pytest/issues/6057>`_: Added tolerances to complex values when printing ``pytest.approx``.
|
||||||
|
|
||||||
|
For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6061 <https://github.com/pytest-dev/pytest/issues/6061>`_: Added the pluginmanager as an argument to ``pytest_addoption``
|
||||||
|
so that hooks can be invoked when setting up command line options. This is
|
||||||
|
useful for having one plugin communicate things to another plugin,
|
||||||
|
such as default values or which set of command line options to add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#5061 <https://github.com/pytest-dev/pytest/issues/5061>`_: Use multiple colors with terminal summary statistics.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5630 <https://github.com/pytest-dev/pytest/issues/5630>`_: Quitting from debuggers is now properly handled in ``doctest`` items.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5924 <https://github.com/pytest-dev/pytest/issues/5924>`_: Improved verbose diff output with sequences.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...]
|
||||||
|
E Right contains 3 more items, first extra item: ' '
|
||||||
|
E Full diff:
|
||||||
|
E - ['version', 'version_info', 'sys.version', 'sys.version_info']
|
||||||
|
E + ['version',
|
||||||
|
E + 'version_info',
|
||||||
|
E + 'sys.version',
|
||||||
|
E + 'sys.version_info',
|
||||||
|
E + ' ',
|
||||||
|
E + 'sys.version',
|
||||||
|
E + 'sys.version_info']
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...]
|
||||||
|
E Right contains 3 more items, first extra item: ' '
|
||||||
|
E Full diff:
|
||||||
|
E [
|
||||||
|
E 'version',
|
||||||
|
E 'version_info',
|
||||||
|
E 'sys.version',
|
||||||
|
E 'sys.version_info',
|
||||||
|
E + ' ',
|
||||||
|
E + 'sys.version',
|
||||||
|
E + 'sys.version_info',
|
||||||
|
E ]
|
||||||
|
|
||||||
|
|
||||||
|
- `#5936 <https://github.com/pytest-dev/pytest/issues/5936>`_: Display untruncated assertion message with ``-vv``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#5990 <https://github.com/pytest-dev/pytest/issues/5990>`_: Fixed plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors").
|
||||||
|
|
||||||
|
|
||||||
|
- `#6008 <https://github.com/pytest-dev/pytest/issues/6008>`_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be
|
||||||
|
immutable and avoid accidental modifications.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6023 <https://github.com/pytest-dev/pytest/issues/6023>`_: ``pytest.main`` now returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still).
|
||||||
|
|
||||||
|
|
||||||
|
- `#6026 <https://github.com/pytest-dev/pytest/issues/6026>`_: Align prefixes in output of pytester's ``LineMatcher``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6059 <https://github.com/pytest-dev/pytest/issues/6059>`_: Collection errors are reported as errors (and not failures like before) in the terminal's short test summary.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6069 <https://github.com/pytest-dev/pytest/issues/6069>`_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6097 <https://github.com/pytest-dev/pytest/issues/6097>`_: The "[XXX%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6116 <https://github.com/pytest-dev/pytest/issues/6116>`_: Added ``--co`` as a synonym to ``--collect-only``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6148 <https://github.com/pytest-dev/pytest/issues/6148>`_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6152 <https://github.com/pytest-dev/pytest/issues/6152>`_: Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6176 <https://github.com/pytest-dev/pytest/issues/6176>`_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6181 <https://github.com/pytest-dev/pytest/issues/6181>`_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x``, now gets reported in the test summary.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6206 <https://github.com/pytest-dev/pytest/issues/6206>`_: Improved ``cache.set`` robustness and performance.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#2049 <https://github.com/pytest-dev/pytest/issues/2049>`_: Fixed ``--setup-plan`` showing inaccurate information about fixture lifetimes.
|
||||||
|
|
||||||
|
|
||||||
|
- `#2548 <https://github.com/pytest-dev/pytest/issues/2548>`_: Fixed line offset mismatch of skipped tests in terminal summary.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6039 <https://github.com/pytest-dev/pytest/issues/6039>`_: The ``PytestDoctestRunner`` is now properly invalidated when unconfiguring the doctest plugin.
|
||||||
|
|
||||||
|
This is important when used with ``pytester``'s ``runpytest_inprocess``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6047 <https://github.com/pytest-dev/pytest/issues/6047>`_: BaseExceptions are now handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6074 <https://github.com/pytest-dev/pytest/issues/6074>`_: pytester: fixed order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6189 <https://github.com/pytest-dev/pytest/issues/6189>`_: Fixed result of ``getmodpath`` method.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#4901 <https://github.com/pytest-dev/pytest/issues/4901>`_: ``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a
|
||||||
|
valid ``pytest.ExitCode`` value.
|
||||||
|
|
||||||
|
|
||||||
pytest 5.2.4 (2019-11-15)
|
pytest 5.2.4 (2019-11-15)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix incorrect result of ``getmodpath`` method.
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Improve check for misspelling of ``pytest.mark.parametrize``.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception.
|
||||||
|
|
@ -6,6 +6,7 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-5.3.0
|
||||||
release-5.2.4
|
release-5.2.4
|
||||||
release-5.2.3
|
release-5.2.3
|
||||||
release-5.2.2
|
release-5.2.2
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
pytest-5.3.0
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 5.3.0 release!
|
||||||
|
|
||||||
|
pytest is a mature Python testing tool with more than a 2000 tests
|
||||||
|
against itself, passing on many different interpreters and platforms.
|
||||||
|
|
||||||
|
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||||
|
to take a look at the CHANGELOG:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/latest/changelog.html
|
||||||
|
|
||||||
|
For complete documentation, please visit:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/latest/
|
||||||
|
|
||||||
|
As usual, you can upgrade from pypi via:
|
||||||
|
|
||||||
|
pip install -U pytest
|
||||||
|
|
||||||
|
Thanks to all who contributed to this release, among them:
|
||||||
|
|
||||||
|
* AnjoMan
|
||||||
|
* Anthony Sottile
|
||||||
|
* Anton Lodder
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Daniel Hahler
|
||||||
|
* Gregory Lee
|
||||||
|
* Josh Karpel
|
||||||
|
* JoshKarpel
|
||||||
|
* Joshua Storck
|
||||||
|
* Kale Kundert
|
||||||
|
* MarcoGorelli
|
||||||
|
* Michael Krebs
|
||||||
|
* NNRepos
|
||||||
|
* Ran Benita
|
||||||
|
* TH3CHARLie
|
||||||
|
* Tibor Arpas
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
* 林玮
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The Pytest Development Team
|
||||||
|
|
@ -19,6 +19,27 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||||
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
||||||
:ref:`standard warning filters <warnings>`.
|
:ref:`standard warning filters <warnings>`.
|
||||||
|
|
||||||
|
``junit_family`` default value change to "xunit2"
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 5.2
|
||||||
|
|
||||||
|
The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given
|
||||||
|
that this is the version supported by default in modern tools that manipulate this type of file.
|
||||||
|
|
||||||
|
In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option
|
||||||
|
is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``::
|
||||||
|
|
||||||
|
PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0.
|
||||||
|
Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible.
|
||||||
|
|
||||||
|
In order to silence this warning, users just need to configure the ``junit_family`` option explicitly:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
junit_family=legacy
|
||||||
|
|
||||||
|
|
||||||
``funcargnames`` alias for ``fixturenames``
|
``funcargnames`` alias for ``fixturenames``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
@ -40,15 +61,15 @@ Result log (``--result-log``)
|
||||||
.. deprecated:: 4.0
|
.. deprecated:: 4.0
|
||||||
|
|
||||||
The ``--result-log`` option produces a stream of test reports which can be
|
The ``--result-log`` option produces a stream of test reports which can be
|
||||||
analysed at runtime. It uses a custom format which requires users to implement their own
|
analysed at runtime, but it uses a custom format which requires users to implement their own
|
||||||
parser, but the team believes using a line-based format that can be parsed using standard
|
parser.
|
||||||
tools would provide a suitable and better alternative.
|
|
||||||
|
|
||||||
The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log``
|
The `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing
|
||||||
option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed
|
one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback.
|
||||||
stable.
|
|
||||||
|
|
||||||
The actual alternative is still being discussed in issue `#4488 <https://github.com/pytest-dev/pytest/issues/4488>`__.
|
The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory
|
||||||
|
to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core
|
||||||
|
at some point, depending on the plans for the plugins and number of users using it.
|
||||||
|
|
||||||
|
|
||||||
Removed Features
|
Removed Features
|
||||||
|
|
|
||||||
|
|
@ -622,7 +622,7 @@ then you will see two tests skipped and two executed tests as expected:
|
||||||
test_plat.py s.s. [100%]
|
test_plat.py s.s. [100%]
|
||||||
|
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux
|
SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux
|
||||||
======================= 2 passed, 2 skipped in 0.12s =======================
|
======================= 2 passed, 2 skipped in 0.12s =======================
|
||||||
|
|
||||||
Note that if you specify a platform via the marker-command line option like this:
|
Note that if you specify a platform via the marker-command line option like this:
|
||||||
|
|
|
||||||
|
|
@ -475,10 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
ssssssssssss......sss...... [100%]
|
ssssssssssss...ssssssssssss [100%]
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found
|
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found
|
||||||
12 passed, 15 skipped in 0.12s
|
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found
|
||||||
|
3 passed, 24 skipped in 0.12s
|
||||||
|
|
||||||
Indirect parametrization of optional implementations/imports
|
Indirect parametrization of optional implementations/imports
|
||||||
--------------------------------------------------------------------
|
--------------------------------------------------------------------
|
||||||
|
|
@ -546,7 +547,7 @@ If you run this with reporting for skips enabled:
|
||||||
test_module.py .s [100%]
|
test_module.py .s [100%]
|
||||||
|
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2'
|
SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:12: could not import 'opt2': No module named 'opt2'
|
||||||
======================= 1 passed, 1 skipped in 0.12s =======================
|
======================= 1 passed, 1 skipped in 0.12s =======================
|
||||||
|
|
||||||
You'll see that we don't have an ``opt2`` module and thus the second test run
|
You'll see that we don't have an ``opt2`` module and thus the second test run
|
||||||
|
|
|
||||||
|
|
@ -443,7 +443,7 @@ Now we can profile which test functions execute the slowest:
|
||||||
========================= slowest 3 test durations =========================
|
========================= slowest 3 test durations =========================
|
||||||
0.30s call test_some_are_slow.py::test_funcslow2
|
0.30s call test_some_are_slow.py::test_funcslow2
|
||||||
0.20s call test_some_are_slow.py::test_funcslow1
|
0.20s call test_some_are_slow.py::test_funcslow1
|
||||||
0.10s call test_some_are_slow.py::test_funcfast
|
0.11s call test_some_are_slow.py::test_funcfast
|
||||||
============================ 3 passed in 0.12s =============================
|
============================ 3 passed in 0.12s =============================
|
||||||
|
|
||||||
incremental testing - test steps
|
incremental testing - test steps
|
||||||
|
|
|
||||||
|
|
@ -1192,6 +1192,29 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
junit_suite_name = my_suite
|
junit_suite_name = my_suite
|
||||||
|
|
||||||
|
.. confval:: log_auto_indent
|
||||||
|
|
||||||
|
Allow selective auto-indentation of multiline log messages.
|
||||||
|
|
||||||
|
Supports command line option ``--log-auto-indent [value]``
|
||||||
|
and config option ``log_auto_indent = [value]`` to set the
|
||||||
|
auto-indentation behavior for all logging.
|
||||||
|
|
||||||
|
``[value]`` can be:
|
||||||
|
* True or "On" - Dynamically auto-indent multiline log messages
|
||||||
|
* False or "Off" or 0 - Do not auto-indent multiline log messages (the default behavior)
|
||||||
|
* [positive integer] - auto-indent multiline log messages by [value] spaces
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
log_auto_indent = False
|
||||||
|
|
||||||
|
Supports passing kwarg ``extra={"auto_indent": [value]}`` to
|
||||||
|
calls to ``logging.log()`` to specify auto-indentation behavior for
|
||||||
|
a specific entry in the log. ``extra`` kwarg overrides the value specified
|
||||||
|
on the command line or in the config.
|
||||||
|
|
||||||
|
|
||||||
.. confval:: log_cli_date_format
|
.. confval:: log_cli_date_format
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ To stop the testing process after the first (N) failures:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -x # stop after first failure
|
pytest -x # stop after first failure
|
||||||
pytest --maxfail=2 # stop after two failures
|
pytest --maxfail=2 # stop after two failures
|
||||||
|
|
||||||
.. _select-tests:
|
.. _select-tests:
|
||||||
|
|
||||||
|
|
@ -241,7 +241,7 @@ Example:
|
||||||
|
|
||||||
test_example.py:14: AssertionError
|
test_example.py:14: AssertionError
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test
|
SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test
|
||||||
XFAIL test_example.py::test_xfail
|
XFAIL test_example.py::test_xfail
|
||||||
reason: xfailing this test
|
reason: xfailing this test
|
||||||
XPASS test_example.py::test_xpass always xfail
|
XPASS test_example.py::test_xpass always xfail
|
||||||
|
|
@ -296,7 +296,7 @@ More than one character can be used, so for example to only see failed and skipp
|
||||||
test_example.py:14: AssertionError
|
test_example.py:14: AssertionError
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
FAILED test_example.py::test_fail - assert 0
|
FAILED test_example.py::test_fail - assert 0
|
||||||
SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test
|
SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test
|
||||||
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
||||||
|
|
||||||
Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had
|
Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had
|
||||||
|
|
@ -679,12 +679,6 @@ Creating resultlog format files
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This option is rarely used and is scheduled for removal in 5.0.
|
|
||||||
|
|
||||||
See `the deprecation docs <https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log>`__
|
|
||||||
for more information.
|
|
||||||
|
|
||||||
To create plain-text machine-readable result files you can issue:
|
To create plain-text machine-readable result files you can issue:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
@ -694,6 +688,16 @@ To create plain-text machine-readable result files you can issue:
|
||||||
and look at the content at the ``path`` location. Such files are used e.g.
|
and look at the content at the ``path`` location. Such files are used e.g.
|
||||||
by the `PyPy-test`_ web page to show test results over several revisions.
|
by the `PyPy-test`_ web page to show test results over several revisions.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This option is rarely used and is scheduled for removal in pytest 6.0.
|
||||||
|
|
||||||
|
If you use this option, consider using the new `pytest-reportlog <https://github.com/pytest-dev/pytest-reportlog>`__ plugin instead.
|
||||||
|
|
||||||
|
See `the deprecation docs <https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log>`__
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
|
||||||
.. _`PyPy-test`: http://buildbot.pypy.org/summary
|
.. _`PyPy-test`: http://buildbot.pypy.org/summary
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ Running pytest now produces this output:
|
||||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||||
|
|
||||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||||
====================== 1 passed, 1 warnings in 0.12s =======================
|
======================= 1 passed, 1 warning in 0.12s =======================
|
||||||
|
|
||||||
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
||||||
them into errors:
|
them into errors:
|
||||||
|
|
@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta
|
||||||
class Test:
|
class Test:
|
||||||
|
|
||||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||||
1 warnings in 0.12s
|
1 warning in 0.12s
|
||||||
|
|
||||||
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
|
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin
|
||||||
testdir.copy_example("test_example.py")
|
testdir.copy_example("test_example.py")
|
||||||
|
|
||||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||||
====================== 2 passed, 1 warnings in 0.12s =======================
|
======================= 2 passed, 1 warning in 0.12s =======================
|
||||||
|
|
||||||
For more information about the result object that ``runpytest()`` returns, and
|
For more information about the result object that ``runpytest()`` returns, and
|
||||||
the methods that it provides please check out the :py:class:`RunResult
|
the methods that it provides please check out the :py:class:`RunResult
|
||||||
|
|
@ -677,6 +677,56 @@ Example:
|
||||||
print(config.hook)
|
print(config.hook)
|
||||||
|
|
||||||
|
|
||||||
|
.. _`addoptionhooks`:
|
||||||
|
|
||||||
|
|
||||||
|
Using hooks in pytest_addoption
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Occasionally, it is necessary to change the way in which command line options
|
||||||
|
are defined by one plugin based on hooks in another plugin. For example,
|
||||||
|
a plugin may expose a command line option for which another plugin needs
|
||||||
|
to define the default value. The pluginmanager can be used to install and
|
||||||
|
use hooks to accomplish this. The plugin would define and add the hooks
|
||||||
|
and use pytest_addoption as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# contents of hooks.py
|
||||||
|
|
||||||
|
# Use firstresult=True because we only want one plugin to define this
|
||||||
|
# default value
|
||||||
|
@hookspec(firstresult=True)
|
||||||
|
def pytest_config_file_default_value():
|
||||||
|
""" Return the default value for the config file command line option. """
|
||||||
|
|
||||||
|
|
||||||
|
# contents of myplugin.py
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addhooks(pluginmanager):
|
||||||
|
""" This example assumes the hooks are grouped in the 'hooks' module. """
|
||||||
|
from . import hook
|
||||||
|
|
||||||
|
pluginmanager.add_hookspecs(hook)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser, pluginmanager):
|
||||||
|
default_value = pluginmanager.hook.pytest_config_file_default_value()
|
||||||
|
parser.addoption(
|
||||||
|
"--config-file",
|
||||||
|
help="Config file to use, defaults to %(default)s",
|
||||||
|
default=default_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
The conftest.py that is using myplugin would simply define the hook as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def pytest_config_file_default_value():
|
||||||
|
return "config.yaml"
|
||||||
|
|
||||||
|
|
||||||
Optionally using hooks from 3rd party plugins
|
Optionally using hooks from 3rd party plugins
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ ignore =
|
||||||
formats = sdist.tgz,bdist_wheel
|
formats = sdist.tgz,bdist_wheel
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
|
mypy_path = src
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
no_implicit_optional = True
|
no_implicit_optional = True
|
||||||
strict_equality = True
|
strict_equality = True
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -7,7 +7,7 @@ INSTALL_REQUIRES = [
|
||||||
"packaging",
|
"packaging",
|
||||||
"attrs>=17.4.0", # should match oldattrs tox env.
|
"attrs>=17.4.0", # should match oldattrs tox env.
|
||||||
"more-itertools>=4.0.0",
|
"more-itertools>=4.0.0",
|
||||||
"atomicwrites>=1.0",
|
'atomicwrites>=1.0;sys_platform=="win32"',
|
||||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||||
'colorama;sys_platform=="win32"',
|
'colorama;sys_platform=="win32"',
|
||||||
"pluggy>=0.12,<1.0",
|
"pluggy>=0.12,<1.0",
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,17 @@ from inspect import CO_VARKEYWORDS
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from traceback import format_exception_only
|
from traceback import format_exception_only
|
||||||
from types import CodeType
|
from types import CodeType
|
||||||
|
from types import FrameType
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
@ -27,9 +31,16 @@ import py
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest._io.saferepr import safeformat
|
from _pytest._io.saferepr import safeformat
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
from _pytest.compat import overload
|
||||||
|
|
||||||
if False: # TYPE_CHECKING
|
if False: # TYPE_CHECKING
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
from typing_extensions import Literal
|
||||||
|
from weakref import ReferenceType # noqa: F401
|
||||||
|
|
||||||
|
from _pytest._code import Source
|
||||||
|
|
||||||
|
_TracebackStyle = Literal["long", "short", "no", "native"]
|
||||||
|
|
||||||
|
|
||||||
class Code:
|
class Code:
|
||||||
|
|
@ -38,13 +49,12 @@ class Code:
|
||||||
def __init__(self, rawcode) -> None:
|
def __init__(self, rawcode) -> None:
|
||||||
if not hasattr(rawcode, "co_filename"):
|
if not hasattr(rawcode, "co_filename"):
|
||||||
rawcode = getrawcode(rawcode)
|
rawcode = getrawcode(rawcode)
|
||||||
try:
|
if not isinstance(rawcode, CodeType):
|
||||||
self.filename = rawcode.co_filename
|
|
||||||
self.firstlineno = rawcode.co_firstlineno - 1
|
|
||||||
self.name = rawcode.co_name
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError("not a code object: {!r}".format(rawcode))
|
raise TypeError("not a code object: {!r}".format(rawcode))
|
||||||
self.raw = rawcode # type: CodeType
|
self.filename = rawcode.co_filename
|
||||||
|
self.firstlineno = rawcode.co_firstlineno - 1
|
||||||
|
self.name = rawcode.co_name
|
||||||
|
self.raw = rawcode
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.raw == other.raw
|
return self.raw == other.raw
|
||||||
|
|
@ -72,7 +82,7 @@ class Code:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fullsource(self):
|
def fullsource(self) -> Optional["Source"]:
|
||||||
""" return a _pytest._code.Source object for the full source file of the code
|
""" return a _pytest._code.Source object for the full source file of the code
|
||||||
"""
|
"""
|
||||||
from _pytest._code import source
|
from _pytest._code import source
|
||||||
|
|
@ -80,7 +90,7 @@ class Code:
|
||||||
full, _ = source.findsource(self.raw)
|
full, _ = source.findsource(self.raw)
|
||||||
return full
|
return full
|
||||||
|
|
||||||
def source(self):
|
def source(self) -> "Source":
|
||||||
""" return a _pytest._code.Source object for the code object's source only
|
""" return a _pytest._code.Source object for the code object's source only
|
||||||
"""
|
"""
|
||||||
# return source only for that part of code
|
# return source only for that part of code
|
||||||
|
|
@ -88,7 +98,7 @@ class Code:
|
||||||
|
|
||||||
return _pytest._code.Source(self.raw)
|
return _pytest._code.Source(self.raw)
|
||||||
|
|
||||||
def getargs(self, var=False):
|
def getargs(self, var: bool = False) -> Tuple[str, ...]:
|
||||||
""" return a tuple with the argument names for the code object
|
""" return a tuple with the argument names for the code object
|
||||||
|
|
||||||
if 'var' is set True also return the names of the variable and
|
if 'var' is set True also return the names of the variable and
|
||||||
|
|
@ -107,7 +117,7 @@ class Frame:
|
||||||
"""Wrapper around a Python frame holding f_locals and f_globals
|
"""Wrapper around a Python frame holding f_locals and f_globals
|
||||||
in which expressions can be evaluated."""
|
in which expressions can be evaluated."""
|
||||||
|
|
||||||
def __init__(self, frame):
|
def __init__(self, frame: FrameType) -> None:
|
||||||
self.lineno = frame.f_lineno - 1
|
self.lineno = frame.f_lineno - 1
|
||||||
self.f_globals = frame.f_globals
|
self.f_globals = frame.f_globals
|
||||||
self.f_locals = frame.f_locals
|
self.f_locals = frame.f_locals
|
||||||
|
|
@ -115,7 +125,7 @@ class Frame:
|
||||||
self.code = Code(frame.f_code)
|
self.code = Code(frame.f_code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def statement(self):
|
def statement(self) -> "Source":
|
||||||
""" statement this frame is at """
|
""" statement this frame is at """
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
|
|
||||||
|
|
@ -134,7 +144,7 @@ class Frame:
|
||||||
f_locals.update(vars)
|
f_locals.update(vars)
|
||||||
return eval(code, self.f_globals, f_locals)
|
return eval(code, self.f_globals, f_locals)
|
||||||
|
|
||||||
def exec_(self, code, **vars):
|
def exec_(self, code, **vars) -> None:
|
||||||
""" exec 'code' in the frame
|
""" exec 'code' in the frame
|
||||||
|
|
||||||
'vars' are optional; additional local variables
|
'vars' are optional; additional local variables
|
||||||
|
|
@ -143,7 +153,7 @@ class Frame:
|
||||||
f_locals.update(vars)
|
f_locals.update(vars)
|
||||||
exec(code, self.f_globals, f_locals)
|
exec(code, self.f_globals, f_locals)
|
||||||
|
|
||||||
def repr(self, object):
|
def repr(self, object: object) -> str:
|
||||||
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
||||||
"""
|
"""
|
||||||
return saferepr(object)
|
return saferepr(object)
|
||||||
|
|
@ -151,7 +161,7 @@ class Frame:
|
||||||
def is_true(self, object):
|
def is_true(self, object):
|
||||||
return object
|
return object
|
||||||
|
|
||||||
def getargs(self, var=False):
|
def getargs(self, var: bool = False):
|
||||||
""" return a list of tuples (name, value) for all arguments
|
""" return a list of tuples (name, value) for all arguments
|
||||||
|
|
||||||
if 'var' is set True also include the variable and keyword
|
if 'var' is set True also include the variable and keyword
|
||||||
|
|
@ -169,35 +179,34 @@ class Frame:
|
||||||
class TracebackEntry:
|
class TracebackEntry:
|
||||||
""" a single entry in a traceback """
|
""" a single entry in a traceback """
|
||||||
|
|
||||||
_repr_style = None
|
_repr_style = None # type: Optional[Literal["short", "long"]]
|
||||||
exprinfo = None
|
exprinfo = None
|
||||||
|
|
||||||
def __init__(self, rawentry, excinfo=None):
|
def __init__(self, rawentry: TracebackType, excinfo=None) -> None:
|
||||||
self._excinfo = excinfo
|
self._excinfo = excinfo
|
||||||
self._rawentry = rawentry
|
self._rawentry = rawentry
|
||||||
self.lineno = rawentry.tb_lineno - 1
|
self.lineno = rawentry.tb_lineno - 1
|
||||||
|
|
||||||
def set_repr_style(self, mode):
|
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||||
assert mode in ("short", "long")
|
assert mode in ("short", "long")
|
||||||
self._repr_style = mode
|
self._repr_style = mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def frame(self):
|
def frame(self) -> Frame:
|
||||||
import _pytest._code
|
return Frame(self._rawentry.tb_frame)
|
||||||
|
|
||||||
return _pytest._code.Frame(self._rawentry.tb_frame)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relline(self):
|
def relline(self) -> int:
|
||||||
return self.lineno - self.frame.code.firstlineno
|
return self.lineno - self.frame.code.firstlineno
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
|
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def statement(self):
|
def statement(self) -> "Source":
|
||||||
""" _pytest._code.Source object for the current statement """
|
""" _pytest._code.Source object for the current statement """
|
||||||
source = self.frame.code.fullsource
|
source = self.frame.code.fullsource
|
||||||
|
assert source is not None
|
||||||
return source.getstatement(self.lineno)
|
return source.getstatement(self.lineno)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -206,14 +215,14 @@ class TracebackEntry:
|
||||||
return self.frame.code.path
|
return self.frame.code.path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locals(self):
|
def locals(self) -> Dict[str, Any]:
|
||||||
""" locals of underlying frame """
|
""" locals of underlying frame """
|
||||||
return self.frame.f_locals
|
return self.frame.f_locals
|
||||||
|
|
||||||
def getfirstlinesource(self):
|
def getfirstlinesource(self) -> int:
|
||||||
return self.frame.code.firstlineno
|
return self.frame.code.firstlineno
|
||||||
|
|
||||||
def getsource(self, astcache=None):
|
def getsource(self, astcache=None) -> Optional["Source"]:
|
||||||
""" return failing source code. """
|
""" return failing source code. """
|
||||||
# we use the passed in astcache to not reparse asttrees
|
# we use the passed in astcache to not reparse asttrees
|
||||||
# within exception info printing
|
# within exception info printing
|
||||||
|
|
@ -258,7 +267,7 @@ class TracebackEntry:
|
||||||
return tbh(None if self._excinfo is None else self._excinfo())
|
return tbh(None if self._excinfo is None else self._excinfo())
|
||||||
return tbh
|
return tbh
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
try:
|
try:
|
||||||
fn = str(self.path)
|
fn = str(self.path)
|
||||||
except py.error.Error:
|
except py.error.Error:
|
||||||
|
|
@ -273,33 +282,42 @@ class TracebackEntry:
|
||||||
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
|
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
""" co_name of underlying code """
|
""" co_name of underlying code """
|
||||||
return self.frame.code.raw.co_name
|
return self.frame.code.raw.co_name
|
||||||
|
|
||||||
|
|
||||||
class Traceback(list):
|
class Traceback(List[TracebackEntry]):
|
||||||
""" Traceback objects encapsulate and offer higher level
|
""" Traceback objects encapsulate and offer higher level
|
||||||
access to Traceback entries.
|
access to Traceback entries.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Entry = TracebackEntry
|
def __init__(
|
||||||
|
self,
|
||||||
def __init__(self, tb, excinfo=None):
|
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||||
|
excinfo: Optional["ReferenceType[ExceptionInfo]"] = None,
|
||||||
|
) -> None:
|
||||||
""" initialize from given python traceback object and ExceptionInfo """
|
""" initialize from given python traceback object and ExceptionInfo """
|
||||||
self._excinfo = excinfo
|
self._excinfo = excinfo
|
||||||
if hasattr(tb, "tb_next"):
|
if isinstance(tb, TracebackType):
|
||||||
|
|
||||||
def f(cur):
|
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||||
while cur is not None:
|
cur_ = cur # type: Optional[TracebackType]
|
||||||
yield self.Entry(cur, excinfo=excinfo)
|
while cur_ is not None:
|
||||||
cur = cur.tb_next
|
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||||
|
cur_ = cur_.tb_next
|
||||||
|
|
||||||
list.__init__(self, f(tb))
|
super().__init__(f(tb))
|
||||||
else:
|
else:
|
||||||
list.__init__(self, tb)
|
super().__init__(tb)
|
||||||
|
|
||||||
def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
|
def cut(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
lineno: Optional[int] = None,
|
||||||
|
firstlineno: Optional[int] = None,
|
||||||
|
excludepath=None,
|
||||||
|
) -> "Traceback":
|
||||||
""" return a Traceback instance wrapping part of this Traceback
|
""" return a Traceback instance wrapping part of this Traceback
|
||||||
|
|
||||||
by providing any combination of path, lineno and firstlineno, the
|
by providing any combination of path, lineno and firstlineno, the
|
||||||
|
|
@ -325,13 +343,25 @@ class Traceback(list):
|
||||||
return Traceback(x._rawentry, self._excinfo)
|
return Traceback(x._rawentry, self._excinfo)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __getitem__(self, key):
|
@overload
|
||||||
val = super().__getitem__(key)
|
def __getitem__(self, key: int) -> TracebackEntry:
|
||||||
if isinstance(key, type(slice(0))):
|
raise NotImplementedError()
|
||||||
val = self.__class__(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def filter(self, fn=lambda x: not x.ishidden()):
|
@overload # noqa: F811
|
||||||
|
def __getitem__(self, key: slice) -> "Traceback": # noqa: F811
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __getitem__( # noqa: F811
|
||||||
|
self, key: Union[int, slice]
|
||||||
|
) -> Union[TracebackEntry, "Traceback"]:
|
||||||
|
if isinstance(key, slice):
|
||||||
|
return self.__class__(super().__getitem__(key))
|
||||||
|
else:
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
||||||
|
def filter(
|
||||||
|
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
||||||
|
) -> "Traceback":
|
||||||
""" return a Traceback instance with certain items removed
|
""" return a Traceback instance with certain items removed
|
||||||
|
|
||||||
fn is a function that gets a single argument, a TracebackEntry
|
fn is a function that gets a single argument, a TracebackEntry
|
||||||
|
|
@ -343,7 +373,7 @@ class Traceback(list):
|
||||||
"""
|
"""
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
return Traceback(filter(fn, self), self._excinfo)
|
||||||
|
|
||||||
def getcrashentry(self):
|
def getcrashentry(self) -> TracebackEntry:
|
||||||
""" return last non-hidden traceback entry that lead
|
""" return last non-hidden traceback entry that lead
|
||||||
to the exception of a traceback.
|
to the exception of a traceback.
|
||||||
"""
|
"""
|
||||||
|
|
@ -353,7 +383,7 @@ class Traceback(list):
|
||||||
return entry
|
return entry
|
||||||
return self[-1]
|
return self[-1]
|
||||||
|
|
||||||
def recursionindex(self):
|
def recursionindex(self) -> Optional[int]:
|
||||||
""" return the index of the frame/TracebackEntry where recursion
|
""" return the index of the frame/TracebackEntry where recursion
|
||||||
originates if appropriate, None if no recursion occurred
|
originates if appropriate, None if no recursion occurred
|
||||||
"""
|
"""
|
||||||
|
|
@ -449,7 +479,7 @@ class ExceptionInfo(Generic[_E]):
|
||||||
assert tup[1] is not None, "no current exception"
|
assert tup[1] is not None, "no current exception"
|
||||||
assert tup[2] is not None, "no current exception"
|
assert tup[2] is not None, "no current exception"
|
||||||
exc_info = (tup[0], tup[1], tup[2])
|
exc_info = (tup[0], tup[1], tup[2])
|
||||||
return cls.from_exc_info(exc_info)
|
return cls.from_exc_info(exc_info, exprinfo)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_later(cls) -> "ExceptionInfo[_E]":
|
def for_later(cls) -> "ExceptionInfo[_E]":
|
||||||
|
|
@ -508,7 +538,9 @@ class ExceptionInfo(Generic[_E]):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self._excinfo is None:
|
if self._excinfo is None:
|
||||||
return "<ExceptionInfo for raises contextmanager>"
|
return "<ExceptionInfo for raises contextmanager>"
|
||||||
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
|
return "<{} {} tblen={}>".format(
|
||||||
|
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
|
||||||
|
)
|
||||||
|
|
||||||
def exconly(self, tryshort: bool = False) -> str:
|
def exconly(self, tryshort: bool = False) -> str:
|
||||||
""" return the exception as a string
|
""" return the exception as a string
|
||||||
|
|
@ -541,13 +573,13 @@ class ExceptionInfo(Generic[_E]):
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
showlocals: bool = False,
|
showlocals: bool = False,
|
||||||
style: str = "long",
|
style: "_TracebackStyle" = "long",
|
||||||
abspath: bool = False,
|
abspath: bool = False,
|
||||||
tbfilter: bool = True,
|
tbfilter: bool = True,
|
||||||
funcargs: bool = False,
|
funcargs: bool = False,
|
||||||
truncate_locals: bool = True,
|
truncate_locals: bool = True,
|
||||||
chain: bool = True,
|
chain: bool = True,
|
||||||
):
|
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
|
||||||
"""
|
"""
|
||||||
Return str()able representation of this exception info.
|
Return str()able representation of this exception info.
|
||||||
|
|
||||||
|
|
@ -619,16 +651,16 @@ class FormattedExcinfo:
|
||||||
flow_marker = ">"
|
flow_marker = ">"
|
||||||
fail_marker = "E"
|
fail_marker = "E"
|
||||||
|
|
||||||
showlocals = attr.ib(default=False)
|
showlocals = attr.ib(type=bool, default=False)
|
||||||
style = attr.ib(default="long")
|
style = attr.ib(type="_TracebackStyle", default="long")
|
||||||
abspath = attr.ib(default=True)
|
abspath = attr.ib(type=bool, default=True)
|
||||||
tbfilter = attr.ib(default=True)
|
tbfilter = attr.ib(type=bool, default=True)
|
||||||
funcargs = attr.ib(default=False)
|
funcargs = attr.ib(type=bool, default=False)
|
||||||
truncate_locals = attr.ib(default=True)
|
truncate_locals = attr.ib(type=bool, default=True)
|
||||||
chain = attr.ib(default=True)
|
chain = attr.ib(type=bool, default=True)
|
||||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||||
|
|
||||||
def _getindent(self, source):
|
def _getindent(self, source: "Source") -> int:
|
||||||
# figure out indent for given source
|
# figure out indent for given source
|
||||||
try:
|
try:
|
||||||
s = str(source.getstatement(len(source) - 1))
|
s = str(source.getstatement(len(source) - 1))
|
||||||
|
|
@ -643,20 +675,27 @@ class FormattedExcinfo:
|
||||||
return 0
|
return 0
|
||||||
return 4 + (len(s) - len(s.lstrip()))
|
return 4 + (len(s) - len(s.lstrip()))
|
||||||
|
|
||||||
def _getentrysource(self, entry):
|
def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
|
||||||
source = entry.getsource(self.astcache)
|
source = entry.getsource(self.astcache)
|
||||||
if source is not None:
|
if source is not None:
|
||||||
source = source.deindent()
|
source = source.deindent()
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def repr_args(self, entry):
|
def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
|
||||||
if self.funcargs:
|
if self.funcargs:
|
||||||
args = []
|
args = []
|
||||||
for argname, argvalue in entry.frame.getargs(var=True):
|
for argname, argvalue in entry.frame.getargs(var=True):
|
||||||
args.append((argname, saferepr(argvalue)))
|
args.append((argname, saferepr(argvalue)))
|
||||||
return ReprFuncArgs(args)
|
return ReprFuncArgs(args)
|
||||||
|
return None
|
||||||
|
|
||||||
def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]:
|
def get_source(
|
||||||
|
self,
|
||||||
|
source: "Source",
|
||||||
|
line_index: int = -1,
|
||||||
|
excinfo: Optional[ExceptionInfo] = None,
|
||||||
|
short: bool = False,
|
||||||
|
) -> List[str]:
|
||||||
""" return formatted and marked up source lines. """
|
""" return formatted and marked up source lines. """
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
|
|
||||||
|
|
@ -680,19 +719,21 @@ class FormattedExcinfo:
|
||||||
lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
|
lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def get_exconly(self, excinfo, indent=4, markall=False):
|
def get_exconly(
|
||||||
|
self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False
|
||||||
|
) -> List[str]:
|
||||||
lines = []
|
lines = []
|
||||||
indent = " " * indent
|
indentstr = " " * indent
|
||||||
# get the real exception information out
|
# get the real exception information out
|
||||||
exlines = excinfo.exconly(tryshort=True).split("\n")
|
exlines = excinfo.exconly(tryshort=True).split("\n")
|
||||||
failindent = self.fail_marker + indent[1:]
|
failindent = self.fail_marker + indentstr[1:]
|
||||||
for line in exlines:
|
for line in exlines:
|
||||||
lines.append(failindent + line)
|
lines.append(failindent + line)
|
||||||
if not markall:
|
if not markall:
|
||||||
failindent = indent
|
failindent = indentstr
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def repr_locals(self, locals):
|
def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]:
|
||||||
if self.showlocals:
|
if self.showlocals:
|
||||||
lines = []
|
lines = []
|
||||||
keys = [loc for loc in locals if loc[0] != "@"]
|
keys = [loc for loc in locals if loc[0] != "@"]
|
||||||
|
|
@ -717,8 +758,11 @@ class FormattedExcinfo:
|
||||||
# # XXX
|
# # XXX
|
||||||
# pprint.pprint(value, stream=self.excinfowriter)
|
# pprint.pprint(value, stream=self.excinfowriter)
|
||||||
return ReprLocals(lines)
|
return ReprLocals(lines)
|
||||||
|
return None
|
||||||
|
|
||||||
def repr_traceback_entry(self, entry, excinfo=None):
|
def repr_traceback_entry(
|
||||||
|
self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
|
||||||
|
) -> "ReprEntry":
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
|
|
||||||
source = self._getentrysource(entry)
|
source = self._getentrysource(entry)
|
||||||
|
|
@ -729,9 +773,7 @@ class FormattedExcinfo:
|
||||||
line_index = entry.lineno - entry.getfirstlinesource()
|
line_index = entry.lineno - entry.getfirstlinesource()
|
||||||
|
|
||||||
lines = [] # type: List[str]
|
lines = [] # type: List[str]
|
||||||
style = entry._repr_style
|
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||||
if style is None:
|
|
||||||
style = self.style
|
|
||||||
if style in ("short", "long"):
|
if style in ("short", "long"):
|
||||||
short = style == "short"
|
short = style == "short"
|
||||||
reprargs = self.repr_args(entry) if not short else None
|
reprargs = self.repr_args(entry) if not short else None
|
||||||
|
|
@ -761,7 +803,7 @@ class FormattedExcinfo:
|
||||||
path = np
|
path = np
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def repr_traceback(self, excinfo):
|
def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback":
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
if self.tbfilter:
|
if self.tbfilter:
|
||||||
traceback = traceback.filter()
|
traceback = traceback.filter()
|
||||||
|
|
@ -779,7 +821,9 @@ class FormattedExcinfo:
|
||||||
entries.append(reprentry)
|
entries.append(reprentry)
|
||||||
return ReprTraceback(entries, extraline, style=self.style)
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
def _truncate_recursive_traceback(self, traceback):
|
def _truncate_recursive_traceback(
|
||||||
|
self, traceback: Traceback
|
||||||
|
) -> Tuple[Traceback, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Truncate the given recursive traceback trying to find the starting point
|
Truncate the given recursive traceback trying to find the starting point
|
||||||
of the recursion.
|
of the recursion.
|
||||||
|
|
@ -806,7 +850,9 @@ class FormattedExcinfo:
|
||||||
max_frames=max_frames,
|
max_frames=max_frames,
|
||||||
total=len(traceback),
|
total=len(traceback),
|
||||||
) # type: Optional[str]
|
) # type: Optional[str]
|
||||||
traceback = traceback[:max_frames] + traceback[-max_frames:]
|
# Type ignored because adding two instaces of a List subtype
|
||||||
|
# currently incorrectly has type List instead of the subtype.
|
||||||
|
traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
|
||||||
else:
|
else:
|
||||||
if recursionindex is not None:
|
if recursionindex is not None:
|
||||||
extraline = "!!! Recursion detected (same locals & position)"
|
extraline = "!!! Recursion detected (same locals & position)"
|
||||||
|
|
@ -816,19 +862,19 @@ class FormattedExcinfo:
|
||||||
|
|
||||||
return traceback, extraline
|
return traceback, extraline
|
||||||
|
|
||||||
def repr_excinfo(self, excinfo):
|
def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr":
|
||||||
|
|
||||||
repr_chain = (
|
repr_chain = (
|
||||||
[]
|
[]
|
||||||
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
|
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
|
||||||
e = excinfo.value
|
e = excinfo.value
|
||||||
|
excinfo_ = excinfo # type: Optional[ExceptionInfo]
|
||||||
descr = None
|
descr = None
|
||||||
seen = set() # type: Set[int]
|
seen = set() # type: Set[int]
|
||||||
while e is not None and id(e) not in seen:
|
while e is not None and id(e) not in seen:
|
||||||
seen.add(id(e))
|
seen.add(id(e))
|
||||||
if excinfo:
|
if excinfo_:
|
||||||
reprtraceback = self.repr_traceback(excinfo)
|
reprtraceback = self.repr_traceback(excinfo_)
|
||||||
reprcrash = excinfo._getreprcrash()
|
reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation]
|
||||||
else:
|
else:
|
||||||
# fallback to native repr if the exception doesn't have a traceback:
|
# fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work
|
# ExceptionInfo objects require a full traceback to work
|
||||||
|
|
@ -840,7 +886,7 @@ class FormattedExcinfo:
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||||
if e.__cause__ is not None and self.chain:
|
if e.__cause__ is not None and self.chain:
|
||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
excinfo = (
|
excinfo_ = (
|
||||||
ExceptionInfo((type(e), e, e.__traceback__))
|
ExceptionInfo((type(e), e, e.__traceback__))
|
||||||
if e.__traceback__
|
if e.__traceback__
|
||||||
else None
|
else None
|
||||||
|
|
@ -850,7 +896,7 @@ class FormattedExcinfo:
|
||||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||||
):
|
):
|
||||||
e = e.__context__
|
e = e.__context__
|
||||||
excinfo = (
|
excinfo_ = (
|
||||||
ExceptionInfo((type(e), e, e.__traceback__))
|
ExceptionInfo((type(e), e, e.__traceback__))
|
||||||
if e.__traceback__
|
if e.__traceback__
|
||||||
else None
|
else None
|
||||||
|
|
@ -863,7 +909,7 @@ class FormattedExcinfo:
|
||||||
|
|
||||||
|
|
||||||
class TerminalRepr:
|
class TerminalRepr:
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
# FYI this is called from pytest-xdist's serialization of exception
|
# FYI this is called from pytest-xdist's serialization of exception
|
||||||
# information.
|
# information.
|
||||||
io = StringIO()
|
io = StringIO()
|
||||||
|
|
@ -871,25 +917,33 @@ class TerminalRepr:
|
||||||
self.toterminal(tw)
|
self.toterminal(tw)
|
||||||
return io.getvalue().strip()
|
return io.getvalue().strip()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "<{} instance at {:0x}>".format(self.__class__, id(self))
|
return "<{} instance at {:0x}>".format(self.__class__, id(self))
|
||||||
|
|
||||||
|
def toterminal(self, tw) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class ExceptionRepr(TerminalRepr):
|
class ExceptionRepr(TerminalRepr):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.sections = [] # type: List[Tuple[str, str, str]]
|
self.sections = [] # type: List[Tuple[str, str, str]]
|
||||||
|
|
||||||
def addsection(self, name, content, sep="-"):
|
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||||
self.sections.append((name, content, sep))
|
self.sections.append((name, content, sep))
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
for name, content, sep in self.sections:
|
for name, content, sep in self.sections:
|
||||||
tw.sep(sep, name)
|
tw.sep(sep, name)
|
||||||
tw.line(content)
|
tw.line(content)
|
||||||
|
|
||||||
|
|
||||||
class ExceptionChainRepr(ExceptionRepr):
|
class ExceptionChainRepr(ExceptionRepr):
|
||||||
def __init__(self, chain):
|
def __init__(
|
||||||
|
self,
|
||||||
|
chain: Sequence[
|
||||||
|
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.chain = chain
|
self.chain = chain
|
||||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||||
|
|
@ -897,7 +951,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||||
self.reprtraceback = chain[-1][0]
|
self.reprtraceback = chain[-1][0]
|
||||||
self.reprcrash = chain[-1][1]
|
self.reprcrash = chain[-1][1]
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
for element in self.chain:
|
for element in self.chain:
|
||||||
element[0].toterminal(tw)
|
element[0].toterminal(tw)
|
||||||
if element[2] is not None:
|
if element[2] is not None:
|
||||||
|
|
@ -907,12 +961,14 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||||
|
|
||||||
|
|
||||||
class ReprExceptionInfo(ExceptionRepr):
|
class ReprExceptionInfo(ExceptionRepr):
|
||||||
def __init__(self, reprtraceback, reprcrash):
|
def __init__(
|
||||||
|
self, reprtraceback: "ReprTraceback", reprcrash: "ReprFileLocation"
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.reprtraceback = reprtraceback
|
self.reprtraceback = reprtraceback
|
||||||
self.reprcrash = reprcrash
|
self.reprcrash = reprcrash
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
self.reprtraceback.toterminal(tw)
|
self.reprtraceback.toterminal(tw)
|
||||||
super().toterminal(tw)
|
super().toterminal(tw)
|
||||||
|
|
||||||
|
|
@ -920,12 +976,17 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||||
class ReprTraceback(TerminalRepr):
|
class ReprTraceback(TerminalRepr):
|
||||||
entrysep = "_ "
|
entrysep = "_ "
|
||||||
|
|
||||||
def __init__(self, reprentries, extraline, style):
|
def __init__(
|
||||||
|
self,
|
||||||
|
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]],
|
||||||
|
extraline: Optional[str],
|
||||||
|
style: "_TracebackStyle",
|
||||||
|
) -> None:
|
||||||
self.reprentries = reprentries
|
self.reprentries = reprentries
|
||||||
self.extraline = extraline
|
self.extraline = extraline
|
||||||
self.style = style
|
self.style = style
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
# the entries might have different styles
|
# the entries might have different styles
|
||||||
for i, entry in enumerate(self.reprentries):
|
for i, entry in enumerate(self.reprentries):
|
||||||
if entry.style == "long":
|
if entry.style == "long":
|
||||||
|
|
@ -945,32 +1006,40 @@ class ReprTraceback(TerminalRepr):
|
||||||
|
|
||||||
|
|
||||||
class ReprTracebackNative(ReprTraceback):
|
class ReprTracebackNative(ReprTraceback):
|
||||||
def __init__(self, tblines):
|
def __init__(self, tblines: Sequence[str]) -> None:
|
||||||
self.style = "native"
|
self.style = "native"
|
||||||
self.reprentries = [ReprEntryNative(tblines)]
|
self.reprentries = [ReprEntryNative(tblines)]
|
||||||
self.extraline = None
|
self.extraline = None
|
||||||
|
|
||||||
|
|
||||||
class ReprEntryNative(TerminalRepr):
|
class ReprEntryNative(TerminalRepr):
|
||||||
style = "native"
|
style = "native" # type: _TracebackStyle
|
||||||
|
|
||||||
def __init__(self, tblines):
|
def __init__(self, tblines: Sequence[str]) -> None:
|
||||||
self.lines = tblines
|
self.lines = tblines
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
tw.write("".join(self.lines))
|
tw.write("".join(self.lines))
|
||||||
|
|
||||||
|
|
||||||
class ReprEntry(TerminalRepr):
|
class ReprEntry(TerminalRepr):
|
||||||
def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style):
|
def __init__(
|
||||||
|
self,
|
||||||
|
lines: Sequence[str],
|
||||||
|
reprfuncargs: Optional["ReprFuncArgs"],
|
||||||
|
reprlocals: Optional["ReprLocals"],
|
||||||
|
filelocrepr: Optional["ReprFileLocation"],
|
||||||
|
style: "_TracebackStyle",
|
||||||
|
) -> None:
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
self.reprfuncargs = reprfuncargs
|
self.reprfuncargs = reprfuncargs
|
||||||
self.reprlocals = reprlocals
|
self.reprlocals = reprlocals
|
||||||
self.reprfileloc = filelocrepr
|
self.reprfileloc = filelocrepr
|
||||||
self.style = style
|
self.style = style
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
if self.style == "short":
|
if self.style == "short":
|
||||||
|
assert self.reprfileloc is not None
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
for line in self.lines:
|
for line in self.lines:
|
||||||
red = line.startswith("E ")
|
red = line.startswith("E ")
|
||||||
|
|
@ -989,19 +1058,19 @@ class ReprEntry(TerminalRepr):
|
||||||
tw.line("")
|
tw.line("")
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "{}\n{}\n{}".format(
|
return "{}\n{}\n{}".format(
|
||||||
"\n".join(self.lines), self.reprlocals, self.reprfileloc
|
"\n".join(self.lines), self.reprlocals, self.reprfileloc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReprFileLocation(TerminalRepr):
|
class ReprFileLocation(TerminalRepr):
|
||||||
def __init__(self, path, lineno, message):
|
def __init__(self, path, lineno: int, message: str) -> None:
|
||||||
self.path = str(path)
|
self.path = str(path)
|
||||||
self.lineno = lineno
|
self.lineno = lineno
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
# filename and lineno output for each entry,
|
# filename and lineno output for each entry,
|
||||||
# using an output format that most editors understand
|
# using an output format that most editors understand
|
||||||
msg = self.message
|
msg = self.message
|
||||||
|
|
@ -1013,19 +1082,19 @@ class ReprFileLocation(TerminalRepr):
|
||||||
|
|
||||||
|
|
||||||
class ReprLocals(TerminalRepr):
|
class ReprLocals(TerminalRepr):
|
||||||
def __init__(self, lines):
|
def __init__(self, lines: Sequence[str]) -> None:
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
for line in self.lines:
|
for line in self.lines:
|
||||||
tw.line(line)
|
tw.line(line)
|
||||||
|
|
||||||
|
|
||||||
class ReprFuncArgs(TerminalRepr):
|
class ReprFuncArgs(TerminalRepr):
|
||||||
def __init__(self, args):
|
def __init__(self, args: Sequence[Tuple[str, object]]) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
if self.args:
|
if self.args:
|
||||||
linesofar = ""
|
linesofar = ""
|
||||||
for name, value in self.args:
|
for name, value in self.args:
|
||||||
|
|
@ -1044,13 +1113,11 @@ class ReprFuncArgs(TerminalRepr):
|
||||||
tw.line("")
|
tw.line("")
|
||||||
|
|
||||||
|
|
||||||
def getrawcode(obj, trycall=True):
|
def getrawcode(obj, trycall: bool = True):
|
||||||
""" return code object for given function. """
|
""" return code object for given function. """
|
||||||
try:
|
try:
|
||||||
return obj.__code__
|
return obj.__code__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
obj = getattr(obj, "im_func", obj)
|
|
||||||
obj = getattr(obj, "func_code", obj)
|
|
||||||
obj = getattr(obj, "f_code", obj)
|
obj = getattr(obj, "f_code", obj)
|
||||||
obj = getattr(obj, "__code__", obj)
|
obj = getattr(obj, "__code__", obj)
|
||||||
if trycall and not hasattr(obj, "co_firstlineno"):
|
if trycall and not hasattr(obj, "co_firstlineno"):
|
||||||
|
|
@ -1074,7 +1141,7 @@ _PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
|
||||||
_PY_DIR = py.path.local(py.__file__).dirpath()
|
_PY_DIR = py.path.local(py.__file__).dirpath()
|
||||||
|
|
||||||
|
|
||||||
def filter_traceback(entry):
|
def filter_traceback(entry: TracebackEntry) -> bool:
|
||||||
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
||||||
* dynamically generated code (no code to show up for it);
|
* dynamically generated code (no code to show up for it);
|
||||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,18 @@ import tokenize
|
||||||
import warnings
|
import warnings
|
||||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||||
from bisect import bisect_right
|
from bisect import bisect_right
|
||||||
|
from types import FrameType
|
||||||
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
from _pytest.compat import overload
|
||||||
|
|
||||||
|
|
||||||
class Source:
|
class Source:
|
||||||
""" an immutable object holding a source code fragment,
|
""" an immutable object holding a source code fragment,
|
||||||
|
|
@ -19,7 +27,7 @@ class Source:
|
||||||
|
|
||||||
_compilecounter = 0
|
_compilecounter = 0
|
||||||
|
|
||||||
def __init__(self, *parts, **kwargs):
|
def __init__(self, *parts, **kwargs) -> None:
|
||||||
self.lines = lines = [] # type: List[str]
|
self.lines = lines = [] # type: List[str]
|
||||||
de = kwargs.get("deindent", True)
|
de = kwargs.get("deindent", True)
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
|
@ -48,7 +56,15 @@ class Source:
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||||
__hash__ = None # type: ignore
|
__hash__ = None # type: ignore
|
||||||
|
|
||||||
def __getitem__(self, key):
|
@overload
|
||||||
|
def __getitem__(self, key: int) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@overload # noqa: F811
|
||||||
|
def __getitem__(self, key: slice) -> "Source": # noqa: F811
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
return self.lines[key]
|
return self.lines[key]
|
||||||
else:
|
else:
|
||||||
|
|
@ -58,10 +74,13 @@ class Source:
|
||||||
newsource.lines = self.lines[key.start : key.stop]
|
newsource.lines = self.lines[key.start : key.stop]
|
||||||
return newsource
|
return newsource
|
||||||
|
|
||||||
def __len__(self):
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return iter(self.lines)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
return len(self.lines)
|
return len(self.lines)
|
||||||
|
|
||||||
def strip(self):
|
def strip(self) -> "Source":
|
||||||
""" return new source object with trailing
|
""" return new source object with trailing
|
||||||
and leading blank lines removed.
|
and leading blank lines removed.
|
||||||
"""
|
"""
|
||||||
|
|
@ -74,18 +93,20 @@ class Source:
|
||||||
source.lines[:] = self.lines[start:end]
|
source.lines[:] = self.lines[start:end]
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def putaround(self, before="", after="", indent=" " * 4):
|
def putaround(
|
||||||
|
self, before: str = "", after: str = "", indent: str = " " * 4
|
||||||
|
) -> "Source":
|
||||||
""" return a copy of the source object with
|
""" return a copy of the source object with
|
||||||
'before' and 'after' wrapped around it.
|
'before' and 'after' wrapped around it.
|
||||||
"""
|
"""
|
||||||
before = Source(before)
|
beforesource = Source(before)
|
||||||
after = Source(after)
|
aftersource = Source(after)
|
||||||
newsource = Source()
|
newsource = Source()
|
||||||
lines = [(indent + line) for line in self.lines]
|
lines = [(indent + line) for line in self.lines]
|
||||||
newsource.lines = before.lines + lines + after.lines
|
newsource.lines = beforesource.lines + lines + aftersource.lines
|
||||||
return newsource
|
return newsource
|
||||||
|
|
||||||
def indent(self, indent=" " * 4):
|
def indent(self, indent: str = " " * 4) -> "Source":
|
||||||
""" return a copy of the source object with
|
""" return a copy of the source object with
|
||||||
all lines indented by the given indent-string.
|
all lines indented by the given indent-string.
|
||||||
"""
|
"""
|
||||||
|
|
@ -93,14 +114,14 @@ class Source:
|
||||||
newsource.lines = [(indent + line) for line in self.lines]
|
newsource.lines = [(indent + line) for line in self.lines]
|
||||||
return newsource
|
return newsource
|
||||||
|
|
||||||
def getstatement(self, lineno):
|
def getstatement(self, lineno: int) -> "Source":
|
||||||
""" return Source statement which contains the
|
""" return Source statement which contains the
|
||||||
given linenumber (counted from 0).
|
given linenumber (counted from 0).
|
||||||
"""
|
"""
|
||||||
start, end = self.getstatementrange(lineno)
|
start, end = self.getstatementrange(lineno)
|
||||||
return self[start:end]
|
return self[start:end]
|
||||||
|
|
||||||
def getstatementrange(self, lineno):
|
def getstatementrange(self, lineno: int):
|
||||||
""" return (start, end) tuple which spans the minimal
|
""" return (start, end) tuple which spans the minimal
|
||||||
statement region which containing the given lineno.
|
statement region which containing the given lineno.
|
||||||
"""
|
"""
|
||||||
|
|
@ -109,13 +130,13 @@ class Source:
|
||||||
ast, start, end = getstatementrange_ast(lineno, self)
|
ast, start, end = getstatementrange_ast(lineno, self)
|
||||||
return start, end
|
return start, end
|
||||||
|
|
||||||
def deindent(self):
|
def deindent(self) -> "Source":
|
||||||
"""return a new source object deindented."""
|
"""return a new source object deindented."""
|
||||||
newsource = Source()
|
newsource = Source()
|
||||||
newsource.lines[:] = deindent(self.lines)
|
newsource.lines[:] = deindent(self.lines)
|
||||||
return newsource
|
return newsource
|
||||||
|
|
||||||
def isparseable(self, deindent=True):
|
def isparseable(self, deindent: bool = True) -> bool:
|
||||||
""" return True if source is parseable, heuristically
|
""" return True if source is parseable, heuristically
|
||||||
deindenting it by default.
|
deindenting it by default.
|
||||||
"""
|
"""
|
||||||
|
|
@ -135,11 +156,16 @@ class Source:
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "\n".join(self.lines)
|
return "\n".join(self.lines)
|
||||||
|
|
||||||
def compile(
|
def compile(
|
||||||
self, filename=None, mode="exec", flag=0, dont_inherit=0, _genframe=None
|
self,
|
||||||
|
filename=None,
|
||||||
|
mode="exec",
|
||||||
|
flag: int = 0,
|
||||||
|
dont_inherit: int = 0,
|
||||||
|
_genframe: Optional[FrameType] = None,
|
||||||
):
|
):
|
||||||
""" return compiled code object. if filename is None
|
""" return compiled code object. if filename is None
|
||||||
invent an artificial filename which displays
|
invent an artificial filename which displays
|
||||||
|
|
@ -183,7 +209,7 @@ class Source:
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0):
|
def compile_(source, filename=None, mode="exec", flags: int = 0, dont_inherit: int = 0):
|
||||||
""" compile the given source to a raw code object,
|
""" compile the given source to a raw code object,
|
||||||
and maintain an internal cache which allows later
|
and maintain an internal cache which allows later
|
||||||
retrieval of the source code for the code object
|
retrieval of the source code for the code object
|
||||||
|
|
@ -233,7 +259,7 @@ def getfslineno(obj):
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def findsource(obj):
|
def findsource(obj) -> Tuple[Optional[Source], int]:
|
||||||
try:
|
try:
|
||||||
sourcelines, lineno = inspect.findsource(obj)
|
sourcelines, lineno = inspect.findsource(obj)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -243,7 +269,7 @@ def findsource(obj):
|
||||||
return source, lineno
|
return source, lineno
|
||||||
|
|
||||||
|
|
||||||
def getsource(obj, **kwargs):
|
def getsource(obj, **kwargs) -> Source:
|
||||||
from .code import getrawcode
|
from .code import getrawcode
|
||||||
|
|
||||||
obj = getrawcode(obj)
|
obj = getrawcode(obj)
|
||||||
|
|
@ -255,21 +281,21 @@ def getsource(obj, **kwargs):
|
||||||
return Source(strsrc, **kwargs)
|
return Source(strsrc, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def deindent(lines):
|
def deindent(lines: Sequence[str]) -> List[str]:
|
||||||
return textwrap.dedent("\n".join(lines)).splitlines()
|
return textwrap.dedent("\n".join(lines)).splitlines()
|
||||||
|
|
||||||
|
|
||||||
def get_statement_startend2(lineno, node):
|
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
# flatten all statements and except handlers into one lineno-list
|
# flatten all statements and except handlers into one lineno-list
|
||||||
# AST's line numbers start indexing at 1
|
# AST's line numbers start indexing at 1
|
||||||
values = []
|
values = [] # type: List[int]
|
||||||
for x in ast.walk(node):
|
for x in ast.walk(node):
|
||||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||||
values.append(x.lineno - 1)
|
values.append(x.lineno - 1)
|
||||||
for name in ("finalbody", "orelse"):
|
for name in ("finalbody", "orelse"):
|
||||||
val = getattr(x, name, None)
|
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
|
||||||
if val:
|
if val:
|
||||||
# treat the finally/orelse part as its own statement
|
# treat the finally/orelse part as its own statement
|
||||||
values.append(val[0].lineno - 1 - 1)
|
values.append(val[0].lineno - 1 - 1)
|
||||||
|
|
@ -283,7 +309,12 @@ def get_statement_startend2(lineno, node):
|
||||||
return start, end
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
def getstatementrange_ast(lineno, source: Source, assertion=False, astnode=None):
|
def getstatementrange_ast(
|
||||||
|
lineno: int,
|
||||||
|
source: Source,
|
||||||
|
assertion: bool = False,
|
||||||
|
astnode: Optional[ast.AST] = None,
|
||||||
|
) -> Tuple[ast.AST, int, int]:
|
||||||
if astnode is None:
|
if astnode is None:
|
||||||
content = str(source)
|
content = str(source)
|
||||||
# See #4260:
|
# See #4260:
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
import pprint
|
import pprint
|
||||||
import reprlib
|
import reprlib
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def _format_repr_exception(exc, obj):
|
def _try_repr_or_str(obj):
|
||||||
exc_name = type(exc).__name__
|
|
||||||
try:
|
try:
|
||||||
exc_info = str(exc)
|
return repr(obj)
|
||||||
except Exception:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
exc_info = "unknown"
|
raise
|
||||||
return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format(
|
except BaseException:
|
||||||
exc_name, exc_info, obj.__class__.__name__, id(obj)
|
return '{}("{}")'.format(type(obj).__name__, obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_repr_exception(exc: BaseException, obj: Any) -> str:
|
||||||
|
try:
|
||||||
|
exc_info = _try_repr_or_str(exc)
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except BaseException as exc:
|
||||||
|
exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc))
|
||||||
|
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
||||||
|
exc_info, obj.__class__.__name__, id(obj)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ellipsize(s, maxsize):
|
def _ellipsize(s: str, maxsize: int) -> str:
|
||||||
if len(s) > maxsize:
|
if len(s) > maxsize:
|
||||||
i = max(0, (maxsize - 3) // 2)
|
i = max(0, (maxsize - 3) // 2)
|
||||||
j = max(0, maxsize - 3 - i)
|
j = max(0, maxsize - 3 - i)
|
||||||
|
|
@ -26,27 +37,31 @@ class SafeRepr(reprlib.Repr):
|
||||||
and includes information on exceptions raised during the call.
|
and includes information on exceptions raised during the call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maxsize):
|
def __init__(self, maxsize: int) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.maxstring = maxsize
|
self.maxstring = maxsize
|
||||||
self.maxsize = maxsize
|
self.maxsize = maxsize
|
||||||
|
|
||||||
def repr(self, x):
|
def repr(self, x: Any) -> str:
|
||||||
try:
|
try:
|
||||||
s = super().repr(x)
|
s = super().repr(x)
|
||||||
except Exception as exc:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except BaseException as exc:
|
||||||
s = _format_repr_exception(exc, x)
|
s = _format_repr_exception(exc, x)
|
||||||
return _ellipsize(s, self.maxsize)
|
return _ellipsize(s, self.maxsize)
|
||||||
|
|
||||||
def repr_instance(self, x, level):
|
def repr_instance(self, x: Any, level: int) -> str:
|
||||||
try:
|
try:
|
||||||
s = repr(x)
|
s = repr(x)
|
||||||
except Exception as exc:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except BaseException as exc:
|
||||||
s = _format_repr_exception(exc, x)
|
s = _format_repr_exception(exc, x)
|
||||||
return _ellipsize(s, self.maxsize)
|
return _ellipsize(s, self.maxsize)
|
||||||
|
|
||||||
|
|
||||||
def safeformat(obj):
|
def safeformat(obj: Any) -> str:
|
||||||
"""return a pretty printed string for the given object.
|
"""return a pretty printed string for the given object.
|
||||||
Failing __repr__ functions of user instances will be represented
|
Failing __repr__ functions of user instances will be represented
|
||||||
with a short exception info.
|
with a short exception info.
|
||||||
|
|
@ -57,7 +72,7 @@ def safeformat(obj):
|
||||||
return _format_repr_exception(exc, obj)
|
return _format_repr_exception(exc, obj)
|
||||||
|
|
||||||
|
|
||||||
def saferepr(obj, maxsize=240):
|
def saferepr(obj: Any, maxsize: int = 240) -> str:
|
||||||
"""return a size-limited safe repr-string for the given object.
|
"""return a size-limited safe repr-string for the given object.
|
||||||
Failing __repr__ functions of user instances will be represented
|
Failing __repr__ functions of user instances will be represented
|
||||||
with a short exception info and 'saferepr' generally takes
|
with a short exception info and 'saferepr' generally takes
|
||||||
|
|
|
||||||
|
|
@ -163,5 +163,5 @@ def pytest_sessionfinish(session):
|
||||||
assertstate.hook.set_session(None)
|
assertstate.hook.set_session(None)
|
||||||
|
|
||||||
|
|
||||||
# Expose this plugin's implementation for the pytest_assertrepr_compare hook
|
def pytest_assertrepr_compare(config, op, left, right):
|
||||||
pytest_assertrepr_compare = util.assertrepr_compare
|
return util.assertrepr_compare(config=config, op=op, left=left, right=right)
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,18 @@ from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import atomicwrites
|
|
||||||
|
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._version import version
|
from _pytest._version import version
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
from _pytest.assertion.util import ( # noqa: F401
|
from _pytest.assertion.util import ( # noqa: F401
|
||||||
format_explanation as _format_explanation,
|
format_explanation as _format_explanation,
|
||||||
)
|
)
|
||||||
|
from _pytest.compat import fspath
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
|
from _pytest.pathlib import Path
|
||||||
from _pytest.pathlib import PurePath
|
from _pytest.pathlib import PurePath
|
||||||
|
|
||||||
# pytest caches rewritten pycs in __pycache__.
|
# pytest caches rewritten pycs in pycache dirs
|
||||||
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
|
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
|
||||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||||
|
|
@ -78,7 +78,8 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||||
# there's nothing to rewrite there
|
# there's nothing to rewrite there
|
||||||
# python3.5 - python3.6: `namespace`
|
# python3.5 - python3.6: `namespace`
|
||||||
# python3.7+: `None`
|
# python3.7+: `None`
|
||||||
or spec.origin in {None, "namespace"}
|
or spec.origin == "namespace"
|
||||||
|
or spec.origin is None
|
||||||
# we can only rewrite source files
|
# we can only rewrite source files
|
||||||
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
|
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
|
||||||
# if the file doesn't exist, we can't rewrite it
|
# if the file doesn't exist, we can't rewrite it
|
||||||
|
|
@ -102,7 +103,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||||
return None # default behaviour is fine
|
return None # default behaviour is fine
|
||||||
|
|
||||||
def exec_module(self, module):
|
def exec_module(self, module):
|
||||||
fn = module.__spec__.origin
|
fn = Path(module.__spec__.origin)
|
||||||
state = self.config._assertstate
|
state = self.config._assertstate
|
||||||
|
|
||||||
self._rewritten_names.add(module.__name__)
|
self._rewritten_names.add(module.__name__)
|
||||||
|
|
@ -116,15 +117,15 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||||
# cached pyc is always a complete, valid pyc. Operations on it must be
|
# cached pyc is always a complete, valid pyc. Operations on it must be
|
||||||
# atomic. POSIX's atomic rename comes in handy.
|
# atomic. POSIX's atomic rename comes in handy.
|
||||||
write = not sys.dont_write_bytecode
|
write = not sys.dont_write_bytecode
|
||||||
cache_dir = os.path.join(os.path.dirname(fn), "__pycache__")
|
cache_dir = get_cache_dir(fn)
|
||||||
if write:
|
if write:
|
||||||
ok = try_mkdir(cache_dir)
|
ok = try_makedirs(cache_dir)
|
||||||
if not ok:
|
if not ok:
|
||||||
write = False
|
write = False
|
||||||
state.trace("read only directory: {}".format(os.path.dirname(fn)))
|
state.trace("read only directory: {}".format(cache_dir))
|
||||||
|
|
||||||
cache_name = os.path.basename(fn)[:-3] + PYC_TAIL
|
cache_name = fn.name[:-3] + PYC_TAIL
|
||||||
pyc = os.path.join(cache_dir, cache_name)
|
pyc = cache_dir / cache_name
|
||||||
# Notice that even if we're in a read-only directory, I'm going
|
# Notice that even if we're in a read-only directory, I'm going
|
||||||
# to check for a cached pyc. This may not be optimal...
|
# to check for a cached pyc. This may not be optimal...
|
||||||
co = _read_pyc(fn, pyc, state.trace)
|
co = _read_pyc(fn, pyc, state.trace)
|
||||||
|
|
@ -138,7 +139,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||||
finally:
|
finally:
|
||||||
self._writing_pyc = False
|
self._writing_pyc = False
|
||||||
else:
|
else:
|
||||||
state.trace("found cached rewritten pyc for {!r}".format(fn))
|
state.trace("found cached rewritten pyc for {}".format(fn))
|
||||||
exec(co, module.__dict__)
|
exec(co, module.__dict__)
|
||||||
|
|
||||||
def _early_rewrite_bailout(self, name, state):
|
def _early_rewrite_bailout(self, name, state):
|
||||||
|
|
@ -252,30 +253,64 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
def _write_pyc(state, co, source_stat, pyc):
|
def _write_pyc_fp(fp, source_stat, co):
|
||||||
# Technically, we don't have to have the same pyc format as
|
# Technically, we don't have to have the same pyc format as
|
||||||
# (C)Python, since these "pycs" should never be seen by builtin
|
# (C)Python, since these "pycs" should never be seen by builtin
|
||||||
# import. However, there's little reason deviate.
|
# import. However, there's little reason deviate.
|
||||||
try:
|
fp.write(importlib.util.MAGIC_NUMBER)
|
||||||
with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp:
|
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
||||||
fp.write(importlib.util.MAGIC_NUMBER)
|
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
|
||||||
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
size = source_stat.st_size & 0xFFFFFFFF
|
||||||
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
|
# "<LL" stands for 2 unsigned longs, little-ending
|
||||||
size = source_stat.st_size & 0xFFFFFFFF
|
fp.write(struct.pack("<LL", mtime, size))
|
||||||
# "<LL" stands for 2 unsigned longs, little-ending
|
fp.write(marshal.dumps(co))
|
||||||
fp.write(struct.pack("<LL", mtime, size))
|
|
||||||
fp.write(marshal.dumps(co))
|
|
||||||
except EnvironmentError as e:
|
if sys.platform == "win32":
|
||||||
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno))
|
from atomicwrites import atomic_write
|
||||||
# we ignore any failure to write the cache file
|
|
||||||
# there are many reasons, permission-denied, __pycache__ being a
|
def _write_pyc(state, co, source_stat, pyc):
|
||||||
# file etc.
|
try:
|
||||||
return False
|
with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp:
|
||||||
return True
|
_write_pyc_fp(fp, source_stat, co)
|
||||||
|
except EnvironmentError as e:
|
||||||
|
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno))
|
||||||
|
# we ignore any failure to write the cache file
|
||||||
|
# there are many reasons, permission-denied, pycache dir being a
|
||||||
|
# file etc.
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def _write_pyc(state, co, source_stat, pyc):
|
||||||
|
proc_pyc = "{}.{}".format(pyc, os.getpid())
|
||||||
|
try:
|
||||||
|
fp = open(proc_pyc, "wb")
|
||||||
|
except EnvironmentError as e:
|
||||||
|
state.trace(
|
||||||
|
"error writing pyc file at {}: errno={}".format(proc_pyc, e.errno)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
_write_pyc_fp(fp, source_stat, co)
|
||||||
|
os.rename(proc_pyc, fspath(pyc))
|
||||||
|
except BaseException as e:
|
||||||
|
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno))
|
||||||
|
# we ignore any failure to write the cache file
|
||||||
|
# there are many reasons, permission-denied, pycache dir being a
|
||||||
|
# file etc.
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
fp.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _rewrite_test(fn, config):
|
def _rewrite_test(fn, config):
|
||||||
"""read and rewrite *fn* and return the code object."""
|
"""read and rewrite *fn* and return the code object."""
|
||||||
|
fn = fspath(fn)
|
||||||
stat = os.stat(fn)
|
stat = os.stat(fn)
|
||||||
with open(fn, "rb") as f:
|
with open(fn, "rb") as f:
|
||||||
source = f.read()
|
source = f.read()
|
||||||
|
|
@ -291,12 +326,12 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
Return rewritten code if successful or None if not.
|
Return rewritten code if successful or None if not.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fp = open(pyc, "rb")
|
fp = open(fspath(pyc), "rb")
|
||||||
except IOError:
|
except IOError:
|
||||||
return None
|
return None
|
||||||
with fp:
|
with fp:
|
||||||
try:
|
try:
|
||||||
stat_result = os.stat(source)
|
stat_result = os.stat(fspath(source))
|
||||||
mtime = int(stat_result.st_mtime)
|
mtime = int(stat_result.st_mtime)
|
||||||
size = stat_result.st_size
|
size = stat_result.st_size
|
||||||
data = fp.read(12)
|
data = fp.read(12)
|
||||||
|
|
@ -743,13 +778,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
# Ignore type: typeshed bug https://github.com/python/typeshed/pull/3121
|
warnings.warn_explicit(
|
||||||
warnings.warn_explicit( # type: ignore
|
|
||||||
PytestAssertRewriteWarning(
|
PytestAssertRewriteWarning(
|
||||||
"assertion is always true, perhaps remove parentheses?"
|
"assertion is always true, perhaps remove parentheses?"
|
||||||
),
|
),
|
||||||
category=None,
|
category=None,
|
||||||
filename=self.module_path,
|
filename=fspath(self.module_path),
|
||||||
lineno=assert_.lineno,
|
lineno=assert_.lineno,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -773,8 +807,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
|
||||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
|
||||||
msg = self.pop_format_context(ast.Str(explanation))
|
msg = self.pop_format_context(ast.Str(explanation))
|
||||||
|
|
||||||
# Failed
|
# Failed
|
||||||
|
|
@ -826,7 +861,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
else: # Original assertion rewriting
|
else: # Original assertion rewriting
|
||||||
# Create failure message.
|
# Create failure message.
|
||||||
body = self.expl_stmts
|
body = self.expl_stmts
|
||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
|
||||||
self.statements.append(ast.If(negation, body, []))
|
self.statements.append(ast.If(negation, body, []))
|
||||||
if assert_.msg:
|
if assert_.msg:
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
|
|
@ -872,7 +906,7 @@ warn_explicit(
|
||||||
lineno={lineno},
|
lineno={lineno},
|
||||||
)
|
)
|
||||||
""".format(
|
""".format(
|
||||||
filename=module_path, lineno=lineno
|
filename=fspath(module_path), lineno=lineno
|
||||||
)
|
)
|
||||||
).body
|
).body
|
||||||
return ast.If(val_is_none, send_warning, [])
|
return ast.If(val_is_none, send_warning, [])
|
||||||
|
|
@ -1018,18 +1052,15 @@ warn_explicit(
|
||||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||||
|
|
||||||
|
|
||||||
def try_mkdir(cache_dir):
|
def try_makedirs(cache_dir) -> bool:
|
||||||
"""Attempts to create the given directory, returns True if successful"""
|
"""Attempts to create the given directory and sub-directories exist, returns True if
|
||||||
|
successful or it already exists"""
|
||||||
try:
|
try:
|
||||||
os.mkdir(cache_dir)
|
os.makedirs(fspath(cache_dir), exist_ok=True)
|
||||||
except FileExistsError:
|
except (FileNotFoundError, NotADirectoryError, FileExistsError):
|
||||||
# Either the __pycache__ directory already exists (the
|
# One of the path components was not a directory:
|
||||||
# common case) or it's blocked by a non-dir node. In the
|
# - we're in a zip file
|
||||||
# latter case, we'll ignore it in _write_pyc.
|
# - it is a file
|
||||||
return True
|
|
||||||
except (FileNotFoundError, NotADirectoryError):
|
|
||||||
# One of the path components was not a directory, likely
|
|
||||||
# because we're in a zip file.
|
|
||||||
return False
|
return False
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
return False
|
return False
|
||||||
|
|
@ -1039,3 +1070,18 @@ def try_mkdir(cache_dir):
|
||||||
return False
|
return False
|
||||||
raise
|
raise
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_dir(file_path: Path) -> Path:
|
||||||
|
"""Returns the cache directory to write .pyc files for the given .py file path"""
|
||||||
|
# Type ignored until added in next mypy release.
|
||||||
|
if sys.version_info >= (3, 8) and sys.pycache_prefix: # type: ignore
|
||||||
|
# given:
|
||||||
|
# prefix = '/tmp/pycs'
|
||||||
|
# path = '/home/user/proj/test_app.py'
|
||||||
|
# we want:
|
||||||
|
# '/tmp/pycs/home/user/proj'
|
||||||
|
return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) # type: ignore
|
||||||
|
else:
|
||||||
|
# classic pycache directory
|
||||||
|
return file_path.parent / "__pycache__"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
"""Utilities for assertion debugging"""
|
"""Utilities for assertion debugging"""
|
||||||
|
import collections.abc
|
||||||
import pprint
|
import pprint
|
||||||
from collections.abc import Sequence
|
from typing import AbstractSet
|
||||||
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
|
from _pytest._io.saferepr import safeformat
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import ATTRS_EQ_FIELD
|
from _pytest.compat import ATTRS_EQ_FIELD
|
||||||
|
|
||||||
|
|
@ -21,7 +28,28 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s
|
||||||
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
||||||
|
|
||||||
|
|
||||||
def format_explanation(explanation):
|
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
||||||
|
"""PrettyPrinter that always dispatches (regardless of width)."""
|
||||||
|
|
||||||
|
def _format(self, object, stream, indent, allowance, context, level):
|
||||||
|
p = self._dispatch.get(type(object).__repr__, None)
|
||||||
|
|
||||||
|
objid = id(object)
|
||||||
|
if objid in context or p is None:
|
||||||
|
return super()._format(object, stream, indent, allowance, context, level)
|
||||||
|
|
||||||
|
context[objid] = 1
|
||||||
|
p(self, object, stream, indent, allowance, context, level + 1)
|
||||||
|
del context[objid]
|
||||||
|
|
||||||
|
|
||||||
|
def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False):
|
||||||
|
return AlwaysDispatchingPrettyPrinter(
|
||||||
|
indent=1, width=80, depth=None, compact=False
|
||||||
|
).pformat(object)
|
||||||
|
|
||||||
|
|
||||||
|
def format_explanation(explanation: str) -> str:
|
||||||
"""This formats an explanation
|
"""This formats an explanation
|
||||||
|
|
||||||
Normally all embedded newlines are escaped, however there are
|
Normally all embedded newlines are escaped, however there are
|
||||||
|
|
@ -36,7 +64,7 @@ def format_explanation(explanation):
|
||||||
return "\n".join(result)
|
return "\n".join(result)
|
||||||
|
|
||||||
|
|
||||||
def _split_explanation(explanation):
|
def _split_explanation(explanation: str) -> List[str]:
|
||||||
"""Return a list of individual lines in the explanation
|
"""Return a list of individual lines in the explanation
|
||||||
|
|
||||||
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
||||||
|
|
@ -53,7 +81,7 @@ def _split_explanation(explanation):
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _format_lines(lines):
|
def _format_lines(lines: Sequence[str]) -> List[str]:
|
||||||
"""Format the individual lines
|
"""Format the individual lines
|
||||||
|
|
||||||
This will replace the '{', '}' and '~' characters of our mini
|
This will replace the '{', '}' and '~' characters of our mini
|
||||||
|
|
@ -62,7 +90,7 @@ def _format_lines(lines):
|
||||||
|
|
||||||
Return a list of formatted lines.
|
Return a list of formatted lines.
|
||||||
"""
|
"""
|
||||||
result = lines[:1]
|
result = list(lines[:1])
|
||||||
stack = [0]
|
stack = [0]
|
||||||
stackcnt = [0]
|
stackcnt = [0]
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
|
|
@ -88,31 +116,31 @@ def _format_lines(lines):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def issequence(x):
|
def issequence(x: Any) -> bool:
|
||||||
return isinstance(x, Sequence) and not isinstance(x, str)
|
return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
|
||||||
|
|
||||||
|
|
||||||
def istext(x):
|
def istext(x: Any) -> bool:
|
||||||
return isinstance(x, str)
|
return isinstance(x, str)
|
||||||
|
|
||||||
|
|
||||||
def isdict(x):
|
def isdict(x: Any) -> bool:
|
||||||
return isinstance(x, dict)
|
return isinstance(x, dict)
|
||||||
|
|
||||||
|
|
||||||
def isset(x):
|
def isset(x: Any) -> bool:
|
||||||
return isinstance(x, (set, frozenset))
|
return isinstance(x, (set, frozenset))
|
||||||
|
|
||||||
|
|
||||||
def isdatacls(obj):
|
def isdatacls(obj: Any) -> bool:
|
||||||
return getattr(obj, "__dataclass_fields__", None) is not None
|
return getattr(obj, "__dataclass_fields__", None) is not None
|
||||||
|
|
||||||
|
|
||||||
def isattrs(obj):
|
def isattrs(obj: Any) -> bool:
|
||||||
return getattr(obj, "__attrs_attrs__", None) is not None
|
return getattr(obj, "__attrs_attrs__", None) is not None
|
||||||
|
|
||||||
|
|
||||||
def isiterable(obj):
|
def isiterable(obj: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
iter(obj)
|
iter(obj)
|
||||||
return not istext(obj)
|
return not istext(obj)
|
||||||
|
|
@ -120,15 +148,23 @@ def isiterable(obj):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def assertrepr_compare(config, op, left, right):
|
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
||||||
"""Return specialised explanations for some operators/operands"""
|
"""Return specialised explanations for some operators/operands"""
|
||||||
maxsize = (80 - 15 - len(op) - 2) // 2 # 15 chars indentation, 1 space around op
|
verbose = config.getoption("verbose")
|
||||||
left_repr = saferepr(left, maxsize=maxsize)
|
if verbose > 1:
|
||||||
right_repr = saferepr(right, maxsize=maxsize)
|
left_repr = safeformat(left)
|
||||||
|
right_repr = safeformat(right)
|
||||||
|
else:
|
||||||
|
# XXX: "15 chars indentation" is wrong
|
||||||
|
# ("E AssertionError: assert "); should use term width.
|
||||||
|
maxsize = (
|
||||||
|
80 - 15 - len(op) - 2
|
||||||
|
) // 2 # 15 chars indentation, 1 space around op
|
||||||
|
left_repr = saferepr(left, maxsize=maxsize)
|
||||||
|
right_repr = saferepr(right, maxsize=maxsize)
|
||||||
|
|
||||||
summary = "{} {} {}".format(left_repr, op, right_repr)
|
summary = "{} {} {}".format(left_repr, op, right_repr)
|
||||||
|
|
||||||
verbose = config.getoption("verbose")
|
|
||||||
explanation = None
|
explanation = None
|
||||||
try:
|
try:
|
||||||
if op == "==":
|
if op == "==":
|
||||||
|
|
@ -170,33 +206,16 @@ def assertrepr_compare(config, op, left, right):
|
||||||
return [summary] + explanation
|
return [summary] + explanation
|
||||||
|
|
||||||
|
|
||||||
def _diff_text(left, right, verbose=0):
|
def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
"""Return the explanation for the diff between text or bytes.
|
"""Return the explanation for the diff between text.
|
||||||
|
|
||||||
Unless --verbose is used this will skip leading and trailing
|
Unless --verbose is used this will skip leading and trailing
|
||||||
characters which are identical to keep the diff minimal.
|
characters which are identical to keep the diff minimal.
|
||||||
|
|
||||||
If the input are bytes they will be safely converted to text.
|
|
||||||
"""
|
"""
|
||||||
from difflib import ndiff
|
from difflib import ndiff
|
||||||
|
|
||||||
explanation = [] # type: List[str]
|
explanation = [] # type: List[str]
|
||||||
|
|
||||||
def escape_for_readable_diff(binary_text):
|
|
||||||
"""
|
|
||||||
Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode.
|
|
||||||
This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape
|
|
||||||
newlines and carriage returns (#429).
|
|
||||||
"""
|
|
||||||
r = str(repr(binary_text)[1:-1])
|
|
||||||
r = r.replace(r"\n", "\n")
|
|
||||||
r = r.replace(r"\r", "\r")
|
|
||||||
return r
|
|
||||||
|
|
||||||
if isinstance(left, bytes):
|
|
||||||
left = escape_for_readable_diff(left)
|
|
||||||
if isinstance(right, bytes):
|
|
||||||
right = escape_for_readable_diff(right)
|
|
||||||
if verbose < 1:
|
if verbose < 1:
|
||||||
i = 0 # just in case left or right has zero length
|
i = 0 # just in case left or right has zero length
|
||||||
for i in range(min(len(left), len(right))):
|
for i in range(min(len(left), len(right))):
|
||||||
|
|
@ -233,7 +252,7 @@ def _diff_text(left, right, verbose=0):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_verbose(left, right):
|
def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
|
||||||
keepends = True
|
keepends = True
|
||||||
left_lines = repr(left).splitlines(keepends)
|
left_lines = repr(left).splitlines(keepends)
|
||||||
right_lines = repr(right).splitlines(keepends)
|
right_lines = repr(right).splitlines(keepends)
|
||||||
|
|
@ -245,7 +264,21 @@ def _compare_eq_verbose(left, right):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_iterable(left, right, verbose=0):
|
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
|
||||||
|
"""Move opening/closing parenthesis/bracket to own lines."""
|
||||||
|
opening = lines[0][:1]
|
||||||
|
if opening in ["(", "[", "{"]:
|
||||||
|
lines[0] = " " + lines[0][1:]
|
||||||
|
lines[:] = [opening] + lines
|
||||||
|
closing = lines[-1][-1:]
|
||||||
|
if closing in [")", "]", "}"]:
|
||||||
|
lines[-1] = lines[-1][:-1] + ","
|
||||||
|
lines[:] = lines + [closing]
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_eq_iterable(
|
||||||
|
left: Iterable[Any], right: Iterable[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
if not verbose:
|
if not verbose:
|
||||||
return ["Use -v to get the full diff"]
|
return ["Use -v to get the full diff"]
|
||||||
# dynamic import to speedup pytest
|
# dynamic import to speedup pytest
|
||||||
|
|
@ -253,14 +286,28 @@ def _compare_eq_iterable(left, right, verbose=0):
|
||||||
|
|
||||||
left_formatting = pprint.pformat(left).splitlines()
|
left_formatting = pprint.pformat(left).splitlines()
|
||||||
right_formatting = pprint.pformat(right).splitlines()
|
right_formatting = pprint.pformat(right).splitlines()
|
||||||
|
|
||||||
|
# Re-format for different output lengths.
|
||||||
|
lines_left = len(left_formatting)
|
||||||
|
lines_right = len(right_formatting)
|
||||||
|
if lines_left != lines_right:
|
||||||
|
left_formatting = _pformat_dispatch(left).splitlines()
|
||||||
|
right_formatting = _pformat_dispatch(right).splitlines()
|
||||||
|
|
||||||
|
if lines_left > 1 or lines_right > 1:
|
||||||
|
_surrounding_parens_on_own_lines(left_formatting)
|
||||||
|
_surrounding_parens_on_own_lines(right_formatting)
|
||||||
|
|
||||||
explanation = ["Full diff:"]
|
explanation = ["Full diff:"]
|
||||||
explanation.extend(
|
explanation.extend(
|
||||||
line.strip() for line in difflib.ndiff(left_formatting, right_formatting)
|
line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting)
|
||||||
)
|
)
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_sequence(left, right, verbose=0):
|
def _compare_eq_sequence(
|
||||||
|
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
||||||
explanation = [] # type: List[str]
|
explanation = [] # type: List[str]
|
||||||
len_left = len(left)
|
len_left = len(left)
|
||||||
|
|
@ -314,7 +361,9 @@ def _compare_eq_sequence(left, right, verbose=0):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_set(left, right, verbose=0):
|
def _compare_eq_set(
|
||||||
|
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
explanation = []
|
explanation = []
|
||||||
diff_left = left - right
|
diff_left = left - right
|
||||||
diff_right = right - left
|
diff_right = right - left
|
||||||
|
|
@ -329,7 +378,9 @@ def _compare_eq_set(left, right, verbose=0):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_dict(left, right, verbose=0):
|
def _compare_eq_dict(
|
||||||
|
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
|
||||||
|
) -> List[str]:
|
||||||
explanation = [] # type: List[str]
|
explanation = [] # type: List[str]
|
||||||
set_left = set(left)
|
set_left = set(left)
|
||||||
set_right = set(right)
|
set_right = set(right)
|
||||||
|
|
@ -368,7 +419,12 @@ def _compare_eq_dict(left, right, verbose=0):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_cls(left, right, verbose, type_fns):
|
def _compare_eq_cls(
|
||||||
|
left: Any,
|
||||||
|
right: Any,
|
||||||
|
verbose: int,
|
||||||
|
type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]],
|
||||||
|
) -> List[str]:
|
||||||
isdatacls, isattrs = type_fns
|
isdatacls, isattrs = type_fns
|
||||||
if isdatacls(left):
|
if isdatacls(left):
|
||||||
all_fields = left.__dataclass_fields__
|
all_fields = left.__dataclass_fields__
|
||||||
|
|
@ -402,7 +458,7 @@ def _compare_eq_cls(left, right, verbose, type_fns):
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _notin_text(term, text, verbose=0):
|
def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
|
||||||
index = text.find(term)
|
index = text.find(term)
|
||||||
head = text[:index]
|
head = text[:index]
|
||||||
tail = text[index + len(term) :]
|
tail = text[index + len(term) :]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ ignores the external pytest-cache
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
@ -15,6 +16,9 @@ import pytest
|
||||||
from .pathlib import Path
|
from .pathlib import Path
|
||||||
from .pathlib import resolve_from_str
|
from .pathlib import resolve_from_str
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
|
from _pytest import nodes
|
||||||
|
from _pytest.config import Config
|
||||||
|
from _pytest.main import Session
|
||||||
|
|
||||||
README_CONTENT = """\
|
README_CONTENT = """\
|
||||||
# pytest cache directory #
|
# pytest cache directory #
|
||||||
|
|
@ -121,13 +125,14 @@ class Cache:
|
||||||
return
|
return
|
||||||
if not cache_dir_exists_already:
|
if not cache_dir_exists_already:
|
||||||
self._ensure_supporting_files()
|
self._ensure_supporting_files()
|
||||||
|
data = json.dumps(value, indent=2, sort_keys=True)
|
||||||
try:
|
try:
|
||||||
f = path.open("w")
|
f = path.open("w")
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
self.warn("cache could not write path {path}", path=path)
|
self.warn("cache could not write path {path}", path=path)
|
||||||
else:
|
else:
|
||||||
with f:
|
with f:
|
||||||
json.dump(value, f, indent=2, sort_keys=True)
|
f.write(data)
|
||||||
|
|
||||||
def _ensure_supporting_files(self):
|
def _ensure_supporting_files(self):
|
||||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||||
|
|
@ -263,10 +268,12 @@ class NFPlugin:
|
||||||
self.active = config.option.newfirst
|
self.active = config.option.newfirst
|
||||||
self.cached_nodeids = config.cache.get("cache/nodeids", [])
|
self.cached_nodeids = config.cache.get("cache/nodeids", [])
|
||||||
|
|
||||||
def pytest_collection_modifyitems(self, session, config, items):
|
def pytest_collection_modifyitems(
|
||||||
new_items = OrderedDict()
|
self, session: Session, config: Config, items: List[nodes.Item]
|
||||||
|
) -> None:
|
||||||
|
new_items = OrderedDict() # type: OrderedDict[str, nodes.Item]
|
||||||
if self.active:
|
if self.active:
|
||||||
other_items = OrderedDict()
|
other_items = OrderedDict() # type: OrderedDict[str, nodes.Item]
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.nodeid not in self.cached_nodeids:
|
if item.nodeid not in self.cached_nodeids:
|
||||||
new_items[item.nodeid] = item
|
new_items[item.nodeid] = item
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from tempfile import TemporaryFile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import CaptureIO
|
from _pytest.compat import CaptureIO
|
||||||
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
|
||||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||||
|
|
||||||
|
|
@ -241,13 +242,12 @@ class CaptureManager:
|
||||||
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_only_one_capture_fixture(request, name):
|
def _ensure_only_one_capture_fixture(request: FixtureRequest, name):
|
||||||
fixtures = set(request.fixturenames) & capture_fixtures - {name}
|
fixtures = sorted(set(request.fixturenames) & capture_fixtures - {name})
|
||||||
if fixtures:
|
if fixtures:
|
||||||
fixtures = sorted(fixtures)
|
arg = fixtures[0] if len(fixtures) == 1 else fixtures
|
||||||
fixtures = fixtures[0] if len(fixtures) == 1 else fixtures
|
|
||||||
raise request.raiseerror(
|
raise request.raiseerror(
|
||||||
"cannot use {} and {} at the same time".format(fixtures, name)
|
"cannot use {} and {} at the same time".format(arg, name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -693,17 +693,12 @@ class SysCaptureBinary(SysCapture):
|
||||||
|
|
||||||
|
|
||||||
class DontReadFromInput:
|
class DontReadFromInput:
|
||||||
"""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,
|
|
||||||
because in automated test runs it is better to crash than
|
|
||||||
hang indefinitely.
|
|
||||||
"""
|
|
||||||
|
|
||||||
encoding = None
|
encoding = None
|
||||||
|
|
||||||
def read(self, *args):
|
def read(self, *args):
|
||||||
raise IOError("reading from stdin while output is captured")
|
raise IOError(
|
||||||
|
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||||
|
)
|
||||||
|
|
||||||
readline = read
|
readline = read
|
||||||
readlines = read
|
readlines = read
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ python version compatibility code
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Generic
|
||||||
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
@ -19,6 +27,13 @@ from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
from typing import Type # noqa: F401 (used in type string)
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
_S = TypeVar("_S")
|
||||||
|
|
||||||
|
|
||||||
NOTSET = object()
|
NOTSET = object()
|
||||||
|
|
||||||
|
|
@ -28,12 +43,13 @@ MODULE_NOT_FOUND_ERROR = (
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
from importlib import metadata as importlib_metadata # noqa: F401
|
# Type ignored until next mypy release.
|
||||||
|
from importlib import metadata as importlib_metadata # type: ignore
|
||||||
else:
|
else:
|
||||||
import importlib_metadata # noqa: F401
|
import importlib_metadata # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def _format_args(func):
|
def _format_args(func: Callable[..., Any]) -> str:
|
||||||
return str(signature(func))
|
return str(signature(func))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,12 +57,25 @@ def _format_args(func):
|
||||||
REGEX_TYPE = type(re.compile(""))
|
REGEX_TYPE = type(re.compile(""))
|
||||||
|
|
||||||
|
|
||||||
def is_generator(func):
|
if sys.version_info < (3, 6):
|
||||||
|
|
||||||
|
def fspath(p):
|
||||||
|
"""os.fspath replacement, useful to point out when we should replace it by the
|
||||||
|
real function once we drop py35.
|
||||||
|
"""
|
||||||
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
fspath = os.fspath
|
||||||
|
|
||||||
|
|
||||||
|
def is_generator(func: object) -> bool:
|
||||||
genfunc = inspect.isgeneratorfunction(func)
|
genfunc = inspect.isgeneratorfunction(func)
|
||||||
return genfunc and not iscoroutinefunction(func)
|
return genfunc and not iscoroutinefunction(func)
|
||||||
|
|
||||||
|
|
||||||
def iscoroutinefunction(func):
|
def iscoroutinefunction(func: object) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if func is a coroutine function (a function defined with async
|
Return True if func is a coroutine function (a function defined with async
|
||||||
def syntax, and doesn't contain yield), or a function decorated with
|
def syntax, and doesn't contain yield), or a function decorated with
|
||||||
|
|
@ -59,7 +88,7 @@ def iscoroutinefunction(func):
|
||||||
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
|
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
|
||||||
|
|
||||||
|
|
||||||
def getlocation(function, curdir=None):
|
def getlocation(function, curdir=None) -> str:
|
||||||
function = get_real_func(function)
|
function = get_real_func(function)
|
||||||
fn = py.path.local(inspect.getfile(function))
|
fn = py.path.local(inspect.getfile(function))
|
||||||
lineno = function.__code__.co_firstlineno
|
lineno = function.__code__.co_firstlineno
|
||||||
|
|
@ -68,7 +97,7 @@ def getlocation(function, curdir=None):
|
||||||
return "%s:%d" % (fn, lineno + 1)
|
return "%s:%d" % (fn, lineno + 1)
|
||||||
|
|
||||||
|
|
||||||
def num_mock_patch_args(function):
|
def num_mock_patch_args(function) -> int:
|
||||||
""" return number of arguments used up by mock arguments (if any) """
|
""" return number of arguments used up by mock arguments (if any) """
|
||||||
patchings = getattr(function, "patchings", None)
|
patchings = getattr(function, "patchings", None)
|
||||||
if not patchings:
|
if not patchings:
|
||||||
|
|
@ -87,7 +116,13 @@ def num_mock_patch_args(function):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def getfuncargnames(function, *, name: str = "", is_method=False, cls=None):
|
def getfuncargnames(
|
||||||
|
function: Callable[..., Any],
|
||||||
|
*,
|
||||||
|
name: str = "",
|
||||||
|
is_method: bool = False,
|
||||||
|
cls: Optional[type] = None
|
||||||
|
) -> Tuple[str, ...]:
|
||||||
"""Returns the names of a function's mandatory arguments.
|
"""Returns the names of a function's mandatory arguments.
|
||||||
|
|
||||||
This should return the names of all function arguments that:
|
This should return the names of all function arguments that:
|
||||||
|
|
@ -155,7 +190,7 @@ else:
|
||||||
from contextlib import nullcontext # noqa
|
from contextlib import nullcontext # noqa
|
||||||
|
|
||||||
|
|
||||||
def get_default_arg_names(function):
|
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||||
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
|
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
|
||||||
# to get the arguments which were excluded from its result because they had default values
|
# to get the arguments which were excluded from its result because they had default values
|
||||||
return tuple(
|
return tuple(
|
||||||
|
|
@ -174,18 +209,18 @@ _non_printable_ascii_translate_table.update(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _translate_non_printable(s):
|
def _translate_non_printable(s: str) -> str:
|
||||||
return s.translate(_non_printable_ascii_translate_table)
|
return s.translate(_non_printable_ascii_translate_table)
|
||||||
|
|
||||||
|
|
||||||
STRING_TYPES = bytes, str
|
STRING_TYPES = bytes, str
|
||||||
|
|
||||||
|
|
||||||
def _bytes_to_ascii(val):
|
def _bytes_to_ascii(val: bytes) -> str:
|
||||||
return val.decode("ascii", "backslashreplace")
|
return val.decode("ascii", "backslashreplace")
|
||||||
|
|
||||||
|
|
||||||
def ascii_escaped(val):
|
def ascii_escaped(val: Union[bytes, str]):
|
||||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
||||||
bytes objects into a sequence of escaped bytes:
|
bytes objects into a sequence of escaped bytes:
|
||||||
|
|
||||||
|
|
@ -282,7 +317,7 @@ def getimfunc(func):
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
def safe_getattr(object, name, default):
|
def safe_getattr(object: Any, name: str, default: Any) -> Any:
|
||||||
""" Like getattr but return default upon any Exception or any OutcomeException.
|
""" Like getattr but return default upon any Exception or any OutcomeException.
|
||||||
|
|
||||||
Attribute access can potentially fail for 'evil' Python objects.
|
Attribute access can potentially fail for 'evil' Python objects.
|
||||||
|
|
@ -296,7 +331,7 @@ def safe_getattr(object, name, default):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def safe_isclass(obj):
|
def safe_isclass(obj: object) -> bool:
|
||||||
"""Ignore any exception via isinstance on Python 3."""
|
"""Ignore any exception via isinstance on Python 3."""
|
||||||
try:
|
try:
|
||||||
return inspect.isclass(obj)
|
return inspect.isclass(obj)
|
||||||
|
|
@ -317,39 +352,26 @@ COLLECT_FAKEMODULE_ATTRIBUTES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_collect_fakemodule():
|
def _setup_collect_fakemodule() -> None:
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytest.collect = ModuleType("pytest.collect")
|
# Types ignored because the module is created dynamically.
|
||||||
pytest.collect.__all__ = [] # used for setns
|
pytest.collect = ModuleType("pytest.collect") # type: ignore
|
||||||
|
pytest.collect.__all__ = [] # type: ignore # used for setns
|
||||||
for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES:
|
for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES:
|
||||||
setattr(pytest.collect, attr_name, getattr(pytest, attr_name))
|
setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class CaptureIO(io.TextIOWrapper):
|
class CaptureIO(io.TextIOWrapper):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
|
super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
|
||||||
|
|
||||||
def getvalue(self):
|
def getvalue(self) -> str:
|
||||||
|
assert isinstance(self.buffer, io.BytesIO)
|
||||||
return self.buffer.getvalue().decode("UTF-8")
|
return self.buffer.getvalue().decode("UTF-8")
|
||||||
|
|
||||||
|
|
||||||
class FuncargnamesCompatAttr:
|
|
||||||
""" helper class so that Metafunc, Function and FixtureRequest
|
|
||||||
don't need to each define the "funcargnames" compatibility attribute.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def funcargnames(self):
|
|
||||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
|
||||||
import warnings
|
|
||||||
from _pytest.deprecated import FUNCARGNAMES
|
|
||||||
|
|
||||||
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
|
||||||
return self.fixturenames
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 5, 2): # pragma: no cover
|
if sys.version_info < (3, 5, 2): # pragma: no cover
|
||||||
|
|
||||||
def overload(f): # noqa: F811
|
def overload(f): # noqa: F811
|
||||||
|
|
@ -360,3 +382,35 @@ if getattr(attr, "__version_info__", ()) >= (19, 2):
|
||||||
ATTRS_EQ_FIELD = "eq"
|
ATTRS_EQ_FIELD = "eq"
|
||||||
else:
|
else:
|
||||||
ATTRS_EQ_FIELD = "cmp"
|
ATTRS_EQ_FIELD = "cmp"
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
# TODO: Remove type ignore on next mypy update.
|
||||||
|
# https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709
|
||||||
|
from functools import cached_property # type: ignore
|
||||||
|
else:
|
||||||
|
|
||||||
|
class cached_property(Generic[_S, _T]):
|
||||||
|
__slots__ = ("func", "__doc__")
|
||||||
|
|
||||||
|
def __init__(self, func: Callable[[_S], _T]) -> None:
|
||||||
|
self.func = func
|
||||||
|
self.__doc__ = func.__doc__
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(
|
||||||
|
self, instance: None, owner: Optional["Type[_S]"] = ...
|
||||||
|
) -> "cached_property[_S, _T]":
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@overload # noqa: F811
|
||||||
|
def __get__( # noqa: F811
|
||||||
|
self, instance: _S, owner: Optional["Type[_S]"] = ...
|
||||||
|
) -> _T:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __get__(self, instance, owner=None): # noqa: F811
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||||
|
return value
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import sys
|
||||||
import types
|
import types
|
||||||
import warnings
|
import warnings
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
@ -18,6 +17,7 @@ from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
@ -39,6 +39,7 @@ from _pytest._code import filter_traceback
|
||||||
from _pytest.compat import importlib_metadata
|
from _pytest.compat import importlib_metadata
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
|
from _pytest.pathlib import Path
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
|
|
||||||
if False: # TYPE_CHECKING
|
if False: # TYPE_CHECKING
|
||||||
|
|
@ -56,7 +57,7 @@ class ConftestImportFailure(Exception):
|
||||||
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
|
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
|
||||||
|
|
||||||
|
|
||||||
def main(args=None, plugins=None):
|
def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
|
||||||
""" return exit code, after performing an in-process test run.
|
""" return exit code, after performing an in-process test run.
|
||||||
|
|
||||||
:arg args: list of command line arguments.
|
:arg args: list of command line arguments.
|
||||||
|
|
@ -84,10 +85,16 @@ def main(args=None, plugins=None):
|
||||||
formatted_tb = str(exc_repr)
|
formatted_tb = str(exc_repr)
|
||||||
for line in formatted_tb.splitlines():
|
for line in formatted_tb.splitlines():
|
||||||
tw.line(line.rstrip(), red=True)
|
tw.line(line.rstrip(), red=True)
|
||||||
return 4
|
return ExitCode.USAGE_ERROR
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return config.hook.pytest_cmdline_main(config=config)
|
ret = config.hook.pytest_cmdline_main(
|
||||||
|
config=config
|
||||||
|
) # type: Union[ExitCode, int]
|
||||||
|
try:
|
||||||
|
return ExitCode(ret)
|
||||||
|
except ValueError:
|
||||||
|
return ret
|
||||||
finally:
|
finally:
|
||||||
config._ensure_unconfigure()
|
config._ensure_unconfigure()
|
||||||
except UsageError as e:
|
except UsageError as e:
|
||||||
|
|
@ -124,13 +131,13 @@ def directory_arg(path, optname):
|
||||||
|
|
||||||
|
|
||||||
# Plugins that cannot be disabled via "-p no:X" currently.
|
# Plugins that cannot be disabled via "-p no:X" currently.
|
||||||
essential_plugins = ( # fmt: off
|
essential_plugins = (
|
||||||
"mark",
|
"mark",
|
||||||
"main",
|
"main",
|
||||||
"runner",
|
"runner",
|
||||||
"fixtures",
|
"fixtures",
|
||||||
"helpconfig", # Provides -p.
|
"helpconfig", # Provides -p.
|
||||||
) # fmt: on
|
)
|
||||||
|
|
||||||
default_plugins = essential_plugins + (
|
default_plugins = essential_plugins + (
|
||||||
"python",
|
"python",
|
||||||
|
|
@ -169,7 +176,7 @@ def get_config(args=None, plugins=None):
|
||||||
config = Config(
|
config = Config(
|
||||||
pluginmanager,
|
pluginmanager,
|
||||||
invocation_params=Config.InvocationParams(
|
invocation_params=Config.InvocationParams(
|
||||||
args=args, plugins=plugins, dir=Path().resolve()
|
args=args or (), plugins=plugins, dir=Path().resolve()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -649,7 +656,7 @@ class Config:
|
||||||
|
|
||||||
Contains the following read-only attributes:
|
Contains the following read-only attributes:
|
||||||
|
|
||||||
* ``args``: list of command-line arguments as passed to ``pytest.main()``.
|
* ``args``: tuple of command-line arguments as passed to ``pytest.main()``.
|
||||||
* ``plugins``: list of extra plugins, might be None.
|
* ``plugins``: list of extra plugins, might be None.
|
||||||
* ``dir``: directory where ``pytest.main()`` was invoked from.
|
* ``dir``: directory where ``pytest.main()`` was invoked from.
|
||||||
"""
|
"""
|
||||||
|
|
@ -662,13 +669,13 @@ class Config:
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Currently the environment variable PYTEST_ADDOPTS is also handled by
|
Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
|
||||||
pytest implicitly, not being part of the invocation.
|
ini option are handled by pytest, not being included in the ``args`` attribute.
|
||||||
|
|
||||||
Plugins accessing ``InvocationParams`` must be aware of that.
|
Plugins accessing ``InvocationParams`` must be aware of that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args = attr.ib()
|
args = attr.ib(converter=tuple)
|
||||||
plugins = attr.ib()
|
plugins = attr.ib()
|
||||||
dir = attr.ib(type=Path)
|
dir = attr.ib(type=Path)
|
||||||
|
|
||||||
|
|
@ -697,7 +704,9 @@ class Config:
|
||||||
self._cleanup = [] # type: List[Callable[[], None]]
|
self._cleanup = [] # type: List[Callable[[], None]]
|
||||||
self.pluginmanager.register(self, "pytestconfig")
|
self.pluginmanager.register(self, "pytestconfig")
|
||||||
self._configured = False
|
self._configured = False
|
||||||
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
|
self.hook.pytest_addoption.call_historic(
|
||||||
|
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invocation_dir(self):
|
def invocation_dir(self):
|
||||||
|
|
@ -933,7 +942,6 @@ class Config:
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
self, "args"
|
self, "args"
|
||||||
), "can only parse cmdline args at most once per Config object"
|
), "can only parse cmdline args at most once per Config object"
|
||||||
assert self.invocation_params.args == args
|
|
||||||
self.hook.pytest_addhooks.call_historic(
|
self.hook.pytest_addhooks.call_historic(
|
||||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||||
)
|
)
|
||||||
|
|
@ -965,7 +973,7 @@ class Config:
|
||||||
def getini(self, name: str):
|
def getini(self, name: str):
|
||||||
""" return configuration value from an :ref:`ini file <inifiles>`. If the
|
""" return configuration value from an :ref:`ini file <inifiles>`. If the
|
||||||
specified name hasn't been registered through a prior
|
specified name hasn't been registered through a prior
|
||||||
:py:func:`parser.addini <_pytest.config.Parser.addini>`
|
:py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
|
||||||
call (usually from a plugin), a ValueError is raised. """
|
call (usually from a plugin), a ValueError is raised. """
|
||||||
try:
|
try:
|
||||||
return self._inicache[name]
|
return self._inicache[name]
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class Parser:
|
||||||
|
|
||||||
The returned group object has an ``addoption`` method with the same
|
The returned group object has an ``addoption`` method with the same
|
||||||
signature as :py:func:`parser.addoption
|
signature as :py:func:`parser.addoption
|
||||||
<_pytest.config.Parser.addoption>` but will be shown in the
|
<_pytest.config.argparsing.Parser.addoption>` but will be shown in the
|
||||||
respective group in the output of ``pytest. --help``.
|
respective group in the output of ``pytest. --help``.
|
||||||
"""
|
"""
|
||||||
for group in self._groups:
|
for group in self._groups:
|
||||||
|
|
@ -395,7 +395,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
options = ", ".join(option for _, option, _ in option_tuples)
|
options = ", ".join(option for _, option, _ in option_tuples)
|
||||||
self.error(msg % {"option": arg_string, "matches": options})
|
self.error(msg % {"option": arg_string, "matches": options})
|
||||||
elif len(option_tuples) == 1:
|
elif len(option_tuples) == 1:
|
||||||
option_tuple, = option_tuples
|
(option_tuple,) = option_tuples
|
||||||
return option_tuple
|
return option_tuple
|
||||||
if self._negative_number_matcher.match(arg_string):
|
if self._negative_number_matcher.match(arg_string):
|
||||||
if not self._has_negative_number_optionals:
|
if not self._has_negative_number_optionals:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
""" interactive debugging with PDB, the Python Debugger. """
|
""" interactive debugging with PDB, the Python Debugger. """
|
||||||
import argparse
|
import argparse
|
||||||
import functools
|
import functools
|
||||||
import pdb
|
|
||||||
import sys
|
import sys
|
||||||
from doctest import UnexpectedException
|
|
||||||
|
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
|
@ -46,6 +44,8 @@ def pytest_addoption(parser):
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
|
import pdb
|
||||||
|
|
||||||
if config.getvalue("trace"):
|
if config.getvalue("trace"):
|
||||||
config.pluginmanager.register(PdbTrace(), "pdbtrace")
|
config.pluginmanager.register(PdbTrace(), "pdbtrace")
|
||||||
if config.getvalue("usepdb"):
|
if config.getvalue("usepdb"):
|
||||||
|
|
@ -88,6 +88,8 @@ class pytestPDB:
|
||||||
@classmethod
|
@classmethod
|
||||||
def _import_pdb_cls(cls, capman):
|
def _import_pdb_cls(cls, capman):
|
||||||
if not cls._config:
|
if not cls._config:
|
||||||
|
import pdb
|
||||||
|
|
||||||
# Happens when using pytest.set_trace outside of a test.
|
# Happens when using pytest.set_trace outside of a test.
|
||||||
return pdb.Pdb
|
return pdb.Pdb
|
||||||
|
|
||||||
|
|
@ -114,6 +116,8 @@ class pytestPDB:
|
||||||
"--pdbcls: could not import {!r}: {}".format(value, exc)
|
"--pdbcls: could not import {!r}: {}".format(value, exc)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
import pdb
|
||||||
|
|
||||||
pdb_cls = pdb.Pdb
|
pdb_cls = pdb.Pdb
|
||||||
|
|
||||||
wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
|
wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
|
||||||
|
|
@ -317,6 +321,8 @@ def _enter_pdb(node, excinfo, rep):
|
||||||
|
|
||||||
|
|
||||||
def _postmortem_traceback(excinfo):
|
def _postmortem_traceback(excinfo):
|
||||||
|
from doctest import UnexpectedException
|
||||||
|
|
||||||
if isinstance(excinfo.value, UnexpectedException):
|
if isinstance(excinfo.value, UnexpectedException):
|
||||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||||
# Use the underlying exception instead:
|
# Use the underlying exception instead:
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ FUNCARGNAMES = PytestDeprecationWarning(
|
||||||
|
|
||||||
|
|
||||||
RESULT_LOG = PytestDeprecationWarning(
|
RESULT_LOG = PytestDeprecationWarning(
|
||||||
"--result-log is deprecated and scheduled for removal in pytest 6.0.\n"
|
"--result-log is deprecated, please try the new pytest-reportlog plugin.\n"
|
||||||
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -34,3 +34,8 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
|
||||||
"Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
|
"Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
|
||||||
"as a keyword argument instead."
|
"as a keyword argument instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning(
|
||||||
|
"The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n"
|
||||||
|
"Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible."
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
""" discover and run doctests in modules and test files."""
|
""" discover and run doctests in modules and test files."""
|
||||||
|
import bdb
|
||||||
import inspect
|
import inspect
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest import outcomes
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import ReprFileLocation
|
from _pytest._code.code import ReprFileLocation
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
|
@ -16,6 +24,10 @@ from _pytest.outcomes import Skipped
|
||||||
from _pytest.python_api import approx
|
from _pytest.python_api import approx
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
import doctest
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||||
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
||||||
|
|
@ -32,6 +44,8 @@ DOCTEST_REPORT_CHOICES = (
|
||||||
|
|
||||||
# Lazy definition of runner class
|
# Lazy definition of runner class
|
||||||
RUNNER_CLASS = None
|
RUNNER_CLASS = None
|
||||||
|
# Lazy definition of output checker class
|
||||||
|
CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]]
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
|
@ -84,6 +98,12 @@ def pytest_addoption(parser):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_unconfigure():
|
||||||
|
global RUNNER_CLASS
|
||||||
|
|
||||||
|
RUNNER_CLASS = None
|
||||||
|
|
||||||
|
|
||||||
def pytest_collect_file(path, parent):
|
def pytest_collect_file(path, parent):
|
||||||
config = parent.config
|
config = parent.config
|
||||||
if path.ext == ".py":
|
if path.ext == ".py":
|
||||||
|
|
@ -111,11 +131,12 @@ def _is_doctest(config, path, parent):
|
||||||
|
|
||||||
|
|
||||||
class ReprFailDoctest(TerminalRepr):
|
class ReprFailDoctest(TerminalRepr):
|
||||||
def __init__(self, reprlocation_lines):
|
def __init__(
|
||||||
# List of (reprlocation, lines) tuples
|
self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
|
||||||
|
):
|
||||||
self.reprlocation_lines = reprlocation_lines
|
self.reprlocation_lines = reprlocation_lines
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
for reprlocation, lines in self.reprlocation_lines:
|
for reprlocation, lines in self.reprlocation_lines:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
tw.line(line)
|
tw.line(line)
|
||||||
|
|
@ -128,7 +149,7 @@ class MultipleDoctestFailures(Exception):
|
||||||
self.failures = failures
|
self.failures = failures
|
||||||
|
|
||||||
|
|
||||||
def _init_runner_class():
|
def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class PytestDoctestRunner(doctest.DebugRunner):
|
class PytestDoctestRunner(doctest.DebugRunner):
|
||||||
|
|
@ -155,6 +176,8 @@ def _init_runner_class():
|
||||||
def report_unexpected_exception(self, out, test, example, exc_info):
|
def report_unexpected_exception(self, out, test, example, exc_info):
|
||||||
if isinstance(exc_info[1], Skipped):
|
if isinstance(exc_info[1], Skipped):
|
||||||
raise exc_info[1]
|
raise exc_info[1]
|
||||||
|
if isinstance(exc_info[1], bdb.BdbQuit):
|
||||||
|
outcomes.exit("Quitting debugger")
|
||||||
failure = doctest.UnexpectedException(test, example, exc_info)
|
failure = doctest.UnexpectedException(test, example, exc_info)
|
||||||
if self.continue_on_failure:
|
if self.continue_on_failure:
|
||||||
out.append(failure)
|
out.append(failure)
|
||||||
|
|
@ -164,12 +187,19 @@ def _init_runner_class():
|
||||||
return PytestDoctestRunner
|
return PytestDoctestRunner
|
||||||
|
|
||||||
|
|
||||||
def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True):
|
def _get_runner(
|
||||||
|
checker: Optional["doctest.OutputChecker"] = None,
|
||||||
|
verbose: Optional[bool] = None,
|
||||||
|
optionflags: int = 0,
|
||||||
|
continue_on_failure: bool = True,
|
||||||
|
) -> "doctest.DocTestRunner":
|
||||||
# We need this in order to do a lazy import on doctest
|
# We need this in order to do a lazy import on doctest
|
||||||
global RUNNER_CLASS
|
global RUNNER_CLASS
|
||||||
if RUNNER_CLASS is None:
|
if RUNNER_CLASS is None:
|
||||||
RUNNER_CLASS = _init_runner_class()
|
RUNNER_CLASS = _init_runner_class()
|
||||||
return RUNNER_CLASS(
|
# Type ignored because the continue_on_failure argument is only defined on
|
||||||
|
# PytestDoctestRunner, which is lazily defined so can't be used as a type.
|
||||||
|
return RUNNER_CLASS( # type: ignore
|
||||||
checker=checker,
|
checker=checker,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
|
|
@ -198,7 +228,7 @@ class DoctestItem(pytest.Item):
|
||||||
def runtest(self):
|
def runtest(self):
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures = []
|
failures = [] # type: List[doctest.DocTestFailure]
|
||||||
self.runner.run(self.dtest, out=failures)
|
self.runner.run(self.dtest, out=failures)
|
||||||
if failures:
|
if failures:
|
||||||
raise MultipleDoctestFailures(failures)
|
raise MultipleDoctestFailures(failures)
|
||||||
|
|
@ -219,7 +249,9 @@ class DoctestItem(pytest.Item):
|
||||||
def repr_failure(self, excinfo):
|
def repr_failure(self, excinfo):
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
failures = None
|
failures = (
|
||||||
|
None
|
||||||
|
) # type: Optional[List[Union[doctest.DocTestFailure, doctest.UnexpectedException]]]
|
||||||
if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
|
if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
|
||||||
failures = [excinfo.value]
|
failures = [excinfo.value]
|
||||||
elif excinfo.errisinstance(MultipleDoctestFailures):
|
elif excinfo.errisinstance(MultipleDoctestFailures):
|
||||||
|
|
@ -242,8 +274,10 @@ class DoctestItem(pytest.Item):
|
||||||
self.config.getoption("doctestreport")
|
self.config.getoption("doctestreport")
|
||||||
)
|
)
|
||||||
if lineno is not None:
|
if lineno is not None:
|
||||||
|
assert failure.test.docstring is not None
|
||||||
lines = failure.test.docstring.splitlines(False)
|
lines = failure.test.docstring.splitlines(False)
|
||||||
# add line numbers to the left of the error message
|
# add line numbers to the left of the error message
|
||||||
|
assert test.lineno is not None
|
||||||
lines = [
|
lines = [
|
||||||
"%03d %s" % (i + test.lineno + 1, x)
|
"%03d %s" % (i + test.lineno + 1, x)
|
||||||
for (i, x) in enumerate(lines)
|
for (i, x) in enumerate(lines)
|
||||||
|
|
@ -271,11 +305,11 @@ class DoctestItem(pytest.Item):
|
||||||
else:
|
else:
|
||||||
return super().repr_failure(excinfo)
|
return super().repr_failure(excinfo)
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self) -> Tuple[str, int, str]:
|
||||||
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
def _get_flag_lookup():
|
def _get_flag_lookup() -> Dict[str, int]:
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
|
|
@ -327,7 +361,7 @@ class DoctestTextfile(pytest.Module):
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self)
|
||||||
|
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=0,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
checker=_get_checker(),
|
checker=_get_checker(),
|
||||||
continue_on_failure=_get_continue_on_failure(self.config),
|
continue_on_failure=_get_continue_on_failure(self.config),
|
||||||
|
|
@ -406,7 +440,8 @@ class DoctestModule(pytest.Module):
|
||||||
return
|
return
|
||||||
with _patch_unwrap_mock_aware():
|
with _patch_unwrap_mock_aware():
|
||||||
|
|
||||||
doctest.DocTestFinder._find(
|
# Type ignored because this is a private function.
|
||||||
|
doctest.DocTestFinder._find( # type: ignore
|
||||||
self, tests, obj, name, module, source_lines, globs, seen
|
self, tests, obj, name, module, source_lines, globs, seen
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -424,7 +459,7 @@ class DoctestModule(pytest.Module):
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=0,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
checker=_get_checker(),
|
checker=_get_checker(),
|
||||||
continue_on_failure=_get_continue_on_failure(self.config),
|
continue_on_failure=_get_continue_on_failure(self.config),
|
||||||
|
|
@ -453,24 +488,7 @@ def _setup_fixtures(doctest_item):
|
||||||
return fixture_request
|
return fixture_request
|
||||||
|
|
||||||
|
|
||||||
def _get_checker():
|
def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||||
"""
|
|
||||||
Returns a doctest.OutputChecker subclass that supports some
|
|
||||||
additional options:
|
|
||||||
|
|
||||||
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
|
||||||
prefixes (respectively) in string literals. Useful when the same
|
|
||||||
doctest should run in Python 2 and Python 3.
|
|
||||||
|
|
||||||
* NUMBER to ignore floating-point differences smaller than the
|
|
||||||
precision of the literal number in the doctest.
|
|
||||||
|
|
||||||
An inner class is used to avoid importing "doctest" at the module
|
|
||||||
level.
|
|
||||||
"""
|
|
||||||
if hasattr(_get_checker, "LiteralsOutputChecker"):
|
|
||||||
return _get_checker.LiteralsOutputChecker()
|
|
||||||
|
|
||||||
import doctest
|
import doctest
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
@ -560,11 +578,31 @@ def _get_checker():
|
||||||
offset += w.end() - w.start() - (g.end() - g.start())
|
offset += w.end() - w.start() - (g.end() - g.start())
|
||||||
return got
|
return got
|
||||||
|
|
||||||
_get_checker.LiteralsOutputChecker = LiteralsOutputChecker
|
return LiteralsOutputChecker
|
||||||
return _get_checker.LiteralsOutputChecker()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_unicode_flag():
|
def _get_checker() -> "doctest.OutputChecker":
|
||||||
|
"""
|
||||||
|
Returns a doctest.OutputChecker subclass that supports some
|
||||||
|
additional options:
|
||||||
|
|
||||||
|
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
||||||
|
prefixes (respectively) in string literals. Useful when the same
|
||||||
|
doctest should run in Python 2 and Python 3.
|
||||||
|
|
||||||
|
* NUMBER to ignore floating-point differences smaller than the
|
||||||
|
precision of the literal number in the doctest.
|
||||||
|
|
||||||
|
An inner class is used to avoid importing "doctest" at the module
|
||||||
|
level.
|
||||||
|
"""
|
||||||
|
global CHECKER_CLASS
|
||||||
|
if CHECKER_CLASS is None:
|
||||||
|
CHECKER_CLASS = _init_checker_class()
|
||||||
|
return CHECKER_CLASS()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_allow_unicode_flag() -> int:
|
||||||
"""
|
"""
|
||||||
Registers and returns the ALLOW_UNICODE flag.
|
Registers and returns the ALLOW_UNICODE flag.
|
||||||
"""
|
"""
|
||||||
|
|
@ -573,7 +611,7 @@ def _get_allow_unicode_flag():
|
||||||
return doctest.register_optionflag("ALLOW_UNICODE")
|
return doctest.register_optionflag("ALLOW_UNICODE")
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_bytes_flag():
|
def _get_allow_bytes_flag() -> int:
|
||||||
"""
|
"""
|
||||||
Registers and returns the ALLOW_BYTES flag.
|
Registers and returns the ALLOW_BYTES flag.
|
||||||
"""
|
"""
|
||||||
|
|
@ -582,7 +620,7 @@ def _get_allow_bytes_flag():
|
||||||
return doctest.register_optionflag("ALLOW_BYTES")
|
return doctest.register_optionflag("ALLOW_BYTES")
|
||||||
|
|
||||||
|
|
||||||
def _get_number_flag():
|
def _get_number_flag() -> int:
|
||||||
"""
|
"""
|
||||||
Registers and returns the NUMBER flag.
|
Registers and returns the NUMBER flag.
|
||||||
"""
|
"""
|
||||||
|
|
@ -591,7 +629,7 @@ def _get_number_flag():
|
||||||
return doctest.register_optionflag("NUMBER")
|
return doctest.register_optionflag("NUMBER")
|
||||||
|
|
||||||
|
|
||||||
def _get_report_choice(key):
|
def _get_report_choice(key: str) -> int:
|
||||||
"""
|
"""
|
||||||
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
||||||
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,17 @@ from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest import nodes
|
|
||||||
from _pytest._code.code import FormattedExcinfo
|
from _pytest._code.code import FormattedExcinfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest.compat import _format_args
|
from _pytest.compat import _format_args
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
from _pytest.compat import FuncargnamesCompatAttr
|
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import get_real_method
|
from _pytest.compat import get_real_method
|
||||||
from _pytest.compat import getfslineno
|
from _pytest.compat import getfslineno
|
||||||
|
|
@ -29,12 +28,15 @@ from _pytest.compat import is_generator
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
|
||||||
|
from _pytest.deprecated import FUNCARGNAMES
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
if False: # TYPE_CHECKING
|
if False: # TYPE_CHECKING
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from _pytest import nodes
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True)
|
@attr.s(frozen=True)
|
||||||
class PseudoFixtureDef:
|
class PseudoFixtureDef:
|
||||||
|
|
@ -334,7 +336,7 @@ class FuncFixtureInfo:
|
||||||
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
||||||
|
|
||||||
|
|
||||||
class FixtureRequest(FuncargnamesCompatAttr):
|
class FixtureRequest:
|
||||||
""" A request for a fixture from a test or fixture function.
|
""" A request for a fixture from a test or fixture function.
|
||||||
|
|
||||||
A request object gives access to the requesting test context
|
A request object gives access to the requesting test context
|
||||||
|
|
@ -361,6 +363,12 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
result.extend(set(self._fixture_defs).difference(result))
|
result.extend(set(self._fixture_defs).difference(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def funcargnames(self):
|
||||||
|
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||||
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
|
return self.fixturenames
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node(self):
|
def node(self):
|
||||||
""" underlying collection node (depends on current request scope)"""
|
""" underlying collection node (depends on current request scope)"""
|
||||||
|
|
@ -689,8 +697,8 @@ class FixtureLookupError(LookupError):
|
||||||
self.fixturestack = request._get_fixturestack()
|
self.fixturestack = request._get_fixturestack()
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
||||||
def formatrepr(self):
|
def formatrepr(self) -> "FixtureLookupErrorRepr":
|
||||||
tblines = []
|
tblines = [] # type: List[str]
|
||||||
addline = tblines.append
|
addline = tblines.append
|
||||||
stack = [self.request._pyfuncitem.obj]
|
stack = [self.request._pyfuncitem.obj]
|
||||||
stack.extend(map(lambda x: x.func, self.fixturestack))
|
stack.extend(map(lambda x: x.func, self.fixturestack))
|
||||||
|
|
@ -742,7 +750,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
||||||
self.firstlineno = firstlineno
|
self.firstlineno = firstlineno
|
||||||
self.argname = argname
|
self.argname = argname
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
# tw.line("FixtureLookupError: %s" %(self.argname), red=True)
|
# tw.line("FixtureLookupError: %s" %(self.argname), red=True)
|
||||||
for tbline in self.tblines:
|
for tbline in self.tblines:
|
||||||
tw.line(tbline.rstrip())
|
tw.line(tbline.rstrip())
|
||||||
|
|
@ -1283,6 +1291,8 @@ class FixtureManager:
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
from _pytest import nodes
|
||||||
|
|
||||||
# construct the base nodeid which is later used to check
|
# construct the base nodeid which is later used to check
|
||||||
# what fixtures are visible for particular tests (as denoted
|
# what fixtures are visible for particular tests (as denoted
|
||||||
# by their test id)
|
# by their test id)
|
||||||
|
|
@ -1459,6 +1469,8 @@ class FixtureManager:
|
||||||
return tuple(self._matchfactories(fixturedefs, nodeid))
|
return tuple(self._matchfactories(fixturedefs, nodeid))
|
||||||
|
|
||||||
def _matchfactories(self, fixturedefs, nodeid):
|
def _matchfactories(self, fixturedefs, nodeid):
|
||||||
|
from _pytest import nodes
|
||||||
|
|
||||||
for fixturedef in fixturedefs:
|
for fixturedef in fixturedefs:
|
||||||
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
||||||
yield fixturedef
|
yield fixturedef
|
||||||
|
|
|
||||||
|
|
@ -115,9 +115,10 @@ def pytest_cmdline_parse():
|
||||||
|
|
||||||
|
|
||||||
def showversion(config):
|
def showversion(config):
|
||||||
p = py.path.local(pytest.__file__)
|
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"This is pytest version {}, imported from {}\n".format(pytest.__version__, p)
|
"This is pytest version {}, imported from {}\n".format(
|
||||||
|
pytest.__version__, pytest.__file__
|
||||||
|
)
|
||||||
)
|
)
|
||||||
plugininfo = getpluginversioninfo(config)
|
plugininfo = getpluginversioninfo(config)
|
||||||
if plugininfo:
|
if plugininfo:
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ def pytest_plugin_registered(plugin, manager):
|
||||||
|
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser, pluginmanager):
|
||||||
"""register argparse-style options and ini-style config values,
|
"""register argparse-style options and ini-style config values,
|
||||||
called once at the beginning of a test run.
|
called once at the beginning of a test run.
|
||||||
|
|
||||||
|
|
@ -45,10 +45,15 @@ def pytest_addoption(parser):
|
||||||
files situated at the tests root directory due to how pytest
|
files situated at the tests root directory due to how pytest
|
||||||
:ref:`discovers plugins during startup <pluginorder>`.
|
:ref:`discovers plugins during startup <pluginorder>`.
|
||||||
|
|
||||||
:arg _pytest.config.Parser parser: To add command line options, call
|
:arg _pytest.config.argparsing.Parser parser: To add command line options, call
|
||||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
:py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`.
|
||||||
To add ini-file values call :py:func:`parser.addini(...)
|
To add ini-file values call :py:func:`parser.addini(...)
|
||||||
<_pytest.config.Parser.addini>`.
|
<_pytest.config.argparsing.Parser.addini>`.
|
||||||
|
|
||||||
|
:arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager,
|
||||||
|
which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s
|
||||||
|
and allow one plugin to call another plugin's hooks to change how
|
||||||
|
command line options are added.
|
||||||
|
|
||||||
Options can later be accessed through the
|
Options can later be accessed through the
|
||||||
:py:class:`config <_pytest.config.Config>` object, respectively:
|
:py:class:`config <_pytest.config.Config>` object, respectively:
|
||||||
|
|
@ -143,7 +148,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||||
|
|
||||||
:param _pytest.config.Config early_config: pytest config object
|
:param _pytest.config.Config early_config: pytest config object
|
||||||
:param list[str] args: list of arguments passed on the command line
|
:param list[str] args: list of arguments passed on the command line
|
||||||
:param _pytest.config.Parser parser: to add command line options
|
:param _pytest.config.argparsing.Parser parser: to add command line options
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -381,16 +386,6 @@ def pytest_runtest_logreport(report):
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_to_serializable(config, report):
|
def pytest_report_to_serializable(config, report):
|
||||||
"""
|
"""
|
||||||
.. warning::
|
|
||||||
This hook is experimental and subject to change between pytest releases, even
|
|
||||||
bug fixes.
|
|
||||||
|
|
||||||
The intent is for this to be used by plugins maintained by the core-devs, such
|
|
||||||
as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal
|
|
||||||
'resultlog' plugin.
|
|
||||||
|
|
||||||
In the future it might become part of the public hook API.
|
|
||||||
|
|
||||||
Serializes the given report object into a data structure suitable for sending
|
Serializes the given report object into a data structure suitable for sending
|
||||||
over the wire, e.g. converted to JSON.
|
over the wire, e.g. converted to JSON.
|
||||||
"""
|
"""
|
||||||
|
|
@ -399,16 +394,6 @@ def pytest_report_to_serializable(config, report):
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_from_serializable(config, data):
|
def pytest_report_from_serializable(config, data):
|
||||||
"""
|
"""
|
||||||
.. warning::
|
|
||||||
This hook is experimental and subject to change between pytest releases, even
|
|
||||||
bug fixes.
|
|
||||||
|
|
||||||
The intent is for this to be used by plugins maintained by the core-devs, such
|
|
||||||
as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal
|
|
||||||
'resultlog' plugin.
|
|
||||||
|
|
||||||
In the future it might become part of the public hook API.
|
|
||||||
|
|
||||||
Restores a report object previously serialized with pytest_report_to_serializable().
|
Restores a report object previously serialized with pytest_report_to_serializable().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ from datetime import datetime
|
||||||
import py
|
import py
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest import deprecated
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest.config import filename_arg
|
from _pytest.config import filename_arg
|
||||||
|
from _pytest.warnings import _issue_warning_captured
|
||||||
|
|
||||||
|
|
||||||
class Junit(py.xml.Namespace):
|
class Junit(py.xml.Namespace):
|
||||||
|
|
@ -421,9 +423,7 @@ def pytest_addoption(parser):
|
||||||
default="total",
|
default="total",
|
||||||
) # choices=['total', 'call'])
|
) # choices=['total', 'call'])
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"junit_family",
|
"junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None
|
||||||
"Emit XML for schema: one of legacy|xunit1|xunit2",
|
|
||||||
default="xunit1",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -431,13 +431,17 @@ def pytest_configure(config):
|
||||||
xmlpath = config.option.xmlpath
|
xmlpath = config.option.xmlpath
|
||||||
# prevent opening xmllog on slave nodes (xdist)
|
# prevent opening xmllog on slave nodes (xdist)
|
||||||
if xmlpath and not hasattr(config, "slaveinput"):
|
if xmlpath and not hasattr(config, "slaveinput"):
|
||||||
|
junit_family = config.getini("junit_family")
|
||||||
|
if not junit_family:
|
||||||
|
_issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2)
|
||||||
|
junit_family = "xunit1"
|
||||||
config._xml = LogXML(
|
config._xml = LogXML(
|
||||||
xmlpath,
|
xmlpath,
|
||||||
config.option.junitprefix,
|
config.option.junitprefix,
|
||||||
config.getini("junit_suite_name"),
|
config.getini("junit_suite_name"),
|
||||||
config.getini("junit_logging"),
|
config.getini("junit_logging"),
|
||||||
config.getini("junit_duration_report"),
|
config.getini("junit_duration_report"),
|
||||||
config.getini("junit_family"),
|
junit_family,
|
||||||
config.getini("junit_log_passing_tests"),
|
config.getini("junit_log_passing_tests"),
|
||||||
)
|
)
|
||||||
config.pluginmanager.register(config._xml)
|
config.pluginmanager.register(config._xml)
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@ import logging
|
||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from typing import AbstractSet
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import nullcontext
|
from _pytest.compat import nullcontext
|
||||||
|
from _pytest.config import _strtobool
|
||||||
from _pytest.config import create_terminal_writer
|
from _pytest.config import create_terminal_writer
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
@ -31,14 +36,15 @@ class ColoredLevelFormatter(logging.Formatter):
|
||||||
logging.INFO: {"green"},
|
logging.INFO: {"green"},
|
||||||
logging.DEBUG: {"purple"},
|
logging.DEBUG: {"purple"},
|
||||||
logging.NOTSET: set(),
|
logging.NOTSET: set(),
|
||||||
}
|
} # type: Mapping[int, AbstractSet[str]]
|
||||||
LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)")
|
LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)")
|
||||||
|
|
||||||
def __init__(self, terminalwriter, *args, **kwargs):
|
def __init__(self, terminalwriter, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._original_fmt = self._style._fmt
|
self._original_fmt = self._style._fmt
|
||||||
self._level_to_fmt_mapping = {}
|
self._level_to_fmt_mapping = {} # type: Dict[int, str]
|
||||||
|
|
||||||
|
assert self._fmt is not None
|
||||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||||
if not levelname_fmt_match:
|
if not levelname_fmt_match:
|
||||||
return
|
return
|
||||||
|
|
@ -71,24 +77,87 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
formats the message as if each line were logged separately.
|
formats the message as if each line were logged separately.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fmt, auto_indent):
|
||||||
|
super().__init__(fmt)
|
||||||
|
self._auto_indent = self._get_auto_indent(auto_indent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _update_message(record_dict, message):
|
def _update_message(record_dict, message):
|
||||||
tmp = record_dict.copy()
|
tmp = record_dict.copy()
|
||||||
tmp["message"] = message
|
tmp["message"] = message
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_auto_indent(auto_indent_option) -> int:
|
||||||
|
"""Determines the current auto indentation setting
|
||||||
|
|
||||||
|
Specify auto indent behavior (on/off/fixed) by passing in
|
||||||
|
extra={"auto_indent": [value]} to the call to logging.log() or
|
||||||
|
using a --log-auto-indent [value] command line or the
|
||||||
|
log_auto_indent [value] config option.
|
||||||
|
|
||||||
|
Default behavior is auto-indent off.
|
||||||
|
|
||||||
|
Using the string "True" or "on" or the boolean True as the value
|
||||||
|
turns auto indent on, using the string "False" or "off" or the
|
||||||
|
boolean False or the int 0 turns it off, and specifying a
|
||||||
|
positive integer fixes the indentation position to the value
|
||||||
|
specified.
|
||||||
|
|
||||||
|
Any other values for the option are invalid, and will silently be
|
||||||
|
converted to the default.
|
||||||
|
|
||||||
|
:param any auto_indent_option: User specified option for indentation
|
||||||
|
from command line, config or extra kwarg. Accepts int, bool or str.
|
||||||
|
str option accepts the same range of values as boolean config options,
|
||||||
|
as well as positive integers represented in str form.
|
||||||
|
|
||||||
|
:returns: indentation value, which can be
|
||||||
|
-1 (automatically determine indentation) or
|
||||||
|
0 (auto-indent turned off) or
|
||||||
|
>0 (explicitly set indentation position).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if type(auto_indent_option) is int:
|
||||||
|
return int(auto_indent_option)
|
||||||
|
elif type(auto_indent_option) is str:
|
||||||
|
try:
|
||||||
|
return int(auto_indent_option)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if _strtobool(auto_indent_option):
|
||||||
|
return -1
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
elif type(auto_indent_option) is bool:
|
||||||
|
if auto_indent_option:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
if "\n" in record.message:
|
if "\n" in record.message:
|
||||||
lines = record.message.splitlines()
|
if hasattr(record, "auto_indent"):
|
||||||
formatted = self._fmt % self._update_message(record.__dict__, lines[0])
|
# passed in from the "extra={}" kwarg on the call to logging.log()
|
||||||
# TODO optimize this by introducing an option that tells the
|
auto_indent = self._get_auto_indent(record.auto_indent)
|
||||||
# logging framework that the indentation doesn't
|
else:
|
||||||
# change. This allows to compute the indentation only once.
|
auto_indent = self._auto_indent
|
||||||
indentation = _remove_ansi_escape_sequences(formatted).find(lines[0])
|
|
||||||
lines[0] = formatted
|
if auto_indent:
|
||||||
return ("\n" + " " * indentation).join(lines)
|
lines = record.message.splitlines()
|
||||||
else:
|
formatted = self._fmt % self._update_message(record.__dict__, lines[0])
|
||||||
return self._fmt % record.__dict__
|
|
||||||
|
if auto_indent < 0:
|
||||||
|
indentation = _remove_ansi_escape_sequences(formatted).find(
|
||||||
|
lines[0]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# optimizes logging by allowing a fixed indentation
|
||||||
|
indentation = auto_indent
|
||||||
|
lines[0] = formatted
|
||||||
|
return ("\n" + " " * indentation).join(lines)
|
||||||
|
return self._fmt % record.__dict__
|
||||||
|
|
||||||
|
|
||||||
def get_option_ini(config, *names):
|
def get_option_ini(config, *names):
|
||||||
|
|
@ -182,6 +251,12 @@ def pytest_addoption(parser):
|
||||||
default=DEFAULT_LOG_DATE_FORMAT,
|
default=DEFAULT_LOG_DATE_FORMAT,
|
||||||
help="log date format as used by the logging module.",
|
help="log date format as used by the logging module.",
|
||||||
)
|
)
|
||||||
|
add_option_ini(
|
||||||
|
"--log-auto-indent",
|
||||||
|
dest="log_auto_indent",
|
||||||
|
default=None,
|
||||||
|
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|
@ -215,17 +290,17 @@ def catching_logs(handler, formatter=None, level=None):
|
||||||
class LogCaptureHandler(logging.StreamHandler):
|
class LogCaptureHandler(logging.StreamHandler):
|
||||||
"""A logging handler that stores log records and the log text."""
|
"""A logging handler that stores log records and the log text."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Creates a new log handler."""
|
"""Creates a new log handler."""
|
||||||
logging.StreamHandler.__init__(self, StringIO())
|
logging.StreamHandler.__init__(self, StringIO())
|
||||||
self.records = []
|
self.records = [] # type: List[logging.LogRecord]
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
"""Keep the log records in a list in addition to the log text."""
|
"""Keep the log records in a list in addition to the log text."""
|
||||||
self.records.append(record)
|
self.records.append(record)
|
||||||
logging.StreamHandler.emit(self, record)
|
logging.StreamHandler.emit(self, record)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
self.records = []
|
self.records = []
|
||||||
self.stream = StringIO()
|
self.stream = StringIO()
|
||||||
|
|
||||||
|
|
@ -233,13 +308,13 @@ class LogCaptureHandler(logging.StreamHandler):
|
||||||
class LogCaptureFixture:
|
class LogCaptureFixture:
|
||||||
"""Provides access and control of log capturing."""
|
"""Provides access and control of log capturing."""
|
||||||
|
|
||||||
def __init__(self, item):
|
def __init__(self, item) -> None:
|
||||||
"""Creates a new funcarg."""
|
"""Creates a new funcarg."""
|
||||||
self._item = item
|
self._item = item
|
||||||
# dict of log name -> log level
|
# dict of log name -> log level
|
||||||
self._initial_log_levels = {} # Dict[str, int]
|
self._initial_log_levels = {} # type: Dict[str, int]
|
||||||
|
|
||||||
def _finalize(self):
|
def _finalize(self) -> None:
|
||||||
"""Finalizes the fixture.
|
"""Finalizes the fixture.
|
||||||
|
|
||||||
This restores the log levels changed by :meth:`set_level`.
|
This restores the log levels changed by :meth:`set_level`.
|
||||||
|
|
@ -413,6 +488,7 @@ class LoggingPlugin:
|
||||||
self.formatter = self._create_formatter(
|
self.formatter = self._create_formatter(
|
||||||
get_option_ini(config, "log_format"),
|
get_option_ini(config, "log_format"),
|
||||||
get_option_ini(config, "log_date_format"),
|
get_option_ini(config, "log_date_format"),
|
||||||
|
get_option_ini(config, "log_auto_indent"),
|
||||||
)
|
)
|
||||||
self.log_level = get_actual_log_level(config, "log_level")
|
self.log_level = get_actual_log_level(config, "log_level")
|
||||||
|
|
||||||
|
|
@ -444,7 +520,7 @@ class LoggingPlugin:
|
||||||
if self._log_cli_enabled():
|
if self._log_cli_enabled():
|
||||||
self._setup_cli_logging()
|
self._setup_cli_logging()
|
||||||
|
|
||||||
def _create_formatter(self, log_format, log_date_format):
|
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
||||||
# color option doesn't exist if terminal plugin is disabled
|
# color option doesn't exist if terminal plugin is disabled
|
||||||
color = getattr(self._config.option, "color", "no")
|
color = getattr(self._config.option, "color", "no")
|
||||||
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
|
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
|
||||||
|
|
@ -452,11 +528,14 @@ class LoggingPlugin:
|
||||||
):
|
):
|
||||||
formatter = ColoredLevelFormatter(
|
formatter = ColoredLevelFormatter(
|
||||||
create_terminal_writer(self._config), log_format, log_date_format
|
create_terminal_writer(self._config), log_format, log_date_format
|
||||||
)
|
) # type: logging.Formatter
|
||||||
else:
|
else:
|
||||||
formatter = logging.Formatter(log_format, log_date_format)
|
formatter = logging.Formatter(log_format, log_date_format)
|
||||||
|
|
||||||
formatter._style = PercentStyleMultiline(formatter._style._fmt)
|
formatter._style = PercentStyleMultiline(
|
||||||
|
formatter._style._fmt, auto_indent=auto_indent
|
||||||
|
)
|
||||||
|
|
||||||
return formatter
|
return formatter
|
||||||
|
|
||||||
def _setup_cli_logging(self):
|
def _setup_cli_logging(self):
|
||||||
|
|
@ -473,6 +552,7 @@ class LoggingPlugin:
|
||||||
log_cli_formatter = self._create_formatter(
|
log_cli_formatter = self._create_formatter(
|
||||||
get_option_ini(config, "log_cli_format", "log_format"),
|
get_option_ini(config, "log_cli_format", "log_format"),
|
||||||
get_option_ini(config, "log_cli_date_format", "log_date_format"),
|
get_option_ini(config, "log_cli_date_format", "log_date_format"),
|
||||||
|
get_option_ini(config, "log_auto_indent"),
|
||||||
)
|
)
|
||||||
|
|
||||||
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
|
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import functools
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
@ -16,6 +17,7 @@ from _pytest.config import hookimpl
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
from _pytest.outcomes import exit
|
from _pytest.outcomes import exit
|
||||||
from _pytest.runner import collect_one_node
|
from _pytest.runner import collect_one_node
|
||||||
|
from _pytest.runner import SetupState
|
||||||
|
|
||||||
|
|
||||||
class ExitCode(enum.IntEnum):
|
class ExitCode(enum.IntEnum):
|
||||||
|
|
@ -107,6 +109,7 @@ def pytest_addoption(parser):
|
||||||
group.addoption(
|
group.addoption(
|
||||||
"--collectonly",
|
"--collectonly",
|
||||||
"--collect-only",
|
"--collect-only",
|
||||||
|
"--co",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="only collect tests, don't execute them.",
|
help="only collect tests, don't execute them.",
|
||||||
),
|
),
|
||||||
|
|
@ -248,7 +251,10 @@ def pytest_collection(session):
|
||||||
|
|
||||||
def pytest_runtestloop(session):
|
def pytest_runtestloop(session):
|
||||||
if session.testsfailed and not session.config.option.continue_on_collection_errors:
|
if session.testsfailed and not session.config.option.continue_on_collection_errors:
|
||||||
raise session.Interrupted("%d errors during collection" % session.testsfailed)
|
raise session.Interrupted(
|
||||||
|
"%d error%s during collection"
|
||||||
|
% (session.testsfailed, "s" if session.testsfailed != 1 else "")
|
||||||
|
)
|
||||||
|
|
||||||
if session.config.option.collectonly:
|
if session.config.option.collectonly:
|
||||||
return True
|
return True
|
||||||
|
|
@ -356,8 +362,8 @@ class Failed(Exception):
|
||||||
class _bestrelpath_cache(dict):
|
class _bestrelpath_cache(dict):
|
||||||
path = attr.ib()
|
path = attr.ib()
|
||||||
|
|
||||||
def __missing__(self, path):
|
def __missing__(self, path: str) -> str:
|
||||||
r = self.path.bestrelpath(path)
|
r = self.path.bestrelpath(path) # type: str
|
||||||
self[path] = r
|
self[path] = r
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
@ -365,6 +371,7 @@ class _bestrelpath_cache(dict):
|
||||||
class Session(nodes.FSCollector):
|
class Session(nodes.FSCollector):
|
||||||
Interrupted = Interrupted
|
Interrupted = Interrupted
|
||||||
Failed = Failed
|
Failed = Failed
|
||||||
|
_setupstate = None # type: SetupState
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
nodes.FSCollector.__init__(
|
nodes.FSCollector.__init__(
|
||||||
|
|
@ -380,7 +387,9 @@ class Session(nodes.FSCollector):
|
||||||
self._initialpaths = frozenset()
|
self._initialpaths = frozenset()
|
||||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
||||||
self._node_cache = {}
|
self._node_cache = {}
|
||||||
self._bestrelpathcache = _bestrelpath_cache(config.rootdir)
|
self._bestrelpathcache = _bestrelpath_cache(
|
||||||
|
config.rootdir
|
||||||
|
) # type: Dict[str, str]
|
||||||
# Dirnames of pkgs with dunder-init files.
|
# Dirnames of pkgs with dunder-init files.
|
||||||
self._pkg_roots = {}
|
self._pkg_roots = {}
|
||||||
|
|
||||||
|
|
@ -395,7 +404,7 @@ class Session(nodes.FSCollector):
|
||||||
self.testscollected,
|
self.testscollected,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _node_location_to_relpath(self, node_path):
|
def _node_location_to_relpath(self, node_path: str) -> str:
|
||||||
# bestrelpath is a quite slow function
|
# bestrelpath is a quite slow function
|
||||||
return self._bestrelpathcache[node_path]
|
return self._bestrelpathcache[node_path]
|
||||||
|
|
||||||
|
|
@ -468,7 +477,6 @@ class Session(nodes.FSCollector):
|
||||||
for arg, exc in self._notfound:
|
for arg, exc in self._notfound:
|
||||||
line = "(no name {!r} in any of {!r})".format(arg, exc.args[0])
|
line = "(no name {!r} in any of {!r})".format(arg, exc.args[0])
|
||||||
errors.append("not found: {}\n{}".format(arg, line))
|
errors.append("not found: {}\n{}".format(arg, line))
|
||||||
# XXX: test this
|
|
||||||
raise UsageError(*errors)
|
raise UsageError(*errors)
|
||||||
if not genitems:
|
if not genitems:
|
||||||
return rep.result
|
return rep.result
|
||||||
|
|
@ -480,22 +488,22 @@ class Session(nodes.FSCollector):
|
||||||
|
|
||||||
def collect(self):
|
def collect(self):
|
||||||
for initialpart in self._initialparts:
|
for initialpart in self._initialparts:
|
||||||
arg = "::".join(map(str, initialpart))
|
self.trace("processing argument", initialpart)
|
||||||
self.trace("processing argument", arg)
|
|
||||||
self.trace.root.indent += 1
|
self.trace.root.indent += 1
|
||||||
try:
|
try:
|
||||||
yield from self._collect(arg)
|
yield from self._collect(initialpart)
|
||||||
except NoMatch:
|
except NoMatch:
|
||||||
|
report_arg = "::".join(map(str, initialpart))
|
||||||
# we are inside a make_report hook so
|
# we are inside a make_report hook so
|
||||||
# we cannot directly pass through the exception
|
# we cannot directly pass through the exception
|
||||||
self._notfound.append((arg, sys.exc_info()[1]))
|
self._notfound.append((report_arg, sys.exc_info()[1]))
|
||||||
|
|
||||||
self.trace.root.indent -= 1
|
self.trace.root.indent -= 1
|
||||||
|
|
||||||
def _collect(self, arg):
|
def _collect(self, arg):
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
|
|
||||||
names = self._parsearg(arg)
|
names = arg[:]
|
||||||
argpath = names.pop(0)
|
argpath = names.pop(0)
|
||||||
|
|
||||||
# Start with a Session root, and delve to argpath item (dir or file)
|
# Start with a Session root, and delve to argpath item (dir or file)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from .structures import MARK_GEN
|
||||||
from .structures import MarkDecorator
|
from .structures import MarkDecorator
|
||||||
from .structures import MarkGenerator
|
from .structures import MarkGenerator
|
||||||
from .structures import ParameterSet
|
from .structures import ParameterSet
|
||||||
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
|
|
||||||
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
|
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
|
||||||
|
|
@ -74,6 +75,7 @@ def pytest_addoption(parser):
|
||||||
parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets")
|
parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets")
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl(tryfirst=True)
|
||||||
def pytest_cmdline_main(config):
|
def pytest_cmdline_main(config):
|
||||||
import _pytest.config
|
import _pytest.config
|
||||||
|
|
||||||
|
|
@ -91,10 +93,6 @@ def pytest_cmdline_main(config):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/2087.
|
|
||||||
pytest_cmdline_main.tryfirst = True # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def deselect_by_keyword(items, config):
|
def deselect_by_keyword(items, config):
|
||||||
keywordexpr = config.option.keyword.lstrip()
|
keywordexpr = config.option.keyword.lstrip()
|
||||||
if not keywordexpr:
|
if not keywordexpr:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import inspect
|
||||||
import warnings
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
from operator import attrgetter
|
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
@ -17,16 +16,6 @@ from _pytest.warning_types import PytestUnknownMarkWarning
|
||||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||||
|
|
||||||
|
|
||||||
def alias(name, warning=None):
|
|
||||||
getter = attrgetter(name)
|
|
||||||
|
|
||||||
def warned(self):
|
|
||||||
warnings.warn(warning, stacklevel=2)
|
|
||||||
return getter(self)
|
|
||||||
|
|
||||||
return property(getter if warning is None else warned, doc="alias for " + name)
|
|
||||||
|
|
||||||
|
|
||||||
def istestfunc(func):
|
def istestfunc(func):
|
||||||
return (
|
return (
|
||||||
hasattr(func, "__call__")
|
hasattr(func, "__call__")
|
||||||
|
|
@ -205,17 +194,25 @@ class MarkDecorator:
|
||||||
|
|
||||||
mark = attr.ib(validator=attr.validators.instance_of(Mark))
|
mark = attr.ib(validator=attr.validators.instance_of(Mark))
|
||||||
|
|
||||||
name = alias("mark.name")
|
@property
|
||||||
args = alias("mark.args")
|
def name(self):
|
||||||
kwargs = alias("mark.kwargs")
|
"""alias for mark.name"""
|
||||||
|
return self.mark.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def args(self):
|
||||||
|
"""alias for mark.args"""
|
||||||
|
return self.mark.args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kwargs(self):
|
||||||
|
"""alias for mark.kwargs"""
|
||||||
|
return self.mark.kwargs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def markname(self):
|
def markname(self):
|
||||||
return self.name # for backward-compat (2.4.1 had this attr)
|
return self.name # for backward-compat (2.4.1 had this attr)
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.mark == other.mark if isinstance(other, MarkDecorator) else False
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<MarkDecorator {!r}>".format(self.mark)
|
return "<MarkDecorator {!r}>".format(self.mark)
|
||||||
|
|
||||||
|
|
@ -317,13 +314,18 @@ class MarkGenerator:
|
||||||
"{!r} not found in `markers` configuration option".format(name),
|
"{!r} not found in `markers` configuration option".format(name),
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
warnings.warn(
|
# Raise a specific error for common misspellings of "parametrize".
|
||||||
"Unknown pytest.mark.%s - is this a typo? You can register "
|
if name in ["parameterize", "parametrise", "parameterise"]:
|
||||||
"custom marks to avoid this warning - for details, see "
|
__tracebackhide__ = True
|
||||||
"https://docs.pytest.org/en/latest/mark.html" % name,
|
fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name))
|
||||||
PytestUnknownMarkWarning,
|
|
||||||
)
|
warnings.warn(
|
||||||
|
"Unknown pytest.mark.%s - is this a typo? You can register "
|
||||||
|
"custom marks to avoid this warning - for details, see "
|
||||||
|
"https://docs.pytest.org/en/latest/mark.html" % name,
|
||||||
|
PytestUnknownMarkWarning,
|
||||||
|
)
|
||||||
|
|
||||||
return MarkDecorator(Mark(name, (), {}))
|
return MarkDecorator(Mark(name, (), {}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from functools import lru_cache
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
@ -11,15 +12,23 @@ from typing import Union
|
||||||
import py
|
import py
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
|
from _pytest._code.code import ExceptionChainRepr
|
||||||
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
from _pytest._code.code import ReprExceptionInfo
|
||||||
|
from _pytest.compat import cached_property
|
||||||
from _pytest.compat import getfslineno
|
from _pytest.compat import getfslineno
|
||||||
|
from _pytest.config import Config
|
||||||
|
from _pytest.fixtures import FixtureDef
|
||||||
|
from _pytest.fixtures import FixtureLookupError
|
||||||
|
from _pytest.fixtures import FixtureLookupErrorRepr
|
||||||
from _pytest.mark.structures import Mark
|
from _pytest.mark.structures import Mark
|
||||||
from _pytest.mark.structures import MarkDecorator
|
from _pytest.mark.structures import MarkDecorator
|
||||||
from _pytest.mark.structures import NodeKeywords
|
from _pytest.mark.structures import NodeKeywords
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import Failed
|
||||||
|
|
||||||
if False: # TYPE_CHECKING
|
if False: # TYPE_CHECKING
|
||||||
# Imported here due to circular import.
|
# Imported here due to circular import.
|
||||||
from _pytest.fixtures import FixtureDef
|
from _pytest.main import Session # noqa: F401
|
||||||
|
|
||||||
SEP = "/"
|
SEP = "/"
|
||||||
|
|
||||||
|
|
@ -69,8 +78,14 @@ class Node:
|
||||||
Collector subclasses have children, Items are terminal nodes."""
|
Collector subclasses have children, Items are terminal nodes."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, parent=None, config=None, session=None, fspath=None, nodeid=None
|
self,
|
||||||
):
|
name,
|
||||||
|
parent: Optional["Node"] = None,
|
||||||
|
config: Optional[Config] = None,
|
||||||
|
session: Optional["Session"] = None,
|
||||||
|
fspath: Optional[py.path.local] = None,
|
||||||
|
nodeid: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
#: a unique name within the scope of the parent node
|
#: a unique name within the scope of the parent node
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
|
@ -78,10 +93,20 @@ class Node:
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
#: the pytest config object
|
#: the pytest config object
|
||||||
self.config = config or parent.config
|
if config:
|
||||||
|
self.config = config
|
||||||
|
else:
|
||||||
|
if not parent:
|
||||||
|
raise TypeError("config or parent must be provided")
|
||||||
|
self.config = parent.config
|
||||||
|
|
||||||
#: the session this node is part of
|
#: the session this node is part of
|
||||||
self.session = session or parent.session
|
if session:
|
||||||
|
self.session = session
|
||||||
|
else:
|
||||||
|
if not parent:
|
||||||
|
raise TypeError("session or parent must be provided")
|
||||||
|
self.session = parent.session
|
||||||
|
|
||||||
#: filesystem path where this node was collected from (can be None)
|
#: filesystem path where this node was collected from (can be None)
|
||||||
self.fspath = fspath or getattr(parent, "fspath", None)
|
self.fspath = fspath or getattr(parent, "fspath", None)
|
||||||
|
|
@ -102,6 +127,8 @@ class Node:
|
||||||
assert "::()" not in nodeid
|
assert "::()" not in nodeid
|
||||||
self._nodeid = nodeid
|
self._nodeid = nodeid
|
||||||
else:
|
else:
|
||||||
|
if not self.parent:
|
||||||
|
raise TypeError("nodeid or parent must be provided")
|
||||||
self._nodeid = self.parent.nodeid
|
self._nodeid = self.parent.nodeid
|
||||||
if self.name != "()":
|
if self.name != "()":
|
||||||
self._nodeid += "::" + self.name
|
self._nodeid += "::" + self.name
|
||||||
|
|
@ -139,8 +166,7 @@ class Node:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
path, lineno = get_fslocation_from_item(self)
|
path, lineno = get_fslocation_from_item(self)
|
||||||
# Type ignored: https://github.com/python/typeshed/pull/3121
|
warnings.warn_explicit(
|
||||||
warnings.warn_explicit( # type: ignore
|
|
||||||
warning,
|
warning,
|
||||||
category=None,
|
category=None,
|
||||||
filename=str(path),
|
filename=str(path),
|
||||||
|
|
@ -166,7 +192,7 @@ class Node:
|
||||||
""" return list of all parent collectors up to self,
|
""" return list of all parent collectors up to self,
|
||||||
starting from root of collection tree. """
|
starting from root of collection tree. """
|
||||||
chain = []
|
chain = []
|
||||||
item = self
|
item = self # type: Optional[Node]
|
||||||
while item is not None:
|
while item is not None:
|
||||||
chain.append(item)
|
chain.append(item)
|
||||||
item = item.parent
|
item = item.parent
|
||||||
|
|
@ -247,7 +273,7 @@ class Node:
|
||||||
def getparent(self, cls):
|
def getparent(self, cls):
|
||||||
""" get the next parent node (including ourself)
|
""" get the next parent node (including ourself)
|
||||||
which is an instance of the given class"""
|
which is an instance of the given class"""
|
||||||
current = self
|
current = self # type: Optional[Node]
|
||||||
while current and not isinstance(current, cls):
|
while current and not isinstance(current, cls):
|
||||||
current = current.parent
|
current = current.parent
|
||||||
return current
|
return current
|
||||||
|
|
@ -255,13 +281,13 @@ class Node:
|
||||||
def _prunetraceback(self, excinfo):
|
def _prunetraceback(self, excinfo):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _repr_failure_py(self, excinfo, style=None):
|
def _repr_failure_py(
|
||||||
# Type ignored: see comment where fail.Exception is defined.
|
self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None
|
||||||
if excinfo.errisinstance(fail.Exception): # type: ignore
|
) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]:
|
||||||
|
if isinstance(excinfo.value, Failed):
|
||||||
if not excinfo.value.pytrace:
|
if not excinfo.value.pytrace:
|
||||||
return str(excinfo.value)
|
return str(excinfo.value)
|
||||||
fm = self.session._fixturemanager
|
if isinstance(excinfo.value, FixtureLookupError):
|
||||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
|
||||||
return excinfo.value.formatrepr()
|
return excinfo.value.formatrepr()
|
||||||
if self.config.getoption("fulltrace", False):
|
if self.config.getoption("fulltrace", False):
|
||||||
style = "long"
|
style = "long"
|
||||||
|
|
@ -299,7 +325,9 @@ class Node:
|
||||||
truncate_locals=truncate_locals,
|
truncate_locals=truncate_locals,
|
||||||
)
|
)
|
||||||
|
|
||||||
def repr_failure(self, excinfo, style=None):
|
def repr_failure(
|
||||||
|
self, excinfo, style=None
|
||||||
|
) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]:
|
||||||
return self._repr_failure_py(excinfo, style)
|
return self._repr_failure_py(excinfo, style)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -365,8 +393,9 @@ def _check_initialpaths_for_relpath(session, fspath):
|
||||||
|
|
||||||
|
|
||||||
class FSCollector(Collector):
|
class FSCollector(Collector):
|
||||||
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
|
def __init__(
|
||||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None
|
||||||
|
) -> None:
|
||||||
name = fspath.basename
|
name = fspath.basename
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
rel = fspath.relto(parent.fspath)
|
rel = fspath.relto(parent.fspath)
|
||||||
|
|
@ -426,16 +455,12 @@ class Item(Node):
|
||||||
if content:
|
if content:
|
||||||
self._report_sections.append((when, key, content))
|
self._report_sections.append((when, key, content))
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self) -> Tuple[str, Optional[int], str]:
|
||||||
return self.fspath, None, ""
|
return self.fspath, None, ""
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def location(self):
|
def location(self) -> Tuple[str, Optional[int], str]:
|
||||||
try:
|
location = self.reportinfo()
|
||||||
return self._location
|
fspath = self.session._node_location_to_relpath(location[0])
|
||||||
except AttributeError:
|
assert type(location[2]) is str
|
||||||
location = self.reportinfo()
|
return (fspath, location[1], location[2])
|
||||||
fspath = self.session._node_location_to_relpath(location[0])
|
|
||||||
location = (fspath, location[1], str(location[2]))
|
|
||||||
self._location = location
|
|
||||||
return location
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import atexit
|
import atexit
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import itertools
|
import itertools
|
||||||
import operator
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -13,6 +12,11 @@ from os.path import expandvars
|
||||||
from os.path import isabs
|
from os.path import isabs
|
||||||
from os.path import sep
|
from os.path import sep
|
||||||
from posixpath import sep as posix_sep
|
from posixpath import sep as posix_sep
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import Iterator
|
||||||
|
from typing import Set
|
||||||
|
from typing import TypeVar
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
@ -26,10 +30,15 @@ __all__ = ["Path", "PurePath"]
|
||||||
|
|
||||||
LOCK_TIMEOUT = 60 * 60 * 3
|
LOCK_TIMEOUT = 60 * 60 * 3
|
||||||
|
|
||||||
get_lock_path = operator.methodcaller("joinpath", ".lock")
|
|
||||||
|
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
|
||||||
|
|
||||||
|
|
||||||
def ensure_reset_dir(path):
|
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||||
|
return path.joinpath(".lock")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_reset_dir(path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
ensures the given path is an empty directory
|
ensures the given path is an empty directory
|
||||||
"""
|
"""
|
||||||
|
|
@ -38,7 +47,7 @@ def ensure_reset_dir(path):
|
||||||
path.mkdir()
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool:
|
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||||
"""Handles known read-only errors during rmtree.
|
"""Handles known read-only errors during rmtree.
|
||||||
|
|
||||||
The returned value is used only by our own tests.
|
The returned value is used only by our own tests.
|
||||||
|
|
@ -59,19 +68,20 @@ def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if func not in (os.rmdir, os.remove, os.unlink):
|
if func not in (os.rmdir, os.remove, os.unlink):
|
||||||
warnings.warn(
|
if func not in (os.open,):
|
||||||
PytestWarning(
|
warnings.warn(
|
||||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
PytestWarning(
|
||||||
path, func, exctype, excvalue
|
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||||
|
func, path, exctype, excvalue
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Chmod + retry.
|
# Chmod + retry.
|
||||||
import stat
|
import stat
|
||||||
|
|
||||||
def chmod_rw(p: str):
|
def chmod_rw(p: str) -> None:
|
||||||
mode = os.stat(p).st_mode
|
mode = os.stat(p).st_mode
|
||||||
os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
|
os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
|
||||||
|
|
@ -90,7 +100,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def rm_rf(path: Path):
|
def rm_rf(path: Path) -> None:
|
||||||
"""Remove the path contents recursively, even if some elements
|
"""Remove the path contents recursively, even if some elements
|
||||||
are read-only.
|
are read-only.
|
||||||
"""
|
"""
|
||||||
|
|
@ -98,7 +108,7 @@ def rm_rf(path: Path):
|
||||||
shutil.rmtree(str(path), onerror=onerror)
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root, prefix):
|
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
"""finds all elements in root that begin with the prefix, case insensitive"""
|
"""finds all elements in root that begin with the prefix, case insensitive"""
|
||||||
l_prefix = prefix.lower()
|
l_prefix = prefix.lower()
|
||||||
for x in root.iterdir():
|
for x in root.iterdir():
|
||||||
|
|
@ -106,7 +116,7 @@ def find_prefixed(root, prefix):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
|
||||||
def extract_suffixes(iter, prefix):
|
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
|
||||||
"""
|
"""
|
||||||
:param iter: iterator over path names
|
:param iter: iterator over path names
|
||||||
:param prefix: expected prefix of the path names
|
:param prefix: expected prefix of the path names
|
||||||
|
|
@ -117,13 +127,13 @@ def extract_suffixes(iter, prefix):
|
||||||
yield p.name[p_len:]
|
yield p.name[p_len:]
|
||||||
|
|
||||||
|
|
||||||
def find_suffixes(root, prefix):
|
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
|
||||||
"""combines find_prefixes and extract_suffixes
|
"""combines find_prefixes and extract_suffixes
|
||||||
"""
|
"""
|
||||||
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
||||||
|
|
||||||
|
|
||||||
def parse_num(maybe_num):
|
def parse_num(maybe_num) -> int:
|
||||||
"""parses number path suffixes, returns -1 on error"""
|
"""parses number path suffixes, returns -1 on error"""
|
||||||
try:
|
try:
|
||||||
return int(maybe_num)
|
return int(maybe_num)
|
||||||
|
|
@ -131,7 +141,9 @@ def parse_num(maybe_num):
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def _force_symlink(root, target, link_to):
|
def _force_symlink(
|
||||||
|
root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
|
||||||
|
) -> None:
|
||||||
"""helper to create the current symlink
|
"""helper to create the current symlink
|
||||||
|
|
||||||
it's full of race conditions that are reasonably ok to ignore
|
it's full of race conditions that are reasonably ok to ignore
|
||||||
|
|
@ -151,7 +163,7 @@ def _force_symlink(root, target, link_to):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir(root, prefix):
|
def make_numbered_dir(root: Path, prefix: str) -> Path:
|
||||||
"""create a directory with an 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):
|
for i in range(10):
|
||||||
# try up to 10 times to create the folder
|
# try up to 10 times to create the folder
|
||||||
|
|
@ -172,7 +184,7 @@ def make_numbered_dir(root, prefix):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_cleanup_lock(p):
|
def create_cleanup_lock(p: Path) -> Path:
|
||||||
"""crates a lock to prevent premature folder cleanup"""
|
"""crates a lock to prevent premature folder cleanup"""
|
||||||
lock_path = get_lock_path(p)
|
lock_path = get_lock_path(p)
|
||||||
try:
|
try:
|
||||||
|
|
@ -189,11 +201,11 @@ def create_cleanup_lock(p):
|
||||||
return lock_path
|
return lock_path
|
||||||
|
|
||||||
|
|
||||||
def register_cleanup_lock_removal(lock_path, register=atexit.register):
|
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
||||||
"""registers a cleanup function for removing a lock, by default on atexit"""
|
"""registers a cleanup function for removing a lock, by default on atexit"""
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
|
|
||||||
def cleanup_on_exit(lock_path=lock_path, original_pid=pid):
|
def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
|
||||||
current_pid = os.getpid()
|
current_pid = os.getpid()
|
||||||
if current_pid != original_pid:
|
if current_pid != original_pid:
|
||||||
# fork
|
# fork
|
||||||
|
|
@ -206,7 +218,7 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register):
|
||||||
return register(cleanup_on_exit)
|
return register(cleanup_on_exit)
|
||||||
|
|
||||||
|
|
||||||
def maybe_delete_a_numbered_dir(path):
|
def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||||
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
|
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
|
||||||
lock_path = None
|
lock_path = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -232,7 +244,7 @@ def maybe_delete_a_numbered_dir(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def ensure_deletable(path, consider_lock_dead_if_created_before):
|
def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
|
||||||
"""checks if a lock exists and breaks it if its considered dead"""
|
"""checks if a lock exists and breaks it if its considered dead"""
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
return False
|
return False
|
||||||
|
|
@ -251,13 +263,13 @@ def ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def try_cleanup(path, consider_lock_dead_if_created_before):
|
def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
|
||||||
"""tries to cleanup a folder if we can ensure it's deletable"""
|
"""tries to cleanup a folder if we can ensure it's deletable"""
|
||||||
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||||
maybe_delete_a_numbered_dir(path)
|
maybe_delete_a_numbered_dir(path)
|
||||||
|
|
||||||
|
|
||||||
def cleanup_candidates(root, prefix, keep):
|
def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
"""lists candidates for numbered directories to be removed - follows py.path"""
|
"""lists candidates for numbered directories to be removed - follows py.path"""
|
||||||
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||||
max_delete = max_existing - keep
|
max_delete = max_existing - keep
|
||||||
|
|
@ -269,7 +281,9 @@ def cleanup_candidates(root, prefix, keep):
|
||||||
yield path
|
yield path
|
||||||
|
|
||||||
|
|
||||||
def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before):
|
def cleanup_numbered_dir(
|
||||||
|
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
||||||
|
) -> None:
|
||||||
"""cleanup for lock driven numbered directories"""
|
"""cleanup for lock driven numbered directories"""
|
||||||
for path in cleanup_candidates(root, prefix, keep):
|
for path in cleanup_candidates(root, prefix, keep):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
@ -277,7 +291,9 @@ def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_befor
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout):
|
def make_numbered_dir_with_cleanup(
|
||||||
|
root: Path, prefix: str, keep: int, lock_timeout: float
|
||||||
|
) -> Path:
|
||||||
"""creates a numbered dir with a cleanup lock and removes old ones"""
|
"""creates a numbered dir with a cleanup lock and removes old ones"""
|
||||||
e = None
|
e = None
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
|
|
@ -311,7 +327,7 @@ def resolve_from_str(input, root):
|
||||||
return root.joinpath(input)
|
return root.joinpath(input)
|
||||||
|
|
||||||
|
|
||||||
def fnmatch_ex(pattern, path):
|
def fnmatch_ex(pattern: str, path) -> bool:
|
||||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
||||||
|
|
||||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
||||||
|
|
@ -346,6 +362,6 @@ def fnmatch_ex(pattern, path):
|
||||||
return fnmatch.fnmatch(name, pattern)
|
return fnmatch.fnmatch(name, pattern)
|
||||||
|
|
||||||
|
|
||||||
def parts(s):
|
def parts(s: str) -> Set[str]:
|
||||||
parts = s.split(sep)
|
parts = s.split(sep)
|
||||||
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
|
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""(disabled by default) support for testing pytest and pytest plugins."""
|
"""(disabled by default) support for testing pytest and pytest plugins."""
|
||||||
|
import collections.abc
|
||||||
import gc
|
import gc
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
|
@ -8,9 +9,16 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from collections.abc import Sequence
|
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Iterable
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
@ -20,10 +28,16 @@ from _pytest._code import Source
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.capture import MultiCapture
|
from _pytest.capture import MultiCapture
|
||||||
from _pytest.capture import SysCapture
|
from _pytest.capture import SysCapture
|
||||||
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
|
from _pytest.reports import TestReport
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
|
||||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||||
"/var/lib/sss/mc/passwd"
|
"/var/lib/sss/mc/passwd"
|
||||||
|
|
@ -141,7 +155,7 @@ class LsofFdLeakChecker:
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def _pytest(request):
|
def _pytest(request: FixtureRequest) -> "PytestArg":
|
||||||
"""Return a helper which offers a gethookrecorder(hook) method which
|
"""Return a helper which offers a gethookrecorder(hook) method which
|
||||||
returns a HookRecorder instance which helps to make assertions about called
|
returns a HookRecorder instance which helps to make assertions about called
|
||||||
hooks.
|
hooks.
|
||||||
|
|
@ -151,10 +165,10 @@ def _pytest(request):
|
||||||
|
|
||||||
|
|
||||||
class PytestArg:
|
class PytestArg:
|
||||||
def __init__(self, request):
|
def __init__(self, request: FixtureRequest) -> None:
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
def gethookrecorder(self, hook):
|
def gethookrecorder(self, hook) -> "HookRecorder":
|
||||||
hookrecorder = HookRecorder(hook._pm)
|
hookrecorder = HookRecorder(hook._pm)
|
||||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||||
return hookrecorder
|
return hookrecorder
|
||||||
|
|
@ -175,6 +189,11 @@ class ParsedCall:
|
||||||
del d["_name"]
|
del d["_name"]
|
||||||
return "<ParsedCall {!r}(**{!r})>".format(self._name, d)
|
return "<ParsedCall {!r}(**{!r})>".format(self._name, d)
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
# The class has undetermined attributes, this tells mypy about it.
|
||||||
|
def __getattr__(self, key):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class HookRecorder:
|
class HookRecorder:
|
||||||
"""Record all hooks called in a plugin manager.
|
"""Record all hooks called in a plugin manager.
|
||||||
|
|
@ -184,27 +203,27 @@ class HookRecorder:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pluginmanager):
|
def __init__(self, pluginmanager) -> None:
|
||||||
self._pluginmanager = pluginmanager
|
self._pluginmanager = pluginmanager
|
||||||
self.calls = []
|
self.calls = [] # type: List[ParsedCall]
|
||||||
|
|
||||||
def before(hook_name, hook_impls, kwargs):
|
def before(hook_name: str, hook_impls, kwargs) -> None:
|
||||||
self.calls.append(ParsedCall(hook_name, kwargs))
|
self.calls.append(ParsedCall(hook_name, kwargs))
|
||||||
|
|
||||||
def after(outcome, hook_name, hook_impls, kwargs):
|
def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
|
self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
|
||||||
|
|
||||||
def finish_recording(self):
|
def finish_recording(self) -> None:
|
||||||
self._undo_wrapping()
|
self._undo_wrapping()
|
||||||
|
|
||||||
def getcalls(self, names):
|
def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]:
|
||||||
if isinstance(names, str):
|
if isinstance(names, str):
|
||||||
names = names.split()
|
names = names.split()
|
||||||
return [call for call in self.calls if call._name in names]
|
return [call for call in self.calls if call._name in names]
|
||||||
|
|
||||||
def assert_contains(self, entries):
|
def assert_contains(self, entries) -> None:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
i = 0
|
i = 0
|
||||||
entries = list(entries)
|
entries = list(entries)
|
||||||
|
|
@ -225,7 +244,7 @@ class HookRecorder:
|
||||||
else:
|
else:
|
||||||
pytest.fail("could not find {!r} check {!r}".format(name, check))
|
pytest.fail("could not find {!r} check {!r}".format(name, check))
|
||||||
|
|
||||||
def popcall(self, name):
|
def popcall(self, name: str) -> ParsedCall:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
for i, call in enumerate(self.calls):
|
for i, call in enumerate(self.calls):
|
||||||
if call._name == name:
|
if call._name == name:
|
||||||
|
|
@ -235,20 +254,27 @@ class HookRecorder:
|
||||||
lines.extend([" %s" % x for x in self.calls])
|
lines.extend([" %s" % x for x in self.calls])
|
||||||
pytest.fail("\n".join(lines))
|
pytest.fail("\n".join(lines))
|
||||||
|
|
||||||
def getcall(self, name):
|
def getcall(self, name: str) -> ParsedCall:
|
||||||
values = self.getcalls(name)
|
values = self.getcalls(name)
|
||||||
assert len(values) == 1, (name, values)
|
assert len(values) == 1, (name, values)
|
||||||
return values[0]
|
return values[0]
|
||||||
|
|
||||||
# functionality for test reports
|
# functionality for test reports
|
||||||
|
|
||||||
def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
|
def getreports(
|
||||||
|
self,
|
||||||
|
names: Union[
|
||||||
|
str, Iterable[str]
|
||||||
|
] = "pytest_runtest_logreport pytest_collectreport",
|
||||||
|
) -> List[TestReport]:
|
||||||
return [x.report for x in self.getcalls(names)]
|
return [x.report for x in self.getcalls(names)]
|
||||||
|
|
||||||
def matchreport(
|
def matchreport(
|
||||||
self,
|
self,
|
||||||
inamepart="",
|
inamepart: str = "",
|
||||||
names="pytest_runtest_logreport pytest_collectreport",
|
names: Union[
|
||||||
|
str, Iterable[str]
|
||||||
|
] = "pytest_runtest_logreport pytest_collectreport",
|
||||||
when=None,
|
when=None,
|
||||||
):
|
):
|
||||||
"""return a testreport whose dotted import path matches"""
|
"""return a testreport whose dotted import path matches"""
|
||||||
|
|
@ -274,13 +300,20 @@ class HookRecorder:
|
||||||
)
|
)
|
||||||
return values[0]
|
return values[0]
|
||||||
|
|
||||||
def getfailures(self, names="pytest_runtest_logreport pytest_collectreport"):
|
def getfailures(
|
||||||
|
self,
|
||||||
|
names: Union[
|
||||||
|
str, Iterable[str]
|
||||||
|
] = "pytest_runtest_logreport pytest_collectreport",
|
||||||
|
) -> List[TestReport]:
|
||||||
return [rep for rep in self.getreports(names) if rep.failed]
|
return [rep for rep in self.getreports(names) if rep.failed]
|
||||||
|
|
||||||
def getfailedcollections(self):
|
def getfailedcollections(self) -> List[TestReport]:
|
||||||
return self.getfailures("pytest_collectreport")
|
return self.getfailures("pytest_collectreport")
|
||||||
|
|
||||||
def listoutcomes(self):
|
def listoutcomes(
|
||||||
|
self,
|
||||||
|
) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]:
|
||||||
passed = []
|
passed = []
|
||||||
skipped = []
|
skipped = []
|
||||||
failed = []
|
failed = []
|
||||||
|
|
@ -295,31 +328,38 @@ class HookRecorder:
|
||||||
failed.append(rep)
|
failed.append(rep)
|
||||||
return passed, skipped, failed
|
return passed, skipped, failed
|
||||||
|
|
||||||
def countoutcomes(self):
|
def countoutcomes(self) -> List[int]:
|
||||||
return [len(x) for x in self.listoutcomes()]
|
return [len(x) for x in self.listoutcomes()]
|
||||||
|
|
||||||
def assertoutcome(self, passed=0, skipped=0, failed=0):
|
def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
|
||||||
realpassed, realskipped, realfailed = self.listoutcomes()
|
__tracebackhide__ = True
|
||||||
assert passed == len(realpassed)
|
|
||||||
assert skipped == len(realskipped)
|
|
||||||
assert failed == len(realfailed)
|
|
||||||
|
|
||||||
def clear(self):
|
outcomes = self.listoutcomes()
|
||||||
|
realpassed, realskipped, realfailed = outcomes
|
||||||
|
obtained = {
|
||||||
|
"passed": len(realpassed),
|
||||||
|
"skipped": len(realskipped),
|
||||||
|
"failed": len(realfailed),
|
||||||
|
}
|
||||||
|
expected = {"passed": passed, "skipped": skipped, "failed": failed}
|
||||||
|
assert obtained == expected, outcomes
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
self.calls[:] = []
|
self.calls[:] = []
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def linecomp(request):
|
def linecomp(request: FixtureRequest) -> "LineComp":
|
||||||
return LineComp()
|
return LineComp()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="LineMatcher")
|
@pytest.fixture(name="LineMatcher")
|
||||||
def LineMatcher_fixture(request):
|
def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
|
||||||
return LineMatcher
|
return LineMatcher
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def testdir(request, tmpdir_factory):
|
def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir":
|
||||||
return Testdir(request, tmpdir_factory)
|
return Testdir(request, tmpdir_factory)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -362,21 +402,30 @@ class RunResult:
|
||||||
:ivar duration: duration in seconds
|
:ivar duration: duration in seconds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ret, outlines, errlines, duration):
|
def __init__(
|
||||||
self.ret = ret
|
self,
|
||||||
|
ret: Union[int, ExitCode],
|
||||||
|
outlines: Sequence[str],
|
||||||
|
errlines: Sequence[str],
|
||||||
|
duration: float,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode]
|
||||||
|
except ValueError:
|
||||||
|
self.ret = ret
|
||||||
self.outlines = outlines
|
self.outlines = outlines
|
||||||
self.errlines = errlines
|
self.errlines = errlines
|
||||||
self.stdout = LineMatcher(outlines)
|
self.stdout = LineMatcher(outlines)
|
||||||
self.stderr = LineMatcher(errlines)
|
self.stderr = LineMatcher(errlines)
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
"<RunResult ret=%r len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
|
"<RunResult ret=%s len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>"
|
||||||
% (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
|
% (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parseoutcomes(self):
|
def parseoutcomes(self) -> Dict[str, int]:
|
||||||
"""Return a dictionary of outcomestring->num from parsing the terminal
|
"""Return a dictionary of outcomestring->num from parsing the terminal
|
||||||
output that the test process produced.
|
output that the test process produced.
|
||||||
|
|
||||||
|
|
@ -389,12 +438,19 @@ class RunResult:
|
||||||
raise ValueError("Pytest terminal summary report not found")
|
raise ValueError("Pytest terminal summary report not found")
|
||||||
|
|
||||||
def assert_outcomes(
|
def assert_outcomes(
|
||||||
self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0
|
self,
|
||||||
):
|
passed: int = 0,
|
||||||
|
skipped: int = 0,
|
||||||
|
failed: int = 0,
|
||||||
|
error: int = 0,
|
||||||
|
xpassed: int = 0,
|
||||||
|
xfailed: int = 0,
|
||||||
|
) -> None:
|
||||||
"""Assert that the specified outcomes appear with the respective
|
"""Assert that the specified outcomes appear with the respective
|
||||||
numbers (0 means it didn't occur) in the text output from a test run.
|
numbers (0 means it didn't occur) in the text output from a test run.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
|
||||||
d = self.parseoutcomes()
|
d = self.parseoutcomes()
|
||||||
obtained = {
|
obtained = {
|
||||||
"passed": d.get("passed", 0),
|
"passed": d.get("passed", 0),
|
||||||
|
|
@ -416,19 +472,19 @@ class RunResult:
|
||||||
|
|
||||||
|
|
||||||
class CwdSnapshot:
|
class CwdSnapshot:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.__saved = os.getcwd()
|
self.__saved = os.getcwd()
|
||||||
|
|
||||||
def restore(self):
|
def restore(self) -> None:
|
||||||
os.chdir(self.__saved)
|
os.chdir(self.__saved)
|
||||||
|
|
||||||
|
|
||||||
class SysModulesSnapshot:
|
class SysModulesSnapshot:
|
||||||
def __init__(self, preserve=None):
|
def __init__(self, preserve: Optional[Callable[[str], bool]] = None):
|
||||||
self.__preserve = preserve
|
self.__preserve = preserve
|
||||||
self.__saved = dict(sys.modules)
|
self.__saved = dict(sys.modules)
|
||||||
|
|
||||||
def restore(self):
|
def restore(self) -> None:
|
||||||
if self.__preserve:
|
if self.__preserve:
|
||||||
self.__saved.update(
|
self.__saved.update(
|
||||||
(k, m) for k, m in sys.modules.items() if self.__preserve(k)
|
(k, m) for k, m in sys.modules.items() if self.__preserve(k)
|
||||||
|
|
@ -438,10 +494,10 @@ class SysModulesSnapshot:
|
||||||
|
|
||||||
|
|
||||||
class SysPathsSnapshot:
|
class SysPathsSnapshot:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.__saved = list(sys.path), list(sys.meta_path)
|
self.__saved = list(sys.path), list(sys.meta_path)
|
||||||
|
|
||||||
def restore(self):
|
def restore(self) -> None:
|
||||||
sys.path[:], sys.meta_path[:] = self.__saved
|
sys.path[:], sys.meta_path[:] = self.__saved
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -480,11 +536,7 @@ class Testdir:
|
||||||
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
||||||
self.chdir()
|
self.chdir()
|
||||||
self.request.addfinalizer(self.finalize)
|
self.request.addfinalizer(self.finalize)
|
||||||
method = self.request.config.getoption("--runpytest")
|
self._method = self.request.config.getoption("--runpytest")
|
||||||
if method == "inprocess":
|
|
||||||
self._runpytest_method = self.runpytest_inprocess
|
|
||||||
elif method == "subprocess":
|
|
||||||
self._runpytest_method = self.runpytest_subprocess
|
|
||||||
|
|
||||||
mp = self.monkeypatch = MonkeyPatch()
|
mp = self.monkeypatch = MonkeyPatch()
|
||||||
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot))
|
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot))
|
||||||
|
|
@ -832,7 +884,7 @@ class Testdir:
|
||||||
reprec = rec.pop()
|
reprec = rec.pop()
|
||||||
else:
|
else:
|
||||||
|
|
||||||
class reprec:
|
class reprec: # type: ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
reprec.ret = ret
|
reprec.ret = ret
|
||||||
|
|
@ -848,7 +900,7 @@ class Testdir:
|
||||||
for finalizer in finalizers:
|
for finalizer in finalizers:
|
||||||
finalizer()
|
finalizer()
|
||||||
|
|
||||||
def runpytest_inprocess(self, *args, **kwargs):
|
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
||||||
"""Return result of running pytest in-process, providing a similar
|
"""Return result of running pytest in-process, providing a similar
|
||||||
interface to what self.runpytest() provides.
|
interface to what self.runpytest() provides.
|
||||||
"""
|
"""
|
||||||
|
|
@ -863,15 +915,20 @@ class Testdir:
|
||||||
try:
|
try:
|
||||||
reprec = self.inline_run(*args, **kwargs)
|
reprec = self.inline_run(*args, **kwargs)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
|
ret = e.args[0]
|
||||||
|
try:
|
||||||
|
ret = ExitCode(e.args[0])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
class reprec:
|
class reprec: # type: ignore
|
||||||
ret = e.args[0]
|
ret = ret
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
class reprec:
|
class reprec: # type: ignore
|
||||||
ret = 3
|
ret = ExitCode(3)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
out, err = capture.readouterr()
|
out, err = capture.readouterr()
|
||||||
|
|
@ -879,17 +936,23 @@ class Testdir:
|
||||||
sys.stdout.write(out)
|
sys.stdout.write(out)
|
||||||
sys.stderr.write(err)
|
sys.stderr.write(err)
|
||||||
|
|
||||||
res = RunResult(reprec.ret, out.split("\n"), err.split("\n"), time.time() - now)
|
res = RunResult(
|
||||||
res.reprec = reprec
|
reprec.ret, out.splitlines(), err.splitlines(), time.time() - now
|
||||||
|
)
|
||||||
|
res.reprec = reprec # type: ignore
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def runpytest(self, *args, **kwargs):
|
def runpytest(self, *args, **kwargs) -> RunResult:
|
||||||
"""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`.
|
option "--runpytest" and return a :py:class:`RunResult`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args = self._ensure_basetemp(args)
|
args = self._ensure_basetemp(args)
|
||||||
return self._runpytest_method(*args, **kwargs)
|
if self._method == "inprocess":
|
||||||
|
return self.runpytest_inprocess(*args, **kwargs)
|
||||||
|
elif self._method == "subprocess":
|
||||||
|
return self.runpytest_subprocess(*args, **kwargs)
|
||||||
|
raise RuntimeError("Unrecognized runpytest option: {}".format(self._method))
|
||||||
|
|
||||||
def _ensure_basetemp(self, args):
|
def _ensure_basetemp(self, args):
|
||||||
args = list(args)
|
args = list(args)
|
||||||
|
|
@ -928,11 +991,9 @@ class Testdir:
|
||||||
|
|
||||||
This returns a new :py:class:`_pytest.config.Config` instance like
|
This returns a new :py:class:`_pytest.config.Config` instance like
|
||||||
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
config = self.parseconfig(*args)
|
config = self.parseconfig(*args)
|
||||||
config._do_configure()
|
config._do_configure()
|
||||||
self.request.addfinalizer(config._ensure_unconfigure)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def getitem(self, source, funcname="test_func"):
|
def getitem(self, source, funcname="test_func"):
|
||||||
|
|
@ -1048,7 +1109,7 @@ class Testdir:
|
||||||
|
|
||||||
return popen
|
return popen
|
||||||
|
|
||||||
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN):
|
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
|
||||||
"""Run a command with arguments.
|
"""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.
|
||||||
|
|
@ -1066,9 +1127,9 @@ class Testdir:
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
cmdargs = [
|
cmdargs = tuple(
|
||||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||||
]
|
)
|
||||||
p1 = self.tmpdir.join("stdout")
|
p1 = self.tmpdir.join("stdout")
|
||||||
p2 = self.tmpdir.join("stderr")
|
p2 = self.tmpdir.join("stderr")
|
||||||
print("running:", *cmdargs)
|
print("running:", *cmdargs)
|
||||||
|
|
@ -1119,6 +1180,10 @@ class Testdir:
|
||||||
f2.close()
|
f2.close()
|
||||||
self._dump_lines(out, sys.stdout)
|
self._dump_lines(out, sys.stdout)
|
||||||
self._dump_lines(err, sys.stderr)
|
self._dump_lines(err, sys.stderr)
|
||||||
|
try:
|
||||||
|
ret = ExitCode(ret)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
return RunResult(ret, out, err, time.time() - now)
|
return RunResult(ret, out, err, time.time() - now)
|
||||||
|
|
||||||
def _dump_lines(self, lines, fp):
|
def _dump_lines(self, lines, fp):
|
||||||
|
|
@ -1131,7 +1196,7 @@ class Testdir:
|
||||||
def _getpytestargs(self):
|
def _getpytestargs(self):
|
||||||
return sys.executable, "-mpytest"
|
return sys.executable, "-mpytest"
|
||||||
|
|
||||||
def runpython(self, script):
|
def runpython(self, script) -> RunResult:
|
||||||
"""Run a python script using sys.executable as interpreter.
|
"""Run a python script using sys.executable as interpreter.
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
Returns a :py:class:`RunResult`.
|
||||||
|
|
@ -1143,7 +1208,7 @@ class Testdir:
|
||||||
"""Run python -c "command", return a :py:class:`RunResult`."""
|
"""Run python -c "command", return a :py:class:`RunResult`."""
|
||||||
return self.run(sys.executable, "-c", command)
|
return self.run(sys.executable, "-c", command)
|
||||||
|
|
||||||
def runpytest_subprocess(self, *args, timeout=None):
|
def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
|
||||||
"""Run pytest as a subprocess with given arguments.
|
"""Run pytest as a subprocess with given arguments.
|
||||||
|
|
||||||
Any plugins added to the :py:attr:`plugins` list will be added using the
|
Any plugins added to the :py:attr:`plugins` list will be added using the
|
||||||
|
|
@ -1192,8 +1257,6 @@ class Testdir:
|
||||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||||
if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
|
if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
|
||||||
pytest.skip("pypy-64 bit not supported")
|
pytest.skip("pypy-64 bit not supported")
|
||||||
if sys.platform.startswith("freebsd"):
|
|
||||||
pytest.xfail("pexpect does not work reliably on freebsd")
|
|
||||||
if not hasattr(pexpect, "spawn"):
|
if not hasattr(pexpect, "spawn"):
|
||||||
pytest.skip("pexpect.spawn not available")
|
pytest.skip("pexpect.spawn not available")
|
||||||
logfile = self.tmpdir.join("spawn.out").open("wb")
|
logfile = self.tmpdir.join("spawn.out").open("wb")
|
||||||
|
|
@ -1319,8 +1382,7 @@ class LineMatcher:
|
||||||
|
|
||||||
The argument is a list of lines which have to match and can use glob
|
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
|
wildcards. If they do not match a pytest.fail() is called. The
|
||||||
matches and non-matches are also printed on stdout.
|
matches and non-matches are also shown as part of the error message.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines(lines2, fnmatch, "fnmatch")
|
self._match_lines(lines2, fnmatch, "fnmatch")
|
||||||
|
|
@ -1331,8 +1393,7 @@ class LineMatcher:
|
||||||
The argument is a list of lines which have to match using ``re.match``.
|
The argument is a list of lines which have to match using ``re.match``.
|
||||||
If they do not match a pytest.fail() is called.
|
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 shown as part of the error message.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
|
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
|
||||||
|
|
@ -1347,14 +1408,14 @@ class LineMatcher:
|
||||||
pattern
|
pattern
|
||||||
:param str match_nickname: the nickname for the match function that
|
:param str match_nickname: the nickname for the match function that
|
||||||
will be logged to stdout when a match occurs
|
will be logged to stdout when a match occurs
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert isinstance(lines2, Sequence)
|
assert isinstance(lines2, collections.abc.Sequence)
|
||||||
lines2 = self._getlines(lines2)
|
lines2 = self._getlines(lines2)
|
||||||
lines1 = self.lines[:]
|
lines1 = self.lines[:]
|
||||||
nextline = None
|
nextline = None
|
||||||
extralines = []
|
extralines = []
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
wnick = len(match_nickname) + 1
|
||||||
for line in lines2:
|
for line in lines2:
|
||||||
nomatchprinted = False
|
nomatchprinted = False
|
||||||
while lines1:
|
while lines1:
|
||||||
|
|
@ -1364,14 +1425,58 @@ class LineMatcher:
|
||||||
break
|
break
|
||||||
elif match_func(nextline, line):
|
elif match_func(nextline, line):
|
||||||
self._log("%s:" % match_nickname, repr(line))
|
self._log("%s:" % match_nickname, repr(line))
|
||||||
self._log(" with:", repr(nextline))
|
self._log(
|
||||||
|
"{:>{width}}".format("with:", width=wnick), repr(nextline)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if not nomatchprinted:
|
if not nomatchprinted:
|
||||||
self._log("nomatch:", repr(line))
|
self._log(
|
||||||
|
"{:>{width}}".format("nomatch:", width=wnick), repr(line)
|
||||||
|
)
|
||||||
nomatchprinted = True
|
nomatchprinted = True
|
||||||
self._log(" and:", repr(nextline))
|
self._log("{:>{width}}".format("and:", width=wnick), repr(nextline))
|
||||||
extralines.append(nextline)
|
extralines.append(nextline)
|
||||||
else:
|
else:
|
||||||
self._log("remains unmatched: {!r}".format(line))
|
self._log("remains unmatched: {!r}".format(line))
|
||||||
pytest.fail(self._log_text)
|
pytest.fail(self._log_text.lstrip())
|
||||||
|
|
||||||
|
def no_fnmatch_line(self, pat):
|
||||||
|
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
|
||||||
|
|
||||||
|
:param str pat: the pattern to match lines.
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
self._no_match_line(pat, fnmatch, "fnmatch")
|
||||||
|
|
||||||
|
def no_re_match_line(self, pat):
|
||||||
|
"""Ensure captured lines do not match the given pattern, using ``re.match``.
|
||||||
|
|
||||||
|
:param str pat: the regular expression to match lines.
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match")
|
||||||
|
|
||||||
|
def _no_match_line(self, pat, match_func, match_nickname):
|
||||||
|
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
|
||||||
|
|
||||||
|
:param str pat: the pattern to match lines
|
||||||
|
"""
|
||||||
|
__tracebackhide__ = True
|
||||||
|
nomatch_printed = False
|
||||||
|
wnick = len(match_nickname) + 1
|
||||||
|
try:
|
||||||
|
for line in self.lines:
|
||||||
|
if match_func(line, pat):
|
||||||
|
self._log("%s:" % match_nickname, repr(pat))
|
||||||
|
self._log("{:>{width}}".format("with:", width=wnick), repr(line))
|
||||||
|
pytest.fail(self._log_text.lstrip())
|
||||||
|
else:
|
||||||
|
if not nomatch_printed:
|
||||||
|
self._log(
|
||||||
|
"{:>{width}}".format("nomatch:", width=wnick), repr(pat)
|
||||||
|
)
|
||||||
|
nomatch_printed = True
|
||||||
|
self._log("{:>{width}}".format("and:", width=wnick), repr(line))
|
||||||
|
finally:
|
||||||
|
self._log_output = []
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from collections import Counter
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from typing import List
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
|
@ -30,6 +32,7 @@ from _pytest.compat import safe_getattr
|
||||||
from _pytest.compat import safe_isclass
|
from _pytest.compat import safe_isclass
|
||||||
from _pytest.compat import STRING_TYPES
|
from _pytest.compat import STRING_TYPES
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
from _pytest.deprecated import FUNCARGNAMES
|
||||||
from _pytest.main import FSHookProxy
|
from _pytest.main import FSHookProxy
|
||||||
from _pytest.mark import MARK_GEN
|
from _pytest.mark import MARK_GEN
|
||||||
from _pytest.mark.structures import get_unpacked_marks
|
from _pytest.mark.structures import get_unpacked_marks
|
||||||
|
|
@ -118,13 +121,6 @@ def pytest_cmdline_main(config):
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
# those alternative spellings are common - raise a specific error to alert
|
|
||||||
# the user
|
|
||||||
alt_spellings = ["parameterize", "parametrise", "parameterise"]
|
|
||||||
for mark_name in alt_spellings:
|
|
||||||
if metafunc.definition.get_closest_marker(mark_name):
|
|
||||||
msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
|
|
||||||
fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False)
|
|
||||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
metafunc.parametrize(*marker.args, **marker.kwargs)
|
||||||
|
|
||||||
|
|
@ -235,10 +231,6 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
outcome.force_result(res)
|
outcome.force_result(res)
|
||||||
|
|
||||||
|
|
||||||
def pytest_make_parametrize_id(config, val, argname=None):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class PyobjContext:
|
class PyobjContext:
|
||||||
module = pyobj_property("Module")
|
module = pyobj_property("Module")
|
||||||
cls = pyobj_property("Class")
|
cls = pyobj_property("Class")
|
||||||
|
|
@ -287,7 +279,7 @@ class PyobjMixin(PyobjContext):
|
||||||
parts.reverse()
|
parts.reverse()
|
||||||
return ".".join(parts)
|
return ".".join(parts)
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self) -> Tuple[str, int, str]:
|
||||||
# XXX caching?
|
# XXX caching?
|
||||||
obj = self.obj
|
obj = self.obj
|
||||||
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
|
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
|
||||||
|
|
@ -880,7 +872,7 @@ class CallSpec2:
|
||||||
self.marks.extend(normalize_mark_list(marks))
|
self.marks.extend(normalize_mark_list(marks))
|
||||||
|
|
||||||
|
|
||||||
class Metafunc(fixtures.FuncargnamesCompatAttr):
|
class Metafunc:
|
||||||
"""
|
"""
|
||||||
Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
|
Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
|
||||||
They help to inspect a test function and to generate tests according to
|
They help to inspect a test function and to generate tests according to
|
||||||
|
|
@ -888,11 +880,14 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||||
test function is defined.
|
test function is defined.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
|
def __init__(
|
||||||
assert (
|
self,
|
||||||
isinstance(definition, FunctionDefinition)
|
definition: "FunctionDefinition",
|
||||||
or type(definition).__name__ == "DefinitionMock"
|
fixtureinfo,
|
||||||
)
|
config,
|
||||||
|
cls=None,
|
||||||
|
module=None,
|
||||||
|
) -> None:
|
||||||
self.definition = definition
|
self.definition = definition
|
||||||
|
|
||||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
#: access to the :class:`_pytest.config.Config` object for the test session
|
||||||
|
|
@ -910,10 +905,15 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||||
#: class object where the test function is defined in or ``None``.
|
#: class object where the test function is defined in or ``None``.
|
||||||
self.cls = cls
|
self.cls = cls
|
||||||
|
|
||||||
self._calls = []
|
self._calls = [] # type: List[CallSpec2]
|
||||||
self._ids = set()
|
|
||||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def funcargnames(self):
|
||||||
|
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||||
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
|
return self.fixturenames
|
||||||
|
|
||||||
def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None):
|
def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None):
|
||||||
""" Add new invocations to the underlying test function using the list
|
""" Add new invocations to the underlying test function using the list
|
||||||
of argvalues for the given argnames. Parametrization is performed
|
of argvalues for the given argnames. Parametrization is performed
|
||||||
|
|
@ -1166,7 +1166,8 @@ def _idval(val, argname, idx, idfn, item, config):
|
||||||
return ascii_escaped(val.pattern)
|
return ascii_escaped(val.pattern)
|
||||||
elif isinstance(val, enum.Enum):
|
elif isinstance(val, enum.Enum):
|
||||||
return str(val)
|
return str(val)
|
||||||
elif (inspect.isclass(val) or inspect.isfunction(val)) and hasattr(val, "__name__"):
|
elif hasattr(val, "__name__") and isinstance(val.__name__, str):
|
||||||
|
# name of a class, function, module, etc.
|
||||||
return val.__name__
|
return val.__name__
|
||||||
return str(argname) + str(idx)
|
return str(argname) + str(idx)
|
||||||
|
|
||||||
|
|
@ -1336,7 +1337,7 @@ def write_docstring(tw, doc, indent=" "):
|
||||||
tw.write(indent + line + "\n")
|
tw.write(indent + line + "\n")
|
||||||
|
|
||||||
|
|
||||||
class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
class Function(FunctionMixin, nodes.Item):
|
||||||
""" a Function Item is responsible for setting up and executing a
|
""" a Function Item is responsible for setting up and executing a
|
||||||
Python test function.
|
Python test function.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1423,6 +1424,12 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||||
"(compatonly) for code expecting pytest-2.2 style request objects"
|
"(compatonly) for code expecting pytest-2.2 style request objects"
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def funcargnames(self):
|
||||||
|
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||||
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
|
return self.fixturenames
|
||||||
|
|
||||||
def runtest(self):
|
def runtest(self):
|
||||||
""" execute the underlying test function. """
|
""" execute the underlying test function. """
|
||||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||||
|
|
|
||||||
|
|
@ -223,26 +223,24 @@ class ApproxScalar(ApproxBase):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
Return a string communicating both the expected value and the tolerance
|
Return a string communicating both the expected value and the tolerance
|
||||||
for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode
|
for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'.
|
||||||
plus/minus symbol if this is python3 (it's too hard to get right for
|
|
||||||
python2).
|
|
||||||
"""
|
"""
|
||||||
if isinstance(self.expected, complex):
|
|
||||||
return str(self.expected)
|
|
||||||
|
|
||||||
# Infinities aren't compared using tolerances, so don't show a
|
# Infinities aren't compared using tolerances, so don't show a
|
||||||
# tolerance.
|
# tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j)
|
||||||
if math.isinf(self.expected):
|
if math.isinf(abs(self.expected)):
|
||||||
return str(self.expected)
|
return str(self.expected)
|
||||||
|
|
||||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||||
# raise a ValueError. In this case, display '???'.
|
# raise a ValueError. In this case, display '???'.
|
||||||
try:
|
try:
|
||||||
vetted_tolerance = "{:.1e}".format(self.tolerance)
|
vetted_tolerance = "{:.1e}".format(self.tolerance)
|
||||||
|
if isinstance(self.expected, complex) and not math.isinf(self.tolerance):
|
||||||
|
vetted_tolerance += " ∠ ±180°"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
vetted_tolerance = "???"
|
vetted_tolerance = "???"
|
||||||
|
|
||||||
return "{} \u00b1 {}".format(self.expected, vetted_tolerance)
|
return "{} ± {}".format(self.expected, vetted_tolerance)
|
||||||
|
|
||||||
def __eq__(self, actual):
|
def __eq__(self, actual):
|
||||||
"""
|
"""
|
||||||
|
|
@ -554,7 +552,7 @@ def raises(
|
||||||
|
|
||||||
|
|
||||||
@overload # noqa: F811
|
@overload # noqa: F811
|
||||||
def raises(
|
def raises( # noqa: F811
|
||||||
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
|
||||||
func: Callable,
|
func: Callable,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
|
|
|
||||||
|
|
@ -60,18 +60,18 @@ def warns(
|
||||||
*,
|
*,
|
||||||
match: "Optional[Union[str, Pattern]]" = ...
|
match: "Optional[Union[str, Pattern]]" = ...
|
||||||
) -> "WarningsChecker":
|
) -> "WarningsChecker":
|
||||||
... # pragma: no cover
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
@overload # noqa: F811
|
@overload # noqa: F811
|
||||||
def warns(
|
def warns( # noqa: F811
|
||||||
expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]],
|
expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]],
|
||||||
func: Callable,
|
func: Callable,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
match: Optional[Union[str, "Pattern"]] = ...,
|
match: Optional[Union[str, "Pattern"]] = ...,
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
) -> Union[Any]:
|
) -> Union[Any]:
|
||||||
... # pragma: no cover
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def warns( # noqa: F811
|
def warns( # noqa: F811
|
||||||
|
|
@ -187,7 +187,7 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||||
exc_type: Optional["Type[BaseException]"],
|
exc_type: Optional["Type[BaseException]"],
|
||||||
exc_val: Optional[BaseException],
|
exc_val: Optional[BaseException],
|
||||||
exc_tb: Optional[TracebackType],
|
exc_tb: Optional[TracebackType],
|
||||||
) -> bool:
|
) -> None:
|
||||||
if not self._entered:
|
if not self._entered:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise RuntimeError("Cannot exit %r without entering first" % self)
|
raise RuntimeError("Cannot exit %r without entering first" % self)
|
||||||
|
|
@ -198,8 +198,6 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||||
# manually here for this context manager to become reusable.
|
# manually here for this context manager to become reusable.
|
||||||
self._entered = False
|
self._entered = False
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class WarningsChecker(WarningsRecorder):
|
class WarningsChecker(WarningsRecorder):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -232,7 +230,7 @@ class WarningsChecker(WarningsRecorder):
|
||||||
exc_type: Optional["Type[BaseException]"],
|
exc_type: Optional["Type[BaseException]"],
|
||||||
exc_val: Optional[BaseException],
|
exc_val: Optional[BaseException],
|
||||||
exc_tb: Optional[TracebackType],
|
exc_tb: Optional[TracebackType],
|
||||||
) -> bool:
|
) -> None:
|
||||||
super().__exit__(exc_type, exc_val, exc_tb)
|
super().__exit__(exc_type, exc_val, exc_tb)
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
@ -263,4 +261,3 @@ class WarningsChecker(WarningsRecorder):
|
||||||
[each.message for each in self],
|
[each.message for each in self],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
@ -15,6 +17,7 @@ from _pytest._code.code import ReprFuncArgs
|
||||||
from _pytest._code.code import ReprLocals
|
from _pytest._code.code import ReprLocals
|
||||||
from _pytest._code.code import ReprTraceback
|
from _pytest._code.code import ReprTraceback
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
from _pytest.nodes import Node
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.pathlib import Path
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
@ -33,14 +36,17 @@ def getslaveinfoline(node):
|
||||||
|
|
||||||
class BaseReport:
|
class BaseReport:
|
||||||
when = None # type: Optional[str]
|
when = None # type: Optional[str]
|
||||||
location = None
|
location = None # type: Optional[Tuple[str, Optional[int], str]]
|
||||||
|
longrepr = None
|
||||||
|
sections = [] # type: List[Tuple[str, str]]
|
||||||
|
nodeid = None # type: str
|
||||||
|
|
||||||
def __init__(self, **kw):
|
def __init__(self, **kw):
|
||||||
self.__dict__.update(kw)
|
self.__dict__.update(kw)
|
||||||
|
|
||||||
def toterminal(self, out):
|
def toterminal(self, out) -> None:
|
||||||
if hasattr(self, "node"):
|
if hasattr(self, "node"):
|
||||||
out.line(getslaveinfoline(self.node))
|
out.line(getslaveinfoline(self.node)) # type: ignore
|
||||||
|
|
||||||
longrepr = self.longrepr
|
longrepr = self.longrepr
|
||||||
if longrepr is None:
|
if longrepr is None:
|
||||||
|
|
@ -201,7 +207,7 @@ class TestReport(BaseReport):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
nodeid,
|
nodeid,
|
||||||
location,
|
location: Tuple[str, Optional[int], str],
|
||||||
keywords,
|
keywords,
|
||||||
outcome,
|
outcome,
|
||||||
longrepr,
|
longrepr,
|
||||||
|
|
@ -210,14 +216,14 @@ class TestReport(BaseReport):
|
||||||
duration=0,
|
duration=0,
|
||||||
user_properties=None,
|
user_properties=None,
|
||||||
**extra
|
**extra
|
||||||
):
|
) -> None:
|
||||||
#: normalized collection node id
|
#: normalized collection node id
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
|
|
||||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||||
#: actual location of a test item - it might be different from the
|
#: actual location of a test item - it might be different from the
|
||||||
#: collected one e.g. if a method is inherited from a different module.
|
#: collected one e.g. if a method is inherited from a different module.
|
||||||
self.location = location
|
self.location = location # type: Tuple[str, Optional[int], str]
|
||||||
|
|
||||||
#: a name -> value dictionary containing all keywords and
|
#: a name -> value dictionary containing all keywords and
|
||||||
#: markers associated with a test invocation.
|
#: markers associated with a test invocation.
|
||||||
|
|
@ -300,7 +306,9 @@ class TestReport(BaseReport):
|
||||||
class CollectReport(BaseReport):
|
class CollectReport(BaseReport):
|
||||||
when = "collect"
|
when = "collect"
|
||||||
|
|
||||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
def __init__(
|
||||||
|
self, nodeid: str, outcome, longrepr, result: List[Node], sections=(), **extra
|
||||||
|
) -> None:
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
self.outcome = outcome
|
self.outcome = outcome
|
||||||
self.longrepr = longrepr
|
self.longrepr = longrepr
|
||||||
|
|
@ -322,25 +330,25 @@ class CollectErrorRepr(TerminalRepr):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.longrepr = msg
|
self.longrepr = msg
|
||||||
|
|
||||||
def toterminal(self, out):
|
def toterminal(self, out) -> None:
|
||||||
out.line(self.longrepr, red=True)
|
out.line(self.longrepr, red=True)
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_to_serializable(report):
|
def pytest_report_to_serializable(report):
|
||||||
if isinstance(report, (TestReport, CollectReport)):
|
if isinstance(report, (TestReport, CollectReport)):
|
||||||
data = report._to_json()
|
data = report._to_json()
|
||||||
data["_report_type"] = report.__class__.__name__
|
data["$report_type"] = report.__class__.__name__
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_from_serializable(data):
|
def pytest_report_from_serializable(data):
|
||||||
if "_report_type" in data:
|
if "$report_type" in data:
|
||||||
if data["_report_type"] == "TestReport":
|
if data["$report_type"] == "TestReport":
|
||||||
return TestReport._from_json(data)
|
return TestReport._from_json(data)
|
||||||
elif data["_report_type"] == "CollectReport":
|
elif data["$report_type"] == "CollectReport":
|
||||||
return CollectReport._from_json(data)
|
return CollectReport._from_json(data)
|
||||||
assert False, "Unknown report_type unserialize data: {}".format(
|
assert False, "Unknown report_type unserialize data: {}".format(
|
||||||
data["_report_type"]
|
data["$report_type"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -472,7 +480,9 @@ def _report_kwargs_from_json(reportdict):
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
exception_info = ExceptionChainRepr(chain)
|
exception_info = ExceptionChainRepr(
|
||||||
|
chain
|
||||||
|
) # type: Union[ExceptionChainRepr,ReprExceptionInfo]
|
||||||
else:
|
else:
|
||||||
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from time import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
@ -207,8 +208,7 @@ class CallInfo:
|
||||||
""" Result/Exception info a function invocation. """
|
""" Result/Exception info a function invocation. """
|
||||||
|
|
||||||
_result = attr.ib()
|
_result = attr.ib()
|
||||||
# Optional[ExceptionInfo]
|
excinfo = attr.ib(type=Optional[ExceptionInfo])
|
||||||
excinfo = attr.ib()
|
|
||||||
start = attr.ib()
|
start = attr.ib()
|
||||||
stop = attr.ib()
|
stop = attr.ib()
|
||||||
when = attr.ib()
|
when = attr.ib()
|
||||||
|
|
@ -220,7 +220,7 @@ class CallInfo:
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_call(cls, func, when, reraise=None):
|
def from_call(cls, func, when, reraise=None) -> "CallInfo":
|
||||||
#: context of invocation: one of "setup", "call",
|
#: context of invocation: one of "setup", "call",
|
||||||
#: "teardown", "memocollect"
|
#: "teardown", "memocollect"
|
||||||
start = time()
|
start = time()
|
||||||
|
|
@ -236,16 +236,9 @@ class CallInfo:
|
||||||
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
|
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.excinfo is not None:
|
if self.excinfo is None:
|
||||||
status = "exception"
|
return "<CallInfo when={!r} result: {!r}>".format(self.when, self._result)
|
||||||
value = self.excinfo.value
|
return "<CallInfo when={!r} excinfo={!r}>".format(self.when, self.excinfo)
|
||||||
else:
|
|
||||||
# TODO: investigate unification
|
|
||||||
value = repr(self._result)
|
|
||||||
status = "result"
|
|
||||||
return "<CallInfo when={when!r} {status}: {value}>".format(
|
|
||||||
when=self.when, value=value, status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ def pytest_addoption(parser):
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_fixture_setup(fixturedef, request):
|
def pytest_fixture_setup(fixturedef, request):
|
||||||
yield
|
yield
|
||||||
config = request.config
|
if request.config.option.setupshow:
|
||||||
if config.option.setupshow:
|
|
||||||
if hasattr(request, "param"):
|
if hasattr(request, "param"):
|
||||||
# Save the fixture parameter so ._show_fixture_action() can
|
# Save the fixture parameter so ._show_fixture_action() can
|
||||||
# display it now and during the teardown (in .finish()).
|
# display it now and during the teardown (in .finish()).
|
||||||
|
|
|
||||||
|
|
@ -161,9 +161,9 @@ def pytest_runtest_makereport(item, call):
|
||||||
# skipped by mark.skipif; change the location of the failure
|
# skipped by mark.skipif; change the location of the failure
|
||||||
# to point to the item definition, otherwise it will display
|
# to point to the item definition, otherwise it will display
|
||||||
# the location of where the skip exception was raised within pytest
|
# the location of where the skip exception was raised within pytest
|
||||||
filename, line, reason = rep.longrepr
|
_, _, reason = rep.longrepr
|
||||||
filename, line = item.location[:2]
|
filename, line = item.location[:2]
|
||||||
rep.longrepr = filename, line, reason
|
rep.longrepr = filename, line + 1, reason
|
||||||
|
|
||||||
|
|
||||||
# called by terminalreporter progress reporting
|
# called by terminalreporter progress reporting
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@ import platform
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
@ -17,7 +25,11 @@ from more_itertools import collapse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
|
from _pytest.main import Session
|
||||||
|
from _pytest.reports import CollectReport
|
||||||
|
from _pytest.reports import TestReport
|
||||||
|
|
||||||
REPORT_COLLECTING_RESOLUTION = 0.5
|
REPORT_COLLECTING_RESOLUTION = 0.5
|
||||||
|
|
||||||
|
|
@ -66,7 +78,11 @@ def pytest_addoption(parser):
|
||||||
help="decrease verbosity.",
|
help="decrease verbosity.",
|
||||||
),
|
),
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"--verbosity", dest="verbose", type=int, default=0, help="set verbosity"
|
"--verbosity",
|
||||||
|
dest="verbose",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="set verbosity. Default is 0.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-r",
|
"-r",
|
||||||
|
|
@ -137,7 +153,7 @@ def pytest_addoption(parser):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config: Config) -> None:
|
||||||
reporter = TerminalReporter(config, sys.stdout)
|
reporter = TerminalReporter(config, sys.stdout)
|
||||||
config.pluginmanager.register(reporter, "terminalreporter")
|
config.pluginmanager.register(reporter, "terminalreporter")
|
||||||
if config.option.debug or config.option.traceconfig:
|
if config.option.debug or config.option.traceconfig:
|
||||||
|
|
@ -149,7 +165,7 @@ def pytest_configure(config):
|
||||||
config.trace.root.setprocessor("pytest:config", mywriter)
|
config.trace.root.setprocessor("pytest:config", mywriter)
|
||||||
|
|
||||||
|
|
||||||
def getreportopt(config):
|
def getreportopt(config: Config) -> str:
|
||||||
reportopts = ""
|
reportopts = ""
|
||||||
reportchars = config.option.reportchars
|
reportchars = config.option.reportchars
|
||||||
if not config.option.disable_warnings and "w" not in reportchars:
|
if not config.option.disable_warnings and "w" not in reportchars:
|
||||||
|
|
@ -168,7 +184,7 @@ def getreportopt(config):
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True) # after _pytest.runner
|
@pytest.hookimpl(trylast=True) # after _pytest.runner
|
||||||
def pytest_report_teststatus(report):
|
def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
|
||||||
if report.passed:
|
if report.passed:
|
||||||
letter = "."
|
letter = "."
|
||||||
elif report.skipped:
|
elif report.skipped:
|
||||||
|
|
@ -177,7 +193,13 @@ def pytest_report_teststatus(report):
|
||||||
letter = "F"
|
letter = "F"
|
||||||
if report.when != "call":
|
if report.when != "call":
|
||||||
letter = "f"
|
letter = "f"
|
||||||
return report.outcome, letter, report.outcome.upper()
|
|
||||||
|
# Report failed CollectReports as "error" (in line with pytest_collectreport).
|
||||||
|
outcome = report.outcome
|
||||||
|
if report.when == "collect" and outcome == "failed":
|
||||||
|
outcome = "error"
|
||||||
|
|
||||||
|
return outcome, letter, outcome.upper()
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
|
|
@ -191,8 +213,8 @@ class WarningReport:
|
||||||
file system location of the source of the warning (see ``get_location``).
|
file system location of the source of the warning (see ``get_location``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
message = attr.ib()
|
message = attr.ib(type=str)
|
||||||
nodeid = attr.ib(default=None)
|
nodeid = attr.ib(type=Optional[str], default=None)
|
||||||
fslocation = attr.ib(default=None)
|
fslocation = attr.ib(default=None)
|
||||||
count_towards_summary = True
|
count_towards_summary = True
|
||||||
|
|
||||||
|
|
@ -216,15 +238,15 @@ class WarningReport:
|
||||||
|
|
||||||
|
|
||||||
class TerminalReporter:
|
class TerminalReporter:
|
||||||
def __init__(self, config, file=None):
|
def __init__(self, config: Config, file=None) -> None:
|
||||||
import _pytest.config
|
import _pytest.config
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self._numcollected = 0
|
self._numcollected = 0
|
||||||
self._session = None
|
self._session = None # type: Optional[Session]
|
||||||
self._showfspath = None
|
self._showfspath = None
|
||||||
|
|
||||||
self.stats = {}
|
self.stats = {} # type: Dict[str, List[Any]]
|
||||||
self.startdir = config.invocation_dir
|
self.startdir = config.invocation_dir
|
||||||
if file is None:
|
if file is None:
|
||||||
file = sys.stdout
|
file = sys.stdout
|
||||||
|
|
@ -232,13 +254,13 @@ class TerminalReporter:
|
||||||
# self.writer will be deprecated in pytest-3.4
|
# self.writer will be deprecated in pytest-3.4
|
||||||
self.writer = self._tw
|
self.writer = self._tw
|
||||||
self._screen_width = self._tw.fullwidth
|
self._screen_width = self._tw.fullwidth
|
||||||
self.currentfspath = None
|
self.currentfspath = None # type: Any
|
||||||
self.reportchars = getreportopt(config)
|
self.reportchars = getreportopt(config)
|
||||||
self.hasmarkup = self._tw.hasmarkup
|
self.hasmarkup = self._tw.hasmarkup
|
||||||
self.isatty = file.isatty()
|
self.isatty = file.isatty()
|
||||||
self._progress_nodeids_reported = set()
|
self._progress_nodeids_reported = set() # type: Set[str]
|
||||||
self._show_progress_info = self._determine_show_progress_info()
|
self._show_progress_info = self._determine_show_progress_info()
|
||||||
self._collect_report_last_write = None
|
self._collect_report_last_write = None # type: Optional[float]
|
||||||
|
|
||||||
def _determine_show_progress_info(self):
|
def _determine_show_progress_info(self):
|
||||||
"""Return True if we should display progress information based on the current config"""
|
"""Return True if we should display progress information based on the current config"""
|
||||||
|
|
@ -383,7 +405,7 @@ class TerminalReporter:
|
||||||
fsid = nodeid.split("::")[0]
|
fsid = nodeid.split("::")[0]
|
||||||
self.write_fspath_result(fsid, "")
|
self.write_fspath_result(fsid, "")
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report):
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
self._tests_ran = True
|
self._tests_ran = True
|
||||||
rep = report
|
rep = report
|
||||||
res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||||
|
|
@ -423,7 +445,7 @@ class TerminalReporter:
|
||||||
self._write_progress_information_filling_space()
|
self._write_progress_information_filling_space()
|
||||||
else:
|
else:
|
||||||
self.ensure_newline()
|
self.ensure_newline()
|
||||||
self._tw.write("[%s]" % rep.node.gateway.id)
|
self._tw.write("[%s]" % rep.node.gateway.id) # type: ignore
|
||||||
if self._show_progress_info:
|
if self._show_progress_info:
|
||||||
self._tw.write(
|
self._tw.write(
|
||||||
self._get_progress_information_message() + " ", cyan=True
|
self._get_progress_information_message() + " ", cyan=True
|
||||||
|
|
@ -435,6 +457,7 @@ class TerminalReporter:
|
||||||
self.currentfspath = -2
|
self.currentfspath = -2
|
||||||
|
|
||||||
def pytest_runtest_logfinish(self, nodeid):
|
def pytest_runtest_logfinish(self, nodeid):
|
||||||
|
assert self._session
|
||||||
if self.verbosity <= 0 and self._show_progress_info:
|
if self.verbosity <= 0 and self._show_progress_info:
|
||||||
if self._show_progress_info == "count":
|
if self._show_progress_info == "count":
|
||||||
num_tests = self._session.testscollected
|
num_tests = self._session.testscollected
|
||||||
|
|
@ -442,20 +465,23 @@ class TerminalReporter:
|
||||||
else:
|
else:
|
||||||
progress_length = len(" [100%]")
|
progress_length = len(" [100%]")
|
||||||
|
|
||||||
|
main_color, _ = _get_main_color(self.stats)
|
||||||
|
|
||||||
self._progress_nodeids_reported.add(nodeid)
|
self._progress_nodeids_reported.add(nodeid)
|
||||||
is_last_item = (
|
is_last_item = (
|
||||||
len(self._progress_nodeids_reported) == self._session.testscollected
|
len(self._progress_nodeids_reported) == self._session.testscollected
|
||||||
)
|
)
|
||||||
if is_last_item:
|
if is_last_item:
|
||||||
self._write_progress_information_filling_space()
|
self._write_progress_information_filling_space(color=main_color)
|
||||||
else:
|
else:
|
||||||
w = self._width_of_current_line
|
w = self._width_of_current_line
|
||||||
past_edge = w + progress_length + 1 >= self._screen_width
|
past_edge = w + progress_length + 1 >= self._screen_width
|
||||||
if past_edge:
|
if past_edge:
|
||||||
msg = self._get_progress_information_message()
|
msg = self._get_progress_information_message()
|
||||||
self._tw.write(msg + "\n", cyan=True)
|
self._tw.write(msg + "\n", **{main_color: True})
|
||||||
|
|
||||||
def _get_progress_information_message(self):
|
def _get_progress_information_message(self) -> str:
|
||||||
|
assert self._session
|
||||||
collected = self._session.testscollected
|
collected = self._session.testscollected
|
||||||
if self._show_progress_info == "count":
|
if self._show_progress_info == "count":
|
||||||
if collected:
|
if collected:
|
||||||
|
|
@ -466,15 +492,18 @@ class TerminalReporter:
|
||||||
return " [ {} / {} ]".format(collected, collected)
|
return " [ {} / {} ]".format(collected, collected)
|
||||||
else:
|
else:
|
||||||
if collected:
|
if collected:
|
||||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
return " [{:3d}%]".format(
|
||||||
return " [{:3d}%]".format(progress)
|
len(self._progress_nodeids_reported) * 100 // collected
|
||||||
|
)
|
||||||
return " [100%]"
|
return " [100%]"
|
||||||
|
|
||||||
def _write_progress_information_filling_space(self):
|
def _write_progress_information_filling_space(self, color=None):
|
||||||
|
if not color:
|
||||||
|
color, _ = _get_main_color(self.stats)
|
||||||
msg = self._get_progress_information_message()
|
msg = self._get_progress_information_message()
|
||||||
w = self._width_of_current_line
|
w = self._width_of_current_line
|
||||||
fill = self._tw.fullwidth - w - 1
|
fill = self._tw.fullwidth - w - 1
|
||||||
self.write(msg.rjust(fill), cyan=True)
|
self.write(msg.rjust(fill), **{color: True})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _width_of_current_line(self):
|
def _width_of_current_line(self):
|
||||||
|
|
@ -493,7 +522,7 @@ class TerminalReporter:
|
||||||
elif self.config.option.verbose >= 1:
|
elif self.config.option.verbose >= 1:
|
||||||
self.write("collecting ... ", bold=True)
|
self.write("collecting ... ", bold=True)
|
||||||
|
|
||||||
def pytest_collectreport(self, report):
|
def pytest_collectreport(self, report: CollectReport) -> None:
|
||||||
if report.failed:
|
if report.failed:
|
||||||
self.stats.setdefault("error", []).append(report)
|
self.stats.setdefault("error", []).append(report)
|
||||||
elif report.skipped:
|
elif report.skipped:
|
||||||
|
|
@ -529,7 +558,7 @@ class TerminalReporter:
|
||||||
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
|
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
line += " / %d errors" % errors
|
line += " / %d error%s" % (errors, "s" if errors != 1 else "")
|
||||||
if deselected:
|
if deselected:
|
||||||
line += " / %d deselected" % deselected
|
line += " / %d deselected" % deselected
|
||||||
if skipped:
|
if skipped:
|
||||||
|
|
@ -544,7 +573,7 @@ class TerminalReporter:
|
||||||
self.write_line(line)
|
self.write_line(line)
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True)
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_sessionstart(self, session):
|
def pytest_sessionstart(self, session: Session) -> None:
|
||||||
self._session = session
|
self._session = session
|
||||||
self._sessionstarttime = time.time()
|
self._sessionstarttime = time.time()
|
||||||
if not self.showheader:
|
if not self.showheader:
|
||||||
|
|
@ -552,9 +581,10 @@ class TerminalReporter:
|
||||||
self.write_sep("=", "test session starts", bold=True)
|
self.write_sep("=", "test session starts", bold=True)
|
||||||
verinfo = platform.python_version()
|
verinfo = platform.python_version()
|
||||||
msg = "platform {} -- Python {}".format(sys.platform, verinfo)
|
msg = "platform {} -- Python {}".format(sys.platform, verinfo)
|
||||||
if hasattr(sys, "pypy_version_info"):
|
pypy_version_info = getattr(sys, "pypy_version_info", None)
|
||||||
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
|
if pypy_version_info:
|
||||||
msg += "[pypy-{}-{}]".format(verinfo, sys.pypy_version_info[3])
|
verinfo = ".".join(map(str, pypy_version_info[:3]))
|
||||||
|
msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3])
|
||||||
msg += ", pytest-{}, py-{}, pluggy-{}".format(
|
msg += ", pytest-{}, py-{}, pluggy-{}".format(
|
||||||
pytest.__version__, py.__version__, pluggy.__version__
|
pytest.__version__, py.__version__, pluggy.__version__
|
||||||
)
|
)
|
||||||
|
|
@ -604,9 +634,10 @@ class TerminalReporter:
|
||||||
self._write_report_lines_from_hooks(lines)
|
self._write_report_lines_from_hooks(lines)
|
||||||
|
|
||||||
if self.config.getoption("collectonly"):
|
if self.config.getoption("collectonly"):
|
||||||
if self.stats.get("failed"):
|
failed = self.stats.get("failed")
|
||||||
|
if failed:
|
||||||
self._tw.sep("!", "collection failures")
|
self._tw.sep("!", "collection failures")
|
||||||
for rep in self.stats.get("failed"):
|
for rep in failed:
|
||||||
rep.toterminal(self._tw)
|
rep.toterminal(self._tw)
|
||||||
|
|
||||||
def _printcollecteditems(self, items):
|
def _printcollecteditems(self, items):
|
||||||
|
|
@ -615,7 +646,7 @@ class TerminalReporter:
|
||||||
# because later versions are going to get rid of them anyway
|
# because later versions are going to get rid of them anyway
|
||||||
if self.config.option.verbose < 0:
|
if self.config.option.verbose < 0:
|
||||||
if self.config.option.verbose < -1:
|
if self.config.option.verbose < -1:
|
||||||
counts = {}
|
counts = {} # type: Dict[str, int]
|
||||||
for item in items:
|
for item in items:
|
||||||
name = item.nodeid.split("::", 1)[0]
|
name = item.nodeid.split("::", 1)[0]
|
||||||
counts[name] = counts.get(name, 0) + 1
|
counts[name] = counts.get(name, 0) + 1
|
||||||
|
|
@ -645,7 +676,7 @@ class TerminalReporter:
|
||||||
self._tw.line("{}{}".format(indent + " ", line.strip()))
|
self._tw.line("{}{}".format(indent + " ", line.strip()))
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_sessionfinish(self, exitstatus):
|
def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
outcome.get_result()
|
outcome.get_result()
|
||||||
self._tw.line("")
|
self._tw.line("")
|
||||||
|
|
@ -660,9 +691,13 @@ class TerminalReporter:
|
||||||
self.config.hook.pytest_terminal_summary(
|
self.config.hook.pytest_terminal_summary(
|
||||||
terminalreporter=self, exitstatus=exitstatus, config=self.config
|
terminalreporter=self, exitstatus=exitstatus, config=self.config
|
||||||
)
|
)
|
||||||
|
if session.shouldfail:
|
||||||
|
self.write_sep("!", session.shouldfail, red=True)
|
||||||
if exitstatus == ExitCode.INTERRUPTED:
|
if exitstatus == ExitCode.INTERRUPTED:
|
||||||
self._report_keyboardinterrupt()
|
self._report_keyboardinterrupt()
|
||||||
del self._keyboardinterrupt_memo
|
del self._keyboardinterrupt_memo
|
||||||
|
elif session.shouldstop:
|
||||||
|
self.write_sep("!", session.shouldstop, red=True)
|
||||||
self.summary_stats()
|
self.summary_stats()
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
|
@ -746,7 +781,9 @@ class TerminalReporter:
|
||||||
|
|
||||||
def summary_warnings(self):
|
def summary_warnings(self):
|
||||||
if self.hasopt("w"):
|
if self.hasopt("w"):
|
||||||
all_warnings = self.stats.get("warnings")
|
all_warnings = self.stats.get(
|
||||||
|
"warnings"
|
||||||
|
) # type: Optional[List[WarningReport]]
|
||||||
if not all_warnings:
|
if not all_warnings:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -759,7 +796,9 @@ class TerminalReporter:
|
||||||
if not warning_reports:
|
if not warning_reports:
|
||||||
return
|
return
|
||||||
|
|
||||||
reports_grouped_by_message = collections.OrderedDict()
|
reports_grouped_by_message = (
|
||||||
|
collections.OrderedDict()
|
||||||
|
) # type: collections.OrderedDict[str, List[WarningReport]]
|
||||||
for wr in warning_reports:
|
for wr in warning_reports:
|
||||||
reports_grouped_by_message.setdefault(wr.message, []).append(wr)
|
reports_grouped_by_message.setdefault(wr.message, []).append(wr)
|
||||||
|
|
||||||
|
|
@ -860,21 +899,47 @@ class TerminalReporter:
|
||||||
self._tw.line(content)
|
self._tw.line(content)
|
||||||
|
|
||||||
def summary_stats(self):
|
def summary_stats(self):
|
||||||
|
if self.verbosity < -1:
|
||||||
|
return
|
||||||
|
|
||||||
session_duration = time.time() - self._sessionstarttime
|
session_duration = time.time() - self._sessionstarttime
|
||||||
(line, color) = build_summary_stats_line(self.stats)
|
(parts, main_color) = build_summary_stats_line(self.stats)
|
||||||
msg = "{} in {}".format(line, format_session_duration(session_duration))
|
line_parts = []
|
||||||
markup = {color: True, "bold": True}
|
|
||||||
|
|
||||||
if self.verbosity >= 0:
|
display_sep = self.verbosity >= 0
|
||||||
self.write_sep("=", msg, **markup)
|
if display_sep:
|
||||||
if self.verbosity == -1:
|
fullwidth = self._tw.fullwidth
|
||||||
self.write_line(msg, **markup)
|
for text, markup in parts:
|
||||||
|
with_markup = self._tw.markup(text, **markup)
|
||||||
|
if display_sep:
|
||||||
|
fullwidth += len(with_markup) - len(text)
|
||||||
|
line_parts.append(with_markup)
|
||||||
|
msg = ", ".join(line_parts)
|
||||||
|
|
||||||
def short_test_summary(self):
|
main_markup = {main_color: True}
|
||||||
|
duration = " in {}".format(format_session_duration(session_duration))
|
||||||
|
duration_with_markup = self._tw.markup(duration, **main_markup)
|
||||||
|
if display_sep:
|
||||||
|
fullwidth += len(duration_with_markup) - len(duration)
|
||||||
|
msg += duration_with_markup
|
||||||
|
|
||||||
|
if display_sep:
|
||||||
|
markup_for_end_sep = self._tw.markup("", **main_markup)
|
||||||
|
if markup_for_end_sep.endswith("\x1b[0m"):
|
||||||
|
markup_for_end_sep = markup_for_end_sep[:-4]
|
||||||
|
fullwidth += len(markup_for_end_sep)
|
||||||
|
msg += markup_for_end_sep
|
||||||
|
|
||||||
|
if display_sep:
|
||||||
|
self.write_sep("=", msg, fullwidth=fullwidth, **main_markup)
|
||||||
|
else:
|
||||||
|
self.write_line(msg, **main_markup)
|
||||||
|
|
||||||
|
def short_test_summary(self) -> None:
|
||||||
if not self.reportchars:
|
if not self.reportchars:
|
||||||
return
|
return
|
||||||
|
|
||||||
def show_simple(stat, lines):
|
def show_simple(stat, lines: List[str]) -> None:
|
||||||
failed = self.stats.get(stat, [])
|
failed = self.stats.get(stat, [])
|
||||||
if not failed:
|
if not failed:
|
||||||
return
|
return
|
||||||
|
|
@ -884,7 +949,7 @@ class TerminalReporter:
|
||||||
line = _get_line_with_reprcrash_message(config, rep, termwidth)
|
line = _get_line_with_reprcrash_message(config, rep, termwidth)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
|
||||||
def show_xfailed(lines):
|
def show_xfailed(lines: List[str]) -> None:
|
||||||
xfailed = self.stats.get("xfailed", [])
|
xfailed = self.stats.get("xfailed", [])
|
||||||
for rep in xfailed:
|
for rep in xfailed:
|
||||||
verbose_word = rep._get_verbose_word(self.config)
|
verbose_word = rep._get_verbose_word(self.config)
|
||||||
|
|
@ -894,7 +959,7 @@ class TerminalReporter:
|
||||||
if reason:
|
if reason:
|
||||||
lines.append(" " + str(reason))
|
lines.append(" " + str(reason))
|
||||||
|
|
||||||
def show_xpassed(lines):
|
def show_xpassed(lines: List[str]) -> None:
|
||||||
xpassed = self.stats.get("xpassed", [])
|
xpassed = self.stats.get("xpassed", [])
|
||||||
for rep in xpassed:
|
for rep in xpassed:
|
||||||
verbose_word = rep._get_verbose_word(self.config)
|
verbose_word = rep._get_verbose_word(self.config)
|
||||||
|
|
@ -902,7 +967,7 @@ class TerminalReporter:
|
||||||
reason = rep.wasxfail
|
reason = rep.wasxfail
|
||||||
lines.append("{} {} {}".format(verbose_word, pos, reason))
|
lines.append("{} {} {}".format(verbose_word, pos, reason))
|
||||||
|
|
||||||
def show_skipped(lines):
|
def show_skipped(lines: List[str]) -> None:
|
||||||
skipped = self.stats.get("skipped", [])
|
skipped = self.stats.get("skipped", [])
|
||||||
fskips = _folded_skips(skipped) if skipped else []
|
fskips = _folded_skips(skipped) if skipped else []
|
||||||
if not fskips:
|
if not fskips:
|
||||||
|
|
@ -914,7 +979,7 @@ class TerminalReporter:
|
||||||
if lineno is not None:
|
if lineno is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
"%s [%d] %s:%d: %s"
|
"%s [%d] %s:%d: %s"
|
||||||
% (verbose_word, num, fspath, lineno + 1, reason)
|
% (verbose_word, num, fspath, lineno, reason)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
|
lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason))
|
||||||
|
|
@ -928,9 +993,9 @@ class TerminalReporter:
|
||||||
"S": show_skipped,
|
"S": show_skipped,
|
||||||
"p": partial(show_simple, "passed"),
|
"p": partial(show_simple, "passed"),
|
||||||
"E": partial(show_simple, "error"),
|
"E": partial(show_simple, "error"),
|
||||||
}
|
} # type: Mapping[str, Callable[[List[str]], None]]
|
||||||
|
|
||||||
lines = []
|
lines = [] # type: List[str]
|
||||||
for char in self.reportchars:
|
for char in self.reportchars:
|
||||||
action = REPORTCHAR_ACTIONS.get(char)
|
action = REPORTCHAR_ACTIONS.get(char)
|
||||||
if action: # skipping e.g. "P" (passed with output) here.
|
if action: # skipping e.g. "P" (passed with output) here.
|
||||||
|
|
@ -1007,7 +1072,29 @@ def _folded_skips(skipped):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def build_summary_stats_line(stats):
|
_color_for_type = {
|
||||||
|
"failed": "red",
|
||||||
|
"error": "red",
|
||||||
|
"warnings": "yellow",
|
||||||
|
"passed": "green",
|
||||||
|
}
|
||||||
|
_color_for_type_default = "yellow"
|
||||||
|
|
||||||
|
|
||||||
|
def _make_plural(count, noun):
|
||||||
|
# No need to pluralize words such as `failed` or `passed`.
|
||||||
|
if noun not in ["error", "warnings"]:
|
||||||
|
return count, noun
|
||||||
|
|
||||||
|
# The `warnings` key is plural. To avoid API breakage, we keep it that way but
|
||||||
|
# set it to singular here so we can determine plurality in the same way as we do
|
||||||
|
# for `error`.
|
||||||
|
noun = noun.replace("warnings", "warning")
|
||||||
|
|
||||||
|
return count, noun + "s" if count != 1 else noun
|
||||||
|
|
||||||
|
|
||||||
|
def _get_main_color(stats) -> Tuple[str, List[str]]:
|
||||||
known_types = (
|
known_types = (
|
||||||
"failed passed skipped deselected xfailed xpassed warnings error".split()
|
"failed passed skipped deselected xfailed xpassed warnings error".split()
|
||||||
)
|
)
|
||||||
|
|
@ -1017,6 +1104,23 @@ def build_summary_stats_line(stats):
|
||||||
if found_type: # setup/teardown reports have an empty key, ignore them
|
if found_type: # setup/teardown reports have an empty key, ignore them
|
||||||
known_types.append(found_type)
|
known_types.append(found_type)
|
||||||
unknown_type_seen = True
|
unknown_type_seen = True
|
||||||
|
|
||||||
|
# main color
|
||||||
|
if "failed" in stats or "error" in stats:
|
||||||
|
main_color = "red"
|
||||||
|
elif "warnings" in stats or unknown_type_seen:
|
||||||
|
main_color = "yellow"
|
||||||
|
elif "passed" in stats:
|
||||||
|
main_color = "green"
|
||||||
|
else:
|
||||||
|
main_color = "yellow"
|
||||||
|
|
||||||
|
return main_color, known_types
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary_stats_line(stats):
|
||||||
|
main_color, known_types = _get_main_color(stats)
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
for key in known_types:
|
for key in known_types:
|
||||||
reports = stats.get(key, None)
|
reports = stats.get(key, None)
|
||||||
|
|
@ -1024,27 +1128,18 @@ def build_summary_stats_line(stats):
|
||||||
count = sum(
|
count = sum(
|
||||||
1 for rep in reports if getattr(rep, "count_towards_summary", True)
|
1 for rep in reports if getattr(rep, "count_towards_summary", True)
|
||||||
)
|
)
|
||||||
parts.append("%d %s" % (count, key))
|
color = _color_for_type.get(key, _color_for_type_default)
|
||||||
|
markup = {color: True, "bold": color == main_color}
|
||||||
|
parts.append(("%d %s" % _make_plural(count, key), markup))
|
||||||
|
|
||||||
if parts:
|
if not parts:
|
||||||
line = ", ".join(parts)
|
parts = [("no tests ran", {_color_for_type_default: True})]
|
||||||
else:
|
|
||||||
line = "no tests ran"
|
|
||||||
|
|
||||||
if "failed" in stats or "error" in stats:
|
return parts, main_color
|
||||||
color = "red"
|
|
||||||
elif "warnings" in stats or unknown_type_seen:
|
|
||||||
color = "yellow"
|
|
||||||
elif "passed" in stats:
|
|
||||||
color = "green"
|
|
||||||
else:
|
|
||||||
color = "yellow"
|
|
||||||
|
|
||||||
return line, color
|
|
||||||
|
|
||||||
|
|
||||||
def _plugin_nameversions(plugininfo):
|
def _plugin_nameversions(plugininfo) -> List[str]:
|
||||||
values = []
|
values = [] # type: List[str]
|
||||||
for plugin, dist in plugininfo:
|
for plugin, dist in plugininfo:
|
||||||
# gets us name and version!
|
# gets us name and version!
|
||||||
name = "{dist.project_name}-{dist.version}".format(dist=dist)
|
name = "{dist.project_name}-{dist.version}".format(dist=dist)
|
||||||
|
|
@ -1058,7 +1153,7 @@ def _plugin_nameversions(plugininfo):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def format_session_duration(seconds):
|
def format_session_duration(seconds: float) -> str:
|
||||||
"""Format the given seconds in a human readable manner to show in the final summary"""
|
"""Format the given seconds in a human readable manner to show in the final summary"""
|
||||||
if seconds < 60:
|
if seconds < 60:
|
||||||
return "{:.2f}s".format(seconds)
|
return "{:.2f}s".format(seconds)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
import py
|
import py
|
||||||
|
|
@ -12,6 +13,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import Path
|
from .pathlib import Path
|
||||||
|
from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,19 +24,20 @@ class TempPathFactory:
|
||||||
The base directory can be configured using the ``--basetemp`` option."""
|
The base directory can be configured using the ``--basetemp`` option."""
|
||||||
|
|
||||||
_given_basetemp = attr.ib(
|
_given_basetemp = attr.ib(
|
||||||
|
type=Path,
|
||||||
# using os.path.abspath() to get absolute path instead of resolve() as it
|
# using os.path.abspath() to get absolute path instead of resolve() as it
|
||||||
# does not work the same in all platforms (see #4427)
|
# does not work the same in all platforms (see #4427)
|
||||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
|
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/6172.
|
# Ignore type because of https://github.com/python/mypy/issues/6172.
|
||||||
converter=attr.converters.optional(
|
converter=attr.converters.optional(
|
||||||
lambda p: Path(os.path.abspath(str(p))) # type: ignore
|
lambda p: Path(os.path.abspath(str(p))) # type: ignore
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
_trace = attr.ib()
|
_trace = attr.ib()
|
||||||
_basetemp = attr.ib(default=None)
|
_basetemp = attr.ib(type=Optional[Path], default=None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, config):
|
def from_config(cls, config) -> "TempPathFactory":
|
||||||
"""
|
"""
|
||||||
:param config: a pytest configuration
|
:param config: a pytest configuration
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,7 +45,7 @@ class TempPathFactory:
|
||||||
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
||||||
)
|
)
|
||||||
|
|
||||||
def mktemp(self, basename, numbered=True):
|
def mktemp(self, basename: str, numbered: bool = True) -> Path:
|
||||||
"""makes a temporary directory managed by the factory"""
|
"""makes a temporary directory managed by the factory"""
|
||||||
if not numbered:
|
if not numbered:
|
||||||
p = self.getbasetemp().joinpath(basename)
|
p = self.getbasetemp().joinpath(basename)
|
||||||
|
|
@ -52,7 +55,7 @@ class TempPathFactory:
|
||||||
self._trace("mktemp", p)
|
self._trace("mktemp", p)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def getbasetemp(self):
|
def getbasetemp(self) -> Path:
|
||||||
""" return base temporary directory. """
|
""" return base temporary directory. """
|
||||||
if self._basetemp is not None:
|
if self._basetemp is not None:
|
||||||
return self._basetemp
|
return self._basetemp
|
||||||
|
|
@ -85,9 +88,9 @@ class TempdirFactory:
|
||||||
:class:``py.path.local`` for :class:``TempPathFactory``
|
:class:``py.path.local`` for :class:``TempPathFactory``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_tmppath_factory = attr.ib()
|
_tmppath_factory = attr.ib(type=TempPathFactory)
|
||||||
|
|
||||||
def mktemp(self, basename, numbered=True):
|
def mktemp(self, basename: str, numbered: bool = True):
|
||||||
"""Create a subdirectory of the base temporary directory and return it.
|
"""Create a subdirectory of the base temporary directory and return it.
|
||||||
If ``numbered``, ensure the directory is unique by adding a number
|
If ``numbered``, ensure the directory is unique by adding a number
|
||||||
prefix greater than any existing one.
|
prefix greater than any existing one.
|
||||||
|
|
@ -99,7 +102,7 @@ class TempdirFactory:
|
||||||
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
||||||
|
|
||||||
|
|
||||||
def get_user():
|
def get_user() -> Optional[str]:
|
||||||
"""Return the current user name, or None if getuser() does not work
|
"""Return the current user name, or None if getuser() does not work
|
||||||
in the current environment (see #1010).
|
in the current environment (see #1010).
|
||||||
"""
|
"""
|
||||||
|
|
@ -111,7 +114,7 @@ def get_user():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config) -> None:
|
||||||
"""Create a TempdirFactory and attach it to the config object.
|
"""Create a TempdirFactory and attach it to the config object.
|
||||||
|
|
||||||
This is to comply with existing plugins which expect the handler to be
|
This is to comply with existing plugins which expect the handler to be
|
||||||
|
|
@ -127,20 +130,22 @@ def pytest_configure(config):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmpdir_factory(request):
|
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
||||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||||
"""
|
"""
|
||||||
return request.config._tmpdirhandler
|
# Set dynamically by pytest_configure() above.
|
||||||
|
return request.config._tmpdirhandler # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmp_path_factory(request):
|
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||||
"""
|
"""
|
||||||
return request.config._tmp_path_factory
|
# Set dynamically by pytest_configure() above.
|
||||||
|
return request.config._tmp_path_factory # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def _mk_tmp(request, factory):
|
def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||||
name = request.node.name
|
name = request.node.name
|
||||||
name = re.sub(r"[\W]", "_", name)
|
name = re.sub(r"[\W]", "_", name)
|
||||||
MAXVAL = 30
|
MAXVAL = 30
|
||||||
|
|
@ -162,7 +167,7 @@ def tmpdir(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmp_path(request, tmp_path_factory):
|
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
||||||
"""Return a temporary directory path object
|
"""Return a temporary directory path object
|
||||||
which is unique to each test function invocation,
|
which is unique to each test function invocation,
|
||||||
created as a sub directory of the base temporary
|
created as a sub directory of the base temporary
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
from typing import Any
|
||||||
|
from typing import Generic
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
from typing import Type # noqa: F401 (used in type string)
|
||||||
|
|
||||||
|
|
||||||
class PytestWarning(UserWarning):
|
class PytestWarning(UserWarning):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`UserWarning`.
|
Bases: :class:`UserWarning`.
|
||||||
|
|
@ -72,7 +80,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
|
||||||
__module__ = "pytest"
|
__module__ = "pytest"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def simple(cls, apiname):
|
def simple(cls, apiname: str) -> "PytestExperimentalApiWarning":
|
||||||
return cls(
|
return cls(
|
||||||
"{apiname} is an experimental api that may change over time".format(
|
"{apiname} is an experimental api that may change over time".format(
|
||||||
apiname=apiname
|
apiname=apiname
|
||||||
|
|
@ -103,17 +111,20 @@ class PytestUnknownMarkWarning(PytestWarning):
|
||||||
__module__ = "pytest"
|
__module__ = "pytest"
|
||||||
|
|
||||||
|
|
||||||
|
_W = TypeVar("_W", bound=PytestWarning)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class UnformattedWarning:
|
class UnformattedWarning(Generic[_W]):
|
||||||
"""Used to hold warnings that need to format their message at runtime, as opposed to a direct message.
|
"""Used to hold warnings that need to format their message at runtime, as opposed to a direct message.
|
||||||
|
|
||||||
Using this class avoids to keep all the warning types and messages in this module, avoiding misuse.
|
Using this class avoids to keep all the warning types and messages in this module, avoiding misuse.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
category = attr.ib()
|
category = attr.ib(type="Type[_W]")
|
||||||
template = attr.ib()
|
template = attr.ib(type=str)
|
||||||
|
|
||||||
def format(self, **kwargs):
|
def format(self, **kwargs: Any) -> _W:
|
||||||
"""Returns an instance of the warning category, formatted with given kwargs"""
|
"""Returns an instance of the warning category, formatted with given kwargs"""
|
||||||
return self.category(self.template.format(**kwargs))
|
return self.category(self.template.format(**kwargs))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def pytest_addoption(parser):
|
||||||
type="linelist",
|
type="linelist",
|
||||||
help="Each line specifies a pattern for "
|
help="Each line specifies a pattern for "
|
||||||
"warnings.filterwarnings. "
|
"warnings.filterwarnings. "
|
||||||
"Processed after -W and --pythonwarnings.",
|
"Processed after -W/--pythonwarnings.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,6 +66,8 @@ def catch_warnings_for_item(config, ihook, when, item):
|
||||||
cmdline_filters = config.getoption("pythonwarnings") or []
|
cmdline_filters = config.getoption("pythonwarnings") or []
|
||||||
inifilters = config.getini("filterwarnings")
|
inifilters = config.getini("filterwarnings")
|
||||||
with warnings.catch_warnings(record=True) as log:
|
with warnings.catch_warnings(record=True) as log:
|
||||||
|
# mypy can't infer that record=True means log is not None; help it.
|
||||||
|
assert log is not None
|
||||||
|
|
||||||
if not sys.warnoptions:
|
if not sys.warnoptions:
|
||||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
||||||
|
|
@ -136,7 +138,7 @@ def _issue_warning_captured(warning, hook, stacklevel):
|
||||||
"""
|
"""
|
||||||
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
|
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
|
||||||
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
|
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
|
||||||
hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891.
|
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
|
||||||
|
|
||||||
:param warning: the warning instance.
|
:param warning: the warning instance.
|
||||||
:param hook: the hook caller
|
:param hook: the hook caller
|
||||||
|
|
@ -145,6 +147,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
|
||||||
with warnings.catch_warnings(record=True) as records:
|
with warnings.catch_warnings(record=True) as records:
|
||||||
warnings.simplefilter("always", type(warning))
|
warnings.simplefilter("always", type(warning))
|
||||||
warnings.warn(warning, stacklevel=stacklevel)
|
warnings.warn(warning, stacklevel=stacklevel)
|
||||||
|
# Mypy can't infer that record=True means records is not None; help it.
|
||||||
|
assert records is not None
|
||||||
hook.pytest_warning_captured.call_historic(
|
hook.pytest_warning_captured.call_historic(
|
||||||
kwargs=dict(warning_message=records[0], when="config", item=None)
|
kwargs=dict(warning_message=records[0], when="config", item=None)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,14 @@ class TestGeneralUsage:
|
||||||
p1 = testdir.makepyfile("")
|
p1 = testdir.makepyfile("")
|
||||||
p2 = testdir.makefile(".pyc", "123")
|
p2 = testdir.makefile(".pyc", "123")
|
||||||
result = testdir.runpytest(p1, p2)
|
result = testdir.runpytest(p1, p2)
|
||||||
assert result.ret
|
assert result.ret == ExitCode.USAGE_ERROR
|
||||||
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])
|
result.stderr.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"ERROR: not found: {}".format(p2),
|
||||||
|
"(no name {!r} in any of [[][]])".format(str(p2)),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("default")
|
@pytest.mark.filterwarnings("default")
|
||||||
def test_better_reporting_on_conftest_load_failure(self, testdir, request):
|
def test_better_reporting_on_conftest_load_failure(self, testdir, request):
|
||||||
|
|
@ -246,7 +252,7 @@ class TestGeneralUsage:
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
assert "should not be seen" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*should not be seen*")
|
||||||
assert "stderr42" not in result.stderr.str()
|
assert "stderr42" not in result.stderr.str()
|
||||||
|
|
||||||
def test_conftest_printing_shows_if_error(self, testdir):
|
def test_conftest_printing_shows_if_error(self, testdir):
|
||||||
|
|
@ -628,7 +634,7 @@ class TestInvocationVariants:
|
||||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
|
|
||||||
result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"])
|
result.stdout.fnmatch_lines(["collected*0*items*/*1*error"])
|
||||||
|
|
||||||
def test_pyargs_only_imported_once(self, testdir):
|
def test_pyargs_only_imported_once(self, testdir):
|
||||||
pkg = testdir.mkpydir("foo")
|
pkg = testdir.mkpydir("foo")
|
||||||
|
|
@ -858,16 +864,21 @@ class TestInvocationVariants:
|
||||||
4
|
4
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-rf")
|
testid = "test_doctest_id.txt::test_doctest_id.txt"
|
||||||
lines = result.stdout.str().splitlines()
|
expected_lines = [
|
||||||
for line in lines:
|
"*= FAILURES =*",
|
||||||
if line.startswith(("FAIL ", "FAILED ")):
|
"*_ ?doctest? test_doctest_id.txt _*",
|
||||||
_fail, _sep, testid = line.partition(" ")
|
"FAILED test_doctest_id.txt::test_doctest_id.txt",
|
||||||
break
|
"*= 1 failed in*",
|
||||||
result = testdir.runpytest(testid, "-rf")
|
]
|
||||||
result.stdout.fnmatch_lines(
|
result = testdir.runpytest(testid, "-rf", "--tb=short")
|
||||||
["FAILED test_doctest_id.txt::test_doctest_id.txt", "*1 failed*"]
|
result.stdout.fnmatch_lines(expected_lines)
|
||||||
)
|
|
||||||
|
# Ensure that re-running it will still handle it as
|
||||||
|
# doctest.DocTestFailure, which was not the case before when
|
||||||
|
# re-importing doctest, but not creating a new RUNNER_CLASS.
|
||||||
|
result = testdir.runpytest(testid, "-rf", "--tb=short")
|
||||||
|
result.stdout.fnmatch_lines(expected_lines)
|
||||||
|
|
||||||
def test_core_backward_compatibility(self):
|
def test_core_backward_compatibility(self):
|
||||||
"""Test backward compatibility for get_plugin_manager function. See #787."""
|
"""Test backward compatibility for get_plugin_manager function. See #787."""
|
||||||
|
|
@ -950,10 +961,10 @@ class TestDurations:
|
||||||
testdir.makepyfile(test_collecterror="""xyz""")
|
testdir.makepyfile(test_collecterror="""xyz""")
|
||||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||||
assert result.ret == 2
|
assert result.ret == 2
|
||||||
result.stdout.fnmatch_lines(["*Interrupted: 1 errors during collection*"])
|
result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"])
|
||||||
# Collection errors abort test execution, therefore no duration is
|
# Collection errors abort test execution, therefore no duration is
|
||||||
# output
|
# output
|
||||||
assert "duration" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*duration*")
|
||||||
|
|
||||||
def test_with_not(self, testdir):
|
def test_with_not(self, testdir):
|
||||||
testdir.makepyfile(self.source)
|
testdir.makepyfile(self.source)
|
||||||
|
|
@ -1007,7 +1018,7 @@ def test_zipimport_hook(testdir, tmpdir):
|
||||||
result = testdir.runpython(target)
|
result = testdir.runpython(target)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stderr.fnmatch_lines(["*not found*foo*"])
|
result.stderr.fnmatch_lines(["*not found*foo*"])
|
||||||
assert "INTERNALERROR>" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR>*")
|
||||||
|
|
||||||
|
|
||||||
def test_import_plugin_unicode_name(testdir):
|
def test_import_plugin_unicode_name(testdir):
|
||||||
|
|
@ -1237,3 +1248,40 @@ def test_warn_on_async_gen_function(testdir):
|
||||||
assert (
|
assert (
|
||||||
result.stdout.str().count("async def functions are not natively supported") == 1
|
result.stdout.str().count("async def functions are not natively supported") == 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pdb_can_be_rewritten(testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
**{
|
||||||
|
"conftest.py": """
|
||||||
|
import pytest
|
||||||
|
pytest.register_assert_rewrite("pdb")
|
||||||
|
""",
|
||||||
|
"__init__.py": "",
|
||||||
|
"pdb.py": """
|
||||||
|
def check():
|
||||||
|
assert 1 == 2
|
||||||
|
""",
|
||||||
|
"test_pdb.py": """
|
||||||
|
def test():
|
||||||
|
import pdb
|
||||||
|
assert pdb.check()
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Disable debugging plugin itself to avoid:
|
||||||
|
# > INTERNALERROR> AttributeError: module 'pdb' has no attribute 'set_trace'
|
||||||
|
result = testdir.runpytest_subprocess("-p", "no:debugging", "-vv")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
" def check():",
|
||||||
|
"> assert 1 == 2",
|
||||||
|
"E assert 1 == 2",
|
||||||
|
"E -1",
|
||||||
|
"E +2",
|
||||||
|
"",
|
||||||
|
"pdb.py:2: AssertionError",
|
||||||
|
"*= 1 failed in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result.ret == 1
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import sys
|
import sys
|
||||||
|
from types import FrameType
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_ne():
|
def test_ne() -> None:
|
||||||
code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec"))
|
code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec"))
|
||||||
assert code1 == code1
|
assert code1 == code1
|
||||||
code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec"))
|
code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec"))
|
||||||
assert code2 != code1
|
assert code2 != code1
|
||||||
|
|
||||||
|
|
||||||
def test_code_gives_back_name_for_not_existing_file():
|
def test_code_gives_back_name_for_not_existing_file() -> None:
|
||||||
name = "abc-123"
|
name = "abc-123"
|
||||||
co_code = compile("pass\n", name, "exec")
|
co_code = compile("pass\n", name, "exec")
|
||||||
assert co_code.co_filename == name
|
assert co_code.co_filename == name
|
||||||
|
|
@ -21,68 +22,67 @@ def test_code_gives_back_name_for_not_existing_file():
|
||||||
assert code.fullsource is None
|
assert code.fullsource is None
|
||||||
|
|
||||||
|
|
||||||
def test_code_with_class():
|
def test_code_with_class() -> None:
|
||||||
class A:
|
class A:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pytest.raises(TypeError, _pytest._code.Code, A)
|
pytest.raises(TypeError, _pytest._code.Code, A)
|
||||||
|
|
||||||
|
|
||||||
def x():
|
def x() -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def test_code_fullsource():
|
def test_code_fullsource() -> None:
|
||||||
code = _pytest._code.Code(x)
|
code = _pytest._code.Code(x)
|
||||||
full = code.fullsource
|
full = code.fullsource
|
||||||
assert "test_code_fullsource()" in str(full)
|
assert "test_code_fullsource()" in str(full)
|
||||||
|
|
||||||
|
|
||||||
def test_code_source():
|
def test_code_source() -> None:
|
||||||
code = _pytest._code.Code(x)
|
code = _pytest._code.Code(x)
|
||||||
src = code.source()
|
src = code.source()
|
||||||
expected = """def x():
|
expected = """def x() -> None:
|
||||||
raise NotImplementedError()"""
|
raise NotImplementedError()"""
|
||||||
assert str(src) == expected
|
assert str(src) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_frame_getsourcelineno_myself():
|
def test_frame_getsourcelineno_myself() -> None:
|
||||||
def func():
|
def func() -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
f = func()
|
f = _pytest._code.Frame(func())
|
||||||
f = _pytest._code.Frame(f)
|
|
||||||
source, lineno = f.code.fullsource, f.lineno
|
source, lineno = f.code.fullsource, f.lineno
|
||||||
|
assert source is not None
|
||||||
assert source[lineno].startswith(" return sys._getframe(0)")
|
assert source[lineno].startswith(" return sys._getframe(0)")
|
||||||
|
|
||||||
|
|
||||||
def test_getstatement_empty_fullsource():
|
def test_getstatement_empty_fullsource() -> None:
|
||||||
def func():
|
def func() -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
f = func()
|
f = _pytest._code.Frame(func())
|
||||||
f = _pytest._code.Frame(f)
|
|
||||||
with mock.patch.object(f.code.__class__, "fullsource", None):
|
with mock.patch.object(f.code.__class__, "fullsource", None):
|
||||||
assert f.statement == ""
|
assert f.statement == ""
|
||||||
|
|
||||||
|
|
||||||
def test_code_from_func():
|
def test_code_from_func() -> None:
|
||||||
co = _pytest._code.Code(test_frame_getsourcelineno_myself)
|
co = _pytest._code.Code(test_frame_getsourcelineno_myself)
|
||||||
assert co.firstlineno
|
assert co.firstlineno
|
||||||
assert co.path
|
assert co.path
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_handling():
|
def test_unicode_handling() -> None:
|
||||||
value = "ąć".encode()
|
value = "ąć".encode()
|
||||||
|
|
||||||
def f():
|
def f() -> None:
|
||||||
raise Exception(value)
|
raise Exception(value)
|
||||||
|
|
||||||
excinfo = pytest.raises(Exception, f)
|
excinfo = pytest.raises(Exception, f)
|
||||||
str(excinfo)
|
str(excinfo)
|
||||||
|
|
||||||
|
|
||||||
def test_code_getargs():
|
def test_code_getargs() -> None:
|
||||||
def f1(x):
|
def f1(x):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
@ -108,26 +108,26 @@ def test_code_getargs():
|
||||||
assert c4.getargs(var=True) == ("x", "y", "z")
|
assert c4.getargs(var=True) == ("x", "y", "z")
|
||||||
|
|
||||||
|
|
||||||
def test_frame_getargs():
|
def test_frame_getargs() -> None:
|
||||||
def f1(x):
|
def f1(x) -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
fr1 = _pytest._code.Frame(f1("a"))
|
fr1 = _pytest._code.Frame(f1("a"))
|
||||||
assert fr1.getargs(var=True) == [("x", "a")]
|
assert fr1.getargs(var=True) == [("x", "a")]
|
||||||
|
|
||||||
def f2(x, *y):
|
def f2(x, *y) -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
fr2 = _pytest._code.Frame(f2("a", "b", "c"))
|
fr2 = _pytest._code.Frame(f2("a", "b", "c"))
|
||||||
assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))]
|
assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))]
|
||||||
|
|
||||||
def f3(x, **z):
|
def f3(x, **z) -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
fr3 = _pytest._code.Frame(f3("a", b="c"))
|
fr3 = _pytest._code.Frame(f3("a", b="c"))
|
||||||
assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})]
|
assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})]
|
||||||
|
|
||||||
def f4(x, *y, **z):
|
def f4(x, *y, **z) -> FrameType:
|
||||||
return sys._getframe(0)
|
return sys._getframe(0)
|
||||||
|
|
||||||
fr4 = _pytest._code.Frame(f4("a", "b", c="d"))
|
fr4 = _pytest._code.Frame(f4("a", "b", c="d"))
|
||||||
|
|
@ -135,7 +135,7 @@ def test_frame_getargs():
|
||||||
|
|
||||||
|
|
||||||
class TestExceptionInfo:
|
class TestExceptionInfo:
|
||||||
def test_bad_getsource(self):
|
def test_bad_getsource(self) -> None:
|
||||||
try:
|
try:
|
||||||
if False:
|
if False:
|
||||||
pass
|
pass
|
||||||
|
|
@ -145,13 +145,13 @@ class TestExceptionInfo:
|
||||||
exci = _pytest._code.ExceptionInfo.from_current()
|
exci = _pytest._code.ExceptionInfo.from_current()
|
||||||
assert exci.getrepr()
|
assert exci.getrepr()
|
||||||
|
|
||||||
def test_from_current_with_missing(self):
|
def test_from_current_with_missing(self) -> None:
|
||||||
with pytest.raises(AssertionError, match="no current exception"):
|
with pytest.raises(AssertionError, match="no current exception"):
|
||||||
_pytest._code.ExceptionInfo.from_current()
|
_pytest._code.ExceptionInfo.from_current()
|
||||||
|
|
||||||
|
|
||||||
class TestTracebackEntry:
|
class TestTracebackEntry:
|
||||||
def test_getsource(self):
|
def test_getsource(self) -> None:
|
||||||
try:
|
try:
|
||||||
if False:
|
if False:
|
||||||
pass
|
pass
|
||||||
|
|
@ -161,12 +161,13 @@ class TestTracebackEntry:
|
||||||
exci = _pytest._code.ExceptionInfo.from_current()
|
exci = _pytest._code.ExceptionInfo.from_current()
|
||||||
entry = exci.traceback[0]
|
entry = exci.traceback[0]
|
||||||
source = entry.getsource()
|
source = entry.getsource()
|
||||||
|
assert source is not None
|
||||||
assert len(source) == 6
|
assert len(source) == 6
|
||||||
assert "assert False" in source[5]
|
assert "assert False" in source[5]
|
||||||
|
|
||||||
|
|
||||||
class TestReprFuncArgs:
|
class TestReprFuncArgs:
|
||||||
def test_not_raise_exception_with_mixed_encoding(self, tw_mock):
|
def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None:
|
||||||
from _pytest._code.code import ReprFuncArgs
|
from _pytest._code.code import ReprFuncArgs
|
||||||
|
|
||||||
args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]
|
args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import os
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
|
@ -59,9 +60,9 @@ def test_excinfo_getstatement():
|
||||||
except ValueError:
|
except ValueError:
|
||||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||||
linenumbers = [
|
linenumbers = [
|
||||||
_pytest._code.getrawcode(f).co_firstlineno - 1 + 4,
|
f.__code__.co_firstlineno - 1 + 4,
|
||||||
_pytest._code.getrawcode(f).co_firstlineno - 1 + 1,
|
f.__code__.co_firstlineno - 1 + 1,
|
||||||
_pytest._code.getrawcode(g).co_firstlineno - 1 + 1,
|
g.__code__.co_firstlineno - 1 + 1,
|
||||||
]
|
]
|
||||||
values = list(excinfo.traceback)
|
values = list(excinfo.traceback)
|
||||||
foundlinenumbers = [x.lineno for x in values]
|
foundlinenumbers = [x.lineno for x in values]
|
||||||
|
|
@ -224,23 +225,25 @@ class TestTraceback_f_g_h:
|
||||||
repr = excinfo.getrepr()
|
repr = excinfo.getrepr()
|
||||||
assert "RuntimeError: hello" in str(repr.reprcrash)
|
assert "RuntimeError: hello" in str(repr.reprcrash)
|
||||||
|
|
||||||
def test_traceback_no_recursion_index(self):
|
def test_traceback_no_recursion_index(self) -> None:
|
||||||
def do_stuff():
|
def do_stuff() -> None:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
def reraise_me():
|
def reraise_me() -> None:
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
exc, val, tb = sys.exc_info()
|
exc, val, tb = sys.exc_info()
|
||||||
|
assert val is not None
|
||||||
raise val.with_traceback(tb)
|
raise val.with_traceback(tb)
|
||||||
|
|
||||||
def f(n):
|
def f(n: int) -> None:
|
||||||
try:
|
try:
|
||||||
do_stuff()
|
do_stuff()
|
||||||
except: # noqa
|
except: # noqa
|
||||||
reraise_me()
|
reraise_me()
|
||||||
|
|
||||||
excinfo = pytest.raises(RuntimeError, f, 8)
|
excinfo = pytest.raises(RuntimeError, f, 8)
|
||||||
|
assert excinfo is not None
|
||||||
traceback = excinfo.traceback
|
traceback = excinfo.traceback
|
||||||
recindex = traceback.recursionindex()
|
recindex = traceback.recursionindex()
|
||||||
assert recindex is None
|
assert recindex is None
|
||||||
|
|
@ -316,8 +319,19 @@ def test_excinfo_exconly():
|
||||||
|
|
||||||
def test_excinfo_repr_str():
|
def test_excinfo_repr_str():
|
||||||
excinfo = pytest.raises(ValueError, h)
|
excinfo = pytest.raises(ValueError, h)
|
||||||
assert repr(excinfo) == "<ExceptionInfo ValueError tblen=4>"
|
assert repr(excinfo) == "<ExceptionInfo ValueError() tblen=4>"
|
||||||
assert str(excinfo) == "<ExceptionInfo ValueError tblen=4>"
|
assert str(excinfo) == "<ExceptionInfo ValueError() tblen=4>"
|
||||||
|
|
||||||
|
class CustomException(Exception):
|
||||||
|
def __repr__(self):
|
||||||
|
return "custom_repr"
|
||||||
|
|
||||||
|
def raises():
|
||||||
|
raise CustomException()
|
||||||
|
|
||||||
|
excinfo = pytest.raises(CustomException, raises)
|
||||||
|
assert repr(excinfo) == "<ExceptionInfo custom_repr tblen=2>"
|
||||||
|
assert str(excinfo) == "<ExceptionInfo custom_repr tblen=2>"
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_for_later():
|
def test_excinfo_for_later():
|
||||||
|
|
@ -399,7 +413,7 @@ def test_match_raises_error(testdir):
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"])
|
result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"])
|
||||||
assert "__tracebackhide__ = True" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*__tracebackhide__ = True*")
|
||||||
|
|
||||||
result = testdir.runpytest("--fulltrace")
|
result = testdir.runpytest("--fulltrace")
|
||||||
assert result.ret != 0
|
assert result.ret != 0
|
||||||
|
|
@ -491,65 +505,18 @@ raise ValueError()
|
||||||
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
||||||
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
|
||||||
|
|
||||||
def test_repr_source_failing_fullsource(self):
|
def test_repr_source_failing_fullsource(self, monkeypatch) -> None:
|
||||||
pr = FormattedExcinfo()
|
pr = FormattedExcinfo()
|
||||||
|
|
||||||
class FakeCode:
|
try:
|
||||||
class raw:
|
1 / 0
|
||||||
co_filename = "?"
|
except ZeroDivisionError:
|
||||||
|
excinfo = ExceptionInfo.from_current()
|
||||||
|
|
||||||
path = "?"
|
with monkeypatch.context() as m:
|
||||||
firstlineno = 5
|
m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None))
|
||||||
|
repr = pr.repr_excinfo(excinfo)
|
||||||
|
|
||||||
def fullsource(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
fullsource = property(fullsource)
|
|
||||||
|
|
||||||
class FakeFrame:
|
|
||||||
code = FakeCode()
|
|
||||||
f_locals = {}
|
|
||||||
f_globals = {}
|
|
||||||
|
|
||||||
class FakeTracebackEntry(_pytest._code.Traceback.Entry):
|
|
||||||
def __init__(self, tb, excinfo=None):
|
|
||||||
self.lineno = 5 + 3
|
|
||||||
|
|
||||||
@property
|
|
||||||
def frame(self):
|
|
||||||
return FakeFrame()
|
|
||||||
|
|
||||||
class Traceback(_pytest._code.Traceback):
|
|
||||||
Entry = FakeTracebackEntry
|
|
||||||
|
|
||||||
class FakeExcinfo(_pytest._code.ExceptionInfo):
|
|
||||||
typename = "Foo"
|
|
||||||
value = Exception()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def exconly(self, tryshort):
|
|
||||||
return "EXC"
|
|
||||||
|
|
||||||
def errisinstance(self, cls):
|
|
||||||
return False
|
|
||||||
|
|
||||||
excinfo = FakeExcinfo()
|
|
||||||
|
|
||||||
class FakeRawTB:
|
|
||||||
tb_next = None
|
|
||||||
|
|
||||||
tb = FakeRawTB()
|
|
||||||
excinfo.traceback = Traceback(tb)
|
|
||||||
|
|
||||||
fail = IOError()
|
|
||||||
repr = pr.repr_excinfo(excinfo)
|
|
||||||
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
|
||||||
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] == "> ???"
|
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
||||||
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
|
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
|
||||||
|
|
||||||
|
|
@ -573,7 +540,7 @@ raise ValueError()
|
||||||
reprlocals = p.repr_locals(loc)
|
reprlocals = p.repr_locals(loc)
|
||||||
assert reprlocals.lines
|
assert reprlocals.lines
|
||||||
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
||||||
assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1]
|
assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1]
|
||||||
|
|
||||||
def test_repr_local_with_exception_in_class_property(self):
|
def test_repr_local_with_exception_in_class_property(self):
|
||||||
class ExceptionWithBrokenClass(Exception):
|
class ExceptionWithBrokenClass(Exception):
|
||||||
|
|
@ -591,7 +558,7 @@ raise ValueError()
|
||||||
reprlocals = p.repr_locals(loc)
|
reprlocals = p.repr_locals(loc)
|
||||||
assert reprlocals.lines
|
assert reprlocals.lines
|
||||||
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
|
||||||
assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1]
|
assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1]
|
||||||
|
|
||||||
def test_repr_local_truncated(self):
|
def test_repr_local_truncated(self):
|
||||||
loc = {"l": [i for i in range(10)]}
|
loc = {"l": [i for i in range(10)]}
|
||||||
|
|
@ -632,7 +599,6 @@ raise ValueError()
|
||||||
assert lines[3] == "E world"
|
assert lines[3] == "E world"
|
||||||
assert not lines[4:]
|
assert not lines[4:]
|
||||||
|
|
||||||
loc = repr_entry.reprlocals is not None
|
|
||||||
loc = repr_entry.reprfileloc
|
loc = repr_entry.reprfileloc
|
||||||
assert loc.path == mod.__file__
|
assert loc.path == mod.__file__
|
||||||
assert loc.lineno == 3
|
assert loc.lineno == 3
|
||||||
|
|
@ -891,7 +857,7 @@ raise ValueError()
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
|
||||||
class MyRepr(TerminalRepr):
|
class MyRepr(TerminalRepr):
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw) -> None:
|
||||||
tw.line("я")
|
tw.line("я")
|
||||||
|
|
||||||
x = str(MyRepr())
|
x = str(MyRepr())
|
||||||
|
|
@ -1218,13 +1184,15 @@ raise ValueError()
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"reason, description",
|
"reason, description",
|
||||||
[
|
[
|
||||||
(
|
pytest.param(
|
||||||
"cause",
|
"cause",
|
||||||
"The above exception was the direct cause of the following exception:",
|
"The above exception was the direct cause of the following exception:",
|
||||||
|
id="cause",
|
||||||
),
|
),
|
||||||
(
|
pytest.param(
|
||||||
"context",
|
"context",
|
||||||
"During handling of the above exception, another exception occurred:",
|
"During handling of the above exception, another exception occurred:",
|
||||||
|
id="context",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -1320,9 +1288,10 @@ raise ValueError()
|
||||||
@pytest.mark.parametrize("style", ["short", "long"])
|
@pytest.mark.parametrize("style", ["short", "long"])
|
||||||
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
||||||
def test_repr_traceback_with_unicode(style, encoding):
|
def test_repr_traceback_with_unicode(style, encoding):
|
||||||
msg = "☹"
|
if encoding is None:
|
||||||
if encoding is not None:
|
msg = "☹" # type: Union[str, bytes]
|
||||||
msg = msg.encode(encoding)
|
else:
|
||||||
|
msg = "☹".encode(encoding)
|
||||||
try:
|
try:
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
|
@ -1343,7 +1312,8 @@ def test_cwd_deleted(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
||||||
assert "INTERNALERROR" not in result.stdout.str() + result.stderr.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
|
result.stderr.no_fnmatch_line("*INTERNALERROR*")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("limited_recursion_depth")
|
@pytest.mark.usefixtures("limited_recursion_depth")
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,16 @@
|
||||||
import ast
|
import ast
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._code import Source
|
from _pytest._code import Source
|
||||||
|
|
||||||
|
|
||||||
def test_source_str_function():
|
def test_source_str_function() -> None:
|
||||||
x = Source("3")
|
x = Source("3")
|
||||||
assert str(x) == "3"
|
assert str(x) == "3"
|
||||||
|
|
||||||
|
|
@ -25,7 +28,7 @@ def test_source_str_function():
|
||||||
assert str(x) == "\n3"
|
assert str(x) == "\n3"
|
||||||
|
|
||||||
|
|
||||||
def test_unicode():
|
def test_unicode() -> None:
|
||||||
x = Source("4")
|
x = Source("4")
|
||||||
assert str(x) == "4"
|
assert str(x) == "4"
|
||||||
co = _pytest._code.compile('"å"', mode="eval")
|
co = _pytest._code.compile('"å"', mode="eval")
|
||||||
|
|
@ -33,12 +36,12 @@ def test_unicode():
|
||||||
assert isinstance(val, str)
|
assert isinstance(val, str)
|
||||||
|
|
||||||
|
|
||||||
def test_source_from_function():
|
def test_source_from_function() -> None:
|
||||||
source = _pytest._code.Source(test_source_str_function)
|
source = _pytest._code.Source(test_source_str_function)
|
||||||
assert str(source).startswith("def test_source_str_function():")
|
assert str(source).startswith("def test_source_str_function() -> None:")
|
||||||
|
|
||||||
|
|
||||||
def test_source_from_method():
|
def test_source_from_method() -> None:
|
||||||
class TestClass:
|
class TestClass:
|
||||||
def test_method(self):
|
def test_method(self):
|
||||||
pass
|
pass
|
||||||
|
|
@ -47,13 +50,13 @@ def test_source_from_method():
|
||||||
assert source.lines == ["def test_method(self):", " pass"]
|
assert source.lines == ["def test_method(self):", " pass"]
|
||||||
|
|
||||||
|
|
||||||
def test_source_from_lines():
|
def test_source_from_lines() -> None:
|
||||||
lines = ["a \n", "b\n", "c"]
|
lines = ["a \n", "b\n", "c"]
|
||||||
source = _pytest._code.Source(lines)
|
source = _pytest._code.Source(lines)
|
||||||
assert source.lines == ["a ", "b", "c"]
|
assert source.lines == ["a ", "b", "c"]
|
||||||
|
|
||||||
|
|
||||||
def test_source_from_inner_function():
|
def test_source_from_inner_function() -> None:
|
||||||
def f():
|
def f():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -63,7 +66,7 @@ def test_source_from_inner_function():
|
||||||
assert str(source).startswith("def f():")
|
assert str(source).startswith("def f():")
|
||||||
|
|
||||||
|
|
||||||
def test_source_putaround_simple():
|
def test_source_putaround_simple() -> None:
|
||||||
source = Source("raise ValueError")
|
source = Source("raise ValueError")
|
||||||
source = source.putaround(
|
source = source.putaround(
|
||||||
"try:",
|
"try:",
|
||||||
|
|
@ -85,7 +88,7 @@ else:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_source_putaround():
|
def test_source_putaround() -> None:
|
||||||
source = Source()
|
source = Source()
|
||||||
source = source.putaround(
|
source = source.putaround(
|
||||||
"""
|
"""
|
||||||
|
|
@ -96,28 +99,29 @@ def test_source_putaround():
|
||||||
assert str(source).strip() == "if 1:\n x=1"
|
assert str(source).strip() == "if 1:\n x=1"
|
||||||
|
|
||||||
|
|
||||||
def test_source_strips():
|
def test_source_strips() -> None:
|
||||||
source = Source("")
|
source = Source("")
|
||||||
assert source == Source()
|
assert source == Source()
|
||||||
assert str(source) == ""
|
assert str(source) == ""
|
||||||
assert source.strip() == source
|
assert source.strip() == source
|
||||||
|
|
||||||
|
|
||||||
def test_source_strip_multiline():
|
def test_source_strip_multiline() -> None:
|
||||||
source = Source()
|
source = Source()
|
||||||
source.lines = ["", " hello", " "]
|
source.lines = ["", " hello", " "]
|
||||||
source2 = source.strip()
|
source2 = source.strip()
|
||||||
assert source2.lines == [" hello"]
|
assert source2.lines == [" hello"]
|
||||||
|
|
||||||
|
|
||||||
def test_syntaxerror_rerepresentation():
|
def test_syntaxerror_rerepresentation() -> None:
|
||||||
ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
|
ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
|
||||||
|
assert ex is not None
|
||||||
assert ex.value.lineno == 1
|
assert ex.value.lineno == 1
|
||||||
assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5
|
assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5
|
||||||
assert ex.value.text.strip(), "x x"
|
assert ex.value.text == "xyz xyz\n"
|
||||||
|
|
||||||
|
|
||||||
def test_isparseable():
|
def test_isparseable() -> None:
|
||||||
assert Source("hello").isparseable()
|
assert Source("hello").isparseable()
|
||||||
assert Source("if 1:\n pass").isparseable()
|
assert Source("if 1:\n pass").isparseable()
|
||||||
assert Source(" \nif 1:\n pass").isparseable()
|
assert Source(" \nif 1:\n pass").isparseable()
|
||||||
|
|
@ -127,56 +131,58 @@ def test_isparseable():
|
||||||
|
|
||||||
|
|
||||||
class TestAccesses:
|
class TestAccesses:
|
||||||
source = Source(
|
def setup_class(self) -> None:
|
||||||
"""\
|
self.source = Source(
|
||||||
def f(x):
|
"""\
|
||||||
pass
|
def f(x):
|
||||||
def g(x):
|
pass
|
||||||
pass
|
def g(x):
|
||||||
"""
|
pass
|
||||||
)
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
def test_getrange(self):
|
def test_getrange(self) -> None:
|
||||||
x = self.source[0:2]
|
x = self.source[0:2]
|
||||||
assert x.isparseable()
|
assert x.isparseable()
|
||||||
assert len(x.lines) == 2
|
assert len(x.lines) == 2
|
||||||
assert str(x) == "def f(x):\n pass"
|
assert str(x) == "def f(x):\n pass"
|
||||||
|
|
||||||
def test_getline(self):
|
def test_getline(self) -> None:
|
||||||
x = self.source[0]
|
x = self.source[0]
|
||||||
assert x == "def f(x):"
|
assert x == "def f(x):"
|
||||||
|
|
||||||
def test_len(self):
|
def test_len(self) -> None:
|
||||||
assert len(self.source) == 4
|
assert len(self.source) == 4
|
||||||
|
|
||||||
def test_iter(self):
|
def test_iter(self) -> None:
|
||||||
values = [x for x in self.source]
|
values = [x for x in self.source]
|
||||||
assert len(values) == 4
|
assert len(values) == 4
|
||||||
|
|
||||||
|
|
||||||
class TestSourceParsingAndCompiling:
|
class TestSourceParsingAndCompiling:
|
||||||
source = Source(
|
def setup_class(self) -> None:
|
||||||
"""\
|
self.source = Source(
|
||||||
def f(x):
|
"""\
|
||||||
assert (x ==
|
def f(x):
|
||||||
3 +
|
assert (x ==
|
||||||
4)
|
3 +
|
||||||
"""
|
4)
|
||||||
).strip()
|
"""
|
||||||
|
).strip()
|
||||||
|
|
||||||
def test_compile(self):
|
def test_compile(self) -> None:
|
||||||
co = _pytest._code.compile("x=3")
|
co = _pytest._code.compile("x=3")
|
||||||
d = {}
|
d = {} # type: Dict[str, Any]
|
||||||
exec(co, d)
|
exec(co, d)
|
||||||
assert d["x"] == 3
|
assert d["x"] == 3
|
||||||
|
|
||||||
def test_compile_and_getsource_simple(self):
|
def test_compile_and_getsource_simple(self) -> None:
|
||||||
co = _pytest._code.compile("x=3")
|
co = _pytest._code.compile("x=3")
|
||||||
exec(co)
|
exec(co)
|
||||||
source = _pytest._code.Source(co)
|
source = _pytest._code.Source(co)
|
||||||
assert str(source) == "x=3"
|
assert str(source) == "x=3"
|
||||||
|
|
||||||
def test_compile_and_getsource_through_same_function(self):
|
def test_compile_and_getsource_through_same_function(self) -> None:
|
||||||
def gensource(source):
|
def gensource(source):
|
||||||
return _pytest._code.compile(source)
|
return _pytest._code.compile(source)
|
||||||
|
|
||||||
|
|
@ -197,7 +203,7 @@ class TestSourceParsingAndCompiling:
|
||||||
source2 = inspect.getsource(co2)
|
source2 = inspect.getsource(co2)
|
||||||
assert "ValueError" in source2
|
assert "ValueError" in source2
|
||||||
|
|
||||||
def test_getstatement(self):
|
def test_getstatement(self) -> None:
|
||||||
# print str(self.source)
|
# print str(self.source)
|
||||||
ass = str(self.source[1:])
|
ass = str(self.source[1:])
|
||||||
for i in range(1, 4):
|
for i in range(1, 4):
|
||||||
|
|
@ -206,7 +212,7 @@ class TestSourceParsingAndCompiling:
|
||||||
# x = s.deindent()
|
# x = s.deindent()
|
||||||
assert str(s) == ass
|
assert str(s) == ass
|
||||||
|
|
||||||
def test_getstatementrange_triple_quoted(self):
|
def test_getstatementrange_triple_quoted(self) -> None:
|
||||||
# print str(self.source)
|
# print str(self.source)
|
||||||
source = Source(
|
source = Source(
|
||||||
"""hello('''
|
"""hello('''
|
||||||
|
|
@ -217,7 +223,7 @@ class TestSourceParsingAndCompiling:
|
||||||
s = source.getstatement(1)
|
s = source.getstatement(1)
|
||||||
assert s == str(source)
|
assert s == str(source)
|
||||||
|
|
||||||
def test_getstatementrange_within_constructs(self):
|
def test_getstatementrange_within_constructs(self) -> None:
|
||||||
source = Source(
|
source = Source(
|
||||||
"""\
|
"""\
|
||||||
try:
|
try:
|
||||||
|
|
@ -239,7 +245,7 @@ class TestSourceParsingAndCompiling:
|
||||||
# assert source.getstatementrange(5) == (0, 7)
|
# assert source.getstatementrange(5) == (0, 7)
|
||||||
assert source.getstatementrange(6) == (6, 7)
|
assert source.getstatementrange(6) == (6, 7)
|
||||||
|
|
||||||
def test_getstatementrange_bug(self):
|
def test_getstatementrange_bug(self) -> None:
|
||||||
source = Source(
|
source = Source(
|
||||||
"""\
|
"""\
|
||||||
try:
|
try:
|
||||||
|
|
@ -253,7 +259,7 @@ class TestSourceParsingAndCompiling:
|
||||||
assert len(source) == 6
|
assert len(source) == 6
|
||||||
assert source.getstatementrange(2) == (1, 4)
|
assert source.getstatementrange(2) == (1, 4)
|
||||||
|
|
||||||
def test_getstatementrange_bug2(self):
|
def test_getstatementrange_bug2(self) -> None:
|
||||||
source = Source(
|
source = Source(
|
||||||
"""\
|
"""\
|
||||||
assert (
|
assert (
|
||||||
|
|
@ -270,7 +276,7 @@ class TestSourceParsingAndCompiling:
|
||||||
assert len(source) == 9
|
assert len(source) == 9
|
||||||
assert source.getstatementrange(5) == (0, 9)
|
assert source.getstatementrange(5) == (0, 9)
|
||||||
|
|
||||||
def test_getstatementrange_ast_issue58(self):
|
def test_getstatementrange_ast_issue58(self) -> None:
|
||||||
source = Source(
|
source = Source(
|
||||||
"""\
|
"""\
|
||||||
|
|
||||||
|
|
@ -284,38 +290,44 @@ class TestSourceParsingAndCompiling:
|
||||||
assert getstatement(2, source).lines == source.lines[2:3]
|
assert getstatement(2, source).lines == source.lines[2:3]
|
||||||
assert getstatement(3, source).lines == source.lines[3:4]
|
assert getstatement(3, source).lines == source.lines[3:4]
|
||||||
|
|
||||||
def test_getstatementrange_out_of_bounds_py3(self):
|
def test_getstatementrange_out_of_bounds_py3(self) -> None:
|
||||||
source = Source("if xxx:\n from .collections import something")
|
source = Source("if xxx:\n from .collections import something")
|
||||||
r = source.getstatementrange(1)
|
r = source.getstatementrange(1)
|
||||||
assert r == (1, 2)
|
assert r == (1, 2)
|
||||||
|
|
||||||
def test_getstatementrange_with_syntaxerror_issue7(self):
|
def test_getstatementrange_with_syntaxerror_issue7(self) -> None:
|
||||||
source = Source(":")
|
source = Source(":")
|
||||||
pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
|
pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
|
||||||
|
|
||||||
def test_compile_to_ast(self):
|
def test_compile_to_ast(self) -> None:
|
||||||
source = Source("x = 4")
|
source = Source("x = 4")
|
||||||
mod = source.compile(flag=ast.PyCF_ONLY_AST)
|
mod = source.compile(flag=ast.PyCF_ONLY_AST)
|
||||||
assert isinstance(mod, ast.Module)
|
assert isinstance(mod, ast.Module)
|
||||||
compile(mod, "<filename>", "exec")
|
compile(mod, "<filename>", "exec")
|
||||||
|
|
||||||
def test_compile_and_getsource(self):
|
def test_compile_and_getsource(self) -> None:
|
||||||
co = self.source.compile()
|
co = self.source.compile()
|
||||||
exec(co, globals())
|
exec(co, globals())
|
||||||
f(7)
|
f(7) # type: ignore
|
||||||
excinfo = pytest.raises(AssertionError, f, 6)
|
excinfo = pytest.raises(AssertionError, f, 6) # type: ignore
|
||||||
|
assert excinfo is not None
|
||||||
frame = excinfo.traceback[-1].frame
|
frame = excinfo.traceback[-1].frame
|
||||||
|
assert isinstance(frame.code.fullsource, Source)
|
||||||
stmt = frame.code.fullsource.getstatement(frame.lineno)
|
stmt = frame.code.fullsource.getstatement(frame.lineno)
|
||||||
assert str(stmt).strip().startswith("assert")
|
assert str(stmt).strip().startswith("assert")
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["", None, "my"])
|
@pytest.mark.parametrize("name", ["", None, "my"])
|
||||||
def test_compilefuncs_and_path_sanity(self, name):
|
def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None:
|
||||||
def check(comp, name):
|
def check(comp, name):
|
||||||
co = comp(self.source, name)
|
co = comp(self.source, name)
|
||||||
if not name:
|
if not name:
|
||||||
expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2)
|
expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore
|
||||||
else:
|
else:
|
||||||
expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2)
|
expected = "codegen %r %s:%d>" % (
|
||||||
|
name,
|
||||||
|
mypath, # type: ignore
|
||||||
|
mylineno + 2 + 2, # type: ignore
|
||||||
|
) # type: ignore
|
||||||
fn = co.co_filename
|
fn = co.co_filename
|
||||||
assert fn.endswith(expected)
|
assert fn.endswith(expected)
|
||||||
|
|
||||||
|
|
@ -330,9 +342,9 @@ class TestSourceParsingAndCompiling:
|
||||||
pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval")
|
pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval")
|
||||||
|
|
||||||
|
|
||||||
def test_getstartingblock_singleline():
|
def test_getstartingblock_singleline() -> None:
|
||||||
class A:
|
class A:
|
||||||
def __init__(self, *args):
|
def __init__(self, *args) -> None:
|
||||||
frame = sys._getframe(1)
|
frame = sys._getframe(1)
|
||||||
self.source = _pytest._code.Frame(frame).statement
|
self.source = _pytest._code.Frame(frame).statement
|
||||||
|
|
||||||
|
|
@ -342,22 +354,22 @@ def test_getstartingblock_singleline():
|
||||||
assert len(values) == 1
|
assert len(values) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_getline_finally():
|
def test_getline_finally() -> None:
|
||||||
def c():
|
def c() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with pytest.raises(TypeError) as excinfo:
|
with pytest.raises(TypeError) as excinfo:
|
||||||
teardown = None
|
teardown = None
|
||||||
try:
|
try:
|
||||||
c(1)
|
c(1) # type: ignore
|
||||||
finally:
|
finally:
|
||||||
if teardown:
|
if teardown:
|
||||||
teardown()
|
teardown()
|
||||||
source = excinfo.traceback[-1].statement
|
source = excinfo.traceback[-1].statement
|
||||||
assert str(source).strip() == "c(1)"
|
assert str(source).strip() == "c(1) # type: ignore"
|
||||||
|
|
||||||
|
|
||||||
def test_getfuncsource_dynamic():
|
def test_getfuncsource_dynamic() -> None:
|
||||||
source = """
|
source = """
|
||||||
def f():
|
def f():
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
@ -366,11 +378,13 @@ def test_getfuncsource_dynamic():
|
||||||
"""
|
"""
|
||||||
co = _pytest._code.compile(source)
|
co = _pytest._code.compile(source)
|
||||||
exec(co, globals())
|
exec(co, globals())
|
||||||
assert str(_pytest._code.Source(f)).strip() == "def f():\n raise ValueError"
|
f_source = _pytest._code.Source(f) # type: ignore
|
||||||
assert str(_pytest._code.Source(g)).strip() == "def g(): pass"
|
g_source = _pytest._code.Source(g) # type: ignore
|
||||||
|
assert str(f_source).strip() == "def f():\n raise ValueError"
|
||||||
|
assert str(g_source).strip() == "def g(): pass"
|
||||||
|
|
||||||
|
|
||||||
def test_getfuncsource_with_multine_string():
|
def test_getfuncsource_with_multine_string() -> None:
|
||||||
def f():
|
def f():
|
||||||
c = """while True:
|
c = """while True:
|
||||||
pass
|
pass
|
||||||
|
|
@ -385,7 +399,7 @@ def test_getfuncsource_with_multine_string():
|
||||||
assert str(_pytest._code.Source(f)) == expected.rstrip()
|
assert str(_pytest._code.Source(f)) == expected.rstrip()
|
||||||
|
|
||||||
|
|
||||||
def test_deindent():
|
def test_deindent() -> None:
|
||||||
from _pytest._code.source import deindent as deindent
|
from _pytest._code.source import deindent as deindent
|
||||||
|
|
||||||
assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"]
|
assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"]
|
||||||
|
|
@ -399,7 +413,7 @@ def test_deindent():
|
||||||
assert lines == ["def f():", " def g():", " pass"]
|
assert lines == ["def f():", " def g():", " pass"]
|
||||||
|
|
||||||
|
|
||||||
def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot):
|
def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None:
|
||||||
# this test fails because the implicit inspect.getsource(A) below
|
# this test fails because the implicit inspect.getsource(A) below
|
||||||
# does not return the "x = 1" last line.
|
# does not return the "x = 1" last line.
|
||||||
source = _pytest._code.Source(
|
source = _pytest._code.Source(
|
||||||
|
|
@ -421,7 +435,7 @@ if True:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_getsource_fallback():
|
def test_getsource_fallback() -> None:
|
||||||
from _pytest._code.source import getsource
|
from _pytest._code.source import getsource
|
||||||
|
|
||||||
expected = """def x():
|
expected = """def x():
|
||||||
|
|
@ -430,7 +444,7 @@ def test_getsource_fallback():
|
||||||
assert src == expected
|
assert src == expected
|
||||||
|
|
||||||
|
|
||||||
def test_idem_compile_and_getsource():
|
def test_idem_compile_and_getsource() -> None:
|
||||||
from _pytest._code.source import getsource
|
from _pytest._code.source import getsource
|
||||||
|
|
||||||
expected = "def x(): pass"
|
expected = "def x(): pass"
|
||||||
|
|
@ -439,15 +453,16 @@ def test_idem_compile_and_getsource():
|
||||||
assert src == expected
|
assert src == expected
|
||||||
|
|
||||||
|
|
||||||
def test_findsource_fallback():
|
def test_findsource_fallback() -> None:
|
||||||
from _pytest._code.source import findsource
|
from _pytest._code.source import findsource
|
||||||
|
|
||||||
src, lineno = findsource(x)
|
src, lineno = findsource(x)
|
||||||
|
assert src is not None
|
||||||
assert "test_findsource_simple" in str(src)
|
assert "test_findsource_simple" in str(src)
|
||||||
assert src[lineno] == " def x():"
|
assert src[lineno] == " def x():"
|
||||||
|
|
||||||
|
|
||||||
def test_findsource():
|
def test_findsource() -> None:
|
||||||
from _pytest._code.source import findsource
|
from _pytest._code.source import findsource
|
||||||
|
|
||||||
co = _pytest._code.compile(
|
co = _pytest._code.compile(
|
||||||
|
|
@ -458,25 +473,27 @@ def test_findsource():
|
||||||
)
|
)
|
||||||
|
|
||||||
src, lineno = findsource(co)
|
src, lineno = findsource(co)
|
||||||
|
assert src is not None
|
||||||
assert "if 1:" in str(src)
|
assert "if 1:" in str(src)
|
||||||
|
|
||||||
d = {}
|
d = {} # type: Dict[str, Any]
|
||||||
eval(co, d)
|
eval(co, d)
|
||||||
src, lineno = findsource(d["x"])
|
src, lineno = findsource(d["x"])
|
||||||
|
assert src is not None
|
||||||
assert "if 1:" in str(src)
|
assert "if 1:" in str(src)
|
||||||
assert src[lineno] == " def x():"
|
assert src[lineno] == " def x():"
|
||||||
|
|
||||||
|
|
||||||
def test_getfslineno():
|
def test_getfslineno() -> None:
|
||||||
from _pytest._code import getfslineno
|
from _pytest._code import getfslineno
|
||||||
|
|
||||||
def f(x):
|
def f(x) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fspath, lineno = getfslineno(f)
|
fspath, lineno = getfslineno(f)
|
||||||
|
|
||||||
assert fspath.basename == "test_source.py"
|
assert fspath.basename == "test_source.py"
|
||||||
assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource
|
assert lineno == f.__code__.co_firstlineno - 1 # see findsource
|
||||||
|
|
||||||
class A:
|
class A:
|
||||||
pass
|
pass
|
||||||
|
|
@ -496,40 +513,40 @@ def test_getfslineno():
|
||||||
assert getfslineno(B)[1] == -1
|
assert getfslineno(B)[1] == -1
|
||||||
|
|
||||||
|
|
||||||
def test_code_of_object_instance_with_call():
|
def test_code_of_object_instance_with_call() -> None:
|
||||||
class A:
|
class A:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pytest.raises(TypeError, lambda: _pytest._code.Source(A()))
|
pytest.raises(TypeError, lambda: _pytest._code.Source(A()))
|
||||||
|
|
||||||
class WithCall:
|
class WithCall:
|
||||||
def __call__(self):
|
def __call__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
code = _pytest._code.Code(WithCall())
|
code = _pytest._code.Code(WithCall())
|
||||||
assert "pass" in str(code.source())
|
assert "pass" in str(code.source())
|
||||||
|
|
||||||
class Hello:
|
class Hello:
|
||||||
def __call__(self):
|
def __call__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pytest.raises(TypeError, lambda: _pytest._code.Code(Hello))
|
pytest.raises(TypeError, lambda: _pytest._code.Code(Hello))
|
||||||
|
|
||||||
|
|
||||||
def getstatement(lineno, source):
|
def getstatement(lineno: int, source) -> Source:
|
||||||
from _pytest._code.source import getstatementrange_ast
|
from _pytest._code.source import getstatementrange_ast
|
||||||
|
|
||||||
source = _pytest._code.Source(source, deindent=False)
|
src = _pytest._code.Source(source, deindent=False)
|
||||||
ast, start, end = getstatementrange_ast(lineno, source)
|
ast, start, end = getstatementrange_ast(lineno, src)
|
||||||
return source[start:end]
|
return src[start:end]
|
||||||
|
|
||||||
|
|
||||||
def test_oneline():
|
def test_oneline() -> None:
|
||||||
source = getstatement(0, "raise ValueError")
|
source = getstatement(0, "raise ValueError")
|
||||||
assert str(source) == "raise ValueError"
|
assert str(source) == "raise ValueError"
|
||||||
|
|
||||||
|
|
||||||
def test_comment_and_no_newline_at_end():
|
def test_comment_and_no_newline_at_end() -> None:
|
||||||
from _pytest._code.source import getstatementrange_ast
|
from _pytest._code.source import getstatementrange_ast
|
||||||
|
|
||||||
source = Source(
|
source = Source(
|
||||||
|
|
@ -543,12 +560,12 @@ def test_comment_and_no_newline_at_end():
|
||||||
assert end == 2
|
assert end == 2
|
||||||
|
|
||||||
|
|
||||||
def test_oneline_and_comment():
|
def test_oneline_and_comment() -> None:
|
||||||
source = getstatement(0, "raise ValueError\n#hello")
|
source = getstatement(0, "raise ValueError\n#hello")
|
||||||
assert str(source) == "raise ValueError"
|
assert str(source) == "raise ValueError"
|
||||||
|
|
||||||
|
|
||||||
def test_comments():
|
def test_comments() -> None:
|
||||||
source = '''def test():
|
source = '''def test():
|
||||||
"comment 1"
|
"comment 1"
|
||||||
x = 1
|
x = 1
|
||||||
|
|
@ -574,7 +591,7 @@ comment 4
|
||||||
assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'
|
assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'
|
||||||
|
|
||||||
|
|
||||||
def test_comment_in_statement():
|
def test_comment_in_statement() -> None:
|
||||||
source = """test(foo=1,
|
source = """test(foo=1,
|
||||||
# comment 1
|
# comment 1
|
||||||
bar=2)
|
bar=2)
|
||||||
|
|
@ -586,17 +603,17 @@ def test_comment_in_statement():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_single_line_else():
|
def test_single_line_else() -> None:
|
||||||
source = getstatement(1, "if False: 2\nelse: 3")
|
source = getstatement(1, "if False: 2\nelse: 3")
|
||||||
assert str(source) == "else: 3"
|
assert str(source) == "else: 3"
|
||||||
|
|
||||||
|
|
||||||
def test_single_line_finally():
|
def test_single_line_finally() -> None:
|
||||||
source = getstatement(1, "try: 1\nfinally: 3")
|
source = getstatement(1, "try: 1\nfinally: 3")
|
||||||
assert str(source) == "finally: 3"
|
assert str(source) == "finally: 3"
|
||||||
|
|
||||||
|
|
||||||
def test_issue55():
|
def test_issue55() -> None:
|
||||||
source = (
|
source = (
|
||||||
"def round_trip(dinp):\n assert 1 == dinp\n"
|
"def round_trip(dinp):\n assert 1 == dinp\n"
|
||||||
'def test_rt():\n round_trip("""\n""")\n'
|
'def test_rt():\n round_trip("""\n""")\n'
|
||||||
|
|
@ -605,7 +622,7 @@ def test_issue55():
|
||||||
assert str(s) == ' round_trip("""\n""")'
|
assert str(s) == ' round_trip("""\n""")'
|
||||||
|
|
||||||
|
|
||||||
def test_multiline():
|
def test_multiline() -> None:
|
||||||
source = getstatement(
|
source = getstatement(
|
||||||
0,
|
0,
|
||||||
"""\
|
"""\
|
||||||
|
|
@ -619,7 +636,8 @@ x = 3
|
||||||
|
|
||||||
|
|
||||||
class TestTry:
|
class TestTry:
|
||||||
source = """\
|
def setup_class(self) -> None:
|
||||||
|
self.source = """\
|
||||||
try:
|
try:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
except Something:
|
except Something:
|
||||||
|
|
@ -628,42 +646,44 @@ else:
|
||||||
raise KeyError()
|
raise KeyError()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_body(self):
|
def test_body(self) -> None:
|
||||||
source = getstatement(1, self.source)
|
source = getstatement(1, self.source)
|
||||||
assert str(source) == " raise ValueError"
|
assert str(source) == " raise ValueError"
|
||||||
|
|
||||||
def test_except_line(self):
|
def test_except_line(self) -> None:
|
||||||
source = getstatement(2, self.source)
|
source = getstatement(2, self.source)
|
||||||
assert str(source) == "except Something:"
|
assert str(source) == "except Something:"
|
||||||
|
|
||||||
def test_except_body(self):
|
def test_except_body(self) -> None:
|
||||||
source = getstatement(3, self.source)
|
source = getstatement(3, self.source)
|
||||||
assert str(source) == " raise IndexError(1)"
|
assert str(source) == " raise IndexError(1)"
|
||||||
|
|
||||||
def test_else(self):
|
def test_else(self) -> None:
|
||||||
source = getstatement(5, self.source)
|
source = getstatement(5, self.source)
|
||||||
assert str(source) == " raise KeyError()"
|
assert str(source) == " raise KeyError()"
|
||||||
|
|
||||||
|
|
||||||
class TestTryFinally:
|
class TestTryFinally:
|
||||||
source = """\
|
def setup_class(self) -> None:
|
||||||
|
self.source = """\
|
||||||
try:
|
try:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
finally:
|
finally:
|
||||||
raise IndexError(1)
|
raise IndexError(1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_body(self):
|
def test_body(self) -> None:
|
||||||
source = getstatement(1, self.source)
|
source = getstatement(1, self.source)
|
||||||
assert str(source) == " raise ValueError"
|
assert str(source) == " raise ValueError"
|
||||||
|
|
||||||
def test_finally(self):
|
def test_finally(self) -> None:
|
||||||
source = getstatement(3, self.source)
|
source = getstatement(3, self.source)
|
||||||
assert str(source) == " raise IndexError(1)"
|
assert str(source) == " raise IndexError(1)"
|
||||||
|
|
||||||
|
|
||||||
class TestIf:
|
class TestIf:
|
||||||
source = """\
|
def setup_class(self) -> None:
|
||||||
|
self.source = """\
|
||||||
if 1:
|
if 1:
|
||||||
y = 3
|
y = 3
|
||||||
elif False:
|
elif False:
|
||||||
|
|
@ -672,24 +692,24 @@ else:
|
||||||
y = 7
|
y = 7
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_body(self):
|
def test_body(self) -> None:
|
||||||
source = getstatement(1, self.source)
|
source = getstatement(1, self.source)
|
||||||
assert str(source) == " y = 3"
|
assert str(source) == " y = 3"
|
||||||
|
|
||||||
def test_elif_clause(self):
|
def test_elif_clause(self) -> None:
|
||||||
source = getstatement(2, self.source)
|
source = getstatement(2, self.source)
|
||||||
assert str(source) == "elif False:"
|
assert str(source) == "elif False:"
|
||||||
|
|
||||||
def test_elif(self):
|
def test_elif(self) -> None:
|
||||||
source = getstatement(3, self.source)
|
source = getstatement(3, self.source)
|
||||||
assert str(source) == " y = 5"
|
assert str(source) == " y = 5"
|
||||||
|
|
||||||
def test_else(self):
|
def test_else(self) -> None:
|
||||||
source = getstatement(5, self.source)
|
source = getstatement(5, self.source)
|
||||||
assert str(source) == " y = 7"
|
assert str(source) == " y = 7"
|
||||||
|
|
||||||
|
|
||||||
def test_semicolon():
|
def test_semicolon() -> None:
|
||||||
s = """\
|
s = """\
|
||||||
hello ; pytest.skip()
|
hello ; pytest.skip()
|
||||||
"""
|
"""
|
||||||
|
|
@ -697,7 +717,7 @@ hello ; pytest.skip()
|
||||||
assert str(source) == s.strip()
|
assert str(source) == s.strip()
|
||||||
|
|
||||||
|
|
||||||
def test_def_online():
|
def test_def_online() -> None:
|
||||||
s = """\
|
s = """\
|
||||||
def func(): raise ValueError(42)
|
def func(): raise ValueError(42)
|
||||||
|
|
||||||
|
|
@ -708,7 +728,7 @@ def something():
|
||||||
assert str(source) == "def func(): raise ValueError(42)"
|
assert str(source) == "def func(): raise ValueError(42)"
|
||||||
|
|
||||||
|
|
||||||
def XXX_test_expression_multiline():
|
def XXX_test_expression_multiline() -> None:
|
||||||
source = """\
|
source = """\
|
||||||
something
|
something
|
||||||
'''
|
'''
|
||||||
|
|
@ -717,7 +737,7 @@ something
|
||||||
assert str(result) == "'''\n'''"
|
assert str(result) == "'''\n'''"
|
||||||
|
|
||||||
|
|
||||||
def test_getstartingblock_multiline():
|
def test_getstartingblock_multiline() -> None:
|
||||||
class A:
|
class A:
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
frame = sys._getframe(1)
|
frame = sys._getframe(1)
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,12 @@ def pytest_collection_modifyitems(config, items):
|
||||||
neutral_items.append(item)
|
neutral_items.append(item)
|
||||||
else:
|
else:
|
||||||
if "testdir" in fixtures:
|
if "testdir" in fixtures:
|
||||||
if spawn_names.intersection(item.function.__code__.co_names):
|
co_names = item.function.__code__.co_names
|
||||||
|
if spawn_names.intersection(co_names):
|
||||||
item.add_marker(pytest.mark.uses_pexpect)
|
item.add_marker(pytest.mark.uses_pexpect)
|
||||||
slowest_items.append(item)
|
slowest_items.append(item)
|
||||||
|
elif "runpytest_subprocess" in co_names:
|
||||||
|
slowest_items.append(item)
|
||||||
else:
|
else:
|
||||||
slow_items.append(item)
|
slow_items.append(item)
|
||||||
item.add_marker(pytest.mark.slow)
|
item.add_marker(pytest.mark.slow)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ def test_resultlog_is_deprecated(testdir):
|
||||||
result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log"))
|
result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log"))
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*--result-log is deprecated and scheduled for removal in pytest 6.0*",
|
"*--result-log is deprecated, please try the new pytest-reportlog plugin.",
|
||||||
"*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*",
|
"*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
@ -44,3 +44,32 @@ def test_external_plugins_integrated(testdir, plugin):
|
||||||
|
|
||||||
with pytest.warns(pytest.PytestConfigWarning):
|
with pytest.warns(pytest.PytestConfigWarning):
|
||||||
testdir.parseconfig("-p", plugin)
|
testdir.parseconfig("-p", plugin)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"])
|
||||||
|
def test_warn_about_imminent_junit_family_default_change(testdir, junit_family):
|
||||||
|
"""Show a warning if junit_family is not defined and --junitxml is used (#6179)"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_foo():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if junit_family:
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
junit_family={junit_family}
|
||||||
|
""".format(
|
||||||
|
junit_family=junit_family
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = testdir.runpytest("--junit-xml=foo.xml")
|
||||||
|
warning_msg = (
|
||||||
|
"*PytestDeprecationWarning: The 'junit_family' default value will change*"
|
||||||
|
)
|
||||||
|
if junit_family:
|
||||||
|
result.stdout.no_fnmatch_line(warning_msg)
|
||||||
|
else:
|
||||||
|
result.stdout.fnmatch_lines([warning_msg])
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import pytest
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,9 +41,81 @@ def test_exceptions():
|
||||||
assert "TypeError" in s
|
assert "TypeError" in s
|
||||||
assert "TypeError" in saferepr(BrokenRepr("string"))
|
assert "TypeError" in saferepr(BrokenRepr("string"))
|
||||||
|
|
||||||
s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse")))
|
none = None
|
||||||
assert "NameError" not in s2
|
try:
|
||||||
assert "unknown" in s2
|
none()
|
||||||
|
except BaseException as exc:
|
||||||
|
exp_exc = repr(exc)
|
||||||
|
obj = BrokenRepr(BrokenReprException("omg even worse"))
|
||||||
|
s2 = saferepr(obj)
|
||||||
|
assert s2 == (
|
||||||
|
"<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format(
|
||||||
|
exp_exc, id(obj)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_baseexception():
|
||||||
|
"""Test saferepr() with BaseExceptions, which includes pytest outcomes."""
|
||||||
|
|
||||||
|
class RaisingOnStrRepr(BaseException):
|
||||||
|
def __init__(self, exc_types):
|
||||||
|
self.exc_types = exc_types
|
||||||
|
|
||||||
|
def raise_exc(self, *args):
|
||||||
|
try:
|
||||||
|
self.exc_type = self.exc_types.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
if hasattr(self.exc_type, "__call__"):
|
||||||
|
raise self.exc_type(*args)
|
||||||
|
raise self.exc_type
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
self.raise_exc("__str__")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
self.raise_exc("__repr__")
|
||||||
|
|
||||||
|
class BrokenObj:
|
||||||
|
def __init__(self, exc):
|
||||||
|
self.exc = exc
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
raise self.exc
|
||||||
|
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
baseexc_str = BaseException("__str__")
|
||||||
|
obj = BrokenObj(RaisingOnStrRepr([BaseException]))
|
||||||
|
assert saferepr(obj) == (
|
||||||
|
"<[unpresentable exception ({!r}) "
|
||||||
|
"raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj))
|
||||||
|
)
|
||||||
|
obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])]))
|
||||||
|
assert saferepr(obj) == (
|
||||||
|
"<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format(
|
||||||
|
baseexc_str, id(obj)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(KeyboardInterrupt):
|
||||||
|
saferepr(BrokenObj(KeyboardInterrupt()))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
saferepr(BrokenObj(SystemExit()))
|
||||||
|
|
||||||
|
with pytest.raises(KeyboardInterrupt):
|
||||||
|
saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt])))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
saferepr(BrokenObj(RaisingOnStrRepr([SystemExit])))
|
||||||
|
|
||||||
|
with pytest.raises(KeyboardInterrupt):
|
||||||
|
print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt]))))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit])))
|
||||||
|
|
||||||
|
|
||||||
def test_buggy_builtin_repr():
|
def test_buggy_builtin_repr():
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ def test_change_level_undo(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
|
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
|
||||||
assert "log from test2" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*log from test2*")
|
||||||
|
|
||||||
|
|
||||||
def test_with_statement(caplog):
|
def test_with_statement(caplog):
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,77 @@ def test_multiline_message():
|
||||||
# this is called by logging.Formatter.format
|
# this is called by logging.Formatter.format
|
||||||
record.message = record.getMessage()
|
record.message = record.getMessage()
|
||||||
|
|
||||||
style = PercentStyleMultiline(logfmt)
|
ai_on_style = PercentStyleMultiline(logfmt, True)
|
||||||
output = style.format(record)
|
output = ai_on_style.format(record)
|
||||||
assert output == (
|
assert output == (
|
||||||
"dummypath 10 INFO Test Message line1\n"
|
"dummypath 10 INFO Test Message line1\n"
|
||||||
" line2"
|
" line2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ai_off_style = PercentStyleMultiline(logfmt, False)
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
ai_none_style = PercentStyleMultiline(logfmt, None)
|
||||||
|
output = ai_none_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = False
|
||||||
|
output = ai_on_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = True
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\n"
|
||||||
|
" line2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = "False"
|
||||||
|
output = ai_on_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = "True"
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\n"
|
||||||
|
" line2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# bad string values default to False
|
||||||
|
record.auto_indent = "junk"
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# anything other than string or int will default to False
|
||||||
|
record.auto_indent = dict()
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\nline2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = "5"
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\n line2"
|
||||||
|
)
|
||||||
|
|
||||||
|
record.auto_indent = 5
|
||||||
|
output = ai_off_style.format(record)
|
||||||
|
assert output == (
|
||||||
|
"dummypath 10 INFO Test Message line1\n line2"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_colored_short_level():
|
def test_colored_short_level():
|
||||||
logfmt = "%(levelname).1s %(message)s"
|
logfmt = "%(levelname).1s %(message)s"
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ def test_log_cli_level_log_level_interaction(testdir):
|
||||||
"=* 1 failed in *=",
|
"=* 1 failed in *=",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "DEBUG" not in result.stdout.str()
|
result.stdout.no_re_match_line("DEBUG")
|
||||||
|
|
||||||
|
|
||||||
def test_setup_logging(testdir):
|
def test_setup_logging(testdir):
|
||||||
|
|
@ -282,7 +282,7 @@ def test_log_cli_default_level(testdir):
|
||||||
"WARNING*test_log_cli_default_level.py* message will be shown*",
|
"WARNING*test_log_cli_default_level.py* message will be shown*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "INFO message won't be shown" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INFO message won't be shown*")
|
||||||
# make sure that that we get a '0' exit code for the testsuite
|
# make sure that that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
@ -566,7 +566,7 @@ def test_log_cli_level(testdir):
|
||||||
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "This log message won't be shown" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*This log message won't be shown*")
|
||||||
|
|
||||||
# make sure that that we get a '0' exit code for the testsuite
|
# make sure that that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
@ -580,7 +580,7 @@ def test_log_cli_level(testdir):
|
||||||
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "This log message won't be shown" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*This log message won't be shown*")
|
||||||
|
|
||||||
# make sure that that we get a '0' exit code for the testsuite
|
# make sure that that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
@ -616,7 +616,7 @@ def test_log_cli_ini_level(testdir):
|
||||||
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
"PASSED", # 'PASSED' on its own line because the log message prints a new line
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "This log message won't be shown" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*This log message won't be shown*")
|
||||||
|
|
||||||
# make sure that that we get a '0' exit code for the testsuite
|
# make sure that that we get a '0' exit code for the testsuite
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
@ -942,7 +942,7 @@ def test_collection_collect_only_live_logging(testdir, verbose):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
elif verbose == "-q":
|
elif verbose == "-q":
|
||||||
assert "collected 1 item*" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*collected 1 item**")
|
||||||
expected_lines.extend(
|
expected_lines.extend(
|
||||||
[
|
[
|
||||||
"*test_collection_collect_only_live_logging.py::test_simple*",
|
"*test_collection_collect_only_live_logging.py::test_simple*",
|
||||||
|
|
@ -950,7 +950,7 @@ def test_collection_collect_only_live_logging(testdir, verbose):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
elif verbose == "-qq":
|
elif verbose == "-qq":
|
||||||
assert "collected 1 item*" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*collected 1 item**")
|
||||||
expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"])
|
expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"])
|
||||||
|
|
||||||
result.stdout.fnmatch_lines(expected_lines)
|
result.stdout.fnmatch_lines(expected_lines)
|
||||||
|
|
@ -983,7 +983,7 @@ def test_collection_logging_to_file(testdir):
|
||||||
|
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
||||||
assert "--- live log collection ---" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*--- live log collection ---*")
|
||||||
|
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert os.path.isfile(log_file)
|
assert os.path.isfile(log_file)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import doctest
|
|
||||||
import operator
|
import operator
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
@ -11,68 +10,81 @@ from pytest import approx
|
||||||
inf, nan = float("inf"), float("nan")
|
inf, nan = float("inf"), float("nan")
|
||||||
|
|
||||||
|
|
||||||
class MyDocTestRunner(doctest.DocTestRunner):
|
@pytest.fixture
|
||||||
def __init__(self):
|
def mocked_doctest_runner(monkeypatch):
|
||||||
doctest.DocTestRunner.__init__(self)
|
import doctest
|
||||||
|
|
||||||
def report_failure(self, out, test, example, got):
|
class MockedPdb:
|
||||||
raise AssertionError(
|
def __init__(self, out):
|
||||||
"'{}' evaluates to '{}', not '{}'".format(
|
pass
|
||||||
example.source.strip(), got.strip(), example.want.strip()
|
|
||||||
|
def set_trace(self):
|
||||||
|
raise NotImplementedError("not used")
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_continue(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr("doctest._OutputRedirectingPdb", MockedPdb)
|
||||||
|
|
||||||
|
class MyDocTestRunner(doctest.DocTestRunner):
|
||||||
|
def report_failure(self, out, test, example, got):
|
||||||
|
raise AssertionError(
|
||||||
|
"'{}' evaluates to '{}', not '{}'".format(
|
||||||
|
example.source.strip(), got.strip(), example.want.strip()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
return MyDocTestRunner()
|
||||||
|
|
||||||
|
|
||||||
class TestApprox:
|
class TestApprox:
|
||||||
@pytest.fixture
|
def test_repr_string(self):
|
||||||
def plus_minus(self):
|
assert repr(approx(1.0)) == "1.0 ± 1.0e-06"
|
||||||
return "\u00b1"
|
assert repr(approx([1.0, 2.0])) == "approx([1.0 ± 1.0e-06, 2.0 ± 2.0e-06])"
|
||||||
|
assert repr(approx((1.0, 2.0))) == "approx((1.0 ± 1.0e-06, 2.0 ± 2.0e-06))"
|
||||||
def test_repr_string(self, plus_minus):
|
|
||||||
tol1, tol2, infr = "1.0e-06", "2.0e-06", "inf"
|
|
||||||
assert repr(approx(1.0)) == "1.0 {pm} {tol1}".format(pm=plus_minus, tol1=tol1)
|
|
||||||
assert repr(
|
|
||||||
approx([1.0, 2.0])
|
|
||||||
) == "approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])".format(
|
|
||||||
pm=plus_minus, tol1=tol1, tol2=tol2
|
|
||||||
)
|
|
||||||
assert repr(
|
|
||||||
approx((1.0, 2.0))
|
|
||||||
) == "approx((1.0 {pm} {tol1}, 2.0 {pm} {tol2}))".format(
|
|
||||||
pm=plus_minus, tol1=tol1, tol2=tol2
|
|
||||||
)
|
|
||||||
assert repr(approx(inf)) == "inf"
|
assert repr(approx(inf)) == "inf"
|
||||||
assert repr(approx(1.0, rel=nan)) == "1.0 {pm} ???".format(pm=plus_minus)
|
assert repr(approx(1.0, rel=nan)) == "1.0 ± ???"
|
||||||
assert repr(approx(1.0, rel=inf)) == "1.0 {pm} {infr}".format(
|
assert repr(approx(1.0, rel=inf)) == "1.0 ± inf"
|
||||||
pm=plus_minus, infr=infr
|
|
||||||
)
|
|
||||||
assert repr(approx(1.0j, rel=inf)) == "1j"
|
|
||||||
|
|
||||||
# Dictionaries aren't ordered, so we need to check both orders.
|
# Dictionaries aren't ordered, so we need to check both orders.
|
||||||
assert repr(approx({"a": 1.0, "b": 2.0})) in (
|
assert repr(approx({"a": 1.0, "b": 2.0})) in (
|
||||||
"approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format(
|
"approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})",
|
||||||
pm=plus_minus, tol1=tol1, tol2=tol2
|
"approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})",
|
||||||
),
|
|
||||||
"approx({{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}})".format(
|
|
||||||
pm=plus_minus, tol1=tol1, tol2=tol2
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_repr_complex_numbers(self):
|
||||||
|
assert repr(approx(inf + 1j)) == "(inf+1j)"
|
||||||
|
assert repr(approx(1.0j, rel=inf)) == "1j ± inf"
|
||||||
|
|
||||||
|
# can't compute a sensible tolerance
|
||||||
|
assert repr(approx(nan + 1j)) == "(nan+1j) ± ???"
|
||||||
|
|
||||||
|
assert repr(approx(1.0j)) == "1j ± 1.0e-06 ∠ ±180°"
|
||||||
|
|
||||||
|
# relative tolerance is scaled to |3+4j| = 5
|
||||||
|
assert repr(approx(3 + 4 * 1j)) == "(3+4j) ± 5.0e-06 ∠ ±180°"
|
||||||
|
|
||||||
|
# absolute tolerance is not scaled
|
||||||
|
assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 2.0e-02 ∠ ±180°"
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"value, repr_string",
|
"value, expected_repr_string",
|
||||||
[
|
[
|
||||||
(5.0, "approx(5.0 {pm} 5.0e-06)"),
|
(5.0, "approx(5.0 ± 5.0e-06)"),
|
||||||
([5.0], "approx([5.0 {pm} 5.0e-06])"),
|
([5.0], "approx([5.0 ± 5.0e-06])"),
|
||||||
([[5.0]], "approx([[5.0 {pm} 5.0e-06]])"),
|
([[5.0]], "approx([[5.0 ± 5.0e-06]])"),
|
||||||
([[5.0, 6.0]], "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"),
|
([[5.0, 6.0]], "approx([[5.0 ± 5.0e-06, 6.0 ± 6.0e-06]])"),
|
||||||
([[5.0], [6.0]], "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])"),
|
([[5.0], [6.0]], "approx([[5.0 ± 5.0e-06], [6.0 ± 6.0e-06]])"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_repr_nd_array(self, plus_minus, value, repr_string):
|
def test_repr_nd_array(self, value, expected_repr_string):
|
||||||
"""Make sure that arrays of all different dimensions are repr'd correctly."""
|
"""Make sure that arrays of all different dimensions are repr'd correctly."""
|
||||||
np = pytest.importorskip("numpy")
|
np = pytest.importorskip("numpy")
|
||||||
np_array = np.array(value)
|
np_array = np.array(value)
|
||||||
assert repr(approx(np_array)) == repr_string.format(pm=plus_minus)
|
assert repr(approx(np_array)) == expected_repr_string
|
||||||
|
|
||||||
def test_operator_overloading(self):
|
def test_operator_overloading(self):
|
||||||
assert 1 == approx(1, rel=1e-6, abs=1e-12)
|
assert 1 == approx(1, rel=1e-6, abs=1e-12)
|
||||||
|
|
@ -416,13 +428,14 @@ class TestApprox:
|
||||||
assert a12 != approx(a21)
|
assert a12 != approx(a21)
|
||||||
assert a21 != approx(a12)
|
assert a21 != approx(a12)
|
||||||
|
|
||||||
def test_doctests(self):
|
def test_doctests(self, mocked_doctest_runner):
|
||||||
|
import doctest
|
||||||
|
|
||||||
parser = doctest.DocTestParser()
|
parser = doctest.DocTestParser()
|
||||||
test = parser.get_doctest(
|
test = parser.get_doctest(
|
||||||
approx.__doc__, {"approx": approx}, approx.__name__, None, None
|
approx.__doc__, {"approx": approx}, approx.__name__, None, None
|
||||||
)
|
)
|
||||||
runner = MyDocTestRunner()
|
mocked_doctest_runner.run(test)
|
||||||
runner.run(test)
|
|
||||||
|
|
||||||
def test_unicode_plus_minus(self, testdir):
|
def test_unicode_plus_minus(self, testdir):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1139,7 +1139,7 @@ def test_unorderable_types(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "TypeError" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*TypeError*")
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1167,7 +1167,7 @@ def test_dont_collect_non_function_callable(testdir):
|
||||||
[
|
[
|
||||||
"*collected 1 item*",
|
"*collected 1 item*",
|
||||||
"*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*",
|
"*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*",
|
||||||
"*1 passed, 1 warnings in *",
|
"*1 passed, 1 warning in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,7 @@ class TestFillFixtures:
|
||||||
"*1 error*",
|
"*1 error*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "INTERNAL" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNAL*")
|
||||||
|
|
||||||
def test_fixture_excinfo_leak(self, testdir):
|
def test_fixture_excinfo_leak(self, testdir):
|
||||||
# on python2 sys.excinfo would leak into fixture executions
|
# on python2 sys.excinfo would leak into fixture executions
|
||||||
|
|
@ -503,7 +503,7 @@ class TestRequestBasic:
|
||||||
assert repr(req).find(req.function.__name__) != -1
|
assert repr(req).find(req.function.__name__) != -1
|
||||||
|
|
||||||
def test_request_attributes_method(self, testdir):
|
def test_request_attributes_method(self, testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
class TestB(object):
|
class TestB(object):
|
||||||
|
|
@ -531,7 +531,7 @@ class TestRequestBasic:
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
item1, = testdir.genitems([modcol])
|
(item1,) = testdir.genitems([modcol])
|
||||||
assert item1.name == "test_method"
|
assert item1.name == "test_method"
|
||||||
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
|
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
|
||||||
assert len(arg2fixturedefs) == 1
|
assert len(arg2fixturedefs) == 1
|
||||||
|
|
@ -781,7 +781,7 @@ class TestRequestBasic:
|
||||||
|
|
||||||
def test_request_getmodulepath(self, testdir):
|
def test_request_getmodulepath(self, testdir):
|
||||||
modcol = testdir.getmodulecol("def test_somefunc(): pass")
|
modcol = testdir.getmodulecol("def test_somefunc(): pass")
|
||||||
item, = testdir.genitems([modcol])
|
(item,) = testdir.genitems([modcol])
|
||||||
req = fixtures.FixtureRequest(item)
|
req = fixtures.FixtureRequest(item)
|
||||||
assert req.fspath == modcol.fspath
|
assert req.fspath == modcol.fspath
|
||||||
|
|
||||||
|
|
@ -2647,7 +2647,7 @@ class TestFixtureMarker:
|
||||||
*3 passed*
|
*3 passed*
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
assert "error" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*error*")
|
||||||
|
|
||||||
def test_fixture_finalizer(self, testdir):
|
def test_fixture_finalizer(self, testdir):
|
||||||
testdir.makeconftest(
|
testdir.makeconftest(
|
||||||
|
|
@ -3081,7 +3081,7 @@ class TestErrors:
|
||||||
*KeyError*
|
*KeyError*
|
||||||
*ERROR*teardown*test_2*
|
*ERROR*teardown*test_2*
|
||||||
*KeyError*
|
*KeyError*
|
||||||
*3 pass*2 error*
|
*3 pass*2 errors*
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3151,7 +3151,7 @@ class TestShowFixtures:
|
||||||
*hello world*
|
*hello world*
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
assert "arg0" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*arg0*")
|
||||||
|
|
||||||
@pytest.mark.parametrize("testmod", [True, False])
|
@pytest.mark.parametrize("testmod", [True, False])
|
||||||
def test_show_fixtures_conftest(self, testdir, testmod):
|
def test_show_fixtures_conftest(self, testdir, testmod):
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from _pytest import python
|
||||||
|
|
||||||
|
|
||||||
class TestMetafunc:
|
class TestMetafunc:
|
||||||
def Metafunc(self, func, config=None):
|
def Metafunc(self, func, config=None) -> python.Metafunc:
|
||||||
# the unit tests of this class check if things work correctly
|
# the unit tests of this class check if things work correctly
|
||||||
# on the funcarg level, so we don't need a full blown
|
# on the funcarg level, so we don't need a full blown
|
||||||
# initialization
|
# initialization
|
||||||
|
|
@ -23,7 +23,7 @@ class TestMetafunc:
|
||||||
self.names_closure = names
|
self.names_closure = names
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class DefinitionMock:
|
class DefinitionMock(python.FunctionDefinition):
|
||||||
obj = attr.ib()
|
obj = attr.ib()
|
||||||
|
|
||||||
names = fixtures.getfuncargnames(func)
|
names = fixtures.getfuncargnames(func)
|
||||||
|
|
@ -1323,25 +1323,29 @@ class TestMetafuncFunctional:
|
||||||
reprec = testdir.runpytest()
|
reprec = testdir.runpytest()
|
||||||
reprec.assert_outcomes(passed=4)
|
reprec.assert_outcomes(passed=4)
|
||||||
|
|
||||||
@pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"])
|
def test_parametrize_misspelling(self, testdir):
|
||||||
def test_parametrize_misspelling(self, testdir, attr):
|
|
||||||
"""#463"""
|
"""#463"""
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@pytest.mark.{}("x", range(2))
|
@pytest.mark.parametrise("x", range(2))
|
||||||
def test_foo(x):
|
def test_foo(x):
|
||||||
pass
|
pass
|
||||||
""".format(
|
"""
|
||||||
attr
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--collectonly")
|
result = testdir.runpytest("--collectonly")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"test_foo has '{}' mark, spelling should be 'parametrize'".format(attr),
|
"collected 0 items / 1 error",
|
||||||
"*1 error in*",
|
"",
|
||||||
|
"*= ERRORS =*",
|
||||||
|
"*_ ERROR collecting test_parametrize_misspelling.py _*",
|
||||||
|
"test_parametrize_misspelling.py:3: in <module>",
|
||||||
|
' @pytest.mark.parametrise("x", range(2))',
|
||||||
|
"E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?",
|
||||||
|
"*! Interrupted: 1 error during collection !*",
|
||||||
|
"*= 1 error in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1551,27 +1555,6 @@ class TestMarkersWithParametrization:
|
||||||
assert len(skipped) == 0
|
assert len(skipped) == 0
|
||||||
assert len(fail) == 0
|
assert len(fail) == 0
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="is this important to support??")
|
|
||||||
def test_nested_marks(self, testdir):
|
|
||||||
s = """
|
|
||||||
import pytest
|
|
||||||
mastermark = pytest.mark.foo(pytest.mark.bar)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("n", "expected"), [
|
|
||||||
(1, 2),
|
|
||||||
mastermark((1, 3)),
|
|
||||||
(2, 3),
|
|
||||||
])
|
|
||||||
def test_increment(n, expected):
|
|
||||||
assert n + 1 == expected
|
|
||||||
"""
|
|
||||||
items = testdir.getitems(s)
|
|
||||||
assert len(items) == 3
|
|
||||||
for mark in ["foo", "bar"]:
|
|
||||||
assert mark not in items[0].keywords
|
|
||||||
assert mark in items[1].keywords
|
|
||||||
assert mark not in items[2].keywords
|
|
||||||
|
|
||||||
def test_simple_xfail(self, testdir):
|
def test_simple_xfail(self, testdir):
|
||||||
s = """
|
s = """
|
||||||
import pytest
|
import pytest
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ class TestRaises:
|
||||||
with pytest.raises(AssertionError) as excinfo:
|
with pytest.raises(AssertionError) as excinfo:
|
||||||
with pytest.raises(AssertionError, match="'foo"):
|
with pytest.raises(AssertionError, match="'foo"):
|
||||||
raise AssertionError("'bar")
|
raise AssertionError("'bar")
|
||||||
msg, = excinfo.value.args
|
(msg,) = excinfo.value.args
|
||||||
assert msg == 'Pattern "\'foo" not found in "\'bar"'
|
assert msg == 'Pattern "\'foo" not found in "\'bar"'
|
||||||
|
|
||||||
def test_raises_match_wrong_type(self):
|
def test_raises_match_wrong_type(self):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
def test_no_items_should_not_show_output(testdir):
|
def test_no_items_should_not_show_output(testdir):
|
||||||
result = testdir.runpytest("--fixtures-per-test")
|
result = testdir.runpytest("--fixtures-per-test")
|
||||||
assert "fixtures used by" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*fixtures used by*")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ def test_fixtures_in_module(testdir):
|
||||||
" arg1 docstring",
|
" arg1 docstring",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "_arg0" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*_arg0*")
|
||||||
|
|
||||||
|
|
||||||
def test_fixtures_in_conftest(testdir):
|
def test_fixtures_in_conftest(testdir):
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,11 @@ from _pytest.assertion import util
|
||||||
from _pytest.compat import ATTRS_EQ_FIELD
|
from _pytest.compat import ATTRS_EQ_FIELD
|
||||||
|
|
||||||
|
|
||||||
def mock_config():
|
def mock_config(verbose=0):
|
||||||
class Config:
|
class Config:
|
||||||
verbose = False
|
|
||||||
|
|
||||||
def getoption(self, name):
|
def getoption(self, name):
|
||||||
if name == "verbose":
|
if name == "verbose":
|
||||||
return self.verbose
|
return verbose
|
||||||
raise KeyError("Not mocked out: %s" % name)
|
raise KeyError("Not mocked out: %s" % name)
|
||||||
|
|
||||||
return Config()
|
return Config()
|
||||||
|
|
@ -72,7 +70,14 @@ class TestImportHookInstallation:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
result.stdout.fnmatch_lines(["*assert 1 == 0*"])
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"E * AssertionError: ([[][]], [[][]], [[]<TestReport *>[]])*",
|
||||||
|
"E * assert"
|
||||||
|
" {'failed': 1, 'passed': 0, 'skipped': 0} =="
|
||||||
|
" {'failed': 0, 'passed': 1, 'skipped': 0}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
||||||
def test_pytest_plugins_rewrite(self, testdir, mode):
|
def test_pytest_plugins_rewrite(self, testdir, mode):
|
||||||
|
|
@ -296,9 +301,8 @@ class TestBinReprIntegration:
|
||||||
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])
|
result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"])
|
||||||
|
|
||||||
|
|
||||||
def callequal(left, right, verbose=False):
|
def callequal(left, right, verbose=0):
|
||||||
config = mock_config()
|
config = mock_config(verbose=verbose)
|
||||||
config.verbose = verbose
|
|
||||||
return plugin.pytest_assertrepr_compare(config, "==", left, right)
|
return plugin.pytest_assertrepr_compare(config, "==", left, right)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -322,7 +326,7 @@ class TestAssert_reprcompare:
|
||||||
assert "a" * 50 not in line
|
assert "a" * 50 not in line
|
||||||
|
|
||||||
def test_text_skipping_verbose(self):
|
def test_text_skipping_verbose(self):
|
||||||
lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=True)
|
lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=1)
|
||||||
assert "- " + "a" * 50 + "spam" in lines
|
assert "- " + "a" * 50 + "spam" in lines
|
||||||
assert "+ " + "a" * 50 + "eggs" in lines
|
assert "+ " + "a" * 50 + "eggs" in lines
|
||||||
|
|
||||||
|
|
@ -345,7 +349,7 @@ class TestAssert_reprcompare:
|
||||||
|
|
||||||
def test_bytes_diff_verbose(self):
|
def test_bytes_diff_verbose(self):
|
||||||
"""Check special handling for bytes diff (#5260)"""
|
"""Check special handling for bytes diff (#5260)"""
|
||||||
diff = callequal(b"spam", b"eggs", verbose=True)
|
diff = callequal(b"spam", b"eggs", verbose=1)
|
||||||
assert diff == [
|
assert diff == [
|
||||||
"b'spam' == b'eggs'",
|
"b'spam' == b'eggs'",
|
||||||
"At index 0 diff: b's' != b'e'",
|
"At index 0 diff: b's' != b'e'",
|
||||||
|
|
@ -361,7 +365,7 @@ class TestAssert_reprcompare:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
["left", "right", "expected"],
|
["left", "right", "expected"],
|
||||||
[
|
[
|
||||||
(
|
pytest.param(
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[0, 2],
|
[0, 2],
|
||||||
"""
|
"""
|
||||||
|
|
@ -371,8 +375,9 @@ class TestAssert_reprcompare:
|
||||||
+ [0, 2]
|
+ [0, 2]
|
||||||
? ^
|
? ^
|
||||||
""",
|
""",
|
||||||
|
id="lists",
|
||||||
),
|
),
|
||||||
(
|
pytest.param(
|
||||||
{0: 1},
|
{0: 1},
|
||||||
{0: 2},
|
{0: 2},
|
||||||
"""
|
"""
|
||||||
|
|
@ -382,8 +387,9 @@ class TestAssert_reprcompare:
|
||||||
+ {0: 2}
|
+ {0: 2}
|
||||||
? ^
|
? ^
|
||||||
""",
|
""",
|
||||||
|
id="dicts",
|
||||||
),
|
),
|
||||||
(
|
pytest.param(
|
||||||
{0, 1},
|
{0, 1},
|
||||||
{0, 2},
|
{0, 2},
|
||||||
"""
|
"""
|
||||||
|
|
@ -393,6 +399,7 @@ class TestAssert_reprcompare:
|
||||||
+ {0, 2}
|
+ {0, 2}
|
||||||
? ^
|
? ^
|
||||||
""",
|
""",
|
||||||
|
id="sets",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -402,9 +409,9 @@ class TestAssert_reprcompare:
|
||||||
When verbose is False, then just a -v notice to get the diff is rendered,
|
When verbose is False, then just a -v notice to get the diff is rendered,
|
||||||
when verbose is True, then ndiff of the pprint is returned.
|
when verbose is True, then ndiff of the pprint is returned.
|
||||||
"""
|
"""
|
||||||
expl = callequal(left, right, verbose=False)
|
expl = callequal(left, right, verbose=0)
|
||||||
assert expl[-1] == "Use -v to get the full diff"
|
assert expl[-1] == "Use -v to get the full diff"
|
||||||
expl = "\n".join(callequal(left, right, verbose=True))
|
expl = "\n".join(callequal(left, right, verbose=1))
|
||||||
assert expl.endswith(textwrap.dedent(expected).strip())
|
assert expl.endswith(textwrap.dedent(expected).strip())
|
||||||
|
|
||||||
def test_list_different_lengths(self):
|
def test_list_different_lengths(self):
|
||||||
|
|
@ -413,6 +420,113 @@ class TestAssert_reprcompare:
|
||||||
expl = callequal([0, 1, 2], [0, 1])
|
expl = callequal([0, 1, 2], [0, 1])
|
||||||
assert len(expl) > 1
|
assert len(expl) > 1
|
||||||
|
|
||||||
|
def test_list_wrap_for_multiple_lines(self):
|
||||||
|
long_d = "d" * 80
|
||||||
|
l1 = ["a", "b", "c"]
|
||||||
|
l2 = ["a", "b", "c", long_d]
|
||||||
|
diff = callequal(l1, l2, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']",
|
||||||
|
"Right contains one more item: '" + long_d + "'",
|
||||||
|
"Full diff:",
|
||||||
|
" [",
|
||||||
|
" 'a',",
|
||||||
|
" 'b',",
|
||||||
|
" 'c',",
|
||||||
|
"+ '" + long_d + "',",
|
||||||
|
" ]",
|
||||||
|
]
|
||||||
|
|
||||||
|
diff = callequal(l2, l1, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']",
|
||||||
|
"Left contains one more item: '" + long_d + "'",
|
||||||
|
"Full diff:",
|
||||||
|
" [",
|
||||||
|
" 'a',",
|
||||||
|
" 'b',",
|
||||||
|
" 'c',",
|
||||||
|
"- '" + long_d + "',",
|
||||||
|
" ]",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_list_wrap_for_width_rewrap_same_length(self):
|
||||||
|
long_a = "a" * 30
|
||||||
|
long_b = "b" * 30
|
||||||
|
long_c = "c" * 30
|
||||||
|
l1 = [long_a, long_b, long_c]
|
||||||
|
l2 = [long_b, long_c, long_a]
|
||||||
|
diff = callequal(l1, l2, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']",
|
||||||
|
"At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'",
|
||||||
|
"Full diff:",
|
||||||
|
" [",
|
||||||
|
"- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
|
||||||
|
" 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',",
|
||||||
|
" 'cccccccccccccccccccccccccccccc',",
|
||||||
|
"+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',",
|
||||||
|
" ]",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_list_dont_wrap_strings(self):
|
||||||
|
long_a = "a" * 10
|
||||||
|
l1 = ["a"] + [long_a for _ in range(0, 7)]
|
||||||
|
l2 = ["should not get wrapped"]
|
||||||
|
diff = callequal(l1, l2, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']",
|
||||||
|
"At index 0 diff: 'a' != 'should not get wrapped'",
|
||||||
|
"Left contains 7 more items, first extra item: 'aaaaaaaaaa'",
|
||||||
|
"Full diff:",
|
||||||
|
" [",
|
||||||
|
"+ 'should not get wrapped',",
|
||||||
|
"- 'a',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
"- 'aaaaaaaaaa',",
|
||||||
|
" ]",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_dict_wrap(self):
|
||||||
|
d1 = {"common": 1, "env": {"env1": 1}}
|
||||||
|
d2 = {"common": 1, "env": {"env1": 1, "env2": 2}}
|
||||||
|
|
||||||
|
diff = callequal(d1, d2, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"{'common': 1,...: {'env1': 1}} == {'common': 1,...1, 'env2': 2}}",
|
||||||
|
"Omitting 1 identical items, use -vv to show",
|
||||||
|
"Differing items:",
|
||||||
|
"{'env': {'env1': 1}} != {'env': {'env1': 1, 'env2': 2}}",
|
||||||
|
"Full diff:",
|
||||||
|
"- {'common': 1, 'env': {'env1': 1}}",
|
||||||
|
"+ {'common': 1, 'env': {'env1': 1, 'env2': 2}}",
|
||||||
|
"? +++++++++++",
|
||||||
|
]
|
||||||
|
|
||||||
|
long_a = "a" * 80
|
||||||
|
sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}}
|
||||||
|
d1 = {"env": {"sub": sub}}
|
||||||
|
d2 = {"env": {"sub": sub}, "new": 1}
|
||||||
|
diff = callequal(d1, d2, verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}",
|
||||||
|
"Omitting 1 identical items, use -vv to show",
|
||||||
|
"Right contains 1 more item:",
|
||||||
|
"{'new': 1}",
|
||||||
|
"Full diff:",
|
||||||
|
" {",
|
||||||
|
" 'env': {'sub': {'long_a': '" + long_a + "',",
|
||||||
|
" 'sub1': {'long_a': 'substring that gets wrapped substring '",
|
||||||
|
" 'that gets wrapped '}}},",
|
||||||
|
"+ 'new': 1,",
|
||||||
|
" }",
|
||||||
|
]
|
||||||
|
|
||||||
def test_dict(self):
|
def test_dict(self):
|
||||||
expl = callequal({"a": 0}, {"a": 1})
|
expl = callequal({"a": 0}, {"a": 1})
|
||||||
assert len(expl) > 1
|
assert len(expl) > 1
|
||||||
|
|
@ -1034,7 +1148,7 @@ def test_assertion_options(testdir):
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "3 == 4" in result.stdout.str()
|
assert "3 == 4" in result.stdout.str()
|
||||||
result = testdir.runpytest_subprocess("--assert=plain")
|
result = testdir.runpytest_subprocess("--assert=plain")
|
||||||
assert "3 == 4" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*3 == 4*")
|
||||||
|
|
||||||
|
|
||||||
def test_triple_quoted_string_issue113(testdir):
|
def test_triple_quoted_string_issue113(testdir):
|
||||||
|
|
@ -1046,7 +1160,7 @@ def test_triple_quoted_string_issue113(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--fulltrace")
|
result = testdir.runpytest("--fulltrace")
|
||||||
result.stdout.fnmatch_lines(["*1 failed*"])
|
result.stdout.fnmatch_lines(["*1 failed*"])
|
||||||
assert "SyntaxError" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*SyntaxError*")
|
||||||
|
|
||||||
|
|
||||||
def test_traceback_failure(testdir):
|
def test_traceback_failure(testdir):
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@ import pytest
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
from _pytest.assertion.rewrite import _get_assertion_exprs
|
from _pytest.assertion.rewrite import _get_assertion_exprs
|
||||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||||
|
from _pytest.assertion.rewrite import get_cache_dir
|
||||||
|
from _pytest.assertion.rewrite import PYC_TAIL
|
||||||
from _pytest.assertion.rewrite import PYTEST_TAG
|
from _pytest.assertion.rewrite import PYTEST_TAG
|
||||||
from _pytest.assertion.rewrite import rewrite_asserts
|
from _pytest.assertion.rewrite import rewrite_asserts
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def setup_module(mod):
|
def setup_module(mod):
|
||||||
|
|
@ -119,7 +122,7 @@ class TestAssertionRewrite:
|
||||||
}
|
}
|
||||||
testdir.makepyfile(**contents)
|
testdir.makepyfile(**contents)
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
assert "warnings" not in "".join(result.outlines)
|
assert "warning" not in "".join(result.outlines)
|
||||||
|
|
||||||
def test_rewrites_plugin_as_a_package(self, testdir):
|
def test_rewrites_plugin_as_a_package(self, testdir):
|
||||||
pkgdir = testdir.mkpydir("plugin")
|
pkgdir = testdir.mkpydir("plugin")
|
||||||
|
|
@ -190,11 +193,12 @@ class TestAssertionRewrite:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
msg = getmsg(f, {"cls": X}).splitlines()
|
msg = getmsg(f, {"cls": X}).splitlines()
|
||||||
if verbose > 0:
|
if verbose > 1:
|
||||||
|
assert msg == ["assert {!r} == 42".format(X), " -{!r}".format(X), " +42"]
|
||||||
|
elif verbose > 0:
|
||||||
assert msg == [
|
assert msg == [
|
||||||
"assert <class 'test_...e.<locals>.X'> == 42",
|
"assert <class 'test_...e.<locals>.X'> == 42",
|
||||||
" -<class 'test_assertrewrite.TestAssertionRewrite.test_name.<locals>.X'>",
|
" -{!r}".format(X),
|
||||||
" +42",
|
" +42",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
|
@ -206,9 +210,17 @@ class TestAssertionRewrite:
|
||||||
def f():
|
def f():
|
||||||
assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B"
|
assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B"
|
||||||
|
|
||||||
assert getmsg(f).splitlines()[0] == (
|
msg = getmsg(f).splitlines()[0]
|
||||||
"assert '123456789012...901234567890A' == '123456789012...901234567890B'"
|
if request.config.getoption("verbose") > 1:
|
||||||
)
|
assert msg == (
|
||||||
|
"assert '12345678901234567890123456789012345678901234567890A' "
|
||||||
|
"== '12345678901234567890123456789012345678901234567890B'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert msg == (
|
||||||
|
"assert '123456789012...901234567890A' "
|
||||||
|
"== '123456789012...901234567890B'"
|
||||||
|
)
|
||||||
|
|
||||||
def test_dont_rewrite_if_hasattr_fails(self, request):
|
def test_dont_rewrite_if_hasattr_fails(self, request):
|
||||||
class Y:
|
class Y:
|
||||||
|
|
@ -914,7 +926,7 @@ def test_rewritten():
|
||||||
testdir.chdir()
|
testdir.chdir()
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
|
result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
|
||||||
assert "pytest-warning summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*pytest-warning summary*")
|
||||||
|
|
||||||
def test_rewrite_warning_using_pytest_plugins_env_var(self, testdir, monkeypatch):
|
def test_rewrite_warning_using_pytest_plugins_env_var(self, testdir, monkeypatch):
|
||||||
monkeypatch.setenv("PYTEST_PLUGINS", "plugin")
|
monkeypatch.setenv("PYTEST_PLUGINS", "plugin")
|
||||||
|
|
@ -932,7 +944,7 @@ def test_rewritten():
|
||||||
testdir.chdir()
|
testdir.chdir()
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
|
result.stdout.fnmatch_lines(["*= 1 passed in *=*"])
|
||||||
assert "pytest-warning summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*pytest-warning summary*")
|
||||||
|
|
||||||
|
|
||||||
class TestAssertionRewriteHookDetails:
|
class TestAssertionRewriteHookDetails:
|
||||||
|
|
@ -947,24 +959,34 @@ class TestAssertionRewriteHookDetails:
|
||||||
def test_write_pyc(self, testdir, tmpdir, monkeypatch):
|
def test_write_pyc(self, testdir, tmpdir, monkeypatch):
|
||||||
from _pytest.assertion.rewrite import _write_pyc
|
from _pytest.assertion.rewrite import _write_pyc
|
||||||
from _pytest.assertion import AssertionState
|
from _pytest.assertion import AssertionState
|
||||||
import atomicwrites
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
config = testdir.parseconfig([])
|
config = testdir.parseconfig([])
|
||||||
state = AssertionState(config, "rewrite")
|
state = AssertionState(config, "rewrite")
|
||||||
source_path = tmpdir.ensure("source.py")
|
source_path = str(tmpdir.ensure("source.py"))
|
||||||
pycpath = tmpdir.join("pyc").strpath
|
pycpath = tmpdir.join("pyc").strpath
|
||||||
assert _write_pyc(state, [1], os.stat(source_path.strpath), pycpath)
|
assert _write_pyc(state, [1], os.stat(source_path), pycpath)
|
||||||
|
|
||||||
@contextmanager
|
if sys.platform == "win32":
|
||||||
def atomic_write_failed(fn, mode="r", overwrite=False):
|
from contextlib import contextmanager
|
||||||
e = IOError()
|
|
||||||
e.errno = 10
|
|
||||||
raise e
|
|
||||||
yield
|
|
||||||
|
|
||||||
monkeypatch.setattr(atomicwrites, "atomic_write", atomic_write_failed)
|
@contextmanager
|
||||||
assert not _write_pyc(state, [1], source_path.stat(), pycpath)
|
def atomic_write_failed(fn, mode="r", overwrite=False):
|
||||||
|
e = IOError()
|
||||||
|
e.errno = 10
|
||||||
|
raise e
|
||||||
|
yield
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
|
||||||
|
def raise_ioerror(*args):
|
||||||
|
raise IOError()
|
||||||
|
|
||||||
|
monkeypatch.setattr("os.rename", raise_ioerror)
|
||||||
|
|
||||||
|
assert not _write_pyc(state, [1], os.stat(source_path), pycpath)
|
||||||
|
|
||||||
def test_resources_provider_for_loader(self, testdir):
|
def test_resources_provider_for_loader(self, testdir):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1124,7 +1146,7 @@ def test_issue731(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "unbalanced braces" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*unbalanced braces*")
|
||||||
|
|
||||||
|
|
||||||
class TestIssue925:
|
class TestIssue925:
|
||||||
|
|
@ -1542,41 +1564,97 @@ def test_get_assertion_exprs(src, expected):
|
||||||
assert _get_assertion_exprs(src) == expected
|
assert _get_assertion_exprs(src) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_try_mkdir(monkeypatch, tmp_path):
|
def test_try_makedirs(monkeypatch, tmp_path):
|
||||||
from _pytest.assertion.rewrite import try_mkdir
|
from _pytest.assertion.rewrite import try_makedirs
|
||||||
|
|
||||||
p = tmp_path / "foo"
|
p = tmp_path / "foo"
|
||||||
|
|
||||||
# create
|
# create
|
||||||
assert try_mkdir(str(p))
|
assert try_makedirs(str(p))
|
||||||
assert p.is_dir()
|
assert p.is_dir()
|
||||||
|
|
||||||
# already exist
|
# already exist
|
||||||
assert try_mkdir(str(p))
|
assert try_makedirs(str(p))
|
||||||
|
|
||||||
# monkeypatch to simulate all error situations
|
# monkeypatch to simulate all error situations
|
||||||
def fake_mkdir(p, *, exc):
|
def fake_mkdir(p, exist_ok=False, *, exc):
|
||||||
assert isinstance(p, str)
|
assert isinstance(p, str)
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=FileNotFoundError()))
|
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError()))
|
||||||
assert not try_mkdir(str(p))
|
assert not try_makedirs(str(p))
|
||||||
|
|
||||||
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=NotADirectoryError()))
|
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=NotADirectoryError()))
|
||||||
assert not try_mkdir(str(p))
|
assert not try_makedirs(str(p))
|
||||||
|
|
||||||
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=PermissionError()))
|
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=PermissionError()))
|
||||||
assert not try_mkdir(str(p))
|
assert not try_makedirs(str(p))
|
||||||
|
|
||||||
err = OSError()
|
err = OSError()
|
||||||
err.errno = errno.EROFS
|
err.errno = errno.EROFS
|
||||||
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err))
|
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
|
||||||
assert not try_mkdir(str(p))
|
assert not try_makedirs(str(p))
|
||||||
|
|
||||||
# unhandled OSError should raise
|
# unhandled OSError should raise
|
||||||
err = OSError()
|
err = OSError()
|
||||||
err.errno = errno.ECHILD
|
err.errno = errno.ECHILD
|
||||||
monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err))
|
monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err))
|
||||||
with pytest.raises(OSError) as exc_info:
|
with pytest.raises(OSError) as exc_info:
|
||||||
try_mkdir(str(p))
|
try_makedirs(str(p))
|
||||||
assert exc_info.value.errno == errno.ECHILD
|
assert exc_info.value.errno == errno.ECHILD
|
||||||
|
|
||||||
|
|
||||||
|
class TestPyCacheDir:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"prefix, source, expected",
|
||||||
|
[
|
||||||
|
("c:/tmp/pycs", "d:/projects/src/foo.py", "c:/tmp/pycs/projects/src"),
|
||||||
|
(None, "d:/projects/src/foo.py", "d:/projects/src/__pycache__"),
|
||||||
|
("/tmp/pycs", "/home/projects/src/foo.py", "/tmp/pycs/home/projects/src"),
|
||||||
|
(None, "/home/projects/src/foo.py", "/home/projects/src/__pycache__"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_cache_dir(self, monkeypatch, prefix, source, expected):
|
||||||
|
if prefix:
|
||||||
|
if sys.version_info < (3, 8):
|
||||||
|
pytest.skip("pycache_prefix not available in py<38")
|
||||||
|
monkeypatch.setattr(sys, "pycache_prefix", prefix)
|
||||||
|
|
||||||
|
assert get_cache_dir(Path(source)) == Path(expected)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 8), reason="pycache_prefix not available in py<38"
|
||||||
|
)
|
||||||
|
def test_sys_pycache_prefix_integration(self, tmp_path, monkeypatch, testdir):
|
||||||
|
"""Integration test for sys.pycache_prefix (#4730)."""
|
||||||
|
pycache_prefix = tmp_path / "my/pycs"
|
||||||
|
monkeypatch.setattr(sys, "pycache_prefix", str(pycache_prefix))
|
||||||
|
monkeypatch.setattr(sys, "dont_write_bytecode", False)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
**{
|
||||||
|
"src/test_foo.py": """
|
||||||
|
import bar
|
||||||
|
def test_foo():
|
||||||
|
pass
|
||||||
|
""",
|
||||||
|
"src/bar/__init__.py": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
test_foo = Path(testdir.tmpdir) / "src/test_foo.py"
|
||||||
|
bar_init = Path(testdir.tmpdir) / "src/bar/__init__.py"
|
||||||
|
assert test_foo.is_file()
|
||||||
|
assert bar_init.is_file()
|
||||||
|
|
||||||
|
# test file: rewritten, custom pytest cache tag
|
||||||
|
test_foo_pyc = get_cache_dir(test_foo) / ("test_foo" + PYC_TAIL)
|
||||||
|
assert test_foo_pyc.is_file()
|
||||||
|
|
||||||
|
# normal file: not touched by pytest, normal cache tag
|
||||||
|
bar_init_pyc = get_cache_dir(bar_init) / "__init__.{cache_tag}.pyc".format(
|
||||||
|
cache_tag=sys.implementation.cache_tag
|
||||||
|
)
|
||||||
|
assert bar_init_pyc.is_file()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
|
@ -45,26 +45,35 @@ class TestNewAPI:
|
||||||
)
|
)
|
||||||
def test_cache_writefail_permissions(self, testdir):
|
def test_cache_writefail_permissions(self, testdir):
|
||||||
testdir.makeini("[pytest]")
|
testdir.makeini("[pytest]")
|
||||||
|
cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache"))
|
||||||
|
mode = os.stat(cache_dir)[stat.ST_MODE]
|
||||||
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
|
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
|
||||||
config = testdir.parseconfigure()
|
try:
|
||||||
cache = config.cache
|
config = testdir.parseconfigure()
|
||||||
cache.set("test/broken", [])
|
cache = config.cache
|
||||||
|
cache.set("test/broken", [])
|
||||||
|
finally:
|
||||||
|
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode)
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows")
|
@pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows")
|
||||||
@pytest.mark.filterwarnings("default")
|
@pytest.mark.filterwarnings(
|
||||||
def test_cache_failure_warns(self, testdir):
|
"ignore:could not create cache path:pytest.PytestWarning"
|
||||||
|
)
|
||||||
|
def test_cache_failure_warns(self, testdir, monkeypatch):
|
||||||
|
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
|
||||||
|
cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache"))
|
||||||
|
mode = os.stat(cache_dir)[stat.ST_MODE]
|
||||||
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
|
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0)
|
||||||
testdir.makepyfile(
|
try:
|
||||||
"""
|
testdir.makepyfile("def test_error(): raise Exception")
|
||||||
def test_error():
|
result = testdir.runpytest("-rw")
|
||||||
raise Exception
|
assert result.ret == 1
|
||||||
|
# warnings from nodeids, lastfailed, and stepwise
|
||||||
"""
|
result.stdout.fnmatch_lines(
|
||||||
)
|
["*could not create cache path*", "*3 warnings*"]
|
||||||
result = testdir.runpytest("-rw")
|
)
|
||||||
assert result.ret == 1
|
finally:
|
||||||
# warnings from nodeids, lastfailed, and stepwise
|
testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode)
|
||||||
result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"])
|
|
||||||
|
|
||||||
def test_config_cache(self, testdir):
|
def test_config_cache(self, testdir):
|
||||||
testdir.makeconftest(
|
testdir.makeconftest(
|
||||||
|
|
@ -163,12 +172,7 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory):
|
||||||
"test_cache_reportheader_external_abspath_abs"
|
"test_cache_reportheader_external_abspath_abs"
|
||||||
)
|
)
|
||||||
|
|
||||||
testdir.makepyfile(
|
testdir.makepyfile("def test_hello(): pass")
|
||||||
"""
|
|
||||||
def test_hello():
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
testdir.makeini(
|
testdir.makeini(
|
||||||
"""
|
"""
|
||||||
[pytest]
|
[pytest]
|
||||||
|
|
@ -177,7 +181,6 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory):
|
||||||
abscache=external_cache
|
abscache=external_cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = testdir.runpytest("-v")
|
result = testdir.runpytest("-v")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["cachedir: {abscache}".format(abscache=external_cache)]
|
["cachedir: {abscache}".format(abscache=external_cache)]
|
||||||
|
|
@ -238,36 +241,26 @@ def test_cache_show(testdir):
|
||||||
|
|
||||||
class TestLastFailed:
|
class TestLastFailed:
|
||||||
def test_lastfailed_usecase(self, testdir, monkeypatch):
|
def test_lastfailed_usecase(self, testdir, monkeypatch):
|
||||||
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
|
monkeypatch.setattr("sys.dont_write_bytecode", True)
|
||||||
p = testdir.makepyfile(
|
p = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_1():
|
def test_1(): assert 0
|
||||||
assert 0
|
def test_2(): assert 0
|
||||||
def test_2():
|
def test_3(): assert 1
|
||||||
assert 0
|
"""
|
||||||
def test_3():
|
|
||||||
assert 1
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest(str(p))
|
||||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||||
p.write(
|
p = testdir.makepyfile(
|
||||||
textwrap.dedent(
|
"""
|
||||||
"""\
|
def test_1(): assert 1
|
||||||
def test_1():
|
def test_2(): assert 1
|
||||||
assert 1
|
def test_3(): assert 0
|
||||||
|
"""
|
||||||
def test_2():
|
|
||||||
assert 1
|
|
||||||
|
|
||||||
def test_3():
|
|
||||||
assert 0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--lf")
|
result = testdir.runpytest(str(p), "--lf")
|
||||||
result.stdout.fnmatch_lines(["*2 passed*1 desel*"])
|
result.stdout.fnmatch_lines(["*2 passed*1 desel*"])
|
||||||
result = testdir.runpytest("--lf")
|
result = testdir.runpytest(str(p), "--lf")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"collected 3 items",
|
"collected 3 items",
|
||||||
|
|
@ -275,7 +268,7 @@ class TestLastFailed:
|
||||||
"*1 failed*2 passed*",
|
"*1 failed*2 passed*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--lf", "--cache-clear")
|
result = testdir.runpytest(str(p), "--lf", "--cache-clear")
|
||||||
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
|
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
|
||||||
|
|
||||||
# Run this again to make sure clear-cache is robust
|
# Run this again to make sure clear-cache is robust
|
||||||
|
|
@ -285,21 +278,9 @@ class TestLastFailed:
|
||||||
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
|
result.stdout.fnmatch_lines(["*1 failed*2 passed*"])
|
||||||
|
|
||||||
def test_failedfirst_order(self, testdir):
|
def test_failedfirst_order(self, testdir):
|
||||||
testdir.tmpdir.join("test_a.py").write(
|
testdir.makepyfile(
|
||||||
textwrap.dedent(
|
test_a="def test_always_passes(): pass",
|
||||||
"""\
|
test_b="def test_always_fails(): assert 0",
|
||||||
def test_always_passes():
|
|
||||||
assert 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
testdir.tmpdir.join("test_b.py").write(
|
|
||||||
textwrap.dedent(
|
|
||||||
"""\
|
|
||||||
def test_always_fails():
|
|
||||||
assert 0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
# Test order will be collection order; alphabetical
|
# Test order will be collection order; alphabetical
|
||||||
|
|
@ -310,16 +291,8 @@ class TestLastFailed:
|
||||||
|
|
||||||
def test_lastfailed_failedfirst_order(self, testdir):
|
def test_lastfailed_failedfirst_order(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
**{
|
test_a="def test_always_passes(): assert 1",
|
||||||
"test_a.py": """\
|
test_b="def test_always_fails(): assert 0",
|
||||||
def test_always_passes():
|
|
||||||
assert 1
|
|
||||||
""",
|
|
||||||
"test_b.py": """\
|
|
||||||
def test_always_fails():
|
|
||||||
assert 0
|
|
||||||
""",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
# Test order will be collection order; alphabetical
|
# Test order will be collection order; alphabetical
|
||||||
|
|
@ -327,21 +300,16 @@ class TestLastFailed:
|
||||||
result = testdir.runpytest("--lf", "--ff")
|
result = testdir.runpytest("--lf", "--ff")
|
||||||
# Test order will be failing tests firs
|
# Test order will be failing tests firs
|
||||||
result.stdout.fnmatch_lines(["test_b.py*"])
|
result.stdout.fnmatch_lines(["test_b.py*"])
|
||||||
assert "test_a.py" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_a.py*")
|
||||||
|
|
||||||
def test_lastfailed_difference_invocations(self, testdir, monkeypatch):
|
def test_lastfailed_difference_invocations(self, testdir, monkeypatch):
|
||||||
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
|
monkeypatch.setattr("sys.dont_write_bytecode", True)
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
test_a="""\
|
test_a="""
|
||||||
def test_a1():
|
def test_a1(): assert 0
|
||||||
assert 0
|
def test_a2(): assert 1
|
||||||
def test_a2():
|
|
||||||
assert 1
|
|
||||||
""",
|
|
||||||
test_b="""\
|
|
||||||
def test_b1():
|
|
||||||
assert 0
|
|
||||||
""",
|
""",
|
||||||
|
test_b="def test_b1(): assert 0",
|
||||||
)
|
)
|
||||||
p = testdir.tmpdir.join("test_a.py")
|
p = testdir.tmpdir.join("test_a.py")
|
||||||
p2 = testdir.tmpdir.join("test_b.py")
|
p2 = testdir.tmpdir.join("test_b.py")
|
||||||
|
|
@ -350,36 +318,19 @@ class TestLastFailed:
|
||||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||||
result = testdir.runpytest("--lf", p2)
|
result = testdir.runpytest("--lf", p2)
|
||||||
result.stdout.fnmatch_lines(["*1 failed*"])
|
result.stdout.fnmatch_lines(["*1 failed*"])
|
||||||
p2.write(
|
|
||||||
textwrap.dedent(
|
testdir.makepyfile(test_b="def test_b1(): assert 1")
|
||||||
"""\
|
|
||||||
def test_b1():
|
|
||||||
assert 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = testdir.runpytest("--lf", p2)
|
result = testdir.runpytest("--lf", p2)
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
result = testdir.runpytest("--lf", p)
|
result = testdir.runpytest("--lf", p)
|
||||||
result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
|
result.stdout.fnmatch_lines(["*1 failed*1 desel*"])
|
||||||
|
|
||||||
def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
|
def test_lastfailed_usecase_splice(self, testdir, monkeypatch):
|
||||||
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
|
monkeypatch.setattr("sys.dont_write_bytecode", True)
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""\
|
"def test_1(): assert 0", test_something="def test_2(): assert 0"
|
||||||
def test_1():
|
|
||||||
assert 0
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
p2 = testdir.tmpdir.join("test_something.py")
|
p2 = testdir.tmpdir.join("test_something.py")
|
||||||
p2.write(
|
|
||||||
textwrap.dedent(
|
|
||||||
"""\
|
|
||||||
def test_2():
|
|
||||||
assert 0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||||
result = testdir.runpytest("--lf", p2)
|
result = testdir.runpytest("--lf", p2)
|
||||||
|
|
@ -421,18 +372,14 @@ class TestLastFailed:
|
||||||
def test_terminal_report_lastfailed(self, testdir):
|
def test_terminal_report_lastfailed(self, testdir):
|
||||||
test_a = testdir.makepyfile(
|
test_a = testdir.makepyfile(
|
||||||
test_a="""
|
test_a="""
|
||||||
def test_a1():
|
def test_a1(): pass
|
||||||
pass
|
def test_a2(): pass
|
||||||
def test_a2():
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
test_b = testdir.makepyfile(
|
test_b = testdir.makepyfile(
|
||||||
test_b="""
|
test_b="""
|
||||||
def test_b1():
|
def test_b1(): assert 0
|
||||||
assert 0
|
def test_b2(): assert 0
|
||||||
def test_b2():
|
|
||||||
assert 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -477,10 +424,8 @@ class TestLastFailed:
|
||||||
def test_terminal_report_failedfirst(self, testdir):
|
def test_terminal_report_failedfirst(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
test_a="""
|
test_a="""
|
||||||
def test_a1():
|
def test_a1(): assert 0
|
||||||
assert 0
|
def test_a2(): pass
|
||||||
def test_a2():
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -527,7 +472,6 @@ class TestLastFailed:
|
||||||
assert list(lastfailed) == ["test_maybe.py::test_hello"]
|
assert list(lastfailed) == ["test_maybe.py::test_hello"]
|
||||||
|
|
||||||
def test_lastfailed_failure_subset(self, testdir, monkeypatch):
|
def test_lastfailed_failure_subset(self, testdir, monkeypatch):
|
||||||
|
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
test_maybe="""
|
test_maybe="""
|
||||||
import os
|
import os
|
||||||
|
|
@ -545,6 +489,7 @@ class TestLastFailed:
|
||||||
env = os.environ
|
env = os.environ
|
||||||
if '1' == env['FAILIMPORT']:
|
if '1' == env['FAILIMPORT']:
|
||||||
raise ImportError('fail')
|
raise ImportError('fail')
|
||||||
|
|
||||||
def test_hello():
|
def test_hello():
|
||||||
assert '0' == env['FAILTEST']
|
assert '0' == env['FAILTEST']
|
||||||
|
|
||||||
|
|
@ -598,8 +543,7 @@ class TestLastFailed:
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test():
|
def test(): assert 0
|
||||||
assert 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -611,8 +555,7 @@ class TestLastFailed:
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.mark.xfail(strict=True)
|
@pytest.mark.xfail(strict=True)
|
||||||
def test():
|
def test(): pass
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -626,8 +569,7 @@ class TestLastFailed:
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
def test():
|
def test(): assert 0
|
||||||
assert 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -640,8 +582,7 @@ class TestLastFailed:
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.{mark}
|
@pytest.{mark}
|
||||||
def test():
|
def test(): assert 0
|
||||||
assert 0
|
|
||||||
""".format(
|
""".format(
|
||||||
mark=mark
|
mark=mark
|
||||||
)
|
)
|
||||||
|
|
@ -660,11 +601,11 @@ class TestLastFailed:
|
||||||
if quiet:
|
if quiet:
|
||||||
args.append("-q")
|
args.append("-q")
|
||||||
result = testdir.runpytest(*args)
|
result = testdir.runpytest(*args)
|
||||||
assert "run all" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*run all*")
|
||||||
|
|
||||||
result = testdir.runpytest(*args)
|
result = testdir.runpytest(*args)
|
||||||
if quiet:
|
if quiet:
|
||||||
assert "run all" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*run all*")
|
||||||
else:
|
else:
|
||||||
assert "rerun previous" in result.stdout.str()
|
assert "rerun previous" in result.stdout.str()
|
||||||
|
|
||||||
|
|
@ -679,18 +620,14 @@ class TestLastFailed:
|
||||||
# 1. initial run
|
# 1. initial run
|
||||||
test_bar = testdir.makepyfile(
|
test_bar = testdir.makepyfile(
|
||||||
test_bar="""
|
test_bar="""
|
||||||
def test_bar_1():
|
def test_bar_1(): pass
|
||||||
pass
|
def test_bar_2(): assert 0
|
||||||
def test_bar_2():
|
|
||||||
assert 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
test_foo = testdir.makepyfile(
|
test_foo = testdir.makepyfile(
|
||||||
test_foo="""
|
test_foo="""
|
||||||
def test_foo_3():
|
def test_foo_3(): pass
|
||||||
pass
|
def test_foo_4(): assert 0
|
||||||
def test_foo_4():
|
|
||||||
assert 0
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
testdir.runpytest()
|
testdir.runpytest()
|
||||||
|
|
@ -702,10 +639,8 @@ class TestLastFailed:
|
||||||
# 2. fix test_bar_2, run only test_bar.py
|
# 2. fix test_bar_2, run only test_bar.py
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
test_bar="""
|
test_bar="""
|
||||||
def test_bar_1():
|
def test_bar_1(): pass
|
||||||
pass
|
def test_bar_2(): pass
|
||||||
def test_bar_2():
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest(test_bar)
|
result = testdir.runpytest(test_bar)
|
||||||
|
|
@ -720,10 +655,8 @@ class TestLastFailed:
|
||||||
# 3. fix test_foo_4, run only test_foo.py
|
# 3. fix test_foo_4, run only test_foo.py
|
||||||
test_foo = testdir.makepyfile(
|
test_foo = testdir.makepyfile(
|
||||||
test_foo="""
|
test_foo="""
|
||||||
def test_foo_3():
|
def test_foo_3(): pass
|
||||||
pass
|
def test_foo_4(): pass
|
||||||
def test_foo_4():
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest(test_foo, "--last-failed")
|
result = testdir.runpytest(test_foo, "--last-failed")
|
||||||
|
|
@ -737,10 +670,8 @@ class TestLastFailed:
|
||||||
def test_lastfailed_no_failures_behavior_all_passed(self, testdir):
|
def test_lastfailed_no_failures_behavior_all_passed(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_1():
|
def test_1(): pass
|
||||||
assert True
|
def test_2(): pass
|
||||||
def test_2():
|
|
||||||
assert True
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
@ -762,10 +693,8 @@ class TestLastFailed:
|
||||||
def test_lastfailed_no_failures_behavior_empty_cache(self, testdir):
|
def test_lastfailed_no_failures_behavior_empty_cache(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_1():
|
def test_1(): pass
|
||||||
assert True
|
def test_2(): assert 0
|
||||||
def test_2():
|
|
||||||
assert False
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--lf", "--cache-clear")
|
result = testdir.runpytest("--lf", "--cache-clear")
|
||||||
|
|
@ -1007,22 +936,12 @@ class TestReadme:
|
||||||
return readme.is_file()
|
return readme.is_file()
|
||||||
|
|
||||||
def test_readme_passed(self, testdir):
|
def test_readme_passed(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile("def test_always_passes(): pass")
|
||||||
"""
|
|
||||||
def test_always_passes():
|
|
||||||
assert 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
testdir.runpytest()
|
testdir.runpytest()
|
||||||
assert self.check_readme(testdir) is True
|
assert self.check_readme(testdir) is True
|
||||||
|
|
||||||
def test_readme_failed(self, testdir):
|
def test_readme_failed(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile("def test_always_fails(): assert 0")
|
||||||
"""
|
|
||||||
def test_always_fails():
|
|
||||||
assert 0
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
testdir.runpytest()
|
testdir.runpytest()
|
||||||
assert self.check_readme(testdir) is True
|
assert self.check_readme(testdir) is True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
|
from typing import List
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import capture
|
from _pytest import capture
|
||||||
|
|
@ -90,8 +92,6 @@ class TestCaptureManager:
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", ["fd", "sys"])
|
@pytest.mark.parametrize("method", ["fd", "sys"])
|
||||||
def test_capturing_unicode(testdir, method):
|
def test_capturing_unicode(testdir, method):
|
||||||
if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (2, 2):
|
|
||||||
pytest.xfail("does not work on pypy < 2.2")
|
|
||||||
obj = "'b\u00f6y'"
|
obj = "'b\u00f6y'"
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""\
|
"""\
|
||||||
|
|
@ -451,7 +451,7 @@ class TestCaptureFixture:
|
||||||
"E*capfd*capsys*same*time*",
|
"E*capfd*capsys*same*time*",
|
||||||
"*ERROR*setup*test_two*",
|
"*ERROR*setup*test_two*",
|
||||||
"E*capsys*capfd*same*time*",
|
"E*capsys*capfd*same*time*",
|
||||||
"*2 error*",
|
"*2 errors*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -603,17 +603,13 @@ class TestCaptureFixture:
|
||||||
)
|
)
|
||||||
args = ("-s",) if no_capture else ()
|
args = ("-s",) if no_capture else ()
|
||||||
result = testdir.runpytest_subprocess(*args)
|
result = testdir.runpytest_subprocess(*args)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(["*while capture is disabled*", "*= 2 passed in *"])
|
||||||
"""
|
result.stdout.no_fnmatch_line("*captured before*")
|
||||||
*while capture is disabled*
|
result.stdout.no_fnmatch_line("*captured after*")
|
||||||
"""
|
|
||||||
)
|
|
||||||
assert "captured before" not in result.stdout.str()
|
|
||||||
assert "captured after" not in result.stdout.str()
|
|
||||||
if no_capture:
|
if no_capture:
|
||||||
assert "test_normal executed" in result.stdout.str()
|
assert "test_normal executed" in result.stdout.str()
|
||||||
else:
|
else:
|
||||||
assert "test_normal executed" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_normal executed*")
|
||||||
|
|
||||||
@pytest.mark.parametrize("fixture", ["capsys", "capfd"])
|
@pytest.mark.parametrize("fixture", ["capsys", "capfd"])
|
||||||
def test_fixture_use_by_other_fixtures(self, testdir, fixture):
|
def test_fixture_use_by_other_fixtures(self, testdir, fixture):
|
||||||
|
|
@ -649,8 +645,8 @@ class TestCaptureFixture:
|
||||||
)
|
)
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
assert "stdout contents begin" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*stdout contents begin*")
|
||||||
assert "stderr contents begin" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*stderr contents begin*")
|
||||||
|
|
||||||
@pytest.mark.parametrize("cap", ["capsys", "capfd"])
|
@pytest.mark.parametrize("cap", ["capsys", "capfd"])
|
||||||
def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap):
|
def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap):
|
||||||
|
|
@ -720,7 +716,7 @@ def test_capture_conftest_runtest_setup(testdir):
|
||||||
testdir.makepyfile("def test_func(): pass")
|
testdir.makepyfile("def test_func(): pass")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert "hello19" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*hello19*")
|
||||||
|
|
||||||
|
|
||||||
def test_capture_badoutput_issue412(testdir):
|
def test_capture_badoutput_issue412(testdir):
|
||||||
|
|
@ -824,6 +820,7 @@ def test_dontreadfrominput():
|
||||||
from _pytest.capture import DontReadFromInput
|
from _pytest.capture import DontReadFromInput
|
||||||
|
|
||||||
f = DontReadFromInput()
|
f = DontReadFromInput()
|
||||||
|
assert f.buffer is f
|
||||||
assert not f.isatty()
|
assert not f.isatty()
|
||||||
pytest.raises(IOError, f.read)
|
pytest.raises(IOError, f.read)
|
||||||
pytest.raises(IOError, f.readlines)
|
pytest.raises(IOError, f.readlines)
|
||||||
|
|
@ -833,20 +830,6 @@ def test_dontreadfrominput():
|
||||||
f.close() # just for completeness
|
f.close() # just for completeness
|
||||||
|
|
||||||
|
|
||||||
def test_dontreadfrominput_buffer_python3():
|
|
||||||
from _pytest.capture import DontReadFromInput
|
|
||||||
|
|
||||||
f = DontReadFromInput()
|
|
||||||
fb = f.buffer
|
|
||||||
assert not fb.isatty()
|
|
||||||
pytest.raises(IOError, fb.read)
|
|
||||||
pytest.raises(IOError, fb.readlines)
|
|
||||||
iter_f = iter(f)
|
|
||||||
pytest.raises(IOError, next, iter_f)
|
|
||||||
pytest.raises(ValueError, fb.fileno)
|
|
||||||
f.close() # just for completeness
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmpfile(testdir):
|
def tmpfile(testdir):
|
||||||
f = testdir.makepyfile("").open("wb+")
|
f = testdir.makepyfile("").open("wb+")
|
||||||
|
|
@ -856,8 +839,8 @@ def tmpfile(testdir):
|
||||||
|
|
||||||
|
|
||||||
@needsosdup
|
@needsosdup
|
||||||
def test_dupfile(tmpfile):
|
def test_dupfile(tmpfile) -> None:
|
||||||
flist = []
|
flist = [] # type: List[TextIO]
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
nf = capture.safe_text_dupfile(tmpfile, "wb")
|
nf = capture.safe_text_dupfile(tmpfile, "wb")
|
||||||
assert nf != tmpfile
|
assert nf != tmpfile
|
||||||
|
|
@ -903,9 +886,9 @@ def lsof_check():
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
out = subprocess.check_output(("lsof", "-p", str(pid))).decode()
|
out = subprocess.check_output(("lsof", "-p", str(pid))).decode()
|
||||||
except (OSError, subprocess.CalledProcessError, UnicodeDecodeError):
|
except (OSError, subprocess.CalledProcessError, UnicodeDecodeError) as exc:
|
||||||
# about UnicodeDecodeError, see note on pytester
|
# about UnicodeDecodeError, see note on pytester
|
||||||
pytest.skip("could not run 'lsof'")
|
pytest.skip("could not run 'lsof' ({!r})".format(exc))
|
||||||
yield
|
yield
|
||||||
out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode()
|
out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode()
|
||||||
len1 = len([x for x in out.split("\n") if "REG" in x])
|
len1 = len([x for x in out.split("\n") if "REG" in x])
|
||||||
|
|
@ -1387,7 +1370,7 @@ def test_crash_on_closing_tmpfile_py27(testdir):
|
||||||
result = testdir.runpytest_subprocess(str(p))
|
result = testdir.runpytest_subprocess(str(p))
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert result.stderr.str() == ""
|
assert result.stderr.str() == ""
|
||||||
assert "IOError" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*IOError*")
|
||||||
|
|
||||||
|
|
||||||
def test_pickling_and_unpickling_encoded_file():
|
def test_pickling_and_unpickling_encoded_file():
|
||||||
|
|
@ -1501,11 +1484,9 @@ def test_typeerror_encodedfile_write(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result_without_capture = testdir.runpytest("-s", str(p))
|
result_without_capture = testdir.runpytest("-s", str(p))
|
||||||
|
|
||||||
result_with_capture = testdir.runpytest(str(p))
|
result_with_capture = testdir.runpytest(str(p))
|
||||||
|
|
||||||
assert result_with_capture.ret == result_without_capture.ret
|
assert result_with_capture.ret == result_without_capture.ret
|
||||||
|
|
||||||
result_with_capture.stdout.fnmatch_lines(
|
result_with_capture.stdout.fnmatch_lines(
|
||||||
["E TypeError: write() argument must be str, not bytes"]
|
["E * TypeError: write() argument must be str, not bytes"]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ class TestCollectFS:
|
||||||
|
|
||||||
# by default, ignore tests inside a virtualenv
|
# by default, ignore tests inside a virtualenv
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "test_invenv" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_invenv*")
|
||||||
# allow test collection if user insists
|
# allow test collection if user insists
|
||||||
result = testdir.runpytest("--collect-in-virtualenv")
|
result = testdir.runpytest("--collect-in-virtualenv")
|
||||||
assert "test_invenv" in result.stdout.str()
|
assert "test_invenv" in result.stdout.str()
|
||||||
|
|
@ -165,7 +165,7 @@ class TestCollectFS:
|
||||||
testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py")
|
testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py")
|
||||||
testfile.write("def test_hello(): pass")
|
testfile.write("def test_hello(): pass")
|
||||||
result = testdir.runpytest("--collect-in-virtualenv")
|
result = testdir.runpytest("--collect-in-virtualenv")
|
||||||
assert "test_invenv" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_invenv*")
|
||||||
# ...unless the virtualenv is explicitly given on the CLI
|
# ...unless the virtualenv is explicitly given on the CLI
|
||||||
result = testdir.runpytest("--collect-in-virtualenv", ".virtual")
|
result = testdir.runpytest("--collect-in-virtualenv", ".virtual")
|
||||||
assert "test_invenv" in result.stdout.str()
|
assert "test_invenv" in result.stdout.str()
|
||||||
|
|
@ -364,7 +364,7 @@ class TestCustomConftests:
|
||||||
testdir.makepyfile(test_world="def test_hello(): pass")
|
testdir.makepyfile(test_world="def test_hello(): pass")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
assert "passed" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*passed*")
|
||||||
result = testdir.runpytest("--XX")
|
result = testdir.runpytest("--XX")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert "passed" in result.stdout.str()
|
assert "passed" in result.stdout.str()
|
||||||
|
|
@ -402,7 +402,7 @@ class TestCustomConftests:
|
||||||
)
|
)
|
||||||
testdir.mkdir("sub")
|
testdir.mkdir("sub")
|
||||||
testdir.makepyfile("def test_x(): pass")
|
testdir.makepyfile("def test_x(): pass")
|
||||||
result = testdir.runpytest("--collect-only")
|
result = testdir.runpytest("--co")
|
||||||
result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"])
|
result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"])
|
||||||
|
|
||||||
def test_pytest_collect_file_from_sister_dir(self, testdir):
|
def test_pytest_collect_file_from_sister_dir(self, testdir):
|
||||||
|
|
@ -433,7 +433,7 @@ class TestCustomConftests:
|
||||||
p = testdir.makepyfile("def test_x(): pass")
|
p = testdir.makepyfile("def test_x(): pass")
|
||||||
p.copy(sub1.join(p.basename))
|
p.copy(sub1.join(p.basename))
|
||||||
p.copy(sub2.join(p.basename))
|
p.copy(sub2.join(p.basename))
|
||||||
result = testdir.runpytest("--collect-only")
|
result = testdir.runpytest("--co")
|
||||||
result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"])
|
result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -486,7 +486,7 @@ class TestSession:
|
||||||
p = testdir.makepyfile("def test_func(): pass")
|
p = testdir.makepyfile("def test_func(): pass")
|
||||||
id = "::".join([p.basename, "test_func"])
|
id = "::".join([p.basename, "test_func"])
|
||||||
items, hookrec = testdir.inline_genitems(id)
|
items, hookrec = testdir.inline_genitems(id)
|
||||||
item, = items
|
(item,) = items
|
||||||
assert item.name == "test_func"
|
assert item.name == "test_func"
|
||||||
newid = item.nodeid
|
newid = item.nodeid
|
||||||
assert newid == id
|
assert newid == id
|
||||||
|
|
@ -605,9 +605,9 @@ class TestSession:
|
||||||
testdir.makepyfile("def test_func(): pass")
|
testdir.makepyfile("def test_func(): pass")
|
||||||
items, hookrec = testdir.inline_genitems()
|
items, hookrec = testdir.inline_genitems()
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
item, = items
|
(item,) = items
|
||||||
items2, hookrec = testdir.inline_genitems(item.nodeid)
|
items2, hookrec = testdir.inline_genitems(item.nodeid)
|
||||||
item2, = items2
|
(item2,) = items2
|
||||||
assert item2.name == item.name
|
assert item2.name == item.name
|
||||||
assert item2.fspath == item.fspath
|
assert item2.fspath == item.fspath
|
||||||
|
|
||||||
|
|
@ -622,7 +622,7 @@ class TestSession:
|
||||||
arg = p.basename + "::TestClass::test_method"
|
arg = p.basename + "::TestClass::test_method"
|
||||||
items, hookrec = testdir.inline_genitems(arg)
|
items, hookrec = testdir.inline_genitems(arg)
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
item, = items
|
(item,) = items
|
||||||
assert item.nodeid.endswith("TestClass::test_method")
|
assert item.nodeid.endswith("TestClass::test_method")
|
||||||
# ensure we are reporting the collection of the single test item (#2464)
|
# ensure we are reporting the collection of the single test item (#2464)
|
||||||
assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"]
|
assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"]
|
||||||
|
|
@ -859,12 +859,16 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir):
|
||||||
|
|
||||||
res = testdir.runpytest("--maxfail=1")
|
res = testdir.runpytest("--maxfail=1")
|
||||||
assert res.ret == 1
|
assert res.ret == 1
|
||||||
|
|
||||||
res.stdout.fnmatch_lines(
|
res.stdout.fnmatch_lines(
|
||||||
["*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*"]
|
[
|
||||||
|
"collected 1 item / 1 error",
|
||||||
|
"*ERROR collecting test_02_import_error.py*",
|
||||||
|
"*No module named *asdfa*",
|
||||||
|
"*! stopping after 1 failures !*",
|
||||||
|
"*= 1 error in *",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
res.stdout.no_fnmatch_line("*test_03*")
|
||||||
assert "test_03" not in res.stdout.str()
|
|
||||||
|
|
||||||
|
|
||||||
def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
|
def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
|
||||||
|
|
@ -876,7 +880,6 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
|
||||||
|
|
||||||
res = testdir.runpytest("--maxfail=4")
|
res = testdir.runpytest("--maxfail=4")
|
||||||
assert res.ret == 2
|
assert res.ret == 2
|
||||||
|
|
||||||
res.stdout.fnmatch_lines(
|
res.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"collected 2 items / 2 errors",
|
"collected 2 items / 2 errors",
|
||||||
|
|
@ -884,6 +887,8 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir):
|
||||||
"*No module named *asdfa*",
|
"*No module named *asdfa*",
|
||||||
"*ERROR collecting test_03_import_error.py*",
|
"*ERROR collecting test_03_import_error.py*",
|
||||||
"*No module named *asdfa*",
|
"*No module named *asdfa*",
|
||||||
|
"*! Interrupted: 2 errors during collection !*",
|
||||||
|
"*= 2 errors in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -899,7 +904,7 @@ def test_continue_on_collection_errors(testdir):
|
||||||
assert res.ret == 1
|
assert res.ret == 1
|
||||||
|
|
||||||
res.stdout.fnmatch_lines(
|
res.stdout.fnmatch_lines(
|
||||||
["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 error*"]
|
["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 errors*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -916,7 +921,7 @@ def test_continue_on_collection_errors_maxfail(testdir):
|
||||||
res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3")
|
res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3")
|
||||||
assert res.ret == 1
|
assert res.ret == 1
|
||||||
|
|
||||||
res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 error*"])
|
res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"])
|
||||||
|
|
||||||
|
|
||||||
def test_fixture_scope_sibling_conftests(testdir):
|
def test_fixture_scope_sibling_conftests(testdir):
|
||||||
|
|
@ -1003,12 +1008,12 @@ def test_collect_init_tests(testdir):
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["<Package */tests>", " <Module test_foo.py>", " <Function test_foo>"]
|
["<Package */tests>", " <Module test_foo.py>", " <Function test_foo>"]
|
||||||
)
|
)
|
||||||
assert "test_init" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_init*")
|
||||||
result = testdir.runpytest("./tests/__init__.py", "--collect-only")
|
result = testdir.runpytest("./tests/__init__.py", "--collect-only")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["<Package */tests>", " <Module __init__.py>", " <Function test_init>"]
|
["<Package */tests>", " <Module __init__.py>", " <Function test_init>"]
|
||||||
)
|
)
|
||||||
assert "test_foo" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*test_foo*")
|
||||||
|
|
||||||
|
|
||||||
def test_collect_invalid_signature_message(testdir):
|
def test_collect_invalid_signature_message(testdir):
|
||||||
|
|
@ -1260,7 +1265,7 @@ def test_collector_respects_tbstyle(testdir):
|
||||||
' File "*/test_collector_respects_tbstyle.py", line 1, in <module>',
|
' File "*/test_collector_respects_tbstyle.py", line 1, in <module>',
|
||||||
" assert 0",
|
" assert 0",
|
||||||
"AssertionError: assert 0",
|
"AssertionError: assert 0",
|
||||||
"*! Interrupted: 1 errors during collection !*",
|
"*! Interrupted: 1 error during collection !*",
|
||||||
"*= 1 error in *",
|
"*= 1 error in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from functools import wraps
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import _PytestWrapper
|
from _pytest.compat import _PytestWrapper
|
||||||
|
from _pytest.compat import cached_property
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
|
|
@ -178,3 +179,23 @@ def test_safe_isclass():
|
||||||
assert False, "Should be ignored"
|
assert False, "Should be ignored"
|
||||||
|
|
||||||
assert safe_isclass(CrappyClass()) is False
|
assert safe_isclass(CrappyClass()) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_cached_property() -> None:
|
||||||
|
ncalls = 0
|
||||||
|
|
||||||
|
class Class:
|
||||||
|
@cached_property
|
||||||
|
def prop(self) -> int:
|
||||||
|
nonlocal ncalls
|
||||||
|
ncalls += 1
|
||||||
|
return ncalls
|
||||||
|
|
||||||
|
c1 = Class()
|
||||||
|
assert ncalls == 0
|
||||||
|
assert c1.prop == 1
|
||||||
|
assert c1.prop == 1
|
||||||
|
c2 = Class()
|
||||||
|
assert ncalls == 1
|
||||||
|
assert c2.prop == 2
|
||||||
|
assert c1.prop == 1
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import importlib_metadata
|
from _pytest.compat import importlib_metadata
|
||||||
from _pytest.config import _iter_rewritable_modules
|
from _pytest.config import _iter_rewritable_modules
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.config.findpaths import determine_setup
|
from _pytest.config.findpaths import determine_setup
|
||||||
from _pytest.config.findpaths import get_common_ancestor
|
from _pytest.config.findpaths import get_common_ancestor
|
||||||
from _pytest.config.findpaths import getcfg
|
from _pytest.config.findpaths import getcfg
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class TestParseIni:
|
class TestParseIni:
|
||||||
|
|
@ -456,7 +457,7 @@ class TestConfigFromdictargs:
|
||||||
|
|
||||||
config = Config.fromdictargs(option_dict, args)
|
config = Config.fromdictargs(option_dict, args)
|
||||||
assert config.args == ["a", "b"]
|
assert config.args == ["a", "b"]
|
||||||
assert config.invocation_params.args == args
|
assert config.invocation_params.args == tuple(args)
|
||||||
assert config.option.verbose == 4
|
assert config.option.verbose == 4
|
||||||
assert config.option.capture == "no"
|
assert config.option.capture == "no"
|
||||||
|
|
||||||
|
|
@ -1235,7 +1236,7 @@ def test_invocation_args(testdir):
|
||||||
call = calls[0]
|
call = calls[0]
|
||||||
config = call.item.config
|
config = call.item.config
|
||||||
|
|
||||||
assert config.invocation_params.args == [p, "-v"]
|
assert config.invocation_params.args == (p, "-v")
|
||||||
assert config.invocation_params.dir == Path(str(testdir.tmpdir))
|
assert config.invocation_params.dir == Path(str(testdir.tmpdir))
|
||||||
|
|
||||||
plugins = config.invocation_params.plugins
|
plugins = config.invocation_params.plugins
|
||||||
|
|
@ -1243,6 +1244,10 @@ def test_invocation_args(testdir):
|
||||||
assert plugins[0] is plugin
|
assert plugins[0] is plugin
|
||||||
assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run()
|
assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run()
|
||||||
|
|
||||||
|
# args cannot be None
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Config.InvocationParams(args=None, plugins=None, dir=Path())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"plugin",
|
"plugin",
|
||||||
|
|
@ -1286,7 +1291,7 @@ def test_config_blocked_default_plugins(testdir, plugin):
|
||||||
if plugin != "terminal":
|
if plugin != "terminal":
|
||||||
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
result.stdout.fnmatch_lines(["* 1 failed in *"])
|
||||||
else:
|
else:
|
||||||
assert result.stdout.lines == [""]
|
assert result.stdout.lines == []
|
||||||
|
|
||||||
|
|
||||||
class TestSetupCfg:
|
class TestSetupCfg:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
|
from _pytest.pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def ConftestWithSetinitial(path):
|
def ConftestWithSetinitial(path):
|
||||||
|
|
@ -187,7 +187,7 @@ def test_conftest_confcutdir(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-h", "--confcutdir=%s" % x, x)
|
result = testdir.runpytest("-h", "--confcutdir=%s" % x, x)
|
||||||
result.stdout.fnmatch_lines(["*--xyz*"])
|
result.stdout.fnmatch_lines(["*--xyz*"])
|
||||||
assert "warning: could not load initial" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*warning: could not load initial*")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
@ -648,5 +648,5 @@ def test_required_option_help(testdir):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-h", x)
|
result = testdir.runpytest("-h", x)
|
||||||
assert "argument --xyz is required" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*argument --xyz is required*")
|
||||||
assert "general:" in result.stdout.str()
|
assert "general:" in result.stdout.str()
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,8 @@ class TestDoctests:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# lines below should be trimmed out
|
# lines below should be trimmed out
|
||||||
assert "text-line-2" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*text-line-2*")
|
||||||
assert "text-line-after" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*text-line-after*")
|
||||||
|
|
||||||
def test_docstring_full_context_around_error(self, testdir):
|
def test_docstring_full_context_around_error(self, testdir):
|
||||||
"""Test that we show the whole context before the actual line of a failing
|
"""Test that we show the whole context before the actual line of a failing
|
||||||
|
|
@ -334,7 +334,7 @@ class TestDoctests:
|
||||||
[
|
[
|
||||||
"*ERROR collecting hello.py*",
|
"*ERROR collecting hello.py*",
|
||||||
"*{e}: No module named *asdals*".format(e=MODULE_NOT_FOUND_ERROR),
|
"*{e}: No module named *asdals*".format(e=MODULE_NOT_FOUND_ERROR),
|
||||||
"*Interrupted: 1 errors during collection*",
|
"*Interrupted: 1 error during collection*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -839,7 +839,8 @@ class TestLiterals:
|
||||||
reprec = testdir.inline_run()
|
reprec = testdir.inline_run()
|
||||||
reprec.assertoutcome(failed=1)
|
reprec.assertoutcome(failed=1)
|
||||||
|
|
||||||
def test_number_re(self):
|
def test_number_re(self) -> None:
|
||||||
|
_number_re = _get_checker()._number_re # type: ignore
|
||||||
for s in [
|
for s in [
|
||||||
"1.",
|
"1.",
|
||||||
"+1.",
|
"+1.",
|
||||||
|
|
@ -861,12 +862,12 @@ class TestLiterals:
|
||||||
"-1.2e-3",
|
"-1.2e-3",
|
||||||
]:
|
]:
|
||||||
print(s)
|
print(s)
|
||||||
m = _get_checker()._number_re.match(s)
|
m = _number_re.match(s)
|
||||||
assert m is not None
|
assert m is not None
|
||||||
assert float(m.group()) == pytest.approx(float(s))
|
assert float(m.group()) == pytest.approx(float(s))
|
||||||
for s in ["1", "abc"]:
|
for s in ["1", "abc"]:
|
||||||
print(s)
|
print(s)
|
||||||
assert _get_checker()._number_re.match(s) is None
|
assert _number_re.match(s) is None
|
||||||
|
|
||||||
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
||||||
def test_number_precision(self, testdir, config_mode):
|
def test_number_precision(self, testdir, config_mode):
|
||||||
|
|
@ -1177,7 +1178,7 @@ class TestDoctestAutoUseFixtures:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--doctest-modules")
|
result = testdir.runpytest("--doctest-modules")
|
||||||
assert "FAILURES" not in str(result.stdout.str())
|
result.stdout.no_fnmatch_line("*FAILURES*")
|
||||||
result.stdout.fnmatch_lines(["*=== 1 passed in *"])
|
result.stdout.fnmatch_lines(["*=== 1 passed in *"])
|
||||||
|
|
||||||
@pytest.mark.parametrize("scope", SCOPES)
|
@pytest.mark.parametrize("scope", SCOPES)
|
||||||
|
|
@ -1209,7 +1210,7 @@ class TestDoctestAutoUseFixtures:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--doctest-modules")
|
result = testdir.runpytest("--doctest-modules")
|
||||||
assert "FAILURES" not in str(result.stdout.str())
|
str(result.stdout.no_fnmatch_line("*FAILURES*"))
|
||||||
result.stdout.fnmatch_lines(["*=== 1 passed in *"])
|
result.stdout.fnmatch_lines(["*=== 1 passed in *"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,13 @@ def test_timeout(testdir, enabled):
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
def test_timeout():
|
def test_timeout():
|
||||||
time.sleep(2.0)
|
time.sleep(0.1)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
testdir.makeini(
|
testdir.makeini(
|
||||||
"""
|
"""
|
||||||
[pytest]
|
[pytest]
|
||||||
faulthandler_timeout = 1
|
faulthandler_timeout = 0.01
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
args = ["-p", "no:faulthandler"] if not enabled else []
|
args = ["-p", "no:faulthandler"] if not enabled else []
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
@ -9,6 +8,7 @@ import xmlschema
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.junitxml import LogXML
|
from _pytest.junitxml import LogXML
|
||||||
|
from _pytest.pathlib import Path
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -477,22 +477,25 @@ class TestPython:
|
||||||
assert "ValueError" in fnode.toxml()
|
assert "ValueError" in fnode.toxml()
|
||||||
systemout = fnode.next_sibling
|
systemout = fnode.next_sibling
|
||||||
assert systemout.tag == "system-out"
|
assert systemout.tag == "system-out"
|
||||||
assert "hello-stdout" in systemout.toxml()
|
systemout_xml = systemout.toxml()
|
||||||
assert "info msg" not in systemout.toxml()
|
assert "hello-stdout" in systemout_xml
|
||||||
|
assert "info msg" not in systemout_xml
|
||||||
systemerr = systemout.next_sibling
|
systemerr = systemout.next_sibling
|
||||||
assert systemerr.tag == "system-err"
|
assert systemerr.tag == "system-err"
|
||||||
assert "hello-stderr" in systemerr.toxml()
|
systemerr_xml = systemerr.toxml()
|
||||||
assert "info msg" not in systemerr.toxml()
|
assert "hello-stderr" in systemerr_xml
|
||||||
|
assert "info msg" not in systemerr_xml
|
||||||
|
|
||||||
if junit_logging == "system-out":
|
if junit_logging == "system-out":
|
||||||
assert "warning msg" in systemout.toxml()
|
assert "warning msg" in systemout_xml
|
||||||
assert "warning msg" not in systemerr.toxml()
|
assert "warning msg" not in systemerr_xml
|
||||||
elif junit_logging == "system-err":
|
elif junit_logging == "system-err":
|
||||||
assert "warning msg" not in systemout.toxml()
|
assert "warning msg" not in systemout_xml
|
||||||
assert "warning msg" in systemerr.toxml()
|
assert "warning msg" in systemerr_xml
|
||||||
elif junit_logging == "no":
|
else:
|
||||||
assert "warning msg" not in systemout.toxml()
|
assert junit_logging == "no"
|
||||||
assert "warning msg" not in systemerr.toxml()
|
assert "warning msg" not in systemout_xml
|
||||||
|
assert "warning msg" not in systemerr_xml
|
||||||
|
|
||||||
@parametrize_families
|
@parametrize_families
|
||||||
def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family):
|
def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family):
|
||||||
|
|
@ -1216,7 +1219,7 @@ def test_runs_twice(testdir, run_and_parse):
|
||||||
)
|
)
|
||||||
|
|
||||||
result, dom = run_and_parse(f, f)
|
result, dom = run_and_parse(f, f)
|
||||||
assert "INTERNALERROR" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
|
first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
|
||||||
assert first == second
|
assert first == second
|
||||||
|
|
||||||
|
|
@ -1231,7 +1234,7 @@ def test_runs_twice_xdist(testdir, run_and_parse):
|
||||||
)
|
)
|
||||||
|
|
||||||
result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen")
|
result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen")
|
||||||
assert "INTERNALERROR" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
|
first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
|
||||||
assert first == second
|
assert first == second
|
||||||
|
|
||||||
|
|
@ -1271,7 +1274,7 @@ def test_fancy_items_regression(testdir, run_and_parse):
|
||||||
|
|
||||||
result, dom = run_and_parse()
|
result, dom = run_and_parse()
|
||||||
|
|
||||||
assert "INTERNALERROR" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
|
|
||||||
items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
|
items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
|
||||||
import pprint
|
import pprint
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,21 @@ def test_keyword_option_parametrize(spec, testdir):
|
||||||
assert list(passed) == list(passed_result)
|
assert list(passed) == list(passed_result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parametrize_with_module(testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize("arg", [pytest,])
|
||||||
|
def test_func(arg):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rec = testdir.inline_run()
|
||||||
|
passed, skipped, fail = rec.listoutcomes()
|
||||||
|
expected_id = "test_func[" + pytest.__name__ + "]"
|
||||||
|
assert passed[0].nodeid.split("::")[-1] == expected_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"spec",
|
"spec",
|
||||||
[
|
[
|
||||||
|
|
@ -831,6 +846,12 @@ class TestMarkDecorator:
|
||||||
def test__eq__(self, lhs, rhs, expected):
|
def test__eq__(self, lhs, rhs, expected):
|
||||||
assert (lhs == rhs) == expected
|
assert (lhs == rhs) == expected
|
||||||
|
|
||||||
|
def test_aliases(self) -> None:
|
||||||
|
md = pytest.mark.foo(1, "2", three=3)
|
||||||
|
assert md.name == "foo"
|
||||||
|
assert md.args == (1, "2")
|
||||||
|
assert md.kwargs == {"three": 3}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mark", [None, "", "skip", "xfail"])
|
@pytest.mark.parametrize("mark", [None, "", "skip", "xfail"])
|
||||||
def test_parameterset_for_parametrize_marks(testdir, mark):
|
def test_parameterset_for_parametrize_marks(testdir, mark):
|
||||||
|
|
@ -891,7 +912,7 @@ def test_parameterset_for_fail_at_collect(testdir):
|
||||||
result = testdir.runpytest(str(p1))
|
result = testdir.runpytest(str(p1))
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"collected 0 items / 1 errors",
|
"collected 0 items / 1 error",
|
||||||
"* ERROR collecting test_parameterset_for_fail_at_collect.py *",
|
"* ERROR collecting test_parameterset_for_fail_at_collect.py *",
|
||||||
"Empty parameter set in 'test' at line 3",
|
"Empty parameter set in 'test' at line 3",
|
||||||
"*= 1 error in *",
|
"*= 1 error in *",
|
||||||
|
|
@ -990,7 +1011,7 @@ def test_markers_from_parametrize(testdir):
|
||||||
def test_pytest_param_id_requires_string():
|
def test_pytest_param_id_requires_string():
|
||||||
with pytest.raises(TypeError) as excinfo:
|
with pytest.raises(TypeError) as excinfo:
|
||||||
pytest.param(id=True)
|
pytest.param(id=True)
|
||||||
msg, = excinfo.value.args
|
(msg,) = excinfo.value.args
|
||||||
assert msg == "Expected id to be a string, got <class 'bool'>: True"
|
assert msg == "Expected id to be a string, got <class 'bool'>: True"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ def _modules():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
@pytest.mark.parametrize("module", _modules())
|
@pytest.mark.parametrize("module", _modules())
|
||||||
def test_no_warnings(module):
|
def test_no_warnings(module):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ class TestPDB:
|
||||||
)
|
)
|
||||||
child = testdir.spawn_pytest("-rs --pdb %s" % p1)
|
child = testdir.spawn_pytest("-rs --pdb %s" % p1)
|
||||||
child.expect("Skipping also with pdb active")
|
child.expect("Skipping also with pdb active")
|
||||||
child.expect("1 skipped in")
|
child.expect_exact("= \x1b[33m\x1b[1m1 skipped\x1b[0m\x1b[33m in")
|
||||||
child.sendeof()
|
child.sendeof()
|
||||||
self.flush(child)
|
self.flush(child)
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ class TestPDB:
|
||||||
child.sendeof()
|
child.sendeof()
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
assert "Exit: Quitting debugger" in rest
|
assert "Exit: Quitting debugger" in rest
|
||||||
assert "= 1 failed in" in rest
|
assert "= \x1b[31m\x1b[1m1 failed\x1b[0m\x1b[31m in" in rest
|
||||||
assert "def test_1" not in rest
|
assert "def test_1" not in rest
|
||||||
assert "get rekt" not in rest
|
assert "get rekt" not in rest
|
||||||
self.flush(child)
|
self.flush(child)
|
||||||
|
|
@ -466,7 +466,6 @@ class TestPDB:
|
||||||
def test_pdb_interaction_doctest(self, testdir, monkeypatch):
|
def test_pdb_interaction_doctest(self, testdir, monkeypatch):
|
||||||
p1 = testdir.makepyfile(
|
p1 = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
|
||||||
def function_1():
|
def function_1():
|
||||||
'''
|
'''
|
||||||
>>> i = 0
|
>>> i = 0
|
||||||
|
|
@ -485,9 +484,32 @@ class TestPDB:
|
||||||
|
|
||||||
child.sendeof()
|
child.sendeof()
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
|
assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest
|
||||||
|
assert "BdbQuit" not in rest
|
||||||
assert "1 failed" in rest
|
assert "1 failed" in rest
|
||||||
self.flush(child)
|
self.flush(child)
|
||||||
|
|
||||||
|
def test_doctest_set_trace_quit(self, testdir, monkeypatch):
|
||||||
|
p1 = testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def function_1():
|
||||||
|
'''
|
||||||
|
>>> __import__('pdb').set_trace()
|
||||||
|
'''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# NOTE: does not use pytest.set_trace, but Python's patched pdb,
|
||||||
|
# therefore "-s" is required.
|
||||||
|
child = testdir.spawn_pytest("--doctest-modules --pdb -s %s" % p1)
|
||||||
|
child.expect("Pdb")
|
||||||
|
child.sendline("q")
|
||||||
|
rest = child.read().decode("utf8")
|
||||||
|
|
||||||
|
assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest
|
||||||
|
assert "= \x1b[33mno tests ran\x1b[0m\x1b[33m in" in rest
|
||||||
|
assert "BdbQuit" not in rest
|
||||||
|
assert "UNEXPECTED EXCEPTION" not in rest
|
||||||
|
|
||||||
def test_pdb_interaction_capturing_twice(self, testdir):
|
def test_pdb_interaction_capturing_twice(self, testdir):
|
||||||
p1 = testdir.makepyfile(
|
p1 = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
|
@ -703,7 +725,7 @@ class TestPDB:
|
||||||
assert "> PDB continue (IO-capturing resumed) >" in rest
|
assert "> PDB continue (IO-capturing resumed) >" in rest
|
||||||
else:
|
else:
|
||||||
assert "> PDB continue >" in rest
|
assert "> PDB continue >" in rest
|
||||||
assert "1 passed in" in rest
|
assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest
|
||||||
|
|
||||||
def test_pdb_used_outside_test(self, testdir):
|
def test_pdb_used_outside_test(self, testdir):
|
||||||
p1 = testdir.makepyfile(
|
p1 = testdir.makepyfile(
|
||||||
|
|
@ -1019,7 +1041,7 @@ class TestTraceOption:
|
||||||
child.sendline("q")
|
child.sendline("q")
|
||||||
child.expect_exact("Exit: Quitting debugger")
|
child.expect_exact("Exit: Quitting debugger")
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
assert "2 passed in" in rest
|
assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest
|
||||||
assert "reading from stdin while output" not in rest
|
assert "reading from stdin while output" not in rest
|
||||||
# Only printed once - not on stderr.
|
# Only printed once - not on stderr.
|
||||||
assert "Exit: Quitting debugger" not in child.before.decode("utf8")
|
assert "Exit: Quitting debugger" not in child.before.decode("utf8")
|
||||||
|
|
@ -1064,7 +1086,7 @@ class TestTraceOption:
|
||||||
child.sendline("c")
|
child.sendline("c")
|
||||||
child.expect_exact("> PDB continue (IO-capturing resumed) >")
|
child.expect_exact("> PDB continue (IO-capturing resumed) >")
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
assert "6 passed in" in rest
|
assert "= \x1b[32m\x1b[1m6 passed\x1b[0m\x1b[32m in" in rest
|
||||||
assert "reading from stdin while output" not in rest
|
assert "reading from stdin while output" not in rest
|
||||||
# Only printed once - not on stderr.
|
# Only printed once - not on stderr.
|
||||||
assert "Exit: Quitting debugger" not in child.before.decode("utf8")
|
assert "Exit: Quitting debugger" not in child.before.decode("utf8")
|
||||||
|
|
@ -1175,7 +1197,7 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture):
|
||||||
|
|
||||||
TestPDB.flush(child)
|
TestPDB.flush(child)
|
||||||
assert child.exitstatus == 0
|
assert child.exitstatus == 0
|
||||||
assert "= 1 passed in " in rest
|
assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest
|
||||||
assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest
|
assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,36 @@ class TestPytestPluginInteractions:
|
||||||
ihook_b = session.gethookproxy(testdir.tmpdir.join("tests"))
|
ihook_b = session.gethookproxy(testdir.tmpdir.join("tests"))
|
||||||
assert ihook_a is not ihook_b
|
assert ihook_a is not ihook_b
|
||||||
|
|
||||||
|
def test_hook_with_addoption(self, testdir):
|
||||||
|
"""Test that hooks can be used in a call to pytest_addoption"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
newhooks="""
|
||||||
|
import pytest
|
||||||
|
@pytest.hookspec(firstresult=True)
|
||||||
|
def pytest_default_value():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
testdir.makepyfile(
|
||||||
|
myplugin="""
|
||||||
|
import newhooks
|
||||||
|
def pytest_addhooks(pluginmanager):
|
||||||
|
pluginmanager.add_hookspecs(newhooks)
|
||||||
|
def pytest_addoption(parser, pluginmanager):
|
||||||
|
default_value = pluginmanager.hook.pytest_default_value()
|
||||||
|
parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
testdir.makeconftest(
|
||||||
|
"""
|
||||||
|
pytest_plugins=("myplugin",)
|
||||||
|
def pytest_default_value():
|
||||||
|
return "default_value"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
res = testdir.runpytest("--help")
|
||||||
|
res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"])
|
||||||
|
|
||||||
|
|
||||||
def test_default_markers(testdir):
|
def test_default_markers(testdir):
|
||||||
result = testdir.runpytest("--markers")
|
result = testdir.runpytest("--markers")
|
||||||
|
|
|
||||||
|
|
@ -121,17 +121,6 @@ def test_runresult_assertion_on_xpassed(testdir):
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_runresult_repr():
|
|
||||||
from _pytest.pytester import RunResult
|
|
||||||
|
|
||||||
assert (
|
|
||||||
repr(
|
|
||||||
RunResult(ret="ret", outlines=[""], errlines=["some", "errors"], duration=1)
|
|
||||||
)
|
|
||||||
== "<RunResult ret='ret' len(stdout.lines)=1 len(stderr.lines)=2 duration=1.00s>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_xpassed_with_strict_is_considered_a_failure(testdir):
|
def test_xpassed_with_strict_is_considered_a_failure(testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
|
@ -406,6 +395,27 @@ def test_testdir_subprocess(testdir):
|
||||||
assert testdir.runpytest_subprocess(testfile).ret == 0
|
assert testdir.runpytest_subprocess(testfile).ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdir_subprocess_via_runpytest_arg(testdir) -> None:
|
||||||
|
testfile = testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_testdir_subprocess(testdir):
|
||||||
|
import os
|
||||||
|
testfile = testdir.makepyfile(
|
||||||
|
\"""
|
||||||
|
import os
|
||||||
|
def test_one():
|
||||||
|
assert {} != os.getpid()
|
||||||
|
\""".format(os.getpid())
|
||||||
|
)
|
||||||
|
assert testdir.runpytest(testfile).ret == 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_subprocess(
|
||||||
|
"-p", "pytester", "--runpytest", "subprocess", testfile
|
||||||
|
)
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_args(testdir):
|
def test_unicode_args(testdir):
|
||||||
result = testdir.runpytest("-k", "💩")
|
result = testdir.runpytest("-k", "💩")
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
|
@ -457,6 +467,81 @@ def test_linematcher_with_nonlist():
|
||||||
assert lm._getlines(set()) == set()
|
assert lm._getlines(set()) == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_linematcher_match_failure():
|
||||||
|
lm = LineMatcher(["foo", "foo", "bar"])
|
||||||
|
with pytest.raises(pytest.fail.Exception) as e:
|
||||||
|
lm.fnmatch_lines(["foo", "f*", "baz"])
|
||||||
|
assert e.value.msg.splitlines() == [
|
||||||
|
"exact match: 'foo'",
|
||||||
|
"fnmatch: 'f*'",
|
||||||
|
" with: 'foo'",
|
||||||
|
"nomatch: 'baz'",
|
||||||
|
" and: 'bar'",
|
||||||
|
"remains unmatched: 'baz'",
|
||||||
|
]
|
||||||
|
|
||||||
|
lm = LineMatcher(["foo", "foo", "bar"])
|
||||||
|
with pytest.raises(pytest.fail.Exception) as e:
|
||||||
|
lm.re_match_lines(["foo", "^f.*", "baz"])
|
||||||
|
assert e.value.msg.splitlines() == [
|
||||||
|
"exact match: 'foo'",
|
||||||
|
"re.match: '^f.*'",
|
||||||
|
" with: 'foo'",
|
||||||
|
" nomatch: 'baz'",
|
||||||
|
" and: 'bar'",
|
||||||
|
"remains unmatched: 'baz'",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"])
|
||||||
|
def test_no_matching(function):
|
||||||
|
if function == "no_fnmatch_line":
|
||||||
|
good_pattern = "*.py OK*"
|
||||||
|
bad_pattern = "*X.py OK*"
|
||||||
|
else:
|
||||||
|
assert function == "no_re_match_line"
|
||||||
|
good_pattern = r".*py OK"
|
||||||
|
bad_pattern = r".*Xpy OK"
|
||||||
|
|
||||||
|
lm = LineMatcher(
|
||||||
|
[
|
||||||
|
"cachedir: .pytest_cache",
|
||||||
|
"collecting ... collected 1 item",
|
||||||
|
"",
|
||||||
|
"show_fixtures_per_test.py OK",
|
||||||
|
"=== elapsed 1s ===",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# check the function twice to ensure we don't accumulate the internal buffer
|
||||||
|
for i in range(2):
|
||||||
|
with pytest.raises(pytest.fail.Exception) as e:
|
||||||
|
func = getattr(lm, function)
|
||||||
|
func(good_pattern)
|
||||||
|
obtained = str(e.value).splitlines()
|
||||||
|
if function == "no_fnmatch_line":
|
||||||
|
assert obtained == [
|
||||||
|
"nomatch: '{}'".format(good_pattern),
|
||||||
|
" and: 'cachedir: .pytest_cache'",
|
||||||
|
" and: 'collecting ... collected 1 item'",
|
||||||
|
" and: ''",
|
||||||
|
"fnmatch: '{}'".format(good_pattern),
|
||||||
|
" with: 'show_fixtures_per_test.py OK'",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
assert obtained == [
|
||||||
|
"nomatch: '{}'".format(good_pattern),
|
||||||
|
" and: 'cachedir: .pytest_cache'",
|
||||||
|
" and: 'collecting ... collected 1 item'",
|
||||||
|
" and: ''",
|
||||||
|
"re.match: '{}'".format(good_pattern),
|
||||||
|
" with: 'show_fixtures_per_test.py OK'",
|
||||||
|
]
|
||||||
|
|
||||||
|
func = getattr(lm, function)
|
||||||
|
func(bad_pattern) # bad pattern does not match any line: passes
|
||||||
|
|
||||||
|
|
||||||
def test_pytester_addopts(request, monkeypatch):
|
def test_pytester_addopts(request, monkeypatch):
|
||||||
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
|
monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused")
|
||||||
|
|
||||||
|
|
@ -570,3 +655,22 @@ def test_spawn_uses_tmphome(testdir):
|
||||||
child = testdir.spawn_pytest(str(p1))
|
child = testdir.spawn_pytest(str(p1))
|
||||||
out = child.read()
|
out = child.read()
|
||||||
assert child.wait() == 0, out.decode("utf8")
|
assert child.wait() == 0, out.decode("utf8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_result_repr():
|
||||||
|
outlines = ["some", "normal", "output"]
|
||||||
|
errlines = ["some", "nasty", "errors", "happened"]
|
||||||
|
|
||||||
|
# known exit code
|
||||||
|
r = pytester.RunResult(1, outlines, errlines, duration=0.5)
|
||||||
|
assert (
|
||||||
|
repr(r) == "<RunResult ret=ExitCode.TESTS_FAILED len(stdout.lines)=3"
|
||||||
|
" len(stderr.lines)=4 duration=0.50s>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# unknown exit code: just the number
|
||||||
|
r = pytester.RunResult(99, outlines, errlines, duration=0.5)
|
||||||
|
assert (
|
||||||
|
repr(r) == "<RunResult ret=99 len(stdout.lines)=3"
|
||||||
|
" len(stderr.lines)=4 duration=0.50s>"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ class TestHooks:
|
||||||
data = pytestconfig.hook.pytest_report_to_serializable(
|
data = pytestconfig.hook.pytest_report_to_serializable(
|
||||||
config=pytestconfig, report=rep
|
config=pytestconfig, report=rep
|
||||||
)
|
)
|
||||||
assert data["_report_type"] == "TestReport"
|
assert data["$report_type"] == "TestReport"
|
||||||
new_rep = pytestconfig.hook.pytest_report_from_serializable(
|
new_rep = pytestconfig.hook.pytest_report_from_serializable(
|
||||||
config=pytestconfig, data=data
|
config=pytestconfig, data=data
|
||||||
)
|
)
|
||||||
|
|
@ -352,7 +352,7 @@ class TestHooks:
|
||||||
data = pytestconfig.hook.pytest_report_to_serializable(
|
data = pytestconfig.hook.pytest_report_to_serializable(
|
||||||
config=pytestconfig, report=rep
|
config=pytestconfig, report=rep
|
||||||
)
|
)
|
||||||
assert data["_report_type"] == "CollectReport"
|
assert data["$report_type"] == "CollectReport"
|
||||||
new_rep = pytestconfig.hook.pytest_report_from_serializable(
|
new_rep = pytestconfig.hook.pytest_report_from_serializable(
|
||||||
config=pytestconfig, data=data
|
config=pytestconfig, data=data
|
||||||
)
|
)
|
||||||
|
|
@ -376,7 +376,7 @@ class TestHooks:
|
||||||
data = pytestconfig.hook.pytest_report_to_serializable(
|
data = pytestconfig.hook.pytest_report_to_serializable(
|
||||||
config=pytestconfig, report=rep
|
config=pytestconfig, report=rep
|
||||||
)
|
)
|
||||||
data["_report_type"] = "Unknown"
|
data["$report_type"] = "Unknown"
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
_ = pytestconfig.hook.pytest_report_from_serializable(
|
_ = pytestconfig.hook.pytest_report_from_serializable(
|
||||||
config=pytestconfig, data=data
|
config=pytestconfig, data=data
|
||||||
|
|
|
||||||
|
|
@ -483,13 +483,22 @@ def test_callinfo():
|
||||||
assert ci.result == 0
|
assert ci.result == 0
|
||||||
assert "result" in repr(ci)
|
assert "result" in repr(ci)
|
||||||
assert repr(ci) == "<CallInfo when='123' result: 0>"
|
assert repr(ci) == "<CallInfo when='123' result: 0>"
|
||||||
|
assert str(ci) == "<CallInfo when='123' result: 0>"
|
||||||
|
|
||||||
ci = runner.CallInfo.from_call(lambda: 0 / 0, "123")
|
ci = runner.CallInfo.from_call(lambda: 0 / 0, "123")
|
||||||
assert ci.when == "123"
|
assert ci.when == "123"
|
||||||
assert not hasattr(ci, "result")
|
assert not hasattr(ci, "result")
|
||||||
assert repr(ci) == "<CallInfo when='123' exception: division by zero>"
|
assert repr(ci) == "<CallInfo when='123' excinfo={!r}>".format(ci.excinfo)
|
||||||
|
assert str(ci) == repr(ci)
|
||||||
assert ci.excinfo
|
assert ci.excinfo
|
||||||
assert "exc" in repr(ci)
|
|
||||||
|
# Newlines are escaped.
|
||||||
|
def raise_assertion():
|
||||||
|
assert 0, "assert_msg"
|
||||||
|
|
||||||
|
ci = runner.CallInfo.from_call(raise_assertion, "call")
|
||||||
|
assert repr(ci) == "<CallInfo when='call' excinfo={!r}>".format(ci.excinfo)
|
||||||
|
assert "\n" not in repr(ci)
|
||||||
|
|
||||||
|
|
||||||
# design question: do we want general hooks in python files?
|
# design question: do we want general hooks in python files?
|
||||||
|
|
@ -588,7 +597,7 @@ def test_pytest_exit_returncode(testdir):
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"])
|
result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"])
|
||||||
|
|
||||||
assert _strip_resource_warnings(result.stderr.lines) == [""]
|
assert _strip_resource_warnings(result.stderr.lines) == []
|
||||||
assert result.ret == 99
|
assert result.ret == 99
|
||||||
|
|
||||||
# It prints to stderr also in case of exit during pytest_sessionstart.
|
# It prints to stderr also in case of exit during pytest_sessionstart.
|
||||||
|
|
@ -603,8 +612,7 @@ def test_pytest_exit_returncode(testdir):
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"])
|
result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"])
|
||||||
assert _strip_resource_warnings(result.stderr.lines) == [
|
assert _strip_resource_warnings(result.stderr.lines) == [
|
||||||
"Exit: during_sessionstart",
|
"Exit: during_sessionstart"
|
||||||
"",
|
|
||||||
]
|
]
|
||||||
assert result.ret == 98
|
assert result.ret == 98
|
||||||
|
|
||||||
|
|
@ -622,7 +630,7 @@ def test_pytest_fail_notrace_runtest(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["world", "hello"])
|
result.stdout.fnmatch_lines(["world", "hello"])
|
||||||
assert "def teardown_function" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*def teardown_function*")
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_fail_notrace_collection(testdir):
|
def test_pytest_fail_notrace_collection(testdir):
|
||||||
|
|
@ -637,7 +645,7 @@ def test_pytest_fail_notrace_collection(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["hello"])
|
result.stdout.fnmatch_lines(["hello"])
|
||||||
assert "def some_internal_function()" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*def some_internal_function()*")
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_fail_notrace_non_ascii(testdir):
|
def test_pytest_fail_notrace_non_ascii(testdir):
|
||||||
|
|
@ -655,7 +663,7 @@ def test_pytest_fail_notrace_non_ascii(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"])
|
result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"])
|
||||||
assert "def test_hello" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*def test_hello*")
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_no_tests_collected_exit_status(testdir):
|
def test_pytest_no_tests_collected_exit_status(testdir):
|
||||||
|
|
@ -820,7 +828,7 @@ def test_failure_in_setup(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--tb=line")
|
result = testdir.runpytest("--tb=line")
|
||||||
assert "def setup_module" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*def setup_module*")
|
||||||
|
|
||||||
|
|
||||||
def test_makereport_getsource(testdir):
|
def test_makereport_getsource(testdir):
|
||||||
|
|
@ -832,7 +840,7 @@ def test_makereport_getsource(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "INTERNALERROR" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
result.stdout.fnmatch_lines(["*else: assert False*"])
|
result.stdout.fnmatch_lines(["*else: assert False*"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -863,7 +871,7 @@ def test_makereport_getsource_dynamic_code(testdir, monkeypatch):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-vv")
|
result = testdir.runpytest("-vv")
|
||||||
assert "INTERNALERROR" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*INTERNALERROR*")
|
||||||
result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"])
|
result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,10 +234,10 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
|
||||||
"*ValueError*42*",
|
"*ValueError*42*",
|
||||||
"*function2*",
|
"*function2*",
|
||||||
"*ValueError*42*",
|
"*ValueError*42*",
|
||||||
"*2 error*",
|
"*2 errors*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "xyz43" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*xyz43*")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arg", ["", "arg"])
|
@pytest.mark.parametrize("arg", ["", "arg"])
|
||||||
|
|
|
||||||
|
|
@ -102,15 +102,20 @@ class SessionTests:
|
||||||
p = testdir.makepyfile(
|
p = testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
class reprexc(BaseException):
|
||||||
|
def __str__(self):
|
||||||
|
return "Ha Ha fooled you, I'm a broken repr()."
|
||||||
|
|
||||||
class BrokenRepr1(object):
|
class BrokenRepr1(object):
|
||||||
foo=0
|
foo=0
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
raise Exception("Ha Ha fooled you, I'm a broken repr().")
|
raise reprexc
|
||||||
|
|
||||||
class TestBrokenClass(object):
|
class TestBrokenClass(object):
|
||||||
def test_explicit_bad_repr(self):
|
def test_explicit_bad_repr(self):
|
||||||
t = BrokenRepr1()
|
t = BrokenRepr1()
|
||||||
with pytest.raises(Exception, match="I'm a broken repr"):
|
with pytest.raises(BaseException, match="broken repr"):
|
||||||
repr(t)
|
repr(t)
|
||||||
|
|
||||||
def test_implicit_bad_repr1(self):
|
def test_implicit_bad_repr1(self):
|
||||||
|
|
@ -123,12 +128,7 @@ class SessionTests:
|
||||||
passed, skipped, failed = reprec.listoutcomes()
|
passed, skipped, failed = reprec.listoutcomes()
|
||||||
assert (len(passed), len(skipped), len(failed)) == (1, 0, 1)
|
assert (len(passed), len(skipped), len(failed)) == (1, 0, 1)
|
||||||
out = failed[0].longrepr.reprcrash.message
|
out = failed[0].longrepr.reprcrash.message
|
||||||
assert (
|
assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1
|
||||||
out.find(
|
|
||||||
"""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]"""
|
|
||||||
)
|
|
||||||
!= -1
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_broken_repr_with_showlocals_verbose(self, testdir):
|
def test_broken_repr_with_showlocals_verbose(self, testdir):
|
||||||
p = testdir.makepyfile(
|
p = testdir.makepyfile(
|
||||||
|
|
@ -151,7 +151,7 @@ class SessionTests:
|
||||||
assert repr_locals.lines
|
assert repr_locals.lines
|
||||||
assert len(repr_locals.lines) == 1
|
assert len(repr_locals.lines) == 1
|
||||||
assert repr_locals.lines[0].startswith(
|
assert repr_locals.lines[0].startswith(
|
||||||
'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr'
|
"x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_skip_file_by_conftest(self, testdir):
|
def test_skip_file_by_conftest(self, testdir):
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ def test_show_only_active_fixtures(testdir, mode, dummy_yaml_custom_test):
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*SETUP F arg1*", "*test_arg1 (fixtures used: arg1)*", "*TEARDOWN F arg1*"]
|
["*SETUP F arg1*", "*test_arg1 (fixtures used: arg1)*", "*TEARDOWN F arg1*"]
|
||||||
)
|
)
|
||||||
assert "_arg0" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*_arg0*")
|
||||||
|
|
||||||
|
|
||||||
def test_show_different_scopes(testdir, mode):
|
def test_show_different_scopes(testdir, mode):
|
||||||
|
|
@ -115,7 +115,7 @@ class TestEvaluator:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_skipif_class(self, testdir):
|
def test_skipif_class(self, testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
|
|
@ -731,23 +731,37 @@ def test_skipif_class(testdir):
|
||||||
def test_skipped_reasons_functional(testdir):
|
def test_skipped_reasons_functional(testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
test_one="""
|
test_one="""
|
||||||
|
import pytest
|
||||||
from conftest import doskip
|
from conftest import doskip
|
||||||
|
|
||||||
def setup_function(func):
|
def setup_function(func):
|
||||||
doskip()
|
doskip()
|
||||||
|
|
||||||
def test_func():
|
def test_func():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_method(self):
|
def test_method(self):
|
||||||
doskip()
|
doskip()
|
||||||
""",
|
|
||||||
|
@pytest.mark.skip("via_decorator")
|
||||||
|
def test_deco(self):
|
||||||
|
assert 0
|
||||||
|
""",
|
||||||
conftest="""
|
conftest="""
|
||||||
import pytest
|
import pytest, sys
|
||||||
def doskip():
|
def doskip():
|
||||||
|
assert sys._getframe().f_lineno == 3
|
||||||
pytest.skip('test')
|
pytest.skip('test')
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-rs")
|
result = testdir.runpytest("-rs")
|
||||||
result.stdout.fnmatch_lines(["*SKIP*2*conftest.py:4: test"])
|
result.stdout.fnmatch_lines_random(
|
||||||
|
[
|
||||||
|
"SKIPPED [[]2[]] */conftest.py:4: test",
|
||||||
|
"SKIPPED [[]1[]] test_one.py:14: via_decorator",
|
||||||
|
]
|
||||||
|
)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -886,7 +900,7 @@ def test_errors_in_xfail_skip_expressions(testdir):
|
||||||
" syntax error",
|
" syntax error",
|
||||||
markline,
|
markline,
|
||||||
"SyntaxError: invalid syntax",
|
"SyntaxError: invalid syntax",
|
||||||
"*1 pass*2 error*",
|
"*1 pass*2 errors*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -949,7 +963,7 @@ def test_xfail_test_setup_exception(testdir):
|
||||||
result = testdir.runpytest(p)
|
result = testdir.runpytest(p)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
assert "xfailed" in result.stdout.str()
|
assert "xfailed" in result.stdout.str()
|
||||||
assert "xpassed" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*xpassed*")
|
||||||
|
|
||||||
|
|
||||||
def test_imperativeskip_on_xfail_test(testdir):
|
def test_imperativeskip_on_xfail_test(testdir):
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ def test_stop_on_collection_errors(broken_testdir, broken_first):
|
||||||
if broken_first:
|
if broken_first:
|
||||||
files.reverse()
|
files.reverse()
|
||||||
result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files)
|
result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files)
|
||||||
result.stdout.fnmatch_lines("*errors during collection*")
|
result.stdout.fnmatch_lines("*error during collection*")
|
||||||
|
|
||||||
|
|
||||||
def test_xfail_handling(testdir):
|
def test_xfail_handling(testdir):
|
||||||
|
|
|
||||||
|
|
@ -21,30 +21,26 @@ from _pytest.terminal import getreportopt
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"])
|
DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"])
|
||||||
|
RED = r"\x1b\[31m"
|
||||||
|
GREEN = r"\x1b\[32m"
|
||||||
|
YELLOW = r"\x1b\[33m"
|
||||||
|
RESET = r"\x1b\[0m"
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
def __init__(self, verbosity=0, fulltrace=False):
|
def __init__(self, verbosity=0):
|
||||||
self.verbosity = verbosity
|
self.verbosity = verbosity
|
||||||
self.fulltrace = fulltrace
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def args(self):
|
def args(self):
|
||||||
values = []
|
values = []
|
||||||
values.append("--verbosity=%d" % self.verbosity)
|
values.append("--verbosity=%d" % self.verbosity)
|
||||||
if self.fulltrace:
|
|
||||||
values.append("--fulltrace")
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
params=[
|
params=[Option(verbosity=0), Option(verbosity=1), Option(verbosity=-1)],
|
||||||
Option(verbosity=0),
|
ids=["default", "verbose", "quiet"],
|
||||||
Option(verbosity=1),
|
|
||||||
Option(verbosity=-1),
|
|
||||||
Option(fulltrace=True),
|
|
||||||
],
|
|
||||||
ids=["default", "verbose", "quiet", "fulltrace"],
|
|
||||||
)
|
)
|
||||||
def option(request):
|
def option(request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
@ -165,7 +161,7 @@ class TestTerminal:
|
||||||
child.expect(r"collecting 2 items")
|
child.expect(r"collecting 2 items")
|
||||||
child.expect(r"collected 2 items")
|
child.expect(r"collected 2 items")
|
||||||
rest = child.read().decode("utf8")
|
rest = child.read().decode("utf8")
|
||||||
assert "2 passed in" in rest
|
assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest
|
||||||
|
|
||||||
def test_itemreport_subclasses_show_subclassed_file(self, testdir):
|
def test_itemreport_subclasses_show_subclassed_file(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
|
|
@ -205,9 +201,10 @@ class TestTerminal:
|
||||||
result = testdir.runpytest("-vv")
|
result = testdir.runpytest("-vv")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"])
|
result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"])
|
||||||
assert " <- " not in result.stdout.str()
|
result.stdout.no_fnmatch_line("* <- *")
|
||||||
|
|
||||||
def test_keyboard_interrupt(self, testdir, option):
|
@pytest.mark.parametrize("fulltrace", ("", "--fulltrace"))
|
||||||
|
def test_keyboard_interrupt(self, testdir, fulltrace):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_foobar():
|
def test_foobar():
|
||||||
|
|
@ -219,7 +216,7 @@ class TestTerminal:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
result = testdir.runpytest(*option.args, no_reraise_ctrlc=True)
|
result = testdir.runpytest(fulltrace, no_reraise_ctrlc=True)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
" def test_foobar():",
|
" def test_foobar():",
|
||||||
|
|
@ -228,7 +225,7 @@ class TestTerminal:
|
||||||
"*_keyboard_interrupt.py:6: KeyboardInterrupt*",
|
"*_keyboard_interrupt.py:6: KeyboardInterrupt*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if option.fulltrace:
|
if fulltrace:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*raise KeyboardInterrupt # simulating the user*"]
|
["*raise KeyboardInterrupt # simulating the user*"]
|
||||||
)
|
)
|
||||||
|
|
@ -560,7 +557,7 @@ class TestTerminalFunctional:
|
||||||
"*= 2 passed, 1 deselected in * =*",
|
"*= 2 passed, 1 deselected in * =*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "= 1 deselected =" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*= 1 deselected =*")
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
def test_no_skip_summary_if_failure(self, testdir):
|
def test_no_skip_summary_if_failure(self, testdir):
|
||||||
|
|
@ -760,7 +757,7 @@ def test_fail_extra_reporting(testdir, monkeypatch):
|
||||||
monkeypatch.setenv("COLUMNS", "80")
|
monkeypatch.setenv("COLUMNS", "80")
|
||||||
testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
|
testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "short test summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*short test summary*")
|
||||||
result = testdir.runpytest("-rf")
|
result = testdir.runpytest("-rf")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
|
|
@ -773,13 +770,13 @@ def test_fail_extra_reporting(testdir, monkeypatch):
|
||||||
def test_fail_reporting_on_pass(testdir):
|
def test_fail_reporting_on_pass(testdir):
|
||||||
testdir.makepyfile("def test_this(): assert 1")
|
testdir.makepyfile("def test_this(): assert 1")
|
||||||
result = testdir.runpytest("-rf")
|
result = testdir.runpytest("-rf")
|
||||||
assert "short test summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*short test summary*")
|
||||||
|
|
||||||
|
|
||||||
def test_pass_extra_reporting(testdir):
|
def test_pass_extra_reporting(testdir):
|
||||||
testdir.makepyfile("def test_this(): assert 1")
|
testdir.makepyfile("def test_this(): assert 1")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "short test summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*short test summary*")
|
||||||
result = testdir.runpytest("-rp")
|
result = testdir.runpytest("-rp")
|
||||||
result.stdout.fnmatch_lines(["*test summary*", "PASS*test_pass_extra_reporting*"])
|
result.stdout.fnmatch_lines(["*test summary*", "PASS*test_pass_extra_reporting*"])
|
||||||
|
|
||||||
|
|
@ -787,7 +784,7 @@ def test_pass_extra_reporting(testdir):
|
||||||
def test_pass_reporting_on_fail(testdir):
|
def test_pass_reporting_on_fail(testdir):
|
||||||
testdir.makepyfile("def test_this(): assert 0")
|
testdir.makepyfile("def test_this(): assert 0")
|
||||||
result = testdir.runpytest("-rp")
|
result = testdir.runpytest("-rp")
|
||||||
assert "short test summary" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*short test summary*")
|
||||||
|
|
||||||
|
|
||||||
def test_pass_output_reporting(testdir):
|
def test_pass_output_reporting(testdir):
|
||||||
|
|
@ -830,7 +827,7 @@ def test_color_no(testdir):
|
||||||
testdir.makepyfile("def test_this(): assert 1")
|
testdir.makepyfile("def test_this(): assert 1")
|
||||||
result = testdir.runpytest("--color=no")
|
result = testdir.runpytest("--color=no")
|
||||||
assert "test session starts" in result.stdout.str()
|
assert "test session starts" in result.stdout.str()
|
||||||
assert "\x1b[1m" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*\x1b[1m*")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("verbose", [True, False])
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
|
@ -852,7 +849,7 @@ def test_color_yes_collection_on_non_atty(testdir, verbose):
|
||||||
result = testdir.runpytest(*args)
|
result = testdir.runpytest(*args)
|
||||||
assert "test session starts" in result.stdout.str()
|
assert "test session starts" in result.stdout.str()
|
||||||
assert "\x1b[1m" in result.stdout.str()
|
assert "\x1b[1m" in result.stdout.str()
|
||||||
assert "collecting 10 items" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*collecting 10 items*")
|
||||||
if verbose:
|
if verbose:
|
||||||
assert "collecting ..." in result.stdout.str()
|
assert "collecting ..." in result.stdout.str()
|
||||||
assert "collected 10 items" in result.stdout.str()
|
assert "collected 10 items" in result.stdout.str()
|
||||||
|
|
@ -966,7 +963,31 @@ class TestGenericReporting:
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--maxfail=2", *option.args)
|
result = testdir.runpytest("--maxfail=2", *option.args)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*def test_1():*", "*def test_2():*", "*2 failed*"]
|
[
|
||||||
|
"*def test_1():*",
|
||||||
|
"*def test_2():*",
|
||||||
|
"*! stopping after 2 failures !*",
|
||||||
|
"*2 failed*",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_maxfailures_with_interrupted(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
def test(request):
|
||||||
|
request.session.shouldstop = "session_interrupted"
|
||||||
|
assert 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("--maxfail=1", "-ra")
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*= short test summary info =*",
|
||||||
|
"FAILED *",
|
||||||
|
"*! stopping after 1 failures !*",
|
||||||
|
"*! session_interrupted !*",
|
||||||
|
"*= 1 failed in*",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tb_option(self, testdir, option):
|
def test_tb_option(self, testdir, option):
|
||||||
|
|
@ -1215,7 +1236,7 @@ def test_terminal_summary_warnings_are_displayed(testdir):
|
||||||
"*== 1 failed, 2 warnings in *",
|
"*== 1 failed, 2 warnings in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "None" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*None*")
|
||||||
stdout = result.stdout.str()
|
stdout = result.stdout.str()
|
||||||
assert stdout.count("warning_from_test") == 1
|
assert stdout.count("warning_from_test") == 1
|
||||||
assert stdout.count("=== warnings summary ") == 2
|
assert stdout.count("=== warnings summary ") == 2
|
||||||
|
|
@ -1237,10 +1258,10 @@ def test_terminal_summary_warnings_header_once(testdir):
|
||||||
"*= warnings summary =*",
|
"*= warnings summary =*",
|
||||||
"*warning_from_test*",
|
"*warning_from_test*",
|
||||||
"*= short test summary info =*",
|
"*= short test summary info =*",
|
||||||
"*== 1 failed, 1 warnings in *",
|
"*== 1 failed, 1 warning in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "None" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*None*")
|
||||||
stdout = result.stdout.str()
|
stdout = result.stdout.str()
|
||||||
assert stdout.count("warning_from_test") == 1
|
assert stdout.count("warning_from_test") == 1
|
||||||
assert stdout.count("=== warnings summary ") == 1
|
assert stdout.count("=== warnings summary ") == 1
|
||||||
|
|
@ -1253,42 +1274,120 @@ def test_terminal_summary_warnings_header_once(testdir):
|
||||||
# dict value, not the actual contents, so tuples of anything
|
# dict value, not the actual contents, so tuples of anything
|
||||||
# suffice
|
# suffice
|
||||||
# Important statuses -- the highest priority of these always wins
|
# Important statuses -- the highest priority of these always wins
|
||||||
("red", "1 failed", {"failed": (1,)}),
|
("red", [("1 failed", {"bold": True, "red": True})], {"failed": (1,)}),
|
||||||
("red", "1 failed, 1 passed", {"failed": (1,), "passed": (1,)}),
|
(
|
||||||
("red", "1 error", {"error": (1,)}),
|
"red",
|
||||||
("red", "1 passed, 1 error", {"error": (1,), "passed": (1,)}),
|
[
|
||||||
|
("1 failed", {"bold": True, "red": True}),
|
||||||
|
("1 passed", {"bold": False, "green": True}),
|
||||||
|
],
|
||||||
|
{"failed": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}),
|
||||||
|
("red", [("2 errors", {"bold": True, "red": True})], {"error": (1, 2)}),
|
||||||
|
(
|
||||||
|
"red",
|
||||||
|
[
|
||||||
|
("1 passed", {"bold": False, "green": True}),
|
||||||
|
("1 error", {"bold": True, "red": True}),
|
||||||
|
],
|
||||||
|
{"error": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
# (a status that's not known to the code)
|
# (a status that's not known to the code)
|
||||||
("yellow", "1 weird", {"weird": (1,)}),
|
("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": (1,)}),
|
||||||
("yellow", "1 passed, 1 weird", {"weird": (1,), "passed": (1,)}),
|
(
|
||||||
("yellow", "1 warnings", {"warnings": (1,)}),
|
"yellow",
|
||||||
("yellow", "1 passed, 1 warnings", {"warnings": (1,), "passed": (1,)}),
|
[
|
||||||
("green", "5 passed", {"passed": (1, 2, 3, 4, 5)}),
|
("1 passed", {"bold": False, "green": True}),
|
||||||
|
("1 weird", {"bold": True, "yellow": True}),
|
||||||
|
],
|
||||||
|
{"weird": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": (1,)}),
|
||||||
|
(
|
||||||
|
"yellow",
|
||||||
|
[
|
||||||
|
("1 passed", {"bold": False, "green": True}),
|
||||||
|
("1 warning", {"bold": True, "yellow": True}),
|
||||||
|
],
|
||||||
|
{"warnings": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"green",
|
||||||
|
[("5 passed", {"bold": True, "green": True})],
|
||||||
|
{"passed": (1, 2, 3, 4, 5)},
|
||||||
|
),
|
||||||
# "Boring" statuses. These have no effect on the color of the summary
|
# "Boring" statuses. These have no effect on the color of the summary
|
||||||
# line. Thus, if *every* test has a boring status, the summary line stays
|
# line. Thus, if *every* test has a boring status, the summary line stays
|
||||||
# at its default color, i.e. yellow, to warn the user that the test run
|
# at its default color, i.e. yellow, to warn the user that the test run
|
||||||
# produced no useful information
|
# produced no useful information
|
||||||
("yellow", "1 skipped", {"skipped": (1,)}),
|
("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": (1,)}),
|
||||||
("green", "1 passed, 1 skipped", {"skipped": (1,), "passed": (1,)}),
|
(
|
||||||
("yellow", "1 deselected", {"deselected": (1,)}),
|
"green",
|
||||||
("green", "1 passed, 1 deselected", {"deselected": (1,), "passed": (1,)}),
|
[
|
||||||
("yellow", "1 xfailed", {"xfailed": (1,)}),
|
("1 passed", {"bold": True, "green": True}),
|
||||||
("green", "1 passed, 1 xfailed", {"xfailed": (1,), "passed": (1,)}),
|
("1 skipped", {"bold": False, "yellow": True}),
|
||||||
("yellow", "1 xpassed", {"xpassed": (1,)}),
|
],
|
||||||
("green", "1 passed, 1 xpassed", {"xpassed": (1,), "passed": (1,)}),
|
{"skipped": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"yellow",
|
||||||
|
[("1 deselected", {"bold": True, "yellow": True})],
|
||||||
|
{"deselected": (1,)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"green",
|
||||||
|
[
|
||||||
|
("1 passed", {"bold": True, "green": True}),
|
||||||
|
("1 deselected", {"bold": False, "yellow": True}),
|
||||||
|
],
|
||||||
|
{"deselected": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": (1,)}),
|
||||||
|
(
|
||||||
|
"green",
|
||||||
|
[
|
||||||
|
("1 passed", {"bold": True, "green": True}),
|
||||||
|
("1 xfailed", {"bold": False, "yellow": True}),
|
||||||
|
],
|
||||||
|
{"xfailed": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
|
("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}),
|
||||||
|
(
|
||||||
|
"green",
|
||||||
|
[
|
||||||
|
("1 passed", {"bold": True, "green": True}),
|
||||||
|
("1 xpassed", {"bold": False, "yellow": True}),
|
||||||
|
],
|
||||||
|
{"xpassed": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
# Likewise if no tests were found at all
|
# Likewise if no tests were found at all
|
||||||
("yellow", "no tests ran", {}),
|
("yellow", [("no tests ran", {"yellow": True})], {}),
|
||||||
# Test the empty-key special case
|
# Test the empty-key special case
|
||||||
("yellow", "no tests ran", {"": (1,)}),
|
("yellow", [("no tests ran", {"yellow": True})], {"": (1,)}),
|
||||||
("green", "1 passed", {"": (1,), "passed": (1,)}),
|
(
|
||||||
|
"green",
|
||||||
|
[("1 passed", {"bold": True, "green": True})],
|
||||||
|
{"": (1,), "passed": (1,)},
|
||||||
|
),
|
||||||
# A couple more complex combinations
|
# A couple more complex combinations
|
||||||
(
|
(
|
||||||
"red",
|
"red",
|
||||||
"1 failed, 2 passed, 3 xfailed",
|
[
|
||||||
|
("1 failed", {"bold": True, "red": True}),
|
||||||
|
("2 passed", {"bold": False, "green": True}),
|
||||||
|
("3 xfailed", {"bold": False, "yellow": True}),
|
||||||
|
],
|
||||||
{"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)},
|
{"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"green",
|
"green",
|
||||||
"1 passed, 2 skipped, 3 deselected, 2 xfailed",
|
[
|
||||||
|
("1 passed", {"bold": True, "green": True}),
|
||||||
|
("2 skipped", {"bold": False, "yellow": True}),
|
||||||
|
("3 deselected", {"bold": False, "yellow": True}),
|
||||||
|
("2 xfailed", {"bold": False, "yellow": True}),
|
||||||
|
],
|
||||||
{
|
{
|
||||||
"passed": (1,),
|
"passed": (1,),
|
||||||
"skipped": (1, 2),
|
"skipped": (1, 2),
|
||||||
|
|
@ -1314,11 +1413,11 @@ def test_skip_counting_towards_summary():
|
||||||
r1 = DummyReport()
|
r1 = DummyReport()
|
||||||
r2 = DummyReport()
|
r2 = DummyReport()
|
||||||
res = build_summary_stats_line({"failed": (r1, r2)})
|
res = build_summary_stats_line({"failed": (r1, r2)})
|
||||||
assert res == ("2 failed", "red")
|
assert res == ([("2 failed", {"bold": True, "red": True})], "red")
|
||||||
|
|
||||||
r1.count_towards_summary = False
|
r1.count_towards_summary = False
|
||||||
res = build_summary_stats_line({"failed": (r1, r2)})
|
res = build_summary_stats_line({"failed": (r1, r2)})
|
||||||
assert res == ("1 failed", "red")
|
assert res == ([("1 failed", {"bold": True, "red": True})], "red")
|
||||||
|
|
||||||
|
|
||||||
class TestClassicOutputStyle:
|
class TestClassicOutputStyle:
|
||||||
|
|
@ -1403,7 +1502,7 @@ class TestProgressOutputStyle:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
output = testdir.runpytest()
|
output = testdir.runpytest()
|
||||||
assert "ZeroDivisionError" not in output.stdout.str()
|
output.stdout.no_fnmatch_line("*ZeroDivisionError*")
|
||||||
output.stdout.fnmatch_lines(["=* 2 passed in *="])
|
output.stdout.fnmatch_lines(["=* 2 passed in *="])
|
||||||
|
|
||||||
def test_normal(self, many_tests_files, testdir):
|
def test_normal(self, many_tests_files, testdir):
|
||||||
|
|
@ -1416,6 +1515,43 @@ class TestProgressOutputStyle:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_colored_progress(self, testdir, monkeypatch):
|
||||||
|
monkeypatch.setenv("PY_COLORS", "1")
|
||||||
|
testdir.makepyfile(
|
||||||
|
test_bar="""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize('i', range(10))
|
||||||
|
def test_bar(i): pass
|
||||||
|
""",
|
||||||
|
test_foo="""
|
||||||
|
import pytest
|
||||||
|
import warnings
|
||||||
|
@pytest.mark.parametrize('i', range(5))
|
||||||
|
def test_foo(i):
|
||||||
|
warnings.warn(DeprecationWarning("collection"))
|
||||||
|
pass
|
||||||
|
""",
|
||||||
|
test_foobar="""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.parametrize('i', range(5))
|
||||||
|
def test_foobar(i): raise ValueError()
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
output = testdir.runpytest()
|
||||||
|
output.stdout.re_match_lines(
|
||||||
|
[
|
||||||
|
r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 50%\]{reset}".format(
|
||||||
|
green=GREEN, reset=RESET
|
||||||
|
),
|
||||||
|
r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 75%\]{reset}".format(
|
||||||
|
green=GREEN, reset=RESET, yellow=YELLOW
|
||||||
|
),
|
||||||
|
r"test_foobar.py ({red}F{reset}){{5}}{red} \s+ \[100%\]{reset}".format(
|
||||||
|
reset=RESET, red=RED
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def test_count(self, many_tests_files, testdir):
|
def test_count(self, many_tests_files, testdir):
|
||||||
testdir.makeini(
|
testdir.makeini(
|
||||||
"""
|
"""
|
||||||
|
|
@ -1495,7 +1631,7 @@ class TestProgressOutputStyle:
|
||||||
)
|
)
|
||||||
|
|
||||||
output = testdir.runpytest("--capture=no")
|
output = testdir.runpytest("--capture=no")
|
||||||
assert "%]" not in output.stdout.str()
|
output.stdout.no_fnmatch_line("*%]*")
|
||||||
|
|
||||||
|
|
||||||
class TestProgressWithTeardown:
|
class TestProgressWithTeardown:
|
||||||
|
|
@ -1696,3 +1832,20 @@ def test_format_session_duration(seconds, expected):
|
||||||
from _pytest.terminal import format_session_duration
|
from _pytest.terminal import format_session_duration
|
||||||
|
|
||||||
assert format_session_duration(seconds) == expected
|
assert format_session_duration(seconds) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_collecterror(testdir):
|
||||||
|
p1 = testdir.makepyfile("raise SyntaxError()")
|
||||||
|
result = testdir.runpytest("-ra", str(p1))
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"collected 0 items / 1 error",
|
||||||
|
"*= ERRORS =*",
|
||||||
|
"*_ ERROR collecting test_collecterror.py _*",
|
||||||
|
"E SyntaxError: *",
|
||||||
|
"*= short test summary info =*",
|
||||||
|
"ERROR test_collecterror.py",
|
||||||
|
"*! Interrupted: 1 error during collection !*",
|
||||||
|
"*= 1 error in *",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ class TestNumberedDir:
|
||||||
registry = []
|
registry = []
|
||||||
register_cleanup_lock_removal(lock, register=registry.append)
|
register_cleanup_lock_removal(lock, register=registry.append)
|
||||||
|
|
||||||
cleanup_func, = registry
|
(cleanup_func,) = registry
|
||||||
|
|
||||||
assert lock.is_file()
|
assert lock.is_file()
|
||||||
|
|
||||||
|
|
@ -388,11 +388,21 @@ class TestRmRf:
|
||||||
assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
|
||||||
# unknown function
|
# unknown function
|
||||||
with pytest.warns(pytest.PytestWarning):
|
with pytest.warns(
|
||||||
|
pytest.PytestWarning,
|
||||||
|
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ",
|
||||||
|
):
|
||||||
exc_info = (None, PermissionError(), None)
|
exc_info = (None, PermissionError(), None)
|
||||||
on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
|
|
||||||
|
# ignored function
|
||||||
|
with pytest.warns(None) as warninfo:
|
||||||
|
exc_info = (None, PermissionError(), None)
|
||||||
|
on_rm_rf_error(os.open, str(fn), exc_info, start_path=tmp_path)
|
||||||
|
assert fn.is_file()
|
||||||
|
assert not [x.message for x in warninfo]
|
||||||
|
|
||||||
exc_info = (None, PermissionError(), None)
|
exc_info = (None, PermissionError(), None)
|
||||||
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
|
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
|
||||||
assert not fn.is_file()
|
assert not fn.is_file()
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ def test_unittest_skip_issue148(testdir):
|
||||||
def test_method_and_teardown_failing_reporting(testdir):
|
def test_method_and_teardown_failing_reporting(testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import unittest, pytest
|
import unittest
|
||||||
class TC(unittest.TestCase):
|
class TC(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
assert 0, "down1"
|
assert 0, "down1"
|
||||||
|
|
@ -270,7 +270,7 @@ def test_setup_failure_is_shown(testdir):
|
||||||
result = testdir.runpytest("-s")
|
result = testdir.runpytest("-s")
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
result.stdout.fnmatch_lines(["*setUp*", "*assert 0*down1*", "*1 failed*"])
|
result.stdout.fnmatch_lines(["*setUp*", "*assert 0*down1*", "*1 failed*"])
|
||||||
assert "never42" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*never42*")
|
||||||
|
|
||||||
|
|
||||||
def test_setup_setUpClass(testdir):
|
def test_setup_setUpClass(testdir):
|
||||||
|
|
@ -342,7 +342,7 @@ def test_testcase_adderrorandfailure_defers(testdir, type):
|
||||||
% (type, type)
|
% (type, type)
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "should not raise" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*should not raise*")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("type", ["Error", "Failure"])
|
@pytest.mark.parametrize("type", ["Error", "Failure"])
|
||||||
|
|
@ -383,7 +383,7 @@ def test_testcase_custom_exception_info(testdir, type):
|
||||||
|
|
||||||
|
|
||||||
def test_testcase_totally_incompatible_exception_info(testdir):
|
def test_testcase_totally_incompatible_exception_info(testdir):
|
||||||
item, = testdir.getitems(
|
(item,) = testdir.getitems(
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
class MyTestCase(TestCase):
|
class MyTestCase(TestCase):
|
||||||
|
|
@ -530,19 +530,31 @@ class TestTrialUnittest:
|
||||||
# will crash both at test time and at teardown
|
# will crash both at test time and at teardown
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
# Ignore DeprecationWarning (for `cmp`) from attrs through twisted,
|
||||||
|
# for stable test results.
|
||||||
|
result = testdir.runpytest(
|
||||||
|
"-vv", "-oconsole_output_style=classic", "-W", "ignore::DeprecationWarning"
|
||||||
|
)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
|
"test_trial_error.py::TC::test_four FAILED",
|
||||||
|
"test_trial_error.py::TC::test_four ERROR",
|
||||||
|
"test_trial_error.py::TC::test_one FAILED",
|
||||||
|
"test_trial_error.py::TC::test_three FAILED",
|
||||||
|
"test_trial_error.py::TC::test_two FAILED",
|
||||||
"*ERRORS*",
|
"*ERRORS*",
|
||||||
|
"*_ ERROR at teardown of TC.test_four _*",
|
||||||
"*DelayedCalls*",
|
"*DelayedCalls*",
|
||||||
"*test_four*",
|
"*= FAILURES =*",
|
||||||
|
"*_ TC.test_four _*",
|
||||||
"*NameError*crash*",
|
"*NameError*crash*",
|
||||||
"*test_one*",
|
"*_ TC.test_one _*",
|
||||||
"*NameError*crash*",
|
"*NameError*crash*",
|
||||||
"*test_three*",
|
"*_ TC.test_three _*",
|
||||||
"*DelayedCalls*",
|
"*DelayedCalls*",
|
||||||
"*test_two*",
|
"*_ TC.test_two _*",
|
||||||
"*crash*",
|
"*NameError*crash*",
|
||||||
|
"*= 4 failed, 1 error in *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -684,7 +696,7 @@ def test_unittest_not_shown_in_traceback(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
res = testdir.runpytest()
|
res = testdir.runpytest()
|
||||||
assert "failUnlessEqual" not in res.stdout.str()
|
res.stdout.no_fnmatch_line("*failUnlessEqual*")
|
||||||
|
|
||||||
|
|
||||||
def test_unorderable_types(testdir):
|
def test_unorderable_types(testdir):
|
||||||
|
|
@ -703,7 +715,7 @@ def test_unorderable_types(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "TypeError" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*TypeError*")
|
||||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1020,7 +1032,7 @@ def test_testcase_handles_init_exceptions(testdir):
|
||||||
)
|
)
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
assert "should raise this exception" in result.stdout.str()
|
assert "should raise this exception" in result.stdout.str()
|
||||||
assert "ERROR at teardown of MyTestCase.test_hello" not in result.stdout.str()
|
result.stdout.no_fnmatch_line("*ERROR at teardown of MyTestCase.test_hello*")
|
||||||
|
|
||||||
|
|
||||||
def test_error_message_with_parametrized_fixtures(testdir):
|
def test_error_message_with_parametrized_fixtures(testdir):
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ def test_unicode(testdir, pyfile_with_warnings):
|
||||||
[
|
[
|
||||||
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
||||||
"*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*",
|
"*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*",
|
||||||
"* 1 passed, 1 warnings*",
|
"* 1 passed, 1 warning*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ def test_filterwarnings_mark(testdir, default_config):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-W always" if default_config == "cmdline" else "")
|
result = testdir.runpytest("-W always" if default_config == "cmdline" else "")
|
||||||
result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warnings in *"])
|
result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warning in *"])
|
||||||
|
|
||||||
|
|
||||||
def test_non_string_warning_argument(testdir):
|
def test_non_string_warning_argument(testdir):
|
||||||
|
|
@ -216,7 +216,7 @@ def test_non_string_warning_argument(testdir):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("-W", "always")
|
result = testdir.runpytest("-W", "always")
|
||||||
result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"])
|
result.stdout.fnmatch_lines(["*= 1 passed, 1 warning in *"])
|
||||||
|
|
||||||
|
|
||||||
def test_filterwarnings_mark_registration(testdir):
|
def test_filterwarnings_mark_registration(testdir):
|
||||||
|
|
@ -302,7 +302,7 @@ def test_collection_warnings(testdir):
|
||||||
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
||||||
" *collection_warnings.py:3: UserWarning: collection warning",
|
" *collection_warnings.py:3: UserWarning: collection warning",
|
||||||
' warnings.warn(UserWarning("collection warning"))',
|
' warnings.warn(UserWarning("collection warning"))',
|
||||||
"* 1 passed, 1 warnings*",
|
"* 1 passed, 1 warning*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -358,7 +358,7 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings):
|
||||||
[
|
[
|
||||||
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
||||||
"*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning",
|
"*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning",
|
||||||
"* 1 passed, 1 warnings *",
|
"* 1 passed, 1 warning *",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -476,7 +476,7 @@ class TestDeprecationWarningsByDefault:
|
||||||
[
|
[
|
||||||
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
"*== %s ==*" % WARNINGS_SUMMARY_HEADER,
|
||||||
"*test_hidden_by_mark.py:3: DeprecationWarning: collection",
|
"*test_hidden_by_mark.py:3: DeprecationWarning: collection",
|
||||||
"* 1 passed, 1 warnings*",
|
"* 1 passed, 1 warning*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -605,6 +605,7 @@ def test_warnings_checker_twice():
|
||||||
warnings.warn("Message B", UserWarning)
|
warnings.warn("Message B", UserWarning)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::pytest.PytestExperimentalApiWarning")
|
||||||
@pytest.mark.filterwarnings("always")
|
@pytest.mark.filterwarnings("always")
|
||||||
def test_group_warnings_by_message(testdir):
|
def test_group_warnings_by_message(testdir):
|
||||||
testdir.copy_example("warnings/test_group_warnings_by_message.py")
|
testdir.copy_example("warnings/test_group_warnings_by_message.py")
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue