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