Merge lp:~maxiberta/launchpad/sitesearch-cleanup-2 into lp:launchpad

Proposed by Maximiliano Bertacchini
Status: Superseded
Proposed branch: lp:~maxiberta/launchpad/sitesearch-cleanup-2
Merge into: lp:launchpad
Diff against target: 594 lines (+471/-22)
3 files modified
lib/lp/services/sitesearch/tests/test_bing.py (+163/-10)
lib/lp/services/sitesearch/tests/test_google.py (+234/-4)
lib/lp/services/sitesearch/tests/test_pagematch.py (+74/-8)
To merge this branch: bzr merge lp:~maxiberta/launchpad/sitesearch-cleanup-2
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+343235@code.launchpad.net

Commit message

Assorted sitesearch fixes and improvements (part 2).

Description of the change

Assorted sitesearch fixes and improvements (part 2).
- Add sitesearch unittests based on doctests.
  - sitesearch/doc/google-searchservice.txt => sitesearch/tests/test_google.py
  - sitesearch/doc/bing-searchservice.txt => sitesearch/tests/test_bing.py
- Add PageMatch and PageMatches unittests based on doctests.
  - sitesearch/doc/*-searchservice.txt => sitesearch/tests/test_pagematch.py

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/sitesearch/tests/test_bing.py'
2--- lib/lp/services/sitesearch/tests/test_bing.py 2018-04-12 19:42:23 +0000
3+++ lib/lp/services/sitesearch/tests/test_bing.py 2018-04-13 19:49:46 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
6+# Copyright 2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Test the bing search service."""
10@@ -7,31 +7,177 @@
11
12 __metaclass__ = type
13
14+import json
15+from os import path
16+
17 from fixtures import MockPatch
18 from requests.exceptions import (
19 ConnectionError,
20 HTTPError,
21 )
22-from testtools.matchers import MatchesStructure
23+from testtools.matchers import (
24+ HasLength,
25+ MatchesStructure,
26+)
27
28+from lp.services.config import config
29 from lp.services.sitesearch import BingSearchService
30 from lp.services.sitesearch.interfaces import SiteSearchResponseError
31 from lp.services.timeout import TimeoutError
32 from lp.testing import TestCase
33-from lp.testing.layers import (
34- BingLaunchpadFunctionalLayer,
35- LaunchpadFunctionalLayer,
36- )
37+from lp.testing.layers import BingLaunchpadFunctionalLayer
38
39
40 class TestBingSearchService(TestCase):
41 """Test BingSearchService."""
42
43- layer = LaunchpadFunctionalLayer
44-
45 def setUp(self):
46 super(TestBingSearchService, self).setUp()
47 self.search_service = BingSearchService()
48+ self.base_path = path.normpath(
49+ path.join(path.dirname(__file__), 'data'))
50+
51+ def test_configuration(self):
52+ self.assertEqual(config.bing.site, self.search_service.site)
53+ self.assertEqual(
54+ config.bing.subscription_key, self.search_service.subscription_key)
55+ self.assertEqual(
56+ config.bing.custom_config_id, self.search_service.custom_config_id)
57+
58+ def test_create_search_url(self):
59+ self.assertEndsWith(
60+ self.search_service.create_search_url(terms='svg +bugs'),
61+ '&offset=0&q=svg+%2Bbugs')
62+
63+ def test_create_search_url_escapes_unicode_chars(self):
64+ self.assertEndsWith(
65+ self.search_service.create_search_url('Carlo Perell\xf3 Mar\xedn'),
66+ '&offset=0&q=Carlo+Perell%C3%B3+Mar%C3%ADn')
67+
68+ def test_create_search_url_with_offset(self):
69+ self.assertEndsWith(
70+ self.search_service.create_search_url(terms='svg +bugs', start=20),
71+ '&offset=20&q=svg+%2Bbugs')
72+
73+ def test_create_search_url_empty_terms(self):
74+ e = self.assertRaises(
75+ ValueError, self.search_service.create_search_url, '')
76+ self.assertEqual("Missing value for parameter 'q'.", str(e))
77+
78+ def test_create_search_url_null_terms(self):
79+ e = self.assertRaises(
80+ ValueError, self.search_service.create_search_url, None)
81+ self.assertEqual("Missing value for parameter 'q'.", str(e))
82+
83+ def test_create_search_url_requires_start(self):
84+ e = self.assertRaises(
85+ ValueError, self.search_service.create_search_url, 'bugs', 'true')
86+ self.assertEqual("Value for parameter 'offset' is not an int.", str(e))
87+
88+ def test_parse_search_response_invalid_total(self):
89+ """The PageMatches's total attribute comes from the
90+ `webPages.totalEstimatedMatches` JSON element.
91+ When it cannot be found and the value cast to an int,
92+ an error is raised. If Bing were to redefine the meaning of the
93+ element to use a '~' to indicate an approximate total, an error would
94+ be raised.
95+ """
96+ file_name = path.join(
97+ self.base_path, 'bingsearchservice-incompatible-matches.json')
98+ with open(file_name, 'r') as response_file:
99+ response = response_file.read()
100+ assert (
101+ json.loads(response)['webPages']['totalEstimatedMatches'] == '~25')
102+
103+ e = self.assertRaises(
104+ SiteSearchResponseError,
105+ self.search_service._parse_search_response, response)
106+ self.assertEqual(
107+ "Could not get the total from the Bing JSON response.", str(e))
108+
109+ def test_parse_search_response_negative_total(self):
110+ """If the total is ever less than zero (see bug 683115),
111+ this is expected: we simply return a total of 0.
112+ """
113+ file_name = path.join(
114+ self.base_path, 'bingsearchservice-negative-total.json')
115+ with open(file_name, 'r') as response_file:
116+ response = response_file.read()
117+ assert json.loads(response)['webPages']['totalEstimatedMatches'] == -25
118+
119+ matches = self.search_service._parse_search_response(response)
120+ self.assertEqual(0, matches.total)
121+
122+ def test_parse_search_response_missing_title(self):
123+ """A PageMatch requires a title, url, and a summary. If those elements
124+ cannot be found, a PageMatch cannot be made. A missing title ('name')
125+ indicates a bad page on Launchpad, so it is ignored. In this example,
126+ the first match is missing a title, so only the second page is present
127+ in the PageMatches.
128+ """
129+ file_name = path.join(
130+ self.base_path, 'bingsearchservice-missing-title.json')
131+ with open(file_name, 'r') as response_file:
132+ response = response_file.read()
133+ assert len(json.loads(response)['webPages']['value']) == 2
134+
135+ matches = self.search_service._parse_search_response(response)
136+ self.assertThat(matches, HasLength(1))
137+ self.assertEqual('GCleaner in Launchpad', matches[0].title)
138+ self.assertEqual('http://launchpad.dev/gcleaner', matches[0].url)
139+
140+ def test_parse_search_response_missing_summary(self):
141+ """When a match is missing a summary ('snippet'), the match is skipped
142+ because there is no information about why it matched. This appears to
143+ relate to pages that are in the index, but should be removed. In this
144+ example taken from real data, the links are to the same page on
145+ different vhosts. The edge vhost has no summary, so it is skipped.
146+ """
147+ file_name = path.join(
148+ self.base_path, 'bingsearchservice-missing-summary.json')
149+ with open(file_name, 'r') as response_file:
150+ response = response_file.read()
151+ assert len(json.loads(response)['webPages']['value']) == 2
152+
153+ matches = self.search_service._parse_search_response(response)
154+ self.assertThat(matches, HasLength(1))
155+ self.assertEqual('BugExpiry - Launchpad Help', matches[0].title)
156+ self.assertEqual(
157+ 'https://help.launchpad.net/BugExpiry', matches[0].url)
158+
159+ def test_parse_search_response_missing_url(self):
160+ """When the URL ('url') cannot be found the match is skipped. There are
161+ no examples of this. We do not want this hypothetical situation to give
162+ users a bad experience.
163+ """
164+ file_name = path.join(
165+ self.base_path, 'bingsearchservice-missing-url.json')
166+ with open(file_name, 'r') as response_file:
167+ response = response_file.read()
168+ assert len(json.loads(response)['webPages']['value']) == 2
169+
170+ matches = self.search_service._parse_search_response(response)
171+ self.assertThat(matches, HasLength(1))
172+ self.assertEqual('LongoMatch in Launchpad', matches[0].title)
173+ self.assertEqual('http://launchpad.dev/longomatch', matches[0].url)
174+
175+ def test_parse_search_response_with_no_meaningful_results(self):
176+ """If no matches are found in the response, and there are 20 or fewer
177+ results, an Empty PageMatches is returned. This happens when the
178+ results are missing titles and summaries. This is not considered to be
179+ a problem because the small number implies that Bing did a poor job
180+ of indexing pages or indexed the wrong Launchpad server. In this
181+ example, there is only one match, but the results is missing a title so
182+ there is not enough information to make a PageMatch.
183+ """
184+ file_name = path.join(
185+ self.base_path, 'bingsearchservice-no-meaningful-results.json')
186+ with open(file_name, 'r') as response_file:
187+ response = response_file.read()
188+ assert len(json.loads(response)['webPages']['value']) == 1
189+
190+ matches = self.search_service._parse_search_response(response)
191+ self.assertThat(matches, HasLength(0))
192
193 def test_search_converts_HTTPError(self):
194 # The method converts HTTPError to SiteSearchResponseError.
195@@ -76,13 +222,13 @@
196 self.search_service._parse_search_response, '{}')
197
198
199-class FunctionalTestBingSearchService(TestCase):
200+class FunctionalBingSearchServiceTests(TestCase):
201 """Test BingSearchService."""
202
203 layer = BingLaunchpadFunctionalLayer
204
205 def setUp(self):
206- super(FunctionalTestBingSearchService, self).setUp()
207+ super(FunctionalBingSearchServiceTests, self).setUp()
208 self.search_service = BingSearchService()
209
210 def test_search_with_results(self):
211@@ -96,6 +242,13 @@
212 self.assertEqual(20, matches.start)
213 self.assertEqual(25, matches.total)
214 self.assertEqual(5, len(matches))
215+ self.assertEqual([
216+ 'https://help.launchpad.net/Bugs',
217+ 'http://blog.launchpad.dev/general/of-bugs-and-statuses',
218+ 'http://launchpad.dev/mahara/+milestone/1.8.0',
219+ 'http://launchpad.dev/mb',
220+ 'http://launchpad.dev/bugs'],
221+ [match.url for match in matches])
222
223 def test_search_no_results(self):
224 matches = self.search_service.search('fnord')
225
226=== modified file 'lib/lp/services/sitesearch/tests/test_google.py'
227--- lib/lp/services/sitesearch/tests/test_google.py 2018-04-12 19:46:41 +0000
228+++ lib/lp/services/sitesearch/tests/test_google.py 2018-04-13 19:49:46 +0000
229@@ -3,30 +3,214 @@
230
231 """Test the google search service."""
232
233+from __future__ import absolute_import, print_function, unicode_literals
234+
235 __metaclass__ = type
236
237+from os import path
238
239 from fixtures import MockPatch
240 from requests.exceptions import (
241 ConnectionError,
242 HTTPError,
243 )
244+from testtools.matchers import HasLength
245
246+from lp.services.config import config
247 from lp.services.sitesearch import GoogleSearchService
248-from lp.services.sitesearch.interfaces import SiteSearchResponseError
249+from lp.services.sitesearch.interfaces import (
250+ GoogleWrongGSPVersion,
251+ SiteSearchResponseError,
252+ )
253 from lp.services.timeout import TimeoutError
254 from lp.testing import TestCase
255-from lp.testing.layers import LaunchpadFunctionalLayer
256+from lp.testing.layers import GoogleLaunchpadFunctionalLayer
257
258
259 class TestGoogleSearchService(TestCase):
260 """Test GoogleSearchService."""
261
262- layer = LaunchpadFunctionalLayer
263-
264 def setUp(self):
265 super(TestGoogleSearchService, self).setUp()
266 self.search_service = GoogleSearchService()
267+ self.base_path = path.normpath(
268+ path.join(path.dirname(__file__), 'data'))
269+
270+ def test_configuration(self):
271+ self.assertEqual(config.google.site, self.search_service.site)
272+ self.assertEqual(
273+ config.google.client_id, self.search_service.client_id)
274+
275+ def test_create_search_url(self):
276+ self.assertEndsWith(
277+ self.search_service.create_search_url(terms='svg +bugs'),
278+ '&q=svg+%2Bbugs&start=0')
279+
280+ def test_create_search_url_escapes_unicode_chars(self):
281+ self.assertEndsWith(
282+ self.search_service.create_search_url('Carlo Perell\xf3 Mar\xedn'),
283+ '&q=Carlo+Perell%C3%B3+Mar%C3%ADn&start=0')
284+
285+ def test_create_search_url_with_offset(self):
286+ self.assertEndsWith(
287+ self.search_service.create_search_url(terms='svg +bugs', start=20),
288+ '&q=svg+%2Bbugs&start=20')
289+
290+ def test_create_search_url_empty_terms(self):
291+ e = self.assertRaises(
292+ ValueError, self.search_service.create_search_url, '')
293+ self.assertEqual("Missing value for parameter 'q'.", str(e))
294+
295+ def test_create_search_url_null_terms(self):
296+ e = self.assertRaises(
297+ ValueError, self.search_service.create_search_url, None)
298+ self.assertEqual("Missing value for parameter 'q'.", str(e))
299+
300+ def test_create_search_url_requires_start(self):
301+ e = self.assertRaises(
302+ ValueError, self.search_service.create_search_url, 'bugs', 'true')
303+ self.assertEqual("Value for parameter 'start' is not an int.", str(e))
304+
305+ def test_parse_search_response_incompatible_param(self):
306+ """The PageMatches's start attribute comes from the GSP XML element
307+ '<PARAM name="start" value="0" original_value="0"/>'. When it cannot
308+ be found and the value cast to an int, an error is raised. There is
309+ nothing in the value attribute in the next test, so an error is raised.
310+ """
311+ file_name = path.join(
312+ self.base_path, 'googlesearchservice-incompatible-param.xml')
313+ with open(file_name, 'r') as response_file:
314+ response = response_file.read()
315+ assert '<M>' not in response
316+
317+ e = self.assertRaises(
318+ GoogleWrongGSPVersion,
319+ self.search_service._parse_search_response, response)
320+ self.assertEqual(
321+ "Could not get the 'start' from the GSP XML response.", str(e))
322+
323+ def test_parse_search_response_invalid_total(self):
324+ """The PageMatches's total attribute comes from the GSP XML element
325+ '<M>5</M>'. When it cannot be found and the value cast to an int,
326+ an error is raised. If Google were to redefine the meaning of the M
327+ element to use a '~' to indicate an approximate total, an error would
328+ be raised.
329+ """
330+ file_name = path.join(
331+ self.base_path, 'googlesearchservice-incompatible-matches.xml')
332+ with open(file_name, 'r') as response_file:
333+ response = response_file.read()
334+ assert '<M>~1</M>' in response
335+
336+ e = self.assertRaises(
337+ GoogleWrongGSPVersion,
338+ self.search_service._parse_search_response, response)
339+ self.assertEqual(
340+ "Could not get the 'total' from the GSP XML response.", str(e))
341+
342+ def test_parse_search_response_negative_total(self):
343+ """If the total is ever less than zero (see bug 683115),
344+ this is expected: we simply return a total of 0.
345+ """
346+ file_name = path.join(
347+ self.base_path, 'googlesearchservice-negative-total.xml')
348+ with open(file_name, 'r') as response_file:
349+ response = response_file.read()
350+ assert '<M>-1</M>' in response
351+
352+ matches = self.search_service._parse_search_response(response)
353+ self.assertEqual(0, matches.total)
354+
355+ def test_parse_search_response_missing_title(self):
356+ """A PageMatch requires a title, url, and a summary. If those elements
357+ ('<T>', '<U>', '<S>') cannot be found nested in an '<R>' a PageMatch
358+ cannot be made. A missing title (<T>) indicates a bad page on Launchpad
359+ so it is ignored. In this example, The first match is missing a title,
360+ so only the second page is present in the PageMatches.
361+ """
362+ file_name = path.join(
363+ self.base_path, 'googlesearchservice-missing-title.xml')
364+ with open(file_name, 'r') as response_file:
365+ response = response_file.read()
366+
367+ matches = self.search_service._parse_search_response(response)
368+ self.assertThat(matches, HasLength(1))
369+ self.assertStartsWith(matches[0].title, 'Bug #205991 in Ubuntu:')
370+ self.assertEqual(
371+ 'http://bugs.launchpad.dev/bugs/205991', matches[0].url)
372+
373+ def test_parse_search_response_missing_summary(self):
374+ """When a match is missing a summary (<S>), it is skipped because
375+ there is no information about why it matched. This appears to relate to
376+ pages that are in the index, but should be removed. In this example
377+ taken from real data, the links are to the same page on different
378+ vhosts. The edge vhost has no summary, so it is skipped.
379+ """
380+ file_name = path.join(
381+ self.base_path, 'googlesearchservice-missing-summary.xml')
382+ with open(file_name, 'r') as response_file:
383+ response = response_file.read()
384+
385+ matches = self.search_service._parse_search_response(response)
386+ self.assertThat(matches, HasLength(1))
387+ self.assertEqual('Blueprint: <b>Gobuntu</b> 8.04', matches[0].title)
388+ self.assertEqual(
389+ 'http://blueprints.launchpad.dev/ubuntu/+spec/gobuntu-hardy',
390+ matches[0].url)
391+
392+ def test_parse_search_response_missing_url(self):
393+ """When the URL (<U>) cannot be found the match is skipped. There are
394+ no examples of this. We do not want this hypothetical situation to give
395+ users a bad experience.
396+ """
397+ file_name = path.join(
398+ self.base_path, 'googlesearchservice-missing-url.xml')
399+ with open(file_name, 'r') as response_file:
400+ response = response_file.read()
401+
402+ matches = self.search_service._parse_search_response(response)
403+ self.assertThat(matches, HasLength(1))
404+ self.assertEqual('Blueprint: <b>Gobuntu</b> 8.04', matches[0].title)
405+ self.assertEqual(
406+ 'http://blueprints.launchpad.dev/ubuntu/+spec/gobuntu-hardy',
407+ matches[0].url)
408+
409+ def test_parse_search_response_with_no_meaningful_results(self):
410+ """If no matches are found in the response, and there are 20 or fewer
411+ results, an Empty PageMatches is returned. This happens when the
412+ results are missing titles and summaries. This is not considered to be
413+ a problem because the small number implies that Google did a poor job
414+ of indexing pages or indexed the wrong Launchpad server. In this
415+ example, there is only one match, but the results is missing a title so
416+ there is not enough information to make a PageMatch.
417+ """
418+ file_name = path.join(
419+ self.base_path, 'googlesearchservice-no-meaningful-results.xml')
420+ with open(file_name, 'r') as response_file:
421+ response = response_file.read()
422+ assert '<M>1</M>' in response
423+
424+ matches = self.search_service._parse_search_response(response)
425+ self.assertThat(matches, HasLength(0))
426+
427+ def test_parse_search_response_with_incompatible_result(self):
428+ """If no matches are found in the response, and there are more than 20
429+ possible matches, an error is raised. Unlike the previous example there
430+ are lots of results; there is a possibility that the GSP version is
431+ incompatible. This example says it has 1000 matches, but none of the R
432+ tags can be parsed (because the markup was changed to use RESULT).
433+ """
434+ file_name = path.join(
435+ self.base_path, 'googlesearchservice-incompatible-result.xml')
436+ with open(file_name, 'r') as response_file:
437+ response = response_file.read()
438+ assert '<M>1000</M>' in response
439+
440+ e = self.assertRaises(
441+ GoogleWrongGSPVersion,
442+ self.search_service._parse_search_response, response)
443+ self.assertEqual(
444+ "Could not get any PageMatches from the GSP XML response.", str(e))
445
446 def test_search_converts_HTTPError(self):
447 # The method converts HTTPError to SiteSearchResponseError.
448@@ -71,3 +255,49 @@
449 self.assertRaises(
450 SiteSearchResponseError,
451 self.search_service._parse_search_response, data)
452+
453+
454+class FunctionalGoogleSearchServiceTests(TestCase):
455+ """Test GoogleSearchService."""
456+
457+ layer = GoogleLaunchpadFunctionalLayer
458+
459+ def setUp(self):
460+ super(FunctionalGoogleSearchServiceTests, self).setUp()
461+ self.search_service = GoogleSearchService()
462+
463+ def test_search_with_results(self):
464+ matches = self.search_service.search('bug')
465+ self.assertEqual(0, matches.start)
466+ self.assertEqual(25, matches.total)
467+ self.assertEqual(20, len(matches))
468+
469+ def test_search_with_results_and_offset(self):
470+ matches = self.search_service.search('bug', start=20)
471+ self.assertEqual(20, matches.start)
472+ self.assertEqual(25, matches.total)
473+ self.assertEqual(5, len(matches))
474+ self.assertEqual([
475+ 'http://bugs.launchpad.dev/ubuntu/hoary/+bug/2',
476+ 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2',
477+ 'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/3',
478+ 'http://bugs.launchpad.dev/bugs/bugtrackers',
479+ 'http://bugs.launchpad.dev/bugs/bugtrackers/debbugs'],
480+ [match.url for match in matches])
481+
482+ def test_search_no_results(self):
483+ matches = self.search_service.search('fnord')
484+ self.assertEqual(0, matches.start)
485+ self.assertEqual(0, matches.total)
486+ self.assertEqual(0, len(matches))
487+
488+ def test_search_no_meaningful_results(self):
489+ matches = self.search_service.search('no-meaningful')
490+ self.assertEqual(0, matches.start)
491+ self.assertEqual(1, matches.total)
492+ self.assertEqual(0, len(matches))
493+
494+ def test_search_incomplete_response(self):
495+ self.assertRaises(
496+ SiteSearchResponseError,
497+ self.search_service.search, 'gnomebaker')
498
499=== modified file 'lib/lp/services/sitesearch/tests/test_pagematch.py'
500--- lib/lp/services/sitesearch/tests/test_pagematch.py 2018-03-16 14:02:16 +0000
501+++ lib/lp/services/sitesearch/tests/test_pagematch.py 2018-04-13 19:49:46 +0000
502@@ -5,14 +5,59 @@
503
504 __metaclass__ = type
505
506-from lp.services.sitesearch import PageMatch
507-from lp.testing import TestCaseWithFactory
508-from lp.testing.layers import DatabaseFunctionalLayer
509-
510-
511-class TestPageMatchURLHandling(TestCaseWithFactory):
512-
513- layer = DatabaseFunctionalLayer
514+from lp.services.sitesearch import (
515+ PageMatch,
516+ PageMatches,
517+ )
518+from lp.testing import TestCase
519+
520+
521+class TestPageMatchURLHandling(TestCase):
522+
523+ def test_attributes(self):
524+ p = PageMatch(
525+ u'Unicode Titles in Launchpad',
526+ 'http://example.com/unicode-titles',
527+ u'Unicode Titles is a modest project dedicated to using Unicode.')
528+ self.assertEqual(u'Unicode Titles in Launchpad', p.title)
529+ self.assertEqual(
530+ u'Unicode Titles is a modest project dedicated to using Unicode.',
531+ p.summary)
532+ self.assertEqual('http://example.com/unicode-titles', p.url)
533+
534+ def test_rewrite_url(self):
535+ """The URL scheme used in the rewritten URL is configured via
536+ config.vhosts.use_https. The hostname is set in the shared
537+ key config.vhost.mainsite.hostname.
538+ """
539+ p = PageMatch(
540+ u'Bug #456 in Unicode title: "testrunner hates Unicode"',
541+ 'https://bugs.launchpad.net/unicode-titles/+bug/456',
542+ u'The Zope testrunner likes ASCII more than Unicode.')
543+ self.assertEqual(
544+ 'http://bugs.launchpad.dev/unicode-titles/+bug/456', p.url)
545+
546+ def test_rewrite_url_with_trailing_slash(self):
547+ """A URL's trailing slash is removed; Launchpad does not use trailing
548+ slashes.
549+ """
550+ p = PageMatch(
551+ u'Ubuntu in Launchpad',
552+ 'https://launchpad.net/ubuntu/',
553+ u'Ubuntu also includes more software than any other operating')
554+ self.assertEqual('http://launchpad.dev/ubuntu', p.url)
555+
556+ def test_rewrite_url_exceptions(self):
557+ """There is a list of URLs that are not rewritten configured in
558+ config.sitesearch.url_rewrite_exceptions. For example,
559+ help.launchpad.net is only run in one environment, so links to
560+ that site will be preserved.
561+ """
562+ p = PageMatch(
563+ u'OpenID',
564+ 'https://help.launchpad.net/OpenID',
565+ u'Launchpad uses OpenID.')
566+ self.assertEqual('https://help.launchpad.net/OpenID', p.url)
567
568 def test_rewrite_url_handles_invalid_data(self):
569 # Given a bad url, pagematch can get a valid one.
570@@ -37,3 +82,24 @@
571 "field.text=WUSB54GC+%2Bkarmic&"
572 "field.actions.search=Search")
573 self.assertEqual(expected, p.url)
574+
575+
576+class TestPageMatches(TestCase):
577+
578+ def test_initialisation(self):
579+ matches = PageMatches([], start=12, total=15)
580+ self.assertEqual(12, matches.start)
581+ self.assertEqual(15, matches.total)
582+
583+ def test_len(self):
584+ matches = PageMatches(['match1', 'match2', 'match3'], 12, 15)
585+ self.assertEqual(3, len(matches))
586+
587+ def test_getitem(self):
588+ matches = PageMatches(['match1', 'match2', 'match3'], 12, 15)
589+ self.assertEqual('match2', matches[1])
590+
591+ def test_iter(self):
592+ matches = PageMatches(['match1', 'match2', 'match3'], 12, 15)
593+ self.assertEqual(
594+ ['match1', 'match2', 'match3'], [match for match in matches])