Commit 7e2cc29b authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1511 from gevent/issue1510

Fix Popen.communicate() to raise exceptions from reading the streams. 
parents 785b7b55 3db1b2f6
......@@ -96,6 +96,7 @@ install:
- echo $BUILD_RUNTIMES/snakepit/$TRAVIS_PYTHON_VERSION.d
- ls -l $BUILD_RUNTIMES/snakepit/$TRAVIS_PYTHON_VERSION.d
- python -c 'import gevent; print(gevent.__version__)'
- python -c 'from gevent._compat import get_clock_info; print(get_clock_info("perf_counter"))'
script:
# The generic script for the matrix expansion is to
# test the defaults.
......
......@@ -39,7 +39,21 @@ Library and Dependency Updates
with debugging. The event libraries allocate small amounts of memory
at startup. The allocation functions have to take the GIL, but
because of the limited amount of actual allocation that gets done
this is not expected to be a concern.
this is not expected to be a bottleneck.
Other
-----
- Make `gevent.subprocess.Popen.communicate` raise exceptions raised
by reading from the process, like the standard library. In
particular, under Python 3, if the process output is being decoded
as text, this can now raise ``UnicodeDecodeError``. Reported in
:issue:`1510` by Ofer Koren.
- Make `gevent.subprocess.Popen.communicate` be more careful about
closing files. Previously if a timeout error happened, a second call
to ``communicate`` might not close the pipe.
1.5a3 (2020-01-01)
==================
......
......@@ -188,7 +188,11 @@ except ImportError:
try:
# Python 3.3+ (PEP 418)
from time import perf_counter
from time import get_clock_info
from time import monotonic
perf_counter = perf_counter
monotonic = monotonic
get_clock_info = get_clock_info
except ImportError:
import time
......@@ -196,7 +200,9 @@ except ImportError:
perf_counter = time.clock # pylint:disable=no-member
else:
perf_counter = time.time
monotonic = perf_counter
def get_clock_info(_):
return 'Unknown'
## Monitoring
def get_this_psutil_process():
......
......@@ -371,7 +371,13 @@ class FileObjectBase(object):
return getattr(self._io, name)
def __repr__(self):
return '<%s _fobj=%r%s>' % (self.__class__.__name__, self.io, self._extra_repr())
return '<%s at 0x%x %s_fobj=%r%s>' % (
self.__class__.__name__,
id(self),
'closed' if self.closed else '',
self.io,
self._extra_repr()
)
def _extra_repr(self):
return ''
......
This diff is collapsed.
......@@ -85,6 +85,7 @@ from .skipping import skipOnPurePython
from .skipping import skipWithCExtensions
from .skipping import skipOnLibuvOnTravisOnCPython27
from .skipping import skipOnPy37
from .skipping import skipOnPy3
from .skipping import skipWithoutResource
from .skipping import skipWithoutExternalNetwork
from .skipping import skipOnPy2
......
......@@ -22,16 +22,52 @@ from __future__ import absolute_import, print_function, division
import os
import unittest
import re
import gc
import functools
from . import sysinfo
# Linux/OS X/BSD platforms can implement this by calling out to lsof
# Linux/OS X/BSD platforms /can/ implement this by calling out to lsof.
# However, if psutil is available (it is cross-platform) use that.
# It is *much* faster than shelling out to lsof each time
# (Running 14 tests takes 3.964s with lsof and 0.046 with psutil)
# However, it still doesn't completely solve the issue on Windows: fds are reported
# as -1 there, so we can't fully check those.
def _collects(func):
# We've seen OSError: No such file or directory /proc/PID/fd/NUM.
# This occurs in the loop that checks open files. It first does
# listdir() and then tries readlink() on each file. But the file
# went away. This must be because of async GC in PyPy running
# destructors at arbitrary times. This became an issue in PyPy 7.2
# but could theoretically be an issue with any objects caught in a
# cycle. This is one reason we GC before we begin. (The other is
# to clean up outstanding objects that will close files in
# __del__.)
#
# Note that this can hide errors, though, by causing greenlets to get
# collected and drop references and thus close files. We should be deterministic
# and careful about closing things.
@functools.wraps(func)
def f():
gc.collect()
gc.collect()
enabled = gc.isenabled()
gc.disable()
try:
return func()
finally:
if enabled:
gc.enable()
return f
if sysinfo.WIN:
def _run_lsof():
raise unittest.SkipTest("lsof not expected on Windows")
else:
@_collects
def _run_lsof():
import tempfile
pid = os.getpid()
......@@ -70,6 +106,7 @@ def default_get_open_files(pipes=False):
results['data'] = data
return results
@_collects
def default_get_number_open_files():
if os.path.exists('/proc/'):
# Linux only
......@@ -91,12 +128,8 @@ except ImportError:
get_open_files = default_get_open_files
get_number_open_files = default_get_number_open_files
else:
# If psutil is available (it is cross-platform) use that.
# It is *much* faster than shelling out to lsof each time
# (Running 14 tests takes 3.964s with lsof and 0.046 with psutil)
# However, it still doesn't completely solve the issue on Windows: fds are reported
# as -1 there, so we can't fully check those.
@_collects
def get_open_files():
"""
Return a list of popenfile and pconn objects.
......@@ -108,37 +141,26 @@ else:
(socket.listen(1)). Unlike the lsof implementation, this will only
return sockets in a state like that.
"""
# We've seen OSError: No such file or directory
# /proc/PID/fd/NUM. This occurs in the loop that checks open
# files. It first does listdir() and then tries readlink() on
# each file. But the file went away. This must be because of
# async GC in PyPy running destructors at arbitrary times.
# This became an issue in PyPy 7.2 but could theoretically be
# an issue with any objects caught in a cycle. Try to clean
# that up before we begin.
import gc
gc.collect()
gc.collect()
results = dict()
gc.disable()
try:
for _ in range(3):
try:
process = psutil.Process()
results['data'] = process.open_files() + process.connections('all')
break
except OSError:
pass
else:
# No break executed
raise unittest.SkipTest("Unable to read open files")
finally:
gc.enable()
for _ in range(3):
try:
process = psutil.Process()
results['data'] = process.open_files() + process.connections('all')
break
except OSError:
pass
else:
# No break executed
raise unittest.SkipTest("Unable to read open files")
for x in results['data']:
results[x.fd] = x
results['data'] += ['From psutil', process]
return results
@_collects
def get_number_open_files():
process = psutil.Process()
try:
......@@ -146,3 +168,28 @@ else:
except AttributeError:
# num_fds is unix only. Is num_handles close enough on Windows?
return 0
class DoesNotLeakFilesMixin(object): # pragma: no cover
"""
A test case mixin that helps find a method that's leaking an
open file.
Only mix this in when needed to debug, it slows tests down.
"""
def setUp(self):
self.__open_files_count = get_number_open_files()
super(DoesNotLeakFilesMixin, self).setUp()
def tearDown(self):
super(DoesNotLeakFilesMixin, self).tearDown()
after = get_number_open_files()
if after > self.__open_files_count:
raise AssertionError(
"Too many open files. Before: %s < After: %s.\n%s" % (
self.__open_files_count,
after,
get_open_files()
)
)
......@@ -674,6 +674,11 @@ if WIN:
disabled_tests += [
# Issue with Unix vs DOS newlines in the file vs from the server
'test_ssl.ThreadedTests.test_socketserver',
# On appveyor, this sometimes produces 'A non-blocking socket
# operation could not be completed immediately', followed by
# 'No connection could be made because the target machine
# actively refused it'
'test_socket.NonBlockingTCPTests.testAccept',
]
# These are a problem on 3.5; on 3.6+ they wind up getting (accidentally) disabled.
......@@ -803,6 +808,10 @@ if PY3:
'test_subprocess.ProcessTestCaseNoPoll.test_cwd_with_relative_arg',
'test_subprocess.ProcessTestCase.test_cwd_with_relative_executable',
# In 3.7 and 3.8 on Travis CI, this appears to take the full 3 seconds.
# Can't reproduce it locally. We have our own copy of this that takes
# timing on CI into account.
'test_subprocess.RunFuncTestCase.test_run_with_shell_timeout_and_capture_output',
]
disabled_tests += [
......
......@@ -43,6 +43,7 @@ skipOnPyPy3 = _do_not_skip
skipOnPyPyOnWindows = _do_not_skip
skipOnPy2 = unittest.skip if sysinfo.PY2 else _do_not_skip
skipOnPy3 = unittest.skip if sysinfo.PY3 else _do_not_skip
skipOnPy37 = unittest.skip if sysinfo.PY37 else _do_not_skip
skipOnPurePython = unittest.skip if sysinfo.PURE_PYTHON else _do_not_skip
......
......@@ -20,7 +20,6 @@
from __future__ import absolute_import, print_function, division
import sys
from time import time
import os.path
from contextlib import contextmanager
from unittest import TestCase as BaseTestCase
......@@ -28,6 +27,8 @@ from functools import wraps
import gevent
from gevent._util import LazyOnClass
from gevent._compat import perf_counter
from gevent._compat import get_clock_info
from . import sysinfo
from . import params
......@@ -63,13 +64,17 @@ class TimeAssertMixin(object):
fuzzy = expected * 5.0
else:
fuzzy = expected / 2.0
start = time()
yield
elapsed = time() - start
min_time = expected - fuzzy
max_time = expected + fuzzy
start = perf_counter()
yield (min_time, max_time)
elapsed = perf_counter() - start
try:
self.assertTrue(
expected - fuzzy <= elapsed <= expected + fuzzy,
'Expected: %r; elapsed: %r; fuzzy %r' % (expected, elapsed, fuzzy))
min_time <= elapsed <= max_time,
'Expected: %r; elapsed: %r; fuzzy %r; clock_info: %s' % (
expected, elapsed, fuzzy, get_clock_info('perf_counter')
))
except AssertionError:
flaky.reraiseFlakyTestRaceCondition()
......@@ -248,6 +253,11 @@ class TestCase(TestCaseMetaClass("NewBase",
__timeout__ = params.LOCAL_TIMEOUT if not sysinfo.RUNNING_ON_CI else params.CI_TIMEOUT
switch_expected = 'default'
#: Set this to true to cause errors that get reported to the hub to
#: always get propagated to the main greenlet. This can be done at the
#: class or method level.
#: .. caution:: This can hide errors and make it look like exceptions
#: are propagated even if they're not.
error_fatal = True
uses_handle_error = True
close_on_teardown = ()
......@@ -257,7 +267,7 @@ class TestCase(TestCaseMetaClass("NewBase",
# pylint:disable=arguments-differ
if self.switch_expected == 'default':
self.switch_expected = get_switch_expected(self.fullname)
return BaseTestCase.run(self, *args, **kwargs)
return super(TestCase, self).run(*args, **kwargs)
def setUp(self):
super(TestCase, self).setUp()
......
......@@ -5,7 +5,6 @@ import sys
import os
import glob
import traceback
import time
import importlib
from datetime import timedelta
......@@ -135,7 +134,7 @@ class Runner(object):
def _reap_all(self):
while self._reap() > 0:
time.sleep(self.TIME_WAIT_REAP)
util.sleep(self.TIME_WAIT_REAP)
def _spawn(self, pool, cmd, options):
while True:
......@@ -144,7 +143,7 @@ class Runner(object):
self._running_jobs.append(job)
return
time.sleep(self.TIME_WAIT_SPAWN)
util.sleep(self.TIME_WAIT_SPAWN)
def __call__(self):
util.log("Running tests in parallel with concurrency %s" % (self._worker_count,),)
......@@ -153,11 +152,11 @@ class Runner(object):
# sequentially.
util.BUFFER_OUTPUT = self._worker_count > 1 or self._quiet
start = time.time()
start = util.perf_counter()
try:
self._run_tests()
except KeyboardInterrupt:
self._report(time.time() - start, exit=False)
self._report(util.perf_counter() - start, exit=False)
util.log('(partial results)\n')
raise
except:
......@@ -165,7 +164,7 @@ class Runner(object):
raise
self._reap_all()
self._report(time.time() - start, exit=True)
self._report(util.perf_counter() - start, exit=True)
def _run_tests(self):
"Runs the tests, produces no report."
......@@ -215,7 +214,7 @@ class TravisFoldingRunner(object):
def __init__(self, runner, travis_fold_msg):
self._runner = runner
self._travis_fold_msg = travis_fold_msg
self._travis_fold_name = str(int(time.time()))
self._travis_fold_name = str(int(util.perf_counter()))
# A zope-style acquisition proxy would be convenient here.
run_tests = runner._run_tests
......
......@@ -18,9 +18,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import time
import gevent
from gevent._compat import perf_counter
from . import sysinfo
from . import leakcheck
......@@ -69,11 +68,11 @@ class _DelayWaitMixin(object):
seconds = getattr(timeout, 'seconds', timeout)
gevent.get_hub().loop.update_now()
start = time.time()
start = perf_counter()
try:
result = self.wait(timeout)
finally:
self._check_delay_bounds(seconds, time.time() - start,
self._check_delay_bounds(seconds, perf_counter() - start,
self._default_delay_min_adj,
self._default_delay_max_adj)
return result
......
......@@ -6,10 +6,11 @@ import traceback
import unittest
import threading
import subprocess
import time
from time import sleep
from . import six
from gevent._config import validate_bool
from gevent._compat import perf_counter
from gevent.monkey import get_original
# pylint: disable=broad-except,attribute-defined-outside-init
......@@ -391,9 +392,9 @@ def run(command, **kwargs): # pylint:disable=too-many-locals
name = popen.name
try:
time_start = time.time()
time_start = perf_counter()
out, err = popen.communicate()
took = time.time() - time_start
took = perf_counter() - time_start
if popen.was_killed or popen.poll() is None:
result = 'TIMEOUT'
else:
......@@ -611,7 +612,7 @@ class TestServer(ExampleMixin,
def before(self):
if self.before_delay is not None:
time.sleep(self.before_delay)
sleep(self.before_delay)
self.assertIsNone(self.popen.poll(),
'%s died with code %s' % (
self.example, self.popen.poll(),
......@@ -619,7 +620,7 @@ class TestServer(ExampleMixin,
def after(self):
if self.after_delay is not None:
time.sleep(self.after_delay)
sleep(self.after_delay)
self.assertIsNone(self.popen.poll(),
'%s died with code %s' % (
self.example, self.popen.poll(),
......@@ -646,6 +647,6 @@ class alarm(threading.Thread):
self.start()
def run(self):
time.sleep(self.timeout)
sleep(self.timeout)
sys.stderr.write('Timeout.\n')
os._exit(5)
......@@ -5,37 +5,51 @@ import unittest
import sys
import gevent.testing as greentest
from gevent import core
from gevent._config import Loop
available_loops = Loop().get_options()
available_loops.pop('libuv', None)
def not_available(name):
return isinstance(available_loops[name], ImportError)
class TestCore(unittest.TestCase):
def test_get_version(self):
version = core.get_version() # pylint: disable=no-member
self.assertIsInstance(version, str)
self.assertTrue(version)
header_version = core.get_header_version() # pylint: disable=no-member
self.assertIsInstance(header_version, str)
self.assertTrue(header_version)
self.assertEqual(version, header_version)
class TestWatchers(unittest.TestCase):
class WatcherTestMixin(object):
kind = None
def _makeOne(self):
return core.loop() # pylint:disable=no-member
return self.kind(default=False) # pylint:disable=not-callable
def destroyOne(self, loop):
loop.destroy()
def setUp(self):
self.loop = self._makeOne()
self.core = sys.modules[self.kind.__module__]
def tearDown(self):
self.destroyOne(self.loop)
del self.loop
def test_get_version(self):
version = self.core.get_version() # pylint: disable=no-member
self.assertIsInstance(version, str)
self.assertTrue(version)
header_version = self.core.get_header_version() # pylint: disable=no-member
self.assertIsInstance(header_version, str)
self.assertTrue(header_version)
self.assertEqual(version, header_version)
def test_events_conversion(self):
self.assertEqual(self.core._events_to_str(self.core.READ | self.core.WRITE), # pylint: disable=no-member
'READ|WRITE')
def test_EVENTS(self):
self.assertEqual(str(self.core.EVENTS), # pylint: disable=no-member
'gevent.core.EVENTS')
self.assertEqual(repr(self.core.EVENTS), # pylint: disable=no-member
'gevent.core.EVENTS')
def test_io(self):
if greentest.WIN:
# libev raises IOError, libuv raises ValueError
......@@ -46,30 +60,65 @@ class TestWatchers(unittest.TestCase):
with self.assertRaises(Error):
self.loop.io(-1, 1)
if hasattr(core, 'TIMER'):
if hasattr(self.core, 'TIMER'):
# libev
with self.assertRaises(ValueError):
self.loop.io(1, core.TIMER) # pylint:disable=no-member
self.loop.io(1, self.core.TIMER) # pylint:disable=no-member
# Test we can set events and io before it's started
if not greentest.WIN:
# We can't do this with arbitrary FDs on windows;
# see libev_vfd.h
io = self.loop.io(1, core.READ) # pylint:disable=no-member
io = self.loop.io(1, self.core.READ) # pylint:disable=no-member
io.fd = 2
self.assertEqual(io.fd, 2)
io.events = core.WRITE # pylint:disable=no-member
if not hasattr(core, 'libuv'):
io.events = self.core.WRITE # pylint:disable=no-member
if not hasattr(self.core, 'libuv'):
# libev
# pylint:disable=no-member
self.assertEqual(core._events_to_str(io.events), 'WRITE|_IOFDSET')
self.assertEqual(self.core._events_to_str(io.events), 'WRITE|_IOFDSET')
else:
self.assertEqual(core._events_to_str(io.events), # pylint:disable=no-member
self.assertEqual(self.core._events_to_str(io.events), # pylint:disable=no-member
'WRITE')
io.start(lambda: None)
io.close()
def test_timer_constructor(self):
with self.assertRaises(ValueError):
self.loop.timer(1, -1)
def test_signal_constructor(self):
with self.assertRaises(ValueError):
self.loop.signal(1000)
class LibevTestMixin(WatcherTestMixin):
def test_flags_conversion(self):
# pylint: disable=no-member
core = self.core
if not greentest.WIN:
self.assertEqual(core.loop(2, default=False).backend_int, 2)
self.assertEqual(core.loop('select', default=False).backend, 'select')
self.assertEqual(core._flags_to_int(None), 0)
self.assertEqual(core._flags_to_int(['kqueue', 'SELECT']), core.BACKEND_KQUEUE | core.BACKEND_SELECT)
self.assertEqual(core._flags_to_list(core.BACKEND_PORT | core.BACKEND_POLL), ['port', 'poll'])
self.assertRaises(ValueError, core.loop, ['port', 'blabla'])
self.assertRaises(TypeError, core.loop, object())
@unittest.skipIf(not_available('libev-cext'), "Needs libev-cext")
class TestLibevCext(LibevTestMixin, unittest.TestCase):
kind = available_loops['libev-cext']
@unittest.skipIf(not_available('libev-cffi'), "Needs libev-cffi")
class TestLibevCffi(LibevTestMixin, unittest.TestCase):
kind = available_loops['libev-cffi']
@unittest.skipIf(not_available('libuv-cffi'), "Needs libuv-cffi")
class TestLibuvCffi(WatcherTestMixin, unittest.TestCase):
kind = available_loops['libuv-cffi']
@greentest.skipOnLibev("libuv-specific")
@greentest.skipOnWindows("Destroying the loop somehow fails")
def test_io_multiplex_events(self):
......@@ -77,7 +126,7 @@ class TestWatchers(unittest.TestCase):
import socket
sock = socket.socket()
fd = sock.fileno()
core = self.core
read = self.loop.io(fd, core.READ)
write = self.loop.io(fd, core.WRITE)
......@@ -106,70 +155,6 @@ class TestWatchers(unittest.TestCase):
write.close()
sock.close()
def test_timer_constructor(self):
with self.assertRaises(ValueError):
self.loop.timer(1, -1)
def test_signal_constructor(self):
with self.assertRaises(ValueError):
self.loop.signal(1000)
class TestWatchersDefault(TestWatchers):
def _makeOne(self):
return core.loop(default=True) # pylint:disable=no-member
def destroyOne(self, loop):
return
# XXX: The crash may be fixed? The hang showed up after the crash was
# reproduced and fixed on linux and OS X.
@greentest.skipOnLibuvOnWin(
"This crashes with PyPy 5.10.0, only on Windows. "
"See https://ci.appveyor.com/project/denik/gevent/build/1.0.1380/job/lrlvid6mkjtyrhn5#L1103 "
"It has also timed out, but only on Appveyor CPython 3.6; local CPython 3.6 does not. "
"See https://ci.appveyor.com/project/denik/gevent/build/1.0.1414/job/yn7yi8b53vtqs8lw#L1523")
@greentest.skipIf(
greentest.LIBUV and greentest.RUNNING_ON_TRAVIS and sys.version_info == (3, 8, 0, 'beta', 4),
"Crashes on 3.8.0b4 on TravisCI. "
"(https://travis-ci.org/gevent/gevent/jobs/582031266#L215) "
"Unable to reproduce locally so far on macOS."
)
class TestWatchersDefaultDestroyed(TestWatchers):
def _makeOne(self):
# pylint: disable=no-member
l = core.loop(default=True)
l.destroy()
del l
return core.loop(default=True)
@greentest.skipOnLibuv("Tests for libev-only functions")
class TestLibev(unittest.TestCase):
def test_flags_conversion(self):
# pylint: disable=no-member
if not greentest.WIN:
self.assertEqual(core.loop(2, default=False).backend_int, 2)
self.assertEqual(core.loop('select', default=False).backend, 'select')
self.assertEqual(core._flags_to_int(None), 0)
self.assertEqual(core._flags_to_int(['kqueue', 'SELECT']), core.BACKEND_KQUEUE | core.BACKEND_SELECT)
self.assertEqual(core._flags_to_list(core.BACKEND_PORT | core.BACKEND_POLL), ['port', 'poll'])
self.assertRaises(ValueError, core.loop, ['port', 'blabla'])
self.assertRaises(TypeError, core.loop, object())
class TestEvents(unittest.TestCase):
def test_events_conversion(self):
self.assertEqual(core._events_to_str(core.READ | core.WRITE), # pylint: disable=no-member
'READ|WRITE')
def test_EVENTS(self):
self.assertEqual(str(core.EVENTS), # pylint: disable=no-member
'gevent.core.EVENTS')
self.assertEqual(repr(core.EVENTS), # pylint: disable=no-member
'gevent.core.EVENTS')
if __name__ == '__main__':
greentest.main()
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment