bookmark_provider_unittest.cc revision a02191e04bc25c4935f804f2c080ae28663d096d
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/autocomplete/bookmark_provider.h" 6 7#include <algorithm> 8#include <string> 9#include <vector> 10 11#include "base/memory/ref_counted.h" 12#include "base/memory/scoped_ptr.h" 13#include "base/strings/string16.h" 14#include "base/strings/string_number_conversions.h" 15#include "base/strings/utf_string_conversions.h" 16#include "chrome/browser/autocomplete/autocomplete_provider.h" 17#include "chrome/browser/autocomplete/autocomplete_provider_listener.h" 18#include "chrome/browser/bookmarks/bookmark_model.h" 19#include "chrome/browser/bookmarks/bookmark_model_factory.h" 20#include "chrome/browser/bookmarks/bookmark_title_match.h" 21#include "chrome/test/base/testing_profile.h" 22#include "testing/gtest/include/gtest/gtest.h" 23 24// The bookmark corpus against which we will simulate searches. 25struct BookmarksTestInfo { 26 std::string title; 27 std::string url; 28} bookmark_provider_test_data[] = { 29 { "abc def", "http://www.catsanddogs.com/a" }, 30 { "abcde", "http://www.catsanddogs.com/b" }, 31 { "abcdef", "http://www.catsanddogs.com/c" }, 32 { "a definition", "http://www.catsanddogs.com/d" }, 33 { "carry carbon carefully", "http://www.catsanddogs.com/e" }, 34 { "ghi jkl", "http://www.catsanddogs.com/f" }, 35 { "jkl ghi", "http://www.catsanddogs.com/g" }, 36 { "frankly frankly frank", "http://www.catsanddogs.com/h" }, 37 { "foobar foobar", "http://www.foobar.com/" }, 38 // For testing inline_autocompletion. 39 { "http://blah.com/", "http://blah.com/" }, 40 { "http://fiddle.com/", "http://fiddle.com/" }, 41 { "http://www.www.com/", "http://www.www.com/" }, 42 { "chrome://version", "chrome://version" }, 43 { "chrome://omnibox", "chrome://omnibox" }, 44 // For testing ranking with different URLs. 45 {"achlorhydric featherheads resuscitates mockingbirds", 46 "http://www.featherheads.com/a" }, 47 {"achlorhydric mockingbirds resuscitates featherhead", 48 "http://www.featherheads.com/b" }, 49 {"featherhead resuscitates achlorhydric mockingbirds", 50 "http://www.featherheads.com/c" }, 51 {"mockingbirds resuscitates featherheads achlorhydric", 52 "http://www.featherheads.com/d" }, 53 // For testing URL boosting. 54 {"burning worms #1", "http://www.burned.com/" }, 55 {"burning worms #2", "http://www.worms.com/" }, 56 {"worming burns #10", "http://www.burned.com/" }, 57 {"worming burns #20", "http://www.worms.com/" }, 58 {"jive music", "http://www.worms.com/" }, 59}; 60 61class BookmarkProviderTest : public testing::Test, 62 public AutocompleteProviderListener { 63 public: 64 BookmarkProviderTest() : model_(new BookmarkModel(NULL)) {} 65 66 // AutocompleteProviderListener: Not called. 67 virtual void OnProviderUpdate(bool updated_matches) OVERRIDE {} 68 69 protected: 70 virtual void SetUp() OVERRIDE; 71 72 scoped_ptr<TestingProfile> profile_; 73 scoped_ptr<BookmarkModel> model_; 74 scoped_refptr<BookmarkProvider> provider_; 75 76 private: 77 DISALLOW_COPY_AND_ASSIGN(BookmarkProviderTest); 78}; 79 80void BookmarkProviderTest::SetUp() { 81 profile_.reset(new TestingProfile()); 82 DCHECK(profile_.get()); 83 provider_ = new BookmarkProvider(this, profile_.get()); 84 DCHECK(provider_.get()); 85 provider_->set_bookmark_model_for_testing(model_.get()); 86 87 const BookmarkNode* other_node = model_->other_node(); 88 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(bookmark_provider_test_data); ++i) { 89 const BookmarksTestInfo& cur(bookmark_provider_test_data[i]); 90 const GURL url(cur.url); 91 model_->AddURL(other_node, other_node->child_count(), 92 base::ASCIIToUTF16(cur.title), url); 93 } 94} 95 96// Structures and functions supporting the BookmarkProviderTest.Positions 97// unit test. 98 99struct TestBookmarkPosition { 100 TestBookmarkPosition(size_t begin, size_t end) 101 : begin(begin), end(end) {} 102 103 size_t begin; 104 size_t end; 105}; 106typedef std::vector<TestBookmarkPosition> TestBookmarkPositions; 107 108// Return |positions| as a formatted string for unit test diagnostic output. 109std::string TestBookmarkPositionsAsString( 110 const TestBookmarkPositions& positions) { 111 std::string position_string("{"); 112 for (TestBookmarkPositions::const_iterator i = positions.begin(); 113 i != positions.end(); ++i) { 114 if (i != positions.begin()) 115 position_string += ", "; 116 position_string += "{" + base::IntToString(i->begin) + ", " + 117 base::IntToString(i->end) + "}"; 118 } 119 position_string += "}\n"; 120 return position_string; 121} 122 123// Return the positions in |matches| as a formatted string for unit test 124// diagnostic output. 125base::string16 MatchesAsString16(const ACMatches& matches) { 126 base::string16 matches_string; 127 for (ACMatches::const_iterator i = matches.begin(); i != matches.end(); ++i) { 128 matches_string.append(base::ASCIIToUTF16(" '")); 129 matches_string.append(i->description); 130 matches_string.append(base::ASCIIToUTF16("'\n")); 131 } 132 return matches_string; 133} 134 135// Comparison function for sorting search terms by descending length. 136bool TestBookmarkPositionsEqual(const TestBookmarkPosition& pos_a, 137 const TestBookmarkPosition& pos_b) { 138 return pos_a.begin == pos_b.begin && pos_a.end == pos_b.end; 139} 140 141// Convience function to make comparing ACMatchClassifications against the 142// test expectations structure easier. 143TestBookmarkPositions PositionsFromAutocompleteMatch( 144 const AutocompleteMatch& match) { 145 TestBookmarkPositions positions; 146 bool started = false; 147 size_t start = 0; 148 for (AutocompleteMatch::ACMatchClassifications::const_iterator 149 i = match.description_class.begin(); 150 i != match.description_class.end(); ++i) { 151 if (i->style & AutocompleteMatch::ACMatchClassification::MATCH) { 152 // We have found the start of a match. 153 EXPECT_FALSE(started); 154 started = true; 155 start = i->offset; 156 } else if (started) { 157 // We have found the end of a match. 158 started = false; 159 positions.push_back(TestBookmarkPosition(start, i->offset)); 160 start = 0; 161 } 162 } 163 // Record the final position if the last match goes to the end of the 164 // candidate string. 165 if (started) 166 positions.push_back(TestBookmarkPosition(start, match.description.size())); 167 return positions; 168} 169 170// Convience function to make comparing test expectations structure against the 171// actual ACMatchClassifications easier. 172TestBookmarkPositions PositionsFromExpectations( 173 const size_t expectations[9][2]) { 174 TestBookmarkPositions positions; 175 size_t i = 0; 176 // The array is zero-terminated in the [1]th element. 177 while (expectations[i][1]) { 178 positions.push_back( 179 TestBookmarkPosition(expectations[i][0], expectations[i][1])); 180 ++i; 181 } 182 return positions; 183} 184 185TEST_F(BookmarkProviderTest, Positions) { 186 // Simulate searches. 187 // Description of |positions|: 188 // The first index represents the collection of positions for each expected 189 // match. The count of the actual subarrays in each instance of |query_data| 190 // must equal |match_count|. The second index represents each expected 191 // match position. The third index represents the |start| and |end| of the 192 // expected match's position within the |test_data|. This array must be 193 // terminated by an entry with a value of '0' for |end|. 194 // Example: 195 // Consider the line for 'def' below: 196 // {"def", 2, {{{4, 7}, {XXX, 0}}, {{2, 5}, {11, 14}, {XXX, 0}}}}, 197 // There are two expected matches: 198 // 0. {{4, 7}, {XXX, 0}} 199 // 1. {{2, 5}, {11 ,14}, {XXX, 0}} 200 // For the first match, [0], there is one match within the bookmark's title 201 // expected, {4, 7}, which maps to the 'def' within "abc def". The 'XXX' 202 // value is ignored. The second match, [1], indicates that two matches are 203 // expected within the bookmark title "a definite definition". In each case, 204 // the {XXX, 0} indicates the end of the subarray. Or: 205 // Match #1 Match #2 206 // ------------------ ---------------------------- 207 // Pos1 Term Pos1 Pos2 Term 208 // ------ -------- ------ -------- -------- 209 // {"def", 2, {{{4, 7}, {999, 0}}, {{2, 5}, {11, 14}, {999, 0}}}}, 210 // 211 struct QueryData { 212 const std::string query; 213 const size_t match_count; // This count must match the number of major 214 // elements in the following |positions| array. 215 const size_t positions[99][9][2]; 216 } query_data[] = { 217 // This first set is primarily for position detection validation. 218 {"abc", 3, {{{0, 3}, {0, 0}}, 219 {{0, 3}, {0, 0}}, 220 {{0, 3}, {0, 0}}}}, 221 {"abcde", 2, {{{0, 5}, {0, 0}}, 222 {{0, 5}, {0, 0}}}}, 223 {"foo bar", 0, {{{0, 0}}}}, 224 {"fooey bark", 0, {{{0, 0}}}}, 225 {"def", 2, {{{2, 5}, {0, 0}}, 226 {{4, 7}, {0, 0}}}}, 227 {"ghi jkl", 2, {{{0, 3}, {4, 7}, {0, 0}}, 228 {{0, 3}, {4, 7}, {0, 0}}}}, 229 // NB: GetBookmarksWithTitlesMatching(...) uses exact match for "a". 230 {"a", 1, {{{0, 1}, {0, 0}}}}, 231 {"a d", 0, {{{0, 0}}}}, 232 {"carry carbon", 1, {{{0, 5}, {6, 12}, {0, 0}}}}, 233 // NB: GetBookmarksWithTitlesMatching(...) sorts the match positions. 234 {"carbon carry", 1, {{{0, 5}, {6, 12}, {0, 0}}}}, 235 {"arbon", 0, {{{0, 0}}}}, 236 {"ar", 0, {{{0, 0}}}}, 237 {"arry", 0, {{{0, 0}}}}, 238 // Quoted terms are single terms. 239 {"\"carry carbon\"", 1, {{{0, 12}, {0, 0}}}}, 240 {"\"carry carbon\" care", 1, {{{0, 12}, {13, 17}, {0, 0}}}}, 241 // Quoted terms require complete word matches. 242 {"\"carry carbo\"", 0, {{{0, 0}}}}, 243 // This set uses duplicated and/or overlaps search terms in the title. 244 {"frank", 1, {{{0, 5}, {8, 13}, {16, 21}, {0, 0}}}}, 245 {"frankly", 1, {{{0, 7}, {8, 15}, {0, 0}}}}, 246 {"frankly frankly", 1, {{{0, 7}, {8, 15}, {0, 0}}}}, 247 {"foobar foo", 1, {{{0, 6}, {7, 13}, {0, 0}}}}, 248 {"foo foobar", 1, {{{0, 6}, {7, 13}, {0, 0}}}}, 249 }; 250 251 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(query_data); ++i) { 252 AutocompleteInput input(base::ASCIIToUTF16(query_data[i].query), 253 base::string16::npos, base::string16(), GURL(), 254 AutocompleteInput::INVALID_SPEC, false, false, 255 false, AutocompleteInput::ALL_MATCHES); 256 provider_->Start(input, false); 257 const ACMatches& matches(provider_->matches()); 258 // Validate number of results is as expected. 259 EXPECT_LE(matches.size(), query_data[i].match_count) 260 << "One or more of the following matches were unexpected:\n" 261 << MatchesAsString16(matches) 262 << "For query '" << query_data[i].query << "'."; 263 EXPECT_GE(matches.size(), query_data[i].match_count) 264 << "One or more expected matches are missing. Matches found:\n" 265 << MatchesAsString16(matches) 266 << "for query '" << query_data[i].query << "'."; 267 // Validate positions within each match is as expected. 268 for (size_t j = 0; j < matches.size(); ++j) { 269 // Collect the expected positions as a vector, collect the match's 270 // classifications for match positions as a vector, then compare. 271 TestBookmarkPositions expected_positions( 272 PositionsFromExpectations(query_data[i].positions[j])); 273 TestBookmarkPositions actual_positions( 274 PositionsFromAutocompleteMatch(matches[j])); 275 EXPECT_TRUE(std::equal(expected_positions.begin(), 276 expected_positions.end(), 277 actual_positions.begin(), 278 TestBookmarkPositionsEqual)) 279 << "EXPECTED: " << TestBookmarkPositionsAsString(expected_positions) 280 << "ACTUAL: " << TestBookmarkPositionsAsString(actual_positions) 281 << " for query: '" << query_data[i].query << "'."; 282 } 283 } 284} 285 286TEST_F(BookmarkProviderTest, Rankings) { 287 // Simulate searches. 288 struct QueryData { 289 const std::string query; 290 // |match_count| must match the number of elements in the following 291 // |matches| array. 292 const size_t match_count; 293 // |matches| specifies the titles for all bookmarks expected to be matched 294 // by the |query| 295 const std::string matches[99]; 296 } query_data[] = { 297 // Basic ranking test. 298 {"abc", 3, {"abcde", // Most complete match. 299 "abcdef", 300 "abc def"}}, // Least complete match. 301 {"ghi", 2, {"ghi jkl", // Matched earlier. 302 "jkl ghi"}}, // Matched later. 303 // Rankings of exact-word matches with different URLs. 304 {"achlorhydric", 305 3, {"achlorhydric mockingbirds resuscitates featherhead", 306 "achlorhydric featherheads resuscitates mockingbirds", 307 "featherhead resuscitates achlorhydric mockingbirds"}}, 308 {"achlorhydric featherheads", 309 2, {"achlorhydric featherheads resuscitates mockingbirds", 310 "mockingbirds resuscitates featherheads achlorhydric"}}, 311 {"mockingbirds resuscitates", 312 3, {"mockingbirds resuscitates featherheads achlorhydric", 313 "achlorhydric mockingbirds resuscitates featherhead", 314 "featherhead resuscitates achlorhydric mockingbirds"}}, 315 // Ranking of exact-word matches with URL boost. 316 {"worms", 2, {"burning worms #2", // boosted 317 "burning worms #1"}}, // not boosted 318 // Ranking of prefix matches with URL boost. Note that a query of 319 // "worm burn" will have the same results. 320 {"burn worm", 3, {"burning worms #2", // boosted 321 "worming burns #20", // boosted 322 "burning worms #1"}}, // not boosted but shorter 323 }; 324 325 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(query_data); ++i) { 326 AutocompleteInput input(base::ASCIIToUTF16(query_data[i].query), 327 base::string16::npos, base::string16(), GURL(), 328 AutocompleteInput::INVALID_SPEC, false, false, 329 false, AutocompleteInput::ALL_MATCHES); 330 provider_->Start(input, false); 331 const ACMatches& matches(provider_->matches()); 332 // Validate number and content of results is as expected. 333 for (size_t j = 0; j < std::max(query_data[i].match_count, matches.size()); 334 ++j) { 335 EXPECT_LT(j, query_data[i].match_count) << " Unexpected match '" 336 << base::UTF16ToUTF8(matches[j].description) << "' for query: '" 337 << query_data[i].query << "'."; 338 if (j >= query_data[i].match_count) 339 continue; 340 EXPECT_LT(j, matches.size()) << " Missing match '" 341 << query_data[i].matches[j] << "' for query: '" 342 << query_data[i].query << "'."; 343 if (j >= matches.size()) 344 continue; 345 EXPECT_EQ(query_data[i].matches[j], 346 base::UTF16ToUTF8(matches[j].description)) 347 << " Mismatch at [" << base::IntToString(j) << "] for query '" 348 << query_data[i].query << "'."; 349 } 350 } 351} 352 353TEST_F(BookmarkProviderTest, InlineAutocompletion) { 354 // Simulate searches. 355 struct QueryData { 356 const std::string query; 357 const std::string url; 358 const bool allowed_to_be_default_match; 359 const std::string inline_autocompletion; 360 } query_data[] = { 361 { "bla", "http://blah.com/", true, "h.com" }, 362 { "blah ", "http://blah.com/", false, ".com" }, 363 { "http://bl", "http://blah.com/", true, "ah.com" }, 364 { "fiddle.c", "http://fiddle.com/", true, "om" }, 365 { "www", "http://www.www.com/", true, ".com" }, 366 { "chro", "chrome://version", true, "me://version" }, 367 { "chrome://ve", "chrome://version", true, "rsion" }, 368 { "chrome ver", "chrome://version", false, "" }, 369 { "versi", "chrome://version", false, "" }, 370 { "abou", "chrome://omnibox", false, "" }, 371 { "about:om", "chrome://omnibox", true, "nibox" } 372 // Note: when adding a new URL to this test, be sure to add it to the list 373 // of bookmarks at the top of the file as well. All items in this list 374 // need to be in the bookmarks list because BookmarkProvider's 375 // TitleMatchToACMatch() has an assertion that verifies the URL is 376 // actually bookmarked. 377 }; 378 379 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(query_data); ++i) { 380 const std::string description = "for query=" + query_data[i].query + 381 " and url=" + query_data[i].url; 382 AutocompleteInput input(base::ASCIIToUTF16(query_data[i].query), 383 base::string16::npos, base::string16(), GURL(), 384 AutocompleteInput::INVALID_SPEC, false, false, 385 false, AutocompleteInput::ALL_MATCHES); 386 AutocompleteInput fixed_up_input(input); 387 provider_->FixupUserInput(&fixed_up_input); 388 BookmarkNode node(GURL(query_data[i].url)); 389 node.SetTitle(base::ASCIIToUTF16(query_data[i].url)); 390 BookmarkTitleMatch bookmark_match; 391 bookmark_match.node = &node; 392 const AutocompleteMatch& ac_match = 393 provider_->TitleMatchToACMatch(input, fixed_up_input, bookmark_match); 394 EXPECT_EQ(query_data[i].allowed_to_be_default_match, 395 ac_match.allowed_to_be_default_match) << description; 396 EXPECT_EQ(base::ASCIIToUTF16(query_data[i].inline_autocompletion), 397 ac_match.inline_autocompletion) << description; 398 } 399} 400