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

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 12544
Proposed branch: lp:~sinzui/launchpad/search-oopses-1
Merge into: lp:launchpad
Diff against target: 423 lines (+123/-45)
15 files modified
buildout.cfg (+1/-1)
lib/canonical/config/schema-lazr.conf (+2/-2)
lib/canonical/launchpad/scripts/runlaunchpad.py (+1/-1)
lib/canonical/launchpad/testing/tests/test_googleservice.py (+1/-1)
lib/canonical/testing/layers.py (+1/-1)
lib/lp/app/browser/root.py (+3/-5)
lib/lp/app/browser/tests/launchpad-search-pages.txt (+1/-11)
lib/lp/services/configure.zcml (+1/-1)
lib/lp/services/googlesearch/__init__.py (+11/-4)
lib/lp/services/googlesearch/configure.zcml (+7/-7)
lib/lp/services/googlesearch/doc/google-searchservice.txt (+6/-7)
lib/lp/services/googlesearch/doc/google-service-stub.txt.disabled (+1/-1)
lib/lp/services/googlesearch/tests/googleserviceharness.py (+2/-2)
lib/lp/services/googlesearch/tests/test_google.py (+84/-0)
lib/lp/services/googlesearch/tests/test_googleharness.py (+1/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/search-oopses-1
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+52272@code.launchpad.net

Description of the change

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

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'buildout.cfg'
2--- buildout.cfg 2011-03-04 17:05:09 +0000
3+++ buildout.cfg 2011-03-07 16:35:45 +0000
4@@ -55,7 +55,7 @@
5 from lp_sitecustomize import main
6 main('${configuration:instance_name}') # Initializes LP environment.
7 entry-points = stxdocs=zope.configuration.stxdocs:main
8- googletestservice=lp.services.search.googletestservice:main
9+ googletestservice=lp.services.googlesearch.googletestservice:main
10 windmill=windmill.bin.windmill_bin:main
11 lp-windmill=lp.scripts.utilities.lpwindmill:main
12 jsbuild=lazr.js.build:main
13
14=== modified file 'lib/canonical/config/schema-lazr.conf'
15--- lib/canonical/config/schema-lazr.conf 2011-03-05 00:06:26 +0000
16+++ lib/canonical/config/schema-lazr.conf 2011-03-07 16:35:45 +0000
17@@ -903,10 +903,10 @@
18 # Run a web service stub that simulates the Google search service.
19
20 # Where are our canned XML responses stored?
21-canned_response_directory: lib/lp/services/search/tests/data/
22+canned_response_directory: lib/lp/services/googlesearch/tests/data/
23
24 # Which file maps service URLs to the XML that the server returns?
25-mapfile: lib/lp/services/search/tests/data/mapping.txt
26+mapfile: lib/lp/services/googlesearch/tests/data/mapping.txt
27
28 # Where should the service log files live?
29 log: logs/google-stub.log
30
31=== modified file 'lib/canonical/launchpad/scripts/runlaunchpad.py'
32--- lib/canonical/launchpad/scripts/runlaunchpad.py 2011-03-04 17:05:09 +0000
33+++ lib/canonical/launchpad/scripts/runlaunchpad.py 2011-03-07 16:35:45 +0000
34@@ -24,7 +24,7 @@
35 )
36 from lp.services.mailman import runmailman
37 from lp.services.osutils import ensure_directory_exists
38-from lp.services.search import googletestservice
39+from lp.services.googlesearch import googletestservice
40
41
42 def make_abspath(path):
43
44=== modified file 'lib/canonical/launchpad/testing/tests/test_googleservice.py'
45--- lib/canonical/launchpad/testing/tests/test_googleservice.py 2011-03-04 17:05:09 +0000
46+++ lib/canonical/launchpad/testing/tests/test_googleservice.py 2011-03-07 16:35:45 +0000
47@@ -12,7 +12,7 @@
48 import os
49 import unittest
50
51-from lp.services.search import googletestservice
52+from lp.services.googlesearch import googletestservice
53 from canonical.lazr.pidfile import pidfile_path
54
55
56
57=== modified file 'lib/canonical/testing/layers.py'
58--- lib/canonical/testing/layers.py 2011-03-04 17:12:24 +0000
59+++ lib/canonical/testing/layers.py 2011-03-07 16:35:45 +0000
60@@ -116,7 +116,7 @@
61 import lp.services.mail.stub
62 from lp.services.mail.mailbox import TestMailBox
63 from canonical.launchpad.scripts import execute_zcml_for_scripts
64-from lp.services.search.tests.googleserviceharness import (
65+from lp.services.googlesearch.tests.googleserviceharness import (
66 GoogleServiceTestSetup)
67 from canonical.launchpad.webapp.interfaces import (
68 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
69
70=== modified file 'lib/lp/app/browser/root.py'
71--- lib/lp/app/browser/root.py 2011-03-04 17:05:09 +0000
72+++ lib/lp/app/browser/root.py 2011-03-07 16:35:45 +0000
73@@ -10,13 +10,11 @@
74
75
76 import re
77-import sys
78 import time
79
80 import feedparser
81 from lazr.batchnavigator.z3batching import batch
82 from zope.component import getUtility
83-from zope.error.interfaces import IErrorReportingUtility
84 from zope.schema.interfaces import TooLong
85 from zope.schema.vocabulary import getVocabularyRegistry
86
87@@ -28,7 +26,7 @@
88 from canonical.launchpad.interfaces.launchpadstatistic import (
89 ILaunchpadStatisticSet,
90 )
91-from lp.services.search.interfaces import (
92+from lp.services.googlesearch.interfaces import (
93 GoogleResponseError,
94 ISearchService,
95 )
96@@ -489,8 +487,8 @@
97 page_matches = google_search.search(
98 terms=query_terms, start=start)
99 except GoogleResponseError:
100- error_utility = getUtility(IErrorReportingUtility)
101- error_utility.raising(sys.exc_info(), self.request)
102+ # There was a connectivity or Google service issue that means
103+ # there is no data available at this moment.
104 self.has_page_service = False
105 return None
106 if len(page_matches) == 0:
107
108=== modified file 'lib/lp/app/browser/tests/launchpad-search-pages.txt'
109--- lib/lp/app/browser/tests/launchpad-search-pages.txt 2011-03-04 17:05:09 +0000
110+++ lib/lp/app/browser/tests/launchpad-search-pages.txt 2011-03-07 16:35:45 +0000
111@@ -535,15 +535,6 @@
112 >>> print search_view.url
113 http://launchpad.dev/+search?field.text=gnomebaker
114
115-An oops was reported by the view so that the GoogleResponseError can be
116-tracked.
117-
118- >>> oops = search_view.request.oops
119- >>> oops.type
120- 'GoogleResponseError'
121- >>> oops.value
122- 'The response was incomplete, no xml.'
123-
124
125 SearchFormView and SearchFormPrimaryView
126 ----------------------------------------
127@@ -615,8 +606,7 @@
128 the first 20 items are None. Only the last 5 items are PageMatches.
129
130 >>> from lp.app.browser.root import WindowedList
131- >>> from lp.services.search.google import (
132- ... GoogleSearchService)
133+ >>> from lp.services.googlesearch import GoogleSearchService
134
135 >>> google_search = GoogleSearchService()
136 >>> page_matches = google_search.search(terms='bug', start=20)
137
138=== modified file 'lib/lp/services/configure.zcml'
139--- lib/lp/services/configure.zcml 2011-03-04 17:05:09 +0000
140+++ lib/lp/services/configure.zcml 2011-03-07 16:35:45 +0000
141@@ -8,6 +8,7 @@
142 <include package=".features" />
143 <include package=".fields" />
144 <include package=".geoip" />
145+ <include package=".googlesearch" />
146 <include package=".inlinehelp" file="meta.zcml" />
147 <include package=".job" />
148 <include package=".memcache" />
149@@ -15,6 +16,5 @@
150 <include package=".profile" />
151 <include package=".salesforce" />
152 <include package=".scripts" />
153- <include package=".search" />
154 <include package=".worlddata" />
155 </configure>
156
157=== renamed directory 'lib/lp/services/search' => 'lib/lp/services/googlesearch'
158=== renamed file 'lib/lp/services/search/google.py' => 'lib/lp/services/googlesearch/__init__.py'
159--- lib/lp/services/search/google.py 2011-03-04 17:05:09 +0000
160+++ lib/lp/services/googlesearch/__init__.py 2011-03-07 16:35:45 +0000
161@@ -15,6 +15,7 @@
162
163 import xml.etree.cElementTree as ET
164 import urllib
165+import urllib2
166 from urlparse import urlunparse
167
168 from lazr.restful.utils import get_current_browser_request
169@@ -22,7 +23,8 @@
170 from zope.interface import implements
171
172 from canonical.config import config
173-from lp.services.search.interfaces import (
174+from canonical.lazr.timeout import TimeoutError
175+from lp.services.googlesearch.interfaces import (
176 GoogleResponseError,
177 GoogleWrongGSPVersion,
178 ISearchResult,
179@@ -196,6 +198,11 @@
180 action = timeline.start("google-search-api", search_url)
181 try:
182 gsp_xml = urlfetch(search_url)
183+ except (TimeoutError, urllib2.HTTPError, urllib2.URLError), error:
184+ # Google search service errors are not code errors. Let the
185+ # call site choose to handle the unavailable service.
186+ raise GoogleResponseError(
187+ "The response errored: %s" % str(error))
188 finally:
189 action.finish()
190 page_matches = self._parse_google_search_protocol(gsp_xml)
191@@ -266,10 +273,10 @@
192 """
193 try:
194 gsp_doc = ET.fromstring(gsp_xml)
195- except SyntaxError:
196+ start_param = self._getElementByAttributeValue(
197+ gsp_doc, './PARAM', 'name', 'start')
198+ except (SyntaxError, IndexError):
199 raise GoogleResponseError("The response was incomplete, no xml.")
200- start_param = self._getElementByAttributeValue(
201- gsp_doc, './PARAM', 'name', 'start')
202 try:
203 start = int(start_param.get('value'))
204 except (AttributeError, ValueError):
205
206=== modified file 'lib/lp/services/googlesearch/configure.zcml'
207--- lib/lp/services/search/configure.zcml 2011-03-04 17:57:32 +0000
208+++ lib/lp/services/googlesearch/configure.zcml 2011-03-07 16:35:45 +0000
209@@ -6,18 +6,18 @@
210 xmlns="http://namespaces.zope.org/zope">
211
212 <class
213- class="lp.services.search.google.PageMatch">
214- <allow interface="lp.services.search.interfaces.ISearchResult" />
215+ class="lp.services.googlesearch.PageMatch">
216+ <allow interface="lp.services.googlesearch.interfaces.ISearchResult" />
217 </class>
218
219 <class
220- class="lp.services.search.google.PageMatches">
221- <allow interface="lp.services.search.interfaces.ISearchResults" />
222+ class="lp.services.googlesearch.PageMatches">
223+ <allow interface="lp.services.googlesearch.interfaces.ISearchResults" />
224 </class>
225
226 <securedutility
227- class="lp.services.search.google.GoogleSearchService"
228- provides="lp.services.search.interfaces.ISearchService">
229- <allow interface="lp.services.search.interfaces.ISearchService" />
230+ class="lp.services.googlesearch.GoogleSearchService"
231+ provides="lp.services.googlesearch.interfaces.ISearchService">
232+ <allow interface="lp.services.googlesearch.interfaces.ISearchService" />
233 </securedutility>
234 </configure>
235
236=== modified file 'lib/lp/services/googlesearch/doc/google-searchservice.txt'
237--- lib/lp/services/search/doc/google-searchservice.txt 2011-03-04 17:05:09 +0000
238+++ lib/lp/services/googlesearch/doc/google-searchservice.txt 2011-03-07 16:35:45 +0000
239@@ -14,7 +14,7 @@
240
241 >>> from zope.component import getUtility
242 >>> from zope.interface.verify import verifyObject
243- >>> from lp.services.search.interfaces import (
244+ >>> from lp.services.googlesearch.interfaces import (
245 ... ISearchService)
246
247 >>> google_search = getUtility(ISearchService)
248@@ -32,7 +32,7 @@
249 argument of start. The terms are the same as the text that would be
250 entered in Google search form; the terms should not be escaped.
251
252- >>> from lp.services.search.interfaces import (
253+ >>> from lp.services.googlesearch.interfaces import (
254 ... ISearchResults)
255
256 >>> first_page_matches = google_search.search(terms='bug')
257@@ -121,8 +121,8 @@
258 set. It is created by passing a title, url, and a summary. It is
259 an implementation of ISearchResult.
260
261- >>> from lp.services.search.interfaces import ISearchResult
262- >>> from lp.services.search.google import PageMatch
263+ >>> from lp.services.googlesearch.interfaces import ISearchResult
264+ >>> from lp.services.googlesearch import PageMatch
265
266 >>> page_match = PageMatch(
267 ... u'Unicode Titles in Launchpad',
268@@ -155,8 +155,7 @@
269 configuration may set a testing site.
270
271 >>> from canonical.config import config
272- >>> from lp.services.search.google import (
273- ... GoogleSearchService)
274+ >>> from lp.services.googlesearch import GoogleSearchService
275
276 >>> google_search = GoogleSearchService()
277 >>> config.google.site == google_search.site
278@@ -612,7 +611,7 @@
279 >>> google_search.search(terms='bug')
280 Traceback (most recent call last):
281 ...
282- TimeoutError: ...
283+ GoogleResponseError: ... timeout exceeded.
284
285 # Restore the configuration and the timeout state.
286 >>> timeout_data = config.pop('timeout_data')
287
288=== modified file 'lib/lp/services/googlesearch/doc/google-service-stub.txt.disabled'
289--- lib/lp/services/search/doc/google-service-stub.txt.disabled 2011-03-04 17:05:09 +0000
290+++ lib/lp/services/googlesearch/doc/google-service-stub.txt.disabled 2011-03-07 16:35:45 +0000
291@@ -14,7 +14,7 @@
292 determines which XML files will be returned when a specific URL is
293 accessed.
294
295- >>> from lp.services.search.googletestservice import (
296+ >>> from lp.services.googlesearch.googletestservice import (
297 ... url_to_xml_map)
298 >>> routes = url_to_xml_map()
299
300
301=== modified file 'lib/lp/services/googlesearch/tests/googleserviceharness.py'
302--- lib/lp/services/search/tests/googleserviceharness.py 2011-03-04 17:05:09 +0000
303+++ lib/lp/services/googlesearch/tests/googleserviceharness.py 2011-03-07 16:35:45 +0000
304@@ -14,7 +14,7 @@
305 import os
306 import signal
307
308-from lp.services.search import googletestservice
309+from lp.services.googlesearch import googletestservice
310
311
312 class GoogleServiceTestSetup:
313@@ -29,7 +29,7 @@
314 # google-service-stub.txt, is also disabled. See
315 # canonical/launchpad/ftests/test_system_documentation.py.
316 """
317- >>> from lp.services.search.googletestservice import (
318+ >>> from lp.services.googlesearch.googletestservice import (
319 ... service_is_available)
320 >>> from canonical.config import config
321
322
323=== added file 'lib/lp/services/googlesearch/tests/test_google.py'
324--- lib/lp/services/googlesearch/tests/test_google.py 1970-01-01 00:00:00 +0000
325+++ lib/lp/services/googlesearch/tests/test_google.py 2011-03-07 16:35:45 +0000
326@@ -0,0 +1,84 @@
327+# Copyright 2011 Canonical Ltd. This software is licensed under the
328+# GNU Affero General Public License version 3 (see the file LICENSE).
329+
330+"""Test the google search service."""
331+
332+__metaclass__ = type
333+
334+from contextlib import contextmanager
335+from urllib2 import (
336+ HTTPError,
337+ URLError,
338+ )
339+
340+from canonical.lazr.timeout import TimeoutError
341+from canonical.testing.layers import FunctionalLayer
342+from lp.services.googlesearch import GoogleSearchService
343+from lp.services.googlesearch.interfaces import GoogleResponseError
344+from lp.testing import TestCase
345+
346+
347+@contextmanager
348+def urlfetch_exception(test_error, *args):
349+ """Raise an error during the execution of urlfetch.
350+
351+ This function replaces urlfetch() with a function that
352+ raises an error.
353+ """
354+
355+ def raise_exception(url):
356+ raise test_error(*args)
357+
358+ from canonical.lazr import timeout
359+ original_urlfetch = timeout.urlfetch
360+ timeout.urlfetch = raise_exception
361+ try:
362+ yield
363+ finally:
364+ timeout.urlfetch = original_urlfetch
365+
366+
367+class TestGoogleSearchService(TestCase):
368+ """Test GoogleSearchService."""
369+
370+ layer = FunctionalLayer
371+
372+ def setUp(self):
373+ super(TestGoogleSearchService, self).setUp()
374+ self.search_service = GoogleSearchService()
375+
376+ def test_search_converts_HTTPError(self):
377+ # The method converts HTTPError to GoogleResponseError.
378+ args = ('url', 500, 'oops', {}, None)
379+ with urlfetch_exception(HTTPError, *args):
380+ self.assertRaises(
381+ GoogleResponseError, self.search_service.search, 'fnord')
382+
383+ def test_search_converts_URLError(self):
384+ # The method converts URLError to GoogleResponseError.
385+ with urlfetch_exception(URLError, 'oops'):
386+ self.assertRaises(
387+ GoogleResponseError, self.search_service.search, 'fnord')
388+
389+ def test_search_converts_TimeoutError(self):
390+ # The method converts TimeoutError to GoogleResponseError.
391+ with urlfetch_exception(TimeoutError, 'oops'):
392+ self.assertRaises(
393+ GoogleResponseError, self.search_service.search, 'fnord')
394+
395+ def test___parse_google_search_protocol_SyntaxError(self):
396+ # The method converts SyntaxError to GoogleResponseError.
397+ with urlfetch_exception(SyntaxError, 'oops'):
398+ self.assertRaises(
399+ GoogleResponseError,
400+ self.search_service._parse_google_search_protocol, '')
401+
402+ def test___parse_google_search_protocol_IndexError(self):
403+ # The method converts IndexError to GoogleResponseError.
404+ with urlfetch_exception(IndexError, 'oops'):
405+ data = (
406+ '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
407+ '<GSP VER="3.2"></GSP>')
408+ self.assertRaises(
409+ GoogleResponseError,
410+ self.search_service._parse_google_search_protocol, data)
411
412=== modified file 'lib/lp/services/googlesearch/tests/test_googleharness.py'
413--- lib/lp/services/search/tests/test_googleharness.py 2011-03-04 17:12:24 +0000
414+++ lib/lp/services/googlesearch/tests/test_googleharness.py 2011-03-07 16:35:45 +0000
415@@ -6,5 +6,5 @@
416
417 def test_suite():
418 return doctest.DocTestSuite(
419- 'lp.services.search.tests.googleserviceharness',
420+ 'lp.services.googlesearch.tests.googleserviceharness',
421 optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
422
423=== removed file 'lib/lp/services/search/__init__.py'