1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/bookmarks/core/browser/bookmark_index.h"
10 #include "base/message_loop/message_loop.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_split.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
16 #include "chrome/browser/history/history_service.h"
17 #include "chrome/browser/history/history_service_factory.h"
18 #include "chrome/browser/history/url_database.h"
19 #include "chrome/test/base/testing_profile.h"
20 #include "components/bookmarks/core/browser/bookmark_match.h"
21 #include "components/bookmarks/core/browser/bookmark_model.h"
22 #include "components/bookmarks/core/test/bookmark_test_helpers.h"
23 #include "components/bookmarks/core/test/test_bookmark_client.h"
24 #include "content/public/test/test_browser_thread_bundle.h"
25 #include "testing/gtest/include/gtest/gtest.h"
27 using base::ASCIIToUTF16
;
29 class BookmarkIndexTest
: public testing::Test
{
31 BookmarkIndexTest() : model_(client_
.CreateModel(false)) {}
33 typedef std::pair
<std::string
, std::string
> TitleAndURL
;
35 void AddBookmarks(const char** titles
, const char** urls
, size_t count
) {
36 // The pair is (title, url).
37 std::vector
<TitleAndURL
> bookmarks
;
38 for (size_t i
= 0; i
< count
; ++i
) {
39 TitleAndURL
bookmark(titles
[i
], urls
[i
]);
40 bookmarks
.push_back(bookmark
);
42 AddBookmarks(bookmarks
);
45 void AddBookmarks(const std::vector
<TitleAndURL
> bookmarks
) {
46 for (size_t i
= 0; i
< bookmarks
.size(); ++i
) {
47 model_
->AddURL(model_
->other_node(), static_cast<int>(i
),
48 ASCIIToUTF16(bookmarks
[i
].first
),
49 GURL(bookmarks
[i
].second
));
53 void ExpectMatches(const std::string
& query
,
54 const char** expected_titles
,
55 size_t expected_count
) {
56 std::vector
<std::string
> title_vector
;
57 for (size_t i
= 0; i
< expected_count
; ++i
)
58 title_vector
.push_back(expected_titles
[i
]);
59 ExpectMatches(query
, title_vector
);
62 void ExpectMatches(const std::string
& query
,
63 const std::vector
<std::string
>& expected_titles
) {
64 std::vector
<BookmarkMatch
> matches
;
65 model_
->GetBookmarksMatching(ASCIIToUTF16(query
), 1000, &matches
);
66 ASSERT_EQ(expected_titles
.size(), matches
.size());
67 for (size_t i
= 0; i
< expected_titles
.size(); ++i
) {
69 for (size_t j
= 0; j
< matches
.size(); ++j
) {
70 if (ASCIIToUTF16(expected_titles
[i
]) == matches
[j
].node
->GetTitle()) {
71 matches
.erase(matches
.begin() + j
);
80 void ExtractMatchPositions(const std::string
& string
,
81 BookmarkMatch::MatchPositions
* matches
) {
82 std::vector
<std::string
> match_strings
;
83 base::SplitString(string
, ':', &match_strings
);
84 for (size_t i
= 0; i
< match_strings
.size(); ++i
) {
85 std::vector
<std::string
> chunks
;
86 base::SplitString(match_strings
[i
], ',', &chunks
);
87 ASSERT_EQ(2U, chunks
.size());
88 matches
->push_back(BookmarkMatch::MatchPosition());
90 base::StringToInt(chunks
[0], &chunks0
);
91 base::StringToInt(chunks
[1], &chunks1
);
92 matches
->back().first
= chunks0
;
93 matches
->back().second
= chunks1
;
97 void ExpectMatchPositions(
98 const BookmarkMatch::MatchPositions
& actual_positions
,
99 const BookmarkMatch::MatchPositions
& expected_positions
) {
100 ASSERT_EQ(expected_positions
.size(), actual_positions
.size());
101 for (size_t i
= 0; i
< expected_positions
.size(); ++i
) {
102 EXPECT_EQ(expected_positions
[i
].first
, actual_positions
[i
].first
);
103 EXPECT_EQ(expected_positions
[i
].second
, actual_positions
[i
].second
);
108 test::TestBookmarkClient client_
;
109 scoped_ptr
<BookmarkModel
> model_
;
112 DISALLOW_COPY_AND_ASSIGN(BookmarkIndexTest
);
115 // Various permutations with differing input, queries and output that exercises
117 TEST_F(BookmarkIndexTest
, GetBookmarksMatching
) {
119 const std::string titles
;
120 const std::string query
;
121 const std::string expected
;
123 // Trivial test case of only one term, exact match.
126 // Prefix match, one term.
127 { "abcd;abc;b", "abc", "abcd;abc" },
129 // Prefix match, multiple terms.
130 { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg"},
132 // Exact and prefix match.
133 { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef"},
135 // Exact and prefix match.
136 { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab",
140 // Title with term multiple times.
141 { "ab ab", "ab", "ab ab"},
143 // Make sure quotes don't do a prefix match.
144 { "think", "\"thi\"", ""},
146 // Prefix matches against multiple candidates.
147 { "abc1 abc2 abc3 abc4", "abc", "abc1 abc2 abc3 abc4"},
149 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
150 std::vector
<std::string
> titles
;
151 base::SplitString(data
[i
].titles
, ';', &titles
);
152 std::vector
<TitleAndURL
> bookmarks
;
153 for (size_t j
= 0; j
< titles
.size(); ++j
) {
154 TitleAndURL
bookmark(titles
[j
], "about:blank");
155 bookmarks
.push_back(bookmark
);
157 AddBookmarks(bookmarks
);
159 std::vector
<std::string
> expected
;
160 if (!data
[i
].expected
.empty())
161 base::SplitString(data
[i
].expected
, ';', &expected
);
163 ExpectMatches(data
[i
].query
, expected
);
165 model_
= client_
.CreateModel(false);
169 // Analogous to GetBookmarksMatching, this test tests various permutations
170 // of title, URL, and input to see if the title/URL matches the input as
172 TEST_F(BookmarkIndexTest
, GetBookmarksMatchingWithURLs
) {
174 const std::string query
;
175 const std::string title
;
176 const std::string url
;
177 const bool should_be_retrieved
;
179 // Test single-word inputs. Include both exact matches and prefix matches.
180 { "foo", "Foo", "http://www.bar.com/", true },
181 { "foo", "Foodie", "http://www.bar.com/", true },
182 { "foo", "Bar", "http://www.foo.com/", true },
183 { "foo", "Bar", "http://www.foodie.com/", true },
184 { "foo", "Foo", "http://www.foo.com/", true },
185 { "foo", "Bar", "http://www.bar.com/", false },
186 { "foo", "Bar", "http://www.bar.com/blah/foo/blah-again/ ", true },
187 { "foo", "Bar", "http://www.bar.com/blah/foodie/blah-again/ ", true },
188 { "foo", "Bar", "http://www.bar.com/blah-foo/blah-again/ ", true },
189 { "foo", "Bar", "http://www.bar.com/blah-foodie/blah-again/ ", true },
190 { "foo", "Bar", "http://www.bar.com/blahafoo/blah-again/ ", false },
192 // Test multi-word inputs.
193 { "foo bar", "Foo Bar", "http://baz.com/", true },
194 { "foo bar", "Foodie Bar", "http://baz.com/", true },
195 { "bar foo", "Foo Bar", "http://baz.com/", true },
196 { "bar foo", "Foodie Barly", "http://baz.com/", true },
197 { "foo bar", "Foo Baz", "http://baz.com/", false },
198 { "foo bar", "Foo Baz", "http://bar.com/", true },
199 { "foo bar", "Foo Baz", "http://barly.com/", true },
200 { "foo bar", "Foodie Baz", "http://barly.com/", true },
201 { "bar foo", "Foo Baz", "http://bar.com/", true },
202 { "bar foo", "Foo Baz", "http://barly.com/", true },
203 { "foo bar", "Baz Bar", "http://blah.com/foo", true },
204 { "foo bar", "Baz Barly", "http://blah.com/foodie", true },
205 { "foo bar", "Baz Bur", "http://blah.com/foo/bar", true },
206 { "foo bar", "Baz Bur", "http://blah.com/food/barly", true },
207 { "foo bar", "Baz Bur", "http://bar.com/blah/foo", true },
208 { "foo bar", "Baz Bur", "http://barly.com/blah/food", true },
209 { "foo bar", "Baz Bur", "http://bar.com/blah/flub", false },
210 { "foo bar", "Baz Bur", "http://foo.com/blah/flub", false }
213 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
214 model_
= client_
.CreateModel(true);
215 std::vector
<TitleAndURL
> bookmarks
;
216 bookmarks
.push_back(TitleAndURL(data
[i
].title
, data
[i
].url
));
217 AddBookmarks(bookmarks
);
219 std::vector
<std::string
> expected
;
220 if (data
[i
].should_be_retrieved
)
221 expected
.push_back(data
[i
].title
);
223 ExpectMatches(data
[i
].query
, expected
);
227 TEST_F(BookmarkIndexTest
, Normalization
) {
232 { "fooa\xcc\x88-test", "foo\xc3\xa4-test" },
233 { "fooa\xcc\x88-test", "fooa\xcc\x88-test" },
234 { "fooa\xcc\x88-test", "foo\xc3\xa4" },
235 { "fooa\xcc\x88-test", "fooa\xcc\x88" },
236 { "fooa\xcc\x88-test", "foo" },
237 { "foo\xc3\xa4-test", "foo\xc3\xa4-test" },
238 { "foo\xc3\xa4-test", "fooa\xcc\x88-test" },
239 { "foo\xc3\xa4-test", "foo\xc3\xa4" },
240 { "foo\xc3\xa4-test", "fooa\xcc\x88" },
241 { "foo\xc3\xa4-test", "foo" },
245 GURL
url("about:blank");
246 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
247 model_
->AddURL(model_
->other_node(), 0, base::UTF8ToUTF16(data
[i
].title
),
249 std::vector
<BookmarkMatch
> matches
;
250 model_
->GetBookmarksMatching(
251 base::UTF8ToUTF16(data
[i
].query
), 10, &matches
);
252 EXPECT_EQ(1u, matches
.size());
253 model_
= client_
.CreateModel(false);
257 // Makes sure match positions are updated appropriately for title matches.
258 TEST_F(BookmarkIndexTest
, MatchPositionsTitles
) {
260 const std::string title
;
261 const std::string query
;
262 const std::string expected_title_match_positions
;
264 // Trivial test case of only one term, exact match.
266 { "foo bar", "bar", "4,7" },
267 { "fooey bark", "bar foo", "0,3:6,9" },
268 // Non-trivial tests.
269 { "foobar foo", "foobar foo", "0,6:7,10" },
270 { "foobar foo", "foo foobar", "0,6:7,10" },
271 { "foobar foobar", "foobar foo", "0,6:7,13" },
272 { "foobar foobar", "foo foobar", "0,6:7,13" },
274 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
275 std::vector
<TitleAndURL
> bookmarks
;
276 TitleAndURL
bookmark(data
[i
].title
, "about:blank");
277 bookmarks
.push_back(bookmark
);
278 AddBookmarks(bookmarks
);
280 std::vector
<BookmarkMatch
> matches
;
281 model_
->GetBookmarksMatching(ASCIIToUTF16(data
[i
].query
), 1000, &matches
);
282 ASSERT_EQ(1U, matches
.size());
284 BookmarkMatch::MatchPositions expected_title_matches
;
285 ExtractMatchPositions(data
[i
].expected_title_match_positions
,
286 &expected_title_matches
);
287 ExpectMatchPositions(matches
[0].title_match_positions
,
288 expected_title_matches
);
290 model_
= client_
.CreateModel(false);
294 // Makes sure match positions are updated appropriately for URL matches.
295 TEST_F(BookmarkIndexTest
, MatchPositionsURLs
) {
296 // The encoded stuff between /wiki/ and the # is 第二次世界大戦
297 const std::string ja_wiki_url
= "http://ja.wikipedia.org/wiki/%E7%AC%AC%E4"
298 "%BA%8C%E6%AC%A1%E4%B8%96%E7%95%8C%E5%A4%A7%E6%88%A6#.E3.83.B4.E3.82.A7"
299 ".E3.83.AB.E3.82.B5.E3.82.A4.E3.83.A6.E4.BD.93.E5.88.B6";
301 const std::string query
;
302 const std::string url
;
303 const std::string expected_url_match_positions
;
305 { "foo", "http://www.foo.com/", "11,14" },
306 { "foo", "http://www.foodie.com/", "11,14" },
307 { "foo", "http://www.foofoo.com/", "11,14" },
308 { "www", "http://www.foo.com/", "7,10" },
309 { "foo", "http://www.foodie.com/blah/foo/fi", "11,14:27,30" },
310 { "foo", "http://www.blah.com/blah/foo/fi", "25,28" },
311 { "foo www", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" },
312 { "www foo", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" },
313 { "www bla", "http://www.foodie.com/blah/foo/fi", "7,10:22,25" },
314 { "http", "http://www.foo.com/", "0,4" },
315 { "http www", "http://www.foo.com/", "0,4:7,10" },
316 { "http foo", "http://www.foo.com/", "0,4:11,14" },
317 { "http foo", "http://www.bar.com/baz/foodie/hi", "0,4:23,26" },
318 { "第二次", ja_wiki_url
, "29,56" },
319 { "ja 第二次", ja_wiki_url
, "7,9:29,56" },
320 { "第二次 E3.8", ja_wiki_url
, "29,56:94,98:103,107:"
325 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
326 model_
= client_
.CreateModel(true);
327 std::vector
<TitleAndURL
> bookmarks
;
328 TitleAndURL
bookmark("123456", data
[i
].url
);
329 bookmarks
.push_back(bookmark
);
330 AddBookmarks(bookmarks
);
332 std::vector
<BookmarkMatch
> matches
;
333 model_
->GetBookmarksMatching(
334 base::UTF8ToUTF16(data
[i
].query
), 1000, &matches
);
335 ASSERT_EQ(1U, matches
.size()) << data
[i
].url
<< data
[i
].query
;
337 BookmarkMatch::MatchPositions expected_url_matches
;
338 ExtractMatchPositions(data
[i
].expected_url_match_positions
,
339 &expected_url_matches
);
340 ExpectMatchPositions(matches
[0].url_match_positions
, expected_url_matches
);
344 // Makes sure index is updated when a node is removed.
345 TEST_F(BookmarkIndexTest
, Remove
) {
346 const char* titles
[] = { "a", "b" };
347 const char* urls
[] = { "about:blank", "about:blank" };
348 AddBookmarks(titles
, urls
, ARRAYSIZE_UNSAFE(titles
));
350 // Remove the node and make sure we don't get back any results.
351 model_
->Remove(model_
->other_node(), 0);
352 ExpectMatches("A", NULL
, 0U);
355 // Makes sure index is updated when a node's title is changed.
356 TEST_F(BookmarkIndexTest
, ChangeTitle
) {
357 const char* titles
[] = { "a", "b" };
358 const char* urls
[] = { "about:blank", "about:blank" };
359 AddBookmarks(titles
, urls
, ARRAYSIZE_UNSAFE(titles
));
361 // Remove the node and make sure we don't get back any results.
362 const char* expected
[] = { "blah" };
363 model_
->SetTitle(model_
->other_node()->GetChild(0), ASCIIToUTF16("blah"));
364 ExpectMatches("BlAh", expected
, ARRAYSIZE_UNSAFE(expected
));
367 // Makes sure no more than max queries is returned.
368 TEST_F(BookmarkIndexTest
, HonorMax
) {
369 const char* titles
[] = { "abcd", "abcde" };
370 const char* urls
[] = { "about:blank", "about:blank" };
371 AddBookmarks(titles
, urls
, ARRAYSIZE_UNSAFE(titles
));
373 std::vector
<BookmarkMatch
> matches
;
374 model_
->GetBookmarksMatching(ASCIIToUTF16("ABc"), 1, &matches
);
375 EXPECT_EQ(1U, matches
.size());
378 // Makes sure if the lower case string of a bookmark title is more characters
379 // than the upper case string no match positions are returned.
380 TEST_F(BookmarkIndexTest
, EmptyMatchOnMultiwideLowercaseString
) {
381 const BookmarkNode
* n1
= model_
->AddURL(model_
->other_node(), 0,
382 base::WideToUTF16(L
"\u0130 i"),
383 GURL("http://www.google.com"));
385 std::vector
<BookmarkMatch
> matches
;
386 model_
->GetBookmarksMatching(ASCIIToUTF16("i"), 100, &matches
);
387 ASSERT_EQ(1U, matches
.size());
388 EXPECT_TRUE(matches
[0].node
== n1
);
389 EXPECT_TRUE(matches
[0].title_match_positions
.empty());
392 TEST_F(BookmarkIndexTest
, GetResultsSortedByTypedCount
) {
393 // This ensures MessageLoop::current() will exist, which is needed by
394 // TestingProfile::BlockUntilHistoryProcessesPendingRequests().
395 content::TestBrowserThreadBundle thread_bundle
;
397 TestingProfile profile
;
398 ASSERT_TRUE(profile
.CreateHistoryService(true, false));
399 profile
.BlockUntilHistoryProcessesPendingRequests();
400 profile
.CreateBookmarkModel(true);
402 BookmarkModel
* model
= BookmarkModelFactory::GetForProfile(&profile
);
403 test::WaitForBookmarkModelToLoad(model
);
405 HistoryService
* const history_service
=
406 HistoryServiceFactory::GetForProfile(&profile
, Profile::EXPLICIT_ACCESS
);
408 history::URLDatabase
* url_db
= history_service
->InMemoryDatabase();
413 const int typed_count
;
415 { GURL("http://www.google.com/"), "Google", 100 },
416 { GURL("http://maps.google.com/"), "Google Maps", 40 },
417 { GURL("http://docs.google.com/"), "Google Docs", 50 },
418 { GURL("http://reader.google.com/"), "Google Reader", 80 },
421 for (size_t i
= 0; i
< ARRAYSIZE_UNSAFE(data
); ++i
) {
422 history::URLRow
info(data
[i
].url
);
423 info
.set_title(base::UTF8ToUTF16(data
[i
].title
));
424 info
.set_typed_count(data
[i
].typed_count
);
425 // Populate the InMemoryDatabase....
426 url_db
->AddURL(info
);
427 // Populate the BookmarkIndex.
428 model
->AddURL(model
->other_node(), i
, base::UTF8ToUTF16(data
[i
].title
),
432 // Check that the InMemoryDatabase stored the URLs properly.
433 history::URLRow result1
;
434 url_db
->GetRowForURL(data
[0].url
, &result1
);
435 EXPECT_EQ(data
[0].title
, base::UTF16ToUTF8(result1
.title()));
437 history::URLRow result2
;
438 url_db
->GetRowForURL(data
[1].url
, &result2
);
439 EXPECT_EQ(data
[1].title
, base::UTF16ToUTF8(result2
.title()));
441 history::URLRow result3
;
442 url_db
->GetRowForURL(data
[2].url
, &result3
);
443 EXPECT_EQ(data
[2].title
, base::UTF16ToUTF8(result3
.title()));
445 history::URLRow result4
;
446 url_db
->GetRowForURL(data
[3].url
, &result4
);
447 EXPECT_EQ(data
[3].title
, base::UTF16ToUTF8(result4
.title()));
449 // Populate match nodes.
450 std::vector
<BookmarkMatch
> matches
;
451 model
->GetBookmarksMatching(ASCIIToUTF16("google"), 4, &matches
);
453 // The resulting order should be:
454 // 1. Google (google.com) 100
455 // 2. Google Reader (google.com/reader) 80
456 // 3. Google Docs (docs.google.com) 50
457 // 4. Google Maps (maps.google.com) 40
458 EXPECT_EQ(4, static_cast<int>(matches
.size()));
459 EXPECT_EQ(data
[0].url
, matches
[0].node
->url());
460 EXPECT_EQ(data
[3].url
, matches
[1].node
->url());
461 EXPECT_EQ(data
[2].url
, matches
[2].node
->url());
462 EXPECT_EQ(data
[1].url
, matches
[3].node
->url());
465 // Select top two matches.
466 model
->GetBookmarksMatching(ASCIIToUTF16("google"), 2, &matches
);
468 EXPECT_EQ(2, static_cast<int>(matches
.size()));
469 EXPECT_EQ(data
[0].url
, matches
[0].node
->url());
470 EXPECT_EQ(data
[3].url
, matches
[1].node
->url());