Skip to content

Commit

Permalink
bpo-40275: Move transient_internet from test.support to socket_helper (
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka authored Apr 29, 2020
1 parent bb4a585 commit bfb1cf4
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 144 deletions.
15 changes: 8 additions & 7 deletions Doc/library/test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ The :mod:`test.support` module defines the following constants:

Usually, a timeout using :data:`INTERNET_TIMEOUT` should not mark a test as
failed, but skip the test instead: see
:func:`~test.support.transient_internet`.
:func:`~test.support.socket_helper.transient_internet`.

Its default value is 1 minute.

Expand Down Expand Up @@ -759,12 +759,6 @@ The :mod:`test.support` module defines the following functions:
A context manager that temporarily sets the process umask.


.. function:: transient_internet(resource_name, *, timeout=30.0, errnos=())

A context manager that raises :exc:`ResourceDenied` when various issues
with the internet connection manifest themselves as exceptions.


.. function:: disable_faulthandler()

A context manager that replaces ``sys.stderr`` with ``sys.__stderr__``.
Expand Down Expand Up @@ -1488,6 +1482,13 @@ The :mod:`test.support.socket_helper` module provides support for socket tests.
sockets.


.. function:: transient_internet(resource_name, *, timeout=30.0, errnos=())

A context manager that raises :exc:`~test.support.ResourceDenied` when
various issues with the internet connection manifest themselves as
exceptions.


:mod:`test.support.script_helper` --- Utilities for the Python execution tests
==============================================================================

Expand Down
88 changes: 1 addition & 87 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"requires_linux_version", "requires_mac_ver",
"check_syntax_error", "check_syntax_warning",
"TransientResource", "time_out", "socket_peer_reset", "ioerror_peer_reset",
"transient_internet", "BasicTestRunner", "run_unittest", "run_doctest",
"BasicTestRunner", "run_unittest", "run_doctest",
"skip_unless_symlink", "requires_gzip", "requires_bz2", "requires_lzma",
"bigmemtest", "bigaddrspacetest", "cpython_only", "get_attribute",
"requires_IEEE_754", "skip_unless_xattr", "requires_zlib",
Expand Down Expand Up @@ -144,8 +144,6 @@
# option.
LONG_TIMEOUT = 5 * 60.0

_NOT_SET = object()


class Error(Exception):
"""Base class for regression test exceptions."""
Expand Down Expand Up @@ -1386,90 +1384,6 @@ def __exit__(self, type_=None, value=None, traceback=None):
ioerror_peer_reset = TransientResource(OSError, errno=errno.ECONNRESET)


@contextlib.contextmanager
def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()):
"""Return a context manager that raises ResourceDenied when various issues
with the Internet connection manifest themselves as exceptions."""
import socket
import nntplib
import urllib.error
if timeout is _NOT_SET:
timeout = INTERNET_TIMEOUT

default_errnos = [
('ECONNREFUSED', 111),
('ECONNRESET', 104),
('EHOSTUNREACH', 113),
('ENETUNREACH', 101),
('ETIMEDOUT', 110),
# socket.create_connection() fails randomly with
# EADDRNOTAVAIL on Travis CI.
('EADDRNOTAVAIL', 99),
]
default_gai_errnos = [
('EAI_AGAIN', -3),
('EAI_FAIL', -4),
('EAI_NONAME', -2),
('EAI_NODATA', -5),
# Encountered when trying to resolve IPv6-only hostnames
('WSANO_DATA', 11004),
]

denied = ResourceDenied("Resource %r is not available" % resource_name)
captured_errnos = errnos
gai_errnos = []
if not captured_errnos:
captured_errnos = [getattr(errno, name, num)
for (name, num) in default_errnos]
gai_errnos = [getattr(socket, name, num)
for (name, num) in default_gai_errnos]

def filter_error(err):
n = getattr(err, 'errno', None)
if (isinstance(err, socket.timeout) or
(isinstance(err, socket.gaierror) and n in gai_errnos) or
(isinstance(err, urllib.error.HTTPError) and
500 <= err.code <= 599) or
(isinstance(err, urllib.error.URLError) and
(("ConnectionRefusedError" in err.reason) or
("TimeoutError" in err.reason) or
("EOFError" in err.reason))) or
n in captured_errnos):
if not verbose:
sys.stderr.write(denied.args[0] + "\n")
raise denied from err

old_timeout = socket.getdefaulttimeout()
try:
if timeout is not None:
socket.setdefaulttimeout(timeout)
yield
except nntplib.NNTPTemporaryError as err:
if verbose:
sys.stderr.write(denied.args[0] + "\n")
raise denied from err
except OSError as err:
# urllib can wrap original socket errors multiple times (!), we must
# unwrap to get at the original error.
while True:
a = err.args
if len(a) >= 1 and isinstance(a[0], OSError):
err = a[0]
# The error can also be wrapped as args[1]:
# except socket.error as msg:
# raise OSError('socket error', msg).with_traceback(sys.exc_info()[2])
elif len(a) >= 2 and isinstance(a[1], OSError):
err = a[1]
else:
break
filter_error(err)
raise
# XXX should we catch generic exceptions and look for their
# __cause__ or __context__?
finally:
socket.setdefaulttimeout(old_timeout)


@contextlib.contextmanager
def captured_output(stream_name):
"""Return a context manager used by captured_stdout/stdin/stderr
Expand Down
89 changes: 89 additions & 0 deletions Lib/test/support/socket_helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import contextlib
import errno
import socket
import unittest

from .. import support


HOST = "localhost"
HOSTv4 = "127.0.0.1"
HOSTv6 = "::1"
Expand Down Expand Up @@ -175,3 +179,88 @@ def get_socket_conn_refused_errs():
if not IPV6_ENABLED:
errors.append(errno.EAFNOSUPPORT)
return errors


_NOT_SET = object()

@contextlib.contextmanager
def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()):
"""Return a context manager that raises ResourceDenied when various issues
with the Internet connection manifest themselves as exceptions."""
import nntplib
import urllib.error
if timeout is _NOT_SET:
timeout = support.INTERNET_TIMEOUT

default_errnos = [
('ECONNREFUSED', 111),
('ECONNRESET', 104),
('EHOSTUNREACH', 113),
('ENETUNREACH', 101),
('ETIMEDOUT', 110),
# socket.create_connection() fails randomly with
# EADDRNOTAVAIL on Travis CI.
('EADDRNOTAVAIL', 99),
]
default_gai_errnos = [
('EAI_AGAIN', -3),
('EAI_FAIL', -4),
('EAI_NONAME', -2),
('EAI_NODATA', -5),
# Encountered when trying to resolve IPv6-only hostnames
('WSANO_DATA', 11004),
]

denied = support.ResourceDenied("Resource %r is not available" % resource_name)
captured_errnos = errnos
gai_errnos = []
if not captured_errnos:
captured_errnos = [getattr(errno, name, num)
for (name, num) in default_errnos]
gai_errnos = [getattr(socket, name, num)
for (name, num) in default_gai_errnos]

def filter_error(err):
n = getattr(err, 'errno', None)
if (isinstance(err, socket.timeout) or
(isinstance(err, socket.gaierror) and n in gai_errnos) or
(isinstance(err, urllib.error.HTTPError) and
500 <= err.code <= 599) or
(isinstance(err, urllib.error.URLError) and
(("ConnectionRefusedError" in err.reason) or
("TimeoutError" in err.reason) or
("EOFError" in err.reason))) or
n in captured_errnos):
if not support.verbose:
sys.stderr.write(denied.args[0] + "\n")
raise denied from err

old_timeout = socket.getdefaulttimeout()
try:
if timeout is not None:
socket.setdefaulttimeout(timeout)
yield
except nntplib.NNTPTemporaryError as err:
if support.verbose:
sys.stderr.write(denied.args[0] + "\n")
raise denied from err
except OSError as err:
# urllib can wrap original socket errors multiple times (!), we must
# unwrap to get at the original error.
while True:
a = err.args
if len(a) >= 1 and isinstance(a[0], OSError):
err = a[0]
# The error can also be wrapped as args[1]:
# except socket.error as msg:
# raise OSError('socket error', msg).with_traceback(sys.exc_info()[2])
elif len(a) >= 2 and isinstance(a[1], OSError):
err = a[1]
else:
break
filter_error(err)
raise
# XXX should we catch generic exceptions and look for their
# __cause__ or __context__?
finally:
socket.setdefaulttimeout(old_timeout)
10 changes: 5 additions & 5 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1629,7 +1629,7 @@ def test_networked(self):
# Default settings: requires a valid cert from a trusted CA
import ssl
support.requires('network')
with support.transient_internet('self-signed.pythontest.net'):
with socket_helper.transient_internet('self-signed.pythontest.net'):
h = client.HTTPSConnection('self-signed.pythontest.net', 443)
with self.assertRaises(ssl.SSLError) as exc_info:
h.request('GET', '/')
Expand All @@ -1639,7 +1639,7 @@ def test_networked_noverification(self):
# Switch off cert verification
import ssl
support.requires('network')
with support.transient_internet('self-signed.pythontest.net'):
with socket_helper.transient_internet('self-signed.pythontest.net'):
context = ssl._create_unverified_context()
h = client.HTTPSConnection('self-signed.pythontest.net', 443,
context=context)
Expand All @@ -1653,7 +1653,7 @@ def test_networked_noverification(self):
def test_networked_trusted_by_default_cert(self):
# Default settings: requires a valid cert from a trusted CA
support.requires('network')
with support.transient_internet('www.python.org'):
with socket_helper.transient_internet('www.python.org'):
h = client.HTTPSConnection('www.python.org', 443)
h.request('GET', '/')
resp = h.getresponse()
Expand All @@ -1667,7 +1667,7 @@ def test_networked_good_cert(self):
import ssl
support.requires('network')
selfsigned_pythontestdotnet = 'self-signed.pythontest.net'
with support.transient_internet(selfsigned_pythontestdotnet):
with socket_helper.transient_internet(selfsigned_pythontestdotnet):
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
self.assertEqual(context.verify_mode, ssl.CERT_REQUIRED)
self.assertEqual(context.check_hostname, True)
Expand Down Expand Up @@ -1699,7 +1699,7 @@ def test_networked_bad_cert(self):
# We feed a "CA" cert that is unrelated to the server's cert
import ssl
support.requires('network')
with support.transient_internet('self-signed.pythontest.net'):
with socket_helper.transient_internet('self-signed.pythontest.net'):
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations(CERT_localhost)
h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
Expand Down
20 changes: 10 additions & 10 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import threading
import socket

from test.support import (reap_threads, verbose, transient_internet,
from test.support import (reap_threads, verbose,
run_with_tz, run_with_locale, cpython_only)
from test.support import hashlib_helper
import unittest
Expand Down Expand Up @@ -968,16 +968,16 @@ class RemoteIMAPTest(unittest.TestCase):
imap_class = imaplib.IMAP4

def setUp(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
self.server = self.imap_class(self.host, self.port)

def tearDown(self):
if self.server is not None:
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
self.server.logout()

def test_logincapa(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
for cap in self.server.capabilities:
self.assertIsInstance(cap, str)
self.assertIn('LOGINDISABLED', self.server.capabilities)
Expand All @@ -986,7 +986,7 @@ def test_logincapa(self):
self.assertEqual(rs[0], 'OK')

def test_logout(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
rs = self.server.logout()
self.server = None
self.assertEqual(rs[0], 'BYE', rs)
Expand All @@ -999,7 +999,7 @@ class RemoteIMAP_STARTTLSTest(RemoteIMAPTest):

def setUp(self):
super().setUp()
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
rs = self.server.starttls()
self.assertEqual(rs[0], 'OK')

Expand Down Expand Up @@ -1039,24 +1039,24 @@ def check_logincapa(self, server):
server.logout()

def test_logincapa(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
_server = self.imap_class(self.host, self.port)
self.check_logincapa(_server)

def test_logout(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
_server = self.imap_class(self.host, self.port)
rs = _server.logout()
self.assertEqual(rs[0], 'BYE', rs)

def test_ssl_context_certfile_exclusive(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
self.assertRaises(
ValueError, self.imap_class, self.host, self.port,
certfile=CERTFILE, ssl_context=self.create_ssl_context())

def test_ssl_context_keyfile_exclusive(self):
with transient_internet(self.host):
with socket_helper.transient_internet(self.host):
self.assertRaises(
ValueError, self.imap_class, self.host, self.port,
keyfile=CERTFILE, ssl_context=self.create_ssl_context())
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_nntplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def wrap_methods(cls):
def wrap_meth(meth):
@functools.wraps(meth)
def wrapped(self):
with support.transient_internet(self.NNTP_HOST):
with socket_helper.transient_internet(self.NNTP_HOST):
meth(self)
return wrapped
for name in dir(cls):
Expand Down Expand Up @@ -315,7 +315,7 @@ class NetworkedNNTPTests(NetworkedNNTPTestsMixin, unittest.TestCase):
@classmethod
def setUpClass(cls):
support.requires("network")
with support.transient_internet(cls.NNTP_HOST):
with socket_helper.transient_internet(cls.NNTP_HOST):
try:
cls.server = cls.NNTP_CLASS(cls.NNTP_HOST,
timeout=support.INTERNET_TIMEOUT,
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_robotparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ class NetworkTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
support.requires('network')
with support.transient_internet(cls.base_url):
with socket_helper.transient_internet(cls.base_url):
cls.parser = urllib.robotparser.RobotFileParser(cls.robots_txt)
cls.parser.read()

Expand Down
Loading

0 comments on commit bfb1cf4

Please sign in to comment.