Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82cc3d8cc2 | ||
|
|
e20e376881 | ||
|
|
8052d01a37 | ||
|
|
a8003286b5 | ||
|
|
b4be228330 | ||
|
|
67dd10de26 | ||
|
|
dc8af18a0e | ||
|
|
61b9246afe | ||
|
|
9feb4941f4 | ||
|
|
237f690f8b | ||
|
|
386e801a5a | ||
|
|
5cf05ce149 | ||
|
|
aee67bb1a7 | ||
|
|
5e2d740829 | ||
|
|
82b8ec37fc | ||
|
|
f73fa47b1f | ||
|
|
fd1684e70b | ||
|
|
19501028ca | ||
|
|
3a366f451a | ||
|
|
7f6108beb1 | ||
|
|
76b0660f47 | ||
|
|
75a12b9d2b | ||
|
|
32c6d4f603 | ||
|
|
b4b2f58eab | ||
|
|
8b92d10fb3 | ||
|
|
8e220f0e6f | ||
|
|
e191a65ebb | ||
|
|
5ca81596bb | ||
|
|
7bb504b807 | ||
|
|
9be069f899 | ||
|
|
913a2da6e5 | ||
|
|
ea732464aa | ||
|
|
ddbea29c12 | ||
|
|
4c7ddb8d9b | ||
|
|
a1fcd6e445 | ||
|
|
7b8fd0cc12 | ||
|
|
391dc549c0 | ||
|
|
526f4a95cc | ||
|
|
59e6fb94b5 | ||
|
|
2f083504ee | ||
|
|
3384ffc6eb | ||
|
|
7445b5345f | ||
|
|
429485e621 | ||
|
|
52d497570b | ||
|
|
0c5e717f43 | ||
|
|
54af0f4c65 | ||
|
|
678dfaa6eb | ||
|
|
19c93d16d1 | ||
|
|
0ce8b910ca | ||
|
|
c780d1fa7c | ||
|
|
584c052da4 | ||
|
|
315374008b | ||
|
|
a9457345ee | ||
|
|
726e165932 | ||
|
|
2264db7f4a | ||
|
|
4e93dc2c97 | ||
|
|
8003d8d279 | ||
|
|
f0ecb25acd | ||
|
|
7ec1a1407a | ||
|
|
7dbe40092d | ||
|
|
e53563ebbe | ||
|
|
c2c9b27771 | ||
|
|
c3d7340542 | ||
|
|
7dcd9bf5ad | ||
|
|
8a1afe4213 | ||
|
|
fd4289dae0 | ||
|
|
977adf1354 | ||
|
|
c1fe07276c | ||
|
|
c166b80a8c | ||
|
|
afe9fd5ffd |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ coverage.xml
|
||||
.project
|
||||
.settings
|
||||
.vscode
|
||||
|
||||
# generated by pip
|
||||
pip-wheel-metadata/
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -17,12 +17,17 @@ env:
|
||||
# Specialized factors for py27.
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py27-pluggymaster
|
||||
# Specialized factors for py37.
|
||||
- TOXENV=py37-pexpect,py37-trial,py37-numpy
|
||||
- TOXENV=py37-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py37-pluggymaster
|
||||
- TOXENV=py37-freeze PYTEST_NO_COVERAGE=1
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: '3.8-dev'
|
||||
env: TOXENV=py38
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Coverage tracking is slow with pypy, skip it.
|
||||
@@ -35,6 +40,8 @@ jobs:
|
||||
python: '3.5'
|
||||
- env: TOXENV=py36
|
||||
python: '3.6'
|
||||
- env: TOXENV=py38
|
||||
python: '3.8-dev'
|
||||
- env: TOXENV=py37
|
||||
- &test-macos
|
||||
language: generic
|
||||
|
||||
2
AUTHORS
2
AUTHORS
@@ -27,6 +27,7 @@ Anthony Shaw
|
||||
Anthony Sottile
|
||||
Anton Lodder
|
||||
Antony Lee
|
||||
Arel Cordero
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
@@ -173,6 +174,7 @@ Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
Nicholas Devenish
|
||||
Nicholas Murphy
|
||||
Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Oleg Pidsadnyi
|
||||
|
||||
@@ -18,13 +18,69 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 4.2.1 (2019-02-12)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2895 <https://github.com/pytest-dev/pytest/issues/2895>`_: The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``.
|
||||
|
||||
|
||||
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module.
|
||||
|
||||
|
||||
- `#4347 <https://github.com/pytest-dev/pytest/issues/4347>`_: Fix output capturing when using pdb++ with recursive debugging.
|
||||
|
||||
|
||||
- `#4592 <https://github.com/pytest-dev/pytest/issues/4592>`_: Fix handling of ``collect_ignore`` via parent ``conftest.py``.
|
||||
|
||||
|
||||
- `#4700 <https://github.com/pytest-dev/pytest/issues/4700>`_: Fix regression where ``setUpClass`` would always be called in subclasses even if all tests
|
||||
were skipped by a ``unittest.skip()`` decorator applied in the subclass.
|
||||
|
||||
|
||||
- `#4739 <https://github.com/pytest-dev/pytest/issues/4739>`_: Fix ``parametrize(... ids=<function>)`` when the function returns non-strings.
|
||||
|
||||
|
||||
- `#4745 <https://github.com/pytest-dev/pytest/issues/4745>`_: Fix/improve collection of args when passing in ``__init__.py`` and a test file.
|
||||
|
||||
|
||||
- `#4770 <https://github.com/pytest-dev/pytest/issues/4770>`_: ``more_itertools`` is now constrained to <6.0.0 when required for Python 2.7 compatibility.
|
||||
|
||||
|
||||
- `#526 <https://github.com/pytest-dev/pytest/issues/526>`_: Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3899 <https://github.com/pytest-dev/pytest/issues/3899>`_: Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins.
|
||||
|
||||
|
||||
- `#4324 <https://github.com/pytest-dev/pytest/issues/4324>`_: Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.
|
||||
|
||||
|
||||
- `#4709 <https://github.com/pytest-dev/pytest/issues/4709>`_: Document how to customize test failure messages when using
|
||||
``pytest.warns``.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#4741 <https://github.com/pytest-dev/pytest/issues/4741>`_: Some verbosity related attributes of the TerminalReporter plugin are now
|
||||
read only properties.
|
||||
|
||||
|
||||
pytest 4.2.0 (2019-01-30)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#3094 <https://github.com/pytest-dev/pytest/issues/3094>`_: `Class xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
|
||||
- `#3094 <https://github.com/pytest-dev/pytest/issues/3094>`_: `Classic xunit-style <https://docs.pytest.org/en/latest/xunit_setup.html>`__ functions and methods
|
||||
now obey the scope of *autouse* fixtures.
|
||||
|
||||
This fixes a number of surprising issues like ``setup_method`` being called before session-scoped
|
||||
@@ -96,6 +152,9 @@ Trivial/Internal Changes
|
||||
- `#4657 <https://github.com/pytest-dev/pytest/issues/4657>`_: Copy saferepr from pylib
|
||||
|
||||
|
||||
- `#4668 <https://github.com/pytest-dev/pytest/issues/4668>`_: The verbose word for expected failures in the teststatus report changes from ``xfail`` to ``XFAIL`` to be consistent with other test outcomes.
|
||||
|
||||
|
||||
pytest 4.1.1 (2019-01-12)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -13,11 +13,9 @@ environment:
|
||||
# Specialized factors for py27.
|
||||
- TOXENV: "py27-trial,py27-numpy,py27-nobyte"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
# Specialized factors for py37.
|
||||
- TOXENV: "py37-trial,py37-numpy"
|
||||
- TOXENV: "py37-pluggymaster"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py37-freeze"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-4.2.1
|
||||
release-4.2.0
|
||||
release-4.1.1
|
||||
release-4.1.0
|
||||
|
||||
30
doc/en/announce/release-4.2.1.rst
Normal file
30
doc/en/announce/release-4.2.1.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
pytest-4.2.1
|
||||
=======================================
|
||||
|
||||
pytest 4.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Arel Cordero
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Holger Kohr
|
||||
* Kevin J. Foley
|
||||
* Nick Murphy
|
||||
* Paweł Stradomski
|
||||
* Raphael Pierzina
|
||||
* Ronny Pfannschmidt
|
||||
* Sam Brightman
|
||||
* Thomas Hisch
|
||||
* Zac Hatfield-Dodds
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -205,8 +205,8 @@ Special comparisons are done for a number of cases:
|
||||
|
||||
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
|
||||
|
||||
Defining your own assertion comparison
|
||||
----------------------------------------------
|
||||
Defining your own explanation for failed assertions
|
||||
---------------------------------------------------
|
||||
|
||||
It is possible to add your own detailed explanations by implementing
|
||||
the ``pytest_assertrepr_compare`` hook.
|
||||
|
||||
@@ -565,3 +565,50 @@ As the result:
|
||||
- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing.
|
||||
- The test ``test_eval[basic_2+4]`` passed.
|
||||
- The test ``test_eval[basic_6*9]`` was expected to fail and did fail.
|
||||
|
||||
.. _`parametrizing_conditional_raising`:
|
||||
|
||||
Parametrizing conditional raising
|
||||
--------------------------------------------------------------------
|
||||
|
||||
Use :func:`pytest.raises` with the
|
||||
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
|
||||
in which some tests raise exceptions and others do not.
|
||||
|
||||
It is helpful to define a no-op context manager ``does_not_raise`` to serve
|
||||
as a complement to ``raises``. For example::
|
||||
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
"""Test how much I know division."""
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
|
||||
If you're only supporting Python 3.7+, you can simply use ``nullcontext``
|
||||
to define ``does_not_raise``::
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
|
||||
Or, if you're supporting Python 3.3+ you can use::
|
||||
|
||||
from contextlib import ExitStack as does_not_raise
|
||||
|
||||
Or, if desired, you can ``pip install contextlib2`` and use::
|
||||
|
||||
from contextlib2 import ExitStack as does_not_raise
|
||||
|
||||
@@ -84,6 +84,11 @@ will be loaded as well.
|
||||
:ref:`full explanation <requiring plugins in non-root conftests>`
|
||||
in the Writing plugins section.
|
||||
|
||||
.. note::
|
||||
The name ``pytest_plugins`` is reserved and should not be used as a
|
||||
name for a custom plugin module.
|
||||
|
||||
|
||||
.. _`findpluginname`:
|
||||
|
||||
Finding out which plugins are active
|
||||
|
||||
@@ -1015,6 +1015,20 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
This tells pytest to ignore deprecation warnings and turn all other warnings
|
||||
into errors. For more information please refer to :ref:`warnings`.
|
||||
|
||||
.. confval:: junit_family
|
||||
|
||||
.. versionadded:: 4.2
|
||||
|
||||
Configures the format of the generated JUnit XML file. The possible options are:
|
||||
|
||||
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**.
|
||||
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__,
|
||||
which should be more compatible with latest Jenkins versions.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
junit_family = xunit2
|
||||
|
||||
.. confval:: junit_suite_name
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ You can also use it as a contextmanager::
|
||||
.. _warns:
|
||||
|
||||
Asserting warnings with the warns function
|
||||
-----------------------------------------------
|
||||
------------------------------------------
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
@@ -291,7 +291,7 @@ Alternatively, you can examine raised warnings in detail using the
|
||||
.. _recwarn:
|
||||
|
||||
Recording warnings
|
||||
------------------------
|
||||
------------------
|
||||
|
||||
You can record raised warnings either using ``pytest.warns`` or with
|
||||
the ``recwarn`` fixture.
|
||||
@@ -329,6 +329,26 @@ warnings, or index into it to get a particular recorded warning.
|
||||
|
||||
Full API: :class:`WarningsRecorder`.
|
||||
|
||||
.. _custom_failure_messages:
|
||||
|
||||
Custom failure messages
|
||||
-----------------------
|
||||
|
||||
Recording warnings provides an opportunity to produce custom test
|
||||
failure messages for when no warnings are issued or other conditions
|
||||
are met.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test():
|
||||
with pytest.warns(Warning) as record:
|
||||
f()
|
||||
if not record:
|
||||
pytest.fail("Expected a warning!")
|
||||
|
||||
If no warnings are issued when calling ``f``, then ``not record`` will
|
||||
evaluate to ``True``. You can then call ``pytest.fail`` with a
|
||||
custom error message.
|
||||
|
||||
.. _internal-warnings:
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ requires = [
|
||||
"setuptools-scm",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.towncrier]
|
||||
package = "pytest"
|
||||
|
||||
3
setup.py
3
setup.py
@@ -10,7 +10,8 @@ INSTALL_REQUIRES = [
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
"more-itertools>=4.0.0",
|
||||
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
|
||||
'more-itertools>=4.0.0;python_version>"2.7"',
|
||||
"atomicwrites>=1.0",
|
||||
'funcsigs;python_version<"3.0"',
|
||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||
|
||||
@@ -408,7 +408,10 @@ class PytestPluginManager(PluginManager):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.isfile():
|
||||
mod = self._importconftest(conftestpath)
|
||||
# Use realpath to avoid loading the same conftest twice
|
||||
# with build systems that create build directories containing
|
||||
# symlinks to actual files.
|
||||
mod = self._importconftest(conftestpath.realpath())
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
@@ -559,8 +562,8 @@ def _get_plugin_specs_as_list(specs):
|
||||
which case it is returned as a list. Specs can also be `None` in which case an
|
||||
empty list is returned.
|
||||
"""
|
||||
if specs is not None:
|
||||
if isinstance(specs, str):
|
||||
if specs is not None and not isinstance(specs, types.ModuleType):
|
||||
if isinstance(specs, six.string_types):
|
||||
specs = specs.split(",") if specs else []
|
||||
if not isinstance(specs, (list, tuple)):
|
||||
raise UsageError(
|
||||
|
||||
@@ -75,6 +75,7 @@ class pytestPDB(object):
|
||||
_config = None
|
||||
_pdb_cls = pdb.Pdb
|
||||
_saved = []
|
||||
_recursive_debug = 0
|
||||
|
||||
@classmethod
|
||||
def _init_pdb(cls, *args, **kwargs):
|
||||
@@ -87,29 +88,37 @@ class pytestPDB(object):
|
||||
capman.suspend_global_capture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
# Handle header similar to pdb.set_trace in py37+.
|
||||
header = kwargs.pop("header", None)
|
||||
if header is not None:
|
||||
tw.sep(">", header)
|
||||
elif capman and capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
else:
|
||||
tw.sep(">", "PDB set_trace")
|
||||
if cls._recursive_debug == 0:
|
||||
# Handle header similar to pdb.set_trace in py37+.
|
||||
header = kwargs.pop("header", None)
|
||||
if header is not None:
|
||||
tw.sep(">", header)
|
||||
elif capman and capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
else:
|
||||
tw.sep(">", "PDB set_trace")
|
||||
|
||||
class _PdbWrapper(cls._pdb_cls, object):
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
def do_debug(self, arg):
|
||||
cls._recursive_debug += 1
|
||||
ret = super(_PdbWrapper, self).do_debug(arg)
|
||||
cls._recursive_debug -= 1
|
||||
return ret
|
||||
|
||||
def do_continue(self, arg):
|
||||
ret = super(_PdbWrapper, self).do_continue(arg)
|
||||
if self._pytest_capman:
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
if self._pytest_capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB continue (IO-capturing resumed)")
|
||||
else:
|
||||
tw.sep(">", "PDB continue")
|
||||
self._pytest_capman.resume_global_capture()
|
||||
if cls._recursive_debug == 0:
|
||||
if self._pytest_capman.is_globally_capturing():
|
||||
tw.sep(">", "PDB continue (IO-capturing resumed)")
|
||||
else:
|
||||
tw.sep(">", "PDB continue")
|
||||
self._pytest_capman.resume_global_capture()
|
||||
cls._pluginmanager.hook.pytest_leave_pdb(
|
||||
config=cls._config, pdb=self
|
||||
)
|
||||
|
||||
@@ -370,6 +370,8 @@ def get_actual_log_level(config, *setting_names):
|
||||
)
|
||||
|
||||
|
||||
# run after terminalreporter/capturemanager are configured
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
|
||||
|
||||
@@ -388,8 +390,6 @@ class LoggingPlugin(object):
|
||||
|
||||
# enable verbose output automatically if live logging is enabled
|
||||
if self._log_cli_enabled() and not config.getoption("verbose"):
|
||||
# sanity check: terminal reporter should not have been loaded at this point
|
||||
assert self._config.pluginmanager.get_plugin("terminalreporter") is None
|
||||
config.option.verbose = 1
|
||||
|
||||
self.print_logs = get_option_ini(config, "log_print")
|
||||
@@ -420,6 +420,47 @@ class LoggingPlugin(object):
|
||||
|
||||
self.log_cli_handler = None
|
||||
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager.
|
||||
|
||||
if self._log_cli_enabled():
|
||||
self._setup_cli_logging()
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
config = self._config
|
||||
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
||||
if terminal_reporter is None:
|
||||
# terminal reporter is disabled e.g. by pytest-xdist.
|
||||
return
|
||||
|
||||
capture_manager = config.pluginmanager.get_plugin("capturemanager")
|
||||
# if capturemanager plugin is disabled, live logging still works.
|
||||
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
|
||||
log_cli_format = get_option_ini(config, "log_cli_format", "log_format")
|
||||
log_cli_date_format = get_option_ini(
|
||||
config, "log_cli_date_format", "log_date_format"
|
||||
)
|
||||
if (
|
||||
config.option.color != "no"
|
||||
and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
|
||||
):
|
||||
log_cli_formatter = ColoredLevelFormatter(
|
||||
create_terminal_writer(config),
|
||||
log_cli_format,
|
||||
datefmt=log_cli_date_format,
|
||||
)
|
||||
else:
|
||||
log_cli_formatter = logging.Formatter(
|
||||
log_cli_format, datefmt=log_cli_date_format
|
||||
)
|
||||
log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level")
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
"""Return True if log_cli should be considered enabled, either explicitly
|
||||
or because --log-cli-level was given in the command-line.
|
||||
@@ -430,10 +471,6 @@ class LoggingPlugin(object):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self):
|
||||
# This has to be called before the first log message is logged,
|
||||
# so we can access the terminal reporter plugin.
|
||||
self._setup_cli_logging()
|
||||
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
@@ -513,7 +550,6 @@ class LoggingPlugin(object):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_sessionstart(self):
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("sessionstart")
|
||||
@@ -533,46 +569,6 @@ class LoggingPlugin(object):
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled."""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
|
||||
if self._log_cli_enabled() and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
|
||||
log_cli_handler = _LiveLoggingStreamHandler(
|
||||
terminal_reporter, capture_manager
|
||||
)
|
||||
log_cli_format = get_option_ini(
|
||||
self._config, "log_cli_format", "log_format"
|
||||
)
|
||||
log_cli_date_format = get_option_ini(
|
||||
self._config, "log_cli_date_format", "log_date_format"
|
||||
)
|
||||
if (
|
||||
self._config.option.color != "no"
|
||||
and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
|
||||
):
|
||||
log_cli_formatter = ColoredLevelFormatter(
|
||||
create_terminal_writer(self._config),
|
||||
log_cli_format,
|
||||
datefmt=log_cli_date_format,
|
||||
)
|
||||
else:
|
||||
log_cli_formatter = logging.Formatter(
|
||||
log_cli_format, datefmt=log_cli_date_format
|
||||
)
|
||||
log_cli_level = get_actual_log_level(
|
||||
self._config, "log_cli_level", "log_level"
|
||||
)
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
|
||||
@@ -550,19 +550,9 @@ class Session(nodes.FSCollector):
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
|
||||
if six.PY2:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1) and not f.strpath.endswith("*.pyc")
|
||||
|
||||
else:
|
||||
|
||||
def filter_(f):
|
||||
return f.check(file=1)
|
||||
|
||||
seen_dirs = set()
|
||||
for path in argpath.visit(
|
||||
fil=filter_, rec=self._recurse, bf=True, sort=True
|
||||
fil=self._visit_filter, rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
dirpath = path.dirpath()
|
||||
if dirpath not in seen_dirs:
|
||||
@@ -592,7 +582,7 @@ class Session(nodes.FSCollector):
|
||||
col = self._node_cache[argpath]
|
||||
else:
|
||||
collect_root = self._pkg_roots.get(argpath.dirname, self)
|
||||
col = collect_root._collectfile(argpath)
|
||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||
if col:
|
||||
self._node_cache[argpath] = col
|
||||
m = self.matchnodes(col, names)
|
||||
@@ -607,6 +597,7 @@ class Session(nodes.FSCollector):
|
||||
yield y
|
||||
|
||||
def _collectfile(self, path, handle_dupes=True):
|
||||
assert path.isfile()
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
@@ -636,6 +627,18 @@ class Session(nodes.FSCollector):
|
||||
ihook.pytest_collect_directory(path=dirpath, parent=self)
|
||||
return True
|
||||
|
||||
if six.PY2:
|
||||
|
||||
@staticmethod
|
||||
def _visit_filter(f):
|
||||
return f.check(file=1) and not f.strpath.endswith("*.pyc")
|
||||
|
||||
else:
|
||||
|
||||
@staticmethod
|
||||
def _visit_filter(f):
|
||||
return f.check(file=1)
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
"""Convert a dotted module name to path."""
|
||||
try:
|
||||
|
||||
@@ -81,7 +81,11 @@ class LsofFdLeakChecker(object):
|
||||
|
||||
def _exec_lsof(self):
|
||||
pid = os.getpid()
|
||||
return subprocess.check_output(("lsof", "-Ffn0", "-p", str(pid))).decode()
|
||||
# py3: use subprocess.DEVNULL directly.
|
||||
with open(os.devnull, "wb") as devnull:
|
||||
return subprocess.check_output(
|
||||
("lsof", "-Ffn0", "-p", str(pid)), stderr=devnull
|
||||
).decode()
|
||||
|
||||
def _parse_lsof_output(self, out):
|
||||
def isopen(line):
|
||||
|
||||
@@ -599,6 +599,7 @@ class Package(Module):
|
||||
return proxy
|
||||
|
||||
def _collectfile(self, path, handle_dupes=True):
|
||||
assert path.isfile()
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
@@ -642,11 +643,12 @@ class Package(Module):
|
||||
):
|
||||
continue
|
||||
|
||||
if path.isdir() and path.join("__init__.py").check(file=1):
|
||||
pkg_prefixes.add(path)
|
||||
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
if path.isdir():
|
||||
if path.join("__init__.py").check(file=1):
|
||||
pkg_prefixes.add(path)
|
||||
else:
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
|
||||
|
||||
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
|
||||
@@ -1144,9 +1146,10 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
|
||||
def _idval(val, argname, idx, idfn, item, config):
|
||||
if idfn:
|
||||
s = None
|
||||
try:
|
||||
s = idfn(val)
|
||||
generated_id = idfn(val)
|
||||
if generated_id is not None:
|
||||
val = generated_id
|
||||
except Exception as e:
|
||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
||||
msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n"
|
||||
@@ -1154,10 +1157,7 @@ def _idval(val, argname, idx, idfn, item, config):
|
||||
# we only append the exception type and message because on Python 2 reraise does nothing
|
||||
msg += " {}: {}\n".format(type(e).__name__, e)
|
||||
six.raise_from(ValueError(msg), e)
|
||||
if s:
|
||||
return ascii_escaped(s)
|
||||
|
||||
if config:
|
||||
elif config:
|
||||
hook_id = config.hook.pytest_make_parametrize_id(
|
||||
config=config, val=val, argname=argname
|
||||
)
|
||||
|
||||
@@ -621,6 +621,14 @@ def raises(expected_exception, *args, **kwargs):
|
||||
...
|
||||
>>> assert exc_info.type is ValueError
|
||||
|
||||
**Using with** ``pytest.mark.parametrize``
|
||||
|
||||
When using :ref:`pytest.mark.parametrize ref`
|
||||
it is possible to parametrize tests such that
|
||||
some runs raise an exception and others do not.
|
||||
|
||||
See :ref:`parametrizing_conditional_raising` for an example.
|
||||
|
||||
**Legacy form**
|
||||
|
||||
It is possible to specify a callable by passing a to-be-called lambda::
|
||||
|
||||
@@ -222,12 +222,9 @@ class TerminalReporter(object):
|
||||
import _pytest.config
|
||||
|
||||
self.config = config
|
||||
self.verbosity = self.config.option.verbose
|
||||
self.showheader = self.verbosity >= 0
|
||||
self.showfspath = self.verbosity >= 0
|
||||
self.showlongtestinfo = self.verbosity > 0
|
||||
self._numcollected = 0
|
||||
self._session = None
|
||||
self._showfspath = None
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = py.path.local()
|
||||
@@ -255,6 +252,28 @@ class TerminalReporter(object):
|
||||
return False
|
||||
return self.config.getini("console_output_style") in ("progress", "count")
|
||||
|
||||
@property
|
||||
def verbosity(self):
|
||||
return self.config.option.verbose
|
||||
|
||||
@property
|
||||
def showheader(self):
|
||||
return self.verbosity >= 0
|
||||
|
||||
@property
|
||||
def showfspath(self):
|
||||
if self._showfspath is None:
|
||||
return self.verbosity >= 0
|
||||
return self._showfspath
|
||||
|
||||
@showfspath.setter
|
||||
def showfspath(self, value):
|
||||
self._showfspath = value
|
||||
|
||||
@property
|
||||
def showlongtestinfo(self):
|
||||
return self.verbosity > 0
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
return char in self.reportchars
|
||||
@@ -574,19 +593,20 @@ class TerminalReporter(object):
|
||||
return lines
|
||||
|
||||
def pytest_collection_finish(self, session):
|
||||
if self.config.option.collectonly:
|
||||
if self.config.getoption("collectonly"):
|
||||
self._printcollecteditems(session.items)
|
||||
if self.stats.get("failed"):
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self.stats.get("failed"):
|
||||
rep.toterminal(self._tw)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
lines = self.config.hook.pytest_report_collectionfinish(
|
||||
config=self.config, startdir=self.startdir, items=session.items
|
||||
)
|
||||
self._write_report_lines_from_hooks(lines)
|
||||
|
||||
if self.config.getoption("collectonly"):
|
||||
if self.stats.get("failed"):
|
||||
self._tw.sep("!", "collection failures")
|
||||
for rep in self.stats.get("failed"):
|
||||
rep.toterminal(self._tw)
|
||||
|
||||
def _printcollecteditems(self, items):
|
||||
# to print out items and their parent collectors
|
||||
# we take care to leave out Instances aka ()
|
||||
|
||||
@@ -87,6 +87,9 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
|
||||
|
||||
@pytest.fixture(scope=scope, autouse=True)
|
||||
def fixture(self, request):
|
||||
if getattr(self, "__unittest_skip__", None):
|
||||
reason = self.__unittest_skip_why__
|
||||
pytest.skip(reason)
|
||||
if setup is not None:
|
||||
if pass_self:
|
||||
setup(self, request.function)
|
||||
|
||||
@@ -969,6 +969,20 @@ def test_import_plugin_unicode_name(testdir):
|
||||
assert r.ret == 0
|
||||
|
||||
|
||||
def test_pytest_plugins_as_module(testdir):
|
||||
"""Do not raise an error if pytest_plugins attribute is a module (#3899)"""
|
||||
testdir.makepyfile(
|
||||
**{
|
||||
"__init__.py": "",
|
||||
"pytest_plugins.py": "",
|
||||
"conftest.py": "from . import pytest_plugins",
|
||||
"test_foo.py": "def test(): pass",
|
||||
}
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("* 1 passed in *")
|
||||
|
||||
|
||||
def test_deferred_hook_checking(testdir):
|
||||
"""
|
||||
Check hooks as late as possible (#1821).
|
||||
|
||||
13
testing/example_scripts/unittest/test_setup_skip.py
Normal file
13
testing/example_scripts/unittest/test_setup_skip.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class."""
|
||||
import unittest
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
def setUp(self):
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Test(Base):
|
||||
def test_foo(self):
|
||||
assert 0
|
||||
14
testing/example_scripts/unittest/test_setup_skip_class.py
Normal file
14
testing/example_scripts/unittest/test_setup_skip_class.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class."""
|
||||
import unittest
|
||||
|
||||
|
||||
class Base(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Test(Base):
|
||||
def test_foo(self):
|
||||
assert 0
|
||||
12
testing/example_scripts/unittest/test_setup_skip_module.py
Normal file
12
testing/example_scripts/unittest/test_setup_skip_module.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""setUpModule is always called, even if all tests in the module are skipped"""
|
||||
import unittest
|
||||
|
||||
|
||||
def setUpModule():
|
||||
assert 0
|
||||
|
||||
|
||||
@unittest.skip("skip all tests")
|
||||
class Base(unittest.TestCase):
|
||||
def test(self):
|
||||
assert 0
|
||||
@@ -418,6 +418,21 @@ class TestMetafunc(object):
|
||||
]
|
||||
)
|
||||
|
||||
def test_parametrize_ids_returns_non_string(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""\
|
||||
import pytest
|
||||
|
||||
def ids(d):
|
||||
return d
|
||||
|
||||
@pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids)
|
||||
def test(arg):
|
||||
assert arg
|
||||
"""
|
||||
)
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_idmaker_with_ids(self):
|
||||
from _pytest.python import idmaker
|
||||
|
||||
|
||||
@@ -94,6 +94,54 @@ class TestRaises(object):
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*3 passed*"])
|
||||
|
||||
def test_does_not_raise(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
'''Test how much I know division.'''
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*4 passed*"])
|
||||
|
||||
def test_does_not_raise_does_raise(self, testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
@pytest.mark.parametrize('example_input,expectation', [
|
||||
(0, does_not_raise()),
|
||||
(1, pytest.raises(ZeroDivisionError)),
|
||||
])
|
||||
def test_division(example_input, expectation):
|
||||
'''Test how much I know division.'''
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["*2 failed*"])
|
||||
|
||||
def test_noclass(self):
|
||||
with pytest.raises(TypeError):
|
||||
pytest.raises("wrong", lambda: None)
|
||||
|
||||
@@ -68,38 +68,16 @@ def getmsg(f, extra_ns=None, must_pass=False):
|
||||
pytest.fail("function didn't raise at all")
|
||||
|
||||
|
||||
def adjust_body_for_new_docstring_in_module_node(m):
|
||||
"""Module docstrings in 3.8 are part of Module node.
|
||||
This was briefly in 3.7 as well but got reverted in beta 5.
|
||||
|
||||
It's not in the body so we remove it so the following body items have
|
||||
the same indexes on all Python versions:
|
||||
|
||||
TODO:
|
||||
|
||||
We have a complicated sys.version_info if in here to ease testing on
|
||||
various Python 3.7 versions, but we should remove the 3.7 check after
|
||||
3.7 is released as stable to make this check more straightforward.
|
||||
"""
|
||||
if sys.version_info < (3, 8) and not (
|
||||
(3, 7) <= sys.version_info <= (3, 7, 0, "beta", 4)
|
||||
):
|
||||
assert len(m.body) > 1
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
del m.body[0]
|
||||
|
||||
|
||||
class TestAssertionRewrite(object):
|
||||
def test_place_initial_imports(self):
|
||||
s = """'Doc string'\nother = stuff"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
for imp in m.body[0:2]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[2], ast.Assign)
|
||||
assert isinstance(m.body[3], ast.Assign)
|
||||
s = """from __future__ import division\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
@@ -110,24 +88,24 @@ class TestAssertionRewrite(object):
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
s = """'doc string'\nfrom __future__ import division"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
s = """'doc string'\nfrom __future__ import division\nother"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 3
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
assert isinstance(m.body[4], ast.Expr)
|
||||
s = """from . import relative\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
for imp in m.body[0:2]:
|
||||
for imp in m.body[:2]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 1
|
||||
assert imp.col_offset == 0
|
||||
@@ -136,9 +114,8 @@ class TestAssertionRewrite(object):
|
||||
def test_dont_rewrite(self):
|
||||
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
||||
m = rewrite(s)
|
||||
adjust_body_for_new_docstring_in_module_node(m)
|
||||
assert len(m.body) == 1
|
||||
assert m.body[0].msg is None
|
||||
assert len(m.body) == 2
|
||||
assert m.body[1].msg is None
|
||||
|
||||
def test_dont_rewrite_plugin(self, testdir):
|
||||
contents = {
|
||||
|
||||
@@ -1144,3 +1144,45 @@ def test_collect_symlink_out_of_tree(testdir):
|
||||
]
|
||||
)
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_collectignore_via_conftest(testdir, monkeypatch):
|
||||
"""collect_ignore in parent conftest skips importing child (issue #4592)."""
|
||||
tests = testdir.mkpydir("tests")
|
||||
tests.ensure("conftest.py").write("collect_ignore = ['ignore_me']")
|
||||
|
||||
ignore_me = tests.mkdir("ignore_me")
|
||||
ignore_me.ensure("__init__.py")
|
||||
ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'")
|
||||
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def test_collect_pkg_init_and_file_in_args(testdir):
|
||||
subdir = testdir.mkdir("sub")
|
||||
init = subdir.ensure("__init__.py")
|
||||
init.write("def test_init(): pass")
|
||||
p = subdir.ensure("test_file.py")
|
||||
p.write("def test_file(): pass")
|
||||
|
||||
# NOTE: without "-o python_files=*.py" this collects test_file.py twice.
|
||||
# This changed/broke with "Add package scoped fixtures #2283" (2b1410895)
|
||||
# initially (causing a RecursionError).
|
||||
result = testdir.runpytest("-v", str(init), str(p))
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"*2 passed in*",
|
||||
]
|
||||
)
|
||||
|
||||
result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init), str(p))
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"sub/__init__.py::test_init PASSED*",
|
||||
"sub/test_file.py::test_file PASSED*",
|
||||
"*2 passed in*",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -244,6 +244,42 @@ def test_conftest_symlink(testdir):
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(py.path.local, "mksymlinkto"),
|
||||
reason="symlink not available on this platform",
|
||||
)
|
||||
def test_conftest_symlink_files(testdir):
|
||||
"""Check conftest.py loading when running in directory with symlinks."""
|
||||
real = testdir.tmpdir.mkdir("real")
|
||||
source = {
|
||||
"app/test_foo.py": "def test1(fixture): pass",
|
||||
"app/__init__.py": "",
|
||||
"app/conftest.py": textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
print("conftest_loaded")
|
||||
|
||||
@pytest.fixture
|
||||
def fixture():
|
||||
print("fixture_used")
|
||||
"""
|
||||
),
|
||||
}
|
||||
testdir.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
|
||||
|
||||
# Create a build directory that contains symlinks to actual files
|
||||
# but doesn't symlink actual directories.
|
||||
build = testdir.tmpdir.mkdir("build")
|
||||
build.mkdir("app")
|
||||
for f in source:
|
||||
build.join(f).mksymlinkto(real.join(f))
|
||||
build.chdir()
|
||||
result = testdir.runpytest("-vs", "app/test_foo.py")
|
||||
result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"])
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
|
||||
def test_no_conftest(testdir):
|
||||
testdir.makeconftest("assert 0")
|
||||
result = testdir.runpytest("--noconftest")
|
||||
|
||||
@@ -513,6 +513,76 @@ class TestPDB(object):
|
||||
assert "1 failed" in rest
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_interaction_continue_recursive(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
mytest="""
|
||||
import pdb
|
||||
import pytest
|
||||
|
||||
count_continue = 0
|
||||
|
||||
# Simulates pdbpp, which injects Pdb into do_debug, and uses
|
||||
# self.__class__ in do_continue.
|
||||
class CustomPdb(pdb.Pdb, object):
|
||||
def do_debug(self, arg):
|
||||
import sys
|
||||
import types
|
||||
|
||||
newglobals = {
|
||||
'Pdb': self.__class__, # NOTE: different with pdb.Pdb
|
||||
'sys': sys,
|
||||
}
|
||||
if sys.version_info < (3, ):
|
||||
do_debug_func = pdb.Pdb.do_debug.im_func
|
||||
else:
|
||||
do_debug_func = pdb.Pdb.do_debug
|
||||
|
||||
orig_do_debug = types.FunctionType(
|
||||
do_debug_func.__code__, newglobals,
|
||||
do_debug_func.__name__, do_debug_func.__defaults__,
|
||||
)
|
||||
return orig_do_debug(self, arg)
|
||||
do_debug.__doc__ = pdb.Pdb.do_debug.__doc__
|
||||
|
||||
def do_continue(self, *args, **kwargs):
|
||||
global count_continue
|
||||
count_continue += 1
|
||||
return super(CustomPdb, self).do_continue(*args, **kwargs)
|
||||
|
||||
def foo():
|
||||
print("print_from_foo")
|
||||
|
||||
def test_1():
|
||||
i = 0
|
||||
print("hello17")
|
||||
pytest.set_trace()
|
||||
x = 3
|
||||
print("hello18")
|
||||
|
||||
assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
|
||||
pytest.fail("expected_failure")
|
||||
"""
|
||||
)
|
||||
child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
|
||||
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
|
||||
child.expect(r"\n\(Pdb")
|
||||
child.sendline("debug foo()")
|
||||
child.expect("ENTERING RECURSIVE DEBUGGER")
|
||||
child.expect(r"\n\(\(Pdb")
|
||||
child.sendline("c")
|
||||
child.expect("LEAVING RECURSIVE DEBUGGER")
|
||||
assert b"PDB continue" not in child.before
|
||||
assert b"print_from_foo" in child.before
|
||||
child.sendline("c")
|
||||
child.expect(r"PDB continue \(IO-capturing resumed\)")
|
||||
rest = child.read().decode("utf8")
|
||||
assert "hello17" in rest # out is captured
|
||||
assert "hello18" in rest # out is captured
|
||||
assert "1 failed" in rest
|
||||
assert "Failed: expected_failure" in rest
|
||||
assert "AssertionError: unexpected_failure" not in rest
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_without_capture(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -649,7 +649,10 @@ class TestTerminalFunctional(object):
|
||||
assert "===" not in s
|
||||
assert "passed" not in s
|
||||
|
||||
def test_report_collectionfinish_hook(self, testdir):
|
||||
@pytest.mark.parametrize(
|
||||
"params", [(), ("--collect-only",)], ids=["no-params", "collect-only"]
|
||||
)
|
||||
def test_report_collectionfinish_hook(self, testdir, params):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_report_collectionfinish(config, startdir, items):
|
||||
@@ -664,7 +667,7 @@ class TestTerminalFunctional(object):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest(*params)
|
||||
result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"])
|
||||
|
||||
|
||||
|
||||
@@ -1026,3 +1026,18 @@ def test_error_message_with_parametrized_fixtures(testdir):
|
||||
"*Function type: TestCaseFunction",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name, expected_outcome",
|
||||
[
|
||||
("test_setup_skip.py", "1 skipped"),
|
||||
("test_setup_skip_class.py", "1 skipped"),
|
||||
("test_setup_skip_module.py", "1 error"),
|
||||
],
|
||||
)
|
||||
def test_setup_inheritance_skipping(testdir, test_name, expected_outcome):
|
||||
"""Issue #4700"""
|
||||
testdir.copy_example("unittest/{}".format(test_name))
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("* {} in *".format(expected_outcome))
|
||||
|
||||
29
tox.ini
29
tox.ini
@@ -1,5 +1,6 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
isolated_build = True
|
||||
minversion = 3.5.3
|
||||
distshare = {homedir}/.tox/distshare
|
||||
# make sure to update environment list in travis.yml and appveyor.yml
|
||||
envlist =
|
||||
@@ -9,6 +10,7 @@ envlist =
|
||||
py35
|
||||
py36
|
||||
py37
|
||||
py38
|
||||
pypy
|
||||
{py27,py37}-{pexpect,xdist,trial,numpy,pluggymaster}
|
||||
py27-nobyte
|
||||
@@ -23,7 +25,8 @@ commands =
|
||||
coverage: coverage report
|
||||
passenv = USER USERNAME COVERAGE_* TRAVIS
|
||||
setenv =
|
||||
# configuration if a user runs tox with a "coverage" factor, for example "tox -e py37-coverage"
|
||||
# Configuration to run with coverage similar to Travis/Appveyor, e.g.
|
||||
# "tox -e py37-coverage".
|
||||
coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m
|
||||
coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
coverage: COVERAGE_FILE={toxinidir}/.coverage
|
||||
@@ -50,8 +53,8 @@ commands = pre-commit run --all-files --show-diff-on-failure
|
||||
[testenv:py27-xdist]
|
||||
extras = testing
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest -n auto {posargs}
|
||||
|
||||
@@ -59,15 +62,15 @@ commands =
|
||||
# NOTE: copied from above due to https://github.com/tox-dev/tox/issues/706.
|
||||
extras = testing
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands = {[testenv:py27-xdist]commands}
|
||||
|
||||
[testenv:py27-pexpect]
|
||||
platform = linux|darwin
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pexpect
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_pdb.py testing/test_terminal.py testing/test_unittest.py}
|
||||
|
||||
@@ -79,8 +82,8 @@ commands = {[testenv:py27-pexpect]commands}
|
||||
[testenv:py27-nobyte]
|
||||
extras = testing
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
distribute = true
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
@@ -90,8 +93,8 @@ commands =
|
||||
|
||||
[testenv:py27-trial]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
twisted
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_unittest.py}
|
||||
|
||||
@@ -101,8 +104,8 @@ commands = {[testenv:py27-trial]commands}
|
||||
|
||||
[testenv:py27-numpy]
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
numpy
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands=
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/python/approx.py}
|
||||
|
||||
@@ -114,11 +117,13 @@ commands = {[testenv:py27-numpy]commands}
|
||||
setenv=
|
||||
{[testenv]setenv}
|
||||
_PYTEST_SETUP_SKIP_PLUGGY_DEP=1
|
||||
# NOTE: using env instead of "{[testenv]deps}", because of https://github.com/tox-dev/tox/issues/706.
|
||||
_PYTEST_TOX_EXTRA_DEP=git+https://github.com/pytest-dev/pluggy.git@master
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
git+https://github.com/pytest-dev/pluggy.git@master
|
||||
|
||||
[testenv:py37-pluggymaster]
|
||||
setenv = {[testenv:py27-pluggymaster]setenv}
|
||||
deps = {[testenv:py27-pluggymaster]deps}
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3
|
||||
@@ -134,8 +139,8 @@ commands =
|
||||
basepython = python3
|
||||
skipsdist = True
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
PyYAML
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
commands =
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest doc/en
|
||||
{env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest
|
||||
@@ -165,7 +170,9 @@ commands =
|
||||
|
||||
[testenv:py37-freeze]
|
||||
changedir = testing/freeze
|
||||
# Disable PEP 517 with pip, which does not work with PyInstaller currently.
|
||||
deps =
|
||||
--no-use-pep517
|
||||
pyinstaller
|
||||
commands =
|
||||
{envpython} create_executable.py
|
||||
|
||||
Reference in New Issue
Block a user