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. 4 5 #include "chrome/browser/bookmarks/bookmark_index.h" 6 7 #include <string> 8 #include <vector> 9 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.h" 16 #include "chrome/browser/bookmarks/bookmark_model_factory.h" 17 #include "chrome/browser/bookmarks/bookmark_title_match.h" 18 #include "chrome/browser/history/history_service.h" 19 #include "chrome/browser/history/history_service_factory.h" 20 #include "chrome/browser/history/url_database.h" 21 #include "chrome/test/base/testing_profile.h" 22 #include "chrome/test/base/ui_test_utils.h" 23 #include "content/public/test/test_browser_thread_bundle.h" 24 #include "testing/gtest/include/gtest/gtest.h" 25 26 class BookmarkIndexTest : public testing::Test { 27 public: 28 BookmarkIndexTest() : model_(new BookmarkModel(NULL)) {} 29 30 void AddBookmarksWithTitles(const char** titles, size_t count) { 31 std::vector<std::string> title_vector; 32 for (size_t i = 0; i < count; ++i) 33 title_vector.push_back(titles[i]); 34 AddBookmarksWithTitles(title_vector); 35 } 36 37 void AddBookmarksWithTitles(const std::vector<std::string>& titles) { 38 GURL url("about:blank"); 39 for (size_t i = 0; i < titles.size(); ++i) 40 model_->AddURL(model_->other_node(), static_cast<int>(i), 41 ASCIIToUTF16(titles[i]), url); 42 } 43 44 void ExpectMatches(const std::string& query, 45 const char** expected_titles, 46 size_t expected_count) { 47 std::vector<std::string> title_vector; 48 for (size_t i = 0; i < expected_count; ++i) 49 title_vector.push_back(expected_titles[i]); 50 ExpectMatches(query, title_vector); 51 } 52 53 void ExpectMatches(const std::string& query, 54 const std::vector<std::string>& expected_titles) { 55 std::vector<BookmarkTitleMatch> matches; 56 model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16(query), 1000, &matches); 57 ASSERT_EQ(expected_titles.size(), matches.size()); 58 for (size_t i = 0; i < expected_titles.size(); ++i) { 59 bool found = false; 60 for (size_t j = 0; j < matches.size(); ++j) { 61 if (ASCIIToUTF16(expected_titles[i]) == matches[j].node->GetTitle()) { 62 matches.erase(matches.begin() + j); 63 found = true; 64 break; 65 } 66 } 67 ASSERT_TRUE(found); 68 } 69 } 70 71 void ExtractMatchPositions(const std::string& string, 72 BookmarkTitleMatch::MatchPositions* matches) { 73 std::vector<std::string> match_strings; 74 base::SplitString(string, ':', &match_strings); 75 for (size_t i = 0; i < match_strings.size(); ++i) { 76 std::vector<std::string> chunks; 77 base::SplitString(match_strings[i], ',', &chunks); 78 ASSERT_EQ(2U, chunks.size()); 79 matches->push_back(BookmarkTitleMatch::MatchPosition()); 80 int chunks0, chunks1; 81 base::StringToInt(chunks[0], &chunks0); 82 base::StringToInt(chunks[1], &chunks1); 83 matches->back().first = chunks0; 84 matches->back().second = chunks1; 85 } 86 } 87 88 void ExpectMatchPositions( 89 const std::string& query, 90 const BookmarkTitleMatch::MatchPositions& expected_positions) { 91 std::vector<BookmarkTitleMatch> matches; 92 model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16(query), 1000, &matches); 93 ASSERT_EQ(1U, matches.size()); 94 const BookmarkTitleMatch& match = matches[0]; 95 ASSERT_EQ(expected_positions.size(), match.match_positions.size()); 96 for (size_t i = 0; i < expected_positions.size(); ++i) { 97 EXPECT_EQ(expected_positions[i].first, match.match_positions[i].first); 98 EXPECT_EQ(expected_positions[i].second, match.match_positions[i].second); 99 } 100 } 101 102 protected: 103 scoped_ptr<BookmarkModel> model_; 104 105 private: 106 DISALLOW_COPY_AND_ASSIGN(BookmarkIndexTest); 107 }; 108 109 // Various permutations with differing input, queries and output that exercises 110 // all query paths. 111 TEST_F(BookmarkIndexTest, Tests) { 112 struct TestData { 113 const std::string input; 114 const std::string query; 115 const std::string expected; 116 } data[] = { 117 // Trivial test case of only one term, exact match. 118 { "a;b", "A", "a" }, 119 120 // Prefix match, one term. 121 { "abcd;abc;b", "abc", "abcd;abc" }, 122 123 // Prefix match, multiple terms. 124 { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg"}, 125 126 // Exact and prefix match. 127 { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef"}, 128 129 // Exact and prefix match. 130 { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab", 131 "ab cde ghi", 132 "ab cdef ghij"}, 133 134 // Title with term multiple times. 135 { "ab ab", "ab", "ab ab"}, 136 137 // Make sure quotes don't do a prefix match. 138 { "think", "\"thi\"", ""}, 139 140 // Prefix matches against multiple candidates. 141 { "abc1 abc2 abc3 abc4", "abc", "abc1 abc2 abc3 abc4"}, 142 }; 143 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 144 std::vector<std::string> titles; 145 base::SplitString(data[i].input, ';', &titles); 146 AddBookmarksWithTitles(titles); 147 148 std::vector<std::string> expected; 149 if (!data[i].expected.empty()) 150 base::SplitString(data[i].expected, ';', &expected); 151 152 ExpectMatches(data[i].query, expected); 153 154 model_.reset(new BookmarkModel(NULL)); 155 } 156 } 157 158 // Makes sure match positions are updated appropriately. 159 TEST_F(BookmarkIndexTest, MatchPositions) { 160 struct TestData { 161 const std::string title; 162 const std::string query; 163 const std::string expected; 164 } data[] = { 165 // Trivial test case of only one term, exact match. 166 { "a", "A", "0,1" }, 167 { "foo bar", "bar", "4,7" }, 168 { "fooey bark", "bar foo", "0,3:6,9"}, 169 // Non-trivial tests. 170 { "foobar foo", "foobar foo", "0,6:7,10" }, 171 { "foobar foo", "foo foobar", "0,6:7,10" }, 172 { "foobar foobar", "foobar foo", "0,6:7,13" }, 173 { "foobar foobar", "foo foobar", "0,6:7,13" }, 174 }; 175 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 176 std::vector<std::string> titles; 177 titles.push_back(data[i].title); 178 AddBookmarksWithTitles(titles); 179 180 BookmarkTitleMatch::MatchPositions expected_matches; 181 ExtractMatchPositions(data[i].expected, &expected_matches); 182 ExpectMatchPositions(data[i].query, expected_matches); 183 184 model_.reset(new BookmarkModel(NULL)); 185 } 186 } 187 188 // Makes sure index is updated when a node is removed. 189 TEST_F(BookmarkIndexTest, Remove) { 190 const char* input[] = { "a", "b" }; 191 AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); 192 193 // Remove the node and make sure we don't get back any results. 194 model_->Remove(model_->other_node(), 0); 195 ExpectMatches("A", NULL, 0U); 196 } 197 198 // Makes sure index is updated when a node's title is changed. 199 TEST_F(BookmarkIndexTest, ChangeTitle) { 200 const char* input[] = { "a", "b" }; 201 AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); 202 203 // Remove the node and make sure we don't get back any results. 204 const char* expected[] = { "blah" }; 205 model_->SetTitle(model_->other_node()->GetChild(0), ASCIIToUTF16("blah")); 206 ExpectMatches("BlAh", expected, ARRAYSIZE_UNSAFE(expected)); 207 } 208 209 // Makes sure no more than max queries is returned. 210 TEST_F(BookmarkIndexTest, HonorMax) { 211 const char* input[] = { "abcd", "abcde" }; 212 AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); 213 214 std::vector<BookmarkTitleMatch> matches; 215 model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16("ABc"), 1, &matches); 216 EXPECT_EQ(1U, matches.size()); 217 } 218 219 // Makes sure if the lower case string of a bookmark title is more characters 220 // than the upper case string no match positions are returned. 221 TEST_F(BookmarkIndexTest, EmptyMatchOnMultiwideLowercaseString) { 222 const BookmarkNode* n1 = model_->AddURL(model_->other_node(), 0, 223 WideToUTF16(L"\u0130 i"), 224 GURL("http://www.google.com")); 225 226 std::vector<BookmarkTitleMatch> matches; 227 model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16("i"), 100, &matches); 228 ASSERT_EQ(1U, matches.size()); 229 EXPECT_TRUE(matches[0].node == n1); 230 EXPECT_TRUE(matches[0].match_positions.empty()); 231 } 232 233 TEST_F(BookmarkIndexTest, GetResultsSortedByTypedCount) { 234 // This ensures MessageLoop::current() will exist, which is needed by 235 // TestingProfile::BlockUntilHistoryProcessesPendingRequests(). 236 content::TestBrowserThreadBundle thread_bundle; 237 238 TestingProfile profile; 239 ASSERT_TRUE(profile.CreateHistoryService(true, false)); 240 profile.BlockUntilHistoryProcessesPendingRequests(); 241 profile.CreateBookmarkModel(true); 242 243 BookmarkModel* model = BookmarkModelFactory::GetForProfile(&profile); 244 ui_test_utils::WaitForBookmarkModelToLoad(model); 245 246 HistoryService* const history_service = 247 HistoryServiceFactory::GetForProfile(&profile, Profile::EXPLICIT_ACCESS); 248 249 history::URLDatabase* url_db = history_service->InMemoryDatabase(); 250 251 struct TestData { 252 const GURL url; 253 const char* title; 254 const int typed_count; 255 } data[] = { 256 { GURL("http://www.google.com/"), "Google", 100 }, 257 { GURL("http://maps.google.com/"), "Google Maps", 40 }, 258 { GURL("http://docs.google.com/"), "Google Docs", 50 }, 259 { GURL("http://reader.google.com/"), "Google Reader", 80 }, 260 }; 261 262 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { 263 history::URLRow info(data[i].url); 264 info.set_title(UTF8ToUTF16(data[i].title)); 265 info.set_typed_count(data[i].typed_count); 266 // Populate the InMemoryDatabase.... 267 url_db->AddURL(info); 268 // Populate the BookmarkIndex. 269 model->AddURL(model->other_node(), i, UTF8ToUTF16(data[i].title), 270 data[i].url); 271 } 272 273 // Check that the InMemoryDatabase stored the URLs properly. 274 history::URLRow result1; 275 url_db->GetRowForURL(data[0].url, &result1); 276 EXPECT_EQ(data[0].title, UTF16ToUTF8(result1.title())); 277 278 history::URLRow result2; 279 url_db->GetRowForURL(data[1].url, &result2); 280 EXPECT_EQ(data[1].title, UTF16ToUTF8(result2.title())); 281 282 history::URLRow result3; 283 url_db->GetRowForURL(data[2].url, &result3); 284 EXPECT_EQ(data[2].title, UTF16ToUTF8(result3.title())); 285 286 history::URLRow result4; 287 url_db->GetRowForURL(data[3].url, &result4); 288 EXPECT_EQ(data[3].title, UTF16ToUTF8(result4.title())); 289 290 // Populate match nodes. 291 std::vector<BookmarkTitleMatch> matches; 292 model->GetBookmarksWithTitlesMatching(ASCIIToUTF16("google"), 4, &matches); 293 294 // The resulting order should be: 295 // 1. Google (google.com) 100 296 // 2. Google Reader (google.com/reader) 80 297 // 3. Google Docs (docs.google.com) 50 298 // 4. Google Maps (maps.google.com) 40 299 EXPECT_EQ(4, static_cast<int>(matches.size())); 300 EXPECT_EQ(data[0].url, matches[0].node->url()); 301 EXPECT_EQ(data[3].url, matches[1].node->url()); 302 EXPECT_EQ(data[2].url, matches[2].node->url()); 303 EXPECT_EQ(data[1].url, matches[3].node->url()); 304 305 matches.clear(); 306 // Select top two matches. 307 model->GetBookmarksWithTitlesMatching(ASCIIToUTF16("google"), 2, &matches); 308 309 EXPECT_EQ(2, static_cast<int>(matches.size())); 310 EXPECT_EQ(data[0].url, matches[0].node->url()); 311 EXPECT_EQ(data[3].url, matches[1].node->url()); 312 } 313