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
=== modified file 'Makefile'
--- Makefile 2018-02-01 20:56:23 +0000
+++ Makefile 2018-03-29 12:55:41 +0000
@@ -61,6 +61,7 @@
61# NB: It's important PIP_BIN only mentions things genuinely produced by pip.61# NB: It's important PIP_BIN only mentions things genuinely produced by pip.
62PIP_BIN = \62PIP_BIN = \
63 $(PY) \63 $(PY) \
64 bin/bingtestservice \
64 bin/build-twisted-plugin-cache \65 bin/build-twisted-plugin-cache \
65 bin/combine-css \66 bin/combine-css \
66 bin/googletestservice \67 bin/googletestservice \
@@ -284,7 +285,7 @@
284 bin/test -f $(TESTFLAGS) $(TESTOPTS)285 bin/test -f $(TESTFLAGS) $(TESTOPTS)
285286
286run: build inplace stop287run: build inplace stop
287 bin/run -r librarian,google-webservice,memcached,rabbitmq,txlongpoll \288 bin/run -r librarian,bing-webservice,google-webservice,memcached,rabbitmq,txlongpoll \
288 -i $(LPCONFIG)289 -i $(LPCONFIG)
289290
290run-testapp: LPCONFIG=testrunner-appserver291run-testapp: LPCONFIG=testrunner-appserver
@@ -297,12 +298,12 @@
297298
298start-gdb: build inplace stop support_files run.gdb299start-gdb: build inplace stop support_files run.gdb
299 nohup gdb -x run.gdb --args bin/run -i $(LPCONFIG) \300 nohup gdb -x run.gdb --args bin/run -i $(LPCONFIG) \
300 -r librarian,google-webservice301 -r librarian,bing-webservice,google-webservice
301 > ${LPCONFIG}-nohup.out 2>&1 &302 > ${LPCONFIG}-nohup.out 2>&1 &
302303
303run_all: build inplace stop304run_all: build inplace stop
304 bin/run \305 bin/run \
305 -r librarian,sftp,forker,mailman,codebrowse,google-webservice,\306 -r librarian,sftp,forker,mailman,codebrowse,bing-webservice,google-webservice,\
306 memcached,rabbitmq,txlongpoll -i $(LPCONFIG)307 memcached,rabbitmq,txlongpoll -i $(LPCONFIG)
307308
308run_codebrowse: compile309run_codebrowse: compile
309310
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2018-02-02 15:29:38 +0000
+++ configs/development/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
@@ -79,13 +79,22 @@
79error_dir: /var/tmp/lperr79error_dir: /var/tmp/lperr
8080
81[google]81[google]
82# Development and the testrunner should use the stub service be default.82# Development and the testrunner should use the stub service by default.
83site: http://launchpad.dev:8092/cse83site: http://launchpad.dev:8092/cse
84client_id: ABCDEF232384client_id: ABCDEF2323
8585
86[google_test_service]86[google_test_service]
87launch: True87launch: True
8888
89[bing]
90# Development and the testrunner should use the stub service by default.
91site: http://launchpad.dev:8093/bingcustomsearch/v7.0/search
92subscription_key: abcdef01234567890abcdef012345678
93custom_config_id: 1234567890
94
95[bing_test_service]
96launch: True
97
89[gpghandler]98[gpghandler]
90host: keyserver.launchpad.dev99host: keyserver.launchpad.dev
91public_host: keyserver.launchpad.dev100public_host: keyserver.launchpad.dev
92101
=== modified file 'configs/testrunner-appserver/launchpad-lazr.conf'
--- configs/testrunner-appserver/launchpad-lazr.conf 2016-10-11 15:28:25 +0000
+++ configs/testrunner-appserver/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
@@ -14,6 +14,9 @@
14[google_test_service]14[google_test_service]
15launch: False15launch: False
1616
17[bing_test_service]
18launch: False
19
17[launchpad]20[launchpad]
18openid_provider_root: http://testopenid.dev:8085/21openid_provider_root: http://testopenid.dev:8085/
1922
2023
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf 2018-01-26 22:18:38 +0000
+++ configs/testrunner/launchpad-lazr.conf 2018-03-29 12:55:41 +0000
@@ -91,6 +91,9 @@
91[google]91[google]
92site: http://launchpad.dev:8092/cse92site: http://launchpad.dev:8092/cse
9393
94[bing]
95site: http://launchpad.dev:8093/bingcustomsearch/v7.0/search
96
94[gpghandler]97[gpghandler]
95upload_keys: True98upload_keys: True
96host: localhost99host: localhost
97100
=== added file 'lib/lp/app/browser/doc/launchpad-search-pages-bing.txt'
--- lib/lp/app/browser/doc/launchpad-search-pages-bing.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/browser/doc/launchpad-search-pages-bing.txt 2018-03-29 12:55:41 +0000
@@ -0,0 +1,725 @@
1Launchpad search page
2=====================
3
4Users can search for Launchpad objects and pages from the search form
5located on all pages. The search is performed and displayed by the
6LaunchpadSearchView.
7
8 >>> from zope.component import getMultiAdapter, getUtility
9 >>> from lp.services.webapp.interfaces import ILaunchpadRoot
10 >>> from lp.services.webapp.servers import LaunchpadTestRequest
11
12 >>> root = getUtility(ILaunchpadRoot)
13 >>> request = LaunchpadTestRequest()
14 >>> search_view = getMultiAdapter((root, request), name="+search")
15 >>> search_view.initialize()
16 >>> search_view
17 <....SimpleViewClass from .../templates/launchpad-search.pt ...>
18
19
20Page title and heading
21----------------------
22
23The page title and heading suggest to the user to search launchpad
24when there is no search text.
25
26 >>> print search_view.text
27 None
28 >>> search_view.page_title
29 'Search Launchpad'
30 >>> search_view.page_heading
31 'Search Launchpad'
32
33When text is not None, the title indicates what was searched.
34
35 >>> def getSearchView(form):
36 ... search_param_list = []
37 ... for name in sorted(form):
38 ... value = form[name]
39 ... search_param_list.append('%s=%s' % (name, value))
40 ... query_string = '&'.join(search_param_list)
41 ... request = LaunchpadTestRequest(
42 ... SERVER_URL='https://launchpad.dev/+search',
43 ... QUERY_STRING=query_string, form=form, PATH_INFO='/+search')
44 ... search_view = getMultiAdapter((root, request), name="+search")
45 ... search_view.initialize()
46 ... return search_view
47
48 >>> search_view = getSearchView(
49 ... form={'field.text': 'albatross'})
50
51 >>> search_view.text
52 u'albatross'
53 >>> search_view.page_title
54 u'Pages matching "albatross" in Launchpad'
55 >>> search_view.page_heading
56 u'Pages matching "albatross" in Launchpad'
57
58
59No matches
60----------
61
62There were no matches for 'albatross'.
63
64 >>> search_view.has_matches
65 False
66
67When search text is not submitted there are no matches. Search text is
68required to perform a search. Note that field.actions.search is not a
69required param to call the Search Action. The view always calls the
70search action.
71
72 >>> search_view = getSearchView(form={})
73
74 >>> print search_view.text
75 None
76 >>> search_view.has_matches
77 False
78
79
80Bug and Question Searches
81-------------------------
82
83When a numeric token can be extracted from the submitted search text,
84the view tries to match a bug and question. Bugs and questions are
85matched by their id.
86
87 >>> search_view = getSearchView(
88 ... form={'field.text': '5'})
89 >>> search_view._getNumericToken(search_view.text)
90 u'5'
91 >>> search_view.has_matches
92 True
93 >>> search_view.bug.title
94 u'Firefox install instructions should be complete'
95 >>> search_view.question.title
96 u'Installation failed'
97
98Bugs and questions are matched independent of each other. The number
99extracted may only match one kind of object. For example, there are
100more bugs than questions.
101
102 >>> search_view = getSearchView(
103 ... form={'field.text': '15'})
104 >>> search_view._getNumericToken(search_view.text)
105 u'15'
106 >>> search_view.has_matches
107 True
108 >>> search_view.bug.title
109 u'Nonsensical bugs are useless'
110 >>> print search_view.question
111 None
112
113Private bugs are not matched if the user does not have permission to
114see them. For example, Sample Person can see a private bug that they
115created because they are the owner.
116
117 >>> from lp.services.webapp.interfaces import ILaunchBag
118 >>> from lp.app.enums import InformationType
119
120 >>> login('test@canonical.com')
121 >>> sample_person = getUtility(ILaunchBag).user
122 >>> private_bug = factory.makeBug(
123 ... owner=sample_person, information_type=InformationType.USERDATA)
124
125 >>> search_view = getSearchView(
126 ... form={'field.text': private_bug.id})
127 >>> search_view.bug.private
128 True
129
130But anonymous and unprivileged users cannot see the private bug.
131
132 >>> login(ANONYMOUS)
133 >>> search_view = getSearchView(
134 ... form={'field.text': private_bug.id})
135 >>> print search_view.bug
136 None
137
138The text and punctuation in the search text is ignored, and only the
139first group of numbers is matched. For example a user searches for three
140questions by number ('Question #15, #7, and 5.'). Only the first number
141is used, and it matches a bug, not a question. The second and third
142numbers do match questions, but they are not used.
143
144 >>> search_view = getSearchView(
145 ... form={'field.text': 'Question #15, #7, and 5.'})
146 >>> search_view._getNumericToken(search_view.text)
147 u'15'
148 >>> search_view.has_matches
149 True
150 >>> search_view.bug.title
151 u'Nonsensical bugs are useless'
152 >>> print search_view.question
153 None
154
155It is not an error to search for a non-existent bug or question.
156
157 >>> search_view = getSearchView(
158 ... form={'field.text': '55555'})
159 >>> search_view._getNumericToken(search_view.text)
160 u'55555'
161 >>> search_view.has_matches
162 False
163 >>> print search_view.bug
164 None
165 >>> print search_view.question
166 None
167
168There is no error if a number cannot be extracted from the search text.
169
170 >>> search_view = getSearchView(
171 ... form={'field.text': 'fifteen'})
172 >>> print search_view._getNumericToken(
173 ... search_view.text)
174 None
175 >>> search_view.has_matches
176 False
177 >>> print search_view.bug
178 None
179 >>> print search_view.question
180 None
181
182Bugs and questions are only returned for the first page of search,
183when the start param is 0.
184
185 >>> search_view = getSearchView(
186 ... form={'field.text': '5',
187 ... 'start': '20'})
188 >>> search_view.has_matches
189 False
190 >>> print search_view.bug
191 None
192 >>> print search_view.question
193 None
194
195
196
197Projects and Persons and Teams searches
198---------------------------------------
199
200When a Launchpad name can be made from the search text, the view tries
201to match the name to a pillar or person. a pillar is a distribution,
202product, or project group. A person is a person or a team.
203
204 >>> search_view = getSearchView(
205 ... form={'field.text': 'launchpad'})
206 >>> search_view._getNameToken(search_view.text)
207 u'launchpad'
208 >>> search_view.has_matches
209 True
210 >>> search_view.pillar.displayname
211 u'Launchpad'
212 >>> search_view.person_or_team.displayname
213 u'Launchpad Developers'
214
215A launchpad name is constructed from the search text. The letters are
216converted to lowercase. groups of spaces and punctuation are replaced
217with a hyphen.
218
219 >>> search_view = getSearchView(
220 ... form={'field.text': 'Gnome Terminal'})
221 >>> search_view._getNameToken(search_view.text)
222 u'gnome-terminal'
223 >>> search_view.has_matches
224 True
225 >>> search_view.pillar.displayname
226 u'GNOME Terminal'
227 >>> print search_view.person_or_team
228 None
229
230Since our pillars can have aliases, it's also possible to look up a pillar
231by any of its aliases.
232
233 >>> from lp.registry.interfaces.product import IProductSet
234 >>> firefox = getUtility(IProductSet)['firefox']
235 >>> login('foo.bar@canonical.com')
236 >>> firefox.setAliases(['iceweasel'])
237 >>> login(ANONYMOUS)
238 >>> search_view = getSearchView(
239 ... form={'field.text': 'iceweasel'})
240 >>> search_view._getNameToken(search_view.text)
241 u'iceweasel'
242 >>> search_view.has_matches
243 True
244 >>> search_view.pillar.displayname
245 u'Mozilla Firefox'
246
247This is a harder example that illustrates that text that is clearly not
248the name of a pillar will none-the-less be tried. See the `Page searches`
249section for how this kind of search can return matches.
250
251 >>> search_view = getSearchView(
252 ... form={'field.text': "YAHOO! webservice's Python API."})
253 >>> search_view._getNameToken(search_view.text)
254 u'yahoo-webservices-python-api.'
255 >>> search_view.has_matches
256 False
257 >>> print search_view.pillar
258 None
259 >>> print search_view.person_or_team
260 None
261
262Leading and trailing punctuation and whitespace are stripped.
263
264 >>> search_view = getSearchView(
265 ... form={'field.text': "~name12"})
266 >>> search_view._getNameToken(search_view.text)
267 u'name12'
268 >>> search_view.has_matches
269 True
270 >>> print search_view.pillar
271 None
272 >>> search_view.person_or_team.displayname
273 u'Sample Person'
274
275Pillars, persons and teams are only returned for the first page of
276search, when the start param is 0.
277
278 >>> search_view = getSearchView(
279 ... form={'field.text': 'launchpad',
280 ... 'start': '20'})
281 >>> search_view.has_matches
282 True
283 >>> print search_view.bug
284 None
285 >>> print search_view.question
286 None
287 >>> print search_view.pillar
288 None
289
290Deactivated pillars and non-valid persons and teams cannot be exact
291matches. For example, the python-gnome2-dev product will not match a
292pillar, nor will nsv match Nicolas Velin's unclaimed account.
293
294 >>> from lp.registry.interfaces.person import IPersonSet
295
296 >>> python_gnome2 = getUtility(IProductSet).getByName('python-gnome2-dev')
297 >>> python_gnome2.active
298 False
299
300 >>> search_view = getSearchView(
301 ... form={'field.text': 'python-gnome2-dev',
302 ... 'start': '0'})
303 >>> search_view._getNameToken(search_view.text)
304 u'python-gnome2-dev'
305 >>> print search_view.pillar
306 None
307
308 >>> nsv = getUtility(IPersonSet).getByName('nsv')
309 >>> nsv.displayname
310 u'Nicolas Velin'
311 >>> nsv.is_valid_person_or_team
312 False
313
314 >>> search_view = getSearchView(
315 ... form={'field.text': 'nsv',
316 ... 'start': '0'})
317 >>> search_view._getNameToken(search_view.text)
318 u'nsv'
319 >>> print search_view.person_or_team
320 None
321
322Private pillars are not matched if the user does not have permission to see
323them. For example, Sample Person can see a private project that they created
324because they are the owner.
325
326 >>> from lp.registry.interfaces.product import License
327
328 >>> login('test@canonical.com')
329 >>> private_product = factory.makeProduct(
330 ... owner=sample_person, information_type=InformationType.PROPRIETARY,
331 ... licenses=[License.OTHER_PROPRIETARY])
332 >>> private_product_name = private_product.name
333
334 >>> search_view = getSearchView(form={'field.text': private_product_name})
335 >>> search_view.pillar.private
336 True
337
338But anonymous and unprivileged users cannot see the private project.
339
340 >>> login(ANONYMOUS)
341 >>> search_view = getSearchView(form={'field.text': private_product_name})
342 >>> print search_view.pillar
343 None
344
345
346Shipit CD searches
347------------------
348
349The has_shipit property will be True when the search looks like the user
350is searching for Shipit CDs. There is no correct object in Launchpad to
351display. The page template decides how to handle when has_shipit is
352True.
353
354The match is based on an intersection to the words in the search text
355and the shipit_keywords. The comparison is case-insensitive, has_shipit
356is True when 2 or more words match.
357
358 >>> sorted(search_view.shipit_keywords)
359 ['cd', 'cds', 'disc', 'dvd', 'dvds', 'edubuntu', 'free', 'get', 'kubuntu',
360 'mail', 'send', 'ship', 'shipit', 'ubuntu']
361 >>> search_view = getSearchView(
362 ... form={'field.text': 'ubuntu CDs',
363 ... 'start': '0'})
364 >>> search_view.has_shipit
365 True
366
367 >>> search_view = getSearchView(
368 ... form={'field.text': 'shipit',
369 ... 'start': '0'})
370 >>> search_view.has_shipit
371 False
372
373 >>> search_view = getSearchView(
374 ... form={'field.text': 'get Kubuntu cds',
375 ... 'start': '0'})
376 >>> search_view.has_shipit
377 True
378
379There are shipit_anti_keywords too, words that indicate the search is
380not for free CDs from Shipit. Search that have any of these word will
381set has_shipit to False.
382
383 >>> sorted(search_view.shipit_anti_keywords)
384 ['burn', 'burning', 'enable', 'error', 'errors', 'image', 'iso',
385 'read', 'rip', 'write']
386
387 >>> search_view = getSearchView(
388 ... form={'field.text': 'ubuntu CD write',
389 ... 'start': '0'})
390 >>> search_view.has_shipit
391 False
392
393 >>> search_view = getSearchView(
394 ... form={'field.text': 'shipit error',
395 ... 'start': '0'})
396 >>> search_view.has_shipit
397 False
398
399
400The shipit FAQ URL is provides by the view for the template to use.
401
402 >>> search_view.shipit_faq_url
403 'http://www.ubuntu.com/getubuntu/shipit-faq'
404
405
406Page searches
407-------------
408
409The view uses the BingSearchService to locate pages that match the
410search terms.
411
412 >>> search_view = getSearchView(
413 ... form={'field.text': " bug"})
414 >>> search_view.text
415 u'bug'
416 >>> search_view.has_matches
417 True
418 >>> search_view.pages
419 <...SiteSearchBatchNavigator ...>
420
421The BingSearchService may not be available due to connectivity problems.
422The view's has_page_service attribute reports when the search was performed
423with Bing page matches.
424
425 >>> search_view.has_page_service
426 True
427
428The batch navigation heading is created by the view. The heading
429property returns a 2-tuple of singular and plural heading. There
430is a heading when there are only Bing page matches...
431
432 >>> search_view.has_exact_matches
433 False
434 >>> search_view.batch_heading
435 (u'page matching "bug"', u'pages matching "bug"')
436
437...and a heading for when there are exact matches and Bing page
438matches.
439
440 >>> search_view = getSearchView(
441 ... form={'field.text': " launchpad"})
442 >>> search_view.has_exact_matches
443 True
444 >>> search_view.batch_heading
445 (u'other page matching "launchpad"', u'other pages matching "launchpad"')
446
447The SiteSearchBatchNavigator behaves like most BatchNavigators, except that
448its batch size is always 20. The size restriction conforms to Google's
449maximum number of results that can be returned per request.
450
451 >>> search_view.start
452 0
453 >>> search_view.pages.currentBatch().size
454 20
455 >>> pages = list(search_view.pages.currentBatch())
456 >>> len(pages)
457 20
458 >>> for page in pages[0:5]:
459 ... page.title
460 u'Launchpad Bugs'
461 u'Bugs in Ubuntu Linux'
462 u'Bugs related to Sample Person'
463 u'Bug #1 in Mozilla Firefox: Firefox does not support SVG'
464 u'Question #232632 : Questions : OpenStack Heat'
465
466The batch navigator provides access to the other batches. There are two
467batches of pages that match the search text 'bugs'. The navigator
468provides a link to the next batch, which also happens to be the last
469batch.
470
471 >>> search_view.pages.nextBatchURL()
472 '...start=20'
473 >>> search_view.pages.lastBatchURL()
474 '...start=20'
475
476The second batch has only five matches in it, even though the batch size
477is 20. That is because there were only 25 matching pages.
478
479 >>> search_view = getSearchView(
480 ... form={'field.text': "bug",
481 ... 'start': '20'})
482 >>> search_view.start
483 20
484 >>> search_view.text
485 u'bug'
486 >>> search_view.has_matches
487 True
488
489 >>> search_view.pages.currentBatch().size
490 20
491 >>> pages = list(search_view.pages.currentBatch())
492 >>> len(pages)
493 5
494 >>> for page in pages:
495 ... page.title
496 u'Bugs - Launchpad Help'
497 u'Of Bugs and Statuses - Launchpad Blog'
498 u'Mahara 1.8.0'
499 u'Mighty Box in Launchpad'
500 u'Bug tracking - Launchpad Bugs'
501
502 >>> search_view.pages.nextBatchURL()
503 ''
504 >>> search_view.pages.lastBatchURL()
505 ''
506
507The PageMatch object has a title, url, and summary. The title and url
508are used for making links to the pages. The summary contains markup
509showing the matching terms in context of the page text.
510
511 >>> print range(20)
512 [0, 1, ..., 18, 19]
513 >>> page = pages[0]
514 >>> page
515 <...PageMatch ...>
516 >>> page.title
517 u'Bugs - Launchpad Help'
518 >>> page.url
519 'https://help.launchpad.net/Bugs'
520 >>> page.summary # doctest: +ELLIPSIS
521 u"Launchpad Help > Bugs . Use Launchpad's bug tracker for your project..."
522
523See `google-searchservice.txt` for more information about the
524BingSearchService and PageMatch objects.
525
526
527No page matches
528---------------
529
530When an empty PageMatches object is returned by the BingSearchService to
531the view, there are no matches to show.
532
533 >>> search_view = getSearchView(form={'field.text': 'no-meaningful'})
534 >>> search_view.has_matches
535 False
536
537
538Unintelligible searches
539-----------------------
540
541When a user searches for a malformed string, we don't OOPS, but show an
542error. Also disable warnings, since we are tossing around malformed Unicode.
543
544 >>> import warnings
545 >>> with warnings.catch_warnings():
546 ... warnings.simplefilter('ignore')
547 ... search_view = getSearchView(
548 ... form={'field.text': '\xfe\xfckr\xfc'})
549 >>> html = search_view()
550 >>> 'Can not convert your search term' in html
551 True
552
553
554Bad Bing response handling
555----------------------------
556
557Connectivity problems can cause missing or incomplete responses from
558Bing. The LaunchpadSearchView will display the other searches and
559show a message explaining that the user can search again to find
560matching pages.
561
562 >>> search_view = getSearchView(form={'field.text': 'gnomebaker'})
563 >>> search_view.has_matches
564 True
565 >>> search_view.pillar.displayname
566 u'gnomebaker'
567 >>> search_view.has_page_service
568 False
569
570The view provides the requested URL so that the template can make a
571link to try the search again
572
573 >>> print search_view.url
574 https://launchpad.dev/+search?field.text=gnomebaker
575
576
577SearchFormView and SearchFormPrimaryView
578----------------------------------------
579
580Two companion views are used to help render the global search form.
581They define the required attributes to render the form in the
582correct state.
583
584The LaunchpadSearchFormView provides the minimum information to display
585the form, but cannot handled the submitted data. It appends a suffix
586('-secondary') to the id= and name= of the form and inputs, to prevent
587them from conflicting with the other form. The search text is not the
588default value of the text field; 'bug' was submitted above, but is not
589present in the rendered form.
590
591 >>> search_form_view = getMultiAdapter(
592 ... (search_view, request), name='+search-form')
593 >>> search_form_view.initialize()
594 >>> search_form_view.id_suffix
595 '-secondary'
596 >>> print search_form_view.render()
597 <form action="http://launchpad.dev/+search" method="get"
598 accept-charset="UTF-8" id="sitesearch-secondary"
599 name="sitesearch-secondary">
600 <div>
601 <input class="textType" type="text" size="36"
602 id="field.text-secondary" name="field.text" />
603 <input class="button" type="submit" value="Search"
604 id="field.text-secondary" name="field.actions.search-secondary" />
605 </div>
606 </form>
607
608LaunchpadPrimarySearchFormView can handle submitted form by deferring to
609its context (the LaunchpadSearchView) for the needed information. The
610view does not append a suffix to the form and input ids. The search
611field's value is 'bug', as was submitted above.
612
613 >>> search_form_view = getMultiAdapter(
614 ... (search_view, request), name='+primary-search-form')
615 >>> search_form_view.initialize()
616 >>> search_form_view.id_suffix
617 ''
618 >>> print search_form_view.render()
619 <form action="http://launchpad.dev/+search" method="get"
620 accept-charset="UTF-8" id="sitesearch"
621 name="sitesearch">
622 <div>
623 <input class="textType" type="text" size="36"
624 id="field.text" value="gnomebaker" name="field.text" />
625 <input class="button" type="submit" value="Search"
626 id="field.text" name="field.actions.search" />
627 </div>
628 </form>
629
630WindowedList and SiteSearchBatchNavigator
631-------------------------------------
632
633The LaunchpadSearchView uses two helper classes to work with
634PageMatches.
635
636The PageMatches object returned by the BingSearchService contains 20
637or fewer PageMatches of what could be thousands of matches. Bing
638requires client's to make repeats request to step though the batches of
639matches. The Windowed list is a list that contains only a subset of its
640reported size. It is used to make batches in the SiteSearchBatchNavigator.
641
642For example, the last batch of the 'bug' search contained 5 of the 25
643matching pages. The WindowList claims to be 25 items in length, but
644the first 20 items are None. Only the last 5 items are PageMatches.
645
646 >>> from lp.app.browser.root import WindowedList
647 >>> from lp.services.sitesearch import BingSearchService
648
649 >>> bing_search = BingSearchService()
650 >>> page_matches = bing_search.search(terms='bug', start=20)
651 >>> results = WindowedList(
652 ... page_matches, page_matches.start, page_matches.total)
653 >>> len(results)
654 25
655 >>> print results[0]
656 None
657 >>> results[24].title
658 u'Bug tracking - Launchpad Bugs'
659 >>> results[18, 22]
660 [None, None, <...PageMatch ...>, <...PageMatch ...>]
661
662The SiteSearchBatchNavigator restricts the batch size to 20. the 'batch'
663parameter that comes from the URL is ignored. For example, setting
664the 'batch' parameter to 100 has no affect upon the Bing search
665or on the navigator object.
666
667 >>> from lp.app.browser.root import SiteSearchBatchNavigator
668
669 >>> SiteSearchBatchNavigator.batch_variable_name
670 'batch'
671
672 >>> search_view = getSearchView(
673 ... form={'field.text': "bug",
674 ... 'start': '0',
675 ... 'batch': '100',})
676
677 >>> navigator = search_view.pages
678 >>> navigator.currentBatch().size
679 20
680 >>> len(navigator.currentBatch())
681 20
682 >>> navigator.nextBatchURL()
683 '...start=20'
684
685Even if the PageMatch object to have an impossibly large size, the
686navigator conforms to Google's maximum size of 20.
687
688 >>> matches = list(range(0, 100))
689 >>> page_matches._matches = matches
690 >>> page_matches.start = 0
691 >>> page_matches.total = 100
692 >>> navigator = SiteSearchBatchNavigator(
693 ... page_matches, search_view.request, page_matches.start, size=100)
694 >>> navigator.currentBatch().size
695 20
696 >>> len(navigator.currentBatch())
697 20
698 >>> navigator.nextBatchURL()
699 '...start=20'
700
701The PageMatches object can be smaller than 20, for instance, pages
702without titles are skipped when parsing the Bing Search JSON. The size
703of the batch is still 20, but when the items in the batch are iterated,
704the true size can be seen. For example there could be only 3 matches in
705the PageMatches object, so only 3 are yielded. The start of the next
706batch is 20, which is the start of the next batch from Bing.
707
708 >>> matches = list(range(0, 3))
709 >>> page_matches._matches = matches
710 >>> navigator = SiteSearchBatchNavigator(
711 ... page_matches, search_view.request, page_matches.start, size=100)
712 >>> batch = navigator.currentBatch()
713 >>> batch.size
714 20
715 >>> len(batch)
716 20
717 >>> batch.endNumber()
718 3
719 >>> for item in batch:
720 ... print item
721 0
722 1
723 2
724 >>> navigator.nextBatchURL()
725 '...start=20'
0726
=== modified file 'lib/lp/app/browser/root.py'
--- lib/lp/app/browser/root.py 2018-03-27 14:31:36 +0000
+++ lib/lp/app/browser/root.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3"""Browser code for the Launchpad root page."""3"""Browser code for the Launchpad root page."""
44
@@ -510,18 +510,21 @@
510 def searchPages(self, query_terms, start=0):510 def searchPages(self, query_terms, start=0):
511 """Return the up to 20 pages that match the query_terms, or None.511 """Return the up to 20 pages that match the query_terms, or None.
512512
513 :param query_terms: The unescaped terms to query Google.513 :param query_terms: The unescaped terms to query for.
514 :param start: The index of the page that starts the set of pages.514 :param start: The index of the page that starts the set of pages.
515 :return: A GooglBatchNavigator or None.515 :return: A SiteSearchBatchNavigator or None.
516 """516 """
517 if query_terms in [None, '']:517 if query_terms in [None, '']:
518 return None518 return None
519 google_search = getUtility(ISearchService)519 search_engine = getFeatureFlag("sitesearch.engine.name")
520 # Default to the Google search engine.
521 search_engine = search_engine or "google"
522 site_search = getUtility(ISearchService, name=search_engine)
520 try:523 try:
521 page_matches = google_search.search(524 page_matches = site_search.search(
522 terms=query_terms, start=start)525 terms=query_terms, start=start)
523 except SiteSearchResponseError:526 except SiteSearchResponseError:
524 # There was a connectivity or Google service issue that means527 # There was a connectivity or search service issue that means
525 # there is no data available at this moment.528 # there is no data available at this moment.
526 self.has_page_service = False529 self.has_page_service = False
527 return None530 return None
528531
=== modified file 'lib/lp/app/browser/tests/test_views.py'
--- lib/lp/app/browser/tests/test_views.py 2018-03-28 19:23:18 +0000
+++ lib/lp/app/browser/tests/test_views.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""4"""
@@ -11,6 +11,7 @@
11from lp.services.features.testing import FeatureFixture11from lp.services.features.testing import FeatureFixture
12from lp.services.testing import build_test_suite12from lp.services.testing import build_test_suite
13from lp.testing.layers import (13from lp.testing.layers import (
14 BingLaunchpadFunctionalLayer,
14 GoogleLaunchpadFunctionalLayer,15 GoogleLaunchpadFunctionalLayer,
15 )16 )
16from lp.testing.systemdocs import (17from lp.testing.systemdocs import (
@@ -21,14 +22,42 @@
2122
2223
23here = os.path.dirname(os.path.realpath(__file__))24here = os.path.dirname(os.path.realpath(__file__))
25bing_flag = FeatureFixture({'sitesearch.engine.name': 'bing'})
26google_flag = FeatureFixture({'sitesearch.engine.name': 'google'})
27
28
29def setUp_bing(test):
30 setUp(test)
31 bing_flag.setUp()
32
33
34def setUp_google(test):
35 setUp(test)
36 google_flag.setUp()
37
38
39def tearDown_bing(test):
40 bing_flag.cleanUp()
41 tearDown(test)
42
43
44def tearDown_google(test):
45 google_flag.cleanUp()
46 tearDown(test)
47
2448
25# The default layer of view tests is the DatabaseFunctionalLayer. Tests49# The default layer of view tests is the DatabaseFunctionalLayer. Tests
26# that require something special like the librarian or mailman must run50# that require something special like the librarian or mailman must run
27# on a layer that sets those services up.51# on a layer that sets those services up.
28special = {52special = {
53 'launchpad-search-pages-bing.txt': LayeredDocFileSuite(
54 '../doc/launchpad-search-pages-bing.txt',
55 setUp=setUp_bing, tearDown=tearDown_bing,
56 layer=BingLaunchpadFunctionalLayer,
57 stdout_logging_level=logging.WARNING),
29 'launchpad-search-pages-google.txt': LayeredDocFileSuite(58 'launchpad-search-pages-google.txt': LayeredDocFileSuite(
30 '../doc/launchpad-search-pages-google.txt',59 '../doc/launchpad-search-pages-google.txt',
31 setUp=setUp, tearDown=tearDown,60 setUp=setUp_google, tearDown=tearDown_google,
32 layer=GoogleLaunchpadFunctionalLayer,61 layer=GoogleLaunchpadFunctionalLayer,
33 stdout_logging_level=logging.WARNING),62 stdout_logging_level=logging.WARNING),
34 }63 }
3564
=== modified file 'lib/lp/scripts/runlaunchpad.py'
--- lib/lp/scripts/runlaunchpad.py 2018-03-16 14:50:01 +0000
+++ lib/lp/scripts/runlaunchpad.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -26,7 +26,10 @@
26 pidfile_path,26 pidfile_path,
27 )27 )
28from lp.services.rabbit.server import RabbitServer28from lp.services.rabbit.server import RabbitServer
29from lp.services.sitesearch import googletestservice29from lp.services.sitesearch import (
30 bingtestservice,
31 googletestservice,
32 )
30from lp.services.txlongpoll.server import TxLongPollServer33from lp.services.txlongpoll.server import TxLongPollServer
3134
3235
@@ -153,6 +156,16 @@
153 self.addCleanup(stop_process, googletestservice.start_as_process())156 self.addCleanup(stop_process, googletestservice.start_as_process())
154157
155158
159class BingWebService(Service):
160
161 @property
162 def should_launch(self):
163 return config.bing_test_service.launch
164
165 def launch(self):
166 self.addCleanup(stop_process, bingtestservice.start_as_process())
167
168
156class MemcachedService(Service):169class MemcachedService(Service):
157 """A local memcached service for developer environments."""170 """A local memcached service for developer environments."""
158171
@@ -280,6 +293,7 @@
280 'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),293 'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),
281 'forker': ForkingSessionService(),294 'forker': ForkingSessionService(),
282 'mailman': MailmanService(),295 'mailman': MailmanService(),
296 'bing-webservice': BingWebService(),
283 'codebrowse': CodebrowseService(),297 'codebrowse': CodebrowseService(),
284 'google-webservice': GoogleWebService(),298 'google-webservice': GoogleWebService(),
285 'memcached': MemcachedService(),299 'memcached': MemcachedService(),
286300
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2018-03-16 14:02:16 +0000
+++ lib/lp/services/config/schema-lazr.conf 2018-03-29 12:55:41 +0000
@@ -791,7 +791,43 @@
791# url_rewrite_exceptions is a list of launchpad.net domains that must791# url_rewrite_exceptions is a list of launchpad.net domains that must
792# not be rewritten.792# not be rewritten.
793# datatype: string of space separated domains793# datatype: string of space separated domains
794# Example: help.launchpad.net login.launchapd.net794# Example: help.launchpad.net login.launchpad.net
795url_rewrite_exceptions: help.launchpad.net
796
797[bing_test_service]
798# Run a web service stub that simulates the Bing search service.
799
800# Where are our canned JSON responses stored?
801canned_response_directory: lib/lp/services/sitesearch/tests/data/
802
803# Which file maps service URLs to the JSON that the server returns?
804mapfile: lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt
805
806# Where should the service log files live?
807log: logs/bing-stub.log
808
809# Do we actually want to run the service?
810launch: False
811
812[bing]
813# site is the host and path that search requests are made to.
814# eg. https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search
815# datatype: string, a url to a host
816site: https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search
817
818# subscription_key is the Cognitive Services subscription key for
819# Bing Custom Search API.
820# datatype: string
821subscription_key:
822
823# custom_config_id is the id that identifies the custom search instance.
824# datatype: string
825custom_config_id:
826
827# url_rewrite_exceptions is a list of launchpad.net domains that must
828# not be rewritten.
829# datatype: string of space separated domains
830# Example: help.launchpad.net login.launchpad.net
795url_rewrite_exceptions: help.launchpad.net831url_rewrite_exceptions: help.launchpad.net
796832
797[gpghandler]833[gpghandler]
798834
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2016-10-14 16:16:18 +0000
+++ lib/lp/services/features/flags.py 2018-03-29 12:55:41 +0000
@@ -234,6 +234,12 @@
234 'disabled',234 'disabled',
235 'Named authorization tokens for archives',235 'Named authorization tokens for archives',
236 ''),236 ''),
237 ('sitesearch.engine.name',
238 'space delimited',
239 'Name of the site search engine backend ("google" or "bing").',
240 'google',
241 'Site search engine',
242 ''),
237 ])243 ])
238244
239# The set of all flag names that are documented.245# The set of all flag names that are documented.
240246
=== modified file 'lib/lp/services/sitesearch/__init__.py'
--- lib/lp/services/sitesearch/__init__.py 2018-03-27 15:47:35 +0000
+++ lib/lp/services/sitesearch/__init__.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interfaces for searching and working with results."""4"""Interfaces for searching and working with results."""
@@ -6,11 +6,13 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'BingSearchService',
9 'GoogleSearchService',10 'GoogleSearchService',
10 'PageMatch',11 'PageMatch',
11 'PageMatches',12 'PageMatches',
12 ]13 ]
1314
15import json
14import urllib16import urllib
15from urlparse import (17from urlparse import (
16 parse_qsl,18 parse_qsl,
@@ -163,7 +165,7 @@
163class GoogleSearchService:165class GoogleSearchService:
164 """See `ISearchService`.166 """See `ISearchService`.
165167
166 A search service that search Google for launchpad.net pages.168 A search service that searches Google for launchpad.net pages.
167 """169 """
168170
169 _default_values = {171 _default_values = {
@@ -333,3 +335,160 @@
333 raise GoogleWrongGSPVersion(335 raise GoogleWrongGSPVersion(
334 "Could not get any PageMatches from the GSP XML response.")336 "Could not get any PageMatches from the GSP XML response.")
335 return PageMatches(page_matches, start, total)337 return PageMatches(page_matches, start, total)
338
339
340@implementer(ISearchService)
341class BingSearchService:
342 """See `ISearchService`.
343
344 A search service that searches Bing for launchpad.net pages.
345 """
346
347 _default_values = {
348 # XXX: maxiberta 2018-03-26: Set `mkt` based on the current request.
349 'customConfig': None,
350 'mkt': 'en-US',
351 'count': 20,
352 'offset': 0,
353 'q': None,
354 }
355
356 @property
357 def subscription_key(self):
358 """The subscription key issued by Bing Custom Search."""
359 return config.bing.subscription_key
360
361 @property
362 def custom_config_id(self):
363 """The custom search instance as configured in Bing Custom Search."""
364 return config.bing.custom_config_id
365
366 @property
367 def site(self):
368 """The URL to the Bing Custom Search service.
369
370 The URL is probably
371 https://api.cognitive.microsoft.com/bingcustomsearch/v7.0/search.
372 """
373 return config.bing.site
374
375 def search(self, terms, start=0):
376 """See `ISearchService`.
377
378 The `subscription_key` and `custom_config_id` are used in the
379 search request. Search returns 20 or fewer results for each query.
380 For terms that match more than 20 results, the start param can be
381 used over multiple queries to get successive sets of results.
382
383 :return: `ISearchResults` (PageMatches).
384 :raise: `SiteSearchResponseError` if the json response is incomplete or
385 cannot be parsed.
386 """
387 search_url = self.create_search_url(terms, start=start)
388 search_headers = self.create_search_headers()
389 request = get_current_browser_request()
390 timeline = get_request_timeline(request)
391 action = timeline.start("bing-search-api", search_url)
392 try:
393 response = urlfetch(search_url, headers=search_headers)
394 except (TimeoutError, requests.RequestException) as error:
395 raise SiteSearchResponseError(
396 "The response errored: %s" % str(error))
397 finally:
398 action.finish()
399 page_matches = self._parse_bing_response(response.content, start)
400 return page_matches
401
402 def _checkParameter(self, name, value, is_int=False):
403 """Check that a parameter value is not None or an empty string."""
404 if value in (None, ''):
405 raise ValueError("Missing value for parameter '%s'." % name)
406 if is_int:
407 try:
408 int(value)
409 except ValueError:
410 raise ValueError(
411 "Value for parameter '%s' is not an int." % name)
412
413 def create_search_url(self, terms, start=0):
414 """Return a Bing Custom Search search url."""
415 self._checkParameter('q', terms)
416 self._checkParameter('offset', start, is_int=True)
417 self._checkParameter('customConfig', self.custom_config_id)
418 search_params = dict(self._default_values)
419 search_params['q'] = terms.encode('utf8')
420 search_params['offset'] = start
421 search_params['customConfig'] = self.custom_config_id
422 query_string = urllib.urlencode(sorted(search_params.items()))
423 return self.site + '?' + query_string
424
425 def create_search_headers(self):
426 """Return a dict with Bing Custom Search compatible request headers."""
427 self._checkParameter('subscription_key', self.subscription_key)
428 return {
429 'Ocp-Apim-Subscription-Key': self.subscription_key,
430 }
431
432 def _parse_bing_response(self, bing_json, start=0):
433 """Return a `PageMatches` object.
434
435 :param bing_json: A string containing Bing Custom Search API v7 JSON.
436 :return: `ISearchResults` (PageMatches).
437 :raise: `SiteSearchResponseError` if the json response is incomplete or
438 cannot be parsed.
439 """
440 try:
441 bing_doc = json.loads(bing_json)
442 except (TypeError, ValueError):
443 raise SiteSearchResponseError(
444 "The response was incomplete, no JSON.")
445
446 try:
447 response_type = bing_doc['_type']
448 except (AttributeError, KeyError, ValueError):
449 raise SiteSearchResponseError(
450 "Could not get the '_type' from the Bing JSON response.")
451
452 if response_type == 'ErrorResponse':
453 try:
454 errors = [error['message'] for error in bing_doc['errors']]
455 raise SiteSearchResponseError(
456 "Error response from Bing: %s" % '; '.join(errors))
457 except (AttributeError, KeyError, TypeError, ValueError):
458 raise SiteSearchResponseError(
459 "Unable to parse the Bing JSON error response.")
460 elif response_type != 'SearchResponse':
461 raise SiteSearchResponseError(
462 "Unknown Bing JSON response type: '%s'." % response_type)
463
464 page_matches = []
465 total = 0
466 try:
467 results = bing_doc['webPages']['value']
468 except (AttributeError, KeyError, ValueError):
469 # Bing did not match any pages. Return an empty PageMatches.
470 return PageMatches(page_matches, start, total)
471
472 try:
473 total = int(bing_doc['webPages']['totalEstimatedMatches'])
474 except (AttributeError, KeyError, ValueError):
475 # The datatype is not what PageMatches requires.
476 raise SiteSearchResponseError(
477 "Could not get the total from the Bing JSON response.")
478 if total < 0:
479 # See bug 683115.
480 total = 0
481 for result in results:
482 url = result.get('url')
483 title = result.get('name')
484 summary = result.get('snippet')
485 if None in (url, title, summary):
486 # There is not enough data to create a PageMatch object.
487 # This can be caused by an empty title or summary which
488 # has been observed for pages that are from vhosts that
489 # should not be indexed.
490 continue
491 summary = summary.replace('<br>', '')
492 page_matches.append(PageMatch(title, url, summary))
493
494 return PageMatches(page_matches, start, total)
336495
=== added file 'lib/lp/services/sitesearch/bingtestservice.py'
--- lib/lp/services/sitesearch/bingtestservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/bingtestservice.py 2018-03-29 12:55:41 +0000
@@ -0,0 +1,79 @@
1#!/usr/bin/python
2#
3# Copyright 2018 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""
7This script runs a simple HTTP server. The server returns JSON files
8when given certain user-configurable URLs.
9"""
10
11import logging
12import os
13
14from six.moves.BaseHTTPServer import HTTPServer
15
16from lp.services.config import config
17from lp.services.osutils import ensure_directory_exists
18from lp.services.pidfile import make_pidfile
19from lp.services.sitesearch import testservice
20
21
22# Set up basic logging.
23log = logging.getLogger(__name__)
24
25# The default service name, used by the Launchpad service framework.
26service_name = 'bing-webservice'
27
28
29class BingRequestHandler(testservice.RequestHandler):
30 default_content_type = 'text/xml; charset=UTF-8'
31 log = log
32 mapfile = config.bing_test_service.mapfile
33 content_dir = config.bing_test_service.canned_response_directory
34
35
36def start_as_process():
37 return testservice.start_as_process('bingtestservice')
38
39
40def get_service_endpoint():
41 """Return the host and port that the service is running on."""
42 return testservice.hostpair(config.bing.site)
43
44
45def service_is_available():
46 host, port = get_service_endpoint()
47 return testservice.service_is_available(host, port)
48
49
50def wait_for_service():
51 host, port = get_service_endpoint()
52 return testservice.wait_for_service(host, port)
53
54
55def kill_running_process():
56 global service_name
57 host, port = get_service_endpoint()
58 return testservice.kill_running_process(service_name, host, port)
59
60
61def main():
62 """Run the HTTP server."""
63 # Redirect our service output to a log file.
64 global log
65 ensure_directory_exists(os.path.dirname(config.bing_test_service.log))
66 filelog = logging.FileHandler(config.bing_test_service.log)
67 log.addHandler(filelog)
68 log.setLevel(logging.DEBUG)
69
70 # To support service shutdown we need to create a PID file that is
71 # understood by the Launchpad services framework.
72 global service_name
73 make_pidfile(service_name)
74
75 host, port = get_service_endpoint()
76 server = HTTPServer((host, port), BingRequestHandler)
77
78 log.info("Starting HTTP Bing webservice server on port %s", port)
79 server.serve_forever()
080
=== modified file 'lib/lp/services/sitesearch/configure.zcml'
--- lib/lp/services/sitesearch/configure.zcml 2018-03-16 14:02:16 +0000
+++ lib/lp/services/sitesearch/configure.zcml 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -16,8 +16,17 @@
16 </class>16 </class>
1717
18 <securedutility18 <securedutility
19 name="google"
19 class="lp.services.sitesearch.GoogleSearchService"20 class="lp.services.sitesearch.GoogleSearchService"
20 provides="lp.services.sitesearch.interfaces.ISearchService">21 provides="lp.services.sitesearch.interfaces.ISearchService">
21 <allow interface="lp.services.sitesearch.interfaces.ISearchService" />22 <allow interface="lp.services.sitesearch.interfaces.ISearchService" />
22 </securedutility>23 </securedutility>
24
25 <securedutility
26 name="bing"
27 class="lp.services.sitesearch.BingSearchService"
28 provides="lp.services.sitesearch.interfaces.ISearchService">
29 <allow interface="lp.services.sitesearch.interfaces.ISearchService" />
30 </securedutility>
31
23</configure>32</configure>
2433
=== added file 'lib/lp/services/sitesearch/doc/bing-searchservice.txt'
--- lib/lp/services/sitesearch/doc/bing-searchservice.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/doc/bing-searchservice.txt 2018-03-29 12:55:41 +0000
@@ -0,0 +1,438 @@
1===================
2Bing Search Service
3===================
4
5The BingSearchService is a Bing Custom Search client.
6Given one or more terms, it will retrieve a JSON
7summary of the matching launchpad.net pages.
8
9We silence logging of new HTTP connections from requests throughout.
10
11 >>> from fixtures import FakeLogger
12 >>> logger = FakeLogger()
13 >>> logger.setUp()
14
15
16BingSearchService
17=================
18
19The BingSearchService implements the ISearchService interface.
20
21 >>> from zope.component import getUtility
22 >>> from zope.interface.verify import verifyObject
23 >>> from lp.services.sitesearch.interfaces import (
24 ... ISearchService)
25
26 >>> bing_search = getUtility(ISearchService, name="bing")
27 >>> verifyObject(ISearchService, bing_search)
28 True
29 >>> bing_search
30 <...BingSearchService ...>
31
32
33--------------------------
34BingSearchService search()
35--------------------------
36
37The search method accepts a string argument of terms and an optional int
38argument of start. The terms are the same as the text that would be
39entered in Bing search form; the terms should not be escaped.
40
41 >>> from lp.services.sitesearch.interfaces import (
42 ... ISearchResults)
43
44 >>> first_page_matches = bing_search.search(terms='bug')
45 >>> first_page_matches
46 <...PageMatches ...>
47
48The start parameter specifies the index (starting at 0) of the first
49result returned in the overall set of matches. Since 20 results are
50returned, to get the second batch of matches, you would use start=20.
51
52 >>> second_page_matches = bing_search.search(terms='bug', start=20)
53 >>> second_page_matches
54 <...PageMatches ...>
55
56
57PageMatches
58===========
59
60The PageMatches object returned by BingSearchService.search()
61implements ISearchResults.
62
63 >>> verifyObject(ISearchResults, first_page_matches)
64 True
65
66The 'total' attribute is the total number of matches that the search
67found. If that number is higher than 20, it means that multiple requests
68would be needed to retrieve the entire result set.
69
70The 'start' attribute is the index of the first returned item within the
71entire collection of matches. The 'length' attribute contains the number
72of returned results.
73
74The first search for 'bugs' returned a subset of items in the
75ISearchResult. There are 25 total items, but the results contains the
76first 20 matches (because they start at index 0).
77
78 >>> first_page_matches.start
79 0
80 >>> first_page_matches.total
81 25
82 >>> len(first_page_matches)
83 20
84
85The second search for 'bugs' returned the remainder of the 25 matches.
86They start from index 20.
87
88 >>> verifyObject(ISearchResults, second_page_matches)
89 True
90
91 >>> second_page_matches.start
92 20
93 >>> second_page_matches.total
94 25
95 >>> len(second_page_matches)
96 5
97
98An item can be retrieved from an ISearchResults object using its
99index. All the items in the collection can be iterated.
100
101 >>> second_page_matches[1].url
102 'http://blog.launchpad.dev/general/of-bugs-and-statuses'
103
104 >>> for page_match in second_page_matches:
105 ... page_match.url
106 'https://help.launchpad.net/Bugs'
107 'http://blog.launchpad.dev/general/of-bugs-and-statuses'
108 'http://launchpad.dev/mahara/+milestone/1.8.0'
109 'http://launchpad.dev/mb'
110 'http://launchpad.dev/bugs'
111
112An empty PageMatches is returns if there are no results.
113
114 >>> no_page_matches = bing_search.search(terms='fnord')
115 >>> no_page_matches.start
116 0
117 >>> no_page_matches.total
118 0
119 >>> len(no_page_matches)
120 0
121
122
123PageMatch
124=========
125
126The PageMatch object represents a single result from a search result
127set. It is created by passing a title, url, and a summary. It is
128an implementation of ISearchResult.
129
130 >>> from lp.services.sitesearch.interfaces import ISearchResult
131 >>> from lp.services.sitesearch import PageMatch
132
133 >>> page_match = PageMatch(
134 ... u'Unicode Titles in Launchpad',
135 ... 'https://launchpad.net/unicode-titles',
136 ... u'Unicode Titles is a modest project dedicated to using Unicode.')
137
138 >>> verifyObject(ISearchResult, page_match)
139 True
140
141The title and summary attributes contain the same text that
142initialized the object.
143
144 >>> page_match.title
145 u'Unicode Titles in Launchpad'
146 >>> page_match.summary
147 u'Unicode Titles is a modest project dedicated to using Unicode.'
148
149The URL's domain is rewitten to the so that links from launchpad.net are
150mapped to the local environment.
151
152 >>> page_match.url
153 'http://launchpad.dev/unicode-titles'
154
155
156Search configuration
157====================
158
159The bing search service is configured by the bing section in
160lazr.config. All requests are made to Bing's site, but the
161configuration may set a testing site.
162
163 >>> from lp.services.config import config
164 >>> from lp.services.sitesearch import BingSearchService
165
166 >>> bing_search = BingSearchService()
167 >>> config.bing.site == bing_search.site
168 True
169 >>> bing_search.site
170 'http://launchpad.dev:.../bingcustomsearch/v7.0/search'
171
172The subscription_key is the Cognitive Services subscription key for
173Bing Custom Search API.
174
175 >>> config.bing.subscription_key == bing_search.subscription_key
176 True
177 >>> bing_search.subscription_key
178 'abcdef01234567890abcdef012345678'
179
180The custom_config_id is the id that identifies the custom search instance.
181
182 >>> config.bing.custom_config_id == bing_search.custom_config_id
183 True
184 >>> bing_search.custom_config_id
185 1234567890
186
187Several default query parameters are constant. They are kept in the
188_default_values dict. The q (terms) and start params are provided at
189the time of the search.
190
191 >>> for key, value in sorted(bing_search._default_values.items()):
192 ... print key, ':', repr(value)
193 count : 20
194 customConfig : None
195 mkt : 'en-US'
196 offset : 0
197 q : None
198
199create_search_url()
200===================
201
202The search url used inside the search() method is created by
203create_search_url(). It accepts two optional arguments: terms and start.
204An error is raised if any of the parameters are None.
205
206 >>> bing_search.create_search_url('')
207 Traceback (most recent call last):
208 ...
209 ValueError: Missing value for parameter 'q'.
210
211 >>> bing_search.create_search_url(None)
212 Traceback (most recent call last):
213 ...
214 ValueError: Missing value for parameter 'q'.
215
216 >>> bing_search.create_search_url('bugs', start='true')
217 Traceback (most recent call last):
218 ...
219 ValueError: Value for parameter 'offset' is not an int.
220
221The term parameter in this example can be defined by passing the term
222argument to the method. The argument is url encoded and used as the
223value for the 'q' (query) parameter.
224
225 >>> bing_search.create_search_url(terms='svg +bugs').replace('&', ' ')
226 'http://launchpad.dev:.../bingcustomsearch/v7.0/search?count=20 customConfig=1234567890 mkt=en-US offset=0 q=svg+%2Bbugs'
227
228Unicode characters are escaped correctly in the bing request URL.
229
230 >>> bing_search.create_search_url(terms=u'Carlos Perell\xf3 Mar\xedn')
231 'http://launchpad.dev:.../...offset=0&q=Carlos+Perell%C3%B3+Mar%C3%ADn'
232
233The start parameter can be changed by passing a start int argument.
234
235 >>> bing_search.create_search_url(terms='svg +bugs', start=20)
236 'http://launchpad.dev:.../...offset=20&q=svg+%2Bbugs'
237
238
239Bing Search response parsing
240============================
241
242The BingSearchService's _parse_bing_response() expects a JSON response to
243create the PageMatch and PageMatches objects. An error is raised when
244the JSON document cannot be parsed into objects.
245
246The PageMatches's total attribute comes from the `webPages.totalEstimatedMatches`
247JSON element. When it cannot be found and the value cast to an int,
248an error is raised. If Bing were to redefine the meaning of the
249element to use a '~' to indicate an approximate total, an error would
250be raised.
251
252 >>> from os import path
253
254 >>> base_path = path.normpath(path.join(
255 ... path.dirname(__file__), '..', 'tests', 'data'))
256 >>> json_file_name = path.join(
257 ... base_path, 'bingsearchservice-incompatible-matches.json')
258 >>> with open(json_file_name, 'r') as json_file:
259 ... data = json_file.read()
260 >>> print data
261 {...
262 "totalEstimatedMatches": "~25"...
263
264 >>> bing_search._parse_bing_response(data)
265 Traceback (most recent call last):
266 ...
267 SiteSearchResponseError: Could not get the total from the
268 Bing JSON response.
269
270On the other hand, if the total is ever less than zero (see bug 683115),
271this is expected: we simply return a total of 0.
272
273 >>> json_file_name = path.join(
274 ... base_path, 'bingsearchservice-negative-total.json')
275 >>> with open(json_file_name, 'r') as json_file:
276 ... data = json_file.read()
277 >>> print data
278 {...
279 "totalEstimatedMatches": -25...
280
281 >>> bing_search._parse_bing_response(data).total
282 0
283
284A PageMatch requires a title, url, and a summary. If those elements cannot
285be found, a PageMatch cannot be made. A missing title ('name') indicates
286a bad page on Launchpad, so it is ignored. In this example, the first match
287is missing a title, so only the second page is present in the PageMatches.
288
289 >>> json_file_name = path.join(
290 ... base_path, 'bingsearchservice-missing-title.json')
291 >>> with open(json_file_name, 'r') as json_file:
292 ... data = json_file.read()
293 >>> page_matches = bing_search._parse_bing_response(data)
294 >>> len(page_matches)
295 1
296 >>> page_matches[0].title
297 u'GCleaner in Launchpad'
298 >>> page_matches[0].url
299 'http://launchpad.dev/gcleaner'
300
301When a match is missing a summary ('snippet'), the match is skipped because
302there is no information about why it matched. This appears to relate to
303pages that are in the index, but should be removed. In this example
304taken from real data, the links are to the same page on different
305vhosts. The edge vhost has no summary, so it is skipped.
306
307 >>> json_file_name = path.join(
308 ... base_path, 'bingsearchservice-missing-summary.json')
309 >>> with open(json_file_name, 'r') as json_file:
310 ... data = json_file.read()
311 >>> page_matches = bing_search._parse_bing_response(data)
312 >>> len(page_matches)
313 1
314 >>> page_matches[0].title
315 u'BugExpiry - Launchpad Help'
316 >>> page_matches[0].url
317 'https://help.launchpad.net/BugExpiry'
318
319When the URL ('url') cannot be found the match is skipped. There are no
320examples of this. We do not want this hypothetical situation to give
321users a bad experience.
322
323 >>> json_file_name = path.join(
324 ... base_path, 'bingsearchservice-missing-url.json')
325 >>> with open(json_file_name, 'r') as json_file:
326 ... data = json_file.read()
327 >>> page_matches = bing_search._parse_bing_response(data)
328 >>> len(page_matches)
329 1
330 >>> page_matches[0].title
331 u'LongoMatch in Launchpad'
332 >>> page_matches[0].url
333 'http://launchpad.dev/longomatch'
334
335If no matches are found in the response, and there are 20 or fewer results,
336an Empty PageMatches is returned. This happens when the results are missing
337titles and summaries. This is not considered to be a problem because the
338small number implies that Bing did a poor job of indexing pages or indexed
339the wrong Launchpad server. In this example, there is only one match, but
340the results is missing a title so there is not enough information to make
341a PageMatch.
342
343 >>> json_file_name = path.join(
344 ... base_path, 'bingsearchservice-no-meaningful-results.json')
345 >>> with open(json_file_name, 'r') as json_file:
346 ... data = json_file.read()
347 >>> page_matches = bing_search._parse_bing_response(data)
348 >>> len(page_matches)
349 0
350
351
352-------------
353URL rewriting
354-------------
355
356The URL scheme used in the rewritten URL is configured in
357config.bing.url_rewrite_scheme. The hostname is set in the shared
358key config.vhost.mainsite.hostname.
359
360 >>> config.vhosts.use_https
361 False
362 >>> page_match.url_rewrite_scheme
363 'http'
364
365 >>> config.vhost.mainsite.hostname == page_match.url_rewrite_hostname
366 True
367 >>> page_match.url_rewrite_hostname
368 'launchpad.dev'
369
370URLs are rewritten to map public URL to the private hostname.
371The vhost name is preserved when the URL is rewritten.
372
373 >>> page_match = PageMatch(
374 ... u'Bug #456 in Unicode title: "testrunner hates Unicode"',
375 ... 'https://bugs.launchpad.net/unicode-titles/+bug/456',
376 ... u'The Zope testrunner likes ASCII more than Unicode.')
377 >>> page_match.url
378 'http://bugs.launchpad.dev/unicode-titles/+bug/456'
379
380A URL's trailing slash is removed; Launchpad does not use trailing
381slashes.
382
383 >>> page_match = PageMatch(
384 ... u'Ubuntu in Launchpad',
385 ... 'https://launchpad.net/ubuntu/',
386 ... u'Ubuntu also includes more software than any other operating')
387 >>> page_match.url
388 'http://launchpad.dev/ubuntu'
389
390There is a list of URLs that are not rewritten configured in
391config.bing.url_rewrite_exceptions. For example, help.launchpad.net
392is only run in one environment, so links to that site will be preserved.
393
394 >>> config.bing.url_rewrite_exceptions
395 'help.launchpad.net'
396 >>> page_match.url_rewrite_exceptions
397 ['help.launchpad.net']
398
399 >>> page_match = PageMatch(
400 ... u'OpenID',
401 ... 'https://help.launchpad.net/OpenID',
402 ... u'Launchpad uses OpenID.')
403 >>> page_match.url
404 'https://help.launchpad.net/OpenID'
405
406
407-----------------------------
408Graceful handling of timeouts
409-----------------------------
410
411The external service (Bing Search Engine) may not be available, or
412is not responding quickly because there are network issues. In these
413cases a TimeoutError is issued.
414
415 >>> from socket import socket
416 >>> from textwrap import dedent
417 >>> from lp.services.timeout import (
418 ... get_default_timeout_function, set_default_timeout_function)
419
420 >>> server = socket()
421 >>> server.bind(('127.0.0.01', 0))
422 >>> server.listen(1)
423 >>> config.push('timeout_data', dedent("""
424 ... [bing]
425 ... site: http://%s:%d/cse
426 ... """ % server.getsockname()))
427 >>> old_timeout_function = get_default_timeout_function()
428 >>> set_default_timeout_function(lambda: 0.1)
429 >>> bing_search.search(terms='bug')
430 Traceback (most recent call last):
431 ...
432 SiteSearchResponseError: ... timeout exceeded.
433
434 # Restore the configuration and the timeout state.
435 >>> timeout_data = config.pop('timeout_data')
436 >>> set_default_timeout_function(old_timeout_function)
437
438 >>> logger.cleanUp()
0439
=== modified file 'lib/lp/services/sitesearch/doc/google-searchservice.txt'
--- lib/lp/services/sitesearch/doc/google-searchservice.txt 2018-03-27 15:47:35 +0000
+++ lib/lp/services/sitesearch/doc/google-searchservice.txt 2018-03-29 12:55:41 +0000
@@ -23,7 +23,7 @@
23 >>> from lp.services.sitesearch.interfaces import (23 >>> from lp.services.sitesearch.interfaces import (
24 ... ISearchService)24 ... ISearchService)
2525
26 >>> google_search = getUtility(ISearchService)26 >>> google_search = getUtility(ISearchService, name="google")
27 >>> verifyObject(ISearchService, google_search)27 >>> verifyObject(ISearchService, google_search)
28 True28 True
29 >>> google_search29 >>> google_search
3030
=== modified file 'lib/lp/services/sitesearch/interfaces.py'
--- lib/lp/services/sitesearch/interfaces.py 2018-03-26 21:06:51 +0000
+++ lib/lp/services/sitesearch/interfaces.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interfaces for searching and working with results."""4"""Interfaces for searching and working with results."""
55
=== added file 'lib/lp/services/sitesearch/tests/bingserviceharness.py'
--- lib/lp/services/sitesearch/tests/bingserviceharness.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/bingserviceharness.py 2018-03-29 12:55:41 +0000
@@ -0,0 +1,107 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""
5Fixtures for running the Bing test webservice.
6"""
7
8__metaclass__ = type
9
10__all__ = ['BingServiceTestSetup']
11
12
13import errno
14import os
15import signal
16
17from lp.services.sitesearch import bingtestservice
18
19
20class BingServiceTestSetup:
21 """Set up the Bing web service stub for use in functional tests.
22 """
23
24 # XXX gary 2008-12-06 bug=305858: Spurious test failures discovered on
25 # buildbot, builds 40 and 43. The locations of the failures are marked
26 # below with " # SPURIOUS FAILURE". To reinstate, add the text below back
27 # to the docstring above. Note that the test that uses this setup,
28 # bing-service-stub.txt, is also disabled. See test_doc.py.
29 """
30 >>> from lp.services.sitesearch.bingtestservice import (
31 ... service_is_available)
32 >>> from lp.services.config import config
33
34 >>> assert not service_is_available() # Sanity check. # SPURIOUS FAILURE
35
36 >>> BingServiceTestSetup().setUp()
37
38 After setUp is called, a Bing test service instance is running.
39
40 >>> assert service_is_available()
41 >>> assert BingServiceTestSetup.service is not None
42
43 After tearDown is called, the service is shut down.
44
45 >>> BingServiceTestSetup().tearDown()
46
47 >>> assert not service_is_available()
48 >>> assert BingServiceTestSetup.service is None
49
50 The fixture can be started and stopped multiple time in succession:
51
52 >>> BingServiceTestSetup().setUp()
53 >>> assert service_is_available()
54
55 Having a service instance already running doesn't prevent a new
56 service from starting. The old instance is killed off and replaced
57 by the new one.
58
59 >>> old_pid = BingServiceTestSetup.service.pid
60 >>> BingServiceTestSetup().setUp() # SPURIOUS FAILURE
61 >>> BingServiceTestSetup.service.pid != old_pid
62 True
63
64 Tidy up.
65
66 >>> BingServiceTestSetup().tearDown()
67 >>> assert not service_is_available()
68
69 """
70
71 service = None # A reference to our running service.
72
73 def setUp(self):
74 self.startService()
75
76 def tearDown(self):
77 self.stopService()
78
79 @classmethod
80 def startService(cls):
81 """Start the webservice."""
82 bingtestservice.kill_running_process()
83 cls.service = bingtestservice.start_as_process()
84 assert cls.service, "The Search service process did not start."
85 try:
86 bingtestservice.wait_for_service()
87 except RuntimeError:
88 # The service didn't start itself soon enough. We must
89 # make sure to kill any errant processes that may be
90 # hanging around.
91 cls.stopService()
92 raise
93
94 @classmethod
95 def stopService(cls):
96 """Shut down the webservice instance."""
97 if cls.service:
98 try:
99 os.kill(cls.service.pid, signal.SIGTERM)
100 except OSError as error:
101 if error.errno != errno.ESRCH:
102 raise
103 # The process with the given pid doesn't exist, so there's
104 # nothing to kill or wait for.
105 else:
106 cls.service.wait()
107 cls.service = None
0108
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-1.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,384 @@
1{
2 "_type": "SearchResponse",
3 "instrumentation": {
4 "pingUrlBase": "https://www.bingapis.com/api/ping?IG=4C3A184771914F6BB81F8541E7EE7695&CID=13045103CBA569B80D475AB7CA03687C&ID=",
5 "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=4C3A184771914F6BB81F8541E7EE7695&CID=13045103CBA569B80D475AB7CA03687C&Type=Event.CPT&DATA=0"
6 },
7 "queryContext": {
8 "originalQuery": "bug"
9 },
10 "webPages": {
11 "webSearchUrl": "https://www.bing.com/search?q=bug",
12 "webSearchUrlPingSuffix": "DevEx,5462.1",
13 "totalEstimatedMatches": 25,
14 "value": [
15 {
16 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
17 "name": "Launchpad Bugs",
18 "url": "https://launchpad.net/~ubuntu-bugs",
19 "urlPingSuffix": "DevEx,5080.1",
20 "isFamilyFriendly": true,
21 "displayUrl": "https://launchpad.net/~ubuntu-bugs",
22 "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 ...",
23 "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
24 "fixedPosition": false
25 },
26 {
27 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
28 "name": "Bugs in Ubuntu Linux",
29 "url": "https://launchpad.net/gcleaner",
30 "urlPingSuffix": "DevEx,5095.1",
31 "isFamilyFriendly": true,
32 "displayUrl": "https://launchpad.net/gcleaner",
33 "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 ...",
34 "dateLastCrawled": "2018-02-21T13:17:00.0000000Z",
35 "fixedPosition": false
36 },
37 {
38 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
39 "name": "Bugs related to Sample Person",
40 "url": "https://help.launchpad.net/BugExpiry",
41 "urlPingSuffix": "DevEx,5110.1",
42 "isFamilyFriendly": true,
43 "displayUrl": "https://help.launchpad.net/BugExpiry",
44 "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 ...",
45 "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
46 "fixedPosition": false
47 },
48 {
49 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
50 "name": "Bug #1 in Mozilla Firefox: Firefox does not support SVG",
51 "url": "https://launchpad.net/longomatch",
52 "urlPingSuffix": "DevEx,5125.1",
53 "isFamilyFriendly": true,
54 "displayUrl": "https://launchpad.net/longomatch",
55 "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 ...",
56 "dateLastCrawled": "2018-02-16T21:54:00.0000000Z",
57 "fixedPosition": false
58 },
59 {
60 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4",
61 "name": "Question #232632 : Questions : OpenStack Heat",
62 "url": "https://answers.launchpad.net/heat/+question/232632",
63 "urlPingSuffix": "DevEx,5140.1",
64 "datePublished": "2013-07-18T00:00:00.0000000",
65 "isFamilyFriendly": true,
66 "displayUrl": "https://answers.launchpad.net/heat/+question/232632",
67 "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 ...",
68 "dateLastCrawled": "2018-02-20T23:45:00.0000000Z",
69 "fixedPosition": false
70 },
71 {
72 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.5",
73 "name": "Sandpad in Launchpad",
74 "url": "https://launchpad.net/sandpad",
75 "urlPingSuffix": "DevEx,5154.1",
76 "isFamilyFriendly": true,
77 "displayUrl": "https://launchpad.net/sandpad",
78 "snippet": "Sandpad is an standalone wlua application for making quick scratch programs and sandboxes in Lua. It uses IUP 3 for the user interface.",
79 "dateLastCrawled": "2018-03-03T17:54:00.0000000Z",
80 "fixedPosition": false
81 },
82 {
83 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.6",
84 "name": "Eventum in Launchpad",
85 "url": "https://launchpad.net/eventum",
86 "urlPingSuffix": "DevEx,5169.1",
87 "isFamilyFriendly": true,
88 "displayUrl": "https://launchpad.net/eventum",
89 "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 ...",
90 "dateLastCrawled": "2018-03-07T19:52:00.0000000Z",
91 "fixedPosition": false
92 },
93 {
94 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.7",
95 "name": "Inkscape 0.48.2 \"0.48.2\" - Launchpad",
96 "url": "https://launchpad.net/inkscape/+milestone/0.48.2",
97 "urlPingSuffix": "DevEx,5182.1",
98 "isFamilyFriendly": true,
99 "displayUrl": "https://launchpad.net/inkscape/+milestone/0.48.2",
100 "snippet": "After you've downloaded a file, you can verify its authenticity using its MD5 sum or signature. (How do I verify a download?",
101 "dateLastCrawled": "2018-02-24T19:01:00.0000000Z",
102 "fixedPosition": false
103 },
104 {
105 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.8",
106 "name": "Code Hosting - Launchpad tour",
107 "url": "https://launchpad.net/+tour/branch-hosting-tracking",
108 "urlPingSuffix": "DevEx,5195.1",
109 "isFamilyFriendly": true,
110 "displayUrl": "https://launchpad.net/+tour/branch-hosting-tracking",
111 "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 ...",
112 "dateLastCrawled": "2018-03-10T23:01:00.0000000Z",
113 "fixedPosition": false
114 },
115 {
116 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.9",
117 "name": "gdiskdump project files : gdiskdump",
118 "url": "https://launchpad.net/gdiskdump/+download",
119 "urlPingSuffix": "DevEx,5208.1",
120 "isFamilyFriendly": true,
121 "displayUrl": "https://launchpad.net/gdiskdump/+download",
122 "snippet": "added advanced settings and estimated Time for process to finish. bug fixes for some languages.",
123 "dateLastCrawled": "2018-02-19T11:52:00.0000000Z",
124 "fixedPosition": false
125 },
126 {
127 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.10",
128 "name": "Bug #1747091 “epicsTimeGetEvent() / generalTime bug ...",
129 "url": "https://code.launchpad.net/bugs/1747091",
130 "urlPingSuffix": "DevEx,5220.1",
131 "isFamilyFriendly": true,
132 "displayUrl": "https://code.launchpad.net/bugs/1747091",
133 "snippet": "When changes in epicsGeneralTime.c were made (fetch time provider's eventTime into a local copy) an inconsistency resulted.",
134 "dateLastCrawled": "2018-02-04T02:49:00.0000000Z",
135 "fixedPosition": false
136 },
137 {
138 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.11",
139 "name": "Bug #96441 “timer updates use too much CPU” : Bugs : Jokosher",
140 "url": "https://launchpad.net/jokosher/+bug/96441",
141 "urlPingSuffix": "DevEx,5232.1",
142 "isFamilyFriendly": true,
143 "displayUrl": "https://launchpad.net/jokosher/+bug/96441",
144 "snippet": "The information about this bug in Launchpad is automatically pulled daily from the remote bug. This information was last pulled ...",
145 "dateLastCrawled": "2018-02-17T16:53:00.0000000Z",
146 "fixedPosition": false
147 },
148 {
149 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.12",
150 "name": "Series 17.10 : Mahara",
151 "url": "https://launchpad.net/mahara/17.10",
152 "urlPingSuffix": "DevEx,5247.1",
153 "isFamilyFriendly": true,
154 "displayUrl": "https://launchpad.net/mahara/17.10",
155 "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",
156 "dateLastCrawled": "2018-03-12T13:48:00.0000000Z",
157 "fixedPosition": false
158 },
159 {
160 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.13",
161 "name": "sap in Launchpad",
162 "url": "https://launchpad.net/sap+",
163 "urlPingSuffix": "DevEx,5260.1",
164 "isFamilyFriendly": true,
165 "displayUrl": "https://launchpad.net/sap+",
166 "snippet": "Sap is a simple audio player written in Vala and utilizing gstreamer for audio playback and ncurses for user interaction.",
167 "dateLastCrawled": "2018-03-12T05:18:00.0000000Z",
168 "fixedPosition": false
169 },
170 {
171 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.14",
172 "name": "Code/BugAndBlueprintLinks - Launchpad Help",
173 "url": "https://help.launchpad.net/Code/BugAndBlueprintLinks",
174 "urlPingSuffix": "DevEx,5275.1",
175 "isFamilyFriendly": true,
176 "displayUrl": "https://help.launchpad.net/Code/BugAndBlueprintLinks",
177 "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 ...",
178 "dateLastCrawled": "2018-01-26T14:46:00.0000000Z",
179 "fixedPosition": false
180 },
181 {
182 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.15",
183 "name": "One Hundred Papercuts in Launchpad",
184 "url": "https://launchpad.net/hundredpapercuts",
185 "urlPingSuffix": "DevEx,5289.1",
186 "isFamilyFriendly": true,
187 "displayUrl": "https://launchpad.net/hundredpapercuts",
188 "snippet": "Downloads. One Hundred Papercuts does not have any download files registered with Launchpad. •",
189 "dateLastCrawled": "2018-03-01T20:05:00.0000000Z",
190 "fixedPosition": false
191 },
192 {
193 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.16",
194 "name": "Little Software Stats in Launchpad",
195 "url": "https://launchpad.net/lilsoftstats/",
196 "urlPingSuffix": "DevEx,5304.1",
197 "isFamilyFriendly": true,
198 "displayUrl": "https://launchpad.net/lilsoftstats",
199 "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.",
200 "dateLastCrawled": "2018-03-07T21:46:00.0000000Z",
201 "fixedPosition": false
202 },
203 {
204 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.17",
205 "name": "Take the tour - Launchpad tour",
206 "url": "https://launchpad.net/+tour",
207 "urlPingSuffix": "DevEx,5317.1",
208 "isFamilyFriendly": true,
209 "displayUrl": "https://launchpad.net/+tour",
210 "snippet": "Launchpad helps people to work together on free software by making it easy to share code, bug reports, translations and ideas.",
211 "dateLastCrawled": "2018-02-25T04:37:00.0000000Z",
212 "fixedPosition": false
213 },
214 {
215 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.18",
216 "name": "EOEC in Launchpad",
217 "url": "https://launchpad.net/eoec",
218 "urlPingSuffix": "DevEx,5332.1",
219 "isFamilyFriendly": true,
220 "displayUrl": "https://launchpad.net/eoec",
221 "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 ...",
222 "dateLastCrawled": "2018-03-09T21:39:00.0000000Z",
223 "fixedPosition": false
224 },
225 {
226 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.19",
227 "name": "Bug #1210747 “taskdef name not specified” : Bugs : Radkarte",
228 "url": "https://launchpad.net/radkarte/+bug/1210747",
229 "urlPingSuffix": "DevEx,5345.1",
230 "isFamilyFriendly": true,
231 "displayUrl": "https://launchpad.net/radkarte/+bug/1210747",
232 "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 ...",
233 "dateLastCrawled": "2018-01-11T22:23:00.0000000Z",
234 "fixedPosition": false
235 }
236 ]
237 },
238 "rankingResponse": {
239 "mainline": {
240 "items": [
241 {
242 "answerType": "WebPages",
243 "resultIndex": 0,
244 "value": {
245 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0"
246 }
247 },
248 {
249 "answerType": "WebPages",
250 "resultIndex": 1,
251 "value": {
252 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1"
253 }
254 },
255 {
256 "answerType": "WebPages",
257 "resultIndex": 2,
258 "value": {
259 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2"
260 }
261 },
262 {
263 "answerType": "WebPages",
264 "resultIndex": 3,
265 "value": {
266 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3"
267 }
268 },
269 {
270 "answerType": "WebPages",
271 "resultIndex": 4,
272 "value": {
273 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4"
274 }
275 },
276 {
277 "answerType": "WebPages",
278 "resultIndex": 5,
279 "value": {
280 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.5"
281 }
282 },
283 {
284 "answerType": "WebPages",
285 "resultIndex": 6,
286 "value": {
287 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.6"
288 }
289 },
290 {
291 "answerType": "WebPages",
292 "resultIndex": 7,
293 "value": {
294 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.7"
295 }
296 },
297 {
298 "answerType": "WebPages",
299 "resultIndex": 8,
300 "value": {
301 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.8"
302 }
303 },
304 {
305 "answerType": "WebPages",
306 "resultIndex": 9,
307 "value": {
308 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.9"
309 }
310 },
311 {
312 "answerType": "WebPages",
313 "resultIndex": 10,
314 "value": {
315 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.10"
316 }
317 },
318 {
319 "answerType": "WebPages",
320 "resultIndex": 11,
321 "value": {
322 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.11"
323 }
324 },
325 {
326 "answerType": "WebPages",
327 "resultIndex": 12,
328 "value": {
329 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.12"
330 }
331 },
332 {
333 "answerType": "WebPages",
334 "resultIndex": 13,
335 "value": {
336 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.13"
337 }
338 },
339 {
340 "answerType": "WebPages",
341 "resultIndex": 14,
342 "value": {
343 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.14"
344 }
345 },
346 {
347 "answerType": "WebPages",
348 "resultIndex": 15,
349 "value": {
350 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.15"
351 }
352 },
353 {
354 "answerType": "WebPages",
355 "resultIndex": 16,
356 "value": {
357 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.16"
358 }
359 },
360 {
361 "answerType": "WebPages",
362 "resultIndex": 17,
363 "value": {
364 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.17"
365 }
366 },
367 {
368 "answerType": "WebPages",
369 "resultIndex": 18,
370 "value": {
371 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.18"
372 }
373 },
374 {
375 "answerType": "WebPages",
376 "resultIndex": 19,
377 "value": {
378 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.19"
379 }
380 }
381 ]
382 }
383 }
384}
0385
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-bugs-2.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,151 @@
1{
2 "_type": "SearchResponse",
3 "instrumentation": {
4 "pingUrlBase": "https://www.bingapis.com/api/ping?IG=41744312F5E645D58DE6733982EDC72A&CID=222E331334AA69A5318C38A7350C683B&ID=",
5 "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=41744312F5E645D58DE6733982EDC72A&CID=222E331334AA69A5318C38A7350C683B&Type=Event.CPT&DATA=0"
6 },
7 "queryContext": {
8 "originalQuery": "bug"
9 },
10 "webPages": {
11 "webSearchUrl": "https://www.bing.com/search?q=bug",
12 "webSearchUrlPingSuffix": "DevEx,5530.1",
13 "totalEstimatedMatches": 25,
14 "value": [
15 {
16 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
17 "name": "Bugs - Launchpad Help",
18 "url": "https://help.launchpad.net/Bugs",
19 "urlPingSuffix": "DevEx,5103.1",
20 "isFamilyFriendly": true,
21 "displayUrl": "https://help.launchpad.net/Bugs",
22 "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",
23 "deepLinks": [
24 {
25 "name": "Bugs/EmailInterface",
26 "url": "https://help.launchpad.net/Bugs/EmailInterface",
27 "urlPingSuffix": "DevEx,5093.1",
28 "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 ..."
29 },
30 {
31 "name": "Bugs/PluginAPISpec",
32 "url": "https://help.launchpad.net/Bugs/PluginAPISpec",
33 "urlPingSuffix": "DevEx,5094.1",
34 "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 ..."
35 },
36 {
37 "name": "Bugs/Subscriptions",
38 "url": "https://help.launchpad.net/Bugs/Subscriptions",
39 "urlPingSuffix": "DevEx,5095.1",
40 "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 ..."
41 },
42 {
43 "name": "Bugs/YourProject",
44 "url": "https://help.launchpad.net/Bugs/YourProject",
45 "urlPingSuffix": "DevEx,5096.1",
46 "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."
47 },
48 {
49 "name": "Bugs/Expiry",
50 "url": "https://help.launchpad.net/Bugs/Expiry",
51 "urlPingSuffix": "DevEx,5097.1",
52 "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 ..."
53 }
54 ],
55 "dateLastCrawled": "2018-02-20T23:45:00.0000000Z",
56 "fixedPosition": false
57 },
58 {
59 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
60 "name": "Of Bugs and Statuses - Launchpad Blog",
61 "url": "https://blog.launchpad.net/general/of-bugs-and-statuses",
62 "urlPingSuffix": "DevEx,5149.1",
63 "isFamilyFriendly": true,
64 "displayUrl": "https://blog.launchpad.net/general/of-bugs-and-statuses",
65 "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 ...",
66 "dateLastCrawled": "2018-03-09T07:40:00.0000000Z",
67 "fixedPosition": false
68 },
69 {
70 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
71 "name": "Mahara 1.8.0",
72 "url": "https://launchpad.net/mahara/+milestone/1.8.0",
73 "urlPingSuffix": "DevEx,5178.1",
74 "isFamilyFriendly": true,
75 "displayUrl": "https://launchpad.net/mahara/+milestone/1.8.0",
76 "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:",
77 "dateLastCrawled": "2018-03-04T10:53:00.0000000Z",
78 "fixedPosition": false
79 },
80 {
81 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
82 "name": "Mighty Box in Launchpad",
83 "url": "https://launchpad.net/mb",
84 "urlPingSuffix": "DevEx,5193.1",
85 "isFamilyFriendly": true,
86 "displayUrl": "https://launchpad.net/mb",
87 "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 ...",
88 "dateLastCrawled": "2018-03-10T07:57:00.0000000Z",
89 "fixedPosition": false
90 },
91 {
92 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4",
93 "name": "Bug tracking - Launchpad Bugs",
94 "url": "https://launchpad.net/bugs",
95 "urlPingSuffix": "DevEx,5208.1",
96 "about": [
97 {
98 "name": "Launchpad"
99 }
100 ],
101 "isFamilyFriendly": true,
102 "displayUrl": "https://launchpad.net/bugs",
103 "snippet": "Statistics. 1755185 bugs reported across 12480 projects including 122769 links to 2796 bug trackers; 148160 bugs are shared across multiple projects",
104 "dateLastCrawled": "2018-03-14T01:18:00.0000000Z",
105 "fixedPosition": false
106 }
107 ]
108 },
109 "rankingResponse": {
110 "mainline": {
111 "items": [
112 {
113 "answerType": "WebPages",
114 "resultIndex": 0,
115 "value": {
116 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0"
117 }
118 },
119 {
120 "answerType": "WebPages",
121 "resultIndex": 1,
122 "value": {
123 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1"
124 }
125 },
126 {
127 "answerType": "WebPages",
128 "resultIndex": 2,
129 "value": {
130 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2"
131 }
132 },
133 {
134 "answerType": "WebPages",
135 "resultIndex": 3,
136 "value": {
137 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3"
138 }
139 },
140 {
141 "answerType": "WebPages",
142 "resultIndex": 4,
143 "value": {
144 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.4"
145 }
146 }
147 ]
148 }
149 }
150}
151
0152
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-error.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,20 @@
1{
2 "_type": "ErrorResponse",
3 "errors": [
4 {
5 "code": "InvalidRequest",
6 "subCode": "ParameterInvalidValue",
7 "message": "Parameter has invalid value.",
8 "parameter": "count",
9 "value": "-1"
10 },
11 {
12 "code": "InvalidRequest",
13 "subCode": "ParameterInvalidValue",
14 "message": "Parameter has invalid value.",
15 "parameter": "offset",
16 "value": "a"
17 }
18 ]
19}
20
021
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-incompatible-matches.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,8 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": "~25",
5 "value": []
6 }
7}
8
09
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-incomplete-response.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,1 @@
1
02
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-mapping.txt 2018-03-29 12:55:41 +0000
@@ -0,0 +1,26 @@
1# This file defines a mapping of Bing search service URLs to the JSON
2# files that should be returned by them.
3#
4# The format is 'url JSONfile'. Blank lines and lines starting with '#'
5# are ignored.
6#
7# The special URL, '*', is returned for all un-mapped URLs.
8
9* bingsearchservice-no-results.json
10
11/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=bug bingsearchservice-bugs-1.json
12
13/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=20&q=bug bingsearchservice-bugs-2.json
14
15/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=launchpad bingsearchservice-bugs-1.json
16
17/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=20&q=launchpad bingsearchservice-bugs-2.json
18
19/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=gnomebaker bingsearchservice-incomplete-response.json
20
21/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=no-meaningful bingsearchservice-no-meaningful-results.json
22
23/bingcustomsearch/v7.0/search?count=20&customConfig=1234567890&mkt=en-US&offset=0&q=errors-please bingsearchservice-error.json
24
25# This stub service is also used to impersonate the Blog feed
26/blog-feed blog.launchpad.net-feed.json
027
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-summary.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,30 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": -25,
5 "value": [
6 {
7 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
8 "name": "Ubuntu Bugs in Launchpad",
9 "url": "https://launchpad.net/~ubuntu-bugs",
10 "urlPingSuffix": "DevEx,5080.1",
11 "isFamilyFriendly": true,
12 "displayUrl": "https://launchpad.net/~ubuntu-bugs",
13 "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
14 "fixedPosition": false
15 },
16 {
17 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
18 "name": "BugExpiry - Launchpad Help",
19 "url": "https://help.launchpad.net/BugExpiry",
20 "urlPingSuffix": "DevEx,5110.1",
21 "isFamilyFriendly": true,
22 "displayUrl": "https://help.launchpad.net/BugExpiry",
23 "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 ...",
24 "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
25 "fixedPosition": false
26 }
27 ]
28 }
29}
30
031
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-title.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,30 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": -25,
5 "value": [
6 {
7 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
8 "url": "https://launchpad.net/~ubuntu-bugs",
9 "urlPingSuffix": "DevEx,5080.1",
10 "isFamilyFriendly": true,
11 "displayUrl": "https://launchpad.net/~ubuntu-bugs",
12 "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 ...",
13 "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
14 "fixedPosition": false
15 },
16 {
17 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.1",
18 "name": "GCleaner in Launchpad",
19 "url": "https://launchpad.net/gcleaner",
20 "urlPingSuffix": "DevEx,5095.1",
21 "isFamilyFriendly": true,
22 "displayUrl": "https://launchpad.net/gcleaner",
23 "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 ...",
24 "dateLastCrawled": "2018-02-21T13:17:00.0000000Z",
25 "fixedPosition": false
26 }
27 ]
28 }
29}
30
031
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-missing-url.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,30 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": -25,
5 "value": [
6 {
7 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.0",
8 "name": "Ubuntu Bugs in Launchpad",
9 "urlPingSuffix": "DevEx,5080.1",
10 "isFamilyFriendly": true,
11 "displayUrl": "https://launchpad.net/~ubuntu-bugs",
12 "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 ...",
13 "dateLastCrawled": "2018-03-10T00:32:00.0000000Z",
14 "fixedPosition": false
15 },
16 {
17 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.3",
18 "name": "LongoMatch in Launchpad",
19 "url": "https://launchpad.net/longomatch",
20 "urlPingSuffix": "DevEx,5125.1",
21 "isFamilyFriendly": true,
22 "displayUrl": "https://launchpad.net/longomatch",
23 "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 ...",
24 "dateLastCrawled": "2018-02-16T21:54:00.0000000Z",
25 "fixedPosition": false
26 }
27 ]
28 }
29}
30
031
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-negative-total.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,8 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": -25,
5 "value": []
6 }
7}
8
09
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-no-meaningful-results.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,19 @@
1{
2 "_type": "SearchResponse",
3 "webPages": {
4 "totalEstimatedMatches": 25,
5 "value": [
6 {
7 "id": "https://api.cognitive.microsoft.com/api/v7/#WebPages.2",
8 "url": "https://help.launchpad.net/BugExpiry",
9 "urlPingSuffix": "DevEx,5110.1",
10 "isFamilyFriendly": true,
11 "displayUrl": "https://help.launchpad.net/BugExpiry",
12 "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 ...",
13 "dateLastCrawled": "2018-01-13T13:58:00.0000000Z",
14 "fixedPosition": false
15 }
16 ]
17 }
18}
19
020
=== added file 'lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json'
--- lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/data/bingsearchservice-no-results.json 2018-03-29 12:55:41 +0000
@@ -0,0 +1,12 @@
1{
2 "_type": "SearchResponse",
3 "instrumentation": {
4 "pingUrlBase": "https://www.bingapis.com/api/ping?IG=74D50DC66A774E74AF486941DDF2C89C&CID=2023A1AFC6086AE52F08AA1BC7476B1F&ID=",
5 "pageLoadPingUrl": "https://www.bingapis.com/api/ping/pageload?IG=74D50DC66A774E74AF486941DDF2C89C&CID=2023A1AFC6086AE52F08AA1BC7476B1F&Type=Event.CPT&DATA=0"
6 },
7 "queryContext": {
8 "originalQuery": "AELymgURIr4plE6V5qUaesxj1S8kUSFxCVrVLNU_OeCogh9Q7W5be6lEGMcbb0q6WTDgLL7zsnlnYGLvVrsdxgx3AamFm0M6ARaxerSLvSf-1JQHrOLuhsQ"
9 },
10 "rankingResponse": {}
11}
12
013
=== added file 'lib/lp/services/sitesearch/tests/test_bing.py'
--- lib/lp/services/sitesearch/tests/test_bing.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/test_bing.py 2018-03-29 12:55:41 +0000
@@ -0,0 +1,118 @@
1# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test the bing search service."""
5
6__metaclass__ = type
7
8
9from fixtures import MockPatch
10from requests.exceptions import (
11 ConnectionError,
12 HTTPError,
13 )
14
15from lp.services.sitesearch import BingSearchService
16from lp.services.sitesearch.interfaces import SiteSearchResponseError
17from lp.services.timeout import TimeoutError
18from lp.testing import TestCase
19from lp.testing.layers import (
20 BingLaunchpadFunctionalLayer,
21 LaunchpadFunctionalLayer,
22 )
23
24
25class TestBingSearchService(TestCase):
26 """Test BingSearchService."""
27
28 layer = LaunchpadFunctionalLayer
29
30 def setUp(self):
31 super(TestBingSearchService, self).setUp()
32 self.search_service = BingSearchService()
33
34 def test_search_converts_HTTPError(self):
35 # The method converts HTTPError to SiteSearchResponseError.
36 args = ('url', 500, 'oops', {}, None)
37 self.useFixture(MockPatch(
38 'lp.services.sitesearch.urlfetch', side_effect=HTTPError(*args)))
39 self.assertRaises(
40 SiteSearchResponseError, self.search_service.search, 'fnord')
41
42 def test_search_converts_ConnectionError(self):
43 # The method converts ConnectionError to SiteSearchResponseError.
44 self.useFixture(MockPatch(
45 'lp.services.sitesearch.urlfetch',
46 side_effect=ConnectionError('oops')))
47 self.assertRaises(
48 SiteSearchResponseError, self.search_service.search, 'fnord')
49
50 def test_search_converts_TimeoutError(self):
51 # The method converts TimeoutError to SiteSearchResponseError.
52 self.useFixture(MockPatch(
53 'lp.services.sitesearch.urlfetch',
54 side_effect=TimeoutError('oops')))
55 self.assertRaises(
56 SiteSearchResponseError, self.search_service.search, 'fnord')
57
58 def test_parse_bing_response_TypeError(self):
59 # The method converts TypeError to SiteSearchResponseError.
60 self.assertRaises(
61 SiteSearchResponseError,
62 self.search_service._parse_bing_response, None)
63
64 def test_parse_bing_response_ValueError(self):
65 # The method converts ValueError to SiteSearchResponseError.
66 self.assertRaises(
67 SiteSearchResponseError,
68 self.search_service._parse_bing_response, '')
69
70 def test_parse_bing_response_KeyError(self):
71 # The method converts KeyError to SiteSearchResponseError.
72 self.assertRaises(
73 SiteSearchResponseError,
74 self.search_service._parse_bing_response, '{}')
75
76
77class FunctionalTestBingSearchService(TestCase):
78 """Test BingSearchService."""
79
80 layer = BingLaunchpadFunctionalLayer
81
82 def setUp(self):
83 super(FunctionalTestBingSearchService, self).setUp()
84 self.search_service = BingSearchService()
85
86 def test_search_with_results(self):
87 matches = self.search_service.search('bug')
88 self.assertEqual(0, matches.start)
89 self.assertEqual(25, matches.total)
90 self.assertEqual(20, len(matches))
91
92 def test_search_with_results_and_offset(self):
93 matches = self.search_service.search('bug', start=20)
94 self.assertEqual(20, matches.start)
95 self.assertEqual(25, matches.total)
96 self.assertEqual(5, len(matches))
97
98 def test_search_no_results(self):
99 matches = self.search_service.search('fnord')
100 self.assertEqual(0, matches.start)
101 self.assertEqual(0, matches.total)
102 self.assertEqual(0, len(matches))
103
104 def test_search_no_meaningful_results(self):
105 matches = self.search_service.search('no-meaningful')
106 self.assertEqual(0, matches.start)
107 self.assertEqual(25, matches.total)
108 self.assertEqual(0, len(matches))
109
110 def test_search_incomplete_response(self):
111 self.assertRaises(
112 SiteSearchResponseError,
113 self.search_service.search, 'gnomebaker')
114
115 def test_search_error_response(self):
116 self.assertRaises(
117 SiteSearchResponseError,
118 self.search_service.search, 'errors-please')
0119
=== added file 'lib/lp/services/sitesearch/tests/test_bingharness.py'
--- lib/lp/services/sitesearch/tests/test_bingharness.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/test_bingharness.py 2018-03-29 12:55:41 +0000
@@ -0,0 +1,10 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4import doctest
5
6
7def test_suite():
8 return doctest.DocTestSuite(
9 'lp.services.sitesearch.tests.bingserviceharness',
10 optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
011
=== added file 'lib/lp/services/sitesearch/tests/test_bingservice.py'
--- lib/lp/services/sitesearch/tests/test_bingservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/sitesearch/tests/test_bingservice.py 2018-03-29 12:55:41 +0000
@@ -0,0 +1,38 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""
5Unit tests for the Bing test service stub.
6"""
7
8__metaclass__ = type
9
10
11import os
12import unittest
13
14from lp.services.osutils import process_exists
15from lp.services.pidfile import pidfile_path
16from lp.services.sitesearch import bingtestservice
17
18
19class TestServiceUtilities(unittest.TestCase):
20 """Test the service's supporting functions."""
21
22 def test_stale_pid_file_cleanup(self):
23 """The service should be able to clean up invalid PID files."""
24 bogus_pid = 9999999
25 self.assertFalse(
26 process_exists(bogus_pid),
27 "There is already a process with PID '%d'." % bogus_pid)
28
29 # Create a stale/bogus PID file.
30 filepath = pidfile_path(bingtestservice.service_name)
31 with file(filepath, 'w') as pidfile:
32 pidfile.write(str(bogus_pid))
33
34 # The PID clean-up code should silently remove the file and return.
35 bingtestservice.kill_running_process()
36 self.assertFalse(
37 os.path.exists(filepath),
38 "The PID file '%s' should have been deleted." % filepath)
039
=== modified file 'lib/lp/services/sitesearch/tests/test_doc.py'
--- lib/lp/services/sitesearch/tests/test_doc.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/sitesearch/tests/test_doc.py 2018-03-29 12:55:41 +0000
@@ -9,6 +9,7 @@
99
10from lp.services.testing import build_test_suite10from lp.services.testing import build_test_suite
11from lp.testing.layers import (11from lp.testing.layers import (
12 BingLaunchpadFunctionalLayer,
12 DatabaseFunctionalLayer,13 DatabaseFunctionalLayer,
13 GoogleLaunchpadFunctionalLayer,14 GoogleLaunchpadFunctionalLayer,
14 )15 )
@@ -23,6 +24,10 @@
2324
2425
25special = {26special = {
27 'bing-searchservice.txt': LayeredDocFileSuite(
28 '../doc/bing-searchservice.txt',
29 setUp=setUp, tearDown=tearDown,
30 layer=BingLaunchpadFunctionalLayer,),
26 'google-searchservice.txt': LayeredDocFileSuite(31 'google-searchservice.txt': LayeredDocFileSuite(
27 '../doc/google-searchservice.txt',32 '../doc/google-searchservice.txt',
28 setUp=setUp, tearDown=tearDown,33 setUp=setUp, tearDown=tearDown,
2934
=== modified file 'lib/lp/testing/layers.py'
--- lib/lp/testing/layers.py 2018-03-16 14:55:41 +0000
+++ lib/lp/testing/layers.py 2018-03-29 12:55:41 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Layers used by Launchpad tests.4"""Layers used by Launchpad tests.
@@ -23,6 +23,8 @@
23 'AppServerLayer',23 'AppServerLayer',
24 'AuditorLayer',24 'AuditorLayer',
25 'BaseLayer',25 'BaseLayer',
26 'BingLaunchpadFunctionalLayer',
27 'BingServiceLayer',
26 'DatabaseFunctionalLayer',28 'DatabaseFunctionalLayer',
27 'DatabaseLayer',29 'DatabaseLayer',
28 'FunctionalLayer',30 'FunctionalLayer',
@@ -126,6 +128,9 @@
126from lp.services.osutils import kill_by_pidfile128from lp.services.osutils import kill_by_pidfile
127from lp.services.rabbit.server import RabbitServer129from lp.services.rabbit.server import RabbitServer
128from lp.services.scripts import execute_zcml_for_scripts130from lp.services.scripts import execute_zcml_for_scripts
131from lp.services.sitesearch.tests.bingserviceharness import (
132 BingServiceTestSetup,
133 )
129from lp.services.sitesearch.tests.googleserviceharness import (134from lp.services.sitesearch.tests.googleserviceharness import (
130 GoogleServiceTestSetup,135 GoogleServiceTestSetup,
131 )136 )
@@ -1259,6 +1264,31 @@
1259 pass1264 pass
12601265
12611266
1267class BingServiceLayer(BaseLayer):
1268 """Tests for Bing web service integration."""
1269
1270 @classmethod
1271 def setUp(cls):
1272 bing = BingServiceTestSetup()
1273 bing.setUp()
1274
1275 @classmethod
1276 def tearDown(cls):
1277 BingServiceTestSetup().tearDown()
1278
1279 @classmethod
1280 def testSetUp(self):
1281 # We need to override BaseLayer.testSetUp(), or else we will
1282 # get a LayerIsolationError.
1283 pass
1284
1285 @classmethod
1286 def testTearDown(self):
1287 # We need to override BaseLayer.testTearDown(), or else we will
1288 # get a LayerIsolationError.
1289 pass
1290
1291
1262class DatabaseFunctionalLayer(DatabaseLayer, FunctionalLayer):1292class DatabaseFunctionalLayer(DatabaseLayer, FunctionalLayer):
1263 """Provides the database and the Zope3 application server environment."""1293 """Provides the database and the Zope3 application server environment."""
12641294
@@ -1383,6 +1413,31 @@
1383 pass1413 pass
13841414
13851415
1416class BingLaunchpadFunctionalLayer(LaunchpadFunctionalLayer,
1417 BingServiceLayer):
1418 """Provides Bing service in addition to LaunchpadFunctionalLayer."""
1419
1420 @classmethod
1421 @profiled
1422 def setUp(cls):
1423 pass
1424
1425 @classmethod
1426 @profiled
1427 def tearDown(cls):
1428 pass
1429
1430 @classmethod
1431 @profiled
1432 def testSetUp(cls):
1433 pass
1434
1435 @classmethod
1436 @profiled
1437 def testTearDown(cls):
1438 pass
1439
1440
1386class ZopelessDatabaseLayer(ZopelessLayer, DatabaseLayer):1441class ZopelessDatabaseLayer(ZopelessLayer, DatabaseLayer):
1387 """Testing layer for unit tests with no need for librarian.1442 """Testing layer for unit tests with no need for librarian.
13881443
@@ -1541,7 +1596,8 @@
1541 return self.request._orig_env1596 return self.request._orig_env
15421597
15431598
1544class PageTestLayer(LaunchpadFunctionalLayer, GoogleServiceLayer):1599class PageTestLayer(LaunchpadFunctionalLayer,
1600 BingServiceLayer, GoogleServiceLayer):
1545 """Environment for page tests.1601 """Environment for page tests.
1546 """1602 """
15471603
15481604
=== modified file 'setup.py'
--- setup.py 2018-03-16 15:14:34 +0000
+++ setup.py 2018-03-29 12:55:41 +0000
@@ -284,6 +284,8 @@
284 },284 },
285 entry_points=dict(285 entry_points=dict(
286 console_scripts=[ # `console_scripts` is a magic name to setuptools286 console_scripts=[ # `console_scripts` is a magic name to setuptools
287 'bingtestservice = '
288 'lp.services.sitesearch.bingtestservice:main',
287 'build-twisted-plugin-cache = '289 'build-twisted-plugin-cache = '
288 'lp.services.twistedsupport.plugincache:main',290 'lp.services.twistedsupport.plugincache:main',
289 'combine-css = lp.scripts.utilities.js.combinecss:main',291 'combine-css = lp.scripts.utilities.js.combinecss:main',