// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <string> #include <vector> #include "base/message_loop.h" #include "base/string_number_conversions.h" #include "base/string_split.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/browser/bookmarks/bookmark_index.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/history/history_database.h" #include "chrome/browser/history/in_memory_database.h" #include "chrome/browser/history/query_parser.h" #include "chrome/test/testing_browser_process_test.h" #include "chrome/test/testing_profile.h" #include "content/browser/browser_thread.h" #include "testing/gtest/include/gtest/gtest.h" class BookmarkIndexTest : public TestingBrowserProcessTest { public: BookmarkIndexTest() : model_(new BookmarkModel(NULL)) {} void AddBookmarksWithTitles(const char** titles, size_t count) { std::vector<std::string> title_vector; for (size_t i = 0; i < count; ++i) title_vector.push_back(titles[i]); AddBookmarksWithTitles(title_vector); } void AddBookmarksWithTitles(const std::vector<std::string>& titles) { GURL url("about:blank"); for (size_t i = 0; i < titles.size(); ++i) model_->AddURL(model_->other_node(), static_cast<int>(i), ASCIIToUTF16(titles[i]), url); } void ExpectMatches(const std::string& query, const char** expected_titles, size_t expected_count) { std::vector<std::string> title_vector; for (size_t i = 0; i < expected_count; ++i) title_vector.push_back(expected_titles[i]); ExpectMatches(query, title_vector); } void ExpectMatches(const std::string& query, const std::vector<std::string> expected_titles) { std::vector<bookmark_utils::TitleMatch> matches; model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16(query), 1000, &matches); ASSERT_EQ(expected_titles.size(), matches.size()); for (size_t i = 0; i < expected_titles.size(); ++i) { bool found = false; for (size_t j = 0; j < matches.size(); ++j) { if (ASCIIToUTF16(expected_titles[i]) == matches[j].node->GetTitle()) { matches.erase(matches.begin() + j); found = true; break; } } ASSERT_TRUE(found); } } void ExtractMatchPositions(const std::string& string, Snippet::MatchPositions* matches) { std::vector<std::string> match_strings; base::SplitString(string, ':', &match_strings); for (size_t i = 0; i < match_strings.size(); ++i) { std::vector<std::string> chunks; base::SplitString(match_strings[i], ',', &chunks); ASSERT_EQ(2U, chunks.size()); matches->push_back(Snippet::MatchPosition()); int chunks0, chunks1; base::StringToInt(chunks[0], &chunks0); base::StringToInt(chunks[1], &chunks1); matches->back().first = chunks0; matches->back().second = chunks1; } } void ExpectMatchPositions(const std::string& query, const Snippet::MatchPositions& expected_positions) { std::vector<bookmark_utils::TitleMatch> matches; model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16(query), 1000, &matches); ASSERT_EQ(1U, matches.size()); const bookmark_utils::TitleMatch& match = matches[0]; ASSERT_EQ(expected_positions.size(), match.match_positions.size()); for (size_t i = 0; i < expected_positions.size(); ++i) { EXPECT_EQ(expected_positions[i].first, match.match_positions[i].first); EXPECT_EQ(expected_positions[i].second, match.match_positions[i].second); } } protected: scoped_ptr<BookmarkModel> model_; private: DISALLOW_COPY_AND_ASSIGN(BookmarkIndexTest); }; // Various permutations with differing input, queries and output that exercises // all query paths. TEST_F(BookmarkIndexTest, Tests) { struct TestData { const std::string input; const std::string query; const std::string expected; } data[] = { // Trivial test case of only one term, exact match. { "a;b", "A", "a" }, // Prefix match, one term. { "abcd;abc;b", "abc", "abcd;abc" }, // Prefix match, multiple terms. { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg"}, // Exact and prefix match. { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef"}, // Exact and prefix match. { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab", "ab cde ghi", "ab cdef ghij"}, // Title with term multiple times. { "ab ab", "ab", "ab ab"}, // Make sure quotes don't do a prefix match. { "think", "\"thi\"", ""}, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { std::vector<std::string> titles; base::SplitString(data[i].input, ';', &titles); AddBookmarksWithTitles(titles); std::vector<std::string> expected; if (!data[i].expected.empty()) base::SplitString(data[i].expected, ';', &expected); ExpectMatches(data[i].query, expected); model_.reset(new BookmarkModel(NULL)); } } // Makes sure match positions are updated appropriately. TEST_F(BookmarkIndexTest, MatchPositions) { struct TestData { const std::string title; const std::string query; const std::string expected; } data[] = { // Trivial test case of only one term, exact match. { "a", "A", "0,1" }, { "foo bar", "bar", "4,7" }, { "fooey bark", "bar foo", "0,3:6,9"}, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { std::vector<std::string> titles; titles.push_back(data[i].title); AddBookmarksWithTitles(titles); Snippet::MatchPositions expected_matches; ExtractMatchPositions(data[i].expected, &expected_matches); ExpectMatchPositions(data[i].query, expected_matches); model_.reset(new BookmarkModel(NULL)); } } // Makes sure index is updated when a node is removed. TEST_F(BookmarkIndexTest, Remove) { const char* input[] = { "a", "b" }; AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); // Remove the node and make sure we don't get back any results. model_->Remove(model_->other_node(), 0); ExpectMatches("A", NULL, 0U); } // Makes sure index is updated when a node's title is changed. TEST_F(BookmarkIndexTest, ChangeTitle) { const char* input[] = { "a", "b" }; AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); // Remove the node and make sure we don't get back any results. const char* expected[] = { "blah" }; model_->SetTitle(model_->other_node()->GetChild(0), ASCIIToUTF16("blah")); ExpectMatches("BlAh", expected, ARRAYSIZE_UNSAFE(expected)); } // Makes sure no more than max queries is returned. TEST_F(BookmarkIndexTest, HonorMax) { const char* input[] = { "abcd", "abcde" }; AddBookmarksWithTitles(input, ARRAYSIZE_UNSAFE(input)); std::vector<bookmark_utils::TitleMatch> matches; model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16("ABc"), 1, &matches); EXPECT_EQ(1U, matches.size()); } // Makes sure if the lower case string of a bookmark title is more characters // than the upper case string no match positions are returned. TEST_F(BookmarkIndexTest, EmptyMatchOnMultiwideLowercaseString) { const BookmarkNode* n1 = model_->AddURL(model_->other_node(), 0, WideToUTF16(L"\u0130 i"), GURL("http://www.google.com")); std::vector<bookmark_utils::TitleMatch> matches; model_->GetBookmarksWithTitlesMatching(ASCIIToUTF16("i"), 100, &matches); ASSERT_EQ(1U, matches.size()); EXPECT_TRUE(matches[0].node == n1); EXPECT_TRUE(matches[0].match_positions.empty()); } TEST_F(BookmarkIndexTest, GetResultsSortedByTypedCount) { // This ensures MessageLoop::current() will exist, which is needed by // TestingProfile::BlockUntilHistoryProcessesPendingRequests(). MessageLoop loop(MessageLoop::TYPE_DEFAULT); BrowserThread ui_thread(BrowserThread::UI, &loop); BrowserThread file_thread(BrowserThread::FILE, &loop); TestingProfile profile; profile.CreateHistoryService(true, false); profile.BlockUntilHistoryProcessesPendingRequests(); profile.CreateBookmarkModel(true); profile.BlockUntilBookmarkModelLoaded(); BookmarkModel* model = profile.GetBookmarkModel(); HistoryService* const history_service = profile.GetHistoryService(Profile::EXPLICIT_ACCESS); history::URLDatabase* url_db = history_service->InMemoryDatabase(); struct TestData { const GURL url; const char* title; const int typed_count; } data[] = { { GURL("http://www.google.com/"), "Google", 100 }, { GURL("http://maps.google.com/"), "Google Maps", 40 }, { GURL("http://docs.google.com/"), "Google Docs", 50 }, { GURL("http://reader.google.com/"), "Google Reader", 80 }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(data); ++i) { history::URLRow info(data[i].url); info.set_title(UTF8ToUTF16(data[i].title)); info.set_typed_count(data[i].typed_count); // Populate the InMemoryDatabase.... url_db->AddURL(info); // Populate the BookmarkIndex. model->AddURL(model->other_node(), i, UTF8ToUTF16(data[i].title), data[i].url); } // Check that the InMemoryDatabase stored the URLs properly. history::URLRow result1; url_db->GetRowForURL(data[0].url, &result1); EXPECT_EQ(data[0].title, UTF16ToUTF8(result1.title())); history::URLRow result2; url_db->GetRowForURL(data[1].url, &result2); EXPECT_EQ(data[1].title, UTF16ToUTF8(result2.title())); history::URLRow result3; url_db->GetRowForURL(data[2].url, &result3); EXPECT_EQ(data[2].title, UTF16ToUTF8(result3.title())); history::URLRow result4; url_db->GetRowForURL(data[3].url, &result4); EXPECT_EQ(data[3].title, UTF16ToUTF8(result4.title())); // Populate match nodes. std::vector<bookmark_utils::TitleMatch> matches; model->GetBookmarksWithTitlesMatching(ASCIIToUTF16("google"), 4, &matches); // The resulting order should be: // 1. Google (google.com) 100 // 2. Google Reader (google.com/reader) 80 // 3. Google Docs (docs.google.com) 50 // 4. Google Maps (maps.google.com) 40 EXPECT_EQ(4, static_cast<int>(matches.size())); EXPECT_EQ(data[0].url, matches[0].node->GetURL()); EXPECT_EQ(data[3].url, matches[1].node->GetURL()); EXPECT_EQ(data[2].url, matches[2].node->GetURL()); EXPECT_EQ(data[1].url, matches[3].node->GetURL()); matches.clear(); // Select top two matches. model->GetBookmarksWithTitlesMatching(ASCIIToUTF16("google"), 2, &matches); EXPECT_EQ(2, static_cast<int>(matches.size())); EXPECT_EQ(data[0].url, matches[0].node->GetURL()); EXPECT_EQ(data[3].url, matches[1].node->GetURL()); }