blob: 53ffcf5fc64280a39acdb2d3ced901184af6a12b [file] [log] [blame]
// Copyright (c) 2012 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.
// History unit tests come in two flavors:
//
// 1. The more complicated style is that the unit test creates a full history
// service. This spawns a background thread for the history backend, and
// all communication is asynchronous. This is useful for testing more
// complicated things or end-to-end behavior.
//
// 2. The simpler style is to create a history backend on this thread and
// access it directly without a HistoryService object. This is much simpler
// because communication is synchronous. Generally, sets should go through
// the history backend (since there is a lot of logic) but gets can come
// directly from the HistoryDatabase. This is because the backend generally
// has no logic in the getter except threading stuff, which we don't want
// to run.
#include <time.h>
#include <algorithm>
#include <string>
#include "base/basictypes.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/scoped_vector.h"
#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "chrome/browser/history/download_row.h"
#include "chrome/browser/history/history_backend.h"
#include "chrome/browser/history/history_database.h"
#include "chrome/browser/history/history_db_task.h"
#include "chrome/browser/history/history_notifications.h"
#include "chrome/browser/history/history_service.h"
#include "chrome/browser/history/history_unittest_base.h"
#include "chrome/browser/history/in_memory_database.h"
#include "chrome/browser/history/in_memory_history_backend.h"
#include "chrome/browser/history/page_usage_data.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/thumbnail_score.h"
#include "chrome/tools/profiles/thumbnail-inl.h"
#include "content/public/browser/download_item.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "sql/connection.h"
#include "sql/statement.h"
#include "sync/api/sync_change.h"
#include "sync/api/sync_change_processor.h"
#include "sync/api/sync_error.h"
#include "sync/api/sync_error_factory.h"
#include "sync/api/sync_merge_result.h"
#include "sync/protocol/history_delete_directive_specifics.pb.h"
#include "sync/protocol/sync.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/jpeg_codec.h"
using base::Time;
using base::TimeDelta;
using content::DownloadItem;
namespace history {
class HistoryBackendDBTest;
// Delegate class for when we create a backend without a HistoryService.
//
// This must be outside the anonymous namespace for the friend statement in
// HistoryBackendDBTest to work.
class BackendDelegate : public HistoryBackend::Delegate {
public:
explicit BackendDelegate(HistoryBackendDBTest* history_test)
: history_test_(history_test) {
}
virtual void NotifyProfileError(int backend_id,
sql::InitStatus init_status) OVERRIDE {}
virtual void SetInMemoryBackend(int backend_id,
InMemoryHistoryBackend* backend) OVERRIDE;
virtual void BroadcastNotifications(int type,
HistoryDetails* details) OVERRIDE;
virtual void DBLoaded(int backend_id) OVERRIDE {}
virtual void NotifyVisitDBObserversOnAddVisit(
const BriefVisitInfo& info) OVERRIDE {}
private:
HistoryBackendDBTest* history_test_;
};
// This must be outside the anonymous namespace for the friend statement in
// HistoryBackend to work.
class HistoryBackendDBTest : public HistoryUnitTestBase {
public:
HistoryBackendDBTest() : db_(NULL) {
}
virtual ~HistoryBackendDBTest() {
}
protected:
friend class BackendDelegate;
// Creates the HistoryBackend and HistoryDatabase on the current thread,
// assigning the values to backend_ and db_.
void CreateBackendAndDatabase() {
backend_ = new HistoryBackend(history_dir_, 0, new BackendDelegate(this),
NULL);
backend_->Init(std::string(), false);
db_ = backend_->db_.get();
DCHECK(in_mem_backend_) << "Mem backend should have been set by "
"HistoryBackend::Init";
}
void CreateDBVersion(int version) {
base::FilePath data_path;
ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &data_path));
data_path = data_path.AppendASCII("History");
data_path =
data_path.AppendASCII(base::StringPrintf("history.%d.sql", version));
ASSERT_NO_FATAL_FAILURE(
ExecuteSQLScript(data_path, history_dir_.Append(
chrome::kHistoryFilename)));
}
// testing::Test
virtual void SetUp() {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
history_dir_ = temp_dir_.path().AppendASCII("HistoryBackendDBTest");
ASSERT_TRUE(file_util::CreateDirectory(history_dir_));
}
void DeleteBackend() {
if (backend_.get()) {
backend_->Closing();
backend_ = NULL;
}
}
virtual void TearDown() {
DeleteBackend();
// Make sure we don't have any event pending that could disrupt the next
// test.
base::MessageLoop::current()->PostTask(FROM_HERE,
base::MessageLoop::QuitClosure());
base::MessageLoop::current()->Run();
}
bool AddDownload(uint32 id,
DownloadItem::DownloadState state,
const Time& time) {
std::vector<GURL> url_chain;
url_chain.push_back(GURL("foo-url"));
DownloadRow download(base::FilePath(FILE_PATH_LITERAL("current-path")),
base::FilePath(FILE_PATH_LITERAL("target-path")),
url_chain,
GURL("http://referrer.com/"),
time,
time,
std::string(),
std::string(),
0,
512,
state,
content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
content::DOWNLOAD_INTERRUPT_REASON_NONE,
id,
false,
"by_ext_id",
"by_ext_name");
return db_->CreateDownload(download);
}
base::ScopedTempDir temp_dir_;
base::MessageLoopForUI message_loop_;
// names of the database files
base::FilePath history_dir_;
// Created via CreateBackendAndDatabase.
scoped_refptr<HistoryBackend> backend_;
scoped_ptr<InMemoryHistoryBackend> in_mem_backend_;
HistoryDatabase* db_; // Cached reference to the backend's database.
};
void BackendDelegate::SetInMemoryBackend(int backend_id,
InMemoryHistoryBackend* backend) {
// Save the in-memory backend to the history test object, this happens
// synchronously, so we don't have to do anything fancy.
history_test_->in_mem_backend_.reset(backend);
}
void BackendDelegate::BroadcastNotifications(int type,
HistoryDetails* details) {
// Currently, just send the notifications directly to the in-memory database.
// We may want do do something more fancy in the future.
content::Details<HistoryDetails> det(details);
history_test_->in_mem_backend_->Observe(type,
content::Source<HistoryBackendDBTest>(NULL), det);
// The backend passes ownership of the details pointer to us.
delete details;
}
TEST_F(HistoryBackendDBTest, ClearBrowsingData_Downloads) {
CreateBackendAndDatabase();
// Initially there should be nothing in the downloads database.
std::vector<DownloadRow> downloads;
db_->QueryDownloads(&downloads);
EXPECT_EQ(0U, downloads.size());
// Add a download, test that it was added correctly, remove it, test that it
// was removed.
Time now = Time();
uint32 id = 1;
EXPECT_TRUE(AddDownload(id, DownloadItem::COMPLETE, Time()));
db_->QueryDownloads(&downloads);
EXPECT_EQ(1U, downloads.size());
EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("current-path")),
downloads[0].current_path);
EXPECT_EQ(base::FilePath(FILE_PATH_LITERAL("target-path")),
downloads[0].target_path);
EXPECT_EQ(1UL, downloads[0].url_chain.size());
EXPECT_EQ(GURL("foo-url"), downloads[0].url_chain[0]);
EXPECT_EQ(std::string("http://referrer.com/"),
std::string(downloads[0].referrer_url.spec()));
EXPECT_EQ(now, downloads[0].start_time);
EXPECT_EQ(now, downloads[0].end_time);
EXPECT_EQ(0, downloads[0].received_bytes);
EXPECT_EQ(512, downloads[0].total_bytes);
EXPECT_EQ(DownloadItem::COMPLETE, downloads[0].state);
EXPECT_EQ(content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
downloads[0].danger_type);
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_NONE,
downloads[0].interrupt_reason);
EXPECT_FALSE(downloads[0].opened);
EXPECT_EQ("by_ext_id", downloads[0].by_ext_id);
EXPECT_EQ("by_ext_name", downloads[0].by_ext_name);
db_->QueryDownloads(&downloads);
EXPECT_EQ(1U, downloads.size());
db_->RemoveDownload(id);
db_->QueryDownloads(&downloads);
EXPECT_EQ(0U, downloads.size());
}
TEST_F(HistoryBackendDBTest, MigrateDownloadsState) {
// Create the db we want.
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(22));
{
// Open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// Manually insert corrupted rows; there's infrastructure in place now to
// make this impossible, at least according to the test above.
for (int state = 0; state < 5; ++state) {
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads (id, full_path, url, start_time, "
"received_bytes, total_bytes, state, end_time, opened) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?)"));
s.BindInt64(0, 1 + state);
s.BindString(1, "path");
s.BindString(2, "url");
s.BindInt64(3, base::Time::Now().ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, state);
s.BindInt64(7, base::Time::Now().ToTimeT());
s.BindInt(8, state % 2);
ASSERT_TRUE(s.Run());
}
}
// Re-open the db using the HistoryDatabase, which should migrate from version
// 22 to the current version, fixing just the row whose state was 3.
// Then close the db so that we can re-open it directly.
CreateBackendAndDatabase();
DeleteBackend();
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
{
// The version should have been updated.
int cur_version = HistoryDatabase::GetCurrentVersion();
ASSERT_LT(22, cur_version);
sql::Statement s(db.GetUniqueStatement(
"SELECT value FROM meta WHERE key = 'version'"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(cur_version, s.ColumnInt(0));
}
{
sql::Statement statement(db.GetUniqueStatement(
"SELECT id, state, opened "
"FROM downloads "
"ORDER BY id"));
int counter = 0;
while (statement.Step()) {
EXPECT_EQ(1 + counter, statement.ColumnInt64(0));
// The only thing that migration should have changed was state from 3 to
// 4.
EXPECT_EQ(((counter == 3) ? 4 : counter), statement.ColumnInt(1));
EXPECT_EQ(counter % 2, statement.ColumnInt(2));
++counter;
}
EXPECT_EQ(5, counter);
}
}
}
TEST_F(HistoryBackendDBTest, MigrateDownloadsReasonPathsAndDangerType) {
Time now(base::Time::Now());
// Create the db we want. The schema didn't change from 22->23, so just
// re-use the v22 file.
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(22));
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// Manually insert some rows.
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads (id, full_path, url, start_time, "
"received_bytes, total_bytes, state, end_time, opened) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?)"));
int64 id = 0;
// Null path.
s.BindInt64(0, ++id);
s.BindString(1, std::string());
s.BindString(2, "http://whatever.com/index.html");
s.BindInt64(3, now.ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, 1);
s.BindInt64(7, now.ToTimeT());
s.BindInt(8, 1);
ASSERT_TRUE(s.Run());
s.Reset(true);
// Non-null path.
s.BindInt64(0, ++id);
s.BindString(1, "/path/to/some/file");
s.BindString(2, "http://whatever.com/index1.html");
s.BindInt64(3, now.ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, 1);
s.BindInt64(7, now.ToTimeT());
s.BindInt(8, 1);
ASSERT_TRUE(s.Run());
}
// Re-open the db using the HistoryDatabase, which should migrate from version
// 23 to 24, creating the new tables and creating the new path, reason,
// and danger columns.
CreateBackendAndDatabase();
DeleteBackend();
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
{
// The version should have been updated.
int cur_version = HistoryDatabase::GetCurrentVersion();
ASSERT_LT(23, cur_version);
sql::Statement s(db.GetUniqueStatement(
"SELECT value FROM meta WHERE key = 'version'"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(cur_version, s.ColumnInt(0));
}
{
base::Time nowish(base::Time::FromTimeT(now.ToTimeT()));
// Confirm downloads table is valid.
sql::Statement statement(db.GetUniqueStatement(
"SELECT id, interrupt_reason, current_path, target_path, "
" danger_type, start_time, end_time "
"FROM downloads ORDER BY id"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(1, statement.ColumnInt64(0));
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_NONE,
statement.ColumnInt(1));
EXPECT_EQ("", statement.ColumnString(2));
EXPECT_EQ("", statement.ColumnString(3));
// Implicit dependence on value of kDangerTypeNotDangerous from
// download_database.cc.
EXPECT_EQ(0, statement.ColumnInt(4));
EXPECT_EQ(nowish.ToInternalValue(), statement.ColumnInt64(5));
EXPECT_EQ(nowish.ToInternalValue(), statement.ColumnInt64(6));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(2, statement.ColumnInt64(0));
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_NONE,
statement.ColumnInt(1));
EXPECT_EQ("/path/to/some/file", statement.ColumnString(2));
EXPECT_EQ("/path/to/some/file", statement.ColumnString(3));
EXPECT_EQ(0, statement.ColumnInt(4));
EXPECT_EQ(nowish.ToInternalValue(), statement.ColumnInt64(5));
EXPECT_EQ(nowish.ToInternalValue(), statement.ColumnInt64(6));
EXPECT_FALSE(statement.Step());
}
{
// Confirm downloads_url_chains table is valid.
sql::Statement statement(db.GetUniqueStatement(
"SELECT id, chain_index, url FROM downloads_url_chains "
" ORDER BY id, chain_index"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(1, statement.ColumnInt64(0));
EXPECT_EQ(0, statement.ColumnInt(1));
EXPECT_EQ("http://whatever.com/index.html", statement.ColumnString(2));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(2, statement.ColumnInt64(0));
EXPECT_EQ(0, statement.ColumnInt(1));
EXPECT_EQ("http://whatever.com/index1.html", statement.ColumnString(2));
EXPECT_FALSE(statement.Step());
}
}
}
TEST_F(HistoryBackendDBTest, MigrateReferrer) {
Time now(base::Time::Now());
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(22));
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads (id, full_path, url, start_time, "
"received_bytes, total_bytes, state, end_time, opened) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?)"));
int64 db_handle = 0;
s.BindInt64(0, ++db_handle);
s.BindString(1, "full_path");
s.BindString(2, "http://whatever.com/index.html");
s.BindInt64(3, now.ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, 1);
s.BindInt64(7, now.ToTimeT());
s.BindInt(8, 1);
ASSERT_TRUE(s.Run());
}
// Re-open the db using the HistoryDatabase, which should migrate to version
// 26, creating the referrer column.
CreateBackendAndDatabase();
DeleteBackend();
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// The version should have been updated.
int cur_version = HistoryDatabase::GetCurrentVersion();
ASSERT_LE(26, cur_version);
{
sql::Statement s(db.GetUniqueStatement(
"SELECT value FROM meta WHERE key = 'version'"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(cur_version, s.ColumnInt(0));
}
{
sql::Statement s(db.GetUniqueStatement(
"SELECT referrer from downloads"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(std::string(), s.ColumnString(0));
}
}
}
TEST_F(HistoryBackendDBTest, MigrateDownloadedByExtension) {
Time now(base::Time::Now());
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(26));
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads (id, current_path, target_path, start_time, "
"received_bytes, total_bytes, state, danger_type, interrupt_reason, "
"end_time, opened, referrer) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
s.BindInt64(0, 1);
s.BindString(1, "current_path");
s.BindString(2, "target_path");
s.BindInt64(3, now.ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, 1);
s.BindInt(7, 0);
s.BindInt(8, 0);
s.BindInt64(9, now.ToTimeT());
s.BindInt(10, 1);
s.BindString(11, "referrer");
ASSERT_TRUE(s.Run());
}
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads_url_chains (id, chain_index, url) VALUES "
"(?, ?, ?)"));
s.BindInt64(0, 4);
s.BindInt64(1, 0);
s.BindString(2, "url");
ASSERT_TRUE(s.Run());
}
}
// Re-open the db using the HistoryDatabase, which should migrate to version
// 27, creating the by_ext_id and by_ext_name columns.
CreateBackendAndDatabase();
DeleteBackend();
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// The version should have been updated.
int cur_version = HistoryDatabase::GetCurrentVersion();
ASSERT_LE(27, cur_version);
{
sql::Statement s(db.GetUniqueStatement(
"SELECT value FROM meta WHERE key = 'version'"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(cur_version, s.ColumnInt(0));
}
{
sql::Statement s(db.GetUniqueStatement(
"SELECT by_ext_id, by_ext_name from downloads"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(std::string(), s.ColumnString(0));
EXPECT_EQ(std::string(), s.ColumnString(1));
}
}
}
TEST_F(HistoryBackendDBTest, MigrateDownloadValidators) {
Time now(base::Time::Now());
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(27));
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads (id, current_path, target_path, start_time, "
"received_bytes, total_bytes, state, danger_type, interrupt_reason, "
"end_time, opened, referrer, by_ext_id, by_ext_name) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
s.BindInt64(0, 1);
s.BindString(1, "current_path");
s.BindString(2, "target_path");
s.BindInt64(3, now.ToTimeT());
s.BindInt64(4, 100);
s.BindInt64(5, 100);
s.BindInt(6, 1);
s.BindInt(7, 0);
s.BindInt(8, 0);
s.BindInt64(9, now.ToTimeT());
s.BindInt(10, 1);
s.BindString(11, "referrer");
s.BindString(12, "by extension ID");
s.BindString(13, "by extension name");
ASSERT_TRUE(s.Run());
}
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO downloads_url_chains (id, chain_index, url) VALUES "
"(?, ?, ?)"));
s.BindInt64(0, 4);
s.BindInt64(1, 0);
s.BindString(2, "url");
ASSERT_TRUE(s.Run());
}
}
// Re-open the db using the HistoryDatabase, which should migrate to the
// current version, creating the etag and last_modified columns.
CreateBackendAndDatabase();
DeleteBackend();
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// The version should have been updated.
int cur_version = HistoryDatabase::GetCurrentVersion();
ASSERT_LE(28, cur_version);
{
sql::Statement s(db.GetUniqueStatement(
"SELECT value FROM meta WHERE key = 'version'"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(cur_version, s.ColumnInt(0));
}
{
sql::Statement s(db.GetUniqueStatement(
"SELECT etag, last_modified from downloads"));
EXPECT_TRUE(s.Step());
EXPECT_EQ(std::string(), s.ColumnString(0));
EXPECT_EQ(std::string(), s.ColumnString(1));
}
}
}
TEST_F(HistoryBackendDBTest, ConfirmDownloadRowCreateAndDelete) {
// Create the DB.
CreateBackendAndDatabase();
base::Time now(base::Time::Now());
// Add some downloads.
uint32 id1 = 1, id2 = 2, id3 = 3;
AddDownload(id1, DownloadItem::COMPLETE, now);
AddDownload(id2, DownloadItem::COMPLETE, now + base::TimeDelta::FromDays(2));
AddDownload(id3, DownloadItem::COMPLETE, now - base::TimeDelta::FromDays(2));
// Confirm that resulted in the correct number of rows in the DB.
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement statement(db.GetUniqueStatement(
"Select Count(*) from downloads"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(3, statement.ColumnInt(0));
sql::Statement statement1(db.GetUniqueStatement(
"Select Count(*) from downloads_url_chains"));
EXPECT_TRUE(statement1.Step());
EXPECT_EQ(3, statement1.ColumnInt(0));
}
// Delete some rows and make sure the results are still correct.
CreateBackendAndDatabase();
db_->RemoveDownload(id2);
db_->RemoveDownload(id3);
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement statement(db.GetUniqueStatement(
"Select Count(*) from downloads"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(1, statement.ColumnInt(0));
sql::Statement statement1(db.GetUniqueStatement(
"Select Count(*) from downloads_url_chains"));
EXPECT_TRUE(statement1.Step());
EXPECT_EQ(1, statement1.ColumnInt(0));
}
}
TEST_F(HistoryBackendDBTest, DownloadNukeRecordsMissingURLs) {
CreateBackendAndDatabase();
base::Time now(base::Time::Now());
std::vector<GURL> url_chain;
DownloadRow download(base::FilePath(FILE_PATH_LITERAL("foo-path")),
base::FilePath(FILE_PATH_LITERAL("foo-path")),
url_chain,
GURL(std::string()),
now,
now,
std::string(),
std::string(),
0,
512,
DownloadItem::COMPLETE,
content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
content::DOWNLOAD_INTERRUPT_REASON_NONE,
1,
0,
"by_ext_id",
"by_ext_name");
// Creating records without any urls should fail.
EXPECT_FALSE(db_->CreateDownload(download));
download.url_chain.push_back(GURL("foo-url"));
EXPECT_TRUE(db_->CreateDownload(download));
// Pretend that the URLs were dropped.
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement statement(db.GetUniqueStatement(
"DELETE FROM downloads_url_chains WHERE id=1"));
ASSERT_TRUE(statement.Run());
}
CreateBackendAndDatabase();
std::vector<DownloadRow> downloads;
db_->QueryDownloads(&downloads);
EXPECT_EQ(0U, downloads.size());
// QueryDownloads should have nuked the corrupt record.
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
{
sql::Statement statement(db.GetUniqueStatement(
"SELECT count(*) from downloads"));
ASSERT_TRUE(statement.Step());
EXPECT_EQ(0, statement.ColumnInt(0));
}
}
}
TEST_F(HistoryBackendDBTest, ConfirmDownloadInProgressCleanup) {
// Create the DB.
CreateBackendAndDatabase();
base::Time now(base::Time::Now());
// Put an IN_PROGRESS download in the DB.
AddDownload(1, DownloadItem::IN_PROGRESS, now);
// Confirm that they made it into the DB unchanged.
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement statement(db.GetUniqueStatement(
"Select Count(*) from downloads"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(1, statement.ColumnInt(0));
sql::Statement statement1(db.GetUniqueStatement(
"Select state, interrupt_reason from downloads"));
EXPECT_TRUE(statement1.Step());
EXPECT_EQ(DownloadDatabase::kStateInProgress, statement1.ColumnInt(0));
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_NONE, statement1.ColumnInt(1));
EXPECT_FALSE(statement1.Step());
}
// Read in the DB through query downloads, then test that the
// right transformation was returned.
CreateBackendAndDatabase();
std::vector<DownloadRow> results;
db_->QueryDownloads(&results);
ASSERT_EQ(1u, results.size());
EXPECT_EQ(content::DownloadItem::INTERRUPTED, results[0].state);
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_CRASH,
results[0].interrupt_reason);
// Allow the update to propagate, shut down the DB, and confirm that
// the query updated the on disk database as well.
base::MessageLoop::current()->RunUntilIdle();
DeleteBackend();
{
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
sql::Statement statement(db.GetUniqueStatement(
"Select Count(*) from downloads"));
EXPECT_TRUE(statement.Step());
EXPECT_EQ(1, statement.ColumnInt(0));
sql::Statement statement1(db.GetUniqueStatement(
"Select state, interrupt_reason from downloads"));
EXPECT_TRUE(statement1.Step());
EXPECT_EQ(DownloadDatabase::kStateInterrupted, statement1.ColumnInt(0));
EXPECT_EQ(content::DOWNLOAD_INTERRUPT_REASON_CRASH,
statement1.ColumnInt(1));
EXPECT_FALSE(statement1.Step());
}
}
struct InterruptReasonAssociation {
std::string name;
int value;
};
// Test is dependent on interrupt reasons being listed in header file
// in order.
const InterruptReasonAssociation current_reasons[] = {
#define INTERRUPT_REASON(a, b) { #a, b },
#include "content/public/browser/download_interrupt_reason_values.h"
#undef INTERRUPT_REASON
};
// This represents a list of all reasons we've previously used;
// Do Not Remove Any Entries From This List.
const InterruptReasonAssociation historical_reasons[] = {
{"FILE_FAILED", 1},
{"FILE_ACCESS_DENIED", 2},
{"FILE_NO_SPACE", 3},
{"FILE_NAME_TOO_LONG", 5},
{"FILE_TOO_LARGE", 6},
{"FILE_VIRUS_INFECTED", 7},
{"FILE_TRANSIENT_ERROR", 10},
{"FILE_BLOCKED", 11},
{"FILE_SECURITY_CHECK_FAILED", 12},
{"FILE_TOO_SHORT", 13},
{"NETWORK_FAILED", 20},
{"NETWORK_TIMEOUT", 21},
{"NETWORK_DISCONNECTED", 22},
{"NETWORK_SERVER_DOWN", 23},
{"SERVER_FAILED", 30},
{"SERVER_NO_RANGE", 31},
{"SERVER_PRECONDITION", 32},
{"SERVER_BAD_CONTENT", 33},
{"USER_CANCELED", 40},
{"USER_SHUTDOWN", 41},
{"CRASH", 50},
};
// Make sure no one has changed a DownloadInterruptReason we've previously
// persisted.
TEST_F(HistoryBackendDBTest,
ConfirmDownloadInterruptReasonBackwardsCompatible) {
// Are there any cases in which a historical number has been repurposed
// for an error other than it's original?
for (size_t i = 0; i < arraysize(current_reasons); i++) {
const InterruptReasonAssociation& cur_reason(current_reasons[i]);
bool found = false;
for (size_t j = 0; j < arraysize(historical_reasons); ++j) {
const InterruptReasonAssociation& hist_reason(historical_reasons[j]);
if (hist_reason.value == cur_reason.value) {
EXPECT_EQ(cur_reason.name, hist_reason.name)
<< "Same integer value used for old error \""
<< hist_reason.name
<< "\" as for new error \""
<< cur_reason.name
<< "\"." << std::endl
<< "**This will cause database conflicts with persisted values**"
<< std::endl
<< "Please assign a new, non-conflicting value for the new error.";
}
if (hist_reason.name == cur_reason.name) {
EXPECT_EQ(cur_reason.value, hist_reason.value)
<< "Same name (\"" << hist_reason.name
<< "\") maps to a different value historically ("
<< hist_reason.value << ") and currently ("
<< cur_reason.value << ")" << std::endl
<< "This may cause database conflicts with persisted values"
<< std::endl
<< "If this error is the same as the old one, you should"
<< std::endl
<< "use the old value, and if it is different, you should"
<< std::endl
<< "use a new name.";
found = true;
}
}
EXPECT_TRUE(found)
<< "Error \"" << cur_reason.name << "\" not found in historical list."
<< std::endl
<< "Please add it.";
}
}
// The tracker uses RenderProcessHost pointers for scoping but never
// dereferences them. We use ints because it's easier. This function converts
// between the two.
static void* MakeFakeHost(int id) {
void* host = 0;
memcpy(&host, &id, sizeof(id));
return host;
}
class HistoryTest : public testing::Test {
public:
HistoryTest()
: got_thumbnail_callback_(false),
redirect_query_success_(false),
query_url_success_(false) {
}
virtual ~HistoryTest() {
}
void OnSegmentUsageAvailable(CancelableRequestProvider::Handle handle,
std::vector<PageUsageData*>* data) {
page_usage_data_.swap(*data);
base::MessageLoop::current()->Quit();
}
void OnDeleteURLsDone(CancelableRequestProvider::Handle handle) {
base::MessageLoop::current()->Quit();
}
void OnMostVisitedURLsAvailable(CancelableRequestProvider::Handle handle,
MostVisitedURLList url_list) {
most_visited_urls_.swap(url_list);
base::MessageLoop::current()->Quit();
}
protected:
friend class BackendDelegate;
// testing::Test
virtual void SetUp() {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
history_dir_ = temp_dir_.path().AppendASCII("HistoryTest");
ASSERT_TRUE(file_util::CreateDirectory(history_dir_));
history_service_.reset(new HistoryService);
if (!history_service_->Init(history_dir_, NULL)) {
history_service_.reset();
ADD_FAILURE();
}
}
virtual void TearDown() {
if (history_service_)
CleanupHistoryService();
// Make sure we don't have any event pending that could disrupt the next
// test.
base::MessageLoop::current()->PostTask(FROM_HERE,
base::MessageLoop::QuitClosure());
base::MessageLoop::current()->Run();
}
void CleanupHistoryService() {
DCHECK(history_service_);
history_service_->NotifyRenderProcessHostDestruction(0);
history_service_->SetOnBackendDestroyTask(base::MessageLoop::QuitClosure());
history_service_->Cleanup();
history_service_.reset();
// Wait for the backend class to terminate before deleting the files and
// moving to the next test. Note: if this never terminates, somebody is
// probably leaking a reference to the history backend, so it never calls
// our destroy task.
base::MessageLoop::current()->Run();
}
// Fills the query_url_row_ and query_url_visits_ structures with the
// information about the given URL and returns true. If the URL was not
// found, this will return false and those structures will not be changed.
bool QueryURL(HistoryService* history, const GURL& url) {
history_service_->QueryURL(url, true, &consumer_,
base::Bind(&HistoryTest::SaveURLAndQuit,
base::Unretained(this)));
base::MessageLoop::current()->Run(); // Will be exited in SaveURLAndQuit.
return query_url_success_;
}
// Callback for HistoryService::QueryURL.
void SaveURLAndQuit(HistoryService::Handle handle,
bool success,
const URLRow* url_row,
VisitVector* visit_vector) {
query_url_success_ = success;
if (query_url_success_) {
query_url_row_ = *url_row;
query_url_visits_.swap(*visit_vector);
} else {
query_url_row_ = URLRow();
query_url_visits_.clear();
}
base::MessageLoop::current()->Quit();
}
// Fills in saved_redirects_ with the redirect information for the given URL,
// returning true on success. False means the URL was not found.
bool QueryRedirectsFrom(HistoryService* history, const GURL& url) {
history_service_->QueryRedirectsFrom(
url, &consumer_,
base::Bind(&HistoryTest::OnRedirectQueryComplete,
base::Unretained(this)));
base::MessageLoop::current()->Run(); // Will be exited in *QueryComplete.
return redirect_query_success_;
}
// Callback for QueryRedirects.
void OnRedirectQueryComplete(HistoryService::Handle handle,
GURL url,
bool success,
history::RedirectList* redirects) {
redirect_query_success_ = success;
if (redirect_query_success_)
saved_redirects_.swap(*redirects);
else
saved_redirects_.clear();
base::MessageLoop::current()->Quit();
}
base::ScopedTempDir temp_dir_;
base::MessageLoopForUI message_loop_;
// PageUsageData vector to test segments.
ScopedVector<PageUsageData> page_usage_data_;
MostVisitedURLList most_visited_urls_;
// When non-NULL, this will be deleted on tear down and we will block until
// the backend thread has completed. This allows tests for the history
// service to use this feature, but other tests to ignore this.
scoped_ptr<HistoryService> history_service_;
// names of the database files
base::FilePath history_dir_;
// Set by the thumbnail callback when we get data, you should be sure to
// clear this before issuing a thumbnail request.
bool got_thumbnail_callback_;
std::vector<unsigned char> thumbnail_data_;
// Set by the redirect callback when we get data. You should be sure to
// clear this before issuing a redirect request.
history::RedirectList saved_redirects_;
bool redirect_query_success_;
// For history requests.
CancelableRequestConsumer consumer_;
// For saving URL info after a call to QueryURL
bool query_url_success_;
URLRow query_url_row_;
VisitVector query_url_visits_;
};
TEST_F(HistoryTest, AddPage) {
ASSERT_TRUE(history_service_.get());
// Add the page once from a child frame.
const GURL test_url("http://www.google.com/");
history_service_->AddPage(test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(),
content::PAGE_TRANSITION_MANUAL_SUBFRAME,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(0, query_url_row_.typed_count());
EXPECT_TRUE(query_url_row_.hidden()); // Hidden because of child frame.
// Add the page once from the main frame (should unhide it).
history_service_->AddPage(test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(2, query_url_row_.visit_count()); // Added twice.
EXPECT_EQ(0, query_url_row_.typed_count()); // Never typed.
EXPECT_FALSE(query_url_row_.hidden()); // Because loaded in main frame.
}
TEST_F(HistoryTest, AddRedirect) {
ASSERT_TRUE(history_service_.get());
const char* first_sequence[] = {
"http://first.page.com/",
"http://second.page.com/"};
int first_count = arraysize(first_sequence);
history::RedirectList first_redirects;
for (int i = 0; i < first_count; i++)
first_redirects.push_back(GURL(first_sequence[i]));
// Add the sequence of pages as a server with no referrer. Note that we need
// to have a non-NULL page ID scope.
history_service_->AddPage(
first_redirects.back(), base::Time::Now(), MakeFakeHost(1),
0, GURL(), first_redirects, content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, true);
// The first page should be added once with a link visit type (because we set
// LINK when we added the original URL, and a referrer of nowhere (0).
EXPECT_TRUE(QueryURL(history_service_.get(), first_redirects[0]));
EXPECT_EQ(1, query_url_row_.visit_count());
ASSERT_EQ(1U, query_url_visits_.size());
int64 first_visit = query_url_visits_[0].visit_id;
EXPECT_EQ(content::PAGE_TRANSITION_LINK |
content::PAGE_TRANSITION_CHAIN_START,
query_url_visits_[0].transition);
EXPECT_EQ(0, query_url_visits_[0].referring_visit); // No referrer.
// The second page should be a server redirect type with a referrer of the
// first page.
EXPECT_TRUE(QueryURL(history_service_.get(), first_redirects[1]));
EXPECT_EQ(1, query_url_row_.visit_count());
ASSERT_EQ(1U, query_url_visits_.size());
int64 second_visit = query_url_visits_[0].visit_id;
EXPECT_EQ(content::PAGE_TRANSITION_SERVER_REDIRECT |
content::PAGE_TRANSITION_CHAIN_END,
query_url_visits_[0].transition);
EXPECT_EQ(first_visit, query_url_visits_[0].referring_visit);
// Check that the redirect finding function successfully reports it.
saved_redirects_.clear();
QueryRedirectsFrom(history_service_.get(), first_redirects[0]);
ASSERT_EQ(1U, saved_redirects_.size());
EXPECT_EQ(first_redirects[1], saved_redirects_[0]);
// Now add a client redirect from that second visit to a third, client
// redirects are tracked by the RenderView prior to updating history,
// so we pass in a CLIENT_REDIRECT qualifier to mock that behavior.
history::RedirectList second_redirects;
second_redirects.push_back(first_redirects[1]);
second_redirects.push_back(GURL("http://last.page.com/"));
history_service_->AddPage(second_redirects[1], base::Time::Now(),
MakeFakeHost(1), 1, second_redirects[0], second_redirects,
static_cast<content::PageTransition>(
content::PAGE_TRANSITION_LINK |
content::PAGE_TRANSITION_CLIENT_REDIRECT),
history::SOURCE_BROWSED, true);
// The last page (source of the client redirect) should NOT have an
// additional visit added, because it was a client redirect (normally it
// would). We should only have 1 left over from the first sequence.
EXPECT_TRUE(QueryURL(history_service_.get(), second_redirects[0]));
EXPECT_EQ(1, query_url_row_.visit_count());
// The final page should be set as a client redirect from the previous visit.
EXPECT_TRUE(QueryURL(history_service_.get(), second_redirects[1]));
EXPECT_EQ(1, query_url_row_.visit_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_CLIENT_REDIRECT |
content::PAGE_TRANSITION_CHAIN_END,
query_url_visits_[0].transition);
EXPECT_EQ(second_visit, query_url_visits_[0].referring_visit);
}
TEST_F(HistoryTest, MakeIntranetURLsTyped) {
ASSERT_TRUE(history_service_.get());
// Add a non-typed visit to an intranet URL on an unvisited host. This should
// get promoted to a typed visit.
const GURL test_url("http://intranet_host/path");
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_TYPED,
content::PageTransitionStripQualifier(query_url_visits_[0].transition));
// Add more visits on the same host. None of these should be promoted since
// there is already a typed visit.
// Different path.
const GURL test_url2("http://intranet_host/different_path");
history_service_->AddPage(
test_url2, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url2));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(0, query_url_row_.typed_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_LINK,
content::PageTransitionStripQualifier(query_url_visits_[0].transition));
// No path.
const GURL test_url3("http://intranet_host/");
history_service_->AddPage(
test_url3, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url3));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(0, query_url_row_.typed_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_LINK,
content::PageTransitionStripQualifier(query_url_visits_[0].transition));
// Different scheme.
const GURL test_url4("https://intranet_host/");
history_service_->AddPage(
test_url4, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url4));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(0, query_url_row_.typed_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_LINK,
content::PageTransitionStripQualifier(query_url_visits_[0].transition));
// Different transition.
const GURL test_url5("http://intranet_host/another_path");
history_service_->AddPage(
test_url5, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(),
content::PAGE_TRANSITION_AUTO_BOOKMARK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url5));
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(0, query_url_row_.typed_count());
ASSERT_EQ(1U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_AUTO_BOOKMARK,
content::PageTransitionStripQualifier(query_url_visits_[0].transition));
// Original URL.
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(2, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
ASSERT_EQ(2U, query_url_visits_.size());
EXPECT_EQ(content::PAGE_TRANSITION_LINK,
content::PageTransitionStripQualifier(query_url_visits_[1].transition));
}
TEST_F(HistoryTest, Typed) {
ASSERT_TRUE(history_service_.get());
// Add the page once as typed.
const GURL test_url("http://www.google.com/");
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
// We should have the same typed & visit count.
EXPECT_EQ(1, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
// Add the page again not typed.
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
// The second time should not have updated the typed count.
EXPECT_EQ(2, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
// Add the page again as a generated URL.
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_GENERATED,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
// This should have worked like a link click.
EXPECT_EQ(3, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
// Add the page again as a reload.
history_service_->AddPage(
test_url, base::Time::Now(), NULL, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_RELOAD,
history::SOURCE_BROWSED, false);
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
// This should not have incremented any visit counts.
EXPECT_EQ(3, query_url_row_.visit_count());
EXPECT_EQ(1, query_url_row_.typed_count());
}
TEST_F(HistoryTest, SetTitle) {
ASSERT_TRUE(history_service_.get());
// Add a URL.
const GURL existing_url("http://www.google.com/");
history_service_->AddPage(
existing_url, base::Time::Now(), history::SOURCE_BROWSED);
// Set some title.
const string16 existing_title = UTF8ToUTF16("Google");
history_service_->SetPageTitle(existing_url, existing_title);
// Make sure the title got set.
EXPECT_TRUE(QueryURL(history_service_.get(), existing_url));
EXPECT_EQ(existing_title, query_url_row_.title());
// set a title on a nonexistent page
const GURL nonexistent_url("http://news.google.com/");
const string16 nonexistent_title = UTF8ToUTF16("Google News");
history_service_->SetPageTitle(nonexistent_url, nonexistent_title);
// Make sure nothing got written.
EXPECT_FALSE(QueryURL(history_service_.get(), nonexistent_url));
EXPECT_EQ(string16(), query_url_row_.title());
// TODO(brettw) this should also test redirects, which get the title of the
// destination page.
}
// crbug.com/159387: This test fails when daylight savings time ends.
TEST_F(HistoryTest, DISABLED_Segments) {
ASSERT_TRUE(history_service_.get());
static const void* scope = static_cast<void*>(this);
// Add a URL.
const GURL existing_url("http://www.google.com/");
history_service_->AddPage(
existing_url, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
// Make sure a segment was created.
history_service_->QuerySegmentUsageSince(
&consumer_, Time::Now() - TimeDelta::FromDays(1), 10,
base::Bind(&HistoryTest::OnSegmentUsageAvailable,
base::Unretained(this)));
// Wait for processing.
base::MessageLoop::current()->Run();
ASSERT_EQ(1U, page_usage_data_.size());
EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
EXPECT_DOUBLE_EQ(3.0, page_usage_data_[0]->GetScore());
// Add a URL which doesn't create a segment.
const GURL link_url("http://yahoo.com/");
history_service_->AddPage(
link_url, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
// Query again
history_service_->QuerySegmentUsageSince(
&consumer_, Time::Now() - TimeDelta::FromDays(1), 10,
base::Bind(&HistoryTest::OnSegmentUsageAvailable,
base::Unretained(this)));
// Wait for processing.
base::MessageLoop::current()->Run();
// Make sure we still have one segment.
ASSERT_EQ(1U, page_usage_data_.size());
EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
// Add a page linked from existing_url.
history_service_->AddPage(
GURL("http://www.google.com/foo"), base::Time::Now(),
scope, 3, existing_url, history::RedirectList(),
content::PAGE_TRANSITION_LINK, history::SOURCE_BROWSED,
false);
// Query again
history_service_->QuerySegmentUsageSince(
&consumer_, Time::Now() - TimeDelta::FromDays(1), 10,
base::Bind(&HistoryTest::OnSegmentUsageAvailable,
base::Unretained(this)));
// Wait for processing.
base::MessageLoop::current()->Run();
// Make sure we still have one segment.
ASSERT_EQ(1U, page_usage_data_.size());
EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
// However, the score should have increased.
EXPECT_GT(page_usage_data_[0]->GetScore(), 5.0);
}
TEST_F(HistoryTest, MostVisitedURLs) {
ASSERT_TRUE(history_service_.get());
const GURL url0("http://www.google.com/url0/");
const GURL url1("http://www.google.com/url1/");
const GURL url2("http://www.google.com/url2/");
const GURL url3("http://www.google.com/url3/");
const GURL url4("http://www.google.com/url4/");
static const void* scope = static_cast<void*>(this);
// Add two pages.
history_service_->AddPage(
url0, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->AddPage(
url1, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->QueryMostVisitedURLs(
20, 90, &consumer_,
base::Bind(
&HistoryTest::OnMostVisitedURLsAvailable,
base::Unretained(this)));
base::MessageLoop::current()->Run();
EXPECT_EQ(2U, most_visited_urls_.size());
EXPECT_EQ(url0, most_visited_urls_[0].url);
EXPECT_EQ(url1, most_visited_urls_[1].url);
// Add another page.
history_service_->AddPage(
url2, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->QueryMostVisitedURLs(
20, 90, &consumer_,
base::Bind(
&HistoryTest::OnMostVisitedURLsAvailable,
base::Unretained(this)));
base::MessageLoop::current()->Run();
EXPECT_EQ(3U, most_visited_urls_.size());
EXPECT_EQ(url0, most_visited_urls_[0].url);
EXPECT_EQ(url1, most_visited_urls_[1].url);
EXPECT_EQ(url2, most_visited_urls_[2].url);
// Revisit url2, making it the top URL.
history_service_->AddPage(
url2, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->QueryMostVisitedURLs(
20, 90, &consumer_,
base::Bind(
&HistoryTest::OnMostVisitedURLsAvailable,
base::Unretained(this)));
base::MessageLoop::current()->Run();
EXPECT_EQ(3U, most_visited_urls_.size());
EXPECT_EQ(url2, most_visited_urls_[0].url);
EXPECT_EQ(url0, most_visited_urls_[1].url);
EXPECT_EQ(url1, most_visited_urls_[2].url);
// Revisit url1, making it the top URL.
history_service_->AddPage(
url1, base::Time::Now(), scope, 0, GURL(),
history::RedirectList(), content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->QueryMostVisitedURLs(
20, 90, &consumer_,
base::Bind(
&HistoryTest::OnMostVisitedURLsAvailable,
base::Unretained(this)));
base::MessageLoop::current()->Run();
EXPECT_EQ(3U, most_visited_urls_.size());
EXPECT_EQ(url1, most_visited_urls_[0].url);
EXPECT_EQ(url2, most_visited_urls_[1].url);
EXPECT_EQ(url0, most_visited_urls_[2].url);
// Redirects
history::RedirectList redirects;
redirects.push_back(url3);
redirects.push_back(url4);
// Visit url4 using redirects.
history_service_->AddPage(
url4, base::Time::Now(), scope, 0, GURL(),
redirects, content::PAGE_TRANSITION_TYPED,
history::SOURCE_BROWSED, false);
history_service_->QueryMostVisitedURLs(
20, 90, &consumer_,
base::Bind(
&HistoryTest::OnMostVisitedURLsAvailable,
base::Unretained(this)));
base::MessageLoop::current()->Run();
EXPECT_EQ(4U, most_visited_urls_.size());
EXPECT_EQ(url1, most_visited_urls_[0].url);
EXPECT_EQ(url2, most_visited_urls_[1].url);
EXPECT_EQ(url0, most_visited_urls_[2].url);
EXPECT_EQ(url3, most_visited_urls_[3].url);
EXPECT_EQ(2U, most_visited_urls_[3].redirects.size());
}
namespace {
// A HistoryDBTask implementation. Each time RunOnDBThread is invoked
// invoke_count is increment. When invoked kWantInvokeCount times, true is
// returned from RunOnDBThread which should stop RunOnDBThread from being
// invoked again. When DoneRunOnMainThread is invoked, done_invoked is set to
// true.
class HistoryDBTaskImpl : public HistoryDBTask {
public:
static const int kWantInvokeCount;
HistoryDBTaskImpl() : invoke_count(0), done_invoked(false) {}
virtual bool RunOnDBThread(HistoryBackend* backend,
HistoryDatabase* db) OVERRIDE {
return (++invoke_count == kWantInvokeCount);
}
virtual void DoneRunOnMainThread() OVERRIDE {
done_invoked = true;
base::MessageLoop::current()->Quit();
}
int invoke_count;
bool done_invoked;
private:
virtual ~HistoryDBTaskImpl() {}
DISALLOW_COPY_AND_ASSIGN(HistoryDBTaskImpl);
};
// static
const int HistoryDBTaskImpl::kWantInvokeCount = 2;
} // namespace
TEST_F(HistoryTest, HistoryDBTask) {
ASSERT_TRUE(history_service_.get());
CancelableRequestConsumerT<int, 0> request_consumer;
scoped_refptr<HistoryDBTaskImpl> task(new HistoryDBTaskImpl());
history_service_->ScheduleDBTask(task.get(), &request_consumer);
// Run the message loop. When HistoryDBTaskImpl::DoneRunOnMainThread runs,
// it will stop the message loop. If the test hangs here, it means
// DoneRunOnMainThread isn't being invoked correctly.
base::MessageLoop::current()->Run();
CleanupHistoryService();
// WARNING: history has now been deleted.
history_service_.reset();
ASSERT_EQ(HistoryDBTaskImpl::kWantInvokeCount, task->invoke_count);
ASSERT_TRUE(task->done_invoked);
}
TEST_F(HistoryTest, HistoryDBTaskCanceled) {
ASSERT_TRUE(history_service_.get());
CancelableRequestConsumerT<int, 0> request_consumer;
scoped_refptr<HistoryDBTaskImpl> task(new HistoryDBTaskImpl());
history_service_->ScheduleDBTask(task.get(), &request_consumer);
request_consumer.CancelAllRequests();
CleanupHistoryService();
// WARNING: history has now been deleted.
history_service_.reset();
ASSERT_FALSE(task->done_invoked);
}
// Dummy SyncChangeProcessor used to help review what SyncChanges are pushed
// back up to Sync.
//
// TODO(akalin): Unify all the various test implementations of
// syncer::SyncChangeProcessor.
class TestChangeProcessor : public syncer::SyncChangeProcessor {
public:
TestChangeProcessor() {}
virtual ~TestChangeProcessor() {}
virtual syncer::SyncError ProcessSyncChanges(
const tracked_objects::Location& from_here,
const syncer::SyncChangeList& change_list) OVERRIDE {
changes_.insert(changes_.end(), change_list.begin(), change_list.end());
return syncer::SyncError();
}
const syncer::SyncChangeList& GetChanges() const {
return changes_;
}
private:
syncer::SyncChangeList changes_;
DISALLOW_COPY_AND_ASSIGN(TestChangeProcessor);
};
// SyncChangeProcessor implementation that delegates to another one.
// This is necessary since most things expect a
// scoped_ptr<SyncChangeProcessor>.
//
// TODO(akalin): Unify this too.
class SyncChangeProcessorDelegate : public syncer::SyncChangeProcessor {
public:
explicit SyncChangeProcessorDelegate(syncer::SyncChangeProcessor* recipient)
: recipient_(recipient) {
DCHECK(recipient_);
}
virtual ~SyncChangeProcessorDelegate() {}
// syncer::SyncChangeProcessor implementation.
virtual syncer::SyncError ProcessSyncChanges(
const tracked_objects::Location& from_here,
const syncer::SyncChangeList& change_list) OVERRIDE {
return recipient_->ProcessSyncChanges(from_here, change_list);
}
private:
// The recipient of all sync changes.
syncer::SyncChangeProcessor* const recipient_;
DISALLOW_COPY_AND_ASSIGN(SyncChangeProcessorDelegate);
};
// Create a local delete directive and process it while sync is
// online, and then when offline. The delete directive should be sent to sync,
// no error should be returned for the first time, and an error should be
// returned for the second time.
TEST_F(HistoryTest, ProcessLocalDeleteDirectiveSyncOnline) {
ASSERT_TRUE(history_service_.get());
const GURL test_url("http://www.google.com/");
for (int64 i = 1; i <= 10; ++i) {
base::Time t =
base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(i);
history_service_->AddPage(test_url, t, NULL, 0, GURL(),
history::RedirectList(),
content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
}
sync_pb::HistoryDeleteDirectiveSpecifics delete_directive;
sync_pb::GlobalIdDirective* global_id_directive =
delete_directive.mutable_global_id_directive();
global_id_directive->add_global_id(
(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(1))
.ToInternalValue());
TestChangeProcessor change_processor;
EXPECT_FALSE(
history_service_->MergeDataAndStartSyncing(
syncer::HISTORY_DELETE_DIRECTIVES,
syncer::SyncDataList(),
scoped_ptr<syncer::SyncChangeProcessor>(
new SyncChangeProcessorDelegate(&change_processor)),
scoped_ptr<syncer::SyncErrorFactory>()).error().IsSet());
syncer::SyncError err =
history_service_->ProcessLocalDeleteDirective(delete_directive);
EXPECT_FALSE(err.IsSet());
EXPECT_EQ(1u, change_processor.GetChanges().size());
history_service_->StopSyncing(syncer::HISTORY_DELETE_DIRECTIVES);
err = history_service_->ProcessLocalDeleteDirective(delete_directive);
EXPECT_TRUE(err.IsSet());
EXPECT_EQ(1u, change_processor.GetChanges().size());
}
// Closure function that runs periodically to check result of delete directive
// processing. Stop when timeout or processing ends indicated by the creation
// of sync changes.
void CheckDirectiveProcessingResult(
Time timeout, const TestChangeProcessor* change_processor,
uint32 num_changes) {
if (base::Time::Now() > timeout ||
change_processor->GetChanges().size() >= num_changes) {
return;
}
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&CheckDirectiveProcessingResult, timeout,
change_processor, num_changes));
}
// Create a delete directive for a few specific history entries,
// including ones that don't exist. The expected entries should be
// deleted.
TEST_F(HistoryTest, ProcessGlobalIdDeleteDirective) {
ASSERT_TRUE(history_service_.get());
const GURL test_url("http://www.google.com/");
for (int64 i = 1; i <= 20; i++) {
base::Time t =
base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(i);
history_service_->AddPage(test_url, t, NULL, 0, GURL(),
history::RedirectList(),
content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
}
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(20, query_url_row_.visit_count());
syncer::SyncDataList directives;
// 1st directive.
sync_pb::EntitySpecifics entity_specs;
sync_pb::GlobalIdDirective* global_id_directive =
entity_specs.mutable_history_delete_directive()
->mutable_global_id_directive();
global_id_directive->add_global_id(
(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(6))
.ToInternalValue());
global_id_directive->set_start_time_usec(3);
global_id_directive->set_end_time_usec(10);
directives.push_back(
syncer::SyncData::CreateRemoteData(1, entity_specs, base::Time()));
// 2nd directive.
global_id_directive->Clear();
global_id_directive->add_global_id(
(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(17))
.ToInternalValue());
global_id_directive->set_start_time_usec(13);
global_id_directive->set_end_time_usec(19);
directives.push_back(
syncer::SyncData::CreateRemoteData(2, entity_specs, base::Time()));
TestChangeProcessor change_processor;
EXPECT_FALSE(
history_service_->MergeDataAndStartSyncing(
syncer::HISTORY_DELETE_DIRECTIVES,
directives,
scoped_ptr<syncer::SyncChangeProcessor>(
new SyncChangeProcessorDelegate(&change_processor)),
scoped_ptr<syncer::SyncErrorFactory>()).error().IsSet());
// Inject a task to check status and keep message loop filled before directive
// processing finishes.
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&CheckDirectiveProcessingResult,
base::Time::Now() + base::TimeDelta::FromSeconds(10),
&change_processor, 2));
base::MessageLoop::current()->RunUntilIdle();
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
ASSERT_EQ(5, query_url_row_.visit_count());
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(1),
query_url_visits_[0].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(2),
query_url_visits_[1].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(11),
query_url_visits_[2].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(12),
query_url_visits_[3].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(20),
query_url_visits_[4].visit_time);
// Expect two sync changes for deleting processed directives.
const syncer::SyncChangeList& sync_changes = change_processor.GetChanges();
ASSERT_EQ(2u, sync_changes.size());
EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, sync_changes[0].change_type());
EXPECT_EQ(1, sync_changes[0].sync_data().GetRemoteId());
EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, sync_changes[1].change_type());
EXPECT_EQ(2, sync_changes[1].sync_data().GetRemoteId());
}
// Create delete directives for time ranges. The expected entries should be
// deleted.
TEST_F(HistoryTest, ProcessTimeRangeDeleteDirective) {
ASSERT_TRUE(history_service_.get());
const GURL test_url("http://www.google.com/");
for (int64 i = 1; i <= 10; ++i) {
base::Time t =
base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(i);
history_service_->AddPage(test_url, t, NULL, 0, GURL(),
history::RedirectList(),
content::PAGE_TRANSITION_LINK,
history::SOURCE_BROWSED, false);
}
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
EXPECT_EQ(10, query_url_row_.visit_count());
syncer::SyncDataList directives;
// 1st directive.
sync_pb::EntitySpecifics entity_specs;
sync_pb::TimeRangeDirective* time_range_directive =
entity_specs.mutable_history_delete_directive()
->mutable_time_range_directive();
time_range_directive->set_start_time_usec(2);
time_range_directive->set_end_time_usec(5);
directives.push_back(syncer::SyncData::CreateRemoteData(1,
entity_specs,
base::Time()));
// 2nd directive.
time_range_directive->Clear();
time_range_directive->set_start_time_usec(8);
time_range_directive->set_end_time_usec(10);
directives.push_back(syncer::SyncData::CreateRemoteData(2,
entity_specs,
base::Time()));
TestChangeProcessor change_processor;
EXPECT_FALSE(
history_service_->MergeDataAndStartSyncing(
syncer::HISTORY_DELETE_DIRECTIVES,
directives,
scoped_ptr<syncer::SyncChangeProcessor>(
new SyncChangeProcessorDelegate(&change_processor)),
scoped_ptr<syncer::SyncErrorFactory>()).error().IsSet());
// Inject a task to check status and keep message loop filled before
// directive processing finishes.
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&CheckDirectiveProcessingResult,
base::Time::Now() + base::TimeDelta::FromSeconds(10),
&change_processor, 2));
base::MessageLoop::current()->RunUntilIdle();
EXPECT_TRUE(QueryURL(history_service_.get(), test_url));
ASSERT_EQ(3, query_url_row_.visit_count());
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(1),
query_url_visits_[0].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(6),
query_url_visits_[1].visit_time);
EXPECT_EQ(base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(7),
query_url_visits_[2].visit_time);
// Expect two sync changes for deleting processed directives.
const syncer::SyncChangeList& sync_changes = change_processor.GetChanges();
ASSERT_EQ(2u, sync_changes.size());
EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, sync_changes[0].change_type());
EXPECT_EQ(1, sync_changes[0].sync_data().GetRemoteId());
EXPECT_EQ(syncer::SyncChange::ACTION_DELETE, sync_changes[1].change_type());
EXPECT_EQ(2, sync_changes[1].sync_data().GetRemoteId());
}
TEST_F(HistoryBackendDBTest, MigratePresentations) {
// Create the db we want. Use 22 since segments didn't change in that time
// frame.
ASSERT_NO_FATAL_FAILURE(CreateDBVersion(22));
const SegmentID segment_id = 2;
const URLID url_id = 3;
const GURL url("http://www.foo.com");
const std::string url_name(VisitSegmentDatabase::ComputeSegmentName(url));
const string16 title(ASCIIToUTF16("Title1"));
const Time segment_time(Time::Now());
{
// Re-open the db for manual manipulation.
sql::Connection db;
ASSERT_TRUE(db.Open(history_dir_.Append(chrome::kHistoryFilename)));
// Add an entry to urls.
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO urls "
"(id, url, title, last_visit_time) VALUES "
"(?, ?, ?, ?)"));
s.BindInt64(0, url_id);
s.BindString(1, url.spec());
s.BindString16(2, title);
s.BindInt64(3, segment_time.ToInternalValue());
ASSERT_TRUE(s.Run());
}
// Add an entry to segments.
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO segments "
"(id, name, url_id, pres_index) VALUES "
"(?, ?, ?, ?)"));
s.BindInt64(0, segment_id);
s.BindString(1, url_name);
s.BindInt64(2, url_id);
s.BindInt(3, 4); // pres_index
ASSERT_TRUE(s.Run());
}
// And one to segment_usage.
{
sql::Statement s(db.GetUniqueStatement(
"INSERT INTO segment_usage "
"(id, segment_id, time_slot, visit_count) VALUES "
"(?, ?, ?, ?)"));
s.BindInt64(0, 4); // id.
s.BindInt64(1, segment_id);
s.BindInt64(2, segment_time.ToInternalValue());
s.BindInt(3, 5); // visit count.
ASSERT_TRUE(s.Run());
}
}
// Re-open the db, triggering migration.
CreateBackendAndDatabase();
std::vector<PageUsageData*> results;
db_->QuerySegmentUsage(segment_time, 10, &results);
ASSERT_EQ(1u, results.size());
EXPECT_EQ(url, results[0]->GetURL());
EXPECT_EQ(segment_id, results[0]->GetID());
EXPECT_EQ(title, results[0]->GetTitle());
STLDeleteElements(&results);
}
} // namespace history