Commit 3a3124e7 authored by Jason Madden's avatar Jason Madden

Add basic framework for wrapping stdlib tests.

Use it to fix test_interrupted_write on all Py3, and
test_https_with_cafile on PyPy3.5 instead of disabling them and
patching them inline, respectively.

See #964
parent 1414906d
...@@ -571,28 +571,6 @@ class TestUrlopen(unittest.TestCase): ...@@ -571,28 +571,6 @@ class TestUrlopen(unittest.TestCase):
self.urlopen("https://localhost:%s/bizarre" % handler.port, self.urlopen("https://localhost:%s/bizarre" % handler.port,
cafile=CERT_fakehostname) cafile=CERT_fakehostname)
# XXX: gevent: The error that was raised by that last call
# left a socket open on the server or client. The server gets
# to http/server.py(390)handle_one_request and blocks on
# self.rfile.readline which apparently is where the SSL
# handshake is done. That results in the exception being
# raised on the client above, but apparently *not* on the
# server. Consequently it sits trying to read from that
# socket. On CPython, when the client socket goes out of scope
# it is closed and the server raises an exception, closing the
# socket. On PyPy, we need a GC cycle for that to happen.
# Without the socket being closed and exception being raised,
# the server cannot be stopped (it runs each request in the
# same thread that would notice it had been stopped), and so
# the cleanup method added by start_https_server to stop the
# server blocks "forever".
# This is an important test, so rather than skip it in patched_tests_setup,
# we do the gc before we return.
import gc
gc.collect()
gc.collect()
def test_https_with_cadefault(self): def test_https_with_cadefault(self):
handler = self.start_https_server(certfile=CERT_localhost) handler = self.start_https_server(certfile=CERT_localhost)
# Self-signed cert should fail verification with system certificate store # Self-signed cert should fail verification with system certificate store
......
# pylint:disable=missing-docstring,invalid-name # pylint:disable=missing-docstring,invalid-name
from __future__ import print_function from __future__ import print_function, absolute_import, division
import collections
import contextlib
import functools
import sys import sys
import os import os
import re import re
...@@ -181,6 +185,48 @@ if 'thread' in os.getenv('GEVENT_FILE', ''): ...@@ -181,6 +185,48 @@ if 'thread' in os.getenv('GEVENT_FILE', ''):
# Fails with "OSError: 9 invalid file descriptor"; expect GC/lifetime issues # Fails with "OSError: 9 invalid file descriptor"; expect GC/lifetime issues
] ]
def _make_run_with_original(mod_name, func_name):
@contextlib.contextmanager
def with_orig():
mod = __import__(mod_name)
now = getattr(mod, func_name)
from gevent.monkey import get_original
orig = get_original(mod_name, func_name)
try:
setattr(mod, func_name, orig)
yield
finally:
setattr(mod, func_name, now)
return with_orig
@contextlib.contextmanager
def _gc_at_end():
try:
yield
finally:
import gc
gc.collect()
gc.collect()
# Map from FQN to a context manager that will be wrapped around
# that test.
wrapped_tests = {
}
class _PatchedTest(object):
def __init__(self, test_fqn):
self._patcher = wrapped_tests[test_fqn]
def __call__(self, orig_test_fn):
@functools.wraps(orig_test_fn)
def test(*args, **kwargs):
with self._patcher():
return orig_test_fn(*args, **kwargs)
return test
if sys.version_info[:3] <= (2, 7, 8): if sys.version_info[:3] <= (2, 7, 8):
...@@ -373,6 +419,21 @@ if sys.version_info[0] == 3: ...@@ -373,6 +419,21 @@ if sys.version_info[0] == 3:
'test_subprocess.ProcessTestCase.test_leaking_fds_on_error', 'test_subprocess.ProcessTestCase.test_leaking_fds_on_error',
] ]
wrapped_tests.update({
# XXX: BUG: We simply don't handle this correctly. On CPython,
# we wind up raising a BlockingIOError and then
# BrokenPipeError and then some random TypeErrors, all on the
# server. CPython 3.5 goes directly to socket.send() (via
# socket.makefile), whereas CPython 3.6 uses socket.sendall().
# On PyPy, the behaviour is much worse: we hang indefinitely, perhaps exposing a problem
# with our signal handling.
# In actuality, though, this test doesn't fully test the EINTR it expects
# to under gevent (because if its EWOULDBLOCK retry behaviour.)
# Instead, the failures were all due to `pthread_kill` trying to send a signal
# to a greenlet instead of a real thread. The solution is to deliver the signal
# to the real thread by letting it get the correct ID.
'test_wsgiref.IntegrationTests.test_interrupted_write': _make_run_with_original('threading', 'get_ident')
})
# PyPy3 5.5.0-alpha # PyPy3 5.5.0-alpha
...@@ -459,6 +520,28 @@ if hasattr(sys, 'pypy_version_info') and sys.pypy_version_info[:4] == (5, 7, 1, ...@@ -459,6 +520,28 @@ if hasattr(sys, 'pypy_version_info') and sys.pypy_version_info[:4] == (5, 7, 1,
] ]
wrapped_tests.update({
# XXX: gevent: The error that was raised by that last call
# left a socket open on the server or client. The server gets
# to http/server.py(390)handle_one_request and blocks on
# self.rfile.readline which apparently is where the SSL
# handshake is done. That results in the exception being
# raised on the client above, but apparently *not* on the
# server. Consequently it sits trying to read from that
# socket. On CPython, when the client socket goes out of scope
# it is closed and the server raises an exception, closing the
# socket. On PyPy, we need a GC cycle for that to happen.
# Without the socket being closed and exception being raised,
# the server cannot be stopped (it runs each request in the
# same thread that would notice it had been stopped), and so
# the cleanup method added by start_https_server to stop the
# server blocks "forever".
# This is an important test, so rather than skip it in patched_tests_setup,
# we do the gc before we return.
'test_urllib2_localnet.TestUrlopen.test_https_with_cafile': _gc_at_end,
})
if sys.version_info[:2] == (3, 4) and sys.version_info[:3] < (3, 4, 4): if sys.version_info[:2] == (3, 4) and sys.version_info[:3] < (3, 4, 4):
# Older versions have some issues with the SSL tests. Seen on Appveyor # Older versions have some issues with the SSL tests. Seen on Appveyor
disabled_tests += [ disabled_tests += [
...@@ -567,15 +650,6 @@ if sys.version_info[:2] >= (3, 5): ...@@ -567,15 +650,6 @@ if sys.version_info[:2] >= (3, 5):
'test_socket.GeneralModuleTests.test__sendfile_use_sendfile', 'test_socket.GeneralModuleTests.test__sendfile_use_sendfile',
# XXX: BUG: We simply don't handle this correctly. On CPython,
# we wind up raising a BlockingIOError and then
# BrokenPipeError and then some random TypeErrors, all on the
# server. CPython 3.5 goes directly to socket.send() (via
# socket.makefile), whereas CPython 3.6 uses socket.sendall().
# On PyPy, the behaviour is much worse: we hang indefinitely, perhaps exposing a problem
# with our signal handling.
'test_wsgiref.IntegrationTests.test_interrupted_write',
# Relies on the regex of the repr having the locked state (TODO: it'd be nice if # Relies on the regex of the repr having the locked state (TODO: it'd be nice if
# we did that). # we did that).
# XXX: These are commented out in the source code of test_threading because # XXX: These are commented out in the source code of test_threading because
...@@ -647,36 +721,61 @@ if OPENSSL_VERSION.startswith('LibreSSL'): ...@@ -647,36 +721,61 @@ if OPENSSL_VERSION.startswith('LibreSSL'):
# Now build up the data structure we'll use to actually find disabled tests # Now build up the data structure we'll use to actually find disabled tests
# to avoid a linear scan for every file (it seems the list could get quite large) # to avoid a linear scan for every file (it seems the list could get quite large)
# (First, freeze the source list to make sure it isn't modified anywhere) # (First, freeze the source list to make sure it isn't modified anywhere)
disabled_tests = frozenset(disabled_tests)
_disabled_tests_by_file = {} def _build_test_structure(sequence_of_tests):
for file_case_meth in disabled_tests:
file_name, case, meth = file_case_meth.split('.')
try: _disabled_tests = frozenset(sequence_of_tests)
by_file = _disabled_tests_by_file[file_name]
except KeyError: disabled_tests_by_file = collections.defaultdict(set)
by_file = _disabled_tests_by_file[file_name] = set() for file_case_meth in _disabled_tests:
file_name, _case, meth = file_case_meth.split('.')
by_file.add(meth) by_file = disabled_tests_by_file[file_name]
by_file.add(file_case_meth)
def disable_tests_in_source(source, name): return disabled_tests_by_file
if name.startswith('./'): _disabled_tests_by_file = _build_test_structure(disabled_tests)
_wrapped_tests_by_file = _build_test_structure(wrapped_tests)
def disable_tests_in_source(source, filename):
if filename.startswith('./'):
# turn "./test_socket.py" (used for auto-complete) into "test_socket.py" # turn "./test_socket.py" (used for auto-complete) into "test_socket.py"
name = name[2:] filename = filename[2:]
if name.endswith('.py'): if filename.endswith('.py'):
name = name[:-3] filename = filename[:-3]
my_disabled_tests = _disabled_tests_by_file.get(name)
if not my_disabled_tests: # XXX ignoring TestCase class name (just using function name).
return source # Maybe we should do this with the AST, or even after the test is
# imported.
my_disabled_tests = _disabled_tests_by_file.get(filename, ())
for test in my_disabled_tests: for test in my_disabled_tests:
# XXX ignoring TestCase class name
testcase = test.split('.')[-1] testcase = test.split('.')[-1]
# def foo_bar(self) -> def XXXfoo_bar(self)
source, n = re.subn(testcase, 'XXX' + testcase, source) source, n = re.subn(testcase, 'XXX' + testcase, source)
print('Removed %s (%d)' % (testcase, n), file=sys.stderr) print('Removed %s (%d)' % (testcase, n), file=sys.stderr)
my_wrapped_tests = _wrapped_tests_by_file.get(filename, {})
for test in my_wrapped_tests:
testcase = test.split('.')[-1]
# def foo_bar(self)
# ->
# import patched_tests_setup as _GEVENT_PTS
# @_GEVENT_PTS._PatchedTest('file.Case.name')
# def foo_bar(self)
pattern = r"(\s*)def " + testcase
replacement = r"\1import patched_tests_setup as _GEVENT_PTS\n"
replacement += r"\1@_GEVENT_PTS._PatchedTest('%s')\n" % (test,)
replacement += r"\1def " + testcase
source, n = re.subn(pattern, replacement, source)
print('Wrapped %s (%d)' % (testcase, n), file=sys.stderr)
return source return source
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