Merge lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 into lp:launchpad
- fix-mantis-warnings-bug-218384
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Robert Collins | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | 12508 | ||||||||
Proposed branch: | lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
464 lines (+221/-81) 7 files modified
lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt (+2/-2) lib/lp/bugs/doc/externalbugtracker-trac.txt (+4/-4) lib/lp/bugs/externalbugtracker/base.py (+3/-3) lib/lp/bugs/externalbugtracker/mantis.py (+72/-68) lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py (+28/-2) lib/lp/bugs/externalbugtracker/tests/test_mantis.py (+110/-0) lib/lp/bugs/tests/externalbugtracker.py (+2/-2) |
||||||||
To merge this branch: | bzr merge lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins (community) | Approve | ||
William Grant | code* | Approve | |
Review via email: mp+51839@code.launchpad.net |
This proposal supersedes a proposal from 2011-03-01.
Commit message
[r=lifeless,
Description of the change
This branch fixes two old Mantis issues.
HTTPError when trying to determine if the remote site can
handle batches, and the oops handled when trying to use
a non-existant warning method.
The HTTPError is handled by wrapping the urlopen method with
the _fetchPage method, which translates the urllib2 errors to
a BugTrackerConne
Fixing the warning usage was trivial, but I refactored the
code somewhat to make testing easier. A class was created
that is responsible for parsing the CVS data.
Also an unused method (_getStatusFromCSV) was deleted.
William Grant (wgrant) : Posted in a previous version of this proposal | # |
Robert Collins (lifeless) : Posted in a previous version of this proposal | # |
Robert Collins (lifeless) wrote : | # |
Sure, lets try this again.
Preview Diff
1 | === modified file 'lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt' | |||
2 | --- lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt 2011-02-07 20:41:06 +0000 | |||
3 | +++ lib/lp/bugs/doc/externalbugtracker-trac-lp-plugin.txt 2011-03-02 00:14:04 +0000 | |||
4 | @@ -40,7 +40,7 @@ | |||
5 | 40 | ... self.headers = {} | 40 | ... self.headers = {} |
6 | 41 | 41 | ||
7 | 42 | >>> class TestTracLPPlugin(TracLPPlugin): | 42 | >>> class TestTracLPPlugin(TracLPPlugin): |
9 | 43 | ... def urlopen(self, url): | 43 | ... def urlopen(self, url, data=None): |
10 | 44 | ... with lp_dbuser(): | 44 | ... with lp_dbuser(): |
11 | 45 | ... base_auth_url = urlappend(self.baseurl, 'launchpad-auth') | 45 | ... base_auth_url = urlappend(self.baseurl, 'launchpad-auth') |
12 | 46 | ... if not url.startswith(base_auth_url + '/'): | 46 | ... if not url.startswith(base_auth_url + '/'): |
13 | @@ -133,7 +133,7 @@ | |||
14 | 133 | 133 | ||
15 | 134 | >>> from urllib2 import HTTPError | 134 | >>> from urllib2 import HTTPError |
16 | 135 | >>> class TestFailingTracLPPlugin(TracLPPlugin): | 135 | >>> class TestFailingTracLPPlugin(TracLPPlugin): |
18 | 136 | ... def urlopen(self, url): | 136 | ... def urlopen(self, url, data=None): |
19 | 137 | ... raise HTTPError(url, 401, "Denied!", {}, None) | 137 | ... raise HTTPError(url, 401, "Denied!", {}, None) |
20 | 138 | 138 | ||
21 | 139 | >>> test_trac = TestFailingTracLPPlugin( | 139 | >>> test_trac = TestFailingTracLPPlugin( |
22 | 140 | 140 | ||
23 | === modified file 'lib/lp/bugs/doc/externalbugtracker-trac.txt' | |||
24 | --- lib/lp/bugs/doc/externalbugtracker-trac.txt 2011-01-19 00:10:48 +0000 | |||
25 | +++ lib/lp/bugs/doc/externalbugtracker-trac.txt 2011-03-02 00:14:04 +0000 | |||
26 | @@ -33,7 +33,7 @@ | |||
27 | 33 | 33 | ||
28 | 34 | >>> import urllib2 | 34 | >>> import urllib2 |
29 | 35 | >>> class TracHavingLPPlugin401(Trac): | 35 | >>> class TracHavingLPPlugin401(Trac): |
31 | 36 | ... def urlopen(self, url): | 36 | ... def urlopen(self, url, data=None): |
32 | 37 | ... print url | 37 | ... print url |
33 | 38 | ... raise urllib2.HTTPError( | 38 | ... raise urllib2.HTTPError( |
34 | 39 | ... url, 401, "Unauthorized", None, None) | 39 | ... url, 401, "Unauthorized", None, None) |
35 | @@ -60,13 +60,13 @@ | |||
36 | 60 | >>> from urllib import addinfourl | 60 | >>> from urllib import addinfourl |
37 | 61 | 61 | ||
38 | 62 | >>> class TracReturning200ForPageNotFound(Trac): | 62 | >>> class TracReturning200ForPageNotFound(Trac): |
40 | 63 | ... def urlopen(self, url): | 63 | ... def urlopen(self, url, data=None): |
41 | 64 | ... print url | 64 | ... print url |
42 | 65 | ... return addinfourl( | 65 | ... return addinfourl( |
43 | 66 | ... StringIO(''), HTTPMessage(StringIO('')), url) | 66 | ... StringIO(''), HTTPMessage(StringIO('')), url) |
44 | 67 | 67 | ||
45 | 68 | >>> class TracHavingLPPlugin200(Trac): | 68 | >>> class TracHavingLPPlugin200(Trac): |
47 | 69 | ... def urlopen(self, url): | 69 | ... def urlopen(self, url, data=None): |
48 | 70 | ... print url | 70 | ... print url |
49 | 71 | ... return addinfourl( | 71 | ... return addinfourl( |
50 | 72 | ... StringIO(''), HTTPMessage( | 72 | ... StringIO(''), HTTPMessage( |
51 | @@ -99,7 +99,7 @@ | |||
52 | 99 | If a 404 is returned, the normal Trac instance is returned. | 99 | If a 404 is returned, the normal Trac instance is returned. |
53 | 100 | 100 | ||
54 | 101 | >>> class TracNotHavingLPPlugin(Trac): | 101 | >>> class TracNotHavingLPPlugin(Trac): |
56 | 102 | ... def urlopen(self, url): | 102 | ... def urlopen(self, url, data=None): |
57 | 103 | ... print url | 103 | ... print url |
58 | 104 | ... raise urllib2.HTTPError( | 104 | ... raise urllib2.HTTPError( |
59 | 105 | ... url, 404, "Not found", None, None) | 105 | ... url, 404, "Not found", None, None) |
60 | 106 | 106 | ||
61 | === modified file 'lib/lp/bugs/externalbugtracker/base.py' | |||
62 | --- lib/lp/bugs/externalbugtracker/base.py 2011-03-01 11:49:59 +0000 | |||
63 | +++ lib/lp/bugs/externalbugtracker/base.py 2011-03-02 00:14:04 +0000 | |||
64 | @@ -238,13 +238,13 @@ | |||
65 | 238 | """ | 238 | """ |
66 | 239 | return None | 239 | return None |
67 | 240 | 240 | ||
69 | 241 | def _fetchPage(self, page): | 241 | def _fetchPage(self, page, data=None): |
70 | 242 | """Fetch a page from the remote server. | 242 | """Fetch a page from the remote server. |
71 | 243 | 243 | ||
72 | 244 | A BugTrackerConnectError will be raised if anything goes wrong. | 244 | A BugTrackerConnectError will be raised if anything goes wrong. |
73 | 245 | """ | 245 | """ |
74 | 246 | try: | 246 | try: |
76 | 247 | return self.urlopen(page) | 247 | return self.urlopen(page, data) |
77 | 248 | except (urllib2.HTTPError, urllib2.URLError), val: | 248 | except (urllib2.HTTPError, urllib2.URLError), val: |
78 | 249 | raise BugTrackerConnectError(self.baseurl, val) | 249 | raise BugTrackerConnectError(self.baseurl, val) |
79 | 250 | 250 | ||
80 | @@ -260,7 +260,7 @@ | |||
81 | 260 | def _post(self, url, data): | 260 | def _post(self, url, data): |
82 | 261 | """Post to a given URL.""" | 261 | """Post to a given URL.""" |
83 | 262 | request = urllib2.Request(url, headers={'User-agent': LP_USER_AGENT}) | 262 | request = urllib2.Request(url, headers={'User-agent': LP_USER_AGENT}) |
85 | 263 | return self.urlopen(request, data=data) | 263 | return self._fetchPage(request, data=data) |
86 | 264 | 264 | ||
87 | 265 | def _postPage(self, page, form, repost_on_redirect=False): | 265 | def _postPage(self, page, form, repost_on_redirect=False): |
88 | 266 | """POST to the specified page and form. | 266 | """POST to the specified page and form. |
89 | 267 | 267 | ||
90 | === modified file 'lib/lp/bugs/externalbugtracker/mantis.py' | |||
91 | --- lib/lp/bugs/externalbugtracker/mantis.py 2011-03-01 11:49:59 +0000 | |||
92 | +++ lib/lp/bugs/externalbugtracker/mantis.py 2011-03-02 00:14:04 +0000 | |||
93 | @@ -8,6 +8,7 @@ | |||
94 | 8 | 8 | ||
95 | 9 | import cgi | 9 | import cgi |
96 | 10 | import csv | 10 | import csv |
97 | 11 | import logging | ||
98 | 11 | import urllib | 12 | import urllib |
99 | 12 | import urllib2 | 13 | import urllib2 |
100 | 13 | from urlparse import urlunparse | 14 | from urlparse import urlunparse |
101 | @@ -98,6 +99,70 @@ | |||
102 | 98 | self, request, fp, code, msg, hdrs, self.rewrite_url(new_url)) | 99 | self, request, fp, code, msg, hdrs, self.rewrite_url(new_url)) |
103 | 99 | 100 | ||
104 | 100 | 101 | ||
105 | 102 | class MantisBugBatchParser: | ||
106 | 103 | """A class that parses the batch of bug data. | ||
107 | 104 | |||
108 | 105 | Using the CSV reader is pretty much essential since the data that comes | ||
109 | 106 | back can include title text which can in turn contain field separators. | ||
110 | 107 | You don't want to handle the unquoting yourself. | ||
111 | 108 | """ | ||
112 | 109 | |||
113 | 110 | def __init__(self, csv_data, logger): | ||
114 | 111 | # Clean out stray, unquoted newlines inside csv_data to avoid the CSV | ||
115 | 112 | # module blowing up. IDEA: perhaps if the size of csv_data is large | ||
116 | 113 | # in the future, this could be moved into a generator. | ||
117 | 114 | csv_data = [s.replace("\r", "") for s in csv_data] | ||
118 | 115 | csv_data = [s.replace("\n", "") for s in csv_data] | ||
119 | 116 | self.reader = csv.reader(csv_data) | ||
120 | 117 | self.logger = logger | ||
121 | 118 | |||
122 | 119 | def processCSVBugLine(self, bug_line, headers): | ||
123 | 120 | """Processes a single line of the CSV.""" | ||
124 | 121 | bug = {} | ||
125 | 122 | for index, header in enumerate(headers): | ||
126 | 123 | try: | ||
127 | 124 | data = bug_line[index] | ||
128 | 125 | except IndexError: | ||
129 | 126 | self.logger.warning("Line %r incomplete." % bug_line) | ||
130 | 127 | return None | ||
131 | 128 | bug[header] = data | ||
132 | 129 | try: | ||
133 | 130 | bug['id'] = int(bug['id']) | ||
134 | 131 | except ValueError: | ||
135 | 132 | self.logger.warning("Encountered invalid bug ID: %r." % bug['id']) | ||
136 | 133 | return None | ||
137 | 134 | return bug | ||
138 | 135 | |||
139 | 136 | def parseHeaderLine(self, reader): | ||
140 | 137 | # The first line of the CSV file is the header. We need to read | ||
141 | 138 | # it because different Mantis instances have different header | ||
142 | 139 | # ordering and even different columns in the export. | ||
143 | 140 | try: | ||
144 | 141 | headers = [h.lower() for h in reader.next()] | ||
145 | 142 | except StopIteration: | ||
146 | 143 | raise UnparsableBugData("Missing header line") | ||
147 | 144 | missing_headers = [ | ||
148 | 145 | name for name in ('id', 'status', 'resolution') | ||
149 | 146 | if name not in headers] | ||
150 | 147 | if missing_headers: | ||
151 | 148 | raise UnparsableBugData( | ||
152 | 149 | "CSV header %r missing fields: %r" % ( | ||
153 | 150 | headers, missing_headers)) | ||
154 | 151 | return headers | ||
155 | 152 | |||
156 | 153 | def getBugs(self): | ||
157 | 154 | headers = self.parseHeaderLine(self.reader) | ||
158 | 155 | bugs = {} | ||
159 | 156 | try: | ||
160 | 157 | for bug_line in self.reader: | ||
161 | 158 | bug = self.processCSVBugLine(bug_line, headers) | ||
162 | 159 | if bug is not None: | ||
163 | 160 | bugs[bug['id']] = bug | ||
164 | 161 | return bugs | ||
165 | 162 | except csv.Error, error: | ||
166 | 163 | raise UnparsableBugData("Exception parsing CSV file: %s." % error) | ||
167 | 164 | |||
168 | 165 | |||
169 | 101 | class Mantis(ExternalBugTracker): | 166 | class Mantis(ExternalBugTracker): |
170 | 102 | """An `ExternalBugTracker` for dealing with Mantis instances. | 167 | """An `ExternalBugTracker` for dealing with Mantis instances. |
171 | 103 | 168 | ||
172 | @@ -114,6 +179,7 @@ | |||
173 | 114 | self._cookie_handler = urllib2.HTTPCookieProcessor() | 179 | self._cookie_handler = urllib2.HTTPCookieProcessor() |
174 | 115 | self._opener = urllib2.build_opener( | 180 | self._opener = urllib2.build_opener( |
175 | 116 | self._cookie_handler, MantisLoginHandler()) | 181 | self._cookie_handler, MantisLoginHandler()) |
176 | 182 | self._logger = logging.getLogger() | ||
177 | 117 | 183 | ||
178 | 118 | @ensure_no_transaction | 184 | @ensure_no_transaction |
179 | 119 | def urlopen(self, request, data=None): | 185 | def urlopen(self, request, data=None): |
180 | @@ -178,7 +244,10 @@ | |||
181 | 178 | 'search': '', | 244 | 'search': '', |
182 | 179 | 'filter': 'Apply Filter', | 245 | 'filter': 'Apply Filter', |
183 | 180 | } | 246 | } |
185 | 181 | self.page = self._postPage("view_all_set.php?f=3", data) | 247 | try: |
186 | 248 | self._postPage("view_all_set.php?f=3", data) | ||
187 | 249 | except BugTrackerConnectError: | ||
188 | 250 | return None | ||
189 | 182 | 251 | ||
190 | 183 | # Finally grab the full CSV export, which uses the | 252 | # Finally grab the full CSV export, which uses the |
191 | 184 | # MANTIS_VIEW_ALL_COOKIE set in the previous step to specify | 253 | # MANTIS_VIEW_ALL_COOKIE set in the previous step to specify |
192 | @@ -275,65 +344,8 @@ | |||
193 | 275 | if not csv_data: | 344 | if not csv_data: |
194 | 276 | raise UnparsableBugData("Empty CSV for %s" % self.baseurl) | 345 | raise UnparsableBugData("Empty CSV for %s" % self.baseurl) |
195 | 277 | 346 | ||
255 | 278 | # Clean out stray, unquoted newlines inside csv_data to avoid | 347 | parser = MantisBugBatchParser(csv_data, self._logger) |
256 | 279 | # the CSV module blowing up. | 348 | return parser.getBugs() |
198 | 280 | csv_data = [s.replace("\r", "") for s in csv_data] | ||
199 | 281 | csv_data = [s.replace("\n", "") for s in csv_data] | ||
200 | 282 | |||
201 | 283 | # The first line of the CSV file is the header. We need to read | ||
202 | 284 | # it because different Mantis instances have different header | ||
203 | 285 | # ordering and even different columns in the export. | ||
204 | 286 | self.headers = [h.lower() for h in csv_data.pop(0).split(",")] | ||
205 | 287 | if len(self.headers) < 2: | ||
206 | 288 | raise UnparsableBugData("CSV header mangled: %r" % self.headers) | ||
207 | 289 | |||
208 | 290 | if not csv_data: | ||
209 | 291 | # A file with a header and no bugs is also useless. | ||
210 | 292 | raise UnparsableBugData("CSV for %s contained no bugs!" | ||
211 | 293 | % self.baseurl) | ||
212 | 294 | |||
213 | 295 | try: | ||
214 | 296 | bugs = {} | ||
215 | 297 | # Using the CSV reader is pretty much essential since the | ||
216 | 298 | # data that comes back can include title text which can in | ||
217 | 299 | # turn contain field separators -- you don't want to handle | ||
218 | 300 | # the unquoting yourself. | ||
219 | 301 | for bug_line in csv.reader(csv_data): | ||
220 | 302 | bug = self._processCSVBugLine(bug_line) | ||
221 | 303 | bugs[int(bug['id'])] = bug | ||
222 | 304 | |||
223 | 305 | return bugs | ||
224 | 306 | |||
225 | 307 | except csv.Error, error: | ||
226 | 308 | raise UnparsableBugData("Exception parsing CSV file: %s." % error) | ||
227 | 309 | |||
228 | 310 | def _processCSVBugLine(self, bug_line): | ||
229 | 311 | """Processes a single line of the CSV. | ||
230 | 312 | |||
231 | 313 | Adds the bug it represents to self.bugs. | ||
232 | 314 | """ | ||
233 | 315 | required_fields = ['id', 'status', 'resolution'] | ||
234 | 316 | bug = {} | ||
235 | 317 | for header in self.headers: | ||
236 | 318 | try: | ||
237 | 319 | data = bug_line.pop(0) | ||
238 | 320 | except IndexError: | ||
239 | 321 | self.warning("Line '%r' incomplete." % bug_line) | ||
240 | 322 | return | ||
241 | 323 | bug[header] = data | ||
242 | 324 | for field in required_fields: | ||
243 | 325 | if field not in bug: | ||
244 | 326 | self.warning("Bug %s lacked field '%r'." % (bug['id'], field)) | ||
245 | 327 | return | ||
246 | 328 | try: | ||
247 | 329 | # See __init__ for an explanation of why we use integer | ||
248 | 330 | # IDs in the internal data structure. | ||
249 | 331 | bug_id = int(bug['id']) | ||
250 | 332 | except ValueError: | ||
251 | 333 | self.warning("Encountered invalid bug ID: %r." % bug['id']) | ||
252 | 334 | return | ||
253 | 335 | |||
254 | 336 | return bug | ||
257 | 337 | 349 | ||
258 | 338 | def _checkForApplicationError(self, page_soup): | 350 | def _checkForApplicationError(self, page_soup): |
259 | 339 | """If Mantis does not find the bug it still returns a 200 OK | 351 | """If Mantis does not find the bug it still returns a 200 OK |
260 | @@ -467,14 +479,6 @@ | |||
261 | 467 | # it makes display of the data nicer. | 479 | # it makes display of the data nicer. |
262 | 468 | return "%(status)s: %(resolution)s" % bug | 480 | return "%(status)s: %(resolution)s" % bug |
263 | 469 | 481 | ||
264 | 470 | def _getStatusFromCSV(self, bug_id): | ||
265 | 471 | try: | ||
266 | 472 | bug = self.bugs[int(bug_id)] | ||
267 | 473 | except KeyError: | ||
268 | 474 | raise BugNotFound(bug_id) | ||
269 | 475 | else: | ||
270 | 476 | return bug['status'], bug['resolution'] | ||
271 | 477 | |||
272 | 478 | def convertRemoteImportance(self, remote_importance): | 482 | def convertRemoteImportance(self, remote_importance): |
273 | 479 | """See `ExternalBugTracker`. | 483 | """See `ExternalBugTracker`. |
274 | 480 | 484 | ||
275 | 481 | 485 | ||
276 | === modified file 'lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py' | |||
277 | --- lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py 2011-03-01 11:49:59 +0000 | |||
278 | +++ lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py 2011-03-02 00:14:04 +0000 | |||
279 | @@ -6,17 +6,25 @@ | |||
280 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
281 | 7 | 7 | ||
282 | 8 | from StringIO import StringIO | 8 | from StringIO import StringIO |
283 | 9 | import urllib2 | ||
284 | 9 | 10 | ||
285 | 10 | from zope.interface import implements | 11 | from zope.interface import implements |
286 | 11 | 12 | ||
288 | 12 | from lp.bugs.externalbugtracker.base import ExternalBugTracker | 13 | from canonical.testing.layers import ZopelessLayer |
289 | 14 | from lp.bugs.externalbugtracker.base import ( | ||
290 | 15 | BugTrackerConnectError, | ||
291 | 16 | ExternalBugTracker, | ||
292 | 17 | ) | ||
293 | 13 | from lp.bugs.externalbugtracker.debbugs import DebBugs | 18 | from lp.bugs.externalbugtracker.debbugs import DebBugs |
294 | 14 | from lp.bugs.interfaces.externalbugtracker import ( | 19 | from lp.bugs.interfaces.externalbugtracker import ( |
295 | 15 | ISupportsBackLinking, | 20 | ISupportsBackLinking, |
296 | 16 | ISupportsCommentImport, | 21 | ISupportsCommentImport, |
297 | 17 | ISupportsCommentPushing, | 22 | ISupportsCommentPushing, |
298 | 18 | ) | 23 | ) |
300 | 19 | from lp.testing import TestCase | 24 | from lp.testing import ( |
301 | 25 | monkey_patch, | ||
302 | 26 | TestCase, | ||
303 | 27 | ) | ||
304 | 20 | from lp.testing.fakemethod import FakeMethod | 28 | from lp.testing.fakemethod import FakeMethod |
305 | 21 | 29 | ||
306 | 22 | 30 | ||
307 | @@ -144,3 +152,21 @@ | |||
308 | 144 | self.assertEqual(2, bugtracker._post.call_count) | 152 | self.assertEqual(2, bugtracker._post.call_count) |
309 | 145 | last_args, last_kwargs = bugtracker._post.calls[-1] | 153 | last_args, last_kwargs = bugtracker._post.calls[-1] |
310 | 146 | self.assertEqual((fake_form.url, ), last_args) | 154 | self.assertEqual((fake_form.url, ), last_args) |
311 | 155 | |||
312 | 156 | |||
313 | 157 | class TestExternalBugTracker(TestCase): | ||
314 | 158 | """Tests for various methods of the ExternalBugTracker.""" | ||
315 | 159 | |||
316 | 160 | layer = ZopelessLayer | ||
317 | 161 | |||
318 | 162 | def test_post_raises_on_404(self): | ||
319 | 163 | # When posting, a 404 is converted to a BugTrackerConnectError. | ||
320 | 164 | base_url = "http://example.com/" | ||
321 | 165 | bugtracker = ExternalBugTracker(base_url) | ||
322 | 166 | def raise404(request, data): | ||
323 | 167 | raise urllib2.HTTPError('url', 404, 'Not Found', None, None) | ||
324 | 168 | with monkey_patch(urllib2, urlopen=raise404): | ||
325 | 169 | self.assertRaises( | ||
326 | 170 | BugTrackerConnectError, | ||
327 | 171 | bugtracker._post, | ||
328 | 172 | 'some-url', {'post-data': 'here'}) | ||
329 | 147 | 173 | ||
330 | === added file 'lib/lp/bugs/externalbugtracker/tests/test_mantis.py' | |||
331 | --- lib/lp/bugs/externalbugtracker/tests/test_mantis.py 1970-01-01 00:00:00 +0000 | |||
332 | +++ lib/lp/bugs/externalbugtracker/tests/test_mantis.py 2011-03-02 00:14:04 +0000 | |||
333 | @@ -0,0 +1,110 @@ | |||
334 | 1 | # Copyright 2011 Canonical Ltd. This software is licensed under the | ||
335 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
336 | 3 | |||
337 | 4 | """Tests for the Mantis BugTracker.""" | ||
338 | 5 | |||
339 | 6 | __metaclass__ = type | ||
340 | 7 | |||
341 | 8 | from testtools.matchers import Equals, Is | ||
342 | 9 | import urllib2 | ||
343 | 10 | |||
344 | 11 | from canonical.testing.layers import ZopelessLayer | ||
345 | 12 | from lp.bugs.externalbugtracker import UnparsableBugData | ||
346 | 13 | from lp.bugs.externalbugtracker.mantis import ( | ||
347 | 14 | Mantis, | ||
348 | 15 | MantisBugBatchParser, | ||
349 | 16 | ) | ||
350 | 17 | from lp.testing import ( | ||
351 | 18 | monkey_patch, | ||
352 | 19 | TestCase, | ||
353 | 20 | ) | ||
354 | 21 | from lp.services.log.logger import BufferLogger | ||
355 | 22 | |||
356 | 23 | |||
357 | 24 | class TestMantisBugBatchParser(TestCase): | ||
358 | 25 | """Test the MantisBugBatchParser class.""" | ||
359 | 26 | |||
360 | 27 | def setUp(self): | ||
361 | 28 | super(TestMantisBugBatchParser, self).setUp() | ||
362 | 29 | self.logger = BufferLogger() | ||
363 | 30 | |||
364 | 31 | def test_empty(self): | ||
365 | 32 | data = [] | ||
366 | 33 | parser = MantisBugBatchParser(data, self.logger) | ||
367 | 34 | exc = self.assertRaises( | ||
368 | 35 | UnparsableBugData, | ||
369 | 36 | parser.getBugs) | ||
370 | 37 | self.assertThat( | ||
371 | 38 | str(exc), Equals("Missing header line")) | ||
372 | 39 | |||
373 | 40 | def test_missing_headers(self): | ||
374 | 41 | data = ['some,headers'] | ||
375 | 42 | parser = MantisBugBatchParser(data, self.logger) | ||
376 | 43 | exc = self.assertRaises( | ||
377 | 44 | UnparsableBugData, | ||
378 | 45 | parser.getBugs) | ||
379 | 46 | self.assertThat( | ||
380 | 47 | str(exc), | ||
381 | 48 | Equals("CSV header ['some', 'headers'] missing fields:" | ||
382 | 49 | " ['id', 'status', 'resolution']")) | ||
383 | 50 | |||
384 | 51 | def test_missing_some_headers(self): | ||
385 | 52 | data = ['some,headers,status,resolution'] | ||
386 | 53 | parser = MantisBugBatchParser(data, self.logger) | ||
387 | 54 | exc = self.assertRaises( | ||
388 | 55 | UnparsableBugData, | ||
389 | 56 | parser.getBugs) | ||
390 | 57 | self.assertThat( | ||
391 | 58 | str(exc), | ||
392 | 59 | Equals("CSV header ['some', 'headers', 'status', 'resolution'] " | ||
393 | 60 | "missing fields: ['id']")) | ||
394 | 61 | |||
395 | 62 | def test_no_bugs(self): | ||
396 | 63 | data = ['other,fields,id,status,resolution'] | ||
397 | 64 | parser = MantisBugBatchParser(data, self.logger) | ||
398 | 65 | self.assertThat(parser.getBugs(), Equals({})) | ||
399 | 66 | |||
400 | 67 | def test_passing(self): | ||
401 | 68 | data = ['ignored,id,resolution,status', | ||
402 | 69 | 'foo,42,not,complete', | ||
403 | 70 | 'boo,13,,confirmed'] | ||
404 | 71 | parser = MantisBugBatchParser(data, self.logger) | ||
405 | 72 | bug_42 = dict( | ||
406 | 73 | id=42, status='complete', resolution='not', ignored='foo') | ||
407 | 74 | bug_13 = dict( | ||
408 | 75 | id=13, status='confirmed', resolution='', ignored='boo') | ||
409 | 76 | self.assertThat(parser.getBugs(), Equals({42: bug_42, 13: bug_13})) | ||
410 | 77 | |||
411 | 78 | def test_incomplete_line(self): | ||
412 | 79 | data = ['ignored,id,resolution,status', | ||
413 | 80 | '42,not,complete'] | ||
414 | 81 | parser = MantisBugBatchParser(data, self.logger) | ||
415 | 82 | self.assertThat(parser.getBugs(), Equals({})) | ||
416 | 83 | log = self.logger.getLogBuffer() | ||
417 | 84 | self.assertThat( | ||
418 | 85 | log, Equals("WARNING Line ['42', 'not', 'complete'] incomplete.\n")) | ||
419 | 86 | |||
420 | 87 | def test_non_integer_id(self): | ||
421 | 88 | data = ['ignored,id,resolution,status', | ||
422 | 89 | 'foo,bar,not,complete'] | ||
423 | 90 | parser = MantisBugBatchParser(data, self.logger) | ||
424 | 91 | self.assertThat(parser.getBugs(), Equals({})) | ||
425 | 92 | log = self.logger.getLogBuffer() | ||
426 | 93 | self.assertThat( | ||
427 | 94 | log, Equals("WARNING Encountered invalid bug ID: 'bar'.\n")) | ||
428 | 95 | |||
429 | 96 | |||
430 | 97 | class TestMantisBugTracker(TestCase): | ||
431 | 98 | """Tests for various methods of the Manits bug tracker.""" | ||
432 | 99 | |||
433 | 100 | layer = ZopelessLayer | ||
434 | 101 | |||
435 | 102 | def test_csv_data_on_post_404(self): | ||
436 | 103 | # If the 'view_all_set.php' request raises a 404, then the csv_data | ||
437 | 104 | # attribute is None. | ||
438 | 105 | base_url = "http://example.com/" | ||
439 | 106 | def raise404(self, request, data): | ||
440 | 107 | raise urllib2.HTTPError('url', 404, 'Not Found', None, None) | ||
441 | 108 | with monkey_patch(Mantis, urlopen=raise404): | ||
442 | 109 | bugtracker = Mantis(base_url) | ||
443 | 110 | self.assertThat(bugtracker.csv_data, Is(None)) | ||
444 | 0 | 111 | ||
445 | === modified file 'lib/lp/bugs/tests/externalbugtracker.py' | |||
446 | --- lib/lp/bugs/tests/externalbugtracker.py 2011-02-09 11:37:19 +0000 | |||
447 | +++ lib/lp/bugs/tests/externalbugtracker.py 2011-03-02 00:14:04 +0000 | |||
448 | @@ -1149,7 +1149,7 @@ | |||
449 | 1149 | """See `Trac`.""" | 1149 | """See `Trac`.""" |
450 | 1150 | return self.supports_single_exports | 1150 | return self.supports_single_exports |
451 | 1151 | 1151 | ||
453 | 1152 | def urlopen(self, url): | 1152 | def urlopen(self, url, data=None): |
454 | 1153 | file_path = os.path.join(os.path.dirname(__file__), 'testfiles') | 1153 | file_path = os.path.join(os.path.dirname(__file__), 'testfiles') |
455 | 1154 | 1154 | ||
456 | 1155 | if self.trace_calls: | 1155 | if self.trace_calls: |
457 | @@ -1489,7 +1489,7 @@ | |||
458 | 1489 | batch_size = None | 1489 | batch_size = None |
459 | 1490 | trace_calls = False | 1490 | trace_calls = False |
460 | 1491 | 1491 | ||
462 | 1492 | def urlopen(self, url): | 1492 | def urlopen(self, url, data=None): |
463 | 1493 | if self.trace_calls: | 1493 | if self.trace_calls: |
464 | 1494 | print "CALLED urlopen(%r)" % (url) | 1494 | print "CALLED urlopen(%r)" % (url) |
465 | 1495 | 1495 |
As long as you land it through ec2 this time :)