Skip to content

Commit

Permalink
Merge pull request #5216 from mdboom/nose-internet
Browse files Browse the repository at this point in the history
TST: Enable testing without internet access.
  • Loading branch information
tacaswell committed Oct 9, 2015
1 parent 2ac4536 commit b5d8674
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 4 deletions.
150 changes: 150 additions & 0 deletions lib/matplotlib/testing/disable_internet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Originally from astropy project (http://astropy.org), under BSD
# 3-clause license.

from __future__ import (absolute_import, division, print_function,
unicode_literals)

import contextlib
import socket

from matplotlib.externals.six.moves import urllib

# save original socket method for restoration
# These are global so that re-calling the turn_off_internet function doesn't
# overwrite them again
socket_original = socket.socket
socket_create_connection = socket.create_connection
socket_bind = socket.socket.bind
socket_connect = socket.socket.connect


INTERNET_OFF = False

# urllib2 uses a global variable to cache its default "opener" for opening
# connections for various protocols; we store it off here so we can restore to
# the default after re-enabling internet use
_orig_opener = None


# ::1 is apparently another valid name for localhost?
# it is returned by getaddrinfo when that function is given localhost

def check_internet_off(original_function):
"""
Wraps ``original_function``, which in most cases is assumed
to be a `socket.socket` method, to raise an `IOError` for any operations
on non-local AF_INET sockets.
"""

def new_function(*args, **kwargs):
if isinstance(args[0], socket.socket):
if not args[0].family in (socket.AF_INET, socket.AF_INET6):
# Should be fine in all but some very obscure cases
# More to the point, we don't want to affect AF_UNIX
# sockets.
return original_function(*args, **kwargs)
host = args[1][0]
addr_arg = 1
valid_hosts = ('localhost', '127.0.0.1', '::1')
else:
# The only other function this is used to wrap currently is
# socket.create_connection, which should be passed a 2-tuple, but
# we'll check just in case
if not (isinstance(args[0], tuple) and len(args[0]) == 2):
return original_function(*args, **kwargs)

host = args[0][0]
addr_arg = 0
valid_hosts = ('localhost', '127.0.0.1')

hostname = socket.gethostname()
fqdn = socket.getfqdn()

if host in (hostname, fqdn):
host = 'localhost'
new_addr = (host, args[addr_arg][1])
args = args[:addr_arg] + (new_addr,) + args[addr_arg + 1:]

if any([h in host for h in valid_hosts]):
return original_function(*args, **kwargs)
else:
raise IOError("An attempt was made to connect to the internet "
"by a test that was not marked `remote_data`.")
return new_function


def turn_off_internet(verbose=False):
"""
Disable internet access via python by preventing connections from being
created using the socket module. Presumably this could be worked around by
using some other means of accessing the internet, but all default python
modules (urllib, requests, etc.) use socket [citation needed].
"""

global INTERNET_OFF
global _orig_opener

if INTERNET_OFF:
return

INTERNET_OFF = True

__tracebackhide__ = True
if verbose:
print("Internet access disabled")

# Update urllib2 to force it not to use any proxies
# Must use {} here (the default of None will kick off an automatic search
# for proxies)
_orig_opener = urllib.request.build_opener()
no_proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(no_proxy_handler)
urllib.request.install_opener(opener)

socket.create_connection = check_internet_off(socket_create_connection)
socket.socket.bind = check_internet_off(socket_bind)
socket.socket.connect = check_internet_off(socket_connect)

return socket


def turn_on_internet(verbose=False):
"""
Restore internet access. Not used, but kept in case it is needed.
"""

global INTERNET_OFF
global _orig_opener

if not INTERNET_OFF:
return

INTERNET_OFF = False

if verbose:
print("Internet access enabled")

urllib.request.install_opener(_orig_opener)

socket.create_connection = socket_create_connection
socket.socket.bind = socket_bind
socket.socket.connect = socket_connect
return socket


@contextlib.contextmanager
def no_internet(verbose=False):
"""Context manager to temporarily disable internet access (if not already
disabled). If it was already disabled before entering the context manager
(i.e. `turn_off_internet` was called previously) then this is a no-op and
leaves internet access disabled until a manual call to `turn_on_internet`.
"""

already_disabled = INTERNET_OFF

turn_off_internet(verbose=verbose)
try:
yield
finally:
if not already_disabled:
turn_on_internet(verbose=verbose)
2 changes: 2 additions & 0 deletions lib/matplotlib/tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from nose import SkipTest
from nose.tools import assert_raises
from nose.plugins.attrib import attr

import matplotlib as mpl
from matplotlib import style
Expand Down Expand Up @@ -56,6 +57,7 @@ def test_use():
assert mpl.rcParams[PARAM] == VALUE


@attr('network')
def test_use_url():
with temp_style('test', DUMMY_SETTINGS):
with style.context('https://gist.github.com/adrn/6590261/raw'):
Expand Down
17 changes: 13 additions & 4 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
matplotlib.use('agg')

import nose
from nose.plugins import attrib
from matplotlib.testing.noseclasses import KnownFailure
from matplotlib import default_test_modules

Expand All @@ -27,7 +28,7 @@
while not os.path.exists(font_manager._fmcache):
time.sleep(0.5)

plugins = [KnownFailure]
plugins = [KnownFailure, attrib.Plugin]

# Nose doesn't automatically instantiate all of the plugins in the
# child processes, so we have to provide the multiprocess plugin with
Expand All @@ -36,7 +37,7 @@
multiprocess._instantiate_plugins = plugins


def run():
def run(extra_args):
try:
import faulthandler
except ImportError:
Expand All @@ -45,14 +46,22 @@ def run():
faulthandler.enable()

nose.main(addplugins=[x() for x in plugins],
defaultTest=default_test_modules)
defaultTest=default_test_modules,
argv=sys.argv + extra_args)


if __name__ == '__main__':
extra_args = []

if '--no-pep8' in sys.argv:
default_test_modules.remove('matplotlib.tests.test_coding_standards')
sys.argv.remove('--no-pep8')
elif '--pep8' in sys.argv:
default_test_modules = ['matplotlib.tests.test_coding_standards']
sys.argv.remove('--pep8')
if '--no-network' in sys.argv:
from matplotlib.testing import disable_internet
disable_internet.turn_off_internet()
extra_args.extend(['--eval-attr="not network"'])

run()
run(extra_args)

0 comments on commit b5d8674

Please sign in to comment.