Merge branch 'master' of github.com:pytest-dev/pytest into fix-3854

This commit is contained in:
turturica 2018-08-22 20:36:52 -07:00
commit e4f76f6350
44 changed files with 595 additions and 157 deletions

View File

@ -5,13 +5,13 @@ repos:
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
language_version: python3.6 language_version: python3
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: v0.2.0 rev: v0.2.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==18.6b4] additional_dependencies: [black==18.6b4]
language_version: python3.6 language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0 rev: v1.3.0
hooks: hooks:
@ -37,7 +37,6 @@ repos:
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
language: python language: python
additional_dependencies: [pygments, restructuredtext_lint] additional_dependencies: [pygments, restructuredtext_lint]
python_version: python3.6
- id: changelogs-rst - id: changelogs-rst
name: changelog files must end in .rst name: changelog files must end in .rst
entry: ./scripts/fail entry: ./scripts/fail

View File

@ -98,6 +98,7 @@ Javier Domingo Cansino
Javier Romero Javier Romero
Jeff Rackauckas Jeff Rackauckas
Jeff Widman Jeff Widman
Jenni Rinker
John Eddie Ayson John Eddie Ayson
John Towler John Towler
Jon Sonesen Jon Sonesen
@ -182,6 +183,7 @@ Russel Winder
Ryan Wooden Ryan Wooden
Samuel Dion-Girardeau Samuel Dion-Girardeau
Samuele Pedroni Samuele Pedroni
Sankt Petersbug
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Simon Gomizelj Simon Gomizelj
@ -205,6 +207,7 @@ Trevor Bekolay
Tyler Goodlet Tyler Goodlet
Tzu-ping Chung Tzu-ping Chung
Vasily Kuznetsov Vasily Kuznetsov
Victor Maryama
Victor Uriarte Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Vitaly Lashmanov Vitaly Lashmanov

View File

@ -18,6 +18,40 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 3.7.2 (2018-08-16)
=========================
Bug Fixes
---------
- `#3671 <https://github.com/pytest-dev/pytest/issues/3671>`_: Fix ``filterwarnings`` not being registered as a builtin mark.
- `#3768 <https://github.com/pytest-dev/pytest/issues/3768>`_, `#3789 <https://github.com/pytest-dev/pytest/issues/3789>`_: Fix test collection from packages mixed with normal directories.
- `#3771 <https://github.com/pytest-dev/pytest/issues/3771>`_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``.
- `#3774 <https://github.com/pytest-dev/pytest/issues/3774>`_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``).
- `#3775 <https://github.com/pytest-dev/pytest/issues/3775>`_: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.
- `#3788 <https://github.com/pytest-dev/pytest/issues/3788>`_: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.
- `#3804 <https://github.com/pytest-dev/pytest/issues/3804>`_: Fix traceback reporting for exceptions with ``__cause__`` cycles.
Improved Documentation
----------------------
- `#3746 <https://github.com/pytest-dev/pytest/issues/3746>`_: Add documentation for ``metafunc.config`` that had been mistakenly hidden.
pytest 3.7.1 (2018-08-02) pytest 3.7.1 (2018-08-02)
========================= =========================

View File

@ -0,0 +1 @@
Fixtures during teardown can again use ``capsys`` and ``cafd`` to inspect output captured during tests.

View File

@ -1 +0,0 @@
Fix infinite recursion during collection if a ``pytest_ignore_collect`` returns ``False`` instead of ``None``.

View File

@ -1 +0,0 @@
Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.

View File

@ -1 +0,0 @@
Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.

View File

@ -0,0 +1 @@
Fix bug where ``--show-capture=no`` option would still show logs printed during fixture teardown.

View File

@ -0,0 +1 @@
Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.

1
changelog/3824.doc.rst Normal file
View File

@ -0,0 +1 @@
Added example for multiple glob pattern matches in ``python_files``.

View File

@ -0,0 +1 @@
Replace broken type annotations with type comments.

1
changelog/3833.doc.rst Normal file
View File

@ -0,0 +1 @@
Added missing docs for ``pytester.Testdir``

View File

@ -0,0 +1 @@
Fix collection error when specifying test functions directly in the command line using ``test.py::test`` syntax together with ``--doctest-module``.

View File

@ -0,0 +1,2 @@
Remove a reference to issue `#568 <https://github.com/pytest-dev/pytest/issues/568>`_ from the documentation, which has since been
fixed.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-3.7.2
release-3.7.1 release-3.7.1
release-3.7.0 release-3.7.0
release-3.6.4 release-3.6.4

View File

@ -0,0 +1,25 @@
pytest-3.7.2
=======================================
pytest 3.7.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
Thanks to all who contributed to this release, among them:
* Anthony Sottile
* Bruno Oliveira
* Daniel Hahler
* Josh Holland
* Ronny Pfannschmidt
* Sankt Petersbug
* Wes Thomas
* turturica
Happy testing,
The pytest Development Team

View File

@ -200,6 +200,8 @@ You can ask which markers exist for your test suite - the list includes our just
$ pytest --markers $ pytest --markers
@pytest.mark.webtest: mark a test as a webtest. @pytest.mark.webtest: mark a test as a webtest.
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
@ -374,6 +376,8 @@ The ``--markers`` option always gives you a list of available markers::
$ pytest --markers $ pytest --markers
@pytest.mark.env(name): mark test to run only on named environment @pytest.mark.env(name): mark test to run only on named environment
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html

View File

@ -84,8 +84,9 @@ interesting to just look at the collection tree::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR/nonpython, inifile: rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
collected 2 items collected 2 items
<YamlFile 'test_simple.yml'> <Package '$REGENDOC_TMPDIR/nonpython'>
<YamlItem 'hello'> <YamlFile 'test_simple.yml'>
<YamlItem 'ok'> <YamlItem 'hello'>
<YamlItem 'ok'>
======================= no tests ran in 0.12 seconds ======================= ======================= no tests ran in 0.12 seconds =======================

View File

@ -411,11 +411,10 @@ is to be run with different sets of arguments for its three arguments:
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize):: Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
. $ pytest -rs -q multipython.py . $ pytest -rs -q multipython.py
...ssssssssssssssssssssssss [100%] ...sss...sssssssss...sss... [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.5' not found 12 passed, 15 skipped in 0.12 seconds
3 passed, 24 skipped in 0.12 seconds
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
-------------------------------------------------------------------- --------------------------------------------------------------------

View File

@ -100,19 +100,21 @@ Changing naming conventions
You can configure different naming conventions by setting You can configure different naming conventions by setting
the :confval:`python_files`, :confval:`python_classes` and the :confval:`python_files`, :confval:`python_classes` and
:confval:`python_functions` configuration options. Example:: :confval:`python_functions` configuration options.
Here is an example::
# Example 1: have pytest look for "check" instead of "test"
# content of pytest.ini # content of pytest.ini
# can also be defined in tox.ini or setup.cfg file, although the section # can also be defined in tox.ini or setup.cfg file, although the section
# name in setup.cfg files should be "tool:pytest" # name in setup.cfg files should be "tool:pytest"
[pytest] [pytest]
python_files=check_*.py python_files = check_*.py
python_classes=Check python_classes = Check
python_functions=*_check python_functions = *_check
This would make ``pytest`` look for tests in files that match the ``check_* This would make ``pytest`` look for tests in files that match the ``check_*
.py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods .py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods
that match ``*_check``. For example, if we have:: that match ``*_check``. For example, if we have::
# content of check_myapp.py # content of check_myapp.py
class CheckMyApp(object): class CheckMyApp(object):
@ -121,7 +123,7 @@ that match ``*_check``. For example, if we have::
def complex_check(self): def complex_check(self):
pass pass
then the test collection looks like this:: The test collection would look like this::
$ pytest --collect-only $ pytest --collect-only
=========================== test session starts ============================ =========================== test session starts ============================
@ -136,11 +138,19 @@ then the test collection looks like this::
======================= no tests ran in 0.12 seconds ======================= ======================= no tests ran in 0.12 seconds =======================
You can check for multiple glob patterns by adding a space between the patterns::
# Example 2: have pytest look for files with "test" and "example"
# content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest"
# with "tool:pytest" for setup.cfg)
[pytest]
python_files = test_*.py example_*.py
.. note:: .. note::
the ``python_functions`` and ``python_classes`` options has no effect the ``python_functions`` and ``python_classes`` options has no effect
for ``unittest.TestCase`` test discovery because pytest delegates for ``unittest.TestCase`` test discovery because pytest delegates
detection of test case methods to unittest code. discovery of test case methods to unittest code.
Interpreting cmdline arguments as Python packages Interpreting cmdline arguments as Python packages
----------------------------------------------------- -----------------------------------------------------

View File

@ -460,7 +460,7 @@ To use it, include in your top-most ``conftest.py`` file::
.. autoclass:: Testdir() .. autoclass:: Testdir()
:members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile :members:
.. autoclass:: RunResult() .. autoclass:: RunResult()
:members: :members:
@ -1229,7 +1229,8 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: python_classes .. confval:: python_classes
One or more name prefixes or glob-style patterns determining which classes One or more name prefixes or glob-style patterns determining which classes
are considered for test collection. By default, pytest will consider any are considered for test collection. Search for multiple glob patterns by
adding a space between patterns. By default, pytest will consider any
class prefixed with ``Test`` as a test collection. Here is an example of how class prefixed with ``Test`` as a test collection. Here is an example of how
to collect tests from classes that end in ``Suite``: to collect tests from classes that end in ``Suite``:
@ -1246,15 +1247,23 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: python_files .. confval:: python_files
One or more Glob-style file patterns determining which python files One or more Glob-style file patterns determining which python files
are considered as test modules. By default, pytest will consider are considered as test modules. Search for multiple glob patterns by
any file matching with ``test_*.py`` and ``*_test.py`` globs as a test adding a space between patterns::
module.
.. code-block:: ini
[pytest]
python_files = test_*.py check_*.py example_*.py
By default, pytest will consider any file matching with ``test_*.py``
and ``*_test.py`` globs as a test module.
.. confval:: python_functions .. confval:: python_functions
One or more name prefixes or glob-patterns determining which test functions One or more name prefixes or glob-patterns determining which test functions
and methods are considered tests. By default, pytest will consider any and methods are considered tests. Search for multiple glob patterns by
adding a space between patterns. By default, pytest will consider any
function prefixed with ``test`` as a test. Here is an example of how function prefixed with ``test`` as a test. Here is an example of how
to collect test functions and methods that end in ``_test``: to collect test functions and methods that end in ``_test``:

View File

@ -136,12 +136,6 @@ You can use the ``skipif`` marker (as any other marker) on classes::
If the condition is ``True``, this marker will produce a skip result for If the condition is ``True``, this marker will produce a skip result for
each of the test methods of that class. each of the test methods of that class.
.. warning::
The use of ``skipif`` on classes that use inheritance is strongly
discouraged. `A Known bug <https://github.com/pytest-dev/pytest/issues/568>`_
in pytest's markers may cause unexpected behavior in super classes.
If you want to skip all test functions of a module, you may use If you want to skip all test functions of a module, you may use
the ``pytestmark`` name on the global level: the ``pytestmark`` name on the global level:

View File

@ -719,7 +719,9 @@ class FormattedExcinfo(object):
repr_chain = [] repr_chain = []
e = excinfo.value e = excinfo.value
descr = None descr = None
while e is not None: seen = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo: if excinfo:
reprtraceback = self.repr_traceback(excinfo) reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash() reprcrash = excinfo._getreprcrash()

View File

@ -16,7 +16,6 @@ import six
import pytest import pytest
from _pytest.compat import CaptureIO from _pytest.compat import CaptureIO
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
@ -63,8 +62,9 @@ def pytest_load_initial_conftests(early_config, parser, args):
# finally trigger conftest loading but while capturing (issue93) # finally trigger conftest loading but while capturing (issue93)
capman.start_global_capturing() capman.start_global_capturing()
outcome = yield outcome = yield
out, err = capman.suspend_global_capture() capman.suspend_global_capture()
if outcome.excinfo is not None: if outcome.excinfo is not None:
out, err = capman.read_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
@ -85,6 +85,7 @@ class CaptureManager(object):
def __init__(self, method): def __init__(self, method):
self._method = method self._method = method
self._global_capturing = None self._global_capturing = None
self._current_item = None
def _getcapture(self, method): def _getcapture(self, method):
if method == "fd": if method == "fd":
@ -96,6 +97,8 @@ class CaptureManager(object):
else: else:
raise ValueError("unknown capturing method: %r" % method) raise ValueError("unknown capturing method: %r" % method)
# Global capturing control
def start_global_capturing(self): def start_global_capturing(self):
assert self._global_capturing is None assert self._global_capturing is None
self._global_capturing = self._getcapture(self._method) self._global_capturing = self._getcapture(self._method)
@ -110,16 +113,15 @@ class CaptureManager(object):
def resume_global_capture(self): def resume_global_capture(self):
self._global_capturing.resume_capturing() self._global_capturing.resume_capturing()
def suspend_global_capture(self, item=None, in_=False): def suspend_global_capture(self, in_=False):
if item is not None:
self.deactivate_fixture(item)
cap = getattr(self, "_global_capturing", None) cap = getattr(self, "_global_capturing", None)
if cap is not None: if cap is not None:
try: cap.suspend_capturing(in_=in_)
outerr = cap.readouterr()
finally: def read_global_capture(self):
cap.suspend_capturing(in_=in_) return self._global_capturing.readouterr()
return outerr
# Fixture Control (its just forwarding, think about removing this later)
def activate_fixture(self, item): def activate_fixture(self, item):
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
@ -135,12 +137,53 @@ class CaptureManager(object):
if fixture is not None: if fixture is not None:
fixture.close() fixture.close()
def suspend_fixture(self, item):
fixture = getattr(item, "_capture_fixture", None)
if fixture is not None:
fixture._suspend()
def resume_fixture(self, item):
fixture = getattr(item, "_capture_fixture", None)
if fixture is not None:
fixture._resume()
# Helper context managers
@contextlib.contextmanager
def global_and_fixture_disabled(self):
"""Context manager to temporarily disables global and current fixture capturing."""
# Need to undo local capsys-et-al if exists before disabling global capture
self.suspend_fixture(self._current_item)
self.suspend_global_capture(in_=False)
try:
yield
finally:
self.resume_global_capture()
self.resume_fixture(self._current_item)
@contextlib.contextmanager
def item_capture(self, when, item):
self.resume_global_capture()
self.activate_fixture(item)
try:
yield
finally:
self.deactivate_fixture(item)
self.suspend_global_capture(in_=False)
out, err = self.read_global_capture()
item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err)
# Hooks
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector): def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File): if isinstance(collector, pytest.File):
self.resume_global_capture() self.resume_global_capture()
outcome = yield outcome = yield
out, err = self.suspend_global_capture() self.suspend_global_capture()
out, err = self.read_global_capture()
rep = outcome.get_result() rep = outcome.get_result()
if out: if out:
rep.sections.append(("Captured stdout", out)) rep.sections.append(("Captured stdout", out))
@ -150,29 +193,25 @@ class CaptureManager(object):
yield yield
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item): def pytest_runtest_protocol(self, item):
self.resume_global_capture() self._current_item = item
# no need to activate a capture fixture because they activate themselves during creation; this
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
# be activated during pytest_runtest_call
yield yield
self.suspend_capture_item(item, "setup") self._current_item = None
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
with self.item_capture("setup", item):
yield
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item): def pytest_runtest_call(self, item):
self.resume_global_capture() with self.item_capture("call", item):
# it is important to activate this fixture during the call phase so it overwrites the "global" yield
# capture
self.activate_fixture(item)
yield
self.suspend_capture_item(item, "call")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item): def pytest_runtest_teardown(self, item):
self.resume_global_capture() with self.item_capture("teardown", item):
self.activate_fixture(item) yield
yield
self.suspend_capture_item(item, "teardown")
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo): def pytest_keyboard_interrupt(self, excinfo):
@ -182,11 +221,6 @@ class CaptureManager(object):
def pytest_internalerror(self, excinfo): def pytest_internalerror(self, excinfo):
self.stop_global_capturing() self.stop_global_capturing()
def suspend_capture_item(self, item, when, in_=False):
out, err = self.suspend_global_capture(item, in_=in_)
item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err)
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"} capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
@ -290,40 +324,54 @@ class CaptureFixture(object):
def __init__(self, captureclass, request): def __init__(self, captureclass, request):
self.captureclass = captureclass self.captureclass = captureclass
self.request = request self.request = request
self._capture = None
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
def _start(self): def _start(self):
self._capture = MultiCapture( # Start if not started yet
out=True, err=True, in_=False, Capture=self.captureclass if getattr(self, "_capture", None) is None:
) self._capture = MultiCapture(
self._capture.start_capturing() out=True, err=True, in_=False, Capture=self.captureclass
)
self._capture.start_capturing()
def close(self): def close(self):
cap = self.__dict__.pop("_capture", None) if self._capture is not None:
if cap is not None: out, err = self._capture.pop_outerr_to_orig()
self._outerr = cap.pop_outerr_to_orig() self._captured_out += out
cap.stop_capturing() self._captured_err += err
self._capture.stop_capturing()
self._capture = None
def readouterr(self): def readouterr(self):
"""Read and return the captured output so far, resetting the internal buffer. """Read and return the captured output so far, resetting the internal buffer.
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes :return: captured content as a namedtuple with ``out`` and ``err`` string attributes
""" """
try: captured_out, captured_err = self._captured_out, self._captured_err
return self._capture.readouterr() if self._capture is not None:
except AttributeError: out, err = self._capture.readouterr()
return self._outerr captured_out += out
captured_err += err
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
return CaptureResult(captured_out, captured_err)
def _suspend(self):
"""Suspends this fixture's own capturing temporarily."""
self._capture.suspend_capturing()
def _resume(self):
"""Resumes this fixture's own capturing temporarily."""
self._capture.resume_capturing()
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self): def disabled(self):
"""Temporarily disables capture while inside the 'with' block.""" """Temporarily disables capture while inside the 'with' block."""
self._capture.suspend_capturing()
capmanager = self.request.config.pluginmanager.getplugin("capturemanager") capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
capmanager.suspend_global_capture(item=None, in_=False) with capmanager.global_and_fixture_disabled():
try:
yield yield
finally:
capmanager.resume_global_capture()
self._capture.resume_capturing()
def safe_text_dupfile(f, mode, default_encoding="UTF8"): def safe_text_dupfile(f, mode, default_encoding="UTF8"):
@ -440,6 +488,7 @@ class MultiCapture(object):
class NoCapture(object): class NoCapture(object):
EMPTY_BUFFER = None
__init__ = start = done = suspend = resume = lambda *args: None __init__ = start = done = suspend = resume = lambda *args: None
@ -449,6 +498,8 @@ class FDCaptureBinary(object):
snap() produces `bytes` snap() produces `bytes`
""" """
EMPTY_BUFFER = bytes()
def __init__(self, targetfd, tmpfile=None): def __init__(self, targetfd, tmpfile=None):
self.targetfd = targetfd self.targetfd = targetfd
try: try:
@ -522,6 +573,8 @@ class FDCapture(FDCaptureBinary):
snap() produces text snap() produces text
""" """
EMPTY_BUFFER = str()
def snap(self): def snap(self):
res = FDCaptureBinary.snap(self) res = FDCaptureBinary.snap(self)
enc = getattr(self.tmpfile, "encoding", None) enc = getattr(self.tmpfile, "encoding", None)
@ -531,6 +584,9 @@ class FDCapture(FDCaptureBinary):
class SysCapture(object): class SysCapture(object):
EMPTY_BUFFER = str()
def __init__(self, fd, tmpfile=None): def __init__(self, fd, tmpfile=None):
name = patchsysdict[fd] name = patchsysdict[fd]
self._old = getattr(sys, name) self._old = getattr(sys, name)
@ -568,6 +624,8 @@ class SysCapture(object):
class SysCaptureBinary(SysCapture): class SysCaptureBinary(SysCapture):
EMPTY_BUFFER = bytes()
def snap(self): def snap(self):
res = self.tmpfile.buffer.getvalue() res = self.tmpfile.buffer.getvalue()
self.tmpfile.seek(0) self.tmpfile.seek(0)

View File

@ -8,6 +8,7 @@ import functools
import inspect import inspect
import re import re
import sys import sys
from contextlib import contextmanager
import py import py
@ -151,6 +152,13 @@ def getfuncargnames(function, is_method=False, cls=None):
return arg_names return arg_names
@contextmanager
def dummy_context_manager():
"""Context manager that does nothing, useful in situations where you might need an actual context manager or not
depending on some condition. Using this allow to keep the same code"""
yield
def get_default_arg_names(function): def get_default_arg_names(function):
# 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

View File

@ -174,23 +174,23 @@ class Argument(object):
if isinstance(typ, six.string_types): if isinstance(typ, six.string_types):
if typ == "choice": if typ == "choice":
warnings.warn( warnings.warn(
"type argument to addoption() is a string %r." "`type` argument to addoption() is the string %r."
" For parsearg this is optional and when supplied" " For choices this is optional and can be omitted, "
" should be a type." " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names), " (options: %s)" % (typ, names),
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=4,
) )
# argparse expects a type here take it from # argparse expects a type here take it from
# the type of the first element # the type of the first element
attrs["type"] = type(attrs["choices"][0]) attrs["type"] = type(attrs["choices"][0])
else: else:
warnings.warn( warnings.warn(
"type argument to addoption() is a string %r." "`type` argument to addoption() is the string %r, "
" For parsearg this should be a type." " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names), " (options: %s)" % (typ, names),
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=4,
) )
attrs["type"] = Argument._typ_map[typ] attrs["type"] = Argument._typ_map[typ]
# used in test_parseopt -> test_parse_defaultgetter # used in test_parseopt -> test_parse_defaultgetter

View File

@ -102,7 +102,8 @@ class PdbInvoke(object):
def pytest_exception_interact(self, node, call, report): def pytest_exception_interact(self, node, call, report):
capman = node.config.pluginmanager.getplugin("capturemanager") capman = node.config.pluginmanager.getplugin("capturemanager")
if capman: if capman:
out, err = capman.suspend_global_capture(in_=True) capman.suspend_global_capture(in_=True)
out, err = capman.read_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stdout.write(err) sys.stdout.write(err)
_enter_pdb(node, call.excinfo, report) _enter_pdb(node, call.excinfo, report)

View File

@ -307,8 +307,8 @@ class FuncFixtureInfo(object):
# fixture names specified via usefixtures and via autouse=True in fixture # fixture names specified via usefixtures and via autouse=True in fixture
# definitions. # definitions.
initialnames = attr.ib(type=tuple) initialnames = attr.ib(type=tuple)
names_closure = attr.ib(type="List[str]") names_closure = attr.ib() # type: List[str]
name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]") name2fixturedefs = attr.ib() # type: List[str, List[FixtureDef]]
def prune_dependency_tree(self): def prune_dependency_tree(self):
"""Recompute names_closure from initialnames and name2fixturedefs """Recompute names_closure from initialnames and name2fixturedefs

View File

@ -6,6 +6,7 @@ from contextlib import closing, contextmanager
import re import re
import six import six
from _pytest.compat import dummy_context_manager
from _pytest.config import create_terminal_writer from _pytest.config import create_terminal_writer
import pytest import pytest
import py import py
@ -369,11 +370,6 @@ def pytest_configure(config):
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
@contextmanager
def _dummy_context_manager():
yield
class LoggingPlugin(object): class LoggingPlugin(object):
"""Attaches to the logging module and captures log messages for each test. """Attaches to the logging module and captures log messages for each test.
""" """
@ -537,7 +533,7 @@ class LoggingPlugin(object):
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
) )
else: else:
self.live_logs_context = _dummy_context_manager() self.live_logs_context = dummy_context_manager()
class _LiveLoggingStreamHandler(logging.StreamHandler): class _LiveLoggingStreamHandler(logging.StreamHandler):
@ -572,9 +568,12 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self._test_outcome_written = False self._test_outcome_written = False
def emit(self, record): def emit(self, record):
if self.capture_manager is not None: ctx_manager = (
self.capture_manager.suspend_global_capture() self.capture_manager.global_and_fixture_disabled()
try: if self.capture_manager
else dummy_context_manager()
)
with ctx_manager:
if not self._first_record_emitted: if not self._first_record_emitted:
self.stream.write("\n") self.stream.write("\n")
self._first_record_emitted = True self._first_record_emitted = True
@ -586,6 +585,3 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self.stream.section("live log " + self._when, sep="-", bold=True) self.stream.section("live log " + self._when, sep="-", bold=True)
self._section_name_shown = True self._section_name_shown = True
logging.StreamHandler.emit(self, record) logging.StreamHandler.emit(self, record)
finally:
if self.capture_manager is not None:
self.capture_manager.resume_global_capture()

View File

@ -505,8 +505,9 @@ class Session(nodes.FSCollector):
root = self._node_cache[pkginit] root = self._node_cache[pkginit]
else: else:
col = root._collectfile(pkginit) col = root._collectfile(pkginit)
if col and isinstance(col, Package): if col:
root = col[0] if isinstance(col[0], Package):
root = col[0]
self._node_cache[root.fspath] = root self._node_cache[root.fspath] = root
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.
@ -624,11 +625,12 @@ class Session(nodes.FSCollector):
resultnodes.append(node) resultnodes.append(node)
continue continue
assert isinstance(node, nodes.Collector) assert isinstance(node, nodes.Collector)
if node.nodeid in self._node_cache: key = (type(node), node.nodeid)
rep = self._node_cache[node.nodeid] if key in self._node_cache:
rep = self._node_cache[key]
else: else:
rep = collect_one_node(node) rep = collect_one_node(node)
self._node_cache[node.nodeid] = rep self._node_cache[key] = rep
if rep.passed: if rep.passed:
has_matched = False has_matched = False
for x in rep.result: for x in rep.result:

View File

@ -550,18 +550,22 @@ class Testdir(object):
return ret return ret
def makefile(self, ext, *args, **kwargs): def makefile(self, ext, *args, **kwargs):
"""Create a new file in the testdir. r"""Create new file(s) in the testdir.
ext: The extension the file should use, including the dot, e.g. `.py`. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`.
:param list[str] args: All args will be treated as strings and joined using newlines.
args: All args will be treated as strings and joined using newlines.
The result will be written as contents to the file. The name of the The result will be written as contents to the file. The name of the
file will be based on the test function requesting this fixture. file will be based on the test function requesting this fixture.
E.g. "testdir.makefile('.txt', 'line1', 'line2')" :param kwargs: Each keyword is the name of a file, while the value of it will
kwargs: Each keyword is the name of a file, while the value of it will
be written as contents of the file. be written as contents of the file.
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
Examples:
.. code-block:: python
testdir.makefile(".txt", "line1", "line2")
testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
""" """
return self._makefile(ext, args, kwargs) return self._makefile(ext, args, kwargs)

View File

@ -216,18 +216,6 @@ def pytest_pycollect_makemodule(path, parent):
return Module(path, parent) return Module(path, parent)
def pytest_ignore_collect(path, config):
# Skip duplicate packages.
keepduplicates = config.getoption("keepduplicates")
if keepduplicates:
duplicate_paths = config.pluginmanager._duplicatepaths
if path.basename == "__init__.py":
if path in duplicate_paths:
return True
else:
duplicate_paths.add(path)
@hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield outcome = yield
@ -554,9 +542,7 @@ class Package(Module):
self.name = fspath.dirname self.name = fspath.dirname
self.trace = session.trace self.trace = session.trace
self._norecursepatterns = session._norecursepatterns self._norecursepatterns = session._norecursepatterns
for path in list(session.config.pluginmanager._duplicatepaths): self.fspath = fspath
if path.dirname == fspath.dirname and path != fspath:
session.config.pluginmanager._duplicatepaths.remove(path)
def _recurse(self, path): def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath()) ihook = self.gethookproxy(path.dirpath())
@ -594,6 +580,15 @@ class Package(Module):
return path in self.session._initialpaths return path in self.session._initialpaths
def collect(self): def collect(self):
# XXX: HACK!
# Before starting to collect any files from this package we need
# to cleanup the duplicate paths added by the session's collect().
# Proper fix is to not track these as duplicates in the first place.
for path in list(self.session.config.pluginmanager._duplicatepaths):
# if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts():
if path.dirname.startswith(self.name):
self.session.config.pluginmanager._duplicatepaths.remove(path)
this_path = self.fspath.dirpath() this_path = self.fspath.dirpath()
pkg_prefix = None pkg_prefix = None
yield Module(this_path.join("__init__.py"), self) yield Module(this_path.join("__init__.py"), self)
@ -884,12 +879,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
""" """
def __init__(self, definition, fixtureinfo, config, cls=None, module=None): def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
#: access to the :class:`_pytest.config.Config` object for the test session
assert ( assert (
isinstance(definition, FunctionDefinition) isinstance(definition, FunctionDefinition)
or type(definition).__name__ == "DefinitionMock" or type(definition).__name__ == "DefinitionMock"
) )
self.definition = definition self.definition = definition
#: access to the :class:`_pytest.config.Config` object for the test session
self.config = config self.config = config
#: the module object where the test function is defined in. #: the module object where the test function is defined in.

View File

@ -51,7 +51,8 @@ def _show_fixture_action(fixturedef, msg):
config = fixturedef._fixturemanager.config config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin("capturemanager") capman = config.pluginmanager.getplugin("capturemanager")
if capman: if capman:
out, err = capman.suspend_global_capture() capman.suspend_global_capture()
out, err = capman.read_global_capture()
tw = config.get_terminal_writer() tw = config.get_terminal_writer()
tw.line() tw.line()

View File

@ -706,7 +706,12 @@ class TerminalReporter(object):
self._outrep_summary(rep) self._outrep_summary(rep)
def print_teardown_sections(self, rep): def print_teardown_sections(self, rep):
showcapture = self.config.option.showcapture
if showcapture == "no":
return
for secname, content in rep.sections: for secname, content in rep.sections:
if showcapture != "all" and showcapture not in secname:
continue
if "teardown" in secname: if "teardown" in secname:
self._tw.sep("-", secname) self._tw.sep("-", secname)
if content[-1:] == "\n": if content[-1:] == "\n":

View File

@ -49,6 +49,14 @@ def pytest_addoption(parser):
) )
def pytest_configure(config):
config.addinivalue_line(
"markers",
"filterwarnings(warning): add a warning filter to the given test. "
"see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings ",
)
@contextmanager @contextmanager
def catch_warnings_for_item(item): def catch_warnings_for_item(item):
""" """

View File

@ -660,6 +660,16 @@ class TestInvocationVariants(object):
["*test_world.py::test_other*PASSED*", "*1 passed*"] ["*test_world.py::test_other*PASSED*", "*1 passed*"]
) )
def test_invoke_test_and_doctestmodules(self, testdir):
p = testdir.makepyfile(
"""
def test():
pass
"""
)
result = testdir.runpytest(str(p) + "::test", "--doctest-modules")
result.stdout.fnmatch_lines(["*1 passed*"])
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks") @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
def test_cmdline_python_package_symlink(self, testdir, monkeypatch): def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
""" """

View File

@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function
import operator import operator
import os import os
import sys import sys
import textwrap
import _pytest import _pytest
import py import py
import pytest import pytest
@ -1265,6 +1266,50 @@ raise ValueError()
] ]
) )
@pytest.mark.skipif("sys.version_info[0] < 3")
def test_exc_chain_repr_cycle(self, importasmod):
mod = importasmod(
"""
class Err(Exception):
pass
def fail():
return 0 / 0
def reraise():
try:
fail()
except ZeroDivisionError as e:
raise Err() from e
def unreraise():
try:
reraise()
except Err as e:
raise e.__cause__
"""
)
excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
r = excinfo.getrepr(style="short")
tw = TWMock()
r.toterminal(tw)
out = "\n".join(line for line in tw.lines if isinstance(line, str))
expected_out = textwrap.dedent(
"""\
:13: in unreraise
reraise()
:10: in reraise
raise Err() from e
E test_exc_chain_repr_cycle0.mod.Err
During handling of the above exception, another exception occurred:
:15: in unreraise
raise e.__cause__
:8: in reraise
fail()
:5: in fail
return 0 / 0
E ZeroDivisionError: division by zero"""
)
assert out == expected_out
@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"])

View File

@ -876,22 +876,18 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
is installed. is installed.
""" """
import logging import logging
import contextlib
from functools import partial from functools import partial
from _pytest.capture import CaptureManager
from _pytest.logging import _LiveLoggingStreamHandler from _pytest.logging import _LiveLoggingStreamHandler
class MockCaptureManager: class MockCaptureManager:
calls = [] calls = []
def suspend_global_capture(self): @contextlib.contextmanager
self.calls.append("suspend_global_capture") def global_and_fixture_disabled(self):
self.calls.append("enter disabled")
def resume_global_capture(self): yield
self.calls.append("resume_global_capture") self.calls.append("exit disabled")
# sanity check
assert CaptureManager.suspend_capture_item
assert CaptureManager.resume_global_capture
class DummyTerminal(six.StringIO): class DummyTerminal(six.StringIO):
def section(self, *args, **kwargs): def section(self, *args, **kwargs):
@ -908,10 +904,7 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
logger.critical("some message") logger.critical("some message")
if has_capture_manager: if has_capture_manager:
assert MockCaptureManager.calls == [ assert MockCaptureManager.calls == ["enter disabled", "exit disabled"]
"suspend_global_capture",
"resume_global_capture",
]
else: else:
assert MockCaptureManager.calls == [] assert MockCaptureManager.calls == []
assert out_file.getvalue() == "\nsome message\n" assert out_file.getvalue() == "\nsome message\n"

View File

@ -1583,3 +1583,43 @@ def test_package_collection_infinite_recursion(testdir):
testdir.copy_example("collect/package_infinite_recursion") testdir.copy_example("collect/package_infinite_recursion")
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines("*1 passed*") result.stdout.fnmatch_lines("*1 passed*")
def test_package_with_modules(testdir):
"""
.
root
__init__.py
sub1
__init__.py
sub1_1
__init__.py
test_in_sub1.py
sub2
test
test_in_sub2.py
"""
root = testdir.mkpydir("root")
sub1 = root.mkdir("sub1")
sub1.ensure("__init__.py")
sub1_test = sub1.mkdir("sub1_1")
sub1_test.ensure("__init__.py")
sub2 = root.mkdir("sub2")
sub2_test = sub2.mkdir("sub2")
sub1_test.join("test_in_sub1.py").write("def test_1(): pass")
sub2_test.join("test_in_sub2.py").write("def test_2(): pass")
# Execute from .
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=2)
# Execute from . with one argument "root"
result = testdir.runpytest("-v", "-s", "root")
result.assert_outcomes(passed=2)
# Chdir into package's root and execute with no args
root.chdir()
result = testdir.runpytest("-v", "-s")
result.assert_outcomes(passed=2)

View File

@ -70,19 +70,23 @@ class TestCaptureManager(object):
try: try:
capman = CaptureManager(method) capman = CaptureManager(method)
capman.start_global_capturing() capman.start_global_capturing()
outerr = capman.suspend_global_capture() capman.suspend_global_capture()
outerr = capman.read_global_capture()
assert outerr == ("", "") assert outerr == ("", "")
outerr = capman.suspend_global_capture() capman.suspend_global_capture()
outerr = capman.read_global_capture()
assert outerr == ("", "") assert outerr == ("", "")
print("hello") print("hello")
out, err = capman.suspend_global_capture() capman.suspend_global_capture()
out, err = capman.read_global_capture()
if method == "no": if method == "no":
assert old == (sys.stdout, sys.stderr, sys.stdin) assert old == (sys.stdout, sys.stderr, sys.stdin)
else: else:
assert not out assert not out
capman.resume_global_capture() capman.resume_global_capture()
print("hello") print("hello")
out, err = capman.suspend_global_capture() capman.suspend_global_capture()
out, err = capman.read_global_capture()
if method != "no": if method != "no":
assert out == "hello\n" assert out == "hello\n"
capman.stop_global_capturing() capman.stop_global_capturing()
@ -647,6 +651,34 @@ class TestCaptureFixture(object):
assert "stdout contents begin" not in result.stdout.str() assert "stdout contents begin" not in result.stdout.str()
assert "stderr contents begin" not in result.stdout.str() assert "stderr contents begin" not in result.stdout.str()
@pytest.mark.parametrize("cap", ["capsys", "capfd"])
def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap):
"""Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)"""
testdir.makepyfile(
"""
import sys
import pytest
import os
@pytest.fixture()
def fix({cap}):
print("setup out")
sys.stderr.write("setup err\\n")
yield
out, err = {cap}.readouterr()
assert out == 'setup out\\ncall out\\n'
assert err == 'setup err\\ncall err\\n'
def test_a(fix):
print("call out")
sys.stderr.write("call err\\n")
""".format(
cap=cap
)
)
reprec = testdir.inline_run()
reprec.assertoutcome(passed=1)
def test_setup_failure_does_not_kill_capturing(testdir): def test_setup_failure_does_not_kill_capturing(testdir):
sub1 = testdir.mkpydir("sub1") sub1 = testdir.mkpydir("sub1")
@ -1385,3 +1417,95 @@ def test_pickling_and_unpickling_encoded_file():
ef = capture.EncodedFile(None, None) ef = capture.EncodedFile(None, None)
ef_as_str = pickle.dumps(ef) ef_as_str = pickle.dumps(ef)
pickle.loads(ef_as_str) pickle.loads(ef_as_str)
def test_global_capture_with_live_logging(testdir):
# Issue 3819
# capture should work with live cli logging
# Teardown report seems to have the capture for the whole process (setup, capture, teardown)
testdir.makeconftest(
"""
def pytest_runtest_logreport(report):
if "test_global" in report.nodeid:
if report.when == "teardown":
with open("caplog", "w") as f:
f.write(report.caplog)
with open("capstdout", "w") as f:
f.write(report.capstdout)
"""
)
testdir.makepyfile(
"""
import logging
import sys
import pytest
logger = logging.getLogger(__name__)
@pytest.fixture
def fix1():
print("fix setup")
logging.info("fix setup")
yield
logging.info("fix teardown")
print("fix teardown")
def test_global(fix1):
print("begin test")
logging.info("something in test")
print("end test")
"""
)
result = testdir.runpytest_subprocess("--log-cli-level=INFO")
assert result.ret == 0
with open("caplog", "r") as f:
caplog = f.read()
assert "fix setup" in caplog
assert "something in test" in caplog
assert "fix teardown" in caplog
with open("capstdout", "r") as f:
capstdout = f.read()
assert "fix setup" in capstdout
assert "begin test" in capstdout
assert "end test" in capstdout
assert "fix teardown" in capstdout
@pytest.mark.parametrize("capture_fixture", ["capsys", "capfd"])
def test_capture_with_live_logging(testdir, capture_fixture):
# Issue 3819
# capture should work with live cli logging
testdir.makepyfile(
"""
import logging
import sys
logger = logging.getLogger(__name__)
def test_capture({0}):
print("hello")
sys.stderr.write("world\\n")
captured = {0}.readouterr()
assert captured.out == "hello\\n"
assert captured.err == "world\\n"
logging.info("something")
print("next")
logging.info("something")
captured = {0}.readouterr()
assert captured.out == "next\\n"
""".format(
capture_fixture
)
)
result = testdir.runpytest_subprocess("--log-cli-level=INFO")
assert result.ret == 0

View File

@ -638,6 +638,10 @@ class Test_getinitialnodes(object):
assert col.config is config assert col.config is config
def test_pkgfile(self, testdir): def test_pkgfile(self, testdir):
"""Verify nesting when a module is within a package.
The parent chain should match: Module<x.py> -> Package<subdir> -> Session.
Session's parent should always be None.
"""
tmpdir = testdir.tmpdir tmpdir = testdir.tmpdir
subdir = tmpdir.join("subdir") subdir = tmpdir.join("subdir")
x = subdir.ensure("x.py") x = subdir.ensure("x.py")
@ -645,9 +649,12 @@ class Test_getinitialnodes(object):
with subdir.as_cwd(): with subdir.as_cwd():
config = testdir.parseconfigure(x) config = testdir.parseconfigure(x)
col = testdir.getnode(config, x) col = testdir.getnode(config, x)
assert isinstance(col, pytest.Module)
assert col.name == "x.py" assert col.name == "x.py"
assert col.parent.parent is None assert isinstance(col, pytest.Module)
assert isinstance(col.parent, pytest.Package)
assert isinstance(col.parent.parent, pytest.Session)
# session is batman (has no parents)
assert col.parent.parent.parent is None
for col in col.listchain(): for col in col.listchain():
assert col.config is config assert col.config is config

View File

@ -948,6 +948,46 @@ def pytest_report_header(config, startdir):
assert "!This is stderr!" not in stdout assert "!This is stderr!" not in stdout
assert "!This is a warning log msg!" not in stdout assert "!This is a warning log msg!" not in stdout
def test_show_capture_with_teardown_logs(self, testdir):
"""Ensure that the capturing of teardown logs honor --show-capture setting"""
testdir.makepyfile(
"""
import logging
import sys
import pytest
@pytest.fixture(scope="function", autouse="True")
def hook_each_test(request):
yield
sys.stdout.write("!stdout!")
sys.stderr.write("!stderr!")
logging.warning("!log!")
def test_func():
assert False
"""
)
result = testdir.runpytest("--show-capture=stdout", "--tb=short").stdout.str()
assert "!stdout!" in result
assert "!stderr!" not in result
assert "!log!" not in result
result = testdir.runpytest("--show-capture=stderr", "--tb=short").stdout.str()
assert "!stdout!" not in result
assert "!stderr!" in result
assert "!log!" not in result
result = testdir.runpytest("--show-capture=log", "--tb=short").stdout.str()
assert "!stdout!" not in result
assert "!stderr!" not in result
assert "!log!" in result
result = testdir.runpytest("--show-capture=no", "--tb=short").stdout.str()
assert "!stdout!" not in result
assert "!stderr!" not in result
assert "!log!" not in result
@pytest.mark.xfail("not hasattr(os, 'dup')") @pytest.mark.xfail("not hasattr(os, 'dup')")
def test_fdopen_kept_alive_issue124(testdir): def test_fdopen_kept_alive_issue124(testdir):

View File

@ -287,3 +287,18 @@ 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 warnings in *"])
def test_filterwarnings_mark_registration(testdir):
"""Ensure filterwarnings mark is registered"""
testdir.makepyfile(
"""
import pytest
@pytest.mark.filterwarnings('error')
def test_func():
pass
"""
)
result = testdir.runpytest("--strict")
assert result.ret == 0

View File

@ -115,8 +115,6 @@ skipsdist = True
usedevelop = True usedevelop = True
changedir = doc/en changedir = doc/en
deps = deps =
attrs
more-itertools
PyYAML PyYAML
sphinx sphinx
sphinxcontrib-trio sphinxcontrib-trio