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
1=== modified file 'lib/lp/bugs/doc/bugwatch.txt'
2--- lib/lp/bugs/doc/bugwatch.txt 2013-04-11 04:50:27 +0000
3+++ lib/lp/bugs/doc/bugwatch.txt 2018-06-23 00:01:05 +0000
4@@ -465,7 +465,9 @@
5 >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
6 >>> bug_watch_updater = CheckwatchesMaster(transaction, FakeLogger())
7 >>> external_bugtracker = TestRoundup(bug_tracker.baseurl)
8- >>> bug_watch_updater.updateBugWatches(external_bugtracker, [bug_watch])
9+ >>> with external_bugtracker.responses():
10+ ... bug_watch_updater.updateBugWatches(
11+ ... external_bugtracker, [bug_watch])
12 INFO Updating 1 watches for 1 bugs on http://some.where
13
14 >>> bug.bugtasks[0].status.title
15
16=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
17--- lib/lp/bugs/doc/checkwatches.txt 2016-12-22 16:32:38 +0000
18+++ lib/lp/bugs/doc/checkwatches.txt 2018-06-23 00:01:05 +0000
19@@ -15,8 +15,8 @@
20 >>> transaction.commit()
21
22 We set a default timeout on checkwatches to 30 seconds. In order to test
23-this, we can monkey-patch urllib2.urlopen so that it always raises a
24-timeout and call the checkwatches cronscript machinery directly.
25+this, we can inject a mock timeout using `responses` and call the
26+checkwatches cronscript machinery directly.
27
28 First, we create some bug watches to test with:
29
30@@ -51,30 +51,31 @@
31
32 >>> login('no-priv@canonical.com')
33
34-Next, we monkey-patch urllib2.urlopen so that it always times out.
35+Next, we ensure that the request always times out.
36
37 The timeout will not produce an OOPS report; they happen routinely and
38 require no action from the Launchpad end.
39
40+ >>> from contextlib import contextmanager
41+ >>> import re
42 >>> import socket
43- >>> import urllib2
44- >>> urlopen = urllib2.urlopen
45-
46+ >>> import responses
47 >>> from lp.testing.fixture import CaptureOops
48- >>> capture = CaptureOops()
49- >>> capture.setUp()
50- >>> def do_not_urlopen(url=None, data=None, timeout=None):
51- ... raise socket.timeout("Connection timed out.")
52- >>> try:
53- ... urllib2.urlopen = do_not_urlopen
54+
55+ >>> @contextmanager
56+ ... def timeout_requests():
57+ ... with responses.RequestsMock() as requests_mock:
58+ ... requests_mock.add(
59+ ... 'GET', re.compile(r'.*'),
60+ ... body=socket.timeout('Connection timed out.'))
61+ ... yield
62+
63+ >>> with CaptureOops() as capture, timeout_requests():
64 ... updater = CheckwatchesMaster(transaction.manager)
65 ... updater.updateBugTrackers(
66 ... bug_tracker_names=[example_bug_tracker_name])
67- ... finally:
68- ... urllib2.urlopen = urlopen
69- >>> capture.oopses
70+ ... print(capture.oopses)
71 []
72- >>> capture.cleanUp()
73
74 Errors that occur when updating a bug watch are recorded against that
75 bug watch. The timeout will be recorded against the bug watch we just
76@@ -146,8 +147,8 @@
77
78 Since our example bug tracker is a Roundup bug tracker we can
79 monkey-patch the Roundup ExternalBugTrackerClass in order to set its
80-batch size. We will also monkey-patch urllib2.urlopen again so that no
81-requests are actually made.
82+batch size. We will also insert a mock response again so that no requests
83+are actually made.
84
85 >>> from lp.services.log.logger import FakeLogger
86 >>> from lp.bugs import externalbugtracker
87@@ -156,16 +157,15 @@
88 >>> updater = CheckwatchesMaster(transaction.manager)
89 >>> original_log = updater.logger
90 >>> batch_size = externalbugtracker.Roundup.batch_size
91- >>> try:
92- ... urllib2.urlopen = do_not_urlopen
93- ... externalbugtracker.Roundup.batch_size = 5
94- ... transaction.commit()
95- ... updater.logger = FakeLogger()
96- ... updater.updateBugTrackers([example_bug_tracker_name])
97- ... finally:
98- ... updater.logger = original_log
99- ... externalbugtracker.Roundup.batch_size = batch_size
100- ... urllib2.urlopen = urlopen
101+ >>> with timeout_requests():
102+ ... try:
103+ ... externalbugtracker.Roundup.batch_size = 5
104+ ... transaction.commit()
105+ ... updater.logger = FakeLogger()
106+ ... updater.updateBugTrackers([example_bug_tracker_name])
107+ ... finally:
108+ ... updater.logger = original_log
109+ ... externalbugtracker.Roundup.batch_size = batch_size
110 DEBUG No global batch size specified.
111 INFO Updating 5 watches for 5 bugs on http://bugs.example.com
112 INFO Connection timed out when updating ...
113
114=== modified file 'lib/lp/bugs/doc/externalbugtracker-roundup.txt'
115--- lib/lp/bugs/doc/externalbugtracker-roundup.txt 2012-12-26 01:32:19 +0000
116+++ lib/lp/bugs/doc/externalbugtracker-roundup.txt 2018-06-23 00:01:05 +0000
117@@ -75,13 +75,13 @@
118 local variable for later use.
119
120 We use a test-oriented implementation for the purposes of these tests, which
121-overrides ExternalBugTracker.urlopen() so that we don't have to rely on a
122-working network connection.
123+avoids relying on a working network connection.
124
125 >>> from lp.bugs.tests.externalbugtracker import (
126 ... TestRoundup, print_bugwatches)
127 >>> roundup = TestRoundup(u'http://test.roundup/')
128- >>> roundup.initializeRemoteBugDB([1])
129+ >>> with roundup.responses():
130+ ... roundup.initializeRemoteBugDB([1])
131 >>> sorted(roundup.bugs.keys())
132 [1]
133
134@@ -90,26 +90,27 @@
135
136 There are two means by which we can export Roundup bug statuses: on a
137 bug-by-bug basis and as a batch. When the number of bugs that need updating is
138-less than a given bug roundupker's batch_query_threshold the bugs will be
139+less than a given bug tracker's batch_query_threshold the bugs will be
140 fetched one-at-a-time:
141
142 >>> roundup.batch_query_threshold
143 10
144
145- >>> roundup.trace_calls = True
146- >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
147- CALLED urlopen(u'http://test.roundup/issue?...&id=6')
148- CALLED urlopen(u'http://test.roundup/issue?...&id=7')
149- CALLED urlopen(u'http://test.roundup/issue?...&id=8')
150- CALLED urlopen(u'http://test.roundup/issue?...&id=9')
151- CALLED urlopen(u'http://test.roundup/issue?...&id=10')
152+ >>> with roundup.responses(trace_calls=True):
153+ ... roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
154+ GET http://test.roundup/issue?...&id=6
155+ GET http://test.roundup/issue?...&id=7
156+ GET http://test.roundup/issue?...&id=8
157+ GET http://test.roundup/issue?...&id=9
158+ GET http://test.roundup/issue?...&id=10
159
160 If there are more than batch_query_threshold bugs to update then they are
161 fetched as a batch:
162
163 >>> roundup.batch_query_threshold = 4
164- >>> roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
165- CALLED urlopen(u'http://test.roundup/issue?...@startwith=0')
166+ >>> with roundup.responses(trace_calls=True):
167+ ... roundup.initializeRemoteBugDB([6, 7, 8, 9, 10])
168+ GET http://test.roundup/issue?...@startwith=0
169
170
171 == Updating Bug Watches ==
172@@ -145,8 +146,9 @@
173 >>> bug_watch_updater = CheckwatchesMaster(
174 ... txn, logger=FakeLogger())
175 >>> roundup = TestRoundup(example_bug_tracker.baseurl)
176- >>> bug_watch_updater.updateBugWatches(
177- ... roundup, example_bug_tracker.watches)
178+ >>> with roundup.responses():
179+ ... bug_watch_updater.updateBugWatches(
180+ ... roundup, example_bug_tracker.watches)
181 INFO Updating 1 watches for 1 bugs on http://bugs.some.where
182 >>> print_bugwatches(example_bug_tracker.watches)
183 Remote bug 1: 1
184@@ -178,11 +180,11 @@
185 ... bugtracker=example_bug_tracker,
186 ... remotebug=str(remote_bug_id))
187
188- >>> roundup.trace_calls = True
189- >>> bug_watch_updater.updateBugWatches(
190- ... roundup, example_bug_tracker.watches)
191+ >>> with roundup.responses(trace_calls=True):
192+ ... bug_watch_updater.updateBugWatches(
193+ ... roundup, example_bug_tracker.watches)
194 INFO Updating 11 watches for 11 bugs on http://bugs.some.where
195- CALLED urlopen(u'http://.../issue?...@startwith=0')
196+ GET http://.../issue?...@startwith=0
197
198 >>> print_bugwatches(example_bug_tracker.watches,
199 ... roundup.convertRemoteStatus)
200
201=== modified file 'lib/lp/bugs/externalbugtracker/roundup.py'
202--- lib/lp/bugs/externalbugtracker/roundup.py 2015-10-15 14:09:50 +0000
203+++ lib/lp/bugs/externalbugtracker/roundup.py 2018-06-23 00:01:05 +0000
204@@ -1,4 +1,4 @@
205-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
206+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
207 # GNU Affero General Public License version 3 (see the file LICENSE).
208
209 """Round ExternalBugTracker utility."""
210@@ -13,7 +13,7 @@
211
212 from lp.bugs.externalbugtracker import (
213 BugNotFound,
214- ExternalBugTracker,
215+ ExternalBugTrackerRequests,
216 InvalidBugId,
217 LookupTree,
218 UnknownRemoteStatusError,
219@@ -42,7 +42,7 @@
220 for (key, value) in items)
221
222
223-class Roundup(ExternalBugTracker):
224+class Roundup(ExternalBugTrackerRequests):
225 """An ExternalBugTracker descendant for handling Roundup bug trackers."""
226
227 _status_fields_map = {
228@@ -218,7 +218,7 @@
229 """See `ExternalBugTracker`."""
230 bug_id = int(bug_id)
231 query_url = self.getSingleBugExportURL(bug_id)
232- reader = csv.DictReader(self._fetchPage(query_url))
233+ reader = csv.DictReader(self._getPage(query_url).iter_lines())
234 return (bug_id, reader.next())
235
236 def getRemoteBugBatch(self, bug_ids):
237@@ -230,7 +230,7 @@
238 # export the bug ids needed rather than hitting the remote
239 # tracker for a potentially massive number of bugs.
240 query_url = self.getBatchBugExportURL()
241- remote_bugs = csv.DictReader(self._fetchPage(query_url))
242+ remote_bugs = csv.DictReader(self._getPage(query_url).iter_lines())
243 bugs = {}
244 for remote_bug in remote_bugs:
245 # We're only interested in the bug if it's one of the ones in
246
247=== modified file 'lib/lp/bugs/tests/externalbugtracker.py'
248--- lib/lp/bugs/tests/externalbugtracker.py 2018-06-05 12:18:58 +0000
249+++ lib/lp/bugs/tests/externalbugtracker.py 2018-06-23 00:01:05 +0000
250@@ -5,6 +5,7 @@
251
252 __metaclass__ = type
253
254+from contextlib import contextmanager
255 from copy import deepcopy
256 from datetime import (
257 datetime,
258@@ -20,9 +21,15 @@
259 BaseHandler,
260 Request,
261 )
262-import urlparse
263 import xmlrpclib
264
265+import responses
266+from six.moves.urllib_parse import (
267+ parse_qs,
268+ urljoin,
269+ urlparse,
270+ urlsplit,
271+ )
272 from zope.component import getUtility
273 from zope.security.proxy import removeSecurityProxy
274
275@@ -158,6 +165,30 @@
276 naked.updateStatus(UNKNOWN_REMOTE_STATUS, BugTaskStatus.UNKNOWN)
277
278
279+class BugTrackerResponsesMixin:
280+ """A responses-based test mixin for bug trackers.
281+
282+ The simplest way to use this is as a context manager:
283+
284+ with bug_tracker.responses():
285+ ...
286+
287+ Classes using this mixin should implement `addResponses`.
288+ """
289+
290+ def addResponses(self, requests_mock, **kwargs):
291+ """Add test responses."""
292+
293+ @contextmanager
294+ def responses(self, trace_calls=False, **kwargs):
295+ with responses.RequestsMock() as requests_mock:
296+ self.addResponses(requests_mock, **kwargs)
297+ yield requests_mock
298+ if trace_calls:
299+ for call in requests_mock.calls:
300+ print call.request.method, call.request.url
301+
302+
303 class TestExternalBugTracker(ExternalBugTracker):
304 """A test version of `ExternalBugTracker`.
305
306@@ -1522,30 +1553,26 @@
307 return [self.utc_time]
308
309
310-class TestRoundup(Roundup):
311- """Roundup ExternalBugTracker for testing purposes.
312-
313- It overrides urlopen, so that access to a real Roundup instance isn't
314- needed.
315- """
316+class TestRoundup(BugTrackerResponsesMixin, Roundup):
317+ """Roundup ExternalBugTracker for testing purposes."""
318
319 # We remove the batch_size limit for the purposes of the tests so
320 # that we can test batching and not batching correctly.
321 batch_size = None
322- trace_calls = False
323-
324- def urlopen(self, url, data=None):
325- if self.trace_calls:
326- print "CALLED urlopen(%r)" % (url.get_full_url())
327-
328- file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
329-
330- if self.host == 'bugs.python.org':
331- return open(
332- file_path + '/' + 'python_example_ticket_export.csv', 'r')
333+
334+ @staticmethod
335+ def _getCallback(request):
336+ url = urlsplit(request.url)
337+ if url.netloc == 'bugs.python.org':
338+ body = read_test_file('python_example_ticket_export.csv')
339 else:
340- return open(
341- file_path + '/' + 'roundup_example_ticket_export.csv', 'r')
342+ body = read_test_file('roundup_example_ticket_export.csv')
343+ return 200, {}, body
344+
345+ def addResponses(self, requests_mock):
346+ """Add test responses."""
347+ requests_mock.add_callback(
348+ 'GET', re.compile(re.escape(self.baseurl)), self._getCallback)
349
350
351 class TestRequestTracker(RequestTracker):
352@@ -1559,7 +1586,7 @@
353
354 def urlopen(self, page, data=None):
355 file_path = os.path.join(os.path.dirname(__file__), 'testfiles')
356- path = urlparse.urlparse(page)[2].lstrip('/')
357+ path = urlparse(page)[2].lstrip('/')
358 if self.trace_calls:
359 print "CALLED urlopen(%r)" % path
360