Merge lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 into lp:launchpad

Proposed by Tim Penhey
Status: Superseded
Proposed branch: lp:~thumper/launchpad/fix-mantis-warnings-bug-218384
Merge into: lp:launchpad
Diff against target: 383 lines (+213/-73)
4 files modified
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)
To merge this branch: bzr merge lp:~thumper/launchpad/fix-mantis-warnings-bug-218384
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
William Grant code* Approve
Review via email: mp+51679@code.launchpad.net

This proposal has been superseded by a proposal from 2011-03-02.

Commit message

[r=lifeless,wgrant][bug=214982,218384] Fix two Mantis external bug checker errors.

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 BugTrackerConnectionError.

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.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/externalbugtracker/base.py'
2--- lib/lp/bugs/externalbugtracker/base.py 2011-02-23 06:42:12 +0000
3+++ lib/lp/bugs/externalbugtracker/base.py 2011-03-01 02:35:00 +0000
4@@ -238,13 +238,13 @@
5 """
6 return None
7
8- def _fetchPage(self, page):
9+ def _fetchPage(self, page, data=None):
10 """Fetch a page from the remote server.
11
12 A BugTrackerConnectError will be raised if anything goes wrong.
13 """
14 try:
15- return self.urlopen(page)
16+ return self.urlopen(page, data)
17 except (urllib2.HTTPError, urllib2.URLError), val:
18 raise BugTrackerConnectError(self.baseurl, val)
19
20@@ -260,7 +260,7 @@
21 def _post(self, url, data):
22 """Post to a given URL."""
23 request = urllib2.Request(url, headers={'User-agent': LP_USER_AGENT})
24- return self.urlopen(request, data=data)
25+ return self._fetchPage(request, data=data)
26
27 def _postPage(self, page, form, repost_on_redirect=False):
28 """POST to the specified page and form.
29
30=== modified file 'lib/lp/bugs/externalbugtracker/mantis.py'
31--- lib/lp/bugs/externalbugtracker/mantis.py 2011-01-23 09:34:35 +0000
32+++ lib/lp/bugs/externalbugtracker/mantis.py 2011-03-01 02:35:00 +0000
33@@ -8,6 +8,7 @@
34
35 import cgi
36 import csv
37+import logging
38 import urllib
39 import urllib2
40 from urlparse import urlunparse
41@@ -98,6 +99,70 @@
42 self, request, fp, code, msg, hdrs, self.rewrite_url(new_url))
43
44
45+class MantisBugBatchParser:
46+ """A class that parses the batch of bug data.
47+
48+ Using the CSV reader is pretty much essential since the data that comes
49+ back can include title text which can in turn contain field separators.
50+ You don't want to handle the unquoting yourself.
51+ """
52+
53+ def __init__(self, csv_data, logger):
54+ # Clean out stray, unquoted newlines inside csv_data to avoid the CSV
55+ # module blowing up. IDEA: perhaps if the size of csv_data is large
56+ # in the future, this could be moved into a generator.
57+ csv_data = [s.replace("\r", "") for s in csv_data]
58+ csv_data = [s.replace("\n", "") for s in csv_data]
59+ self.reader = csv.reader(csv_data)
60+ self.logger = logger
61+
62+ def processCSVBugLine(self, bug_line, headers):
63+ """Processes a single line of the CSV."""
64+ bug = {}
65+ for index, header in enumerate(headers):
66+ try:
67+ data = bug_line[index]
68+ except IndexError:
69+ self.logger.warning("Line %r incomplete." % bug_line)
70+ return None
71+ bug[header] = data
72+ try:
73+ bug['id'] = int(bug['id'])
74+ except ValueError:
75+ self.logger.warning("Encountered invalid bug ID: %r." % bug['id'])
76+ return None
77+ return bug
78+
79+ def parseHeaderLine(self, reader):
80+ # The first line of the CSV file is the header. We need to read
81+ # it because different Mantis instances have different header
82+ # ordering and even different columns in the export.
83+ try:
84+ headers = [h.lower() for h in reader.next()]
85+ except StopIteration:
86+ raise UnparsableBugData("Missing header line")
87+ missing_headers = [
88+ name for name in ('id', 'status', 'resolution')
89+ if name not in headers]
90+ if missing_headers:
91+ raise UnparsableBugData(
92+ "CSV header %r missing fields: %r" % (
93+ headers, missing_headers))
94+ return headers
95+
96+ def getBugs(self):
97+ headers = self.parseHeaderLine(self.reader)
98+ bugs = {}
99+ try:
100+ for bug_line in self.reader:
101+ bug = self.processCSVBugLine(bug_line, headers)
102+ if bug is not None:
103+ bugs[bug['id']] = bug
104+ return bugs
105+ except csv.Error, error:
106+ raise UnparsableBugData("Exception parsing CSV file: %s." % error)
107+
108+
109 class Mantis(ExternalBugTracker):
110 """An `ExternalBugTracker` for dealing with Mantis instances.
111
112@@ -114,6 +179,7 @@
113 self._cookie_handler = urllib2.HTTPCookieProcessor()
114 self._opener = urllib2.build_opener(
115 self._cookie_handler, MantisLoginHandler())
116+ self._logger = logging.getLogger()
117
118 @ensure_no_transaction
119 def urlopen(self, request, data=None):
120@@ -178,7 +244,10 @@
121 'search': '',
122 'filter': 'Apply Filter',
123 }
124- self.page = self._postPage("view_all_set.php?f=3", data)
125+ try:
126+ self._postPage("view_all_set.php?f=3", data)
127+ except BugTrackerConnectError:
128+ return None
129
130 # Finally grab the full CSV export, which uses the
131 # MANTIS_VIEW_ALL_COOKIE set in the previous step to specify
132@@ -275,65 +344,8 @@
133 if not csv_data:
134 raise UnparsableBugData("Empty CSV for %s" % self.baseurl)
135
136- # Clean out stray, unquoted newlines inside csv_data to avoid
137- # the CSV module blowing up.
138- csv_data = [s.replace("\r", "") for s in csv_data]
139- csv_data = [s.replace("\n", "") for s in csv_data]
140-
141- # The first line of the CSV file is the header. We need to read
142- # it because different Mantis instances have different header
143- # ordering and even different columns in the export.
144- self.headers = [h.lower() for h in csv_data.pop(0).split(",")]
145- if len(self.headers) < 2:
146- raise UnparsableBugData("CSV header mangled: %r" % self.headers)
147-
148- if not csv_data:
149- # A file with a header and no bugs is also useless.
150- raise UnparsableBugData("CSV for %s contained no bugs!"
151- % self.baseurl)
152-
153- try:
154- bugs = {}
155- # Using the CSV reader is pretty much essential since the
156- # data that comes back can include title text which can in
157- # turn contain field separators -- you don't want to handle
158- # the unquoting yourself.
159- for bug_line in csv.reader(csv_data):
160- bug = self._processCSVBugLine(bug_line)
161- bugs[int(bug['id'])] = bug
162-
163- return bugs
164-
165- except csv.Error, error:
166- raise UnparsableBugData("Exception parsing CSV file: %s." % error)
167-
168- def _processCSVBugLine(self, bug_line):
169- """Processes a single line of the CSV.
170-
171- Adds the bug it represents to self.bugs.
172- """
173- required_fields = ['id', 'status', 'resolution']
174- bug = {}
175- for header in self.headers:
176- try:
177- data = bug_line.pop(0)
178- except IndexError:
179- self.warning("Line '%r' incomplete." % bug_line)
180- return
181- bug[header] = data
182- for field in required_fields:
183- if field not in bug:
184- self.warning("Bug %s lacked field '%r'." % (bug['id'], field))
185- return
186- try:
187- # See __init__ for an explanation of why we use integer
188- # IDs in the internal data structure.
189- bug_id = int(bug['id'])
190- except ValueError:
191- self.warning("Encountered invalid bug ID: %r." % bug['id'])
192- return
193-
194- return bug
195+ parser = MantisBugBatchParser(csv_data, self._logger)
196+ return parser.getBugs()
197
198 def _checkForApplicationError(self, page_soup):
199 """If Mantis does not find the bug it still returns a 200 OK
200@@ -467,14 +479,6 @@
201 # it makes display of the data nicer.
202 return "%(status)s: %(resolution)s" % bug
203
204- def _getStatusFromCSV(self, bug_id):
205- try:
206- bug = self.bugs[int(bug_id)]
207- except KeyError:
208- raise BugNotFound(bug_id)
209- else:
210- return bug['status'], bug['resolution']
211-
212 def convertRemoteImportance(self, remote_importance):
213 """See `ExternalBugTracker`.
214
215
216=== modified file 'lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py'
217--- lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py 2011-01-18 20:46:11 +0000
218+++ lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py 2011-03-01 02:35:00 +0000
219@@ -6,17 +6,25 @@
220 __metaclass__ = type
221
222 from StringIO import StringIO
223+import urllib2
224
225 from zope.interface import implements
226
227-from lp.bugs.externalbugtracker.base import ExternalBugTracker
228+from canonical.testing.layers import ZopelessLayer
229+from lp.bugs.externalbugtracker.base import (
230+ BugTrackerConnectError,
231+ ExternalBugTracker,
232+ )
233 from lp.bugs.externalbugtracker.debbugs import DebBugs
234 from lp.bugs.interfaces.externalbugtracker import (
235 ISupportsBackLinking,
236 ISupportsCommentImport,
237 ISupportsCommentPushing,
238 )
239-from lp.testing import TestCase
240+from lp.testing import (
241+ monkey_patch,
242+ TestCase,
243+ )
244 from lp.testing.fakemethod import FakeMethod
245
246
247@@ -144,3 +152,21 @@
248 self.assertEqual(2, bugtracker._post.call_count)
249 last_args, last_kwargs = bugtracker._post.calls[-1]
250 self.assertEqual((fake_form.url, ), last_args)
251+
252+
253+class TestExternalBugTracker(TestCase):
254+ """Tests for various methods of the ExternalBugTracker."""
255+
256+ layer = ZopelessLayer
257+
258+ def test_post_raises_on_404(self):
259+ # When posting, a 404 is converted to a BugTrackerConnectError.
260+ base_url = "http://example.com/"
261+ bugtracker = ExternalBugTracker(base_url)
262+ def raise404(request, data):
263+ raise urllib2.HTTPError('url', 404, 'Not Found', None, None)
264+ with monkey_patch(urllib2, urlopen=raise404):
265+ self.assertRaises(
266+ BugTrackerConnectError,
267+ bugtracker._post,
268+ 'some-url', {'post-data': 'here'})
269
270=== added file 'lib/lp/bugs/externalbugtracker/tests/test_mantis.py'
271--- lib/lp/bugs/externalbugtracker/tests/test_mantis.py 1970-01-01 00:00:00 +0000
272+++ lib/lp/bugs/externalbugtracker/tests/test_mantis.py 2011-03-01 02:35:00 +0000
273@@ -0,0 +1,110 @@
274+# Copyright 2011 Canonical Ltd. This software is licensed under the
275+# GNU Affero General Public License version 3 (see the file LICENSE).
276+
277+"""Tests for the Mantis BugTracker."""
278+
279+__metaclass__ = type
280+
281+from testtools.matchers import Equals, Is
282+import urllib2
283+
284+from canonical.testing.layers import ZopelessLayer
285+from lp.bugs.externalbugtracker import UnparsableBugData
286+from lp.bugs.externalbugtracker.mantis import (
287+ Mantis,
288+ MantisBugBatchParser,
289+ )
290+from lp.testing import (
291+ monkey_patch,
292+ TestCase,
293+ )
294+from lp.services.log.logger import BufferLogger
295+
296+
297+class TestMantisBugBatchParser(TestCase):
298+ """Test the MantisBugBatchParser class."""
299+
300+ def setUp(self):
301+ super(TestMantisBugBatchParser, self).setUp()
302+ self.logger = BufferLogger()
303+
304+ def test_empty(self):
305+ data = []
306+ parser = MantisBugBatchParser(data, self.logger)
307+ exc = self.assertRaises(
308+ UnparsableBugData,
309+ parser.getBugs)
310+ self.assertThat(
311+ str(exc), Equals("Missing header line"))
312+
313+ def test_missing_headers(self):
314+ data = ['some,headers']
315+ parser = MantisBugBatchParser(data, self.logger)
316+ exc = self.assertRaises(
317+ UnparsableBugData,
318+ parser.getBugs)
319+ self.assertThat(
320+ str(exc),
321+ Equals("CSV header ['some', 'headers'] missing fields:"
322+ " ['id', 'status', 'resolution']"))
323+
324+ def test_missing_some_headers(self):
325+ data = ['some,headers,status,resolution']
326+ parser = MantisBugBatchParser(data, self.logger)
327+ exc = self.assertRaises(
328+ UnparsableBugData,
329+ parser.getBugs)
330+ self.assertThat(
331+ str(exc),
332+ Equals("CSV header ['some', 'headers', 'status', 'resolution'] "
333+ "missing fields: ['id']"))
334+
335+ def test_no_bugs(self):
336+ data = ['other,fields,id,status,resolution']
337+ parser = MantisBugBatchParser(data, self.logger)
338+ self.assertThat(parser.getBugs(), Equals({}))
339+
340+ def test_passing(self):
341+ data = ['ignored,id,resolution,status',
342+ 'foo,42,not,complete',
343+ 'boo,13,,confirmed']
344+ parser = MantisBugBatchParser(data, self.logger)
345+ bug_42 = dict(
346+ id=42, status='complete', resolution='not', ignored='foo')
347+ bug_13 = dict(
348+ id=13, status='confirmed', resolution='', ignored='boo')
349+ self.assertThat(parser.getBugs(), Equals({42: bug_42, 13: bug_13}))
350+
351+ def test_incomplete_line(self):
352+ data = ['ignored,id,resolution,status',
353+ '42,not,complete']
354+ parser = MantisBugBatchParser(data, self.logger)
355+ self.assertThat(parser.getBugs(), Equals({}))
356+ log = self.logger.getLogBuffer()
357+ self.assertThat(
358+ log, Equals("WARNING Line ['42', 'not', 'complete'] incomplete.\n"))
359+
360+ def test_non_integer_id(self):
361+ data = ['ignored,id,resolution,status',
362+ 'foo,bar,not,complete']
363+ parser = MantisBugBatchParser(data, self.logger)
364+ self.assertThat(parser.getBugs(), Equals({}))
365+ log = self.logger.getLogBuffer()
366+ self.assertThat(
367+ log, Equals("WARNING Encountered invalid bug ID: 'bar'.\n"))
368+
369+
370+class TestMantisBugTracker(TestCase):
371+ """Tests for various methods of the Manits bug tracker."""
372+
373+ layer = ZopelessLayer
374+
375+ def test_csv_data_on_post_404(self):
376+ # If the 'view_all_set.php' request raises a 404, then the csv_data
377+ # attribute is None.
378+ base_url = "http://example.com/"
379+ def raise404(self, request, data):
380+ raise urllib2.HTTPError('url', 404, 'Not Found', None, None)
381+ with monkey_patch(Mantis, urlopen=raise404):
382+ bugtracker = Mantis(base_url)
383+ self.assertThat(bugtracker.csv_data, Is(None))