// Copyright (c) 2011 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 "base/file_util.h" #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_temp_dir.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/browser/history/text_database.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" using base::Time; namespace history { namespace { // Note that all pages have "COUNTTAG" which allows us to count the number of // pages in the database withoujt adding any extra functions to the DB object. const char kURL1[] = "http://www.google.com/"; const int kTime1 = 1000; const char kTitle1[] = "Google"; const char kBody1[] = "COUNTTAG Web Images Maps News Shopping Gmail more My Account | " "Sign out Advanced Search Preferences Language Tools Advertising Programs " "- Business Solutions - About Google, 2008 Google"; const char kURL2[] = "http://images.google.com/"; const int kTime2 = 2000; const char kTitle2[] = "Google Image Search"; const char kBody2[] = "COUNTTAG Web Images Maps News Shopping Gmail more My Account | " "Sign out Advanced Image Search Preferences The most comprehensive image " "search on the web. Want to help improve Google Image Search? Try Google " "Image Labeler. Advertising Programs - Business Solutions - About Google " "2008 Google"; const char kURL3[] = "http://slashdot.org/"; const int kTime3 = 3000; const char kTitle3[] = "Slashdot: News for nerds, stuff that matters"; const char kBody3[] = "COUNTTAG Slashdot Log In Create Account Subscribe Firehose Why " "Log In? Why Subscribe? Nickname Password Public Terminal Sections " "Main Apple AskSlashdot Backslash Books Developers Games Hardware " "Interviews IT Linux Mobile Politics Science YRO"; // Returns the number of rows currently in the database. int RowCount(TextDatabase* db) { QueryOptions options; options.begin_time = Time::FromInternalValue(0); // Leave end_time at now. std::vector<TextDatabase::Match> results; Time first_time_searched; TextDatabase::URLSet unique_urls; db->GetTextMatches("COUNTTAG", options, &results, &unique_urls, &first_time_searched); return static_cast<int>(results.size()); } // Adds each of the test pages to the database. void AddAllTestData(TextDatabase* db) { EXPECT_TRUE(db->AddPageData( Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1)); EXPECT_TRUE(db->AddPageData( Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2)); EXPECT_TRUE(db->AddPageData( Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3)); EXPECT_EQ(3, RowCount(db)); } bool ResultsHaveURL(const std::vector<TextDatabase::Match>& results, const char* url) { GURL gurl(url); for (size_t i = 0; i < results.size(); i++) { if (results[i].url == gurl) return true; } return false; } } // namespace class TextDatabaseTest : public PlatformTest { public: TextDatabaseTest() {} protected: void SetUp() { PlatformTest::SetUp(); ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } // Create databases with this function, which will ensure that the files are // deleted on shutdown. Only open one database for each file. Returns NULL on // failure. // // Set |delete_file| to delete any existing file. If we are trying to create // the file for the first time, we don't want a previous test left in a // weird state to have left a file that would affect us. TextDatabase* CreateDB(TextDatabase::DBIdent id, bool allow_create, bool delete_file) { TextDatabase* db = new TextDatabase(temp_dir_.path(), id, allow_create); if (delete_file) file_util::Delete(db->file_name(), false); if (!db->Init()) { delete db; return NULL; } return db; } // Directory containing the databases. ScopedTempDir temp_dir_; // Name of the main database file. FilePath file_name_; }; TEST_F(TextDatabaseTest, AttachDetach) { // First database with one page. const int kIdee1 = 200801; scoped_ptr<TextDatabase> db1(CreateDB(kIdee1, true, true)); ASSERT_TRUE(!!db1.get()); EXPECT_TRUE(db1->AddPageData( Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1)); // Second database with one page. const int kIdee2 = 200802; scoped_ptr<TextDatabase> db2(CreateDB(kIdee2, true, true)); ASSERT_TRUE(!!db2.get()); EXPECT_TRUE(db2->AddPageData( Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2)); // Detach, then reattach database one. The file should exist, so we force // opening an existing file. db1.reset(); db1.reset(CreateDB(kIdee1, false, false)); ASSERT_TRUE(!!db1.get()); // We should not be able to attach this random database for which no file // exists. const int kIdeeNoExisto = 999999999; scoped_ptr<TextDatabase> db3(CreateDB(kIdeeNoExisto, false, true)); EXPECT_FALSE(!!db3.get()); } TEST_F(TextDatabaseTest, AddRemove) { // Create a database and add some pages to it. const int kIdee1 = 200801; scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true)); ASSERT_TRUE(!!db.get()); URLID id1 = db->AddPageData( Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1); EXPECT_NE(0, id1); URLID id2 = db->AddPageData( Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2); EXPECT_NE(0, id2); URLID id3 = db->AddPageData( Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3); EXPECT_NE(0, id3); EXPECT_EQ(3, RowCount(db.get())); // Make sure we can delete some of the data. db->DeletePageData(Time::FromInternalValue(kTime1), kURL1); EXPECT_EQ(2, RowCount(db.get())); // Close and reopen. db.reset(new TextDatabase(temp_dir_.path(), kIdee1, false)); EXPECT_TRUE(db->Init()); // Verify that the deleted ID is gone and try to delete another one. EXPECT_EQ(2, RowCount(db.get())); db->DeletePageData(Time::FromInternalValue(kTime2), kURL2); EXPECT_EQ(1, RowCount(db.get())); } TEST_F(TextDatabaseTest, Query) { // Make a database with some pages. const int kIdee1 = 200801; scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true)); EXPECT_TRUE(!!db.get()); AddAllTestData(db.get()); // Get all the results. QueryOptions options; options.begin_time = Time::FromInternalValue(0); std::vector<TextDatabase::Match> results; Time first_time_searched; TextDatabase::URLSet unique_urls; db->GetTextMatches("COUNTTAG", options, &results, &unique_urls, &first_time_searched); EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs"; // All 3 sites should be returned in order. ASSERT_EQ(3U, results.size()); EXPECT_EQ(GURL(kURL1), results[2].url); EXPECT_EQ(GURL(kURL2), results[1].url); EXPECT_EQ(GURL(kURL3), results[0].url); // Verify the info on those results. EXPECT_TRUE(Time::FromInternalValue(kTime1) == results[2].time); EXPECT_TRUE(Time::FromInternalValue(kTime2) == results[1].time); EXPECT_TRUE(Time::FromInternalValue(kTime3) == results[0].time); EXPECT_EQ(std::string(kTitle1), UTF16ToUTF8(results[2].title)); EXPECT_EQ(std::string(kTitle2), UTF16ToUTF8(results[1].title)); EXPECT_EQ(std::string(kTitle3), UTF16ToUTF8(results[0].title)); // Should have no matches in the title. EXPECT_EQ(0U, results[0].title_match_positions.size()); EXPECT_EQ(0U, results[1].title_match_positions.size()); EXPECT_EQ(0U, results[2].title_match_positions.size()); // We don't want to be dependent on the exact snippet algorithm, but we know // since we searched for "COUNTTAG" which occurs at the beginning of each // document, that each snippet should start with that. EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[0].snippet.text()), "COUNTTAG", false)); EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[1].snippet.text()), "COUNTTAG", false)); EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[2].snippet.text()), "COUNTTAG", false)); } TEST_F(TextDatabaseTest, TimeRange) { // Make a database with some pages. const int kIdee1 = 200801; scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true)); ASSERT_TRUE(!!db.get()); AddAllTestData(db.get()); // Beginning should be inclusive, and the ending exclusive. // Get all the results. QueryOptions options; options.begin_time = Time::FromInternalValue(kTime1); options.end_time = Time::FromInternalValue(kTime3); std::vector<TextDatabase::Match> results; Time first_time_searched; TextDatabase::URLSet unique_urls; db->GetTextMatches("COUNTTAG", options, &results, &unique_urls, &first_time_searched); EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs"; // The first and second should have been returned. EXPECT_EQ(2U, results.size()); EXPECT_TRUE(ResultsHaveURL(results, kURL1)); EXPECT_TRUE(ResultsHaveURL(results, kURL2)); EXPECT_FALSE(ResultsHaveURL(results, kURL3)); EXPECT_EQ(kTime1, first_time_searched.ToInternalValue()); // --------------------------------------------------------------------------- // Do a query where there isn't a result on the begin boundary, so we can // test that the first time searched is set to the minimum time considered // instead of the min value. options.begin_time = Time::FromInternalValue((kTime2 - kTime1) / 2 + kTime1); options.end_time = Time::FromInternalValue(kTime3 + 1); results.clear(); // GetTextMatches does *not* clear the results. db->GetTextMatches("COUNTTAG", options, &results, &unique_urls, &first_time_searched); EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs"; EXPECT_EQ(options.begin_time.ToInternalValue(), first_time_searched.ToInternalValue()); // Should have two results, the second and third. EXPECT_EQ(2U, results.size()); EXPECT_FALSE(ResultsHaveURL(results, kURL1)); EXPECT_TRUE(ResultsHaveURL(results, kURL2)); EXPECT_TRUE(ResultsHaveURL(results, kURL3)); // No results should also set the first_time_searched. options.begin_time = Time::FromInternalValue(kTime3 + 1); options.end_time = Time::FromInternalValue(kTime3 * 100); results.clear(); db->GetTextMatches("COUNTTAG", options, &results, &unique_urls, &first_time_searched); EXPECT_EQ(options.begin_time.ToInternalValue(), first_time_searched.ToInternalValue()); } // Make sure that max_count works. TEST_F(TextDatabaseTest, MaxCount) { // Make a database with some pages. const int kIdee1 = 200801; scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true)); ASSERT_TRUE(!!db.get()); AddAllTestData(db.get()); // Set up the query to return all the results with "Google" (should be 2), but // with a maximum of 1. QueryOptions options; options.begin_time = Time::FromInternalValue(kTime1); options.end_time = Time::FromInternalValue(kTime3 + 1); options.max_count = 1; std::vector<TextDatabase::Match> results; Time first_time_searched; TextDatabase::URLSet unique_urls; db->GetTextMatches("google", options, &results, &unique_urls, &first_time_searched); EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs"; // There should be one result, the most recent one. EXPECT_EQ(1U, results.size()); EXPECT_TRUE(ResultsHaveURL(results, kURL2)); // The max time considered should be the date of the returned item. EXPECT_EQ(kTime2, first_time_searched.ToInternalValue()); } } // namespace history