← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/explicit-proxy-roundup into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/explicit-proxy-roundup into lp:launchpad with lp:~cjwatson/launchpad/explicit-proxy-github as a prerequisite.

Commit message:
Convert the Roundup external bug tracker to urlfetch.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/explicit-proxy-roundup/+merge/348428

I experimented with a couple of different approaches to converting the doctests, and eventually decided that the most readable option was to keep TestRoundup and add some responses-based helper code to it.  This is done in such a way that it can be reused for the other test tracker subclasses.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/explicit-proxy-roundup into lp:launchpad.
=== modified file 'lib/lp/bugs/doc/bugwatch.txt'
--- lib/lp/bugs/doc/bugwatch.txt	2013-04-11 04:50:27 +0000
+++ lib/lp/bugs/doc/bugwatch.txt	2018-06-23 00:01:05 +0000
@@ -465,7 +465,9 @@
     >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
     >>> bug_watch_updater = CheckwatchesMaster(transaction, FakeLogger())
     >>> external_bugtracker = TestRoundup(bug_tracker.baseurl)
-    >>> bug_watch_updater.updateBugWatches(external_bugtracker, [bug_watch])
+    >>> with external_bugtracker.responses():
+    ...     bug_watch_updater.updateBugWatches(
+    ...         external_bugtracker, [bug_watch])
     INFO Updating 1 watches for 1 bugs on http://some.where
 
     >>> bug.bugtasks[0].status.title

=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
--- lib/lp/bugs/doc/checkwatches.txt	2016-12-22 16:32:38 +0000
+++ lib/lp/bugs/doc/checkwatches.txt	2018-06-23 00:01:05 +0000
@@ -15,8 +15,8 @@
     >>> transaction.commit()
 
 We set a default timeout on checkwatches to 30 seconds. In order to test
-this, we can monkey-patch urllib2.urlopen so that it always raises a
-timeout and call the checkwatches cronscript machinery directly.
+this, we can inject a mock timeout using `responses` and call the
+checkwatches cronscript machinery directly.
 
 First, we create some bug watches to test with:
 
@@ -51,30 +51,31 @@
 
     >>> login('no-priv@xxxxxxxxxxxxx')
 
-Next, we monkey-patch urllib2.urlopen so that it always times out.
+Next, we ensure that the request always times out.
 
 The timeout will not produce an OOPS report; they happen routinely and
 require no action from the Launchpad end.
 
+    >>> from contextlib import contextmanager
+    >>> import re
     >>> import socket
-    >>> import urllib2
-    >>> urlopen = urllib2.urlopen
-
+    >>> import responses
     >>> from lp.testing.fixture import CaptureOops
-    >>> capture = CaptureOops()
-    >>> capture.setUp()
-    >>> def do_not_urlopen(url=None, data=None, timeout=None):
-    ...     raise socket.timeout("Connection timed out.")
-    >>> try:
-    ...     urllib2.urlopen = do_not_urlopen
+
+    >>> @contextmanager
+    ... def timeout_requests():
+    ...     with responses.RequestsMock() as requests_mock:
+    ...         requests_mock.add(
+    ...             'GET', re.compile(r'.*'),
+    ...             body=socket.timeout('Connection timed out.'))
+    ...         yield
+
+    >>> with CaptureOops() as capture, timeout_requests():
     ...     updater = CheckwatchesMaster(transaction.manager)
     ...     updater.updateBugTrackers(
     ...         bug_tracker_names=[example_bug_tracker_name])
-    ... finally:
-    ...     urllib2.urlopen = urlopen
-    >>> capture.oopses
+    ...     print(capture.oopses)
     []
-    >>> capture.cleanUp()
 
 Errors that occur when updating a bug watch are recorded against that
 bug watch. The timeout will be recorded against the bug watch we just
@@ -146,8 +147,8 @@
 
 Since our example bug tracker is a Roundup bug tracker we can
 monkey-patch the Roundup ExternalBugTrackerClass in order to set its
-batch size. We will also monkey-patch urllib2.urlopen again so that no
-requests are actually made.
+batch size. We will also insert a mock response again so that no requests
+are actually made.
 
     >>> from lp.services.log.logger import FakeLogger
     >>> from lp.bugs import externalbugtracker
@@ -156,16 +157,15 @@
     >>> updater = CheckwatchesMaster(transaction.manager)
     >>> original_log = updater.logger
     >>> batch_size = externalbugtracker.Roundup.batch_size
-    >>> try:
-    ...     urllib2.urlopen = do_not_urlopen
-    ...     externalbugtracker.Roundup.batch_size = 5
-    ...     transaction.commit()
-    ...     updater.logger = FakeLogger()
-    ...     updater.updateBugTrackers([example_bug_tracker_name])
-    ... finally:
-    ...     updater.logger = original_log
-    ...     externalbugtracker.Roundup.batch_size = batch_size
-    ...     urllib2.urlopen = urlopen
+    >>> with timeout_requests():
+    ...     try:
+    ...         externalbugtracker.Roundup.batch_size = 5
+    ...         transaction.commit()
+    ...         updater.logger = FakeLogger()
+    ...         updater.updateBugTrackers([example_bug_tracker_name])
+    ...     finally:
+    ...         updater.logger = original_log
+    ...         externalbugtracker.Roundup.batch_size = batch_size
     DEBUG No global batch size specified.
     INFO Updating 5 watches for 5 bugs on http://bugs.example.com
     INFO Connection timed out when updating ...

=== modified file 'lib/lp/bugs/doc/externalbugtracker-roundup.txt'
--- lib/lp/bugs/doc/externalbugtracker-roundup.txt	2012-12-26 01:32:19 +0000
+++ lib/lp/bugs/doc/externalbugtracker-roundup.txt	2018-06-23 00:01:05 +0000
@@ -75,13 +75,13 @@
 local variable for later use.
 
 We use a test-oriented implementation for the purposes of these tests, which
-overrides ExternalBugTracker.urlopen() so that we don't have to rely on a
-working network connection.
+avoids relying on a working network connection.
 
     >>> from lp.bugs.tests.externalbugtracker import (
     ...     TestRoundup, print_bugwatches)
     >>> roundup = TestRoundup(u'http://test.roundup/')
-    >>> roundup.initializeRemoteBugDB([1])
+    >>> with roundup.responses():
+    ...     roundup.initializeRemoteBugDB([1])
     >>> sorted(roundup.bugs.keys())
     [1]
 
@@ -90,26 +90,27 @@
 
 There are two means by which we can export Roundup bug statuses: on a
 bug-by-bug basis and as a batch. When the number of bugs that need updating is
-less than a given bug roundupker's batch_query_threshold the bugs will be
+less than a given bug tracker's batch_query_threshold the bugs will be
 fetched one-at-a-time:
 
     >>> roundup.batch_query_threshold
     10
 
-    >>> roundup.trace_calls = True
-    >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
-    CALLED urlopen(u'http://test.roundup/issue?...&id=6')
-    CALLED urlopen(u'http://test.roundup/issue?...&id=7')
-    CALLED urlopen(u'http://test.roundup/issue?...&id=8')
-    CALLED urlopen(u'http://test.roundup/issue?...&id=9')
-    CALLED urlopen(u'http://test.roundup/issue?...&id=10')
+    >>> with roundup.responses(trace_calls=True):
+    ...     roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
+    GET http://test.roundup/issue?...&id=6
+    GET http://test.roundup/issue?...&id=7
+    GET http://test.roundup/issue?...&id=8
+    GET http://test.roundup/issue?...&id=9
+    GET http://test.roundup/issue?...&id=10
 
 If there are more than batch_query_threshold bugs to update then they are
 fetched as a batch:
 
     >>> roundup.batch_query_threshold = 4
-    >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
-    CALLED urlopen(u'http://test.roundup/issue?...@startwith=0')
+    >>> with roundup.responses(trace_calls=True):
+    ...     roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
+    GET http://test.roundup/issue?...@startwith=0
 
 
 == Updating Bug Watches ==
@@ -145,8 +146,9 @@
     >>> bug_watch_updater = CheckwatchesMaster(
     ...     txn, logger=FakeLogger())
     >>> roundup = TestRoundup(example_bug_tracker.baseurl)
-    >>> bug_watch_updater.updateBugWatches(
-    ...     roundup, example_bug_tracker.watches)
+    >>> with roundup.responses():
+    ...     bug_watch_updater.updateBugWatches(
+    ...         roundup, example_bug_tracker.watches)
     INFO Updating 1 watches for 1 bugs on http://bugs.some.where
     >>> print_bugwatches(example_bug_tracker.watches)
     Remote bug 1: 1
@@ -178,11 +180,11 @@
     ...         bugtracker=example_bug_tracker,
     ...         remotebug=str(remote_bug_id))
 
-    >>> roundup.trace_calls = True
-    >>> bug_watch_updater.updateBugWatches(
-    ...     roundup, example_bug_tracker.watches)
+    >>> with roundup.responses(trace_calls=True):
+    ...     bug_watch_updater.updateBugWatches(
+    ...         roundup, example_bug_tracker.watches)
     INFO Updating 11 watches for 11 bugs on http://bugs.some.where
-    CALLED urlopen(u'http://.../issue?...@startwith=0')
+    GET http://.../issue?...@startwith=0
 
     >>> print_bugwatches(example_bug_tracker.watches,
     ...     roundup.convertRemoteStatus)

=== modified file 'lib/lp/bugs/externalbugtracker/roundup.py'
--- lib/lp/bugs/externalbugtracker/roundup.py	2015-10-15 14:09:50 +0000
+++ lib/lp/bugs/externalbugtracker/roundup.py	2018-06-23 00:01:05 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Round ExternalBugTracker utility."""
@@ -13,7 +13,7 @@
 
 from lp.bugs.externalbugtracker import (
     BugNotFound,
-    ExternalBugTracker,
+    ExternalBugTrackerRequests,
     InvalidBugId,
     LookupTree,
     UnknownRemoteStatusError,
@@ -42,7 +42,7 @@
         for (key, value) in items)
 
 
-class Roundup(ExternalBugTracker):
+class Roundup(ExternalBugTrackerRequests):
     """An ExternalBugTracker descendant for handling Roundup bug trackers."""
 
     _status_fields_map = {
@@ -218,7 +218,7 @@
         """See `ExternalBugTracker`."""
         bug_id = int(bug_id)
         query_url = self.getSingleBugExportURL(bug_id)
-        reader = csv.DictReader(self._fetchPage(query_url))
+        reader = csv.DictReader(self._getPage(query_url).iter_lines())
         return (bug_id, reader.next())
 
     def getRemoteBugBatch(self, bug_ids):
@@ -230,7 +230,7 @@
         #      export the bug ids needed rather than hitting the remote
         #      tracker for a potentially massive number of bugs.
         query_url = self.getBatchBugExportURL()
-        remote_bugs = csv.DictReader(self._fetchPage(query_url))
+        remote_bugs = csv.DictReader(self._getPage(query_url).iter_lines())
         bugs = {}
         for remote_bug in remote_bugs:
             # We're only interested in the bug if it's one of the ones in

=== modified file 'lib/lp/bugs/tests/externalbugtracker.py'
--- lib/lp/bugs/tests/externalbugtracker.py	2018-06-05 12:18:58 +0000
+++ lib/lp/bugs/tests/externalbugtracker.py	2018-06-23 00:01:05 +0000
@@ -5,6 +5,7 @@
 
 __metaclass__ = type
 
+from contextlib import contextmanager
 from copy import deepcopy
 from datetime import (
     datetime,
@@ -20,9 +21,15 @@
     BaseHandler,
     Request,
     )
-import urlparse
 import xmlrpclib
 
+import responses
+from six.moves.urllib_parse import (
+    parse_qs,
+    urljoin,
+    urlparse,
+    urlsplit,
+    )
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -158,6 +165,30 @@
         naked.updateStatus(UNKNOWN_REMOTE_STATUS, BugTaskStatus.UNKNOWN)
 
 
+class BugTrackerResponsesMixin:
+    """A responses-based test mixin for bug trackers.
+
+    The simplest way to use this is as a context manager:
+
+        with bug_tracker.responses():
+            ...
+
+    Classes using this mixin should implement `addResponses`.
+    """
+
+    def addResponses(self, requests_mock, **kwargs):
+        """Add test responses."""
+
+    @contextmanager
+    def responses(self, trace_calls=False, **kwargs):
+        with responses.RequestsMock() as requests_mock:
+            self.addResponses(requests_mock, **kwargs)
+            yield requests_mock
+            if trace_calls:
+                for call in requests_mock.calls:
+                    print call.request.method, call.request.url
+
+
 class TestExternalBugTracker(ExternalBugTracker):
     """A test version of `ExternalBugTracker`.
 
@@ -1522,30 +1553,26 @@
         return [self.utc_time]
 
 
-class TestRoundup(Roundup):
-    """Roundup ExternalBugTracker for testing purposes.
-
-    It overrides urlopen, so that access to a real Roundup instance isn't
-    needed.
-    """
+class TestRoundup(BugTrackerResponsesMixin, Roundup):
+    """Roundup ExternalBugTracker for testing purposes."""
 
     # We remove the batch_size limit for the purposes of the tests so
     # that we can test batching and not batching correctly.
     batch_size = None
-    trace_calls = False
-
-    def urlopen(self, url, data=None):
-        if self.trace_calls:
-            print "CALLED urlopen(%r)" % (url.get_full_url())
-
-        file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
-
-        if self.host == 'bugs.python.org':
-            return open(
-                file_path + '/' + 'python_example_ticket_export.csv', 'r')
+
+    @staticmethod
+    def _getCallback(request):
+        url = urlsplit(request.url)
+        if url.netloc == 'bugs.python.org':
+            body = read_test_file('python_example_ticket_export.csv')
         else:
-            return open(
-                file_path + '/' + 'roundup_example_ticket_export.csv', 'r')
+            body = read_test_file('roundup_example_ticket_export.csv')
+        return 200, {}, body
+
+    def addResponses(self, requests_mock):
+        """Add test responses."""
+        requests_mock.add_callback(
+            'GET', re.compile(re.escape(self.baseurl)), self._getCallback)
 
 
 class TestRequestTracker(RequestTracker):
@@ -1559,7 +1586,7 @@
 
     def urlopen(self, page, data=None):
         file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
-        path = urlparse.urlparse(page)[2].lstrip('/')
+        path = urlparse(page)[2].lstrip('/')
         if self.trace_calls:
             print "CALLED urlopen(%r)" % path
 


Follow ups