capture: cleanup item fixture handling (#6663)
This started by looking at how to get the current test item in general, and then I noticed that it is not necessary for the capture plugin to track it manually in the first place.
This commit is contained in:
parent
9e262038c8
commit
b4ace46c42
|
@ -10,11 +10,14 @@ import sys
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from typing import BinaryIO
|
from typing import BinaryIO
|
||||||
|
from typing import Generator
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.compat import CaptureAndPassthroughIO
|
from _pytest.compat import CaptureAndPassthroughIO
|
||||||
from _pytest.compat import CaptureIO
|
from _pytest.compat import CaptureIO
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
|
||||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||||
|
@ -40,7 +43,7 @@ def pytest_addoption(parser):
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_load_initial_conftests(early_config, parser, args):
|
def pytest_load_initial_conftests(early_config: Config):
|
||||||
ns = early_config.known_args_namespace
|
ns = early_config.known_args_namespace
|
||||||
if ns.capture == "fd":
|
if ns.capture == "fd":
|
||||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||||
|
@ -76,14 +79,14 @@ class CaptureManager:
|
||||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method):
|
def __init__(self, method) -> None:
|
||||||
self._method = method
|
self._method = method
|
||||||
self._global_capturing = None
|
self._global_capturing = None
|
||||||
self._current_item = None
|
self._capture_fixture = None # type: Optional[CaptureFixture]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<CaptureManager _method={!r} _global_capturing={!r} _current_item={!r}>".format(
|
return "<CaptureManager _method={!r} _global_capturing={!r} _item_capture={!r}>".format(
|
||||||
self._method, self._global_capturing, self._current_item
|
self._method, self._global_capturing, self._capture_fixture
|
||||||
)
|
)
|
||||||
|
|
||||||
def _getcapture(self, method):
|
def _getcapture(self, method):
|
||||||
|
@ -100,11 +103,8 @@ class CaptureManager:
|
||||||
def is_capturing(self):
|
def is_capturing(self):
|
||||||
if self.is_globally_capturing():
|
if self.is_globally_capturing():
|
||||||
return "global"
|
return "global"
|
||||||
capture_fixture = getattr(self._current_item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if capture_fixture is not None:
|
return "fixture %s" % self._capture_fixture.request.fixturename
|
||||||
return (
|
|
||||||
"fixture %s" % self._current_item._capture_fixture.request.fixturename
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Global capturing control
|
# Global capturing control
|
||||||
|
@ -136,41 +136,59 @@ class CaptureManager:
|
||||||
|
|
||||||
def suspend(self, in_=False):
|
def suspend(self, in_=False):
|
||||||
# Need to undo local capsys-et-al if it exists before disabling global capture.
|
# Need to undo local capsys-et-al if it exists before disabling global capture.
|
||||||
self.suspend_fixture(self._current_item)
|
self.suspend_fixture()
|
||||||
self.suspend_global_capture(in_)
|
self.suspend_global_capture(in_)
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self.resume_global_capture()
|
self.resume_global_capture()
|
||||||
self.resume_fixture(self._current_item)
|
self.resume_fixture()
|
||||||
|
|
||||||
def read_global_capture(self):
|
def read_global_capture(self):
|
||||||
return self._global_capturing.readouterr()
|
return self._global_capturing.readouterr()
|
||||||
|
|
||||||
# Fixture Control (it's just forwarding, think about removing this later)
|
# Fixture Control (it's just forwarding, think about removing this later)
|
||||||
|
|
||||||
def activate_fixture(self, item):
|
@contextlib.contextmanager
|
||||||
|
def _capturing_for_request(
|
||||||
|
self, request: FixtureRequest
|
||||||
|
) -> Generator["CaptureFixture", None, None]:
|
||||||
|
if self._capture_fixture:
|
||||||
|
other_name = next(
|
||||||
|
k
|
||||||
|
for k, v in map_fixname_class.items()
|
||||||
|
if v is self._capture_fixture.captureclass
|
||||||
|
)
|
||||||
|
raise request.raiseerror(
|
||||||
|
"cannot use {} and {} at the same time".format(
|
||||||
|
request.fixturename, other_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
capture_class = map_fixname_class[request.fixturename]
|
||||||
|
self._capture_fixture = CaptureFixture(capture_class, request)
|
||||||
|
self.activate_fixture()
|
||||||
|
yield self._capture_fixture
|
||||||
|
self._capture_fixture.close()
|
||||||
|
self._capture_fixture = None
|
||||||
|
|
||||||
|
def activate_fixture(self):
|
||||||
"""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
|
||||||
the global capture.
|
the global capture.
|
||||||
"""
|
"""
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._start()
|
||||||
fixture._start()
|
|
||||||
|
|
||||||
def deactivate_fixture(self, item):
|
def deactivate_fixture(self):
|
||||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture.close()
|
||||||
fixture.close()
|
|
||||||
|
|
||||||
def suspend_fixture(self, item):
|
def suspend_fixture(self):
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._suspend()
|
||||||
fixture._suspend()
|
|
||||||
|
|
||||||
def resume_fixture(self, item):
|
def resume_fixture(self):
|
||||||
fixture = getattr(item, "_capture_fixture", None)
|
if self._capture_fixture:
|
||||||
if fixture is not None:
|
self._capture_fixture._resume()
|
||||||
fixture._resume()
|
|
||||||
|
|
||||||
# Helper context managers
|
# Helper context managers
|
||||||
|
|
||||||
|
@ -186,11 +204,11 @@ class CaptureManager:
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def item_capture(self, when, item):
|
def item_capture(self, when, item):
|
||||||
self.resume_global_capture()
|
self.resume_global_capture()
|
||||||
self.activate_fixture(item)
|
self.activate_fixture()
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.deactivate_fixture(item)
|
self.deactivate_fixture()
|
||||||
self.suspend_global_capture(in_=False)
|
self.suspend_global_capture(in_=False)
|
||||||
|
|
||||||
out, err = self.read_global_capture()
|
out, err = self.read_global_capture()
|
||||||
|
@ -214,12 +232,6 @@ class CaptureManager:
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
|
||||||
def pytest_runtest_protocol(self, item):
|
|
||||||
self._current_item = item
|
|
||||||
yield
|
|
||||||
self._current_item = None
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_setup(self, item):
|
def pytest_runtest_setup(self, item):
|
||||||
with self.item_capture("setup", item):
|
with self.item_capture("setup", item):
|
||||||
|
@ -244,18 +256,6 @@ class CaptureManager:
|
||||||
self.stop_global_capturing()
|
self.stop_global_capturing()
|
||||||
|
|
||||||
|
|
||||||
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_only_one_capture_fixture(request: FixtureRequest, name):
|
|
||||||
fixtures = sorted(set(request.fixturenames) & capture_fixtures - {name})
|
|
||||||
if fixtures:
|
|
||||||
arg = fixtures[0] if len(fixtures) == 1 else fixtures
|
|
||||||
raise request.raiseerror(
|
|
||||||
"cannot use {} and {} at the same time".format(arg, name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def capsys(request):
|
def capsys(request):
|
||||||
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
@ -264,8 +264,8 @@ def capsys(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``text`` objects.
|
``out`` and ``err`` will be ``text`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capsys")
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -277,8 +277,8 @@ def capsysbinary(request):
|
||||||
method calls, which return a ``(out, err)`` namedtuple.
|
method calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``bytes`` objects.
|
``out`` and ``err`` will be ``bytes`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capsysbinary")
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture:
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -290,12 +290,12 @@ def capfd(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``text`` objects.
|
``out`` and ``err`` will be ``text`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capfd")
|
|
||||||
if not hasattr(os, "dup"):
|
if not hasattr(os, "dup"):
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
"capfd fixture needs os.dup function which is not available in this system"
|
"capfd fixture needs os.dup function which is not available in this system"
|
||||||
)
|
)
|
||||||
with _install_capture_fixture_on_item(request, FDCapture) as fixture:
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
|
@ -307,35 +307,15 @@ def capfdbinary(request):
|
||||||
calls, which return a ``(out, err)`` namedtuple.
|
calls, which return a ``(out, err)`` namedtuple.
|
||||||
``out`` and ``err`` will be ``byte`` objects.
|
``out`` and ``err`` will be ``byte`` objects.
|
||||||
"""
|
"""
|
||||||
_ensure_only_one_capture_fixture(request, "capfdbinary")
|
|
||||||
if not hasattr(os, "dup"):
|
if not hasattr(os, "dup"):
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
"capfdbinary fixture needs os.dup function which is not available in this system"
|
"capfdbinary fixture needs os.dup function which is not available in this system"
|
||||||
)
|
)
|
||||||
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
|
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||||
|
with capman._capturing_for_request(request) as fixture:
|
||||||
yield fixture
|
yield fixture
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def _install_capture_fixture_on_item(request, capture_class):
|
|
||||||
"""
|
|
||||||
Context manager which creates a ``CaptureFixture`` instance and "installs" it on
|
|
||||||
the item/node of the given request. Used by ``capsys`` and ``capfd``.
|
|
||||||
|
|
||||||
The CaptureFixture is added as attribute of the item because it needs to accessed
|
|
||||||
by ``CaptureManager`` during its ``pytest_runtest_*`` hooks.
|
|
||||||
"""
|
|
||||||
request.node._capture_fixture = fixture = CaptureFixture(capture_class, request)
|
|
||||||
capmanager = request.config.pluginmanager.getplugin("capturemanager")
|
|
||||||
# Need to active this fixture right away in case it is being used by another fixture (setup phase).
|
|
||||||
# If this fixture is being used only by a test function (call phase), then we wouldn't need this
|
|
||||||
# activation, but it doesn't hurt.
|
|
||||||
capmanager.activate_fixture(request.node)
|
|
||||||
yield fixture
|
|
||||||
fixture.close()
|
|
||||||
del request.node._capture_fixture
|
|
||||||
|
|
||||||
|
|
||||||
class CaptureFixture:
|
class CaptureFixture:
|
||||||
"""
|
"""
|
||||||
Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
||||||
|
@ -707,6 +687,14 @@ class SysCaptureBinary(SysCapture):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
map_fixname_class = {
|
||||||
|
"capfd": FDCapture,
|
||||||
|
"capfdbinary": FDCaptureBinary,
|
||||||
|
"capsys": SysCapture,
|
||||||
|
"capsysbinary": SysCaptureBinary,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DontReadFromInput:
|
class DontReadFromInput:
|
||||||
encoding = None
|
encoding = None
|
||||||
|
|
||||||
|
|
|
@ -477,9 +477,9 @@ class TestCaptureFixture:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*test_one*",
|
"*test_one*",
|
||||||
"*capsys*capfd*same*time*",
|
"E * cannot use capfd and capsys at the same time",
|
||||||
"*test_two*",
|
"*test_two*",
|
||||||
"*capfd*capsys*same*time*",
|
"E * cannot use capsys and capfd at the same time",
|
||||||
"*2 failed in*",
|
"*2 failed in*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue