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