diff options
-rw-r--r-- | components/components_tests.gypi | 5 | ||||
-rw-r--r-- | components/dom_distiller.gypi | 43 | ||||
-rw-r--r-- | components/dom_distiller/DEPS | 3 | ||||
-rw-r--r-- | components/dom_distiller/core/dom_distiller_database.cc | 228 | ||||
-rw-r--r-- | components/dom_distiller/core/dom_distiller_database.h | 144 | ||||
-rw-r--r-- | components/dom_distiller/core/dom_distiller_database_unittest.cc | 305 | ||||
-rw-r--r-- | components/dom_distiller/core/proto/article_entry.proto | 26 | ||||
-rw-r--r-- | components/dom_distiller/webui/DEPS | 2 | ||||
-rw-r--r-- | components/dom_distiller/webui/dom_distiller_ui.h | 4 |
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" |