← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/search-oopses-1 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/search-oopses-1 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #267852 error page when google search fails is unusable
  https://bugs.launchpad.net/bugs/267852

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/search-oopses-1/+merge/52272

Suppress oopses from the remote Google search service.

    Launchpad bug: https://bugs.launchpad.net/bugs/267852
    Pre-implementation: no one
    Test command: ./bin/test -vv \
      -t launchpad-search-pages -m lp.services.search.tests.test_google

There are several kinds of oopses
    OOPS-1552A1017 (URLError: <urlopen error (110, 'Connection timed out')
    OOPS-979H877 (IndexError: list index out of range)
    OOPS-1539B1178 (HTTPError: HTTP Error 500: Internal Server Error)
that emanate from GoogleSearchService. It should catch these
errors and raise GoogleResponseError which is handled by the view.

LaunchpadSearchView should not report GoogleResponseError as an oopses
because we know there is nothing in Lp's code to fix.

--------------------------------------------------------------------

RULES

    * Remove the oops reporting block from LaunchpadSearchView.
    * Convert the connection/response/incomplete-data errors to
      GoogleResponseError.


QA

    * None


LINT

    lib/lp/app/browser/root.py
    lib/lp/app/browser/tests/launchpad-search-pages.txt
    lib/lp/services/search/google.py
    lib/lp/services/search/tests/test_google.py


IMPLEMENTATION

Removed the oops reporting block from LaunchpadSearchView.
    lib/lp/app/browser/root.py
    lib/lp/app/browser/tests/launchpad-search-pages.txt

Converted the connection/response/data errors to GoogleResponseError.
    lib/lp/services/search/google.py
    lib/lp/services/search/tests/test_google.py
-- 
https://code.launchpad.net/~sinzui/launchpad/search-oopses-1/+merge/52272
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/search-oopses-1 into lp:launchpad.
=== modified file 'lib/lp/app/browser/root.py'
--- lib/lp/app/browser/root.py	2011-03-04 17:05:09 +0000
+++ lib/lp/app/browser/root.py	2011-03-05 00:09:00 +0000
@@ -10,13 +10,11 @@
 
 
 import re
-import sys
 import time
 
 import feedparser
 from lazr.batchnavigator.z3batching import batch
 from zope.component import getUtility
-from zope.error.interfaces import IErrorReportingUtility
 from zope.schema.interfaces import TooLong
 from zope.schema.vocabulary import getVocabularyRegistry
 
@@ -489,8 +487,8 @@
             page_matches = google_search.search(
                 terms=query_terms, start=start)
         except GoogleResponseError:
-            error_utility = getUtility(IErrorReportingUtility)
-            error_utility.raising(sys.exc_info(), self.request)
+            # There was a connectivity or Google service issue that means
+            # there is no data available at this moment.
             self.has_page_service = False
             return None
         if len(page_matches) == 0:

=== modified file 'lib/lp/app/browser/tests/launchpad-search-pages.txt'
--- lib/lp/app/browser/tests/launchpad-search-pages.txt	2011-03-04 17:05:09 +0000
+++ lib/lp/app/browser/tests/launchpad-search-pages.txt	2011-03-05 00:09:00 +0000
@@ -535,15 +535,6 @@
     >>> print search_view.url
     http://launchpad.dev/+search?field.text=gnomebaker
 
-An oops was reported by the view so that the GoogleResponseError can be
-tracked.
-
-    >>> oops = search_view.request.oops
-    >>> oops.type
-    'GoogleResponseError'
-    >>> oops.value
-    'The response was incomplete, no xml.'
-
 
 SearchFormView and SearchFormPrimaryView
 ----------------------------------------

=== modified file 'lib/lp/services/search/google.py'
--- lib/lp/services/search/google.py	2011-03-04 17:05:09 +0000
+++ lib/lp/services/search/google.py	2011-03-05 00:09:00 +0000
@@ -15,6 +15,7 @@
 
 import xml.etree.cElementTree as ET
 import urllib
+import urllib2
 from urlparse import urlunparse
 
 from lazr.restful.utils import get_current_browser_request
@@ -22,6 +23,7 @@
 from zope.interface import implements
 
 from canonical.config import config
+from canonical.lazr.timeout import TimeoutError
 from lp.services.search.interfaces import (
     GoogleResponseError,
     GoogleWrongGSPVersion,
@@ -196,6 +198,11 @@
         action = timeline.start("google-search-api", search_url)
         try:
             gsp_xml = urlfetch(search_url)
+        except (TimeoutError, urllib2.HTTPError, urllib2.URLError), error:
+            # Google search service errors are not code errors. Let the
+            # call site choose to handle the unavailable service.
+            raise GoogleResponseError(
+                "The response errored: %s" % str(error))
         finally:
             action.finish()
         page_matches = self._parse_google_search_protocol(gsp_xml)
@@ -266,10 +273,10 @@
         """
         try:
             gsp_doc = ET.fromstring(gsp_xml)
-        except SyntaxError:
+            start_param = self._getElementByAttributeValue(
+                gsp_doc, './PARAM', 'name', 'start')
+        except (SyntaxError, IndexError):
             raise GoogleResponseError("The response was incomplete, no xml.")
-        start_param = self._getElementByAttributeValue(
-            gsp_doc, './PARAM', 'name', 'start')
         try:
             start = int(start_param.get('value'))
         except (AttributeError, ValueError):

=== added file 'lib/lp/services/search/tests/test_google.py'
--- lib/lp/services/search/tests/test_google.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/search/tests/test_google.py	2011-03-05 00:09:00 +0000
@@ -0,0 +1,84 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test the google search service."""
+
+__metaclass__ = type
+
+from contextlib import contextmanager
+from urllib2 import (
+    HTTPError,
+    URLError,
+    )
+
+from canonical.lazr.timeout import TimeoutError
+from canonical.testing.layers import FunctionalLayer
+from lp.services.search.google import GoogleSearchService
+from lp.services.search.interfaces import GoogleResponseError
+from lp.testing import TestCase
+
+
+@contextmanager
+def urlfetch_exception(test_error, *args):
+    """Raise an error during the execution of urlfetch.
+
+    This function replaces urlfetch() with a function that
+    raises an error.
+    """
+
+    def raise_exception(url):
+        raise test_error(*args)
+
+    from canonical.lazr import timeout
+    original_urlfetch = timeout.urlfetch
+    timeout.urlfetch = raise_exception
+    try:
+        yield
+    finally:
+        timeout.urlfetch = original_urlfetch
+
+
+class TestGoogleSearchService(TestCase):
+    """Test GoogleSearchService."""
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(TestGoogleSearchService, self).setUp()
+        self.search_service = GoogleSearchService()
+
+    def test_search_converts_HTTPError(self):
+        # The method converts HTTPError to GoogleResponseError.
+        args = ('url', 500, 'oops', {}, None)
+        with urlfetch_exception(HTTPError, *args):
+            self.assertRaises(
+                GoogleResponseError, self.search_service.search, 'fnord')
+
+    def test_search_converts_URLError(self):
+        # The method converts URLError to GoogleResponseError.
+        with urlfetch_exception(URLError, 'oops'):
+            self.assertRaises(
+                GoogleResponseError, self.search_service.search, 'fnord')
+
+    def test_search_converts_TimeoutError(self):
+        # The method converts TimeoutError to GoogleResponseError.
+        with urlfetch_exception(TimeoutError, 'oops'):
+            self.assertRaises(
+                GoogleResponseError, self.search_service.search, 'fnord')
+
+    def test___parse_google_search_protocol_SyntaxError(self):
+        # The method converts SyntaxError to GoogleResponseError.
+        with urlfetch_exception(SyntaxError, 'oops'):
+            self.assertRaises(
+                GoogleResponseError,
+                self.search_service._parse_google_search_protocol, '')
+
+    def test___parse_google_search_protocol_IndexError(self):
+        # The method converts IndexError to GoogleResponseError.
+        with urlfetch_exception(IndexError, 'oops'):
+            data = (
+                '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
+                '<GSP VER="3.2"></GSP>')
+            self.assertRaises(
+                GoogleResponseError,
+                self.search_service._parse_google_search_protocol, data)