Merge lp:~maxiberta/launchpad/bing-search into lp:launchpad

Proposed by Maximiliano Bertacchini
Status: Merged
Merged at revision: 18598
Proposed branch: lp:~maxiberta/launchpad/bing-search
Merge into: lp:launchpad
Diff against target: 3016 lines (+2591/-22)
35 files modified
Makefile (+4/-3)
configs/development/launchpad-lazr.conf (+10/-1)
configs/testrunner-appserver/launchpad-lazr.conf (+3/-0)
configs/testrunner/launchpad-lazr.conf (+3/-0)
lib/lp/app/browser/doc/launchpad-search-pages-bing.txt (+725/-0)
lib/lp/app/browser/root.py (+9/-6)
lib/lp/app/browser/tests/test_views.py (+31/-2)
lib/lp/scripts/runlaunchpad.py (+16/-2)
lib/lp/services/config/schema-lazr.conf (+37/-1)
lib/lp/services/features/flags.py (+6/-0)
lib/lp/services/sitesearch/__init__.py (+161/-2)
lib/lp/services/sitesearch/bingtestservice.py (+79/-0)
lib/lp/services/sitesearch/configure.zcml (+10/-1)
lib/lp/services/sitesearch/doc/bing-searchservice.txt (+438/-0)
lib/lp/services/sitesearch/doc/google-searchservice.txt (+1/-1)
lib/lp/services/sitesearch/interfaces.py (+1/-1)
lib/lp/services/sitesearch/tests/bingserviceharness.py (+107/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json (+384/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json (+151/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json (+20/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json (+8/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json (+1/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt (+26/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json (+30/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json (+30/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json (+30/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json (+8/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json (+19/-0)
lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json (+12/-0)
lib/lp/services/sitesearch/tests/test_bing.py (+118/-0)
lib/lp/services/sitesearch/tests/test_bingharness.py (+10/-0)
lib/lp/services/sitesearch/tests/test_bingservice.py (+38/-0)
lib/lp/services/sitesearch/tests/test_doc.py (+5/-0)
lib/lp/testing/layers.py (+58/-2)
setup.py (+2/-0)
To merge this branch: bzr merge lp:~maxiberta/launchpad/bing-search
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+341549@code.launchpad.net

Commit message

Add basic Bing Custom Search site search support.

Description of the change

Add basic Bing Custom Search support.

Should be pretty unobtrusive, while adding a basic site search implementation around Bing Custom Search (shamelessly copied from the Google site search implementation). Might need some more testing; and could use some code deduplication.

Deployment notes:
- New config options: "bing.subscription_key" and "bing.custom_config_id".
- New feature flag "sitesearch.engine.name" should be set to "bing" (defaults to "google").

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This looks like a good start, thanks. This is just a first-pass review; as you say, it could use a good deal of code deduplication, probably in some number of preparatory branches, and I think it needs some thought about how we'll deploy the switch.

review: Needs Fixing
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Replied each comment inline. Most issues are now fixed. Still missing:
- Add feature flag to switch search engine.
- Code deduplication.
- Turn doctests into proper unit tests.
- Run tests on search page with both Google and Bing.

Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Updated branch:
- Register ISearchService implementations with name="google" and name="bing" (defaults to "google" for now; will add a feature flag next).
- Add extra doctests (sorry, copied from google implementation as we are in a rush; will migrate to proper unit tests later).

Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

I believe most issues from feedback are fixed now. Still pending: turn related doctests into unittests (and deduplicate lots of doctests, btw), which I'll work on in a following branch to unlock and hopefully land/deploy this branch ASAP.

Note this branch depends on https://code.launchpad.net/~maxiberta/launchpad/generalized-sitesearch-testservice/+merge/342312.

Revision history for this message
Colin Watson (cjwatson) wrote :

I think this is reasonable now. There's certainly more cleanup to be done, but since the deadline is upon us this should be good enough to get us started on switching.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2018-02-01 20:56:23 +0000
3+++ Makefile 2018-03-29 12:55:41 +0000
4@@ -61,6 +61,7 @@
5 # NB: It's important PIP_BIN only mentions things genuinely produced by pip.
6 PIP_BIN = \
7 $(PY) \
8+ bin/bingtestservice \
9 bin/build-twisted-plugin-cache \
10 bin/combine-css \
11 bin/googletestservice \
12@@ -284,7 +285,7 @@
13 bin/test -f $(TESTFLAGS) $(TESTOPTS)
14
15 run: build inplace stop
16- bin/run -r librarian,google-webservice,memcached,rabbitmq,txlongpoll \
17+ bin/run -r librarian,bing-webservice,google-webservice,memcached,rabbitmq,txlongpoll \
18 -i $(LPCONFIG)
19
20 run-testapp: LPCONFIG=testrunner-appserver
21@@ -297,12 +298,12 @@
22
23 start-gdb: build inplace stop support_files run.gdb
24 nohup gdb -x run.gdb --args bin/run -i $(LPCONFIG) \
25- -r librarian,google-webservice
26+ -r librarian,bing-webservice,google-webservice
27 > ${LPCONFIG}-nohup.out 2>&1 &
28
29 run_all: build inplace stop
30 bin/run \
31- -r librarian,sftp,forker,mailman,codebrowse,google-webservice,\
32+ -r librarian,sftp,forker,mailman,codebrowse,bing-webservice,google-webservice,\
33 memcached,rabbitmq,txlongpoll -i $(LPCONFIG)
34
35 run_codebrowse: compile
36
37=== modified file 'configs/development/launchpad-lazr.conf'
38--- configs/development/launchpad-lazr.conf 2018-02-02 15:29:38 +0000
39+++ configs/development/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
40@@ -79,13 +79,22 @@
41 error_dir: /var/tmp/lperr
42
43 [google]
44-# Development and the testrunner should use the stub service be default.
45+# Development and the testrunner should use the stub service by default.
46 site: http://launchpad.dev:8092/cse
47 client_id: ABCDEF2323
48
49 [google_test_service]
50 launch: True
51
52+[bing]
53+# Development and the testrunner should use the stub service by default.
54+site: http://launchpad.dev:8093/bingcustomsearch/v7.0/search
55+subscription_key: abcdef01234567890abcdef012345678
56+custom_config_id: 1234567890
57+
58+[bing_test_service]
59+launch: True
60+
61 [gpghandler]
62 host: keyserver.launchpad.dev
63 public_host: keyserver.launchpad.dev
64
65=== modified file 'configs/testrunner-appserver/launchpad-lazr.conf'
66--- configs/testrunner-appserver/launchpad-lazr.conf 2016-10-11 15:28:25 +0000
67+++ configs/testrunner-appserver/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
68@@ -14,6 +14,9 @@
69 [google_test_service]
70 launch: False
71
72+[bing_test_service]
73+launch: False
74+
75 [launchpad]
76 openid_provider_root: http://testopenid.dev:8085/
77
78
79=== modified file 'configs/testrunner/launchpad-lazr.conf'
80--- configs/testrunner/launchpad-lazr.conf 2018-01-26 22:18:38 +0000
81+++ configs/testrunner/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
82@@ -91,6 +91,9 @@
83 [google]
84 site: http://launchpad.dev:8092/cse
85
86+[bing]
87+site: http://launchpad.dev:8093/bingcustomsearch/v7.0/search
88+
89 [gpghandler]
90 upload_keys: True
91 host: localhost
92
93=== added file 'lib/lp/app/browser/doc/launchpad-search-pages-bing.txt'
94--- lib/lp/app/browser/doc/launchpad-search-pages-bing.txt 1970-01-01 00:00:00 +0000
95+++ lib/lp/app/browser/doc/launchpad-search-pages-bing.txt 2018-03-29 12:55:41 +0000
96@@ -0,0 +1,725 @@
97+Launchpad search page
98+=====================
99+
100+Users can search for Launchpad objects and pages from the search form
101+located on all pages. The search is performed and displayed by the
102+LaunchpadSearchView.
103+
104+ >>> from zope.component import getMultiAdapter, getUtility
105+ >>> from lp.services.webapp.interfaces import ILaunchpadRoot
106+ >>> from lp.services.webapp.servers import LaunchpadTestRequest
107+
108+ >>> root = getUtility(ILaunchpadRoot)
109+ >>> request = LaunchpadTestRequest()
110+ >>> search_view = getMultiAdapter((root, request), name="+search")
111+ >>> search_view.initialize()
112+ >>> search_view
113+ <....SimpleViewClass from .../templates/launchpad-search.pt ...>
114+
115+
116+Page title and heading
117+----------------------
118+
119+The page title and heading suggest to the user to search launchpad
120+when there is no search text.
121+
122+ >>> print search_view.text
123+ None
124+ >>> search_view.page_title
125+ 'Search Launchpad'
126+ >>> search_view.page_heading
127+ 'Search Launchpad'
128+
129+When text is not None, the title indicates what was searched.
130+
131+ >>> def getSearchView(form):
132+ ... search_param_list = []
133+ ... for name in sorted(form):
134+ ... value = form[name]
135+ ... search_param_list.append('%s=%s' % (name, value))
136+ ... query_string = '&'.join(search_param_list)
137+ ... request = LaunchpadTestRequest(
138+ ... SERVER_URL='https://launchpad.dev/+search',
139+ ... QUERY_STRING=query_string, form=form, PATH_INFO='/+search')
140+ ... search_view = getMultiAdapter((root, request), name="+search")
141+ ... search_view.initialize()
142+ ... return search_view
143+
144+ >>> search_view = getSearchView(
145+ ... form={'field.text': 'albatross'})
146+
147+ >>> search_view.text
148+ u'albatross'
149+ >>> search_view.page_title
150+ u'Pages matching "albatross" in Launchpad'
151+ >>> search_view.page_heading
152+ u'Pages matching "albatross" in Launchpad'
153+
154+
155+No matches
156+----------
157+
158+There were no matches for 'albatross'.
159+
160+ >>> search_view.has_matches
161+ False
162+
163+When search text is not submitted there are no matches. Search text is
164+required to perform a search. Note that field.actions.search is not a
165+required param to call the Search Action. The view always calls the
166+search action.
167+
168+ >>> search_view = getSearchView(form={})
169+
170+ >>> print search_view.text
171+ None
172+ >>> search_view.has_matches
173+ False
174+
175+
176+Bug and Question Searches
177+-------------------------
178+
179+When a numeric token can be extracted from the submitted search text,
180+the view tries to match a bug and question. Bugs and questions are
181+matched by their id.
182+
183+ >>> search_view = getSearchView(
184+ ... form={'field.text': '5'})
185+ >>> search_view._getNumericToken(search_view.text)
186+ u'5'
187+ >>> search_view.has_matches
188+ True
189+ >>> search_view.bug.title
190+ u'Firefox install instructions should be complete'
191+ >>> search_view.question.title
192+ u'Installation failed'
193+
194+Bugs and questions are matched independent of each other. The number
195+extracted may only match one kind of object. For example, there are
196+more bugs than questions.
197+
198+ >>> search_view = getSearchView(
199+ ... form={'field.text': '15'})
200+ >>> search_view._getNumericToken(search_view.text)
201+ u'15'
202+ >>> search_view.has_matches
203+ True
204+ >>> search_view.bug.title
205+ u'Nonsensical bugs are useless'
206+ >>> print search_view.question
207+ None
208+
209+Private bugs are not matched if the user does not have permission to
210+see them. For example, Sample Person can see a private bug that they
211+created because they are the owner.
212+
213+ >>> from lp.services.webapp.interfaces import ILaunchBag
214+ >>> from lp.app.enums import InformationType
215+
216+ >>> login('test@canonical.com')
217+ >>> sample_person = getUtility(ILaunchBag).user
218+ >>> private_bug = factory.makeBug(
219+ ... owner=sample_person, information_type=InformationType.USERDATA)
220+
221+ >>> search_view = getSearchView(
222+ ... form={'field.text': private_bug.id})
223+ >>> search_view.bug.private
224+ True
225+
226+But anonymous and unprivileged users cannot see the private bug.
227+
228+ >>> login(ANONYMOUS)
229+ >>> search_view = getSearchView(
230+ ... form={'field.text': private_bug.id})
231+ >>> print search_view.bug
232+ None
233+
234+The text and punctuation in the search text is ignored, and only the
235+first group of numbers is matched. For example a user searches for three
236+questions by number ('Question #15, #7, and 5.'). Only the first number
237+is used, and it matches a bug, not a question. The second and third
238+numbers do match questions, but they are not used.
239+
240+ >>> search_view = getSearchView(
241+ ... form={'field.text': 'Question #15, #7, and 5.'})
242+ >>> search_view._getNumericToken(search_view.text)
243+ u'15'
244+ >>> search_view.has_matches
245+ True
246+ >>> search_view.bug.title
247+ u'Nonsensical bugs are useless'
248+ >>> print search_view.question
249+ None
250+
251+It is not an error to search for a non-existent bug or question.
252+
253+ >>> search_view = getSearchView(
254+ ... form={'field.text': '55555'})
255+ >>> search_view._getNumericToken(search_view.text)
256+ u'55555'
257+ >>> search_view.has_matches
258+ False
259+ >>> print search_view.bug
260+ None
261+ >>> print search_view.question
262+ None
263+
264+There is no error if a number cannot be extracted from the search text.
265+
266+ >>> search_view = getSearchView(
267+ ... form={'field.text': 'fifteen'})
268+ >>> print search_view._getNumericToken(
269+ ... search_view.text)
270+ None
271+ >>> search_view.has_matches
272+ False
273+ >>> print search_view.bug
274+ None
275+ >>> print search_view.question
276+ None
277+
278+Bugs and questions are only returned for the first page of search,
279+when the start param is 0.
280+
281+ >>> search_view = getSearchView(
282+ ... form={'field.text': '5',
283+ ... 'start': '20'})
284+ >>> search_view.has_matches
285+ False
286+ >>> print search_view.bug
287+ None
288+ >>> print search_view.question
289+ None
290+
291+
292+
293+Projects and Persons and Teams searches
294+---------------------------------------
295+
296+When a Launchpad name can be made from the search text, the view tries
297+to match the name to a pillar or person. a pillar is a distribution,
298+product, or project group. A person is a person or a team.
299+
300+ >>> search_view = getSearchView(
301+ ... form={'field.text': 'launchpad'})
302+ >>> search_view._getNameToken(search_view.text)
303+ u'launchpad'
304+ >>> search_view.has_matches
305+ True
306+ >>> search_view.pillar.displayname
307+ u'Launchpad'
308+ >>> search_view.person_or_team.displayname
309+ u'Launchpad Developers'
310+
311+A launchpad name is constructed from the search text. The letters are
312+converted to lowercase. groups of spaces and punctuation are replaced
313+with a hyphen.
314+
315+ >>> search_view = getSearchView(
316+ ... form={'field.text': 'Gnome Terminal'})
317+ >>> search_view._getNameToken(search_view.text)
318+ u'gnome-terminal'
319+ >>> search_view.has_matches
320+ True
321+ >>> search_view.pillar.displayname
322+ u'GNOME Terminal'
323+ >>> print search_view.person_or_team
324+ None
325+
326+Since our pillars can have aliases, it's also possible to look up a pillar
327+by any of its aliases.
328+
329+ >>> from lp.registry.interfaces.product import IProductSet
330+ >>> firefox = getUtility(IProductSet)['firefox']
331+ >>> login('foo.bar@canonical.com')
332+ >>> firefox.setAliases(['iceweasel'])
333+ >>> login(ANONYMOUS)
334+ >>> search_view = getSearchView(
335+ ... form={'field.text': 'iceweasel'})
336+ >>> search_view._getNameToken(search_view.text)
337+ u'iceweasel'
338+ >>> search_view.has_matches
339+ True
340+ >>> search_view.pillar.displayname
341+ u'Mozilla Firefox'
342+
343+This is a harder example that illustrates that text that is clearly not
344+the name of a pillar will none-the-less be tried. See the `Page searches`
345+section for how this kind of search can return matches.
346+
347+ >>> search_view = getSearchView(
348+ ... form={'field.text': "YAHOO! webservice's Python API."})
349+ >>> search_view._getNameToken(search_view.text)
350+ u'yahoo-webservices-python-api.'
351+ >>> search_view.has_matches
352+ False
353+ >>> print search_view.pillar
354+ None
355+ >>> print search_view.person_or_team
356+ None
357+
358+Leading and trailing punctuation and whitespace are stripped.
359+
360+ >>> search_view = getSearchView(
361+ ... form={'field.text': "~name12"})
362+ >>> search_view._getNameToken(search_view.text)
363+ u'name12'
364+ >>> search_view.has_matches
365+ True
366+ >>> print search_view.pillar
367+ None
368+ >>> search_view.person_or_team.displayname
369+ u'Sample Person'
370+
371+Pillars, persons and teams are only returned for the first page of
372+search, when the start param is 0.
373+
374+ >>> search_view = getSearchView(
375+ ... form={'field.text': 'launchpad',
376+ ... 'start': '20'})
377+ >>> search_view.has_matches
378+ True
379+ >>> print search_view.bug
380+ None
381+ >>> print search_view.question
382+ None
383+ >>> print search_view.pillar
384+ None
385+
386+Deactivated pillars and non-valid persons and teams cannot be exact
387+matches. For example, the python-gnome2-dev product will not match a
388+pillar, nor will nsv match Nicolas Velin's unclaimed account.
389+
390+ >>> from lp.registry.interfaces.person import IPersonSet
391+
392+ >>> python_gnome2 = getUtility(IProductSet).getByName('python-gnome2-dev')
393+ >>> python_gnome2.active
394+ False
395+
396+ >>> search_view = getSearchView(
397+ ... form={'field.text': 'python-gnome2-dev',
398+ ... 'start': '0'})
399+ >>> search_view._getNameToken(search_view.text)
400+ u'python-gnome2-dev'
401+ >>> print search_view.pillar
402+ None
403+
404+ >>> nsv = getUtility(IPersonSet).getByName('nsv')
405+ >>> nsv.displayname
406+ u'Nicolas Velin'
407+ >>> nsv.is_valid_person_or_team
408+ False
409+
410+ >>> search_view = getSearchView(
411+ ... form={'field.text': 'nsv',
412+ ... 'start': '0'})
413+ >>> search_view._getNameToken(search_view.text)
414+ u'nsv'
415+ >>> print search_view.person_or_team
416+ None
417+
418+Private pillars are not matched if the user does not have permission to see
419+them. For example, Sample Person can see a private project that they created
420+because they are the owner.
421+
422+ >>> from lp.registry.interfaces.product import License
423+
424+ >>> login('test@canonical.com')
425+ >>> private_product = factory.makeProduct(
426+ ... owner=sample_person, information_type=InformationType.PROPRIETARY,
427+ ... licenses=[License.OTHER_PROPRIETARY])
428+ >>> private_product_name = private_product.name
429+
430+ >>> search_view = getSearchView(form={'field.text': private_product_name})
431+ >>> search_view.pillar.private
432+ True
433+
434+But anonymous and unprivileged users cannot see the private project.
435+
436+ >>> login(ANONYMOUS)
437+ >>> search_view = getSearchView(form={'field.text': private_product_name})
438+ >>> print search_view.pillar
439+ None
440+
441+
442+Shipit CD searches
443+------------------
444+
445+The has_shipit property will be True when the search looks like the user
446+is searching for Shipit CDs. There is no correct object in Launchpad to
447+display. The page template decides how to handle when has_shipit is
448+True.
449+
450+The match is based on an intersection to the words in the search text
451+and the shipit_keywords. The comparison is case-insensitive, has_shipit
452+is True when 2 or more words match.
453+
454+ >>> sorted(search_view.shipit_keywords)
455+ ['cd', 'cds', 'disc', 'dvd', 'dvds', 'edubuntu', 'free', 'get', 'kubuntu',
456+ 'mail', 'send', 'ship', 'shipit', 'ubuntu']
457+ >>> search_view = getSearchView(
458+ ... form={'field.text': 'ubuntu CDs',
459+ ... 'start': '0'})
460+ >>> search_view.has_shipit
461+ True
462+
463+ >>> search_view = getSearchView(
464+ ... form={'field.text': 'shipit',
465+ ... 'start': '0'})
466+ >>> search_view.has_shipit
467+ False
468+
469+ >>> search_view = getSearchView(
470+ ... form={'field.text': 'get Kubuntu cds',
471+ ... 'start': '0'})
472+ >>> search_view.has_shipit
473+ True
474+
475+There are shipit_anti_keywords too, words that indicate the search is
476+not for free CDs from Shipit. Search that have any of these word will
477+set has_shipit to False.
478+
479+ >>> sorted(search_view.shipit_anti_keywords)
480+ ['burn', 'burning', 'enable', 'error', 'errors', 'image', 'iso',
481+ 'read', 'rip', 'write']
482+
483+ >>> search_view = getSearchView(
484+ ... form={'field.text': 'ubuntu CD write',
485+ ... 'start': '0'})
486+ >>> search_view.has_shipit
487+ False
488+
489+ >>> search_view = getSearchView(
490+ ... form={'field.text': 'shipit error',
491+ ... 'start': '0'})
492+ >>> search_view.has_shipit
493+ False
494+
495+
496+The shipit FAQ URL is provides by the view for the template to use.
497+
498+ >>> search_view.shipit_faq_url
499+ 'http://www.ubuntu.com/getubuntu/shipit-faq'
500+
501+
502+Page searches
503+-------------
504+
505+The view uses the BingSearchService to locate pages that match the
506+search terms.
507+
508+ >>> search_view = getSearchView(
509+ ... form={'field.text': " bug"})
510+ >>> search_view.text
511+ u'bug'
512+ >>> search_view.has_matches
513+ True
514+ >>> search_view.pages
515+ <...SiteSearchBatchNavigator ...>
516+
517+The BingSearchService may not be available due to connectivity problems.
518+The view's has_page_service attribute reports when the search was performed
519+with Bing page matches.
520+
521+ >>> search_view.has_page_service
522+ True
523+
524+The batch navigation heading is created by the view. The heading
525+property returns a 2-tuple of singular and plural heading. There
526+is a heading when there are only Bing page matches...
527+
528+ >>> search_view.has_exact_matches
529+ False
530+ >>> search_view.batch_heading
531+ (u'page matching "bug"', u'pages matching "bug"')
532+
533+...and a heading for when there are exact matches and Bing page
534+matches.
535+
536+ >>> search_view = getSearchView(
537+ ... form={'field.text': " launchpad"})
538+ >>> search_view.has_exact_matches
539+ True
540+ >>> search_view.batch_heading
541+ (u'other page matching "launchpad"', u'other pages matching "launchpad"')
542+
543+The SiteSearchBatchNavigator behaves like most BatchNavigators, except that
544+its batch size is always 20. The size restriction conforms to Google's
545+maximum number of results that can be returned per request.
546+
547+ >>> search_view.start
548+ 0
549+ >>> search_view.pages.currentBatch().size
550+ 20
551+ >>> pages = list(search_view.pages.currentBatch())
552+ >>> len(pages)
553+ 20
554+ >>> for page in pages[0:5]:
555+ ... page.title
556+ u'Launchpad Bugs'
557+ u'Bugs in Ubuntu Linux'
558+ u'Bugs related to Sample Person'
559+ u'Bug #1 in Mozilla Firefox: Firefox does not support SVG'
560+ u'Question #232632 : Questions : OpenStack Heat'
561+
562+The batch navigator provides access to the other batches. There are two
563+batches of pages that match the search text 'bugs'. The navigator
564+provides a link to the next batch, which also happens to be the last
565+batch.
566+
567+ >>> search_view.pages.nextBatchURL()
568+ '...start=20'
569+ >>> search_view.pages.lastBatchURL()
570+ '...start=20'
571+
572+The second batch has only five matches in it, even though the batch size
573+is 20. That is because there were only 25 matching pages.
574+
575+ >>> search_view = getSearchView(
576+ ... form={'field.text': "bug",
577+ ... 'start': '20'})
578+ >>> search_view.start
579+ 20
580+ >>> search_view.text
581+ u'bug'
582+ >>> search_view.has_matches
583+ True
584+
585+ >>> search_view.pages.currentBatch().size
586+ 20
587+ >>> pages = list(search_view.pages.currentBatch())
588+ >>> len(pages)
589+ 5
590+ >>> for page in pages:
591+ ... page.title
592+ u'Bugs - Launchpad Help'
593+ u'Of Bugs and Statuses - Launchpad Blog'
594+ u'Mahara 1.8.0'
595+ u'Mighty Box in Launchpad'
596+ u'Bug tracking - Launchpad Bugs'
597+
598+ >>> search_view.pages.nextBatchURL()
599+ ''
600+ >>> search_view.pages.lastBatchURL()
601+ ''
602+
603+The PageMatch object has a title, url, and summary. The title and url
604+are used for making links to the pages. The summary contains markup
605+showing the matching terms in context of the page text.
606+
607+ >>> print range(20)
608+ [0, 1, ..., 18, 19]
609+ >>> page = pages[0]
610+ >>> page
611+ <...PageMatch ...>
612+ >>> page.title
613+ u'Bugs - Launchpad Help'
614+ >>> page.url
615+ 'https://help.launchpad.net/Bugs'
616+ >>> page.summary # doctest: +ELLIPSIS
617+ u"Launchpad Help > Bugs . Use Launchpad's bug tracker for your project..."
618+
619+See `google-searchservice.txt` for more information about the
620+BingSearchService and PageMatch objects.
621+
622+
623+No page matches
624+---------------
625+
626+When an empty PageMatches object is returned by the BingSearchService to
627+the view, there are no matches to show.
628+
629+ >>> search_view = getSearchView(form={'field.text': 'no-meaningful'})
630+ >>> search_view.has_matches
631+ False
632+
633+
634+Unintelligible searches
635+-----------------------
636+
637+When a user searches for a malformed string, we don't OOPS, but show an
638+error. Also disable warnings, since we are tossing around malformed Unicode.
639+
640+ >>> import warnings
641+ >>> with warnings.catch_warnings():
642+ ... warnings.simplefilter('ignore')
643+ ... search_view = getSearchView(
644+ ... form={'field.text': '\xfe\xfckr\xfc'})
645+ >>> html = search_view()
646+ >>> 'Can not convert your search term' in html
647+ True
648+
649+
650+Bad Bing response handling
651+----------------------------
652+
653+Connectivity problems can cause missing or incomplete responses from
654+Bing. The LaunchpadSearchView will display the other searches and
655+show a message explaining that the user can search again to find
656+matching pages.
657+
658+ >>> search_view = getSearchView(form={'field.text': 'gnomebaker'})
659+ >>> search_view.has_matches
660+ True
661+ >>> search_view.pillar.displayname
662+ u'gnomebaker'
663+ >>> search_view.has_page_service
664+ False
665+
666+The view provides the requested URL so that the template can make a
667+link to try the search again
668+
669+ >>> print search_view.url
670+ https://launchpad.dev/+search?field.text=gnomebaker
671+
672+
673+SearchFormView and SearchFormPrimaryView
674+----------------------------------------
675+
676+Two companion views are used to help render the global search form.
677+They define the required attributes to render the form in the
678+correct state.
679+
680+The LaunchpadSearchFormView provides the minimum information to display
681+the form, but cannot handled the submitted data. It appends a suffix
682+('-secondary') to the id= and name= of the form and inputs, to prevent
683+them from conflicting with the other form. The search text is not the
684+default value of the text field; 'bug' was submitted above, but is not
685+present in the rendered form.
686+
687+ >>> search_form_view = getMultiAdapter(
688+ ... (search_view, request), name='+search-form')
689+ >>> search_form_view.initialize()
690+ >>> search_form_view.id_suffix
691+ '-secondary'
692+ >>> print search_form_view.render()
693+ <form action="http://launchpad.dev/+search" method="get"
694+ accept-charset="UTF-8" id="sitesearch-secondary"
695+ name="sitesearch-secondary">
696+ <div>
697+ <input class="textType" type="text" size="36"
698+ id="field.text-secondary" name="field.text" />
699+ <input class="button" type="submit" value="Search"
700+ id="field.text-secondary" name="field.actions.search-secondary" />
701+ </div>
702+ </form>
703+
704+LaunchpadPrimarySearchFormView can handle submitted form by deferring to
705+its context (the LaunchpadSearchView) for the needed information. The
706+view does not append a suffix to the form and input ids. The search
707+field's value is 'bug', as was submitted above.
708+
709+ >>> search_form_view = getMultiAdapter(
710+ ... (search_view, request), name='+primary-search-form')
711+ >>> search_form_view.initialize()
712+ >>> search_form_view.id_suffix
713+ ''
714+ >>> print search_form_view.render()
715+ <form action="http://launchpad.dev/+search" method="get"
716+ accept-charset="UTF-8" id="sitesearch"
717+ name="sitesearch">
718+ <div>
719+ <input class="textType" type="text" size="36"
720+ id="field.text" value="gnomebaker" name="field.text" />
721+ <input class="button" type="submit" value="Search"
722+ id="field.text" name="field.actions.search" />
723+ </div>
724+ </form>
725+
726+WindowedList and SiteSearchBatchNavigator
727+-------------------------------------
728+
729+The LaunchpadSearchView uses two helper classes to work with
730+PageMatches.
731+
732+The PageMatches object returned by the BingSearchService contains 20
733+or fewer PageMatches of what could be thousands of matches. Bing
734+requires client's to make repeats request to step though the batches of
735+matches. The Windowed list is a list that contains only a subset of its
736+reported size. It is used to make batches in the SiteSearchBatchNavigator.
737+
738+For example, the last batch of the 'bug' search contained 5 of the 25
739+matching pages. The WindowList claims to be 25 items in length, but
740+the first 20 items are None. Only the last 5 items are PageMatches.
741+
742+ >>> from lp.app.browser.root import WindowedList
743+ >>> from lp.services.sitesearch import BingSearchService
744+
745+ >>> bing_search = BingSearchService()
746+ >>> page_matches = bing_search.search(terms='bug', start=20)
747+ >>> results = WindowedList(
748+ ... page_matches, page_matches.start, page_matches.total)
749+ >>> len(results)
750+ 25
751+ >>> print results[0]
752+ None
753+ >>> results[24].title
754+ u'Bug tracking - Launchpad Bugs'
755+ >>> results[18, 22]
756+ [None, None, <...PageMatch ...>, <...PageMatch ...>]
757+
758+The SiteSearchBatchNavigator restricts the batch size to 20. the 'batch'
759+parameter that comes from the URL is ignored. For example, setting
760+the 'batch' parameter to 100 has no affect upon the Bing search
761+or on the navigator object.
762+
763+ >>> from lp.app.browser.root import SiteSearchBatchNavigator
764+
765+ >>> SiteSearchBatchNavigator.batch_variable_name
766+ 'batch'
767+
768+ >>> search_view = getSearchView(
769+ ... form={'field.text': "bug",
770+ ... 'start': '0',
771+ ... 'batch': '100',})
772+
773+ >>> navigator = search_view.pages
774+ >>> navigator.currentBatch().size
775+ 20
776+ >>> len(navigator.currentBatch())
777+ 20
778+ >>> navigator.nextBatchURL()
779+ '...start=20'
780+
781+Even if the PageMatch object to have an impossibly large size, the
782+navigator conforms to Google's maximum size of 20.
783+
784+ >>> matches = list(range(0, 100))
785+ >>> page_matches._matches = matches
786+ >>> page_matches.start = 0
787+ >>> page_matches.total = 100
788+ >>> navigator = SiteSearchBatchNavigator(
789+ ... page_matches, search_view.request, page_matches.start, size=100)
790+ >>> navigator.currentBatch().size
791+ 20
792+ >>> len(navigator.currentBatch())
793+ 20
794+ >>> navigator.nextBatchURL()
795+ '...start=20'
796+
797+The PageMatches object can be smaller than 20, for instance, pages
798+without titles are skipped when parsing the Bing Search JSON. The size
799+of the batch is still 20, but when the items in the batch are iterated,
800+the true size can be seen. For example there could be only 3 matches in
801+the PageMatches object, so only 3 are yielded. The start of the next
802+batch is 20, which is the start of the next batch from Bing.
803+
804+ >>> matches = list(range(0, 3))
805+ >>> page_matches._matches = matches
806+ >>> navigator = SiteSearchBatchNavigator(
807+ ... page_matches, search_view.request, page_matches.start, size=100)
808+ >>> batch = navigator.currentBatch()
809+ >>> batch.size
810+ 20
811+ >>> len(batch)
812+ 20
813+ >>> batch.endNumber()
814+ 3
815+ >>> for item in batch:
816+ ... print item
817+ 0
818+ 1
819+ 2
820+ >>> navigator.nextBatchURL()
821+ '...start=20'
822
823=== modified file 'lib/lp/app/browser/root.py'
824--- lib/lp/app/browser/root.py 2018-03-27 14:31:36 +0000
825+++ lib/lp/app/browser/root.py 2018-03-29 12:55:41 +0000
826@@ -1,4 +1,4 @@
827-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
828+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
829 # GNU Affero General Public License version 3 (see the file LICENSE).
830 """Browser code for the Launchpad root page."""
831
832@@ -510,18 +510,21 @@
833 def searchPages(self, query_terms, start=0):
834 """Return the up to 20 pages that match the query_terms, or None.
835
836- :param query_terms: The unescaped terms to query Google.
837+ :param query_terms: The unescaped terms to query for.
838 :param start: The index of the page that starts the set of pages.
839- :return: A GooglBatchNavigator or None.
840+ :return: A SiteSearchBatchNavigator or None.
841 """
842 if query_terms in [None, '']:
843 return None
844- google_search = getUtility(ISearchService)
845+ search_engine = getFeatureFlag("sitesearch.engine.name")
846+ # Default to the Google search engine.
847+ search_engine = search_engine or "google"
848+ site_search = getUtility(ISearchService, name=search_engine)
849 try:
850- page_matches = google_search.search(
851+ page_matches = site_search.search(
852 terms=query_terms, start=start)
853 except SiteSearchResponseError:
854- # There was a connectivity or Google service issue that means
855+ # There was a connectivity or search service issue that means
856 # there is no data available at this moment.
857 self.has_page_service = False
858 return None
859
860=== modified file 'lib/lp/app/browser/tests/test_views.py'
861--- lib/lp/app/browser/tests/test_views.py 2018-03-28 19:23:18 +0000
862+++ lib/lp/app/browser/tests/test_views.py 2018-03-29 12:55:41 +0000
863@@ -1,4 +1,4 @@
864-# Copyright 2009 Canonical Ltd. This software is licensed under the
865+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
866 # GNU Affero General Public License version 3 (see the file LICENSE).
867
868 """
869@@ -11,6 +11,7 @@
870 from lp.services.features.testing import FeatureFixture
871 from lp.services.testing import build_test_suite
872 from lp.testing.layers import (
873+ BingLaunchpadFunctionalLayer,
874 GoogleLaunchpadFunctionalLayer,
875 )
876 from lp.testing.systemdocs import (
877@@ -21,14 +22,42 @@
878
879
880 here = os.path.dirname(os.path.realpath(__file__))
881+bing_flag = FeatureFixture({'sitesearch.engine.name': 'bing'})
882+google_flag = FeatureFixture({'sitesearch.engine.name': 'google'})
883+
884+
885+def setUp_bing(test):
886+ setUp(test)
887+ bing_flag.setUp()
888+
889+
890+def setUp_google(test):
891+ setUp(test)
892+ google_flag.setUp()
893+
894+
895+def tearDown_bing(test):
896+ bing_flag.cleanUp()
897+ tearDown(test)
898+
899+
900+def tearDown_google(test):
901+ google_flag.cleanUp()
902+ tearDown(test)
903+
904
905 # The default layer of view tests is the DatabaseFunctionalLayer. Tests
906 # that require something special like the librarian or mailman must run
907 # on a layer that sets those services up.
908 special = {
909+ 'launchpad-search-pages-bing.txt': LayeredDocFileSuite(
910+ '../doc/launchpad-search-pages-bing.txt',
911+ setUp=setUp_bing, tearDown=tearDown_bing,
912+ layer=BingLaunchpadFunctionalLayer,
913+ stdout_logging_level=logging.WARNING),
914 'launchpad-search-pages-google.txt': LayeredDocFileSuite(
915 '../doc/launchpad-search-pages-google.txt',
916- setUp=setUp, tearDown=tearDown,
917+ setUp=setUp_google, tearDown=tearDown_google,
918 layer=GoogleLaunchpadFunctionalLayer,
919 stdout_logging_level=logging.WARNING),
920 }
921
922=== modified file 'lib/lp/scripts/runlaunchpad.py'
923--- lib/lp/scripts/runlaunchpad.py 2018-03-16 14:50:01 +0000
924+++ lib/lp/scripts/runlaunchpad.py 2018-03-29 12:55:41 +0000
925@@ -1,4 +1,4 @@
926-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
927+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
928 # GNU Affero General Public License version 3 (see the file LICENSE).
929
930 __metaclass__ = type
931@@ -26,7 +26,10 @@
932 pidfile_path,
933 )
934 from lp.services.rabbit.server import RabbitServer
935-from lp.services.sitesearch import googletestservice
936+from lp.services.sitesearch import (
937+ bingtestservice,
938+ googletestservice,
939+ )
940 from lp.services.txlongpoll.server import TxLongPollServer
941
942
943@@ -153,6 +156,16 @@
944 self.addCleanup(stop_process, googletestservice.start_as_process())
945
946
947+class BingWebService(Service):
948+
949+ @property
950+ def should_launch(self):
951+ return config.bing_test_service.launch
952+
953+ def launch(self):
954+ self.addCleanup(stop_process, bingtestservice.start_as_process())
955+
956+
957 class MemcachedService(Service):
958 """A local memcached service for developer environments."""
959
960@@ -280,6 +293,7 @@
961 'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),
962 'forker': ForkingSessionService(),
963 'mailman': MailmanService(),
964+ 'bing-webservice': BingWebService(),
965 'codebrowse': CodebrowseService(),
966 'google-webservice': GoogleWebService(),
967 'memcached': MemcachedService(),
968
969=== modified file 'lib/lp/services/config/schema-lazr.conf'
970--- lib/lp/services/config/schema-lazr.conf 2018-03-16 14:02:16 +0000
971+++ lib/lp/services/config/schema-lazr.conf 2018-03-29 12:55:41 +0000
972@@ -791,7 +791,43 @@
973 # url_rewrite_exceptions is a list of launchpad.net domains that must
974 # not be rewritten.
975 # datatype: string of space separated domains
976-# Example: help.launchpad.net login.launchapd.net
977+# Example: help.launchpad.net login.launchpad.net
978+url_rewrite_exceptions: help.launchpad.net
979+
980+[bing_test_service]
981+# Run a web service stub that simulates the Bing search service.
982+
983+# Where are our canned JSON responses stored?
984+canned_response_directory: lib/lp/services/sitesearch/tests/data/
985+
986+# Which file maps service URLs to the JSON that the server returns?
987+mapfile: lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt
988+
989+# Where should the service log files live?
990+log: logs/bing-stub.log
991+
992+# Do we actually want to run the service?
993+launch: False
994+
995+[bing]
996+# site is the host and path that search requests are made to.
997+# eg. https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search
998+# datatype: string, a url to a host
999+site: https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search
1000+
1001+# subscription_key is the Cognitive Services subscription key for
1002+# Bing Custom Search API.
1003+# datatype: string
1004+subscription_key:
1005+
1006+# custom_config_id is the id that identifies the custom search instance.
1007+# datatype: string
1008+custom_config_id:
1009+
1010+# url_rewrite_exceptions is a list of launchpad.net domains that must
1011+# not be rewritten.
1012+# datatype: string of space separated domains
1013+# Example: help.launchpad.net login.launchpad.net
1014 url_rewrite_exceptions: help.launchpad.net
1015
1016 [gpghandler]
1017
1018=== modified file 'lib/lp/services/features/flags.py'
1019--- lib/lp/services/features/flags.py 2016-10-14 16:16:18 +0000
1020+++ lib/lp/services/features/flags.py 2018-03-29 12:55:41 +0000
1021@@ -234,6 +234,12 @@
1022 'disabled',
1023 'Named authorization tokens for archives',
1024 ''),
1025+ ('sitesearch.engine.name',
1026+ 'space delimited',
1027+ 'Name of the site search engine backend ("google" or "bing").',
1028+ 'google',
1029+ 'Site search engine',
1030+ ''),
1031 ])
1032
1033 # The set of all flag names that are documented.
1034
1035=== modified file 'lib/lp/services/sitesearch/__init__.py'
1036--- lib/lp/services/sitesearch/__init__.py 2018-03-27 15:47:35 +0000
1037+++ lib/lp/services/sitesearch/__init__.py 2018-03-29 12:55:41 +0000
1038@@ -1,4 +1,4 @@
1039-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
1040+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
1041 # GNU Affero General Public License version 3 (see the file LICENSE).
1042
1043 """Interfaces for searching and working with results."""
1044@@ -6,11 +6,13 @@
1045 __metaclass__ = type
1046
1047 __all__ = [
1048+ 'BingSearchService',
1049 'GoogleSearchService',
1050 'PageMatch',
1051 'PageMatches',
1052 ]
1053
1054+import json
1055 import urllib
1056 from urlparse import (
1057 parse_qsl,
1058@@ -163,7 +165,7 @@
1059 class GoogleSearchService:
1060 """See `ISearchService`.
1061
1062- A search service that search Google for launchpad.net pages.
1063+ A search service that searches Google for launchpad.net pages.
1064 """
1065
1066 _default_values = {
1067@@ -333,3 +335,160 @@
1068 raise GoogleWrongGSPVersion(
1069 "Could not get any PageMatches from the GSP XML response.")
1070 return PageMatches(page_matches, start, total)
1071+
1072+
1073+@implementer(ISearchService)
1074+class BingSearchService:
1075+ """See `ISearchService`.
1076+
1077+ A search service that searches Bing for launchpad.net pages.
1078+ """
1079+
1080+ _default_values = {
1081+ # XXX: maxiberta 2018-03-26: Set `mkt` based on the current request.
1082+ 'customConfig': None,
1083+ 'mkt': 'en-US',
1084+ 'count': 20,
1085+ 'offset': 0,
1086+ 'q': None,
1087+ }
1088+
1089+ @property
1090+ def subscription_key(self):
1091+ """The subscription key issued by Bing Custom Search."""
1092+ return config.bing.subscription_key
1093+
1094+ @property
1095+ def custom_config_id(self):
1096+ """The custom search instance as configured in Bing Custom Search."""
1097+ return config.bing.custom_config_id
1098+
1099+ @property
1100+ def site(self):
1101+ """The URL to the Bing Custom Search service.
1102+
1103+ The URL is probably
1104+ https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search.
1105+ """
1106+ return config.bing.site
1107+
1108+ def search(self, terms, start=0):
1109+ """See `ISearchService`.
1110+
1111+ The `subscription_key` and `custom_config_id` are used in the
1112+ search request. Search returns 20 or fewer results for each query.
1113+ For terms that match more than 20 results, the start param can be
1114+ used over multiple queries to get successive sets of results.
1115+
1116+ :return: `ISearchResults` (PageMatches).
1117+ :raise: `SiteSearchResponseError` if the json response is incomplete or
1118+ cannot be parsed.
1119+ """
1120+ search_url = self.create_search_url(terms, start=start)
1121+ search_headers = self.create_search_headers()
1122+ request = get_current_browser_request()
1123+ timeline = get_request_timeline(request)
1124+ action = timeline.start("bing-search-api", search_url)
1125+ try:
1126+ response = urlfetch(search_url, headers=search_headers)
1127+ except (TimeoutError, requests.RequestException) as error:
1128+ raise SiteSearchResponseError(
1129+ "The response errored: %s" % str(error))
1130+ finally:
1131+ action.finish()
1132+ page_matches = self._parse_bing_response(response.content, start)
1133+ return page_matches
1134+
1135+ def _checkParameter(self, name, value, is_int=False):
1136+ """Check that a parameter value is not None or an empty string."""
1137+ if value in (None, ''):
1138+ raise ValueError("Missing value for parameter '%s'." % name)
1139+ if is_int:
1140+ try:
1141+ int(value)
1142+ except ValueError:
1143+ raise ValueError(
1144+ "Value for parameter '%s' is not an int." % name)
1145+
1146+ def create_search_url(self, terms, start=0):
1147+ """Return a Bing Custom Search search url."""
1148+ self._checkParameter('q', terms)
1149+ self._checkParameter('offset', start, is_int=True)
1150+ self._checkParameter('customConfig', self.custom_config_id)
1151+ search_params = dict(self._default_values)
1152+ search_params['q'] = terms.encode('utf8')
1153+ search_params['offset'] = start
1154+ search_params['customConfig'] = self.custom_config_id
1155+ query_string = urllib.urlencode(sorted(search_params.items()))
1156+ return self.site + '?' + query_string
1157+
1158+ def create_search_headers(self):
1159+ """Return a dict with Bing Custom Search compatible request headers."""
1160+ self._checkParameter('subscription_key', self.subscription_key)
1161+ return {
1162+ 'Ocp-Apim-Subscription-Key': self.subscription_key,
1163+ }
1164+
1165+ def _parse_bing_response(self, bing_json, start=0):
1166+ """Return a `PageMatches` object.
1167+
1168+ :param bing_json: A string containing Bing Custom Search API v7 JSON.
1169+ :return: `ISearchResults` (PageMatches).
1170+ :raise: `SiteSearchResponseError` if the json response is incomplete or
1171+ cannot be parsed.
1172+ """
1173+ try:
1174+ bing_doc = json.loads(bing_json)
1175+ except (TypeError, ValueError):
1176+ raise SiteSearchResponseError(
1177+ "The response was incomplete, no JSON.")
1178+
1179+ try:
1180+ response_type = bing_doc['_type']
1181+ except (AttributeError, KeyError, ValueError):
1182+ raise SiteSearchResponseError(
1183+ "Could not get the '_type' from the Bing JSON response.")
1184+
1185+ if response_type == 'ErrorResponse':
1186+ try:
1187+ errors = [error['message'] for error in bing_doc['errors']]
1188+ raise SiteSearchResponseError(
1189+ "Error response from Bing: %s" % '; '.join(errors))
1190+ except (AttributeError, KeyError, TypeError, ValueError):
1191+ raise SiteSearchResponseError(
1192+ "Unable to parse the Bing JSON error response.")
1193+ elif response_type != 'SearchResponse':
1194+ raise SiteSearchResponseError(
1195+ "Unknown Bing JSON response type: '%s'." % response_type)
1196+
1197+ page_matches = []
1198+ total = 0
1199+ try:
1200+ results = bing_doc['webPages']['value']
1201+ except (AttributeError, KeyError, ValueError):
1202+ # Bing did not match any pages. Return an empty PageMatches.
1203+ return PageMatches(page_matches, start, total)
1204+
1205+ try:
1206+ total = int(bing_doc['webPages']['totalEstimatedMatches'])
1207+ except (AttributeError, KeyError, ValueError):
1208+ # The datatype is not what PageMatches requires.
1209+ raise SiteSearchResponseError(
1210+ "Could not get the total from the Bing JSON response.")
1211+ if total < 0:
1212+ # See bug 683115.
1213+ total = 0
1214+ for result in results:
1215+ url = result.get('url')
1216+ title = result.get('name')
1217+ summary = result.get('snippet')
1218+ if None in (url, title, summary):
1219+ # There is not enough data to create a PageMatch object.
1220+ # This can be caused by an empty title or summary which
1221+ # has been observed for pages that are from vhosts that
1222+ # should not be indexed.
1223+ continue
1224+ summary = summary.replace('<br>', '')
1225+ page_matches.append(PageMatch(title, url, summary))
1226+
1227+ return PageMatches(page_matches, start, total)
1228
1229=== added file 'lib/lp/services/sitesearch/bingtestservice.py'
1230--- lib/lp/services/sitesearch/bingtestservice.py 1970-01-01 00:00:00 +0000
1231+++ lib/lp/services/sitesearch/bingtestservice.py 2018-03-29 12:55:41 +0000
1232@@ -0,0 +1,79 @@
1233+#!/usr/bin/python
1234+#
1235+# Copyright 2018 Canonical Ltd. This software is licensed under the
1236+# GNU Affero General Public License version 3 (see the file LICENSE).
1237+
1238+"""
1239+This script runs a simple HTTP server. The server returns JSON files
1240+when given certain user-configurable URLs.
1241+"""
1242+
1243+import logging
1244+import os
1245+
1246+from six.moves.BaseHTTPServer import HTTPServer
1247+
1248+from lp.services.config import config
1249+from lp.services.osutils import ensure_directory_exists
1250+from lp.services.pidfile import make_pidfile
1251+from lp.services.sitesearch import testservice
1252+
1253+
1254+# Set up basic logging.
1255+log = logging.getLogger(__name__)
1256+
1257+# The default service name, used by the Launchpad service framework.
1258+service_name = 'bing-webservice'
1259+
1260+
1261+class BingRequestHandler(testservice.RequestHandler):
1262+ default_content_type = 'text/xml; charset=UTF-8'
1263+ log = log
1264+ mapfile = config.bing_test_service.mapfile
1265+ content_dir = config.bing_test_service.canned_response_directory
1266+
1267+
1268+def start_as_process():
1269+ return testservice.start_as_process('bingtestservice')
1270+
1271+
1272+def get_service_endpoint():
1273+ """Return the host and port that the service is running on."""
1274+ return testservice.hostpair(config.bing.site)
1275+
1276+
1277+def service_is_available():
1278+ host, port = get_service_endpoint()
1279+ return testservice.service_is_available(host, port)
1280+
1281+
1282+def wait_for_service():
1283+ host, port = get_service_endpoint()
1284+ return testservice.wait_for_service(host, port)
1285+
1286+
1287+def kill_running_process():
1288+ global service_name
1289+ host, port = get_service_endpoint()
1290+ return testservice.kill_running_process(service_name, host, port)
1291+
1292+
1293+def main():
1294+ """Run the HTTP server."""
1295+ # Redirect our service output to a log file.
1296+ global log
1297+ ensure_directory_exists(os.path.dirname(config.bing_test_service.log))
1298+ filelog = logging.FileHandler(config.bing_test_service.log)
1299+ log.addHandler(filelog)
1300+ log.setLevel(logging.DEBUG)
1301+
1302+ # To support service shutdown we need to create a PID file that is
1303+ # understood by the Launchpad services framework.
1304+ global service_name
1305+ make_pidfile(service_name)
1306+
1307+ host, port = get_service_endpoint()
1308+ server = HTTPServer((host, port), BingRequestHandler)
1309+
1310+ log.info("Starting HTTP Bing webservice server on port %s", port)
1311+ server.serve_forever()
1312
1313=== modified file 'lib/lp/services/sitesearch/configure.zcml'
1314--- lib/lp/services/sitesearch/configure.zcml 2018-03-16 14:02:16 +0000
1315+++ lib/lp/services/sitesearch/configure.zcml 2018-03-29 12:55:41 +0000
1316@@ -1,4 +1,4 @@
1317-<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the
1318+<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
1319 GNU Affero General Public License version 3 (see the file LICENSE).
1320 -->
1321
1322@@ -16,8 +16,17 @@
1323 </class>
1324
1325 <securedutility
1326+ name="google"
1327 class="lp.services.sitesearch.GoogleSearchService"
1328 provides="lp.services.sitesearch.interfaces.ISearchService">
1329 <allow interface="lp.services.sitesearch.interfaces.ISearchService" />
1330 </securedutility>
1331+
1332+ <securedutility
1333+ name="bing"
1334+ class="lp.services.sitesearch.BingSearchService"
1335+ provides="lp.services.sitesearch.interfaces.ISearchService">
1336+ <allow interface="lp.services.sitesearch.interfaces.ISearchService" />
1337+ </securedutility>
1338+
1339 </configure>
1340
1341=== added file 'lib/lp/services/sitesearch/doc/bing-searchservice.txt'
1342--- lib/lp/services/sitesearch/doc/bing-searchservice.txt 1970-01-01 00:00:00 +0000
1343+++ lib/lp/services/sitesearch/doc/bing-searchservice.txt 2018-03-29 12:55:41 +0000
1344@@ -0,0 +1,438 @@
1345+===================
1346+Bing Search Service
1347+===================
1348+
1349+The BingSearchService is a Bing Custom Search client.
1350+Given one or more terms, it will retrieve a JSON
1351+summary of the matching launchpad.net pages.
1352+
1353+We silence logging of new HTTP connections from requests throughout.
1354+
1355+ >>> from fixtures import FakeLogger
1356+ >>> logger = FakeLogger()
1357+ >>> logger.setUp()
1358+
1359+
1360+BingSearchService
1361+=================
1362+
1363+The BingSearchService implements the ISearchService interface.
1364+
1365+ >>> from zope.component import getUtility
1366+ >>> from zope.interface.verify import verifyObject
1367+ >>> from lp.services.sitesearch.interfaces import (
1368+ ... ISearchService)
1369+
1370+ >>> bing_search = getUtility(ISearchService, name="bing")
1371+ >>> verifyObject(ISearchService, bing_search)
1372+ True
1373+ >>> bing_search
1374+ <...BingSearchService ...>
1375+
1376+
1377+--------------------------
1378+BingSearchService search()
1379+--------------------------
1380+
1381+The search method accepts a string argument of terms and an optional int
1382+argument of start. The terms are the same as the text that would be
1383+entered in Bing search form; the terms should not be escaped.
1384+
1385+ >>> from lp.services.sitesearch.interfaces import (
1386+ ... ISearchResults)
1387+
1388+ >>> first_page_matches = bing_search.search(terms='bug')
1389+ >>> first_page_matches
1390+ <...PageMatches ...>
1391+
1392+The start parameter specifies the index (starting at 0) of the first
1393+result returned in the overall set of matches. Since 20 results are
1394+returned, to get the second batch of matches, you would use start=20.
1395+
1396+ >>> second_page_matches = bing_search.search(terms='bug', start=20)
1397+ >>> second_page_matches
1398+ <...PageMatches ...>
1399+
1400+
1401+PageMatches
1402+===========
1403+
1404+The PageMatches object returned by BingSearchService.search()
1405+implements ISearchResults.
1406+
1407+ >>> verifyObject(ISearchResults, first_page_matches)
1408+ True
1409+
1410+The 'total' attribute is the total number of matches that the search
1411+found. If that number is higher than 20, it means that multiple requests
1412+would be needed to retrieve the entire result set.
1413+
1414+The 'start' attribute is the index of the first returned item within the
1415+entire collection of matches. The 'length' attribute contains the number
1416+of returned results.
1417+
1418+The first search for 'bugs' returned a subset of items in the
1419+ISearchResult. There are 25 total items, but the results contains the
1420+first 20 matches (because they start at index 0).
1421+
1422+ >>> first_page_matches.start
1423+ 0
1424+ >>> first_page_matches.total
1425+ 25
1426+ >>> len(first_page_matches)
1427+ 20
1428+
1429+The second search for 'bugs' returned the remainder of the 25 matches.
1430+They start from index 20.
1431+
1432+ >>> verifyObject(ISearchResults, second_page_matches)
1433+ True
1434+
1435+ >>> second_page_matches.start
1436+ 20
1437+ >>> second_page_matches.total
1438+ 25
1439+ >>> len(second_page_matches)
1440+ 5
1441+
1442+An item can be retrieved from an ISearchResults object using its
1443+index. All the items in the collection can be iterated.
1444+
1445+ >>> second_page_matches[1].url
1446+ 'http://blog.launchpad.dev/general/of-bugs-and-statuses'
1447+
1448+ >>> for page_match in second_page_matches:
1449+ ... page_match.url
1450+ 'https://help.launchpad.net/Bugs'
1451+ 'http://blog.launchpad.dev/general/of-bugs-and-statuses'
1452+ 'http://launchpad.dev/mahara/+milestone/1.8.0'
1453+ 'http://launchpad.dev/mb'
1454+ 'http://launchpad.dev/bugs'
1455+
1456+An empty PageMatches is returns if there are no results.
1457+
1458+ >>> no_page_matches = bing_search.search(terms='fnord')
1459+ >>> no_page_matches.start
1460+ 0
1461+ >>> no_page_matches.total
1462+ 0
1463+ >>> len(no_page_matches)
1464+ 0
1465+
1466+
1467+PageMatch
1468+=========
1469+
1470+The PageMatch object represents a single result from a search result
1471+set. It is created by passing a title, url, and a summary. It is
1472+an implementation of ISearchResult.
1473+
1474+ >>> from lp.services.sitesearch.interfaces import ISearchResult
1475+ >>> from lp.services.sitesearch import PageMatch
1476+
1477+ >>> page_match = PageMatch(
1478+ ... u'Unicode Titles in Launchpad',
1479+ ... 'https://launchpad.net/unicode-titles',
1480+ ... u'Unicode Titles is a modest project dedicated to using Unicode.')
1481+
1482+ >>> verifyObject(ISearchResult, page_match)
1483+ True
1484+
1485+The title and summary attributes contain the same text that
1486+initialized the object.
1487+
1488+ >>> page_match.title
1489+ u'Unicode Titles in Launchpad'
1490+ >>> page_match.summary
1491+ u'Unicode Titles is a modest project dedicated to using Unicode.'
1492+
1493+The URL's domain is rewitten to the so that links from launchpad.net are
1494+mapped to the local environment.
1495+
1496+ >>> page_match.url
1497+ 'http://launchpad.dev/unicode-titles'
1498+
1499+
1500+Search configuration
1501+====================
1502+
1503+The bing search service is configured by the bing section in
1504+lazr.config. All requests are made to Bing's site, but the
1505+configuration may set a testing site.
1506+
1507+ >>> from lp.services.config import config
1508+ >>> from lp.services.sitesearch import BingSearchService
1509+
1510+ >>> bing_search = BingSearchService()
1511+ >>> config.bing.site == bing_search.site
1512+ True
1513+ >>> bing_search.site
1514+ 'http://launchpad.dev:.../bingcustomsearch/v7.0/search'
1515+
1516+The subscription_key is the Cognitive Services subscription key for
1517+Bing Custom Search API.
1518+
1519+ >>> config.bing.subscription_key == bing_search.subscription_key
1520+ True
1521+ >>> bing_search.subscription_key
1522+ 'abcdef01234567890abcdef012345678'
1523+
1524+The custom_config_id is the id that identifies the custom search instance.
1525+
1526+ >>> config.bing.custom_config_id == bing_search.custom_config_id
1527+ True
1528+ >>> bing_search.custom_config_id
1529+ 1234567890
1530+
1531+Several default query parameters are constant. They are kept in the
1532+_default_values dict. The q (terms) and start params are provided at
1533+the time of the search.
1534+
1535+ >>> for key, value in sorted(bing_search._default_values.items()):
1536+ ... print key, ':', repr(value)
1537+ count : 20
1538+ customConfig : None
1539+ mkt : 'en-US'
1540+ offset : 0
1541+ q : None
1542+
1543+create_search_url()
1544+===================
1545+
1546+The search url used inside the search() method is created by
1547+create_search_url(). It accepts two optional arguments: terms and start.
1548+An error is raised if any of the parameters are None.
1549+
1550+ >>> bing_search.create_search_url('')
1551+ Traceback (most recent call last):
1552+ ...
1553+ ValueError: Missing value for parameter 'q'.
1554+
1555+ >>> bing_search.create_search_url(None)
1556+ Traceback (most recent call last):
1557+ ...
1558+ ValueError: Missing value for parameter 'q'.
1559+
1560+ >>> bing_search.create_search_url('bugs', start='true')
1561+ Traceback (most recent call last):
1562+ ...
1563+ ValueError: Value for parameter 'offset' is not an int.
1564+
1565+The term parameter in this example can be defined by passing the term
1566+argument to the method. The argument is url encoded and used as the
1567+value for the 'q' (query) parameter.
1568+
1569+ >>> bing_search.create_search_url(terms='svg +bugs').replace('&', ' ')
1570+ 'http://launchpad.dev:.../bingcustomsearch/v7.0/search?count=20 customConfig=1234567890 mkt=en-US offset=0 q=svg+%2Bbugs'
1571+
1572+Unicode characters are escaped correctly in the bing request URL.
1573+
1574+ >>> bing_search.create_search_url(terms=u'Carlos Perell\xf3 Mar\xedn')
1575+ 'http://launchpad.dev:.../...offset=0&q=Carlos+Perell%C3%B3+Mar%C3%ADn'
1576+
1577+The start parameter can be changed by passing a start int argument.
1578+
1579+ >>> bing_search.create_search_url(terms='svg +bugs', start=20)
1580+ 'http://launchpad.dev:.../...offset=20&q=svg+%2Bbugs'
1581+
1582+
1583+Bing Search response parsing
1584+============================
1585+
1586+The BingSearchService's _parse_bing_response() expects a JSON response to
1587+create the PageMatch and PageMatches objects. An error is raised when
1588+the JSON document cannot be parsed into objects.
1589+
1590+The PageMatches's total attribute comes from the `webPages.totalEstimatedMatches`
1591+JSON element. When it cannot be found and the value cast to an int,
1592+an error is raised. If Bing were to redefine the meaning of the
1593+element to use a '~' to indicate an approximate total, an error would
1594+be raised.
1595+
1596+ >>> from os import path
1597+
1598+ >>> base_path = path.normpath(path.join(
1599+ ... path.dirname(__file__), '..', 'tests', 'data'))
1600+ >>> json_file_name = path.join(
1601+ ... base_path, 'bingsearchservice-incompatible-matches.json')
1602+ >>> with open(json_file_name, 'r') as json_file:
1603+ ... data = json_file.read()
1604+ >>> print data
1605+ {...
1606+ "totalEstimatedMatches": "~25"...
1607+
1608+ >>> bing_search._parse_bing_response(data)
1609+ Traceback (most recent call last):
1610+ ...
1611+ SiteSearchResponseError: Could not get the total from the
1612+ Bing JSON response.
1613+
1614+On the other hand, if the total is ever less than zero (see bug 683115),
1615+this is expected: we simply return a total of 0.
1616+
1617+ >>> json_file_name = path.join(
1618+ ... base_path, 'bingsearchservice-negative-total.json')
1619+ >>> with open(json_file_name, 'r') as json_file:
1620+ ... data = json_file.read()
1621+ >>> print data
1622+ {...
1623+ "totalEstimatedMatches": -25...
1624+
1625+ >>> bing_search._parse_bing_response(data).total
1626+ 0
1627+
1628+A PageMatch requires a title, url, and a summary. If those elements cannot
1629+be found, a PageMatch cannot be made. A missing title ('name') indicates
1630+a bad page on Launchpad, so it is ignored. In this example, the first match
1631+is missing a title, so only the second page is present in the PageMatches.
1632+
1633+ >>> json_file_name = path.join(
1634+ ... base_path, 'bingsearchservice-missing-title.json')
1635+ >>> with open(json_file_name, 'r') as json_file:
1636+ ... data = json_file.read()
1637+ >>> page_matches = bing_search._parse_bing_response(data)
1638+ >>> len(page_matches)
1639+ 1
1640+ >>> page_matches[0].title
1641+ u'GCleaner in Launchpad'
1642+ >>> page_matches[0].url
1643+ 'http://launchpad.dev/gcleaner'
1644+
1645+When a match is missing a summary ('snippet'), the match is skipped because
1646+there is no information about why it matched. This appears to relate to
1647+pages that are in the index, but should be removed. In this example
1648+taken from real data, the links are to the same page on different
1649+vhosts. The edge vhost has no summary, so it is skipped.
1650+
1651+ >>> json_file_name = path.join(
1652+ ... base_path, 'bingsearchservice-missing-summary.json')
1653+ >>> with open(json_file_name, 'r') as json_file:
1654+ ... data = json_file.read()
1655+ >>> page_matches = bing_search._parse_bing_response(data)
1656+ >>> len(page_matches)
1657+ 1
1658+ >>> page_matches[0].title
1659+ u'BugExpiry - Launchpad Help'
1660+ >>> page_matches[0].url
1661+ 'https://help.launchpad.net/BugExpiry'
1662+
1663+When the URL ('url') cannot be found the match is skipped. There are no
1664+examples of this. We do not want this hypothetical situation to give
1665+users a bad experience.
1666+
1667+ >>> json_file_name = path.join(
1668+ ... base_path, 'bingsearchservice-missing-url.json')
1669+ >>> with open(json_file_name, 'r') as json_file:
1670+ ... data = json_file.read()
1671+ >>> page_matches = bing_search._parse_bing_response(data)
1672+ >>> len(page_matches)
1673+ 1
1674+ >>> page_matches[0].title
1675+ u'LongoMatch in Launchpad'
1676+ >>> page_matches[0].url
1677+ 'http://launchpad.dev/longomatch'
1678+
1679+If no matches are found in the response, and there are 20 or fewer results,
1680+an Empty PageMatches is returned. This happens when the results are missing
1681+titles and summaries. This is not considered to be a problem because the
1682+small number implies that Bing did a poor job of indexing pages or indexed
1683+the wrong Launchpad server. In this example, there is only one match, but
1684+the results is missing a title so there is not enough information to make
1685+a PageMatch.
1686+
1687+ >>> json_file_name = path.join(
1688+ ... base_path, 'bingsearchservice-no-meaningful-results.json')
1689+ >>> with open(json_file_name, 'r') as json_file:
1690+ ... data = json_file.read()
1691+ >>> page_matches = bing_search._parse_bing_response(data)
1692+ >>> len(page_matches)
1693+ 0
1694+
1695+
1696+-------------
1697+URL rewriting
1698+-------------
1699+
1700+The URL scheme used in the rewritten URL is configured in
1701+config.bing.url_rewrite_scheme. The hostname is set in the shared
1702+key config.vhost.mainsite.hostname.
1703+
1704+ >>> config.vhosts.use_https
1705+ False
1706+ >>> page_match.url_rewrite_scheme
1707+ 'http'
1708+
1709+ >>> config.vhost.mainsite.hostname == page_match.url_rewrite_hostname
1710+ True
1711+ >>> page_match.url_rewrite_hostname
1712+ 'launchpad.dev'
1713+
1714+URLs are rewritten to map public URL to the private hostname.
1715+The vhost name is preserved when the URL is rewritten.
1716+
1717+ >>> page_match = PageMatch(
1718+ ... u'Bug #456 in Unicode title: "testrunner hates Unicode"',
1719+ ... 'https://bugs.launchpad.net/unicode-titles/+bug/456',
1720+ ... u'The Zope testrunner likes ASCII more than Unicode.')
1721+ >>> page_match.url
1722+ 'http://bugs.launchpad.dev/unicode-titles/+bug/456'
1723+
1724+A URL's trailing slash is removed; Launchpad does not use trailing
1725+slashes.
1726+
1727+ >>> page_match = PageMatch(
1728+ ... u'Ubuntu in Launchpad',
1729+ ... 'https://launchpad.net/ubuntu/',
1730+ ... u'Ubuntu also includes more software than any other operating')
1731+ >>> page_match.url
1732+ 'http://launchpad.dev/ubuntu'
1733+
1734+There is a list of URLs that are not rewritten configured in
1735+config.bing.url_rewrite_exceptions. For example, help.launchpad.net
1736+is only run in one environment, so links to that site will be preserved.
1737+
1738+ >>> config.bing.url_rewrite_exceptions
1739+ 'help.launchpad.net'
1740+ >>> page_match.url_rewrite_exceptions
1741+ ['help.launchpad.net']
1742+
1743+ >>> page_match = PageMatch(
1744+ ... u'OpenID',
1745+ ... 'https://help.launchpad.net/OpenID',
1746+ ... u'Launchpad uses OpenID.')
1747+ >>> page_match.url
1748+ 'https://help.launchpad.net/OpenID'
1749+
1750+
1751+-----------------------------
1752+Graceful handling of timeouts
1753+-----------------------------
1754+
1755+The external service (Bing Search Engine) may not be available, or
1756+is not responding quickly because there are network issues. In these
1757+cases a TimeoutError is issued.
1758+
1759+ >>> from socket import socket
1760+ >>> from textwrap import dedent
1761+ >>> from lp.services.timeout import (
1762+ ... get_default_timeout_function, set_default_timeout_function)
1763+
1764+ >>> server = socket()
1765+ >>> server.bind(('127.0.0.01', 0))
1766+ >>> server.listen(1)
1767+ >>> config.push('timeout_data', dedent("""
1768+ ... [bing]
1769+ ... site: http://%s:%d/cse
1770+ ... """ % server.getsockname()))
1771+ >>> old_timeout_function = get_default_timeout_function()
1772+ >>> set_default_timeout_function(lambda: 0.1)
1773+ >>> bing_search.search(terms='bug')
1774+ Traceback (most recent call last):
1775+ ...
1776+ SiteSearchResponseError: ... timeout exceeded.
1777+
1778+ # Restore the configuration and the timeout state.
1779+ >>> timeout_data = config.pop('timeout_data')
1780+ >>> set_default_timeout_function(old_timeout_function)
1781+
1782+ >>> logger.cleanUp()
1783
1784=== modified file 'lib/lp/services/sitesearch/doc/google-searchservice.txt'
1785--- lib/lp/services/sitesearch/doc/google-searchservice.txt 2018-03-27 15:47:35 +0000
1786+++ lib/lp/services/sitesearch/doc/google-searchservice.txt 2018-03-29 12:55:41 +0000
1787@@ -23,7 +23,7 @@
1788 >>> from lp.services.sitesearch.interfaces import (
1789 ... ISearchService)
1790
1791- >>> google_search = getUtility(ISearchService)
1792+ >>> google_search = getUtility(ISearchService, name="google")
1793 >>> verifyObject(ISearchService, google_search)
1794 True
1795 >>> google_search
1796
1797=== modified file 'lib/lp/services/sitesearch/interfaces.py'
1798--- lib/lp/services/sitesearch/interfaces.py 2018-03-26 21:06:51 +0000
1799+++ lib/lp/services/sitesearch/interfaces.py 2018-03-29 12:55:41 +0000
1800@@ -1,4 +1,4 @@
1801-# Copyright 2009 Canonical Ltd. This software is licensed under the
1802+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
1803 # GNU Affero General Public License version 3 (see the file LICENSE).
1804
1805 """Interfaces for searching and working with results."""
1806
1807=== added file 'lib/lp/services/sitesearch/tests/bingserviceharness.py'
1808--- lib/lp/services/sitesearch/tests/bingserviceharness.py 1970-01-01 00:00:00 +0000
1809+++ lib/lp/services/sitesearch/tests/bingserviceharness.py 2018-03-29 12:55:41 +0000
1810@@ -0,0 +1,107 @@
1811+# Copyright 2018 Canonical Ltd. This software is licensed under the
1812+# GNU Affero General Public License version 3 (see the file LICENSE).
1813+
1814+"""
1815+Fixtures for running the Bing test webservice.
1816+"""
1817+
1818+__metaclass__ = type
1819+
1820+__all__ = ['BingServiceTestSetup']
1821+
1822+
1823+import errno
1824+import os
1825+import signal
1826+
1827+from lp.services.sitesearch import bingtestservice
1828+
1829+
1830+class BingServiceTestSetup:
1831+ """Set up the Bing web service stub for use in functional tests.
1832+ """
1833+
1834+ # XXX gary 2008-12-06 bug=305858: Spurious test failures discovered on
1835+ # buildbot, builds 40 and 43. The locations of the failures are marked
1836+ # below with " # SPURIOUS FAILURE". To reinstate, add the text below back
1837+ # to the docstring above. Note that the test that uses this setup,
1838+ # bing-service-stub.txt, is also disabled. See test_doc.py.
1839+ """
1840+ >>> from lp.services.sitesearch.bingtestservice import (
1841+ ... service_is_available)
1842+ >>> from lp.services.config import config
1843+
1844+ >>> assert not service_is_available() # Sanity check. # SPURIOUS FAILURE
1845+
1846+ >>> BingServiceTestSetup().setUp()
1847+
1848+ After setUp is called, a Bing test service instance is running.
1849+
1850+ >>> assert service_is_available()
1851+ >>> assert BingServiceTestSetup.service is not None
1852+
1853+ After tearDown is called, the service is shut down.
1854+
1855+ >>> BingServiceTestSetup().tearDown()
1856+
1857+ >>> assert not service_is_available()
1858+ >>> assert BingServiceTestSetup.service is None
1859+
1860+ The fixture can be started and stopped multiple time in succession:
1861+
1862+ >>> BingServiceTestSetup().setUp()
1863+ >>> assert service_is_available()
1864+
1865+ Having a service instance already running doesn't prevent a new
1866+ service from starting. The old instance is killed off and replaced
1867+ by the new one.
1868+
1869+ >>> old_pid = BingServiceTestSetup.service.pid
1870+ >>> BingServiceTestSetup().setUp() # SPURIOUS FAILURE
1871+ >>> BingServiceTestSetup.service.pid != old_pid
1872+ True
1873+
1874+ Tidy up.
1875+
1876+ >>> BingServiceTestSetup().tearDown()
1877+ >>> assert not service_is_available()
1878+
1879+ """
1880+
1881+ service = None # A reference to our running service.
1882+
1883+ def setUp(self):
1884+ self.startService()
1885+
1886+ def tearDown(self):
1887+ self.stopService()
1888+
1889+ @classmethod
1890+ def startService(cls):
1891+ """Start the webservice."""
1892+ bingtestservice.kill_running_process()
1893+ cls.service = bingtestservice.start_as_process()
1894+ assert cls.service, "The Search service process did not start."
1895+ try:
1896+ bingtestservice.wait_for_service()
1897+ except RuntimeError:
1898+ # The service didn't start itself soon enough. We must
1899+ # make sure to kill any errant processes that may be
1900+ # hanging around.
1901+ cls.stopService()
1902+ raise
1903+
1904+ @classmethod
1905+ def stopService(cls):
1906+ """Shut down the webservice instance."""
1907+ if cls.service:
1908+ try:
1909+ os.kill(cls.service.pid, signal.SIGTERM)
1910+ except OSError as error:
1911+ if error.errno != errno.ESRCH:
1912+ raise
1913+ # The process with the given pid doesn't exist, so there's
1914+ # nothing to kill or wait for.
1915+ else:
1916+ cls.service.wait()
1917+ cls.service = None
1918
1919=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json'
1920--- lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json 1970-01-01 00:00:00 +0000
1921+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json 2018-03-29 12:55:41 +0000
1922@@ -0,0 +1,384 @@
1923+{
1924+ "_type": "SearchResponse",
1925+ "instrumentation": {
1926+ "pingUrlBase": "https://www.bingapis.com/api/ping?IG=4C3A184771914F6BB81F8541E7EE7695&CID=13045103CBA569B80D475AB7CA03687C&ID=",
1927+ "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=4C3A184771914F6BB81F8541E7EE7695&CID=13045103CBA569B80D475AB7CA03687C&Type=Event.CPT&DATA=0"
1928+ },
1929+ "queryContext": {
1930+ "originalQuery": "bug"
1931+ },
1932+ "webPages": {
1933+ "webSearchUrl": "https://www.bing.com/search?q=bug",
1934+ "webSearchUrlPingSuffix": "DevEx,5462.1",
1935+ "totalEstimatedMatches": 25,
1936+ "value": [
1937+ {
1938+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
1939+ "name": "Launchpad Bugs",
1940+ "url": "https://launchpad.net/~ubuntu-bugs",
1941+ "urlPingSuffix": "DevEx,5080.1",
1942+ "isFamilyFriendly": true,
1943+ "displayUrl": "https://launchpad.net/~ubuntu-bugs",
1944+ "snippet": "The purpose of this team is to have a mailing list that receives all Ubuntu bug mail. If you wish to receive copies of all bug traffic, instead subscribe to the ...",
1945+ "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
1946+ "fixedPosition": false
1947+ },
1948+ {
1949+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
1950+ "name": "Bugs in Ubuntu Linux",
1951+ "url": "https://launchpad.net/gcleaner",
1952+ "urlPingSuffix": "DevEx,5095.1",
1953+ "isFamilyFriendly": true,
1954+ "displayUrl": "https://launchpad.net/gcleaner",
1955+ "snippet": "GCleaner is a beautiful and fast system cleaner for Elementary OS and Ubuntu or based. Writen in Vala, GTK+, Granite and GLib/GIO for the purpose that the users feel ...",
1956+ "dateLastCrawled": "2018-02-21T13:17:00.0000000Z",
1957+ "fixedPosition": false
1958+ },
1959+ {
1960+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
1961+ "name": "Bugs related to Sample Person",
1962+ "url": "https://help.launchpad.net/BugExpiry",
1963+ "urlPingSuffix": "DevEx,5110.1",
1964+ "isFamilyFriendly": true,
1965+ "displayUrl": "https://help.launchpad.net/BugExpiry",
1966+ "snippet": "Want to know more about Bug Statuses? Check out our BugStatuses page. What is bug expiry? Today, projects and distributions have the option of allowing old ...",
1967+ "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
1968+ "fixedPosition": false
1969+ },
1970+ {
1971+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
1972+ "name": "Bug #1 in Mozilla Firefox: Firefox does not support SVG",
1973+ "url": "https://launchpad.net/longomatch",
1974+ "urlPingSuffix": "DevEx,5125.1",
1975+ "isFamilyFriendly": true,
1976+ "displayUrl": "https://launchpad.net/longomatch",
1977+ "snippet": "LongoMatch is a sports video analyse tool for coaches to assist them on making live video reports from a match. It creates a database with the most important plays of ...",
1978+ "dateLastCrawled": "2018-02-16T21:54:00.0000000Z",
1979+ "fixedPosition": false
1980+ },
1981+ {
1982+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4",
1983+ "name": "Question #232632 : Questions : OpenStack Heat",
1984+ "url": "https://answers.launchpad.net/heat/+question/232632",
1985+ "urlPingSuffix": "DevEx,5140.1",
1986+ "datePublished": "2013-07-18T00:00:00.0000000",
1987+ "isFamilyFriendly": true,
1988+ "displayUrl": "https://answers.launchpad.net/heat/+question/232632",
1989+ "snippet": "Using grizzly version(2013.1.2) from heat/openstack. Failed to create stack, got error: 'NoneType' object has no attribute 'rstrip'. See below my runtime ...",
1990+ "dateLastCrawled": "2018-02-20T23:45:00.0000000Z",
1991+ "fixedPosition": false
1992+ },
1993+ {
1994+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.5",
1995+ "name": "Sandpad in Launchpad",
1996+ "url": "https://launchpad.net/sandpad",
1997+ "urlPingSuffix": "DevEx,5154.1",
1998+ "isFamilyFriendly": true,
1999+ "displayUrl": "https://launchpad.net/sandpad",
2000+ "snippet": "Sandpad is an standalone wlua application for making quick scratch programs and sandboxes in Lua. It uses IUP 3 for the user interface.",
2001+ "dateLastCrawled": "2018-03-03T17:54:00.0000000Z",
2002+ "fixedPosition": false
2003+ },
2004+ {
2005+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.6",
2006+ "name": "Eventum in Launchpad",
2007+ "url": "https://launchpad.net/eventum",
2008+ "urlPingSuffix": "DevEx,5169.1",
2009+ "isFamilyFriendly": true,
2010+ "displayUrl": "https://launchpad.net/eventum",
2011+ "snippet": "Eventum is a user-friendly and flexible issue tracking system that can be used by a support department to track incoming technical support requests, or by a software ...",
2012+ "dateLastCrawled": "2018-03-07T19:52:00.0000000Z",
2013+ "fixedPosition": false
2014+ },
2015+ {
2016+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.7",
2017+ "name": "Inkscape 0.48.2 \"0.48.2\" - Launchpad",
2018+ "url": "https://launchpad.net/inkscape/+milestone/0.48.2",
2019+ "urlPingSuffix": "DevEx,5182.1",
2020+ "isFamilyFriendly": true,
2021+ "displayUrl": "https://launchpad.net/inkscape/+milestone/0.48.2",
2022+ "snippet": "After you've downloaded a file, you can verify its authenticity using its MD5 sum or signature. (How do I verify a download?",
2023+ "dateLastCrawled": "2018-02-24T19:01:00.0000000Z",
2024+ "fixedPosition": false
2025+ },
2026+ {
2027+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.8",
2028+ "name": "Code Hosting - Launchpad tour",
2029+ "url": "https://launchpad.net/+tour/branch-hosting-tracking",
2030+ "urlPingSuffix": "DevEx,5195.1",
2031+ "isFamilyFriendly": true,
2032+ "displayUrl": "https://launchpad.net/+tour/branch-hosting-tracking",
2033+ "snippet": "Code hosting and review. Launchpad and Bazaar make it easy for anyone to get your project's code, make their own changes with full version control, and then propose ...",
2034+ "dateLastCrawled": "2018-03-10T23:01:00.0000000Z",
2035+ "fixedPosition": false
2036+ },
2037+ {
2038+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.9",
2039+ "name": "gdiskdump project files : gdiskdump",
2040+ "url": "https://launchpad.net/gdiskdump/+download",
2041+ "urlPingSuffix": "DevEx,5208.1",
2042+ "isFamilyFriendly": true,
2043+ "displayUrl": "https://launchpad.net/gdiskdump/+download",
2044+ "snippet": "added advanced settings and estimated Time for process to finish. bug fixes for some languages.",
2045+ "dateLastCrawled": "2018-02-19T11:52:00.0000000Z",
2046+ "fixedPosition": false
2047+ },
2048+ {
2049+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.10",
2050+ "name": "Bug #1747091 “epicsTimeGetEvent() / generalTime bug ...",
2051+ "url": "https://code.launchpad.net/bugs/1747091",
2052+ "urlPingSuffix": "DevEx,5220.1",
2053+ "isFamilyFriendly": true,
2054+ "displayUrl": "https://code.launchpad.net/bugs/1747091",
2055+ "snippet": "When changes in epicsGeneralTime.c were made (fetch time provider's eventTime into a local copy) an inconsistency resulted.",
2056+ "dateLastCrawled": "2018-02-04T02:49:00.0000000Z",
2057+ "fixedPosition": false
2058+ },
2059+ {
2060+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.11",
2061+ "name": "Bug #96441 “timer updates use too much CPU” : Bugs : Jokosher",
2062+ "url": "https://launchpad.net/jokosher/+bug/96441",
2063+ "urlPingSuffix": "DevEx,5232.1",
2064+ "isFamilyFriendly": true,
2065+ "displayUrl": "https://launchpad.net/jokosher/+bug/96441",
2066+ "snippet": "The information about this bug in Launchpad is automatically pulled daily from the remote bug. This information was last pulled ...",
2067+ "dateLastCrawled": "2018-02-17T16:53:00.0000000Z",
2068+ "fixedPosition": false
2069+ },
2070+ {
2071+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.12",
2072+ "name": "Series 17.10 : Mahara",
2073+ "url": "https://launchpad.net/mahara/17.10",
2074+ "urlPingSuffix": "DevEx,5247.1",
2075+ "isFamilyFriendly": true,
2076+ "displayUrl": "https://launchpad.net/mahara/17.10",
2077+ "snippet": "All bugs Latest bugs reported. Bug #1752688: MariaDB fails to upgrade - unable to CAST as JSON Reported on 2018-03-01 Bug #1752442: Problems with group forums / topics",
2078+ "dateLastCrawled": "2018-03-12T13:48:00.0000000Z",
2079+ "fixedPosition": false
2080+ },
2081+ {
2082+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.13",
2083+ "name": "sap in Launchpad",
2084+ "url": "https://launchpad.net/sap+",
2085+ "urlPingSuffix": "DevEx,5260.1",
2086+ "isFamilyFriendly": true,
2087+ "displayUrl": "https://launchpad.net/sap+",
2088+ "snippet": "Sap is a simple audio player written in Vala and utilizing gstreamer for audio playback and ncurses for user interaction.",
2089+ "dateLastCrawled": "2018-03-12T05:18:00.0000000Z",
2090+ "fixedPosition": false
2091+ },
2092+ {
2093+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.14",
2094+ "name": "Code/BugAndBlueprintLinks - Launchpad Help",
2095+ "url": "https://help.launchpad.net/Code/BugAndBlueprintLinks",
2096+ "urlPingSuffix": "DevEx,5275.1",
2097+ "isFamilyFriendly": true,
2098+ "displayUrl": "https://help.launchpad.net/Code/BugAndBlueprintLinks",
2099+ "snippet": "Linking code to bug reports and blueprints. Launchpad is much like a fancy china dinner service: you can get a great deal of use and contentment from just one or two ...",
2100+ "dateLastCrawled": "2018-01-26T14:46:00.0000000Z",
2101+ "fixedPosition": false
2102+ },
2103+ {
2104+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.15",
2105+ "name": "One Hundred Papercuts in Launchpad",
2106+ "url": "https://launchpad.net/hundredpapercuts",
2107+ "urlPingSuffix": "DevEx,5289.1",
2108+ "isFamilyFriendly": true,
2109+ "displayUrl": "https://launchpad.net/hundredpapercuts",
2110+ "snippet": "Downloads. One Hundred Papercuts does not have any download files registered with Launchpad. •",
2111+ "dateLastCrawled": "2018-03-01T20:05:00.0000000Z",
2112+ "fixedPosition": false
2113+ },
2114+ {
2115+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.16",
2116+ "name": "Little Software Stats in Launchpad",
2117+ "url": "https://launchpad.net/lilsoftstats/",
2118+ "urlPingSuffix": "DevEx,5304.1",
2119+ "isFamilyFriendly": true,
2120+ "displayUrl": "https://launchpad.net/lilsoftstats",
2121+ "snippet": "Little Software Stats is the first free and open source program that will allow software developers to keep track of how their software is being used.",
2122+ "dateLastCrawled": "2018-03-07T21:46:00.0000000Z",
2123+ "fixedPosition": false
2124+ },
2125+ {
2126+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.17",
2127+ "name": "Take the tour - Launchpad tour",
2128+ "url": "https://launchpad.net/+tour",
2129+ "urlPingSuffix": "DevEx,5317.1",
2130+ "isFamilyFriendly": true,
2131+ "displayUrl": "https://launchpad.net/+tour",
2132+ "snippet": "Launchpad helps people to work together on free software by making it easy to share code, bug reports, translations and ideas.",
2133+ "dateLastCrawled": "2018-02-25T04:37:00.0000000Z",
2134+ "fixedPosition": false
2135+ },
2136+ {
2137+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.18",
2138+ "name": "EOEC in Launchpad",
2139+ "url": "https://launchpad.net/eoec",
2140+ "urlPingSuffix": "DevEx,5332.1",
2141+ "isFamilyFriendly": true,
2142+ "displayUrl": "https://launchpad.net/eoec",
2143+ "snippet": "Barcode example published as standalone extension on 2009-01-15 After adding a number of extra features to the Barcode example of EuroOffice ... EOEC ...",
2144+ "dateLastCrawled": "2018-03-09T21:39:00.0000000Z",
2145+ "fixedPosition": false
2146+ },
2147+ {
2148+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.19",
2149+ "name": "Bug #1210747 “taskdef name not specified” : Bugs : Radkarte",
2150+ "url": "https://launchpad.net/radkarte/+bug/1210747",
2151+ "urlPingSuffix": "DevEx,5345.1",
2152+ "isFamilyFriendly": true,
2153+ "displayUrl": "https://launchpad.net/radkarte/+bug/1210747",
2154+ "snippet": "Sorry for the late response, somehow the notification mail did not work. I use the \"ant-contrib\" package for the ID calculation to prepare the build-file for easy ...",
2155+ "dateLastCrawled": "2018-01-11T22:23:00.0000000Z",
2156+ "fixedPosition": false
2157+ }
2158+ ]
2159+ },
2160+ "rankingResponse": {
2161+ "mainline": {
2162+ "items": [
2163+ {
2164+ "answerType": "WebPages",
2165+ "resultIndex": 0,
2166+ "value": {
2167+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0"
2168+ }
2169+ },
2170+ {
2171+ "answerType": "WebPages",
2172+ "resultIndex": 1,
2173+ "value": {
2174+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1"
2175+ }
2176+ },
2177+ {
2178+ "answerType": "WebPages",
2179+ "resultIndex": 2,
2180+ "value": {
2181+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2"
2182+ }
2183+ },
2184+ {
2185+ "answerType": "WebPages",
2186+ "resultIndex": 3,
2187+ "value": {
2188+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3"
2189+ }
2190+ },
2191+ {
2192+ "answerType": "WebPages",
2193+ "resultIndex": 4,
2194+ "value": {
2195+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4"
2196+ }
2197+ },
2198+ {
2199+ "answerType": "WebPages",
2200+ "resultIndex": 5,
2201+ "value": {
2202+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.5"
2203+ }
2204+ },
2205+ {
2206+ "answerType": "WebPages",
2207+ "resultIndex": 6,
2208+ "value": {
2209+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.6"
2210+ }
2211+ },
2212+ {
2213+ "answerType": "WebPages",
2214+ "resultIndex": 7,
2215+ "value": {
2216+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.7"
2217+ }
2218+ },
2219+ {
2220+ "answerType": "WebPages",
2221+ "resultIndex": 8,
2222+ "value": {
2223+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.8"
2224+ }
2225+ },
2226+ {
2227+ "answerType": "WebPages",
2228+ "resultIndex": 9,
2229+ "value": {
2230+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.9"
2231+ }
2232+ },
2233+ {
2234+ "answerType": "WebPages",
2235+ "resultIndex": 10,
2236+ "value": {
2237+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.10"
2238+ }
2239+ },
2240+ {
2241+ "answerType": "WebPages",
2242+ "resultIndex": 11,
2243+ "value": {
2244+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.11"
2245+ }
2246+ },
2247+ {
2248+ "answerType": "WebPages",
2249+ "resultIndex": 12,
2250+ "value": {
2251+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.12"
2252+ }
2253+ },
2254+ {
2255+ "answerType": "WebPages",
2256+ "resultIndex": 13,
2257+ "value": {
2258+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.13"
2259+ }
2260+ },
2261+ {
2262+ "answerType": "WebPages",
2263+ "resultIndex": 14,
2264+ "value": {
2265+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.14"
2266+ }
2267+ },
2268+ {
2269+ "answerType": "WebPages",
2270+ "resultIndex": 15,
2271+ "value": {
2272+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.15"
2273+ }
2274+ },
2275+ {
2276+ "answerType": "WebPages",
2277+ "resultIndex": 16,
2278+ "value": {
2279+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.16"
2280+ }
2281+ },
2282+ {
2283+ "answerType": "WebPages",
2284+ "resultIndex": 17,
2285+ "value": {
2286+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.17"
2287+ }
2288+ },
2289+ {
2290+ "answerType": "WebPages",
2291+ "resultIndex": 18,
2292+ "value": {
2293+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.18"
2294+ }
2295+ },
2296+ {
2297+ "answerType": "WebPages",
2298+ "resultIndex": 19,
2299+ "value": {
2300+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.19"
2301+ }
2302+ }
2303+ ]
2304+ }
2305+ }
2306+}
2307
2308=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json'
2309--- lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json 1970-01-01 00:00:00 +0000
2310+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json 2018-03-29 12:55:41 +0000
2311@@ -0,0 +1,151 @@
2312+{
2313+ "_type": "SearchResponse",
2314+ "instrumentation": {
2315+ "pingUrlBase": "https://www.bingapis.com/api/ping?IG=41744312F5E645D58DE6733982EDC72A&CID=222E331334AA69A5318C38A7350C683B&ID=",
2316+ "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=41744312F5E645D58DE6733982EDC72A&CID=222E331334AA69A5318C38A7350C683B&Type=Event.CPT&DATA=0"
2317+ },
2318+ "queryContext": {
2319+ "originalQuery": "bug"
2320+ },
2321+ "webPages": {
2322+ "webSearchUrl": "https://www.bing.com/search?q=bug",
2323+ "webSearchUrlPingSuffix": "DevEx,5530.1",
2324+ "totalEstimatedMatches": 25,
2325+ "value": [
2326+ {
2327+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
2328+ "name": "Bugs - Launchpad Help",
2329+ "url": "https://help.launchpad.net/Bugs",
2330+ "urlPingSuffix": "DevEx,5103.1",
2331+ "isFamilyFriendly": true,
2332+ "displayUrl": "https://help.launchpad.net/Bugs",
2333+ "snippet": "Launchpad Help > Bugs . Use Launchpad's bug tracker for your project. Bug heat: a computed estimate of a bug's significance. Learn about automatic bug expiry",
2334+ "deepLinks": [
2335+ {
2336+ "name": "Bugs/EmailInterface",
2337+ "url": "https://help.launchpad.net/Bugs/EmailInterface",
2338+ "urlPingSuffix": "DevEx,5093.1",
2339+ "snippet": "Overview. Launchpad's bug tracker sends you email about the bugs you're interested in. If you see something that requires your attention - for example, you want to ..."
2340+ },
2341+ {
2342+ "name": "Bugs/PluginAPISpec",
2343+ "url": "https://help.launchpad.net/Bugs/PluginAPISpec",
2344+ "urlPingSuffix": "DevEx,5094.1",
2345+ "snippet": "Overview. We want Launchpad to share bug reports, comments, statuses and other information with as many bug trackers as possible. We've already produced plugins that ..."
2346+ },
2347+ {
2348+ "name": "Bugs/Subscriptions",
2349+ "url": "https://help.launchpad.net/Bugs/Subscriptions",
2350+ "urlPingSuffix": "DevEx,5095.1",
2351+ "snippet": "Overview. Launchpad uses notification emails and Atom feeds to help you stay on top of the bugs that interest you. Bug mail. There are three ways to get bug ..."
2352+ },
2353+ {
2354+ "name": "Bugs/YourProject",
2355+ "url": "https://help.launchpad.net/Bugs/YourProject",
2356+ "urlPingSuffix": "DevEx,5096.1",
2357+ "snippet": "Overview. Launchpad's bug tracker is unique: it can track how one bug affects different communities. When you share free software, you share its bugs."
2358+ },
2359+ {
2360+ "name": "Bugs/Expiry",
2361+ "url": "https://help.launchpad.net/Bugs/Expiry",
2362+ "urlPingSuffix": "DevEx,5097.1",
2363+ "snippet": "This gives you three benefits: you can view a report of all bugs that are due to expiry and so deal with any that need your attention ; once the bug's expired ..."
2364+ }
2365+ ],
2366+ "dateLastCrawled": "2018-02-20T23:45:00.0000000Z",
2367+ "fixedPosition": false
2368+ },
2369+ {
2370+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
2371+ "name": "Of Bugs and Statuses - Launchpad Blog",
2372+ "url": "https://blog.launchpad.net/general/of-bugs-and-statuses",
2373+ "urlPingSuffix": "DevEx,5149.1",
2374+ "isFamilyFriendly": true,
2375+ "displayUrl": "https://blog.launchpad.net/general/of-bugs-and-statuses",
2376+ "snippet": "This past week’s Bug Janitor thread has made it clear that we need better descriptions of what our bug statuses actually mean. While it’s true that many project ...",
2377+ "dateLastCrawled": "2018-03-09T07:40:00.0000000Z",
2378+ "fixedPosition": false
2379+ },
2380+ {
2381+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
2382+ "name": "Mahara 1.8.0",
2383+ "url": "https://launchpad.net/mahara/+milestone/1.8.0",
2384+ "urlPingSuffix": "DevEx,5178.1",
2385+ "isFamilyFriendly": true,
2386+ "displayUrl": "https://launchpad.net/mahara/+milestone/1.8.0",
2387+ "snippet": "Mahara 1.8.0 Release Notes. This is a stable release of Mahara 1.8. Stable releases are fit for general use. If you find a bug, please report it to the tracker:",
2388+ "dateLastCrawled": "2018-03-04T10:53:00.0000000Z",
2389+ "fixedPosition": false
2390+ },
2391+ {
2392+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
2393+ "name": "Mighty Box in Launchpad",
2394+ "url": "https://launchpad.net/mb",
2395+ "urlPingSuffix": "DevEx,5193.1",
2396+ "isFamilyFriendly": true,
2397+ "displayUrl": "https://launchpad.net/mb",
2398+ "snippet": "All bugs Latest bugs reported. Bug #766411: The settings dialog box is not available on Windows 7 Reported on 2011-04-19 Bug #766410: App crashes when navigating file ...",
2399+ "dateLastCrawled": "2018-03-10T07:57:00.0000000Z",
2400+ "fixedPosition": false
2401+ },
2402+ {
2403+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4",
2404+ "name": "Bug tracking - Launchpad Bugs",
2405+ "url": "https://launchpad.net/bugs",
2406+ "urlPingSuffix": "DevEx,5208.1",
2407+ "about": [
2408+ {
2409+ "name": "Launchpad"
2410+ }
2411+ ],
2412+ "isFamilyFriendly": true,
2413+ "displayUrl": "https://launchpad.net/bugs",
2414+ "snippet": "Statistics. 1755185 bugs reported across 12480 projects including 122769 links to 2796 bug trackers; 148160 bugs are shared across multiple projects",
2415+ "dateLastCrawled": "2018-03-14T01:18:00.0000000Z",
2416+ "fixedPosition": false
2417+ }
2418+ ]
2419+ },
2420+ "rankingResponse": {
2421+ "mainline": {
2422+ "items": [
2423+ {
2424+ "answerType": "WebPages",
2425+ "resultIndex": 0,
2426+ "value": {
2427+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0"
2428+ }
2429+ },
2430+ {
2431+ "answerType": "WebPages",
2432+ "resultIndex": 1,
2433+ "value": {
2434+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1"
2435+ }
2436+ },
2437+ {
2438+ "answerType": "WebPages",
2439+ "resultIndex": 2,
2440+ "value": {
2441+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2"
2442+ }
2443+ },
2444+ {
2445+ "answerType": "WebPages",
2446+ "resultIndex": 3,
2447+ "value": {
2448+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3"
2449+ }
2450+ },
2451+ {
2452+ "answerType": "WebPages",
2453+ "resultIndex": 4,
2454+ "value": {
2455+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4"
2456+ }
2457+ }
2458+ ]
2459+ }
2460+ }
2461+}
2462+
2463
2464=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json'
2465--- lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json 1970-01-01 00:00:00 +0000
2466+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json 2018-03-29 12:55:41 +0000
2467@@ -0,0 +1,20 @@
2468+{
2469+ "_type": "ErrorResponse",
2470+ "errors": [
2471+ {
2472+ "code": "InvalidRequest",
2473+ "subCode": "ParameterInvalidValue",
2474+ "message": "Parameter has invalid value.",
2475+ "parameter": "count",
2476+ "value": "-1"
2477+ },
2478+ {
2479+ "code": "InvalidRequest",
2480+ "subCode": "ParameterInvalidValue",
2481+ "message": "Parameter has invalid value.",
2482+ "parameter": "offset",
2483+ "value": "a"
2484+ }
2485+ ]
2486+}
2487+
2488
2489=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json'
2490--- lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json 1970-01-01 00:00:00 +0000
2491+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json 2018-03-29 12:55:41 +0000
2492@@ -0,0 +1,8 @@
2493+{
2494+ "_type": "SearchResponse",
2495+ "webPages": {
2496+ "totalEstimatedMatches": "~25",
2497+ "value": []
2498+ }
2499+}
2500+
2501
2502=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json'
2503--- lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json 1970-01-01 00:00:00 +0000
2504+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json 2018-03-29 12:55:41 +0000
2505@@ -0,0 +1,1 @@
2506+
2507
2508=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt'
2509--- lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt 1970-01-01 00:00:00 +0000
2510+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt 2018-03-29 12:55:41 +0000
2511@@ -0,0 +1,26 @@
2512+# This file defines a mapping of Bing search service URLs to the JSON
2513+# files that should be returned by them.
2514+#
2515+# The format is 'url JSONfile'. Blank lines and lines starting with '#'
2516+# are ignored.
2517+#
2518+# The special URL, '*', is returned for all un-mapped URLs.
2519+
2520+* bingsearchservice-no-results.json
2521+
2522+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=bug bingsearchservice-bugs-1.json
2523+
2524+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=20&q=bug bingsearchservice-bugs-2.json
2525+
2526+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=launchpad bingsearchservice-bugs-1.json
2527+
2528+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=20&q=launchpad bingsearchservice-bugs-2.json
2529+
2530+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=gnomebaker bingsearchservice-incomplete-response.json
2531+
2532+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=no-meaningful bingsearchservice-no-meaningful-results.json
2533+
2534+/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=errors-please bingsearchservice-error.json
2535+
2536+# This stub service is also used to impersonate the Blog feed
2537+/blog-feed blog.launchpad.net-feed.json
2538
2539=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json'
2540--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json 1970-01-01 00:00:00 +0000
2541+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json 2018-03-29 12:55:41 +0000
2542@@ -0,0 +1,30 @@
2543+{
2544+ "_type": "SearchResponse",
2545+ "webPages": {
2546+ "totalEstimatedMatches": -25,
2547+ "value": [
2548+ {
2549+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
2550+ "name": "Ubuntu Bugs in Launchpad",
2551+ "url": "https://launchpad.net/~ubuntu-bugs",
2552+ "urlPingSuffix": "DevEx,5080.1",
2553+ "isFamilyFriendly": true,
2554+ "displayUrl": "https://launchpad.net/~ubuntu-bugs",
2555+ "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
2556+ "fixedPosition": false
2557+ },
2558+ {
2559+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
2560+ "name": "BugExpiry - Launchpad Help",
2561+ "url": "https://help.launchpad.net/BugExpiry",
2562+ "urlPingSuffix": "DevEx,5110.1",
2563+ "isFamilyFriendly": true,
2564+ "displayUrl": "https://help.launchpad.net/BugExpiry",
2565+ "snippet": "Want to know more about Bug Statuses? Check out our BugStatuses page. What is bug expiry? Today, projects and distributions have the option of allowing old ...",
2566+ "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
2567+ "fixedPosition": false
2568+ }
2569+ ]
2570+ }
2571+}
2572+
2573
2574=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json'
2575--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json 1970-01-01 00:00:00 +0000
2576+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json 2018-03-29 12:55:41 +0000
2577@@ -0,0 +1,30 @@
2578+{
2579+ "_type": "SearchResponse",
2580+ "webPages": {
2581+ "totalEstimatedMatches": -25,
2582+ "value": [
2583+ {
2584+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
2585+ "url": "https://launchpad.net/~ubuntu-bugs",
2586+ "urlPingSuffix": "DevEx,5080.1",
2587+ "isFamilyFriendly": true,
2588+ "displayUrl": "https://launchpad.net/~ubuntu-bugs",
2589+ "snippet": "The purpose of this team is to have a mailing list that receives all Ubuntu bug mail. If you wish to receive copies of all bug traffic, instead subscribe to the ...",
2590+ "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
2591+ "fixedPosition": false
2592+ },
2593+ {
2594+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
2595+ "name": "GCleaner in Launchpad",
2596+ "url": "https://launchpad.net/gcleaner",
2597+ "urlPingSuffix": "DevEx,5095.1",
2598+ "isFamilyFriendly": true,
2599+ "displayUrl": "https://launchpad.net/gcleaner",
2600+ "snippet": "GCleaner is a beautiful and fast system cleaner for Elementary OS and Ubuntu or based. Writen in Vala, GTK+, Granite and GLib/GIO for the purpose that the users feel ...",
2601+ "dateLastCrawled": "2018-02-21T13:17:00.0000000Z",
2602+ "fixedPosition": false
2603+ }
2604+ ]
2605+ }
2606+}
2607+
2608
2609=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json'
2610--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json 1970-01-01 00:00:00 +0000
2611+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json 2018-03-29 12:55:41 +0000
2612@@ -0,0 +1,30 @@
2613+{
2614+ "_type": "SearchResponse",
2615+ "webPages": {
2616+ "totalEstimatedMatches": -25,
2617+ "value": [
2618+ {
2619+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
2620+ "name": "Ubuntu Bugs in Launchpad",
2621+ "urlPingSuffix": "DevEx,5080.1",
2622+ "isFamilyFriendly": true,
2623+ "displayUrl": "https://launchpad.net/~ubuntu-bugs",
2624+ "snippet": "The purpose of this team is to have a mailing list that receives all Ubuntu bug mail. If you wish to receive copies of all bug traffic, instead subscribe to the ...",
2625+ "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
2626+ "fixedPosition": false
2627+ },
2628+ {
2629+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
2630+ "name": "LongoMatch in Launchpad",
2631+ "url": "https://launchpad.net/longomatch",
2632+ "urlPingSuffix": "DevEx,5125.1",
2633+ "isFamilyFriendly": true,
2634+ "displayUrl": "https://launchpad.net/longomatch",
2635+ "snippet": "LongoMatch is a sports video analyse tool for coaches to assist them on making live video reports from a match. It creates a database with the most important plays of ...",
2636+ "dateLastCrawled": "2018-02-16T21:54:00.0000000Z",
2637+ "fixedPosition": false
2638+ }
2639+ ]
2640+ }
2641+}
2642+
2643
2644=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json'
2645--- lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json 1970-01-01 00:00:00 +0000
2646+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json 2018-03-29 12:55:41 +0000
2647@@ -0,0 +1,8 @@
2648+{
2649+ "_type": "SearchResponse",
2650+ "webPages": {
2651+ "totalEstimatedMatches": -25,
2652+ "value": []
2653+ }
2654+}
2655+
2656
2657=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json'
2658--- lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json 1970-01-01 00:00:00 +0000
2659+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json 2018-03-29 12:55:41 +0000
2660@@ -0,0 +1,19 @@
2661+{
2662+ "_type": "SearchResponse",
2663+ "webPages": {
2664+ "totalEstimatedMatches": 25,
2665+ "value": [
2666+ {
2667+ "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
2668+ "url": "https://help.launchpad.net/BugExpiry",
2669+ "urlPingSuffix": "DevEx,5110.1",
2670+ "isFamilyFriendly": true,
2671+ "displayUrl": "https://help.launchpad.net/BugExpiry",
2672+ "snippet": "Want to know more about Bug Statuses? Check out our BugStatuses page. What is bug expiry? Today, projects and distributions have the option of allowing old ...",
2673+ "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
2674+ "fixedPosition": false
2675+ }
2676+ ]
2677+ }
2678+}
2679+
2680
2681=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json'
2682--- lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json 1970-01-01 00:00:00 +0000
2683+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json 2018-03-29 12:55:41 +0000
2684@@ -0,0 +1,12 @@
2685+{
2686+ "_type": "SearchResponse",
2687+ "instrumentation": {
2688+ "pingUrlBase": "https://www.bingapis.com/api/ping?IG=74D50DC66A774E74AF486941DDF2C89C&CID=2023A1AFC6086AE52F08AA1BC7476B1F&ID=",
2689+ "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=74D50DC66A774E74AF486941DDF2C89C&CID=2023A1AFC6086AE52F08AA1BC7476B1F&Type=Event.CPT&DATA=0"
2690+ },
2691+ "queryContext": {
2692+ "originalQuery": "AELymgURIr4plE6V5qUaesxj1S8kUSFxCVrVLNU_OeCogh9Q7W5be6lEGMcbb0q6WTDgLL7zsnlnYGLvVrsdxgx3AamFm0M6ARaxerSLvSf-1JQHrOLuhsQ"
2693+ },
2694+ "rankingResponse": {}
2695+}
2696+
2697
2698=== added file 'lib/lp/services/sitesearch/tests/test_bing.py'
2699--- lib/lp/services/sitesearch/tests/test_bing.py 1970-01-01 00:00:00 +0000
2700+++ lib/lp/services/sitesearch/tests/test_bing.py 2018-03-29 12:55:41 +0000
2701@@ -0,0 +1,118 @@
2702+# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
2703+# GNU Affero General Public License version 3 (see the file LICENSE).
2704+
2705+"""Test the bing search service."""
2706+
2707+__metaclass__ = type
2708+
2709+
2710+from fixtures import MockPatch
2711+from requests.exceptions import (
2712+ ConnectionError,
2713+ HTTPError,
2714+ )
2715+
2716+from lp.services.sitesearch import BingSearchService
2717+from lp.services.sitesearch.interfaces import SiteSearchResponseError
2718+from lp.services.timeout import TimeoutError
2719+from lp.testing import TestCase
2720+from lp.testing.layers import (
2721+ BingLaunchpadFunctionalLayer,
2722+ LaunchpadFunctionalLayer,
2723+ )
2724+
2725+
2726+class TestBingSearchService(TestCase):
2727+ """Test BingSearchService."""
2728+
2729+ layer = LaunchpadFunctionalLayer
2730+
2731+ def setUp(self):
2732+ super(TestBingSearchService, self).setUp()
2733+ self.search_service = BingSearchService()
2734+
2735+ def test_search_converts_HTTPError(self):
2736+ # The method converts HTTPError to SiteSearchResponseError.
2737+ args = ('url', 500, 'oops', {}, None)
2738+ self.useFixture(MockPatch(
2739+ 'lp.services.sitesearch.urlfetch', side_effect=HTTPError(*args)))
2740+ self.assertRaises(
2741+ SiteSearchResponseError, self.search_service.search, 'fnord')
2742+
2743+ def test_search_converts_ConnectionError(self):
2744+ # The method converts ConnectionError to SiteSearchResponseError.
2745+ self.useFixture(MockPatch(
2746+ 'lp.services.sitesearch.urlfetch',
2747+ side_effect=ConnectionError('oops')))
2748+ self.assertRaises(
2749+ SiteSearchResponseError, self.search_service.search, 'fnord')
2750+
2751+ def test_search_converts_TimeoutError(self):
2752+ # The method converts TimeoutError to SiteSearchResponseError.
2753+ self.useFixture(MockPatch(
2754+ 'lp.services.sitesearch.urlfetch',
2755+ side_effect=TimeoutError('oops')))
2756+ self.assertRaises(
2757+ SiteSearchResponseError, self.search_service.search, 'fnord')
2758+
2759+ def test_parse_bing_response_TypeError(self):
2760+ # The method converts TypeError to SiteSearchResponseError.
2761+ self.assertRaises(
2762+ SiteSearchResponseError,
2763+ self.search_service._parse_bing_response, None)
2764+
2765+ def test_parse_bing_response_ValueError(self):
2766+ # The method converts ValueError to SiteSearchResponseError.
2767+ self.assertRaises(
2768+ SiteSearchResponseError,
2769+ self.search_service._parse_bing_response, '')
2770+
2771+ def test_parse_bing_response_KeyError(self):
2772+ # The method converts KeyError to SiteSearchResponseError.
2773+ self.assertRaises(
2774+ SiteSearchResponseError,
2775+ self.search_service._parse_bing_response, '{}')
2776+
2777+
2778+class FunctionalTestBingSearchService(TestCase):
2779+ """Test BingSearchService."""
2780+
2781+ layer = BingLaunchpadFunctionalLayer
2782+
2783+ def setUp(self):
2784+ super(FunctionalTestBingSearchService, self).setUp()
2785+ self.search_service = BingSearchService()
2786+
2787+ def test_search_with_results(self):
2788+ matches = self.search_service.search('bug')
2789+ self.assertEqual(0, matches.start)
2790+ self.assertEqual(25, matches.total)
2791+ self.assertEqual(20, len(matches))
2792+
2793+ def test_search_with_results_and_offset(self):
2794+ matches = self.search_service.search('bug', start=20)
2795+ self.assertEqual(20, matches.start)
2796+ self.assertEqual(25, matches.total)
2797+ self.assertEqual(5, len(matches))
2798+
2799+ def test_search_no_results(self):
2800+ matches = self.search_service.search('fnord')
2801+ self.assertEqual(0, matches.start)
2802+ self.assertEqual(0, matches.total)
2803+ self.assertEqual(0, len(matches))
2804+
2805+ def test_search_no_meaningful_results(self):
2806+ matches = self.search_service.search('no-meaningful')
2807+ self.assertEqual(0, matches.start)
2808+ self.assertEqual(25, matches.total)
2809+ self.assertEqual(0, len(matches))
2810+
2811+ def test_search_incomplete_response(self):
2812+ self.assertRaises(
2813+ SiteSearchResponseError,
2814+ self.search_service.search, 'gnomebaker')
2815+
2816+ def test_search_error_response(self):
2817+ self.assertRaises(
2818+ SiteSearchResponseError,
2819+ self.search_service.search, 'errors-please')
2820
2821=== added file 'lib/lp/services/sitesearch/tests/test_bingharness.py'
2822--- lib/lp/services/sitesearch/tests/test_bingharness.py 1970-01-01 00:00:00 +0000
2823+++ lib/lp/services/sitesearch/tests/test_bingharness.py 2018-03-29 12:55:41 +0000
2824@@ -0,0 +1,10 @@
2825+# Copyright 2018 Canonical Ltd. This software is licensed under the
2826+# GNU Affero General Public License version 3 (see the file LICENSE).
2827+
2828+import doctest
2829+
2830+
2831+def test_suite():
2832+ return doctest.DocTestSuite(
2833+ 'lp.services.sitesearch.tests.bingserviceharness',
2834+ optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
2835
2836=== added file 'lib/lp/services/sitesearch/tests/test_bingservice.py'
2837--- lib/lp/services/sitesearch/tests/test_bingservice.py 1970-01-01 00:00:00 +0000
2838+++ lib/lp/services/sitesearch/tests/test_bingservice.py 2018-03-29 12:55:41 +0000
2839@@ -0,0 +1,38 @@
2840+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2841+# GNU Affero General Public License version 3 (see the file LICENSE).
2842+
2843+"""
2844+Unit tests for the Bing test service stub.
2845+"""
2846+
2847+__metaclass__ = type
2848+
2849+
2850+import os
2851+import unittest
2852+
2853+from lp.services.osutils import process_exists
2854+from lp.services.pidfile import pidfile_path
2855+from lp.services.sitesearch import bingtestservice
2856+
2857+
2858+class TestServiceUtilities(unittest.TestCase):
2859+ """Test the service's supporting functions."""
2860+
2861+ def test_stale_pid_file_cleanup(self):
2862+ """The service should be able to clean up invalid PID files."""
2863+ bogus_pid = 9999999
2864+ self.assertFalse(
2865+ process_exists(bogus_pid),
2866+ "There is already a process with PID '%d'." % bogus_pid)
2867+
2868+ # Create a stale/bogus PID file.
2869+ filepath = pidfile_path(bingtestservice.service_name)
2870+ with file(filepath, 'w') as pidfile:
2871+ pidfile.write(str(bogus_pid))
2872+
2873+ # The PID clean-up code should silently remove the file and return.
2874+ bingtestservice.kill_running_process()
2875+ self.assertFalse(
2876+ os.path.exists(filepath),
2877+ "The PID file '%s' should have been deleted." % filepath)
2878
2879=== modified file 'lib/lp/services/sitesearch/tests/test_doc.py'
2880--- lib/lp/services/sitesearch/tests/test_doc.py 2012-01-01 02:58:52 +0000
2881+++ lib/lp/services/sitesearch/tests/test_doc.py 2018-03-29 12:55:41 +0000
2882@@ -9,6 +9,7 @@
2883
2884 from lp.services.testing import build_test_suite
2885 from lp.testing.layers import (
2886+ BingLaunchpadFunctionalLayer,
2887 DatabaseFunctionalLayer,
2888 GoogleLaunchpadFunctionalLayer,
2889 )
2890@@ -23,6 +24,10 @@
2891
2892
2893 special = {
2894+ 'bing-searchservice.txt': LayeredDocFileSuite(
2895+ '../doc/bing-searchservice.txt',
2896+ setUp=setUp, tearDown=tearDown,
2897+ layer=BingLaunchpadFunctionalLayer,),
2898 'google-searchservice.txt': LayeredDocFileSuite(
2899 '../doc/google-searchservice.txt',
2900 setUp=setUp, tearDown=tearDown,
2901
2902=== modified file 'lib/lp/testing/layers.py'
2903--- lib/lp/testing/layers.py 2018-03-16 14:55:41 +0000
2904+++ lib/lp/testing/layers.py 2018-03-29 12:55:41 +0000
2905@@ -1,4 +1,4 @@
2906-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2907+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2908 # GNU Affero General Public License version 3 (see the file LICENSE).
2909
2910 """Layers used by Launchpad tests.
2911@@ -23,6 +23,8 @@
2912 'AppServerLayer',
2913 'AuditorLayer',
2914 'BaseLayer',
2915+ 'BingLaunchpadFunctionalLayer',
2916+ 'BingServiceLayer',
2917 'DatabaseFunctionalLayer',
2918 'DatabaseLayer',
2919 'FunctionalLayer',
2920@@ -126,6 +128,9 @@
2921 from lp.services.osutils import kill_by_pidfile
2922 from lp.services.rabbit.server import RabbitServer
2923 from lp.services.scripts import execute_zcml_for_scripts
2924+from lp.services.sitesearch.tests.bingserviceharness import (
2925+ BingServiceTestSetup,
2926+ )
2927 from lp.services.sitesearch.tests.googleserviceharness import (
2928 GoogleServiceTestSetup,
2929 )
2930@@ -1259,6 +1264,31 @@
2931 pass
2932
2933
2934+class BingServiceLayer(BaseLayer):
2935+ """Tests for Bing web service integration."""
2936+
2937+ @classmethod
2938+ def setUp(cls):
2939+ bing = BingServiceTestSetup()
2940+ bing.setUp()
2941+
2942+ @classmethod
2943+ def tearDown(cls):
2944+ BingServiceTestSetup().tearDown()
2945+
2946+ @classmethod
2947+ def testSetUp(self):
2948+ # We need to override BaseLayer.testSetUp(), or else we will
2949+ # get a LayerIsolationError.
2950+ pass
2951+
2952+ @classmethod
2953+ def testTearDown(self):
2954+ # We need to override BaseLayer.testTearDown(), or else we will
2955+ # get a LayerIsolationError.
2956+ pass
2957+
2958+
2959 class DatabaseFunctionalLayer(DatabaseLayer, FunctionalLayer):
2960 """Provides the database and the Zope3 application server environment."""
2961
2962@@ -1383,6 +1413,31 @@
2963 pass
2964
2965
2966+class BingLaunchpadFunctionalLayer(LaunchpadFunctionalLayer,
2967+ BingServiceLayer):
2968+ """Provides Bing service in addition to LaunchpadFunctionalLayer."""
2969+
2970+ @classmethod
2971+ @profiled
2972+ def setUp(cls):
2973+ pass
2974+
2975+ @classmethod
2976+ @profiled
2977+ def tearDown(cls):
2978+ pass
2979+
2980+ @classmethod
2981+ @profiled
2982+ def testSetUp(cls):
2983+ pass
2984+
2985+ @classmethod
2986+ @profiled
2987+ def testTearDown(cls):
2988+ pass
2989+
2990+
2991 class ZopelessDatabaseLayer(ZopelessLayer, DatabaseLayer):
2992 """Testing layer for unit tests with no need for librarian.
2993
2994@@ -1541,7 +1596,8 @@
2995 return self.request._orig_env
2996
2997
2998-class PageTestLayer(LaunchpadFunctionalLayer, GoogleServiceLayer):
2999+class PageTestLayer(LaunchpadFunctionalLayer,
3000+ BingServiceLayer, GoogleServiceLayer):
3001 """Environment for page tests.
3002 """
3003
3004
3005=== modified file 'setup.py'
3006--- setup.py 2018-03-16 15:14:34 +0000
3007+++ setup.py 2018-03-29 12:55:41 +0000
3008@@ -284,6 +284,8 @@
3009 },
3010 entry_points=dict(
3011 console_scripts=[ # `console_scripts` is a magic name to setuptools
3012+ 'bingtestservice = '
3013+ 'lp.services.sitesearch.bingtestservice:main',
3014 'build-twisted-plugin-cache = '
3015 'lp.services.twistedsupport.plugincache:main',
3016 'combine-css = lp.scripts.utilities.js.combinecss:main',