// 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());
}