← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Tim Penhey has proposed merging lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #214982 HTTPErrror raised in Mantis ExternalBugTracker
  https://bugs.launchpad.net/bugs/214982
  #218384 OOPS processing Mantis bugwatches
  https://bugs.launchpad.net/bugs/218384

For more details, see:
https://code.launchpad.net/~thumper/launchpad/fix-mantis-warnings-bug-218384/+merge/51679

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.

-- 
https://code.launchpad.net/~thumper/launchpad/fix-mantis-warnings-bug-218384/+merge/51679
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/fix-mantis-warnings-bug-218384 into lp:launchpad.
=== modified file 'lib/lp/bugs/externalbugtracker/base.py'
--- lib/lp/bugs/externalbugtracker/base.py	2011-02-23 06:42:12 +0000
+++ lib/lp/bugs/externalbugtracker/base.py	2011-03-01 02:35:00 +0000
@@ -238,13 +238,13 @@
         """
         return None
 
-    def _fetchPage(self, page):
+    def _fetchPage(self, page, data=None):
         """Fetch a page from the remote server.
 
         A BugTrackerConnectError will be raised if anything goes wrong.
         """
         try:
-            return self.urlopen(page)
+            return self.urlopen(page, data)
         except (urllib2.HTTPError, urllib2.URLError), val:
             raise BugTrackerConnectError(self.baseurl, val)
 
@@ -260,7 +260,7 @@
     def _post(self, url, data):
         """Post to a given URL."""
         request = urllib2.Request(url, headers={'User-agent': LP_USER_AGENT})
-        return self.urlopen(request, data=data)
+        return self._fetchPage(request, data=data)
 
     def _postPage(self, page, form, repost_on_redirect=False):
         """POST to the specified page and form.

=== modified file 'lib/lp/bugs/externalbugtracker/mantis.py'
--- lib/lp/bugs/externalbugtracker/mantis.py	2011-01-23 09:34:35 +0000
+++ lib/lp/bugs/externalbugtracker/mantis.py	2011-03-01 02:35:00 +0000
@@ -8,6 +8,7 @@
 
 import cgi
 import csv
+import logging
 import urllib
 import urllib2
 from urlparse import urlunparse
@@ -98,6 +99,70 @@
             self, request, fp, code, msg, hdrs, self.rewrite_url(new_url))
 
 
+class MantisBugBatchParser:
+    """A class that parses the batch of bug data.
+
+    Using the CSV reader is pretty much essential since the data that comes
+    back can include title text which can in turn contain field separators.
+    You don't want to handle the unquoting yourself.
+    """
+
+    def __init__(self, csv_data, logger):
+        # Clean out stray, unquoted newlines inside csv_data to avoid the CSV
+        # module blowing up.  IDEA: perhaps if the size of csv_data is large
+        # in the future, this could be moved into a generator.
+        csv_data = [s.replace("\r", "") for s in csv_data]
+        csv_data = [s.replace("\n", "") for s in csv_data]
+        self.reader = csv.reader(csv_data)
+        self.logger = logger
+
+    def processCSVBugLine(self, bug_line, headers):
+        """Processes a single line of the CSV."""
+        bug = {}
+        for index, header in enumerate(headers):
+            try:
+                data = bug_line[index]
+            except IndexError:
+                self.logger.warning("Line %r incomplete." % bug_line)
+                return None
+            bug[header] = data
+        try:
+            bug['id'] = int(bug['id'])
+        except ValueError:
+            self.logger.warning("Encountered invalid bug ID: %r." % bug['id'])
+            return None
+        return bug
+
+    def parseHeaderLine(self, reader):
+        # The first line of the CSV file is the header. We need to read
+        # it because different Mantis instances have different header
+        # ordering and even different columns in the export.
+        try:
+            headers = [h.lower() for h in reader.next()]
+        except StopIteration:
+            raise UnparsableBugData("Missing header line")
+        missing_headers = [
+            name for name in ('id', 'status', 'resolution')
+            if name not in headers]
+        if missing_headers:
+            raise UnparsableBugData(
+                "CSV header %r missing fields: %r" % (
+                    headers, missing_headers))
+        return headers
+
+    def getBugs(self):
+        headers = self.parseHeaderLine(self.reader)
+        bugs = {}
+        try:
+            for bug_line in self.reader:
+                bug = self.processCSVBugLine(bug_line, headers)
+                if bug is not None:
+                    bugs[bug['id']] = bug
+            return bugs
+        except csv.Error, error:
+            raise UnparsableBugData("Exception parsing CSV file: %s." % error)
+
+
 class Mantis(ExternalBugTracker):
     """An `ExternalBugTracker` for dealing with Mantis instances.
 
@@ -114,6 +179,7 @@
         self._cookie_handler = urllib2.HTTPCookieProcessor()
         self._opener = urllib2.build_opener(
             self._cookie_handler, MantisLoginHandler())
+        self._logger = logging.getLogger()
 
     @ensure_no_transaction
     def urlopen(self, request, data=None):
@@ -178,7 +244,10 @@
            'search': '',
            'filter': 'Apply Filter',
         }
-        self.page = self._postPage("view_all_set.php?f=3", data)
+        try:
+            self._postPage("view_all_set.php?f=3", data)
+        except BugTrackerConnectError:
+            return None
 
         # Finally grab the full CSV export, which uses the
         # MANTIS_VIEW_ALL_COOKIE set in the previous step to specify
@@ -275,65 +344,8 @@
         if not csv_data:
             raise UnparsableBugData("Empty CSV for %s" % self.baseurl)
 
-        # Clean out stray, unquoted newlines inside csv_data to avoid
-        # the CSV module blowing up.
-        csv_data = [s.replace("\r", "") for s in csv_data]
-        csv_data = [s.replace("\n", "") for s in csv_data]
-
-        # The first line of the CSV file is the header. We need to read
-        # it because different Mantis instances have different header
-        # ordering and even different columns in the export.
-        self.headers = [h.lower() for h in csv_data.pop(0).split(",")]
-        if len(self.headers) < 2:
-            raise UnparsableBugData("CSV header mangled: %r" % self.headers)
-
-        if not csv_data:
-            # A file with a header and no bugs is also useless.
-            raise UnparsableBugData("CSV for %s contained no bugs!"
-                                     % self.baseurl)
-
-        try:
-            bugs = {}
-            # Using the CSV reader is pretty much essential since the
-            # data that comes back can include title text which can in
-            # turn contain field separators -- you don't want to handle
-            # the unquoting yourself.
-            for bug_line in csv.reader(csv_data):
-                bug = self._processCSVBugLine(bug_line)
-                bugs[int(bug['id'])] = bug
-
-            return bugs
-
-        except csv.Error, error:
-            raise UnparsableBugData("Exception parsing CSV file: %s." % error)
-
-    def _processCSVBugLine(self, bug_line):
-        """Processes a single line of the CSV.
-
-        Adds the bug it represents to self.bugs.
-        """
-        required_fields = ['id', 'status', 'resolution']
-        bug = {}
-        for header in self.headers:
-            try:
-                data = bug_line.pop(0)
-            except IndexError:
-                self.warning("Line '%r' incomplete." % bug_line)
-                return
-            bug[header] = data
-        for field in required_fields:
-            if field not in bug:
-                self.warning("Bug %s lacked field '%r'." % (bug['id'], field))
-                return
-            try:
-                # See __init__ for an explanation of why we use integer
-                # IDs in the internal data structure.
-                bug_id = int(bug['id'])
-            except ValueError:
-                self.warning("Encountered invalid bug ID: %r." % bug['id'])
-                return
-
-        return bug
+        parser = MantisBugBatchParser(csv_data, self._logger)
+        return parser.getBugs()
 
     def _checkForApplicationError(self, page_soup):
         """If Mantis does not find the bug it still returns a 200 OK
@@ -467,14 +479,6 @@
         # it makes display of the data nicer.
         return "%(status)s: %(resolution)s" % bug
 
-    def _getStatusFromCSV(self, bug_id):
-        try:
-            bug = self.bugs[int(bug_id)]
-        except KeyError:
-            raise BugNotFound(bug_id)
-        else:
-            return bug['status'], bug['resolution']
-
     def convertRemoteImportance(self, remote_importance):
         """See `ExternalBugTracker`.
 

=== modified file 'lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py'
--- lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py	2011-01-18 20:46:11 +0000
+++ lib/lp/bugs/externalbugtracker/tests/test_externalbugtracker.py	2011-03-01 02:35:00 +0000
@@ -6,17 +6,25 @@
 __metaclass__ = type
 
 from StringIO import StringIO
+import urllib2
 
 from zope.interface import implements
 
-from lp.bugs.externalbugtracker.base import ExternalBugTracker
+from canonical.testing.layers import ZopelessLayer
+from lp.bugs.externalbugtracker.base import (
+    BugTrackerConnectError,
+    ExternalBugTracker,
+    )
 from lp.bugs.externalbugtracker.debbugs import DebBugs
 from lp.bugs.interfaces.externalbugtracker import (
     ISupportsBackLinking,
     ISupportsCommentImport,
     ISupportsCommentPushing,
     )
-from lp.testing import TestCase
+from lp.testing import (
+    monkey_patch,
+    TestCase,
+    )
 from lp.testing.fakemethod import FakeMethod
 
 
@@ -144,3 +152,21 @@
         self.assertEqual(2, bugtracker._post.call_count)
         last_args, last_kwargs = bugtracker._post.calls[-1]
         self.assertEqual((fake_form.url, ), last_args)
+
+
+class TestExternalBugTracker(TestCase):
+    """Tests for various methods of the ExternalBugTracker."""
+
+    layer = ZopelessLayer
+
+    def test_post_raises_on_404(self):
+        # When posting, a 404 is converted to a BugTrackerConnectError.
+        base_url = "http://example.com/";
+        bugtracker = ExternalBugTracker(base_url)
+        def raise404(request, data):
+            raise urllib2.HTTPError('url', 404, 'Not Found', None, None)
+        with monkey_patch(urllib2, urlopen=raise404):
+            self.assertRaises(
+                BugTrackerConnectError,
+                bugtracker._post,
+                'some-url', {'post-data': 'here'})

=== added file 'lib/lp/bugs/externalbugtracker/tests/test_mantis.py'
--- lib/lp/bugs/externalbugtracker/tests/test_mantis.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/externalbugtracker/tests/test_mantis.py	2011-03-01 02:35:00 +0000
@@ -0,0 +1,110 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the Mantis BugTracker."""
+
+__metaclass__ = type
+
+from testtools.matchers import Equals, Is
+import urllib2
+
+from canonical.testing.layers import ZopelessLayer
+from lp.bugs.externalbugtracker import UnparsableBugData
+from lp.bugs.externalbugtracker.mantis import (
+    Mantis,
+    MantisBugBatchParser,
+    )
+from lp.testing import (
+    monkey_patch,
+    TestCase,
+    )
+from lp.services.log.logger import BufferLogger
+
+
+class TestMantisBugBatchParser(TestCase):
+    """Test the MantisBugBatchParser class."""
+
+    def setUp(self):
+        super(TestMantisBugBatchParser, self).setUp()
+        self.logger = BufferLogger()
+
+    def test_empty(self):
+        data = []
+        parser = MantisBugBatchParser(data, self.logger)
+        exc = self.assertRaises(
+            UnparsableBugData,
+            parser.getBugs)
+        self.assertThat(
+            str(exc), Equals("Missing header line"))
+
+    def test_missing_headers(self):
+        data = ['some,headers']
+        parser = MantisBugBatchParser(data, self.logger)
+        exc = self.assertRaises(
+            UnparsableBugData,
+            parser.getBugs)
+        self.assertThat(
+            str(exc),
+            Equals("CSV header ['some', 'headers'] missing fields:"
+                   " ['id', 'status', 'resolution']"))
+
+    def test_missing_some_headers(self):
+        data = ['some,headers,status,resolution']
+        parser = MantisBugBatchParser(data, self.logger)
+        exc = self.assertRaises(
+            UnparsableBugData,
+            parser.getBugs)
+        self.assertThat(
+            str(exc),
+            Equals("CSV header ['some', 'headers', 'status', 'resolution'] "
+                   "missing fields: ['id']"))
+
+    def test_no_bugs(self):
+        data = ['other,fields,id,status,resolution']
+        parser = MantisBugBatchParser(data, self.logger)
+        self.assertThat(parser.getBugs(), Equals({}))
+
+    def test_passing(self):
+        data = ['ignored,id,resolution,status',
+                'foo,42,not,complete',
+                'boo,13,,confirmed']
+        parser = MantisBugBatchParser(data, self.logger)
+        bug_42 = dict(
+            id=42, status='complete', resolution='not', ignored='foo')
+        bug_13 = dict(
+            id=13, status='confirmed', resolution='', ignored='boo')
+        self.assertThat(parser.getBugs(), Equals({42: bug_42, 13: bug_13}))
+
+    def test_incomplete_line(self):
+        data = ['ignored,id,resolution,status',
+                '42,not,complete']
+        parser = MantisBugBatchParser(data, self.logger)
+        self.assertThat(parser.getBugs(), Equals({}))
+        log = self.logger.getLogBuffer()
+        self.assertThat(
+            log, Equals("WARNING Line ['42', 'not', 'complete'] incomplete.\n"))
+
+    def test_non_integer_id(self):
+        data = ['ignored,id,resolution,status',
+                'foo,bar,not,complete']
+        parser = MantisBugBatchParser(data, self.logger)
+        self.assertThat(parser.getBugs(), Equals({}))
+        log = self.logger.getLogBuffer()
+        self.assertThat(
+            log, Equals("WARNING Encountered invalid bug ID: 'bar'.\n"))
+
+
+class TestMantisBugTracker(TestCase):
+    """Tests for various methods of the Manits bug tracker."""
+
+    layer = ZopelessLayer
+
+    def test_csv_data_on_post_404(self):
+        # If the 'view_all_set.php' request raises a 404, then the csv_data
+        # attribute is None.
+        base_url = "http://example.com/";
+        def raise404(self, request, data):
+            raise urllib2.HTTPError('url', 404, 'Not Found', None, None)
+        with monkey_patch(Mantis, urlopen=raise404):
+            bugtracker = Mantis(base_url)
+            self.assertThat(bugtracker.csv_data, Is(None))