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

Proposed by Colin Watson
Status: Merged
Merged at revision: 18695
Proposed branch: lp:~cjwatson/launchpad/explicit-proxy-roundup
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/explicit-proxy-github
Diff against target: 359 lines (+105/-74)
5 files modified
lib/lp/bugs/doc/bugwatch.txt (+3/-1)
lib/lp/bugs/doc/checkwatches.txt (+28/-28)
lib/lp/bugs/doc/externalbugtracker-roundup.txt (+21/-19)
lib/lp/bugs/externalbugtracker/roundup.py (+5/-5)
lib/lp/bugs/tests/externalbugtracker.py (+48/-21)
To merge this branch: bzr merge lp:~cjwatson/launchpad/explicit-proxy-roundup
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+348428@code.launchpad.net

Commit message

Convert the Roundup external bug tracker to urlfetch.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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 @@
465 >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster465 >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
466 >>> bug_watch_updater = CheckwatchesMaster(transaction, FakeLogger())466 >>> bug_watch_updater = CheckwatchesMaster(transaction, FakeLogger())
467 >>> external_bugtracker = TestRoundup(bug_tracker.baseurl)467 >>> external_bugtracker = TestRoundup(bug_tracker.baseurl)
468 >>> bug_watch_updater.updateBugWatches(external_bugtracker, [bug_watch])468 >>> with external_bugtracker.responses():
469 ... bug_watch_updater.updateBugWatches(
470 ... external_bugtracker, [bug_watch])
469 INFO Updating 1 watches for 1 bugs on http://some.where471 INFO Updating 1 watches for 1 bugs on http://some.where
470472
471 >>> bug.bugtasks[0].status.title473 >>> bug.bugtasks[0].status.title
472474
=== 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 @@
15 >>> transaction.commit()15 >>> transaction.commit()
1616
17We set a default timeout on checkwatches to 30 seconds. In order to test17We set a default timeout on checkwatches to 30 seconds. In order to test
18this, we can monkey-patch urllib2.urlopen so that it always raises a18this, we can inject a mock timeout using `responses` and call the
19timeout and call the checkwatches cronscript machinery directly.19checkwatches cronscript machinery directly.
2020
21First, we create some bug watches to test with:21First, we create some bug watches to test with:
2222
@@ -51,30 +51,31 @@
5151
52 >>> login('no-priv@canonical.com')52 >>> login('no-priv@canonical.com')
5353
54Next, we monkey-patch urllib2.urlopen so that it always times out.54Next, we ensure that the request always times out.
5555
56The timeout will not produce an OOPS report; they happen routinely and56The timeout will not produce an OOPS report; they happen routinely and
57require no action from the Launchpad end.57require no action from the Launchpad end.
5858
59 >>> from contextlib import contextmanager
60 >>> import re
59 >>> import socket61 >>> import socket
60 >>> import urllib262 >>> import responses
61 >>> urlopen = urllib2.urlopen
62
63 >>> from lp.testing.fixture import CaptureOops63 >>> from lp.testing.fixture import CaptureOops
64 >>> capture = CaptureOops()64
65 >>> capture.setUp()65 >>> @contextmanager
66 >>> def do_not_urlopen(url=None, data=None, timeout=None):66 ... def timeout_requests():
67 ... raise socket.timeout("Connection timed out.")67 ... with responses.RequestsMock() as requests_mock:
68 >>> try:68 ... requests_mock.add(
69 ... urllib2.urlopen = do_not_urlopen69 ... 'GET', re.compile(r'.*'),
70 ... body=socket.timeout('Connection timed out.'))
71 ... yield
72
73 >>> with CaptureOops() as capture, timeout_requests():
70 ... updater = CheckwatchesMaster(transaction.manager)74 ... updater = CheckwatchesMaster(transaction.manager)
71 ... updater.updateBugTrackers(75 ... updater.updateBugTrackers(
72 ... bug_tracker_names=[example_bug_tracker_name])76 ... bug_tracker_names=[example_bug_tracker_name])
73 ... finally:77 ... print(capture.oopses)
74 ... urllib2.urlopen = urlopen
75 >>> capture.oopses
76 []78 []
77 >>> capture.cleanUp()
7879
79Errors that occur when updating a bug watch are recorded against that80Errors that occur when updating a bug watch are recorded against that
80bug watch. The timeout will be recorded against the bug watch we just81bug watch. The timeout will be recorded against the bug watch we just
@@ -146,8 +147,8 @@
146147
147Since our example bug tracker is a Roundup bug tracker we can148Since our example bug tracker is a Roundup bug tracker we can
148monkey-patch the Roundup ExternalBugTrackerClass in order to set its149monkey-patch the Roundup ExternalBugTrackerClass in order to set its
149batch size. We will also monkey-patch urllib2.urlopen again so that no150batch size. We will also insert a mock response again so that no requests
150requests are actually made.151are actually made.
151152
152 >>> from lp.services.log.logger import FakeLogger153 >>> from lp.services.log.logger import FakeLogger
153 >>> from lp.bugs import externalbugtracker154 >>> from lp.bugs import externalbugtracker
@@ -156,16 +157,15 @@
156 >>> updater = CheckwatchesMaster(transaction.manager)157 >>> updater = CheckwatchesMaster(transaction.manager)
157 >>> original_log = updater.logger158 >>> original_log = updater.logger
158 >>> batch_size = externalbugtracker.Roundup.batch_size159 >>> batch_size = externalbugtracker.Roundup.batch_size
159 >>> try:160 >>> with timeout_requests():
160 ... urllib2.urlopen = do_not_urlopen161 ... try:
161 ... externalbugtracker.Roundup.batch_size = 5162 ... externalbugtracker.Roundup.batch_size = 5
162 ... transaction.commit()163 ... transaction.commit()
163 ... updater.logger = FakeLogger()164 ... updater.logger = FakeLogger()
164 ... updater.updateBugTrackers([example_bug_tracker_name])165 ... updater.updateBugTrackers([example_bug_tracker_name])
165 ... finally:166 ... finally:
166 ... updater.logger = original_log167 ... updater.logger = original_log
167 ... externalbugtracker.Roundup.batch_size = batch_size168 ... externalbugtracker.Roundup.batch_size = batch_size
168 ... urllib2.urlopen = urlopen
169 DEBUG No global batch size specified.169 DEBUG No global batch size specified.
170 INFO Updating 5 watches for 5 bugs on http://bugs.example.com170 INFO Updating 5 watches for 5 bugs on http://bugs.example.com
171 INFO Connection timed out when updating ...171 INFO Connection timed out when updating ...
172172
=== 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 @@
75local variable for later use.75local variable for later use.
7676
77We use a test-oriented implementation for the purposes of these tests, which77We use a test-oriented implementation for the purposes of these tests, which
78overrides ExternalBugTracker.urlopen() so that we don't have to rely on a78avoids relying on a working network connection.
79working network connection.
8079
81 >>> from lp.bugs.tests.externalbugtracker import (80 >>> from lp.bugs.tests.externalbugtracker import (
82 ... TestRoundup, print_bugwatches)81 ... TestRoundup, print_bugwatches)
83 >>> roundup = TestRoundup(u'http://test.roundup/')82 >>> roundup = TestRoundup(u'http://test.roundup/')
84 >>> roundup.initializeRemoteBugDB([1])83 >>> with roundup.responses():
84 ... roundup.initializeRemoteBugDB([1])
85 >>> sorted(roundup.bugs.keys())85 >>> sorted(roundup.bugs.keys())
86 [1]86 [1]
8787
@@ -90,26 +90,27 @@
9090
91There are two means by which we can export Roundup bug statuses: on a91There are two means by which we can export Roundup bug statuses: on a
92bug-by-bug basis and as a batch. When the number of bugs that need updating is92bug-by-bug basis and as a batch. When the number of bugs that need updating is
93less than a given bug roundupker's batch_query_threshold the bugs will be93less than a given bug tracker's batch_query_threshold the bugs will be
94fetched one-at-a-time:94fetched one-at-a-time:
9595
96 >>> roundup.batch_query_threshold96 >>> roundup.batch_query_threshold
97 1097 10
9898
99 >>> roundup.trace_calls = True99 >>> with roundup.responses(trace_calls=True):
100 >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])100 ... roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
101 CALLED urlopen(u'http://test.roundup/issue?...&id=6')101 GET http://test.roundup/issue?...&id=6
102 CALLED urlopen(u'http://test.roundup/issue?...&id=7')102 GET http://test.roundup/issue?...&id=7
103 CALLED urlopen(u'http://test.roundup/issue?...&id=8')103 GET http://test.roundup/issue?...&id=8
104 CALLED urlopen(u'http://test.roundup/issue?...&id=9')104 GET http://test.roundup/issue?...&id=9
105 CALLED urlopen(u'http://test.roundup/issue?...&id=10')105 GET http://test.roundup/issue?...&id=10
106106
107If there are more than batch_query_threshold bugs to update then they are107If there are more than batch_query_threshold bugs to update then they are
108fetched as a batch:108fetched as a batch:
109109
110 >>> roundup.batch_query_threshold = 4110 >>> roundup.batch_query_threshold = 4
111 >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])111 >>> with roundup.responses(trace_calls=True):
112 CALLED urlopen(u'http://test.roundup/issue?...@startwith=0')112 ... roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
113 GET http://test.roundup/issue?...@startwith=0
113114
114115
115== Updating Bug Watches ==116== Updating Bug Watches ==
@@ -145,8 +146,9 @@
145 >>> bug_watch_updater = CheckwatchesMaster(146 >>> bug_watch_updater = CheckwatchesMaster(
146 ... txn, logger=FakeLogger())147 ... txn, logger=FakeLogger())
147 >>> roundup = TestRoundup(example_bug_tracker.baseurl)148 >>> roundup = TestRoundup(example_bug_tracker.baseurl)
148 >>> bug_watch_updater.updateBugWatches(149 >>> with roundup.responses():
149 ... roundup, example_bug_tracker.watches)150 ... bug_watch_updater.updateBugWatches(
151 ... roundup, example_bug_tracker.watches)
150 INFO Updating 1 watches for 1 bugs on http://bugs.some.where152 INFO Updating 1 watches for 1 bugs on http://bugs.some.where
151 >>> print_bugwatches(example_bug_tracker.watches)153 >>> print_bugwatches(example_bug_tracker.watches)
152 Remote bug 1: 1154 Remote bug 1: 1
@@ -178,11 +180,11 @@
178 ... bugtracker=example_bug_tracker,180 ... bugtracker=example_bug_tracker,
179 ... remotebug=str(remote_bug_id))181 ... remotebug=str(remote_bug_id))
180182
181 >>> roundup.trace_calls = True183 >>> with roundup.responses(trace_calls=True):
182 >>> bug_watch_updater.updateBugWatches(184 ... bug_watch_updater.updateBugWatches(
183 ... roundup, example_bug_tracker.watches)185 ... roundup, example_bug_tracker.watches)
184 INFO Updating 11 watches for 11 bugs on http://bugs.some.where186 INFO Updating 11 watches for 11 bugs on http://bugs.some.where
185 CALLED urlopen(u'http://.../issue?...@startwith=0')187 GET http://.../issue?...@startwith=0
186188
187 >>> print_bugwatches(example_bug_tracker.watches,189 >>> print_bugwatches(example_bug_tracker.watches,
188 ... roundup.convertRemoteStatus)190 ... roundup.convertRemoteStatus)
189191
=== 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 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Round ExternalBugTracker utility."""4"""Round ExternalBugTracker utility."""
@@ -13,7 +13,7 @@
1313
14from lp.bugs.externalbugtracker import (14from lp.bugs.externalbugtracker import (
15 BugNotFound,15 BugNotFound,
16 ExternalBugTracker,16 ExternalBugTrackerRequests,
17 InvalidBugId,17 InvalidBugId,
18 LookupTree,18 LookupTree,
19 UnknownRemoteStatusError,19 UnknownRemoteStatusError,
@@ -42,7 +42,7 @@
42 for (key, value) in items)42 for (key, value) in items)
4343
4444
45class Roundup(ExternalBugTracker):45class Roundup(ExternalBugTrackerRequests):
46 """An ExternalBugTracker descendant for handling Roundup bug trackers."""46 """An ExternalBugTracker descendant for handling Roundup bug trackers."""
4747
48 _status_fields_map = {48 _status_fields_map = {
@@ -218,7 +218,7 @@
218 """See `ExternalBugTracker`."""218 """See `ExternalBugTracker`."""
219 bug_id = int(bug_id)219 bug_id = int(bug_id)
220 query_url = self.getSingleBugExportURL(bug_id)220 query_url = self.getSingleBugExportURL(bug_id)
221 reader = csv.DictReader(self._fetchPage(query_url))221 reader = csv.DictReader(self._getPage(query_url).iter_lines())
222 return (bug_id, reader.next())222 return (bug_id, reader.next())
223223
224 def getRemoteBugBatch(self, bug_ids):224 def getRemoteBugBatch(self, bug_ids):
@@ -230,7 +230,7 @@
230 # export the bug ids needed rather than hitting the remote230 # export the bug ids needed rather than hitting the remote
231 # tracker for a potentially massive number of bugs.231 # tracker for a potentially massive number of bugs.
232 query_url = self.getBatchBugExportURL()232 query_url = self.getBatchBugExportURL()
233 remote_bugs = csv.DictReader(self._fetchPage(query_url))233 remote_bugs = csv.DictReader(self._getPage(query_url).iter_lines())
234 bugs = {}234 bugs = {}
235 for remote_bug in remote_bugs:235 for remote_bug in remote_bugs:
236 # We're only interested in the bug if it's one of the ones in236 # We're only interested in the bug if it's one of the ones in
237237
=== 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 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from contextlib import contextmanager
8from copy import deepcopy9from copy import deepcopy
9from datetime import (10from datetime import (
10 datetime,11 datetime,
@@ -20,9 +21,15 @@
20 BaseHandler,21 BaseHandler,
21 Request,22 Request,
22 )23 )
23import urlparse
24import xmlrpclib24import xmlrpclib
2525
26import responses
27from six.moves.urllib_parse import (
28 parse_qs,
29 urljoin,
30 urlparse,
31 urlsplit,
32 )
26from zope.component import getUtility33from zope.component import getUtility
27from zope.security.proxy import removeSecurityProxy34from zope.security.proxy import removeSecurityProxy
2835
@@ -158,6 +165,30 @@
158 naked.updateStatus(UNKNOWN_REMOTE_STATUS, BugTaskStatus.UNKNOWN)165 naked.updateStatus(UNKNOWN_REMOTE_STATUS, BugTaskStatus.UNKNOWN)
159166
160167
168class BugTrackerResponsesMixin:
169 """A responses-based test mixin for bug trackers.
170
171 The simplest way to use this is as a context manager:
172
173 with bug_tracker.responses():
174 ...
175
176 Classes using this mixin should implement `addResponses`.
177 """
178
179 def addResponses(self, requests_mock, **kwargs):
180 """Add test responses."""
181
182 @contextmanager
183 def responses(self, trace_calls=False, **kwargs):
184 with responses.RequestsMock() as requests_mock:
185 self.addResponses(requests_mock, **kwargs)
186 yield requests_mock
187 if trace_calls:
188 for call in requests_mock.calls:
189 print call.request.method, call.request.url
190
191
161class TestExternalBugTracker(ExternalBugTracker):192class TestExternalBugTracker(ExternalBugTracker):
162 """A test version of `ExternalBugTracker`.193 """A test version of `ExternalBugTracker`.
163194
@@ -1522,30 +1553,26 @@
1522 return [self.utc_time]1553 return [self.utc_time]
15231554
15241555
1525class TestRoundup(Roundup):1556class TestRoundup(BugTrackerResponsesMixin, Roundup):
1526 """Roundup ExternalBugTracker for testing purposes.1557 """Roundup ExternalBugTracker for testing purposes."""
1527
1528 It overrides urlopen, so that access to a real Roundup instance isn't
1529 needed.
1530 """
15311558
1532 # We remove the batch_size limit for the purposes of the tests so1559 # We remove the batch_size limit for the purposes of the tests so
1533 # that we can test batching and not batching correctly.1560 # that we can test batching and not batching correctly.
1534 batch_size = None1561 batch_size = None
1535 trace_calls = False1562
15361563 @staticmethod
1537 def urlopen(self, url, data=None):1564 def _getCallback(request):
1538 if self.trace_calls:1565 url = urlsplit(request.url)
1539 print "CALLED urlopen(%r)" % (url.get_full_url())1566 if url.netloc == 'bugs.python.org':
15401567 body = read_test_file('python_example_ticket_export.csv')
1541 file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
1542
1543 if self.host == 'bugs.python.org':
1544 return open(
1545 file_path + '/' + 'python_example_ticket_export.csv', 'r')
1546 else:1568 else:
1547 return open(1569 body = read_test_file('roundup_example_ticket_export.csv')
1548 file_path + '/' + 'roundup_example_ticket_export.csv', 'r')1570 return 200, {}, body
1571
1572 def addResponses(self, requests_mock):
1573 """Add test responses."""
1574 requests_mock.add_callback(
1575 'GET', re.compile(re.escape(self.baseurl)), self._getCallback)
15491576
15501577
1551class TestRequestTracker(RequestTracker):1578class TestRequestTracker(RequestTracker):
@@ -1559,7 +1586,7 @@
15591586
1560 def urlopen(self, page, data=None):1587 def urlopen(self, page, data=None):
1561 file_path = os.path.join(os.path.dirname(__file__), 'testfiles')1588 file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
1562 path = urlparse.urlparse(page)[2].lstrip('/')1589 path = urlparse(page)[2].lstrip('/')
1563 if self.trace_calls:1590 if self.trace_calls:
1564 print "CALLED urlopen(%r)" % path1591 print "CALLED urlopen(%r)" % path
15651592