-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
[WIP] capture: do not close tmpfile buffer, but redirect #6034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3febdff
678ad65
33f41d3
ad87e06
e2e43e5
96be106
1ca2283
06f2154
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,17 @@ | |
per-test stdout/stderr capturing mechanism. | ||
|
||
""" | ||
import atexit | ||
import collections | ||
import contextlib | ||
import io | ||
import os | ||
import sys | ||
from io import UnsupportedOperation | ||
from tempfile import TemporaryFile | ||
from typing import Callable | ||
from typing import List | ||
from typing import Optional | ||
|
||
import pytest | ||
from _pytest.compat import CaptureIO | ||
|
@@ -77,19 +81,29 @@ def __init__(self, method): | |
self._method = method | ||
self._global_capturing = None | ||
self._current_item = None | ||
self._atexit_funcs = [] # type: List[Callable] | ||
atexit.register(self._atexit_run) | ||
self._tmpfiles = {} | ||
|
||
def __repr__(self): | ||
return "<CaptureManager _method={!r} _global_capturing={!r} _current_item={!r}>".format( | ||
self._method, self._global_capturing, self._current_item | ||
) | ||
|
||
def _atexit_register(self, func): | ||
self._atexit_funcs.append(func) | ||
|
||
def _atexit_run(self): | ||
for func in self._atexit_funcs: | ||
func() | ||
|
||
def _getcapture(self, method): | ||
if method == "fd": | ||
return MultiCapture(out=True, err=True, Capture=FDCapture) | ||
return MultiCapture(out=True, err=True, Capture=FDCapture, capman=self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we would pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. more universal means more coupling here... I prefer to pass only the necessary. I also suggest making it mandatory, so we don't have to check for its existense first (might require a few test changes I suppose). |
||
elif method == "sys": | ||
return MultiCapture(out=True, err=True, Capture=SysCapture) | ||
return MultiCapture(out=True, err=True, Capture=SysCapture, capman=self) | ||
elif method == "no": | ||
return MultiCapture(out=False, err=False, in_=False) | ||
return MultiCapture(out=False, err=False, in_=False, capman=self) | ||
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover | ||
|
||
def is_capturing(self): | ||
|
@@ -450,13 +464,20 @@ class MultiCapture: | |
out = err = in_ = None | ||
_state = None | ||
|
||
def __init__(self, out=True, err=True, in_=True, Capture=None): | ||
def __init__( | ||
self, | ||
out=True, | ||
err=True, | ||
in_=True, | ||
Capture=None, | ||
capman: Optional[CaptureManager] = None, | ||
): | ||
if in_: | ||
self.in_ = Capture(0) | ||
self.in_ = Capture(0, capman=capman) | ||
if out: | ||
self.out = Capture(1) | ||
self.out = Capture(1, capman=capman) | ||
if err: | ||
self.err = Capture(2) | ||
self.err = Capture(2, capman=capman) | ||
|
||
def __repr__(self): | ||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( | ||
|
@@ -539,8 +560,9 @@ class FDCaptureBinary: | |
EMPTY_BUFFER = b"" | ||
_state = None | ||
|
||
def __init__(self, targetfd, tmpfile=None): | ||
def __init__(self, targetfd, tmpfile=None, capman: Optional[CaptureManager] = None): | ||
self.targetfd = targetfd | ||
self._capman = capman | ||
try: | ||
self.targetfd_save = os.dup(self.targetfd) | ||
except OSError: | ||
|
@@ -551,15 +573,33 @@ def __init__(self, targetfd, tmpfile=None): | |
self.done = self._done | ||
if targetfd == 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this entire tmpfile = self._tmpfile_pool.obtain_tmpfile(self.targetfd) |
||
assert not tmpfile, "cannot set tmpfile with stdin" | ||
tmpfile = open(os.devnull, "r") | ||
if capman: | ||
try: | ||
tmpfile = capman._tmpfiles[0] | ||
assert not tmpfile.closed | ||
except KeyError: | ||
tmpfile = open(os.devnull, "r") | ||
capman._tmpfiles[targetfd] = tmpfile | ||
else: | ||
tmpfile = open(os.devnull, "r") | ||
self.syscapture = SysCapture(targetfd) | ||
else: | ||
if tmpfile is None: | ||
f = TemporaryFile() | ||
with f: | ||
tmpfile = safe_text_dupfile(f, mode="wb+") | ||
if capman: | ||
try: | ||
tmpfile = capman._tmpfiles[targetfd] | ||
assert not tmpfile.closed | ||
except KeyError: | ||
f = TemporaryFile() | ||
with f: | ||
tmpfile = safe_text_dupfile(f, mode="wb+") | ||
capman._tmpfiles[targetfd] = tmpfile | ||
else: | ||
f = TemporaryFile() | ||
with f: | ||
tmpfile = safe_text_dupfile(f, mode="wb+") | ||
if targetfd in patchsysdict: | ||
self.syscapture = SysCapture(targetfd, tmpfile) | ||
self.syscapture = SysCapture(targetfd, tmpfile, capman) | ||
else: | ||
self.syscapture = NoCapture() | ||
self.tmpfile = tmpfile | ||
|
@@ -594,7 +634,12 @@ def _done(self): | |
os.dup2(targetfd_save, self.targetfd) | ||
os.close(targetfd_save) | ||
self.syscapture.done() | ||
self.tmpfile.close() | ||
if self._capman: | ||
# Redirect any remaining output. | ||
os.dup2(self.targetfd, self.tmpfile_fd) | ||
self._capman._atexit_register(self.tmpfile.close) | ||
else: | ||
self.tmpfile.close() | ||
self._state = "done" | ||
|
||
def suspend(self): | ||
|
@@ -636,8 +681,9 @@ class SysCapture: | |
EMPTY_BUFFER = str() | ||
_state = None | ||
|
||
def __init__(self, fd, tmpfile=None): | ||
def __init__(self, fd, tmpfile=None, capman: Optional[CaptureManager] = None): | ||
name = patchsysdict[fd] | ||
self._capman = capman | ||
self._old = getattr(sys, name) | ||
self.name = name | ||
if tmpfile is None: | ||
|
@@ -665,7 +711,10 @@ def snap(self): | |
def done(self): | ||
setattr(sys, self.name, self._old) | ||
del self._old | ||
self.tmpfile.close() | ||
if self._capman: | ||
self._capman._atexit_register(self.tmpfile.close) | ||
else: | ||
self.tmpfile.close() | ||
self._state = "done" | ||
|
||
def suspend(self): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the tmpfile management can be isolated in a separate class, which would greatly simplify the code:
We should then make
CaptureManager
create and maintain thetmpfile_pool
, and pass it around toFDCaptureBinary
.CaptureManager
should then register theatexit
handle itself, so these 3 lines would become:This design also allows us to change the pool implementation, so we could even possibly control it via a config variable and get back the old behavior (I'm not actually suggesting it, just mentioning that this is then possible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I also realize now that we don't respect
basetemp
when creatingTemporaryFiles
...