diff --git a/jenkins/__init__.py b/jenkins/__init__.py index e02bfbb..e6e93ff 100644 --- a/jenkins/__init__.py +++ b/jenkins/__init__.py @@ -181,6 +181,10 @@ class BadHTTPException(JenkinsException): pass +class TimeoutException(JenkinsException): + '''A special exception to call out in the case of a socket timeout.''' + + def auth_headers(username, password): '''Simple implementation of HTTP Basic Authentication. @@ -333,7 +337,13 @@ class Jenkins(object): raise NotFoundException('Requested item could not be found') else: raise + except socket.timeout as e: + raise TimeoutException('Error in request: %s' % (e)) except URLError as e: + # python 2.6 compatibility to ensure same exception raised + # since URLError wraps a socket timeout on python 2.6. + if str(e.reason) == "timed out": + raise TimeoutException('Error in request: %s' % (e.reason)) raise JenkinsException('Error in request: %s' % (e.reason)) def get_build_info(self, name, number, depth=0): diff --git a/requirements.txt b/requirements.txt index d2deb98..1b08df6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -six +six>=1.3.0 pbr>=0.8.2,<2.0 diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..1068cf0 --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,33 @@ +from multiprocessing import Process + +from six.moves import socketserver + + +class TestsTimeoutException(Exception): + pass + + +def time_limit(seconds, func, *args, **kwargs): + # although creating a separate process is expensive it's the only way to + # ensure cross platform that we can cleanly terminate after timeout + p = Process(target=func, args=args, kwargs=kwargs) + p.start() + p.join(seconds) + p.terminate() + if p.exitcode is None: + raise TestsTimeoutException + + +class NullServer(socketserver.TCPServer): + + request_queue_size = 1 + + def __init__(self, server_address, *args, **kwargs): + # TCPServer is old style in python 2.x so cannot use + # super() correctly, explicitly call __init__. + + # simply init'ing is sufficient to open the port, which + # with the server not started creates a black hole server + socketserver.TCPServer.__init__( + self, server_address, socketserver.BaseRequestHandler, + *args, **kwargs) diff --git a/tests/test_jenkins_sockets.py b/tests/test_jenkins_sockets.py new file mode 100644 index 0000000..d8fc945 --- /dev/null +++ b/tests/test_jenkins_sockets.py @@ -0,0 +1,39 @@ +import sys + +import jenkins +from tests.helper import NullServer +from tests.helper import TestsTimeoutException +from tests.helper import time_limit + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class JenkinsRequestTimeoutTests(unittest.TestCase): + + def setUp(self): + super(JenkinsRequestTimeoutTests, self).setUp() + self.server = NullServer(("127.0.0.1", 0)) + + def test_jenkins_open_timeout(self): + j = jenkins.Jenkins("http://%s:%s" % self.server.server_address, + None, None, timeout=0.1) + request = jenkins.Request('http://%s:%s/job/TestJob' % + self.server.server_address) + + # assert our request times out when no response + with self.assertRaises(jenkins.TimeoutException): + j.jenkins_open(request, add_crumb=False) + + def test_jenkins_open_no_timeout(self): + j = jenkins.Jenkins("http://%s:%s" % self.server.server_address, + None, None) + request = jenkins.Request('http://%s:%s/job/TestJob' % + self.server.server_address) + + # assert we don't timeout quickly like previous test when + # no timeout defined. + with self.assertRaises(TestsTimeoutException): + time_limit(0.5, j.jenkins_open, request, add_crumb=False)