summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/components_tests.gypi5
-rw-r--r--components/dom_distiller.gypi43
-rw-r--r--components/dom_distiller/DEPS3
-rw-r--r--components/dom_distiller/core/dom_distiller_database.cc228
-rw-r--r--components/dom_distiller/core/dom_distiller_database.h144
-rw-r--r--components/dom_distiller/core/dom_distiller_database_unittest.cc305
-rw-r--r--components/dom_distiller/core/proto/article_entry.proto26
-rw-r--r--components/dom_distiller/webui/DEPS2
-rw-r--r--components/dom_distiller/webui/dom_distiller_ui.h4
9 files changed, 744 insertions, 16 deletions
diff --git a/components/components_tests.gypi b/components/components_tests.gypi
index 312960b0..491e214 100644
--- a/components/components_tests.gypi
+++ b/components/components_tests.gypi
@@ -15,6 +15,7 @@
'auto_login_parser/auto_login_parser_unittest.cc',
'browser_context_keyed_service/browser_context_dependency_manager_unittest.cc',
'browser_context_keyed_service/dependency_graph_unittest.cc',
+ 'dom_distiller/core/dom_distiller_database_unittest.cc',
'json_schema/json_schema_validator_unittest.cc',
'json_schema/json_schema_validator_unittest_base.cc',
'json_schema/json_schema_validator_unittest_base.h',
@@ -48,6 +49,10 @@
# Dependencies of browser_context_keyed_service
'browser_context_keyed_service',
+ # Dependencies of dom_distiller
+ 'dom_distiller_core',
+ 'dom_distiller_core_proto',
+
# Dependencies of encryptor
'encryptor',
diff --git a/components/dom_distiller.gypi b/components/dom_distiller.gypi
index 82d31af..d4f9f44 100644
--- a/components/dom_distiller.gypi
+++ b/components/dom_distiller.gypi
@@ -5,17 +5,6 @@
{
'targets': [
{
- 'target_name': 'dom_distiller_core',
- 'type': 'static_library',
- 'include_dirs': [
- '..',
- ],
- 'sources': [
- 'dom_distiller/core/dom_distiller_constants.cc',
- 'dom_distiller/core/dom_distiller_constants.h',
- ],
- },
- {
'target_name': 'dom_distiller_webui',
'type': 'static_library',
'dependencies': [
@@ -23,7 +12,6 @@
'dom_distiller_core',
'dom_distiller_resources',
'../base/base.gyp:base',
- '../base/base.gyp:base',
'../content/content.gyp:content_browser',
'../skia/skia.gyp:skia',
],
@@ -54,5 +42,36 @@
],
'includes': [ '../build/grit_target.gypi' ],
},
+ {
+ 'target_name': 'dom_distiller_core',
+ 'type': 'static_library',
+ 'dependencies': [
+ 'dom_distiller_core_proto',
+ '../base/base.gyp:base',
+ '../third_party/protobuf/protobuf.gyp:protobuf_lite',
+ '../third_party/leveldatabase/leveldatabase.gyp:leveldatabase',
+ ],
+ 'include_dirs': [
+ '..',
+ ],
+ 'sources': [
+ 'dom_distiller/core/dom_distiller_constants.cc',
+ 'dom_distiller/core/dom_distiller_constants.h',
+ 'dom_distiller/core/dom_distiller_database.cc',
+ 'dom_distiller/core/dom_distiller_database.h',
+ ],
+ },
+ {
+ 'target_name': 'dom_distiller_core_proto',
+ 'type': 'static_library',
+ 'sources': [
+ 'dom_distiller/core/proto/article_entry.proto',
+ ],
+ 'variables': {
+ 'proto_in_dir': 'dom_distiller/core/proto',
+ 'proto_out_dir': 'components/dom_distiller/core/proto',
+ },
+ 'includes': [ '../build/protoc.gypi', ],
+ },
],
}
diff --git a/components/dom_distiller/DEPS b/components/dom_distiller/DEPS
index 66f3cb1..c53eb5f 100644
--- a/components/dom_distiller/DEPS
+++ b/components/dom_distiller/DEPS
@@ -1,5 +1,6 @@
include_rules = [
"+grit", # For generated headers.
+ "+third_party/leveldatabase/src/include",
# The dom distiller is a layered component; subdirectories must explicitly
# introduce the ability to use the content layer as appropriate.
@@ -7,4 +8,4 @@ include_rules = [
"-components/dom_distiller",
"+components/dom_distiller/core",
"-content/public",
-] \ No newline at end of file
+]
diff --git a/components/dom_distiller/core/dom_distiller_database.cc b/components/dom_distiller/core/dom_distiller_database.cc
new file mode 100644
index 0000000..ba85337
--- /dev/null
+++ b/components/dom_distiller/core/dom_distiller_database.cc
@@ -0,0 +1,228 @@
+// Copyright 2013 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 "components/dom_distiller/core/dom_distiller_database.h"
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/message_loop/message_loop.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/string_util.h"
+#include "base/threading/sequenced_worker_pool.h"
+#include "components/dom_distiller/core/proto/article_entry.pb.h"
+#include "third_party/leveldatabase/src/include/leveldb/db.h"
+#include "third_party/leveldatabase/src/include/leveldb/iterator.h"
+#include "third_party/leveldatabase/src/include/leveldb/options.h"
+#include "third_party/leveldatabase/src/include/leveldb/slice.h"
+#include "third_party/leveldatabase/src/include/leveldb/status.h"
+#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
+
+using base::MessageLoop;
+using base::SequencedTaskRunner;
+
+namespace dom_distiller {
+
+DomDistillerDatabase::LevelDB::LevelDB() {}
+
+DomDistillerDatabase::LevelDB::~LevelDB() {}
+
+bool DomDistillerDatabase::LevelDB::Init(const base::FilePath& database_dir) {
+ leveldb::Options options;
+ options.create_if_missing = true;
+ options.max_open_files = 0; // Use minimum.
+
+ std::string path = database_dir.AsUTF8Unsafe();
+
+ leveldb::DB* db = NULL;
+ leveldb::Status status = leveldb::DB::Open(options, path, &db);
+ if (status.IsCorruption()) {
+ LOG(WARNING) << "Deleting possibly-corrupt database";
+ base::DeleteFile(database_dir, true);
+ status = leveldb::DB::Open(options, path, &db);
+ }
+
+ if (status.ok()) {
+ CHECK(db);
+ db_.reset(db);
+ return true;
+ }
+
+ LOG(WARNING) << "Unable to open " << database_dir.value() << ": "
+ << status.ToString();
+ return false;
+}
+
+bool DomDistillerDatabase::LevelDB::Save(const EntryVector& entries) {
+ leveldb::WriteBatch updates;
+ for (EntryVector::const_iterator it = entries.begin(); it != entries.end();
+ ++it) {
+ updates.Put(leveldb::Slice(it->entry_id()),
+ leveldb::Slice(it->SerializeAsString()));
+ }
+
+ leveldb::WriteOptions options;
+ options.sync = true;
+ leveldb::Status status = db_->Write(options, &updates);
+ if (status.ok())
+ return true;
+
+ LOG(WARNING) << "Failed writing dom_distiller entries: " << status.ToString();
+ return false;
+}
+
+bool DomDistillerDatabase::LevelDB::Load(EntryVector* entries) {
+ leveldb::ReadOptions options;
+ scoped_ptr<leveldb::Iterator> db_iterator(db_->NewIterator(options));
+ for (db_iterator->SeekToFirst(); db_iterator->Valid(); db_iterator->Next()) {
+ leveldb::Slice value_slice = db_iterator->value();
+
+ ArticleEntry entry;
+ if (!entry.ParseFromArray(value_slice.data(), value_slice.size())) {
+ LOG(WARNING) << "Unable to parse dom_distiller entry "
+ << db_iterator->key().ToString();
+ // TODO(cjhopman): Decide what to do about un-parseable entries.
+ }
+ entries->push_back(entry);
+ }
+ return true;
+}
+
+DomDistillerDatabase::DomDistillerDatabase(
+ scoped_refptr<base::SequencedTaskRunner> task_runner)
+ : task_runner_(task_runner), weak_ptr_factory_(this) {
+ main_loop_ = MessageLoop::current();
+}
+
+void DomDistillerDatabase::Destroy() {
+ DCHECK(IsRunOnMainLoop());
+ weak_ptr_factory_.InvalidateWeakPtrs();
+ task_runner_->PostNonNestableTask(
+ FROM_HERE,
+ base::Bind(&DomDistillerDatabase::DestroyFromTaskRunner,
+ base::Unretained(this)));
+}
+
+void DomDistillerDatabase::Init(const base::FilePath& database_dir,
+ InitCallback callback) {
+ InitWithDatabase(scoped_ptr<Database>(new LevelDB()), database_dir, callback);
+}
+
+namespace {
+
+void RunInitCallback(DomDistillerDatabaseInterface::InitCallback callback,
+ const bool* success) {
+ callback.Run(*success);
+}
+
+void RunSaveCallback(DomDistillerDatabaseInterface::SaveCallback callback,
+ const bool* success) {
+ callback.Run(*success);
+}
+
+void RunLoadCallback(DomDistillerDatabaseInterface::LoadCallback callback,
+ const bool* success,
+ scoped_ptr<EntryVector> entries) {
+ callback.Run(*success, entries.Pass());
+}
+
+} // namespace
+
+void DomDistillerDatabase::InitWithDatabase(scoped_ptr<Database> database,
+ const base::FilePath& database_dir,
+ InitCallback callback) {
+ DCHECK(IsRunOnMainLoop());
+ DCHECK(!db_);
+ DCHECK(database);
+ db_.reset(database.release());
+ bool* success = new bool(false);
+ task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::Bind(&DomDistillerDatabase::InitFromTaskRunner,
+ base::Unretained(this),
+ database_dir,
+ success),
+ base::Bind(RunInitCallback, callback, base::Owned(success)));
+}
+
+void DomDistillerDatabase::SaveEntries(scoped_ptr<EntryVector> entries,
+ SaveCallback callback) {
+ DCHECK(IsRunOnMainLoop());
+ bool* success = new bool(false);
+ task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::Bind(&DomDistillerDatabase::SaveEntriesFromTaskRunner,
+ base::Unretained(this),
+ base::Passed(&entries),
+ success),
+ base::Bind(RunSaveCallback, callback, base::Owned(success)));
+}
+
+void DomDistillerDatabase::LoadEntries(LoadCallback callback) {
+ DCHECK(IsRunOnMainLoop());
+
+ bool* success = new bool(false);
+
+ scoped_ptr<EntryVector> entries(new EntryVector());
+ // Get this pointer before entries is base::Passed() so we can use it below.
+ EntryVector* entries_ptr = entries.get();
+
+ task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::Bind(&DomDistillerDatabase::LoadEntriesFromTaskRunner,
+ base::Unretained(this),
+ entries_ptr,
+ success),
+ base::Bind(RunLoadCallback,
+ callback,
+ base::Owned(success),
+ base::Passed(&entries)));
+}
+
+DomDistillerDatabase::~DomDistillerDatabase() { DCHECK(IsRunByTaskRunner()); }
+
+bool DomDistillerDatabase::IsRunByTaskRunner() const {
+ return task_runner_->RunsTasksOnCurrentThread();
+}
+
+bool DomDistillerDatabase::IsRunOnMainLoop() const {
+ return MessageLoop::current() == main_loop_;
+}
+
+void DomDistillerDatabase::DestroyFromTaskRunner() {
+ DCHECK(IsRunByTaskRunner());
+ delete this;
+}
+
+void DomDistillerDatabase::InitFromTaskRunner(
+ const base::FilePath& database_dir,
+ bool* success) {
+ DCHECK(IsRunByTaskRunner());
+ DCHECK(success);
+
+ VLOG(1) << "Opening " << database_dir.value();
+
+ // TODO(cjhopman): Histogram for database size.
+ *success = db_->Init(database_dir);
+}
+
+void DomDistillerDatabase::SaveEntriesFromTaskRunner(
+ scoped_ptr<EntryVector> entries,
+ bool* success) {
+ DCHECK(IsRunByTaskRunner());
+ DCHECK(success);
+ VLOG(1) << "Saving " << entries->size() << " entry(ies) to database ";
+ *success = db_->Save(*entries);
+}
+
+void DomDistillerDatabase::LoadEntriesFromTaskRunner(EntryVector* entries,
+ bool* success) {
+ DCHECK(IsRunByTaskRunner());
+ DCHECK(success);
+ DCHECK(entries);
+
+ entries->clear();
+ *success = db_->Load(entries);
+}
+
+} // namespace dom_distiller
diff --git a/components/dom_distiller/core/dom_distiller_database.h b/components/dom_distiller/core/dom_distiller_database.h
new file mode 100644
index 0000000..2fb06c6
--- /dev/null
+++ b/components/dom_distiller/core/dom_distiller_database.h
@@ -0,0 +1,144 @@
+// Copyright 2013 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.
+
+#ifndef COMPONENTS_DOM_DISTILLER_CORE_DOM_DISTILLER_DATABASE_H_
+#define COMPONENTS_DOM_DISTILLER_CORE_DOM_DISTILLER_DATABASE_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/memory/weak_ptr.h"
+#include "base/message_loop/message_loop.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace leveldb {
+class DB;
+}
+
+namespace dom_distiller {
+
+class ArticleEntry;
+typedef std::vector<ArticleEntry> EntryVector;
+
+// Interface for classes providing persistent storage of DomDistiller entries.
+class DomDistillerDatabaseInterface {
+ public:
+ typedef std::vector<std::string> ArticleEntryIds;
+ typedef base::Callback<void(bool success)> InitCallback;
+ typedef base::Callback<void(bool success)> SaveCallback;
+ typedef base::Callback<void(bool success, scoped_ptr<EntryVector>)>
+ LoadCallback;
+
+ // Asynchronously destroys the object after all in-progress file operations
+ // have completed. The callbacks for in-progress operations will still be
+ // called.
+ virtual void Destroy() {}
+
+ // Asynchronously initializes the object. |callback| will be invoked on the UI
+ // thread when complete.
+ virtual void Init(const base::FilePath& database_dir,
+ InitCallback callback) = 0;
+
+ // Asynchronously saves |entries_to_save| database. |callback| will be invoked
+ // on the UI thread when complete.
+ virtual void SaveEntries(scoped_ptr<EntryVector> entries_to_save,
+ SaveCallback callback) = 0;
+
+ // Asynchronously loads all entries from the database and invokes |callback|
+ // when complete.
+ virtual void LoadEntries(LoadCallback callback) = 0;
+
+ protected:
+ virtual ~DomDistillerDatabaseInterface() {}
+};
+
+class DomDistillerDatabase
+ : public DomDistillerDatabaseInterface {
+ public:
+ // The underlying database. Calls to this type may be blocking.
+ class Database {
+ public:
+ virtual bool Init(const base::FilePath& database_dir) = 0;
+ virtual bool Save(const EntryVector& entries) = 0;
+ virtual bool Load(EntryVector* entries) = 0;
+ virtual ~Database() {}
+ };
+
+ class LevelDB : public Database {
+ public:
+ LevelDB();
+ virtual ~LevelDB();
+ virtual bool Init(const base::FilePath& database_dir) OVERRIDE;
+ virtual bool Save(const EntryVector& entries) OVERRIDE;
+ virtual bool Load(EntryVector* entries) OVERRIDE;
+
+ private:
+
+ scoped_ptr<leveldb::DB> db_;
+ };
+
+ DomDistillerDatabase(scoped_refptr<base::SequencedTaskRunner> task_runner);
+
+ // DomDistillerDatabaseInterface implementation.
+ virtual void Destroy() OVERRIDE;
+ virtual void Init(const base::FilePath& database_dir,
+ InitCallback callback) OVERRIDE;
+ virtual void SaveEntries(scoped_ptr<EntryVector> entries_to_save,
+ SaveCallback callback) OVERRIDE;
+ virtual void LoadEntries(LoadCallback callback) OVERRIDE;
+
+ // Allow callers to provide their own Database implementation.
+ void InitWithDatabase(scoped_ptr<Database> database,
+ const base::FilePath& database_dir,
+ InitCallback callback);
+
+ protected:
+ virtual ~DomDistillerDatabase();
+
+ private:
+ // Whether currently being run by |task_runner_|.
+ bool IsRunByTaskRunner() const;
+
+ // Whether currently being run on |main_loop_|.
+ bool IsRunOnMainLoop() const;
+
+ // Deletes |this|.
+ void DestroyFromTaskRunner();
+
+ // Initializes the database in |database_dir| and updates |success|.
+ void InitFromTaskRunner(const base::FilePath& database_dir, bool* success);
+
+ // Saves data to disk and updates |success|.
+ void SaveEntriesFromTaskRunner(scoped_ptr<EntryVector> entries_to_save,
+ bool* success);
+
+ // Loads entries from disk and updates |success|.
+ void LoadEntriesFromTaskRunner(EntryVector* entries, bool* success);
+
+ // The MessageLoop that the database was created on.
+ base::MessageLoop* main_loop_;
+
+ // Used to run blocking tasks in-order.
+ scoped_refptr<base::SequencedTaskRunner> task_runner_;
+
+ scoped_ptr<Database> db_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<DomDistillerDatabase> weak_ptr_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(DomDistillerDatabase);
+};
+
+} // namespace dom_distiller
+
+#endif // COMPONENTS_DOM_DISTILLER_CORE_DOM_DISTILLER_DATABASE_H_
diff --git a/components/dom_distiller/core/dom_distiller_database_unittest.cc b/components/dom_distiller/core/dom_distiller_database_unittest.cc
new file mode 100644
index 0000000..9065968
--- /dev/null
+++ b/components/dom_distiller/core/dom_distiller_database_unittest.cc
@@ -0,0 +1,305 @@
+// Copyright 2013 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 "components/dom_distiller/core/dom_distiller_database.h"
+
+#include <map>
+
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "components/dom_distiller/core/proto/article_entry.pb.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::MessageLoop;
+using base::ScopedTempDir;
+using testing::Invoke;
+using testing::Return;
+using testing::_;
+
+namespace dom_distiller {
+
+namespace {
+
+typedef std::map<std::string, ArticleEntry> EntryMap;
+
+class MockDB : public DomDistillerDatabase::Database {
+ public:
+ MOCK_METHOD1(Init, bool(const base::FilePath&));
+ MOCK_METHOD1(Save, bool(const EntryVector&));
+ MOCK_METHOD1(Load, bool(EntryVector*));
+
+ MockDB() {
+ ON_CALL(*this, Init(_)).WillByDefault(Return(true));
+ ON_CALL(*this, Save(_)).WillByDefault(Return(true));
+ ON_CALL(*this, Load(_)).WillByDefault(Return(true));
+ }
+
+ bool LoadEntries(EntryVector* entries);
+};
+
+class MockDatabaseCaller {
+ public:
+ MOCK_METHOD1(InitCallback, void(bool));
+ MOCK_METHOD1(SaveCallback, void(bool));
+ void LoadCallback(bool success, scoped_ptr<EntryVector> entries) {
+ LoadCallback1(success, entries.get());
+ }
+ MOCK_METHOD2(LoadCallback1, void(bool, EntryVector*));
+};
+
+} // namespace
+
+EntryMap GetSmallModel() {
+ EntryMap model;
+
+ model["key0"].set_entry_id("key0");
+ model["key0"].add_pages()->set_url("http://foo.com/1");
+ model["key0"].add_pages()->set_url("http://foo.com/2");
+ model["key0"].add_pages()->set_url("http://foo.com/3");
+
+ model["key1"].set_entry_id("key1");
+ model["key1"].add_pages()->set_url("http://bar.com/all");
+
+ model["key2"].set_entry_id("key2");
+ model["key2"].add_pages()->set_url("http://baz.com/1");
+
+ return model;
+}
+
+void ExpectEntryPointersEquals(EntryMap expected, const EntryVector& actual) {
+ EXPECT_EQ(expected.size(), actual.size());
+ for (size_t i = 0; i < actual.size(); i++) {
+ EntryMap::iterator expected_it =
+ expected.find(std::string(actual[i].entry_id()));
+ EXPECT_TRUE(expected_it != expected.end());
+ std::string serialized_expected = expected_it->second.SerializeAsString();
+ std::string serialized_actual = actual[i].SerializeAsString();
+ EXPECT_EQ(serialized_expected, serialized_actual);
+ expected.erase(expected_it);
+ }
+}
+
+class DomDistillerDatabaseTest : public testing::Test {
+ public:
+ virtual void SetUp() {
+ main_loop_.reset(new MessageLoop());
+ db_ = new DomDistillerDatabase(main_loop_->message_loop_proxy());
+ }
+
+ virtual void TearDown() {
+ DestroyDB();
+ main_loop_.reset(NULL);
+ }
+
+ void DestroyDB() {
+ if (db_) {
+ db_->Destroy();
+ base::RunLoop().RunUntilIdle();
+ db_ = NULL;
+ }
+ }
+
+ DomDistillerDatabase* db_;
+ scoped_ptr<MessageLoop> main_loop_;
+};
+
+// Test that DomDistillerDatabase calls Init on the underlying database and that
+// the caller's InitCallback is called with the correct value.
+TEST_F(DomDistillerDatabaseTest, TestDBInitSuccess) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ EXPECT_CALL(*mock_db, Init(path)).WillOnce(Return(true));
+
+ MockDatabaseCaller caller;
+ EXPECT_CALL(caller, InitCallback(true));
+
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(DomDistillerDatabaseTest, TestDBInitFailure) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ EXPECT_CALL(*mock_db, Init(path)).WillOnce(Return(false));
+
+ MockDatabaseCaller caller;
+ EXPECT_CALL(caller, InitCallback(false));
+
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+ACTION_P(AppendLoadEntries, model) {
+ EntryVector* output = arg0;
+ for (EntryMap::const_iterator it = model.begin(); it != model.end(); ++it) {
+ output->push_back(it->second);
+ }
+ return true;
+}
+
+ACTION_P(VerifyLoadEntries, expected) {
+ EntryVector* actual = arg1;
+ ExpectEntryPointersEquals(expected, *actual);
+}
+
+// Test that DomDistillerDatabase calls Load on the underlying database and that
+// the caller's LoadCallback is called with the correct success value. Also
+// confirms that on success, the expected entries are passed to the caller's
+// LoadCallback.
+TEST_F(DomDistillerDatabaseTest, TestDBLoadSuccess) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ MockDatabaseCaller caller;
+ EntryMap model = GetSmallModel();
+
+ EXPECT_CALL(*mock_db, Init(_));
+ EXPECT_CALL(caller, InitCallback(_));
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ EXPECT_CALL(*mock_db, Load(_)).WillOnce(AppendLoadEntries(model));
+ EXPECT_CALL(caller, LoadCallback1(true, _))
+ .WillOnce(VerifyLoadEntries(testing::ByRef(model)));
+ db_->LoadEntries(
+ base::Bind(&MockDatabaseCaller::LoadCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(DomDistillerDatabaseTest, TestDBLoadFailure) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ MockDatabaseCaller caller;
+
+ EXPECT_CALL(*mock_db, Init(_));
+ EXPECT_CALL(caller, InitCallback(_));
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ EXPECT_CALL(*mock_db, Load(_)).WillOnce(Return(false));
+ EXPECT_CALL(caller, LoadCallback1(false, _));
+ db_->LoadEntries(
+ base::Bind(&MockDatabaseCaller::LoadCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+ACTION_P(VerifySaveEntries, expected) {
+ const EntryVector& actual = arg0;
+ ExpectEntryPointersEquals(expected, actual);
+ return true;
+}
+
+// Test that DomDistillerDatabase calls Save on the underlying database with the
+// correct entries to save and that the caller's SaveCallback is called with the
+// correct success value.
+TEST_F(DomDistillerDatabaseTest, TestDBSaveSuccess) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ MockDatabaseCaller caller;
+ EntryMap model = GetSmallModel();
+
+ EXPECT_CALL(*mock_db, Init(_));
+ EXPECT_CALL(caller, InitCallback(_));
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ scoped_ptr<EntryVector> entries(new EntryVector());
+ for (EntryMap::iterator it = model.begin(); it != model.end(); ++it) {
+ entries->push_back(it->second);
+ }
+
+ EXPECT_CALL(*mock_db, Save(_)).WillOnce(VerifySaveEntries(model));
+ EXPECT_CALL(caller, SaveCallback(true));
+ db_->SaveEntries(
+ entries.Pass(),
+ base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+TEST_F(DomDistillerDatabaseTest, TestDBSaveFailure) {
+ base::FilePath path(FILE_PATH_LITERAL("/fake/path"));
+
+ MockDB* mock_db = new MockDB();
+ MockDatabaseCaller caller;
+ scoped_ptr<EntryVector> entries(new EntryVector());
+
+ EXPECT_CALL(*mock_db, Init(_));
+ EXPECT_CALL(caller, InitCallback(_));
+ db_->InitWithDatabase(
+ scoped_ptr<DomDistillerDatabase::Database>(mock_db),
+ base::FilePath(path),
+ base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller)));
+
+ EXPECT_CALL(*mock_db, Save(_)).WillOnce(Return(false));
+ EXPECT_CALL(caller, SaveCallback(false));
+ db_->SaveEntries(
+ entries.Pass(),
+ base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller)));
+
+ base::RunLoop().RunUntilIdle();
+}
+
+// Test that the LevelDB properly saves entries and that load returns the saved
+// entries. If |close_after_save| is true, the database will be closed after
+// saving and then re-opened to ensure that the data is properly persisted.
+void TestLevelDBSaveAndLoad(bool close_after_save) {
+ ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+ EntryMap model = GetSmallModel();
+ EntryVector save_entries;
+ EntryVector load_entries;
+
+ for (EntryMap::iterator it = model.begin(); it != model.end(); ++it) {
+ save_entries.push_back(it->second);
+ }
+
+ scoped_ptr<DomDistillerDatabase::LevelDB> db(
+ new DomDistillerDatabase::LevelDB());
+ EXPECT_TRUE(db->Init(temp_dir.path()));
+ EXPECT_TRUE(db->Save(save_entries));
+
+ if (close_after_save) {
+ db.reset(new DomDistillerDatabase::LevelDB());
+ EXPECT_TRUE(db->Init(temp_dir.path()));
+ }
+
+ EXPECT_TRUE(db->Load(&load_entries));
+
+ ExpectEntryPointersEquals(model, load_entries);
+}
+
+TEST(DomDistillerDatabaseLevelDBTest, TestDBSaveAndLoad) {
+ TestLevelDBSaveAndLoad(false);
+}
+
+TEST(DomDistillerDatabaseLevelDBTest, TestDBCloseAndReopen) {
+ TestLevelDBSaveAndLoad(true);
+}
+
+} // namespace dom_distiller
diff --git a/components/dom_distiller/core/proto/article_entry.proto b/components/dom_distiller/core/proto/article_entry.proto
new file mode 100644
index 0000000..70b769d
--- /dev/null
+++ b/components/dom_distiller/core/proto/article_entry.proto
@@ -0,0 +1,26 @@
+// Copyright 2013 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.
+//
+// Protocol buffer definition for a DomDistiller entry.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package dom_distiller;
+
+message ArticleEntry {
+ // Next ID to use: 3
+
+ optional string entry_id = 1;
+
+ message Page {
+ // Next ID to use: 2
+
+ optional string url = 1;
+ }
+
+ repeated Page pages = 2;
+}
+
diff --git a/components/dom_distiller/webui/DEPS b/components/dom_distiller/webui/DEPS
index 2c43d7a..db46e2e 100644
--- a/components/dom_distiller/webui/DEPS
+++ b/components/dom_distiller/webui/DEPS
@@ -10,4 +10,4 @@ include_rules = [
# approach directly allowing the header files that are currently included from
# content.
"+content/public/browser",
-] \ No newline at end of file
+]
diff --git a/components/dom_distiller/webui/dom_distiller_ui.h b/components/dom_distiller/webui/dom_distiller_ui.h
index 762b236..1b9f1c2 100644
--- a/components/dom_distiller/webui/dom_distiller_ui.h
+++ b/components/dom_distiller/webui/dom_distiller_ui.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef COMPONENTS_DOM_DISTILLER_WEBUI_DOM_DISTILLER_UI_H_
-#define COMPONENTS_DOM_DISTILLER_WEBUI_DOM_DISTILLER_UI_H_
+#ifndef COMPONENTS_DOM_DISTILLER_WEBUI_DOM_DISTILLER_H_
+#define COMPONENTS_DOM_DISTILLER_WEBUI_DOM_DISTILLER_H_
#include "content/public/browser/web_ui_controller.h"