diff options
author | mathp@chromium.org <mathp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-18 16:22:30 +0000 |
---|---|---|
committer | mathp@chromium.org <mathp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-18 16:22:30 +0000 |
commit | 2b894b8dae83b4bada9199713504aeb52cdfc8ed (patch) | |
tree | f527ed157c42675be946635c08a81288863b4ef5 /components/leveldb_proto | |
parent | ab4f95a66b2e01a329562d2f7a2b4445b0e4ee47 (diff) | |
download | chromium_src-2b894b8dae83b4bada9199713504aeb52cdfc8ed.zip chromium_src-2b894b8dae83b4bada9199713504aeb52cdfc8ed.tar.gz chromium_src-2b894b8dae83b4bada9199713504aeb52cdfc8ed.tar.bz2 |
Extract protobuf database into a new 'leveldb_proto' component
Code extracted from components/dom_distiller/core/dom_distiller_database.*
Slight API change: callers to UpdateEntries now have to pass a vector of (string, proto) as key and value, instead of just a vector of protos where key is derived.
Ran clang-format on the files I touched so you may see some diffs.
Note: Implementations are in proto_database_impl.h and fake_db.h for proper linking.
BUG=385747
TBR=jochen,dgrogan
TEST=DomDistiller*,ProtoDatabaseImplTest
Review URL: https://codereview.chromium.org/330833002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@278096 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'components/leveldb_proto')
-rw-r--r-- | components/leveldb_proto/DEPS | 3 | ||||
-rw-r--r-- | components/leveldb_proto/OWNERS | 2 | ||||
-rw-r--r-- | components/leveldb_proto/README | 6 | ||||
-rw-r--r-- | components/leveldb_proto/leveldb_database.cc | 93 | ||||
-rw-r--r-- | components/leveldb_proto/leveldb_database.h | 42 | ||||
-rw-r--r-- | components/leveldb_proto/proto_database.h | 52 | ||||
-rw-r--r-- | components/leveldb_proto/proto_database_impl.h | 204 | ||||
-rw-r--r-- | components/leveldb_proto/proto_database_impl_unittest.cc | 397 | ||||
-rw-r--r-- | components/leveldb_proto/testing/fake_db.h | 146 | ||||
-rw-r--r-- | components/leveldb_proto/testing/proto/test.proto | 20 |
10 files changed, 965 insertions, 0 deletions
diff --git a/components/leveldb_proto/DEPS b/components/leveldb_proto/DEPS new file mode 100644 index 0000000..19109d2 --- /dev/null +++ b/components/leveldb_proto/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+third_party/leveldatabase/src/include", +] diff --git a/components/leveldb_proto/OWNERS b/components/leveldb_proto/OWNERS new file mode 100644 index 0000000..480350b --- /dev/null +++ b/components/leveldb_proto/OWNERS @@ -0,0 +1,2 @@ +cjhopman@chromium.org +mathp@chromium.org diff --git a/components/leveldb_proto/README b/components/leveldb_proto/README new file mode 100644 index 0000000..6d255eb --- /dev/null +++ b/components/leveldb_proto/README @@ -0,0 +1,6 @@ +This component provides a database that can be used to store Protocol Buffers. + +It is based on LevelDB[1]. In this implementation, keys are strings chosen by +the developer, and values are serialized Protocol Buffers. + +[1] https://code.google.com/p/leveldb/ diff --git a/components/leveldb_proto/leveldb_database.cc b/components/leveldb_proto/leveldb_database.cc new file mode 100644 index 0000000..811ae1e --- /dev/null +++ b/components/leveldb_proto/leveldb_database.cc @@ -0,0 +1,93 @@ +// Copyright 2014 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/leveldb_proto/leveldb_database.h" + +#include <string> +#include <vector> + +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.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" + +namespace leveldb_proto { + +LevelDB::LevelDB() {} + +LevelDB::~LevelDB() { DFAKE_SCOPED_LOCK(thread_checker_); } + +bool LevelDB::Init(const base::FilePath& database_dir) { + DFAKE_SCOPED_LOCK(thread_checker_); + + 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()) { + 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 LevelDB::Save( + const std::vector<std::pair<std::string, std::string> >& entries_to_save, + const std::vector<std::string>& keys_to_remove) { + DFAKE_SCOPED_LOCK(thread_checker_); + + leveldb::WriteBatch updates; + for (std::vector<std::pair<std::string, std::string> >::const_iterator it = + entries_to_save.begin(); + it != entries_to_save.end(); ++it) { + updates.Put(leveldb::Slice(it->first), leveldb::Slice(it->second)); + } + for (std::vector<std::string>::const_iterator it = keys_to_remove.begin(); + it != keys_to_remove.end(); ++it) { + updates.Delete(leveldb::Slice(*it)); + } + + leveldb::WriteOptions options; + options.sync = true; + leveldb::Status status = db_->Write(options, &updates); + if (status.ok()) return true; + + DLOG(WARNING) << "Failed writing leveldb_proto entries: " + << status.ToString(); + return false; +} + +bool LevelDB::Load(std::vector<std::string>* entries) { + DFAKE_SCOPED_LOCK(thread_checker_); + + 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(); + std::string entry(value_slice.data(), value_slice.size()); + entries->push_back(entry); + } + return true; +} + +} // namespace leveldb_proto diff --git a/components/leveldb_proto/leveldb_database.h b/components/leveldb_proto/leveldb_database.h new file mode 100644 index 0000000..16d3c30 --- /dev/null +++ b/components/leveldb_proto/leveldb_database.h @@ -0,0 +1,42 @@ +// Copyright 2014 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_LEVELDB_PROTO_LEVELDB_DATABASE_H_ +#define COMPONENTS_LEVELDB_PROTO_LEVELDB_DATABASE_H_ + +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_collision_warner.h" + +namespace leveldb { +class DB; +} // namespace leveldb + +namespace leveldb_proto { + +// Interacts with the LevelDB third party module. +// Once constructed, function calls and destruction should all occur on the +// same thread (not necessarily the same as the constructor). +class LevelDB { + public: + LevelDB(); + virtual ~LevelDB(); + + virtual bool Init(const base::FilePath& database_dir); + virtual bool Save( + const std::vector<std::pair<std::string, std::string> >& pairs_to_save, + const std::vector<std::string>& keys_to_remove); + virtual bool Load(std::vector<std::string>* entries); + + private: + DFAKE_MUTEX(thread_checker_); + scoped_ptr<leveldb::DB> db_; +}; + +} // namespace leveldb_proto + +#endif // COMPONENTS_LEVELDB_PROTO_LEVELDB_DATABASE_H_ diff --git a/components/leveldb_proto/proto_database.h b/components/leveldb_proto/proto_database.h new file mode 100644 index 0000000..34ee799 --- /dev/null +++ b/components/leveldb_proto/proto_database.h @@ -0,0 +1,52 @@ +// Copyright 2014 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_LEVELDB_PROTO_PROTO_DATABASE_H_ +#define COMPONENTS_LEVELDB_PROTO_PROTO_DATABASE_H_ + +#include <string> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" + +namespace leveldb_proto { + +// Interface for classes providing persistent storage of Protocol Buffer +// entries (T must be a Proto type extending MessageLite). +template <typename T> +class ProtoDatabase { + public: + typedef base::Callback<void(bool success)> InitCallback; + typedef base::Callback<void(bool success)> UpdateCallback; + typedef base::Callback<void(bool success, scoped_ptr<std::vector<T> >)> + LoadCallback; + // A list of key-value (string, T) tuples. + typedef std::vector<std::pair<std::string, T> > KeyEntryVector; + + virtual ~ProtoDatabase() {} + + // Asynchronously initializes the object. |callback| will be invoked on the + // calling thread when complete. + virtual void Init(const base::FilePath& database_dir, + InitCallback callback) = 0; + + // Asynchronously saves |entries_to_save| and deletes entries from + // |keys_to_remove| from the database. |callback| will be invoked on the + // calling thread when complete. + virtual void UpdateEntries( + scoped_ptr<KeyEntryVector> entries_to_save, + scoped_ptr<std::vector<std::string> > keys_to_remove, + UpdateCallback callback) = 0; + + // Asynchronously loads all entries from the database and invokes |callback| + // when complete. + virtual void LoadEntries(LoadCallback callback) = 0; +}; + +} // namespace leveldb_proto + +#endif // COMPONENTS_LEVELDB_PROTO_PROTO_DATABASE_H_ diff --git a/components/leveldb_proto/proto_database_impl.h b/components/leveldb_proto/proto_database_impl.h new file mode 100644 index 0000000..1b1c740 --- /dev/null +++ b/components/leveldb_proto/proto_database_impl.h @@ -0,0 +1,204 @@ +// Copyright 2014 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_LEVELDB_PROTO_PROTO_DATABASE_IMPL_H_ +#define COMPONENTS_LEVELDB_PROTO_PROTO_DATABASE_IMPL_H_ + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.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 "base/threading/thread_checker.h" +#include "components/leveldb_proto/leveldb_database.h" +#include "components/leveldb_proto/proto_database.h" + +namespace leveldb_proto { + +typedef std::vector<std::pair<std::string, std::string> > KeyValueVector; +typedef std::vector<std::string> KeyVector; + +// When the ProtoDatabaseImpl instance is deleted, in-progress asynchronous +// operations will be completed and the corresponding callbacks will be called. +// Construction/calls/destruction should all happen on the same thread. +template <typename T> +class ProtoDatabaseImpl : public ProtoDatabase<T> { + public: + // All blocking calls/disk access will happen on the provided |task_runner|. + explicit ProtoDatabaseImpl( + scoped_refptr<base::SequencedTaskRunner> task_runner); + + virtual ~ProtoDatabaseImpl(); + + // ProtoDatabase implementation. + // TODO(cjhopman): Perhaps Init() shouldn't be exposed to users and not just + // part of the constructor + virtual void Init(const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback) OVERRIDE; + virtual void UpdateEntries( + scoped_ptr<typename ProtoDatabase<T>::KeyEntryVector> entries_to_save, + scoped_ptr<KeyVector> keys_to_remove, + typename ProtoDatabase<T>::UpdateCallback callback) OVERRIDE; + virtual void LoadEntries( + typename ProtoDatabase<T>::LoadCallback callback) OVERRIDE; + + // Allow callers to provide their own Database implementation. + void InitWithDatabase(scoped_ptr<LevelDB> database, + const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback); + + private: + base::ThreadChecker thread_checker_; + + // Used to run blocking tasks in-order. + scoped_refptr<base::SequencedTaskRunner> task_runner_; + + scoped_ptr<LevelDB> db_; + + DISALLOW_COPY_AND_ASSIGN(ProtoDatabaseImpl); +}; + +namespace { + +template <typename T> +void RunInitCallback(typename ProtoDatabase<T>::InitCallback callback, + const bool* success) { + callback.Run(*success); +} + +template <typename T> +void RunUpdateCallback(typename ProtoDatabase<T>::UpdateCallback callback, + const bool* success) { + callback.Run(*success); +} + +template <typename T> +void RunLoadCallback(typename ProtoDatabase<T>::LoadCallback callback, + const bool* success, scoped_ptr<std::vector<T> > entries) { + callback.Run(*success, entries.Pass()); +} + +void InitFromTaskRunner(LevelDB* database, const base::FilePath& database_dir, + bool* success) { + DCHECK(success); + + // TODO(cjhopman): Histogram for database size. + *success = database->Init(database_dir); +} + +template <typename T> +void UpdateEntriesFromTaskRunner( + LevelDB* database, + scoped_ptr<typename ProtoDatabase<T>::KeyEntryVector> entries_to_save, + scoped_ptr<KeyVector> keys_to_remove, bool* success) { + DCHECK(success); + // Serialize the values from Proto to string before passing on to database. + KeyValueVector pairs_to_save; + for (typename ProtoDatabase<T>::KeyEntryVector::iterator it = + entries_to_save->begin(); + it != entries_to_save->end(); ++it) { + pairs_to_save.push_back( + std::make_pair(it->first, it->second.SerializeAsString())); + } + *success = database->Save(pairs_to_save, *keys_to_remove); +} + +template <typename T> +void LoadEntriesFromTaskRunner(LevelDB* database, std::vector<T>* entries, + bool* success) { + DCHECK(success); + DCHECK(entries); + + entries->clear(); + std::vector<std::string> loaded_entries; + *success = database->Load(&loaded_entries); + for (std::vector<std::string>::iterator it = loaded_entries.begin(); + it != loaded_entries.end(); ++it) { + T entry; + if (!entry.ParseFromString(*it)) { + DLOG(WARNING) << "Unable to parse leveldb_proto entry " << *it; + // TODO(cjhopman): Decide what to do about un-parseable entries. + } + entries->push_back(entry); + } +} + +} // namespace + +template <typename T> +ProtoDatabaseImpl<T>::ProtoDatabaseImpl( + scoped_refptr<base::SequencedTaskRunner> task_runner) + : task_runner_(task_runner) {} + +template <typename T> +ProtoDatabaseImpl<T>::~ProtoDatabaseImpl() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (!task_runner_->DeleteSoon(FROM_HERE, db_.release())) { + DLOG(WARNING) << "DOM distiller database will not be deleted."; + } +} + +template <typename T> +void ProtoDatabaseImpl<T>::Init( + const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + InitWithDatabase(scoped_ptr<LevelDB>(new LevelDB()), database_dir, callback); +} + +template <typename T> +void ProtoDatabaseImpl<T>::InitWithDatabase( + scoped_ptr<LevelDB> database, const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!db_); + DCHECK(database); + db_.reset(database.release()); + bool* success = new bool(false); + task_runner_->PostTaskAndReply( + FROM_HERE, base::Bind(InitFromTaskRunner, base::Unretained(db_.get()), + database_dir, success), + base::Bind(RunInitCallback<T>, callback, base::Owned(success))); +} + +template <typename T> +void ProtoDatabaseImpl<T>::UpdateEntries( + scoped_ptr<typename ProtoDatabase<T>::KeyEntryVector> entries_to_save, + scoped_ptr<KeyVector> keys_to_remove, + typename ProtoDatabase<T>::UpdateCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + bool* success = new bool(false); + task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(UpdateEntriesFromTaskRunner<T>, base::Unretained(db_.get()), + base::Passed(&entries_to_save), base::Passed(&keys_to_remove), + success), + base::Bind(RunUpdateCallback<T>, callback, base::Owned(success))); +} + +template <typename T> +void ProtoDatabaseImpl<T>::LoadEntries( + typename ProtoDatabase<T>::LoadCallback callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + bool* success = new bool(false); + + scoped_ptr<std::vector<T> > entries(new std::vector<T>()); + // Get this pointer before entries is base::Passed() so we can use it below. + std::vector<T>* entries_ptr = entries.get(); + + task_runner_->PostTaskAndReply( + FROM_HERE, base::Bind(LoadEntriesFromTaskRunner<T>, + base::Unretained(db_.get()), entries_ptr, success), + base::Bind(RunLoadCallback<T>, callback, base::Owned(success), + base::Passed(&entries))); +} + +} // namespace leveldb_proto + +#endif // COMPONENTS_LEVELDB_PROTO_PROTO_DATABASE_IMPL_H_ diff --git a/components/leveldb_proto/proto_database_impl_unittest.cc b/components/leveldb_proto/proto_database_impl_unittest.cc new file mode 100644 index 0000000..073328b --- /dev/null +++ b/components/leveldb_proto/proto_database_impl_unittest.cc @@ -0,0 +1,397 @@ +// Copyright 2014 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/leveldb_proto/proto_database_impl.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 "base/threading/thread.h" +#include "components/leveldb_proto/leveldb_database.h" +#include "components/leveldb_proto/testing/proto/test.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 leveldb_proto { + +namespace { + +typedef std::map<std::string, TestProto> EntryMap; + +class MockDB : public LevelDB { + public: + MOCK_METHOD1(Init, bool(const base::FilePath&)); + MOCK_METHOD2(Save, bool(const KeyValueVector&, const KeyVector&)); + MOCK_METHOD1(Load, bool(std::vector<std::string>*)); + + MockDB() { + ON_CALL(*this, Init(_)).WillByDefault(Return(true)); + ON_CALL(*this, Save(_, _)).WillByDefault(Return(true)); + ON_CALL(*this, Load(_)).WillByDefault(Return(true)); + } +}; + +class MockDatabaseCaller { + public: + MOCK_METHOD1(InitCallback, void(bool)); + MOCK_METHOD1(SaveCallback, void(bool)); + void LoadCallback(bool success, scoped_ptr<std::vector<TestProto> > entries) { + LoadCallback1(success, entries.get()); + } + MOCK_METHOD2(LoadCallback1, void(bool, std::vector<TestProto>*)); +}; + +} // namespace + +EntryMap GetSmallModel() { + EntryMap model; + + model["0"].set_id("0"); + model["0"].set_data("http://foo.com/1"); + + model["1"].set_id("1"); + model["1"].set_data("http://bar.com/all"); + + model["2"].set_id("2"); + model["2"].set_data("http://baz.com/1"); + + return model; +} + +void ExpectEntryPointersEquals(EntryMap expected, + const std::vector<TestProto>& actual) { + EXPECT_EQ(expected.size(), actual.size()); + for (size_t i = 0; i < actual.size(); i++) { + EntryMap::iterator expected_it = expected.find(actual[i].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 ProtoDatabaseImplTest : public testing::Test { + public: + virtual void SetUp() { + main_loop_.reset(new MessageLoop()); + db_.reset( + new ProtoDatabaseImpl<TestProto>(main_loop_->message_loop_proxy())); + } + + virtual void TearDown() { + db_.reset(); + base::RunLoop().RunUntilIdle(); + main_loop_.reset(); + } + + scoped_ptr<ProtoDatabaseImpl<TestProto> > db_; + scoped_ptr<MessageLoop> main_loop_; +}; + +// Test that ProtoDatabaseImpl calls Init on the underlying database and that +// the caller's InitCallback is called with the correct value. +TEST_F(ProtoDatabaseImplTest, 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<LevelDB>(mock_db), base::FilePath(path), + base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(ProtoDatabaseImplTest, 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<LevelDB>(mock_db), base::FilePath(path), + base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +ACTION_P(AppendLoadEntries, model) { + std::vector<std::string>* output = arg0; + for (EntryMap::const_iterator it = model.begin(); it != model.end(); ++it) { + output->push_back(it->second.SerializeAsString()); + } + return true; +} + +ACTION_P(VerifyLoadEntries, expected) { + std::vector<TestProto>* actual = arg1; + ExpectEntryPointersEquals(expected, *actual); +} + +// Test that ProtoDatabaseImpl 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(ProtoDatabaseImplTest, 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<LevelDB>(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(ProtoDatabaseImplTest, 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<LevelDB>(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(VerifyUpdateEntries, expected) { + const KeyValueVector actual = arg0; + // Create a vector of TestProto from |actual| to reuse the comparison + // function. + std::vector<TestProto> extracted_entries; + for (KeyValueVector::const_iterator it = actual.begin(); it != actual.end(); + ++it) { + TestProto entry; + entry.ParseFromString(it->second); + extracted_entries.push_back(entry); + } + ExpectEntryPointersEquals(expected, extracted_entries); + return true; +} + +// Test that ProtoDatabaseImpl 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(ProtoDatabaseImplTest, 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<LevelDB>(mock_db), base::FilePath(path), + base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller))); + + scoped_ptr<ProtoDatabase<TestProto>::KeyEntryVector> entries( + new ProtoDatabase<TestProto>::KeyEntryVector()); + for (EntryMap::iterator it = model.begin(); it != model.end(); ++it) { + entries->push_back(std::make_pair(it->second.id(), it->second)); + } + scoped_ptr<KeyVector> keys_to_remove(new KeyVector()); + + EXPECT_CALL(*mock_db, Save(_, _)).WillOnce(VerifyUpdateEntries(model)); + EXPECT_CALL(caller, SaveCallback(true)); + db_->UpdateEntries( + entries.Pass(), keys_to_remove.Pass(), + base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(ProtoDatabaseImplTest, TestDBSaveFailure) { + base::FilePath path(FILE_PATH_LITERAL("/fake/path")); + + MockDB* mock_db = new MockDB(); + MockDatabaseCaller caller; + scoped_ptr<ProtoDatabase<TestProto>::KeyEntryVector> entries( + new ProtoDatabase<TestProto>::KeyEntryVector()); + scoped_ptr<KeyVector> keys_to_remove(new KeyVector()); + + EXPECT_CALL(*mock_db, Init(_)); + EXPECT_CALL(caller, InitCallback(_)); + db_->InitWithDatabase( + scoped_ptr<LevelDB>(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_->UpdateEntries( + entries.Pass(), keys_to_remove.Pass(), + base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +// Test that ProtoDatabaseImpl calls Save on the underlying database with the +// correct entries to delete and that the caller's SaveCallback is called with +// the correct success value. +TEST_F(ProtoDatabaseImplTest, TestDBRemoveSuccess) { + 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<LevelDB>(mock_db), base::FilePath(path), + base::Bind(&MockDatabaseCaller::InitCallback, base::Unretained(&caller))); + + scoped_ptr<ProtoDatabase<TestProto>::KeyEntryVector> entries( + new ProtoDatabase<TestProto>::KeyEntryVector()); + scoped_ptr<KeyVector> keys_to_remove(new KeyVector()); + for (EntryMap::iterator it = model.begin(); it != model.end(); ++it) { + keys_to_remove->push_back(it->second.id()); + } + + KeyVector keys_copy(*keys_to_remove.get()); + EXPECT_CALL(*mock_db, Save(_, keys_copy)).WillOnce(Return(true)); + EXPECT_CALL(caller, SaveCallback(true)); + db_->UpdateEntries( + entries.Pass(), keys_to_remove.Pass(), + base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +TEST_F(ProtoDatabaseImplTest, TestDBRemoveFailure) { + base::FilePath path(FILE_PATH_LITERAL("/fake/path")); + + MockDB* mock_db = new MockDB(); + MockDatabaseCaller caller; + scoped_ptr<ProtoDatabase<TestProto>::KeyEntryVector> entries( + new ProtoDatabase<TestProto>::KeyEntryVector()); + scoped_ptr<KeyVector> keys_to_remove(new KeyVector()); + + EXPECT_CALL(*mock_db, Init(_)); + EXPECT_CALL(caller, InitCallback(_)); + db_->InitWithDatabase( + scoped_ptr<LevelDB>(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_->UpdateEntries( + entries.Pass(), keys_to_remove.Pass(), + base::Bind(&MockDatabaseCaller::SaveCallback, base::Unretained(&caller))); + + base::RunLoop().RunUntilIdle(); +} + +// This tests that normal usage of the real database does not cause any +// threading violations. +TEST(ProtoDatabaseImplThreadingTest, TestDBDestruction) { + base::MessageLoop main_loop; + + ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + base::Thread db_thread("dbthread"); + ASSERT_TRUE(db_thread.Start()); + + scoped_ptr<ProtoDatabaseImpl<TestProto> > db( + new ProtoDatabaseImpl<TestProto>(db_thread.message_loop_proxy())); + + MockDatabaseCaller caller; + EXPECT_CALL(caller, InitCallback(_)); + db->Init(temp_dir.path(), base::Bind(&MockDatabaseCaller::InitCallback, + base::Unretained(&caller))); + + db.reset(); + + base::RunLoop run_loop; + db_thread.message_loop_proxy()->PostTaskAndReply( + FROM_HERE, base::Bind(base::DoNothing), run_loop.QuitClosure()); + run_loop.Run(); +} + +// 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(); + + KeyValueVector save_entries; + std::vector<std::string> load_entries; + KeyVector remove_keys; + + for (EntryMap::iterator it = model.begin(); it != model.end(); ++it) { + save_entries.push_back( + std::make_pair(it->second.id(), it->second.SerializeAsString())); + } + + scoped_ptr<LevelDB> db(new LevelDB()); + EXPECT_TRUE(db->Init(temp_dir.path())); + EXPECT_TRUE(db->Save(save_entries, remove_keys)); + + if (close_after_save) { + db.reset(new LevelDB()); + EXPECT_TRUE(db->Init(temp_dir.path())); + } + + EXPECT_TRUE(db->Load(&load_entries)); + // Convert the strings back to TestProto. + std::vector<TestProto> loaded_protos; + for (std::vector<std::string>::iterator it = load_entries.begin(); + it != load_entries.end(); ++it) { + TestProto entry; + entry.ParseFromString(*it); + loaded_protos.push_back(entry); + } + ExpectEntryPointersEquals(model, loaded_protos); +} + +TEST(ProtoDatabaseImplLevelDBTest, TestDBSaveAndLoad) { + TestLevelDBSaveAndLoad(false); +} + +TEST(ProtoDatabaseImplLevelDBTest, TestDBCloseAndReopen) { + TestLevelDBSaveAndLoad(true); +} + +} // namespace leveldb_proto diff --git a/components/leveldb_proto/testing/fake_db.h b/components/leveldb_proto/testing/fake_db.h new file mode 100644 index 0000000..8e57e0e --- /dev/null +++ b/components/leveldb_proto/testing/fake_db.h @@ -0,0 +1,146 @@ +// Copyright 2014 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_LEVELDB_PROTO_TESTING_FAKE_DB_H_ +#define COMPONENTS_LEVELDB_PROTO_TESTING_FAKE_DB_H_ + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "components/leveldb_proto/proto_database.h" + +namespace leveldb_proto { +namespace test { + +template <typename T> +class FakeDB : public ProtoDatabase<T> { + typedef base::Callback<void(bool)> Callback; + + public: + typedef typename base::hash_map<std::string, T> EntryMap; + + explicit FakeDB(EntryMap* db); + virtual ~FakeDB(); + + virtual void Init(const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback) + OVERRIDE; + + virtual void UpdateEntries( + scoped_ptr<typename ProtoDatabase<T>::KeyEntryVector> entries_to_save, + scoped_ptr<std::vector<std::string> > keys_to_remove, + typename ProtoDatabase<T>::UpdateCallback callback) OVERRIDE; + + virtual void LoadEntries(typename ProtoDatabase<T>::LoadCallback callback) + OVERRIDE; + base::FilePath& GetDirectory(); + + void InitCallback(bool success); + + void LoadCallback(bool success); + + void UpdateCallback(bool success); + + static base::FilePath DirectoryForTestDB(); + + private: + static void RunLoadCallback( + typename ProtoDatabase<T>::LoadCallback callback, + scoped_ptr<typename std::vector<T> > entries, + bool success); + + base::FilePath dir_; + EntryMap* db_; + + Callback init_callback_; + Callback load_callback_; + Callback update_callback_; +}; + +template <typename T> +FakeDB<T>::FakeDB(EntryMap* db) + : db_(db) {} + +template <typename T> +FakeDB<T>::~FakeDB() {} + +template <typename T> +void FakeDB<T>::Init(const base::FilePath& database_dir, + typename ProtoDatabase<T>::InitCallback callback) { + dir_ = database_dir; + init_callback_ = callback; +} + +template <typename T> +void FakeDB<T>::UpdateEntries( + scoped_ptr<typename ProtoDatabase<T>::KeyEntryVector> entries_to_save, + scoped_ptr<std::vector<std::string> > keys_to_remove, + typename ProtoDatabase<T>::UpdateCallback callback) { + for (typename ProtoDatabase<T>::KeyEntryVector::iterator it = + entries_to_save->begin(); + it != entries_to_save->end(); ++it) { + (*db_)[it->first] = it->second; + } + for (std::vector<std::string>::iterator it = keys_to_remove->begin(); + it != keys_to_remove->end(); ++it) { + (*db_).erase(*it); + } + update_callback_ = callback; +} + +template <typename T> +void FakeDB<T>::LoadEntries(typename ProtoDatabase<T>::LoadCallback callback) { + scoped_ptr<std::vector<T> > entries(new std::vector<T>()); + for (typename EntryMap::iterator it = db_->begin(); it != db_->end(); ++it) { + entries->push_back(it->second); + } + load_callback_ = + base::Bind(RunLoadCallback, callback, base::Passed(&entries)); +} + +template <typename T> +base::FilePath& FakeDB<T>::GetDirectory() { + return dir_; +} + +template <typename T> +void FakeDB<T>::InitCallback(bool success) { + init_callback_.Run(success); + init_callback_.Reset(); +} + +template <typename T> +void FakeDB<T>::LoadCallback(bool success) { + load_callback_.Run(success); + load_callback_.Reset(); +} + +template <typename T> +void FakeDB<T>::UpdateCallback(bool success) { + update_callback_.Run(success); + update_callback_.Reset(); +} + +// static +template <typename T> +void FakeDB<T>::RunLoadCallback( + typename ProtoDatabase<T>::LoadCallback callback, + scoped_ptr<typename std::vector<T> > entries, bool success) { + callback.Run(success, entries.Pass()); +} + +// static +template <typename T> +base::FilePath FakeDB<T>::DirectoryForTestDB() { + return base::FilePath(FILE_PATH_LITERAL("/fake/path")); +} + +} // namespace test +} // namespace leveldb_proto + +#endif // COMPONENTS_LEVELDB_PROTO_TESTING_FAKE_DB_H_ diff --git a/components/leveldb_proto/testing/proto/test.proto b/components/leveldb_proto/testing/proto/test.proto new file mode 100644 index 0000000..8498105 --- /dev/null +++ b/components/leveldb_proto/testing/proto/test.proto @@ -0,0 +1,20 @@ +// Copyright 2014 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package leveldb_proto; + +// A proto to test leveldb_proto::ProtoDatabaseImpl. +// +// Next tag: 3 +message TestProto { + // Arbitary id. + optional string id = 1; + + // Arbitrary data. + optional string data = 2; +} |