Home | History | Annotate | Download | only in bookmarks
      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