1 // Copyright 2014 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. 4 5 #include "components/bookmarks/browser/bookmark_index.h" 6 7 #include <string> 8 #include <vector> 9 10 #include "base/macros.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 "components/bookmarks/browser/bookmark_match.h" 16 #include "components/bookmarks/browser/bookmark_model.h" 17 #include "components/bookmarks/test/bookmark_test_helpers.h" 18 #include "components/bookmarks/test/test_bookmark_client.h" 19 #include "testing/gtest/include/gtest/gtest.h" 20 21 using base::ASCIIToUTF16; 22 using base::UTF8ToUTF16; 23 24 namespace bookmarks { 25 namespace { 26 27 const char kAboutBlankURL[] = "about:blank"; 28 29 class BookmarkClientMock : public TestBookmarkClient { 30 public: 31 BookmarkClientMock(const std::map<GURL, int>& typed_count_map) 32 : typed_count_map_(typed_count_map) {} 33 34 virtual bool SupportsTypedCountForNodes() OVERRIDE { return true; } 35 36 virtual void GetTypedCountForNodes( 37 const NodeSet& nodes, 38 NodeTypedCountPairs* node_typed_count_pairs) OVERRIDE { 39 for (NodeSet::const_iterator it = nodes.begin(); it != nodes.end(); ++it) { 40 const BookmarkNode* node = *it; 41 std::map<GURL, int>::const_iterator found = 42 typed_count_map_.find(node->url()); 43 if (found == typed_count_map_.end()) 44 continue; 45 46 node_typed_count_pairs->push_back(std::make_pair(node, found->second)); 47 } 48 } 49 50 private: 51 const std::map<GURL, int> typed_count_map_; 52 53 DISALLOW_COPY_AND_ASSIGN(BookmarkClientMock); 54 }; 55 56 class BookmarkIndexTest : public testing::Test { 57 public: 58 BookmarkIndexTest() : model_(client_.CreateModel()) {} 59 60 typedef std::pair<std::string, std::string> TitleAndURL; 61 62 void AddBookmarks(const char** titles, const char** urls, size_t count) { 63 // The pair is (title, url). 64 std::vector<TitleAndURL> bookmarks; 65 for (size_t i = 0; i < count; ++i) { 66 TitleAndURL bookmark(titles[i], urls[i]); 67 bookmarks.push_back(bookmark); 68 } 69 AddBookmarks(bookmarks); 70 } 71 72 void AddBookmarks(const std::vector<TitleAndURL>& bookmarks) { 73 for (size_t i = 0; i < bookmarks.size(); ++i) { 74 model_->AddURL(model_->other_node(), static_cast<int>(i), 75 ASCIIToUTF16(bookmarks[i].first), 76 GURL(bookmarks[i].second)); 77 } 78 } 79 80 void ExpectMatches(const std::string& query, 81 const char** expected_titles, 82 size_t expected_count) { 83 std::vector<std::string> title_vector; 84 for (size_t i = 0; i < expected_count; ++i) 85 title_vector.push_back(expected_titles[i]); 86 ExpectMatches(query, title_vector); 87 } 88 89 void ExpectMatches(const std::string& query, 90 const std::vector<std::string>& expected_titles) { 91 std::vector<BookmarkMatch> matches; 92 model_->GetBookmarksMatching(ASCIIToUTF16(query), 1000, &matches); 93 ASSERT_EQ(expected_titles.size(), matches.size()); 94 for (size_t i = 0; i < expected_titles.size(); ++i) { 95 bool found = false; 96 for (size_t j = 0; j < matches.size(); ++j) { 97 if (ASCIIToUTF16(expected_titles[i]) == matches[j].node->GetTitle()) { 98 matches.erase(matches.begin() + j); 99 found = true; 100 break; 101 } 102 } 103 ASSERT_TRUE(found); 104 } 105 } 106 107 void ExtractMatchPositions(const std::string& string, 108 BookmarkMatch::MatchPositions* matches) { 109 std::vector<std::string> match_strings; 110 base::SplitString(string, ':', &match_strings); 111 for (size_t i = 0; i < match_strings.size(); ++i) { 112 std::vector<std::string> chunks; 113 base::SplitString(match_strings[i], ',', &chunks); 114 ASSERT_EQ(2U, chunks.size()); 115 matches->push_back(BookmarkMatch::MatchPosition()); 116 int chunks0, chunks1; 117 EXPECT_TRUE(base::StringToInt(chunks[0], &chunks0)); 118 EXPECT_TRUE(base::StringToInt(chunks[1], &chunks1)); 119 matches->back().first = chunks0; 120 matches->back().second = chunks1; 121 } 122 } 123 124 void ExpectMatchPositions( 125 const BookmarkMatch::MatchPositions& actual_positions, 126 const BookmarkMatch::MatchPositions& expected_positions) { 127 ASSERT_EQ(expected_positions.size(), actual_positions.size()); 128 for (size_t i = 0; i < expected_positions.size(); ++i) { 129 EXPECT_EQ(expected_positions[i].first, actual_positions[i].first); 130 EXPECT_EQ(expected_positions[i].second, actual_positions[i].second); 131 } 132 } 133 134 protected: 135 TestBookmarkClient client_; 136 scoped_ptr<BookmarkModel> model_; 137 138 private: 139 DISALLOW_COPY_AND_ASSIGN(BookmarkIndexTest); 140 }; 141 142 // Various permutations with differing input, queries and output that exercises 143 // all query paths. 144 TEST_F(BookmarkIndexTest, GetBookmarksMatching) { 145 struct TestData { 146 const std::string titles; 147 const std::string query; 148 const std::string expected; 149 } data[] = { 150 // Trivial test case of only one term, exact match. 151 { "a;b", "A", "a" }, 152 153 // Prefix match, one term. 154 { "abcd;abc;b", "abc", "abcd;abc" }, 155 156 // Prefix match, multiple terms. 157 { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg"}, 158 159 // Exact and prefix match. 160 { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef"}, 161 162 // Exact and prefix match. 163 { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab", 164 "ab cde ghi", 165 "ab cdef ghij"}, 166 167 // Title with term multiple times. 168 { "ab ab", "ab", "ab ab"}, 169 170 // Make sure quotes don't do a prefix match. 171 { "think", "\"thi\"", ""}, 172 173 // Prefix matches against multiple candidates. 174 { "abc1 abc2 abc3 abc4", "abc", "abc1 abc2 abc3 abc4"}, 175 }; 176 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 177 std::vector<std::string> titles; 178 base::SplitString(data[i].titles, ';', &titles); 179 std::vector<TitleAndURL> bookmarks; 180 for (size_t j = 0; j < titles.size(); ++j) { 181 TitleAndURL bookmark(titles[j], kAboutBlankURL); 182 bookmarks.push_back(bookmark); 183 } 184 AddBookmarks(bookmarks); 185 186 std::vector<std::string> expected; 187 if (!data[i].expected.empty()) 188 base::SplitString(data[i].expected, ';', &expected); 189 190 ExpectMatches(data[i].query, expected); 191 192 model_ = client_.CreateModel(); 193 } 194 } 195 196 // Analogous to GetBookmarksMatching, this test tests various permutations 197 // of title, URL, and input to see if the title/URL matches the input as 198 // expected. 199 TEST_F(BookmarkIndexTest, GetBookmarksMatchingWithURLs) { 200 struct TestData { 201 const std::string query; 202 const std::string title; 203 const std::string url; 204 const bool should_be_retrieved; 205 } data[] = { 206 // Test single-word inputs. Include both exact matches and prefix matches. 207 { "foo", "Foo", "http://www.bar.com/", true }, 208 { "foo", "Foodie", "http://www.bar.com/", true }, 209 { "foo", "Bar", "http://www.foo.com/", true }, 210 { "foo", "Bar", "http://www.foodie.com/", true }, 211 { "foo", "Foo", "http://www.foo.com/", true }, 212 { "foo", "Bar", "http://www.bar.com/", false }, 213 { "foo", "Bar", "http://www.bar.com/blah/foo/blah-again/ ", true }, 214 { "foo", "Bar", "http://www.bar.com/blah/foodie/blah-again/ ", true }, 215 { "foo", "Bar", "http://www.bar.com/blah-foo/blah-again/ ", true }, 216 { "foo", "Bar", "http://www.bar.com/blah-foodie/blah-again/ ", true }, 217 { "foo", "Bar", "http://www.bar.com/blahafoo/blah-again/ ", false }, 218 219 // Test multi-word inputs. 220 { "foo bar", "Foo Bar", "http://baz.com/", true }, 221 { "foo bar", "Foodie Bar", "http://baz.com/", true }, 222 { "bar foo", "Foo Bar", "http://baz.com/", true }, 223 { "bar foo", "Foodie Barly", "http://baz.com/", true }, 224 { "foo bar", "Foo Baz", "http://baz.com/", false }, 225 { "foo bar", "Foo Baz", "http://bar.com/", true }, 226 { "foo bar", "Foo Baz", "http://barly.com/", true }, 227 { "foo bar", "Foodie Baz", "http://barly.com/", true }, 228 { "bar foo", "Foo Baz", "http://bar.com/", true }, 229 { "bar foo", "Foo Baz", "http://barly.com/", true }, 230 { "foo bar", "Baz Bar", "http://blah.com/foo", true }, 231 { "foo bar", "Baz Barly", "http://blah.com/foodie", true }, 232 { "foo bar", "Baz Bur", "http://blah.com/foo/bar", true }, 233 { "foo bar", "Baz Bur", "http://blah.com/food/barly", true }, 234 { "foo bar", "Baz Bur", "http://bar.com/blah/foo", true }, 235 { "foo bar", "Baz Bur", "http://barly.com/blah/food", true }, 236 { "foo bar", "Baz Bur", "http://bar.com/blah/flub", false }, 237 { "foo bar", "Baz Bur", "http://foo.com/blah/flub", false } 238 }; 239 240 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 241 model_ = client_.CreateModel(); 242 std::vector<TitleAndURL> bookmarks; 243 bookmarks.push_back(TitleAndURL(data[i].title, data[i].url)); 244 AddBookmarks(bookmarks); 245 246 std::vector<std::string> expected; 247 if (data[i].should_be_retrieved) 248 expected.push_back(data[i].title); 249 250 ExpectMatches(data[i].query, expected); 251 } 252 } 253 254 TEST_F(BookmarkIndexTest, Normalization) { 255 struct TestData { 256 const char* const title; 257 const char* const query; 258 } data[] = { 259 { "fooa\xcc\x88-test", "foo\xc3\xa4-test" }, 260 { "fooa\xcc\x88-test", "fooa\xcc\x88-test" }, 261 { "fooa\xcc\x88-test", "foo\xc3\xa4" }, 262 { "fooa\xcc\x88-test", "fooa\xcc\x88" }, 263 { "fooa\xcc\x88-test", "foo" }, 264 { "foo\xc3\xa4-test", "foo\xc3\xa4-test" }, 265 { "foo\xc3\xa4-test", "fooa\xcc\x88-test" }, 266 { "foo\xc3\xa4-test", "foo\xc3\xa4" }, 267 { "foo\xc3\xa4-test", "fooa\xcc\x88" }, 268 { "foo\xc3\xa4-test", "foo" }, 269 { "foo", "foo" } 270 }; 271 272 GURL url(kAboutBlankURL); 273 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 274 model_->AddURL(model_->other_node(), 0, UTF8ToUTF16(data[i].title), url); 275 std::vector<BookmarkMatch> matches; 276 model_->GetBookmarksMatching(UTF8ToUTF16(data[i].query), 10, &matches); 277 EXPECT_EQ(1u, matches.size()); 278 model_ = client_.CreateModel(); 279 } 280 } 281 282 // Makes sure match positions are updated appropriately for title matches. 283 TEST_F(BookmarkIndexTest, MatchPositionsTitles) { 284 struct TestData { 285 const std::string title; 286 const std::string query; 287 const std::string expected_title_match_positions; 288 } data[] = { 289 // Trivial test case of only one term, exact match. 290 { "a", "A", "0,1" }, 291 { "foo bar", "bar", "4,7" }, 292 { "fooey bark", "bar foo", "0,3:6,9" }, 293 // Non-trivial tests. 294 { "foobar foo", "foobar foo", "0,6:7,10" }, 295 { "foobar foo", "foo foobar", "0,6:7,10" }, 296 { "foobar foobar", "foobar foo", "0,6:7,13" }, 297 { "foobar foobar", "foo foobar", "0,6:7,13" }, 298 }; 299 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 300 std::vector<TitleAndURL> bookmarks; 301 TitleAndURL bookmark(data[i].title, kAboutBlankURL); 302 bookmarks.push_back(bookmark); 303 AddBookmarks(bookmarks); 304 305 std::vector<BookmarkMatch> matches; 306 model_->GetBookmarksMatching(ASCIIToUTF16(data[i].query), 1000, &matches); 307 ASSERT_EQ(1U, matches.size()); 308 309 BookmarkMatch::MatchPositions expected_title_matches; 310 ExtractMatchPositions(data[i].expected_title_match_positions, 311 &expected_title_matches); 312 ExpectMatchPositions(matches[0].title_match_positions, 313 expected_title_matches); 314 315 model_ = client_.CreateModel(); 316 } 317 } 318 319 // Makes sure match positions are updated appropriately for URL matches. 320 TEST_F(BookmarkIndexTest, MatchPositionsURLs) { 321 // The encoded stuff between /wiki/ and the # is 322 const std::string ja_wiki_url = "http://ja.wikipedia.org/wiki/%E7%AC%AC%E4" 323 "%BA%8C%E6%AC%A1%E4%B8%96%E7%95%8C%E5%A4%A7%E6%88%A6#.E3.83.B4.E3.82.A7" 324 ".E3.83.AB.E3.82.B5.E3.82.A4.E3.83.A6.E4.BD.93.E5.88.B6"; 325 struct TestData { 326 const std::string query; 327 const std::string url; 328 const std::string expected_url_match_positions; 329 } data[] = { 330 { "foo", "http://www.foo.com/", "11,14" }, 331 { "foo", "http://www.foodie.com/", "11,14" }, 332 { "foo", "http://www.foofoo.com/", "11,14" }, 333 { "www", "http://www.foo.com/", "7,10" }, 334 { "foo", "http://www.foodie.com/blah/foo/fi", "11,14:27,30" }, 335 { "foo", "http://www.blah.com/blah/foo/fi", "25,28" }, 336 { "foo www", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" }, 337 { "www foo", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" }, 338 { "www bla", "http://www.foodie.com/blah/foo/fi", "7,10:22,25" }, 339 { "http", "http://www.foo.com/", "0,4" }, 340 { "http www", "http://www.foo.com/", "0,4:7,10" }, 341 { "http foo", "http://www.foo.com/", "0,4:11,14" }, 342 { "http foo", "http://www.bar.com/baz/foodie/hi", "0,4:23,26" }, 343 { "", ja_wiki_url, "29,56" }, 344 { "ja ", ja_wiki_url, "7,9:29,56" }, 345 { " E3.8", ja_wiki_url, "29,56:94,98:103,107:" 346 "112,116:121,125:" 347 "130,134:139,143" } 348 }; 349 350 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 351 model_ = client_.CreateModel(); 352 std::vector<TitleAndURL> bookmarks; 353 TitleAndURL bookmark("123456", data[i].url); 354 bookmarks.push_back(bookmark); 355 AddBookmarks(bookmarks); 356 357 std::vector<BookmarkMatch> matches; 358 model_->GetBookmarksMatching(UTF8ToUTF16(data[i].query), 1000, &matches); 359 ASSERT_EQ(1U, matches.size()) << data[i].url << data[i].query; 360 361 BookmarkMatch::MatchPositions expected_url_matches; 362 ExtractMatchPositions(data[i].expected_url_match_positions, 363 &expected_url_matches); 364 ExpectMatchPositions(matches[0].url_match_positions, expected_url_matches); 365 } 366 } 367 368 // Makes sure index is updated when a node is removed. 369 TEST_F(BookmarkIndexTest, Remove) { 370 const char* titles[] = { "a", "b" }; 371 const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; 372 AddBookmarks(titles, urls, ARRAYSIZE_UNSAFE(titles)); 373 374 // Remove the node and make sure we don't get back any results. 375 model_->Remove(model_->other_node(), 0); 376 ExpectMatches("A", NULL, 0U); 377 } 378 379 // Makes sure index is updated when a node's title is changed. 380 TEST_F(BookmarkIndexTest, ChangeTitle) { 381 const char* titles[] = { "a", "b" }; 382 const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; 383 AddBookmarks(titles, urls, ARRAYSIZE_UNSAFE(titles)); 384 385 // Remove the node and make sure we don't get back any results. 386 const char* expected[] = { "blah" }; 387 model_->SetTitle(model_->other_node()->GetChild(0), ASCIIToUTF16("blah")); 388 ExpectMatches("BlAh", expected, ARRAYSIZE_UNSAFE(expected)); 389 } 390 391 // Makes sure no more than max queries is returned. 392 TEST_F(BookmarkIndexTest, HonorMax) { 393 const char* titles[] = { "abcd", "abcde" }; 394 const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; 395 AddBookmarks(titles, urls, ARRAYSIZE_UNSAFE(titles)); 396 397 std::vector<BookmarkMatch> matches; 398 model_->GetBookmarksMatching(ASCIIToUTF16("ABc"), 1, &matches); 399 EXPECT_EQ(1U, matches.size()); 400 } 401 402 // Makes sure if the lower case string of a bookmark title is more characters 403 // than the upper case string no match positions are returned. 404 TEST_F(BookmarkIndexTest, EmptyMatchOnMultiwideLowercaseString) { 405 const BookmarkNode* n1 = model_->AddURL(model_->other_node(), 0, 406 base::WideToUTF16(L"\u0130 i"), 407 GURL("http://www.google.com")); 408 409 std::vector<BookmarkMatch> matches; 410 model_->GetBookmarksMatching(ASCIIToUTF16("i"), 100, &matches); 411 ASSERT_EQ(1U, matches.size()); 412 EXPECT_EQ(n1, matches[0].node); 413 EXPECT_TRUE(matches[0].title_match_positions.empty()); 414 } 415 416 TEST_F(BookmarkIndexTest, GetResultsSortedByTypedCount) { 417 struct TestData { 418 const GURL url; 419 const char* title; 420 const int typed_count; 421 } data[] = { 422 { GURL("http://www.google.com/"), "Google", 100 }, 423 { GURL("http://maps.google.com/"), "Google Maps", 40 }, 424 { GURL("http://docs.google.com/"), "Google Docs", 50 }, 425 { GURL("http://reader.google.com/"), "Google Reader", 80 }, 426 }; 427 428 std::map<GURL, int> typed_count_map; 429 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) 430 typed_count_map.insert(std::make_pair(data[i].url, data[i].typed_count)); 431 432 BookmarkClientMock client(typed_count_map); 433 scoped_ptr<BookmarkModel> model = client.CreateModel(); 434 435 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) 436 // Populate the BookmarkIndex. 437 model->AddURL( 438 model->other_node(), i, UTF8ToUTF16(data[i].title), data[i].url); 439 440 // Populate match nodes. 441 std::vector<BookmarkMatch> matches; 442 model->GetBookmarksMatching(ASCIIToUTF16("google"), 4, &matches); 443 444 // The resulting order should be: 445 // 1. Google (google.com) 100 446 // 2. Google Reader (google.com/reader) 80 447 // 3. Google Docs (docs.google.com) 50 448 // 4. Google Maps (maps.google.com) 40 449 ASSERT_EQ(4, static_cast<int>(matches.size())); 450 EXPECT_EQ(data[0].url, matches[0].node->url()); 451 EXPECT_EQ(data[3].url, matches[1].node->url()); 452 EXPECT_EQ(data[2].url, matches[2].node->url()); 453 EXPECT_EQ(data[1].url, matches[3].node->url()); 454 455 matches.clear(); 456 // Select top two matches. 457 model->GetBookmarksMatching(ASCIIToUTF16("google"), 2, &matches); 458 459 ASSERT_EQ(2, static_cast<int>(matches.size())); 460 EXPECT_EQ(data[0].url, matches[0].node->url()); 461 EXPECT_EQ(data[3].url, matches[1].node->url()); 462 } 463 464 } // namespace 465 } // namespace bookmarks 466