From 9cff665aba533449ee287975ec01f68c1a3e4138 Mon Sep 17 00:00:00 2001 From: "cjhopman@chromium.org" Date: Thu, 3 Oct 2013 20:28:52 +0000 Subject: Add DOM Distiller syncable service This adds DomDistillerStore which maintains the in-memory model of the DOM distiller list. It implements SyncableService and keeps three things consistent, sync, the local db, and the "user". Whenever a change is received from one of these three, it is applied to the model and each of the three sources are sent the "diff" between there eventual state (i.e. after already sent messages are applied) and the in-memory model's current state. There is currently no support for deleting entries once they are added to the list. BUG=297789,288015 Review URL: https://codereview.chromium.org/24096015 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@226832 0039d316-1c4b-4281-b951-d872f2087c98 --- components/components_tests.gypi | 1 + components/dom_distiller.gypi | 2 + components/dom_distiller/DEPS | 1 + .../dom_distiller/core/dom_distiller_database.h | 12 +- .../dom_distiller/core/dom_distiller_store.cc | 356 ++++++++++++++++ .../dom_distiller/core/dom_distiller_store.h | 141 ++++++ .../core/dom_distiller_store_unittest.cc | 472 +++++++++++++++++++++ 7 files changed, 978 insertions(+), 7 deletions(-) create mode 100644 components/dom_distiller/core/dom_distiller_store.cc create mode 100644 components/dom_distiller/core/dom_distiller_store.h create mode 100644 components/dom_distiller/core/dom_distiller_store_unittest.cc (limited to 'components') diff --git a/components/components_tests.gypi b/components/components_tests.gypi index b38353d..e5c1f59 100644 --- a/components/components_tests.gypi +++ b/components/components_tests.gypi @@ -16,6 +16,7 @@ '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', + 'dom_distiller/core/dom_distiller_store_unittest.cc', 'dom_distiller/core/article_entry_unittest.cc', 'json_schema/json_schema_validator_unittest.cc', 'json_schema/json_schema_validator_unittest_base.cc', diff --git a/components/dom_distiller.gypi b/components/dom_distiller.gypi index edfadd4..1447cf5 100644 --- a/components/dom_distiller.gypi +++ b/components/dom_distiller.gypi @@ -63,6 +63,8 @@ 'dom_distiller/core/dom_distiller_constants.h', 'dom_distiller/core/dom_distiller_database.cc', 'dom_distiller/core/dom_distiller_database.h', + 'dom_distiller/core/dom_distiller_store.cc', + 'dom_distiller/core/dom_distiller_store.h', ], }, ], diff --git a/components/dom_distiller/DEPS b/components/dom_distiller/DEPS index c5499e8..01d28747 100644 --- a/components/dom_distiller/DEPS +++ b/components/dom_distiller/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+grit", # For generated headers. + "+sync/api", "+sync/protocol", "+third_party/leveldatabase/src/include", diff --git a/components/dom_distiller/core/dom_distiller_database.h b/components/dom_distiller/core/dom_distiller_database.h index cf6a609..0f70d2e 100644 --- a/components/dom_distiller/core/dom_distiller_database.h +++ b/components/dom_distiller/core/dom_distiller_database.h @@ -14,11 +14,11 @@ #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" #include "components/dom_distiller/core/article_entry.h" namespace base { class SequencedTaskRunner; +class MessageLoop; } namespace leveldb { @@ -38,6 +38,8 @@ class DomDistillerDatabaseInterface { typedef base::Callback)> LoadCallback; + virtual ~DomDistillerDatabaseInterface() {} + // Asynchronously destroys the object after all in-progress file operations // have completed. The callbacks for in-progress operations will still be // called. @@ -56,9 +58,6 @@ class DomDistillerDatabaseInterface { // Asynchronously loads all entries from the database and invokes |callback| // when complete. virtual void LoadEntries(LoadCallback callback) = 0; - - protected: - virtual ~DomDistillerDatabaseInterface() {} }; class DomDistillerDatabase @@ -88,6 +87,8 @@ class DomDistillerDatabase DomDistillerDatabase(scoped_refptr task_runner); + virtual ~DomDistillerDatabase(); + // DomDistillerDatabaseInterface implementation. virtual void Destroy() OVERRIDE; virtual void Init(const base::FilePath& database_dir, @@ -101,9 +102,6 @@ class DomDistillerDatabase const base::FilePath& database_dir, InitCallback callback); - protected: - virtual ~DomDistillerDatabase(); - private: // Whether currently being run by |task_runner_|. bool IsRunByTaskRunner() const; diff --git a/components/dom_distiller/core/dom_distiller_store.cc b/components/dom_distiller/core/dom_distiller_store.cc new file mode 100644 index 0000000..8f7c7c9 --- /dev/null +++ b/components/dom_distiller/core/dom_distiller_store.cc @@ -0,0 +1,356 @@ +// 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_store.h" + +#include "base/bind.h" +#include "base/logging.h" +#include "components/dom_distiller/core/article_entry.h" +#include "sync/api/sync_change.h" +#include "sync/protocol/article_specifics.pb.h" +#include "sync/protocol/sync.pb.h" + +using sync_pb::ArticleSpecifics; +using sync_pb::EntitySpecifics; +using syncer::ModelType; +using syncer::SyncChange; +using syncer::SyncChangeList; +using syncer::SyncData; +using syncer::SyncDataList; +using syncer::SyncError; +using syncer::SyncMergeResult; + +namespace dom_distiller { + +namespace { + +std::string GetEntryIdFromSyncData(const SyncData& data) { + const EntitySpecifics& entity = data.GetSpecifics(); + DCHECK(entity.has_article()); + const ArticleSpecifics& specifics = entity.article(); + DCHECK(specifics.has_entry_id()); + return specifics.entry_id(); +} + +SyncData CreateLocalData(const ArticleEntry& entry) { + EntitySpecifics specifics = SpecificsFromEntry(entry); + const std::string& entry_id = entry.entry_id(); + return SyncData::CreateLocalData(entry_id, entry_id, specifics); +} + +} // namespace + +DomDistillerStore::DomDistillerStore( + scoped_ptr database, + const base::FilePath& database_dir) + : database_(database.Pass()), + database_loaded_(false), + weak_ptr_factory_(this) { + database_->Init(database_dir, + base::Bind(&DomDistillerStore::OnDatabaseInit, + weak_ptr_factory_.GetWeakPtr())); +} + +DomDistillerStore::DomDistillerStore( + scoped_ptr database, + const EntryMap& initial_model, + const base::FilePath& database_dir) + : database_(database.Pass()), + database_loaded_(false), + model_(initial_model), + weak_ptr_factory_(this) { + database_->Init(database_dir, + base::Bind(&DomDistillerStore::OnDatabaseInit, + weak_ptr_factory_.GetWeakPtr())); +} + +DomDistillerStore::~DomDistillerStore() {} + +// DomDistillerStoreInterface implementation. +bool DomDistillerStore::AddEntry(const ArticleEntry& entry) { + if (!database_loaded_) { + return false; + } + if (model_.find(entry.entry_id()) != model_.end()) { + return false; + } + + SyncChangeList changes_to_apply; + changes_to_apply.push_back( + SyncChange(FROM_HERE, SyncChange::ACTION_ADD, CreateLocalData(entry))); + + SyncChangeList changes_applied; + SyncChangeList changes_missing; + ApplyChangesToModel(changes_to_apply, &changes_applied, &changes_missing); + + DCHECK_EQ(size_t(0), changes_missing.size()); + DCHECK_EQ(size_t(1), changes_applied.size()); + + ApplyChangesToSync(FROM_HERE, changes_applied); + ApplyChangesToDatabase(changes_applied); + + return true; +} + +std::vector DomDistillerStore::GetEntries() const { + std::vector entries; + for (EntryMap::const_iterator it = model_.begin(); it != model_.end(); ++it) { + entries.push_back(it->second); + } + return entries; +} + +// syncer::SyncableService implementation. +SyncMergeResult DomDistillerStore::MergeDataAndStartSyncing( + ModelType type, + const SyncDataList& initial_sync_data, + scoped_ptr sync_processor, + scoped_ptr error_handler) { + DCHECK_EQ(syncer::ARTICLES, type); + DCHECK(!sync_processor_); + DCHECK(!error_factory_); + sync_processor_.reset(sync_processor.release()); + error_factory_.reset(error_handler.release()); + + SyncChangeList database_changes; + SyncChangeList sync_changes; + SyncMergeResult result = + MergeDataWithModel(initial_sync_data, &database_changes, &sync_changes); + ApplyChangesToDatabase(database_changes); + ApplyChangesToSync(FROM_HERE, sync_changes); + + return result; +} + +void DomDistillerStore::StopSyncing(ModelType type) { + sync_processor_.reset(); + error_factory_.reset(); +} + +SyncDataList DomDistillerStore::GetAllSyncData(ModelType type) const { + SyncDataList data; + for (EntryMap::const_iterator it = model_.begin(); it != model_.end(); ++it) { + data.push_back(CreateLocalData(it->second)); + } + return data; +} + +SyncError DomDistillerStore::ProcessSyncChanges( + const tracked_objects::Location& from_here, + const SyncChangeList& change_list) { + DCHECK(database_loaded_); + SyncChangeList database_changes; + SyncChangeList sync_changes; + SyncError error = + ApplyChangesToModel(change_list, &database_changes, &sync_changes); + ApplyChangesToDatabase(database_changes); + DCHECK_EQ(size_t(0), sync_changes.size()); + return error; +} + +ArticleEntry DomDistillerStore::GetEntryFromChange(const SyncChange& change) { + DCHECK(change.IsValid()); + DCHECK(change.sync_data().IsValid()); + return EntryFromSpecifics(change.sync_data().GetSpecifics()); +} + +void DomDistillerStore::OnDatabaseInit(bool success) { + if (!success) { + LOG(INFO) << "DOM Distiller database init failed."; + database_.reset(); + return; + } + database_->LoadEntries(base::Bind(&DomDistillerStore::OnDatabaseLoad, + weak_ptr_factory_.GetWeakPtr())); +} + +void DomDistillerStore::OnDatabaseLoad(bool success, + scoped_ptr entries) { + if (!success) { + LOG(INFO) << "DOM Distiller database load failed."; + database_.reset(); + return; + } + database_loaded_ = true; + + SyncDataList data; + for (EntryVector::iterator it = entries->begin(); it != entries->end(); + ++it) { + data.push_back(CreateLocalData(*it)); + } + SyncChangeList changes_applied; + SyncChangeList database_changes_needed; + MergeDataWithModel(data, &changes_applied, &database_changes_needed); + ApplyChangesToDatabase(database_changes_needed); +} + +void DomDistillerStore::OnDatabaseSave(bool success) { + if (!success) { + LOG(INFO) << "DOM Distiller database save failed." + << " Disabling modifications and sync."; + database_.reset(); + database_loaded_ = false; + StopSyncing(syncer::ARTICLES); + } +} + +bool DomDistillerStore::ApplyChangesToSync( + const tracked_objects::Location& from_here, + const SyncChangeList& change_list) { + if (!sync_processor_) { + return false; + } + if (change_list.empty()) { + return true; + } + + SyncError error = sync_processor_->ProcessSyncChanges(from_here, change_list); + if (error.IsSet()) { + StopSyncing(syncer::ARTICLES); + return false; + } + return true; +} + +bool DomDistillerStore::ApplyChangesToDatabase( + const SyncChangeList& change_list) { + if (!database_loaded_) { + return false; + } + if (change_list.empty()) { + return true; + } + scoped_ptr entries_to_save(new EntryVector()); + + for (SyncChangeList::const_iterator it = change_list.begin(); + it != change_list.end(); + ++it) { + entries_to_save->push_back(GetEntryFromChange(*it)); + } + database_->SaveEntries(entries_to_save.Pass(), + base::Bind(&DomDistillerStore::OnDatabaseSave, + weak_ptr_factory_.GetWeakPtr())); + return true; +} + +void DomDistillerStore::CalculateChangesForMerge( + const SyncDataList& data, + SyncChangeList* changes_to_apply, + SyncChangeList* changes_missing) { + typedef base::hash_set StringSet; + StringSet entries_to_change; + for (SyncDataList::const_iterator it = data.begin(); it != data.end(); ++it) { + std::string entry_id = GetEntryIdFromSyncData(*it); + std::pair insert_result = + entries_to_change.insert(entry_id); + + DCHECK(insert_result.second); + + SyncChange::SyncChangeType change_type = SyncChange::ACTION_ADD; + EntryMap::const_iterator current = model_.find(entry_id); + if (current != model_.end()) { + change_type = SyncChange::ACTION_UPDATE; + } + changes_to_apply->push_back(SyncChange(FROM_HERE, change_type, *it)); + } + + for (EntryMap::const_iterator it = model_.begin(); it != model_.end(); ++it) { + if (entries_to_change.find(it->first) == entries_to_change.end()) { + changes_missing->push_back(SyncChange( + FROM_HERE, SyncChange::ACTION_ADD, CreateLocalData(it->second))); + } + } +} + +SyncMergeResult DomDistillerStore::MergeDataWithModel( + const SyncDataList& data, + SyncChangeList* changes_applied, + SyncChangeList* changes_missing) { + DCHECK(changes_applied); + DCHECK(changes_missing); + + SyncMergeResult result(syncer::ARTICLES); + result.set_num_items_before_association(model_.size()); + + SyncChangeList changes_to_apply; + CalculateChangesForMerge(data, &changes_to_apply, changes_missing); + SyncError error = + ApplyChangesToModel(changes_to_apply, changes_applied, changes_missing); + + int num_added = 0; + int num_modified = 0; + for (SyncChangeList::const_iterator it = changes_applied->begin(); + it != changes_applied->end(); + ++it) { + DCHECK(it->IsValid()); + switch (it->change_type()) { + case SyncChange::ACTION_ADD: + num_added++; + break; + case SyncChange::ACTION_UPDATE: + num_modified++; + break; + default: + NOTREACHED(); + } + } + result.set_num_items_added(num_added); + result.set_num_items_modified(num_modified); + result.set_num_items_deleted(0); + + result.set_pre_association_version(0); + result.set_num_items_after_association(model_.size()); + result.set_error(error); + + return result; +} + +SyncError DomDistillerStore::ApplyChangesToModel( + const SyncChangeList& changes, + SyncChangeList* changes_applied, + SyncChangeList* changes_missing) { + DCHECK(changes_applied); + DCHECK(changes_missing); + + for (SyncChangeList::const_iterator it = changes.begin(); it != changes.end(); + ++it) { + ApplyChangeToModel(*it, changes_applied, changes_missing); + } + return SyncError(); +} + +void DomDistillerStore::ApplyChangeToModel(const SyncChange& change, + SyncChangeList* changes_applied, + SyncChangeList* changes_missing) { + DCHECK(changes_applied); + DCHECK(changes_missing); + DCHECK(change.IsValid()); + + const std::string& entry_id = GetEntryIdFromSyncData(change.sync_data()); + EntryMap::iterator entry_it = model_.find(entry_id); + + if (change.change_type() == SyncChange::ACTION_DELETE) { + // TODO(cjhopman): Support delete. + NOTIMPLEMENTED(); + StopSyncing(syncer::ARTICLES); + return; + } + + ArticleEntry entry = GetEntryFromChange(change); + if (entry_it == model_.end()) { + model_.insert(std::make_pair(entry.entry_id(), entry)); + changes_applied->push_back(SyncChange( + change.location(), SyncChange::ACTION_ADD, change.sync_data())); + } else { + if (!AreEntriesEqual(entry_it->second, entry)) { + // Currently, conflicts are simply resolved by accepting the last one to + // arrive. + entry_it->second = entry; + changes_applied->push_back(SyncChange( + change.location(), SyncChange::ACTION_UPDATE, change.sync_data())); + } + } +} + +} // namespace dom_distiller diff --git a/components/dom_distiller/core/dom_distiller_store.h b/components/dom_distiller/core/dom_distiller_store.h new file mode 100644 index 0000000..b6e8e09 --- /dev/null +++ b/components/dom_distiller/core/dom_distiller_store.h @@ -0,0 +1,141 @@ +// 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_STORE_H_ +#define COMPONENTS_DOM_DISTILLER_CORE_DOM_DISTILLER_STORE_H_ + +#include + +#include "base/containers/hash_tables.h" +#include "base/memory/weak_ptr.h" +#include "components/dom_distiller/core/article_entry.h" +#include "components/dom_distiller/core/dom_distiller_database.h" +#include "sync/api/sync_change.h" +#include "sync/api/sync_data.h" +#include "sync/api/sync_error.h" +#include "sync/api/sync_error_factory.h" +#include "sync/api/sync_merge_result.h" +#include "sync/api/syncable_service.h" + +namespace base { +class FilePath; +} + +namespace dom_distiller { + +// Interface for accessing the stored/synced DomDistiller entries. +class DomDistillerStoreInterface { + public: + virtual ~DomDistillerStoreInterface() {} + + virtual bool AddEntry(const ArticleEntry& entry) = 0; + + // Gets a copy of all the current entries. + virtual std::vector GetEntries() const = 0; + + // TODO(cjhopman): This should have a way to observe changes to the underlying + // model. +}; + +// Implements syncing/storing of DomDistiller entries. This keeps three +// models of the DOM distiller data in sync: the local database, sync, and the +// user (i.e. of DomDistillerStore). No changes are accepted while the local +// database is loading. Once the local database has loaded, changes from any of +// the three sources (technically just two, since changes don't come from the +// database) are handled similarly: +// 1. convert the change to a SyncChangeList. +// 2. apply that change to the in-memory model, calculating what changed +// (changes_applied) and what is missing--i.e. entries missing for a full merge, +// conflict resolution for normal changes-- (changes_missing). +// 3. send a message (possibly handled asynchronously) containing +// changes_missing to the source of the change. +// 4. send messages (possibly handled asynchronously) containing changes_applied +// to the other (i.e. non-source) two models. +// TODO(cjhopman): Support deleting entries. +class DomDistillerStore : public syncer::SyncableService, + DomDistillerStoreInterface { + public: + typedef base::hash_map EntryMap; + + // Creates storage using the given database for local storage. Initializes the + // database with |database_dir|. + DomDistillerStore(scoped_ptr database, + const base::FilePath& database_dir); + + // Creates storage using the given database for local storage. Initializes the + // database with |database_dir|. Also initializes the internal model to + // |initial_model|. + DomDistillerStore(scoped_ptr database, + const EntryMap& initial_model, + const base::FilePath& database_dir); + + virtual ~DomDistillerStore(); + + // DomDistillerStoreInterface implementation. + virtual bool AddEntry(const ArticleEntry& entry) OVERRIDE; + virtual std::vector GetEntries() const OVERRIDE; + + // syncer::SyncableService implementation. + virtual syncer::SyncMergeResult MergeDataAndStartSyncing( + syncer::ModelType type, + const syncer::SyncDataList& initial_sync_data, + scoped_ptr sync_processor, + scoped_ptr error_handler) OVERRIDE; + virtual void StopSyncing(syncer::ModelType type) OVERRIDE; + virtual syncer::SyncDataList GetAllSyncData(syncer::ModelType type) const + OVERRIDE; + virtual syncer::SyncError ProcessSyncChanges( + const tracked_objects::Location& from_here, + const syncer::SyncChangeList& change_list) OVERRIDE; + + static ArticleEntry GetEntryFromChange(const syncer::SyncChange& change); + + private: + void OnDatabaseInit(bool success); + void OnDatabaseLoad(bool success, scoped_ptr entries); + void OnDatabaseSave(bool success); + + syncer::SyncMergeResult MergeDataWithModel( + const syncer::SyncDataList& data, + syncer::SyncChangeList* changes_applied, + syncer::SyncChangeList* changes_missing); + + // Convert a SyncDataList to a SyncChangeList of add or update changes based + // on the state of the in-memory model. Also calculate the entries missing + // from the SyncDataList. + void CalculateChangesForMerge(const syncer::SyncDataList& data, + syncer::SyncChangeList* changes_to_apply, + syncer::SyncChangeList* changes_missing); + + bool ApplyChangesToSync(const tracked_objects::Location& from_here, + const syncer::SyncChangeList& change_list); + bool ApplyChangesToDatabase(const syncer::SyncChangeList& change_list); + + // Applies the change list to the in-memory model, appending the actual + // changes made to the model to changes_applied. If conflict resolution does + // not apply the requested change, then adds the "diff" to changes_missing. + syncer::SyncError ApplyChangesToModel( + const syncer::SyncChangeList& change_list, + syncer::SyncChangeList* changes_applied, + syncer::SyncChangeList* changes_missing); + + void ApplyChangeToModel(const syncer::SyncChange& change, + syncer::SyncChangeList* changes_applied, + syncer::SyncChangeList* changes_missing); + + scoped_ptr sync_processor_; + scoped_ptr error_factory_; + scoped_ptr database_; + bool database_loaded_; + + EntryMap model_; + + base::WeakPtrFactory weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(DomDistillerStore); +}; + +} // namespace dom_distiller + +#endif diff --git a/components/dom_distiller/core/dom_distiller_store_unittest.cc b/components/dom_distiller/core/dom_distiller_store_unittest.cc new file mode 100644 index 0000000..333d72d --- /dev/null +++ b/components/dom_distiller/core/dom_distiller_store_unittest.cc @@ -0,0 +1,472 @@ +// 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_store.h" + +#include "base/bind.h" +#include "base/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/time/time.h" +#include "components/dom_distiller/core/article_entry.h" +#include "sync/protocol/sync.pb.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::Time; +using sync_pb::EntitySpecifics; +using syncer::ModelType; +using syncer::SyncChange; +using syncer::SyncChangeList; +using syncer::SyncChangeProcessor; +using syncer::SyncData; +using syncer::SyncDataList; +using syncer::SyncError; +using syncer::SyncErrorFactory; +using testing::AssertionFailure; +using testing::AssertionResult; +using testing::AssertionSuccess; + +namespace dom_distiller { + +namespace { + +const ModelType kDomDistillerModelType = syncer::ARTICLES; + +typedef DomDistillerStore::EntryMap EntryMap; + +void AddEntry(const ArticleEntry& e, EntryMap* map) { + (*map)[e.entry_id()] = e; +} + +class FakeDB : public DomDistillerDatabaseInterface { + typedef base::Callback Callback; + + public: + explicit FakeDB(EntryMap* db) : db_(db) {} + + virtual void Destroy() OVERRIDE {} + + virtual void Init( + const base::FilePath& database_dir, + DomDistillerDatabaseInterface::InitCallback callback) OVERRIDE { + dir_ = database_dir; + init_callback_ = callback; + } + + virtual void SaveEntries( + scoped_ptr entries_to_save, + DomDistillerDatabaseInterface::SaveCallback callback) OVERRIDE { + for (EntryVector::iterator it = entries_to_save->begin(); + it != entries_to_save->end(); + ++it) { + (*db_)[it->entry_id()] = *it; + } + save_callback_ = callback; + } + + virtual void LoadEntries( + DomDistillerDatabaseInterface::LoadCallback callback) OVERRIDE { + scoped_ptr entries(new EntryVector()); + for (EntryMap::iterator it = db_->begin(); it != db_->end(); ++it) { + entries->push_back(it->second); + } + load_callback_ = + base::Bind(RunLoadCallback, callback, base::Passed(&entries)); + } + + base::FilePath& GetDirectory() { return dir_; } + + void InitCallback(bool success) { + init_callback_.Run(success); + init_callback_.Reset(); + } + + void LoadCallback(bool success) { + load_callback_.Run(success); + load_callback_.Reset(); + } + + void SaveCallback(bool success) { + save_callback_.Run(success); + save_callback_.Reset(); + } + + private: + static void RunLoadCallback( + DomDistillerDatabaseInterface::LoadCallback callback, + scoped_ptr entries, + bool success) { + callback.Run(success, entries.Pass()); + } + + base::FilePath dir_; + EntryMap* db_; + + Callback init_callback_; + Callback load_callback_; + Callback save_callback_; +}; + +class FakeSyncErrorFactory : public syncer::SyncErrorFactory { + public: + virtual syncer::SyncError CreateAndUploadError( + const tracked_objects::Location& location, + const std::string& message) OVERRIDE { + return syncer::SyncError(); + } +}; + +class FakeSyncChangeProcessor : public syncer::SyncChangeProcessor { + public: + FakeSyncChangeProcessor(EntryMap* model) : model_(model) {} + + virtual syncer::SyncDataList GetAllSyncData(syncer::ModelType type) const + OVERRIDE { + ADD_FAILURE() << "FakeSyncChangeProcessor::GetAllSyncData not implemented."; + return syncer::SyncDataList(); + } + + virtual SyncError ProcessSyncChanges( + const tracked_objects::Location&, + const syncer::SyncChangeList& changes) OVERRIDE { + for (SyncChangeList::const_iterator it = changes.begin(); + it != changes.end(); + ++it) { + AddEntry(DomDistillerStore::GetEntryFromChange(*it), model_); + } + return SyncError(); + } + + private: + EntryMap* model_; +}; + +ArticleEntry CreateEntry(std::string entry_id, + std::string page_url1, + std::string page_url2, + std::string page_url3) { + ArticleEntry entry; + entry.set_entry_id(entry_id); + if (!page_url1.empty()) { + ArticleEntryPage* page = entry.add_pages(); + page->set_url(page_url1); + } + if (!page_url2.empty()) { + ArticleEntryPage* page = entry.add_pages(); + page->set_url(page_url2); + } + if (!page_url3.empty()) { + ArticleEntryPage* page = entry.add_pages(); + page->set_url(page_url3); + } + return entry; +} + +ArticleEntry GetSampleEntry(int id) { + static ArticleEntry entries[] = { + CreateEntry("entry0", "example.com/1", "example.com/2", "example.com/3"), + CreateEntry("entry1", "example.com/1", "", ""), + CreateEntry("entry2", "example.com/p1", "example.com/p2", ""), + CreateEntry("entry3", "example.com/something/all", "", ""), + CreateEntry("entry4", "example.com/somethingelse/1", "", ""), + CreateEntry("entry5", "rock.example.com/p1", "rock.example.com/p2", ""), + CreateEntry("entry7", "example.com/entry7/1", "example.com/entry7/2", ""), + CreateEntry("entry8", "example.com/entry8/1", "", ""), + CreateEntry("entry9", "example.com/entry9/all", "", ""), }; + EXPECT_LT(id, 9); + return entries[id % 9]; +} + +} // namespace + +class DomDistillerStoreTest : public testing::Test { + public: + virtual void SetUp() { + base::FilePath db_dir_ = base::FilePath(FILE_PATH_LITERAL("/fake/path")); + db_model_.clear(); + sync_model_.clear(); + store_model_.clear(); + next_sync_id_ = 1; + } + + virtual void TearDown() { + store_.reset(); + fake_db_ = NULL; + fake_sync_processor_ = NULL; + } + + // Creates a simple DomDistillerStore initialized with |store_model_| and + // with a FakeDB backed by |db_model_|. + void CreateStore() { + fake_db_ = new FakeDB(&db_model_); + store_.reset(new DomDistillerStore( + scoped_ptr(fake_db_), + store_model_, + db_dir_)); + } + + void StartSyncing() { + fake_sync_processor_ = new FakeSyncChangeProcessor(&sync_model_); + + store_->MergeDataAndStartSyncing( + kDomDistillerModelType, + SyncDataFromEntryMap(sync_model_), + make_scoped_ptr(fake_sync_processor_), + scoped_ptr(new FakeSyncErrorFactory())); + } + + protected: + SyncData CreateSyncData(const ArticleEntry& entry) { + EntitySpecifics specifics = SpecificsFromEntry(entry); + return SyncData::CreateRemoteData( + next_sync_id_++, specifics, Time::UnixEpoch()); + } + + SyncDataList SyncDataFromEntryMap(const EntryMap& model) { + SyncDataList data; + for (EntryMap::const_iterator it = model.begin(); it != model.end(); ++it) { + data.push_back(CreateSyncData(it->second)); + } + return data; + } + + EntryMap db_model_; + EntryMap sync_model_; + EntryMap store_model_; + + scoped_ptr store_; + + // Both owned by |store_|. + FakeDB* fake_db_; + FakeSyncChangeProcessor* fake_sync_processor_; + + int64 next_sync_id_; + + base::FilePath db_dir_; +}; + +AssertionResult AreEntriesEqual(const EntryVector& entries, + EntryMap expected_entries) { + if (entries.size() != expected_entries.size()) + return AssertionFailure() << "Expected " << expected_entries.size() + << " entries but found " << entries.size(); + + for (EntryVector::const_iterator it = entries.begin(); it != entries.end(); + ++it) { + EntryMap::iterator expected_it = expected_entries.find(it->entry_id()); + if (expected_it == expected_entries.end()) { + return AssertionFailure() << "Found unexpected entry with id <" + << it->entry_id() << ">"; + } + if (!AreEntriesEqual(expected_it->second, *it)) { + return AssertionFailure() << "Mismatched entry with id <" + << it->entry_id() << ">"; + } + expected_entries.erase(expected_it); + } + return AssertionSuccess(); +} + +AssertionResult AreEntryMapsEqual(const EntryMap& left, const EntryMap& right) { + EntryVector entries; + for (EntryMap::const_iterator it = left.begin(); it != left.end(); ++it) { + entries.push_back(it->second); + } + return AreEntriesEqual(entries, right); +} + +TEST_F(DomDistillerStoreTest, TestDatabaseLoad) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + AddEntry(GetSampleEntry(2), &db_model_); + + CreateStore(); + + fake_db_->InitCallback(true); + EXPECT_EQ(fake_db_->GetDirectory(), db_dir_); + + fake_db_->LoadCallback(true); + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), db_model_)); +} + +TEST_F(DomDistillerStoreTest, TestDatabaseLoadMerge) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + AddEntry(GetSampleEntry(2), &db_model_); + + AddEntry(GetSampleEntry(2), &store_model_); + AddEntry(GetSampleEntry(3), &store_model_); + AddEntry(GetSampleEntry(4), &store_model_); + + EntryMap expected_model(db_model_); + AddEntry(GetSampleEntry(3), &expected_model); + AddEntry(GetSampleEntry(4), &expected_model); + + CreateStore(); + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model)); +} + +TEST_F(DomDistillerStoreTest, TestAddEntry) { + CreateStore(); + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + EXPECT_TRUE(store_->GetEntries().empty()); + EXPECT_TRUE(db_model_.empty()); + + store_->AddEntry(GetSampleEntry(0)); + + EntryMap expected_model; + AddEntry(GetSampleEntry(0), &expected_model); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model)); +} + +TEST_F(DomDistillerStoreTest, TestSyncMergeWithEmptyDatabase) { + AddEntry(GetSampleEntry(0), &sync_model_); + AddEntry(GetSampleEntry(1), &sync_model_); + AddEntry(GetSampleEntry(2), &sync_model_); + + CreateStore(); + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + StartSyncing(); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), sync_model_)); + EXPECT_TRUE(AreEntryMapsEqual(db_model_, sync_model_)); +} + +TEST_F(DomDistillerStoreTest, TestSyncMergeAfterDatabaseLoad) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + AddEntry(GetSampleEntry(2), &db_model_); + + AddEntry(GetSampleEntry(2), &sync_model_); + AddEntry(GetSampleEntry(3), &sync_model_); + AddEntry(GetSampleEntry(4), &sync_model_); + + EntryMap expected_model(db_model_); + AddEntry(GetSampleEntry(3), &expected_model); + AddEntry(GetSampleEntry(4), &expected_model); + + CreateStore(); + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), db_model_)); + + StartSyncing(); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model)); + EXPECT_TRUE(AreEntryMapsEqual(sync_model_, expected_model)); +} + +TEST_F(DomDistillerStoreTest, TestGetAllSyncData) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + AddEntry(GetSampleEntry(2), &db_model_); + + AddEntry(GetSampleEntry(2), &sync_model_); + AddEntry(GetSampleEntry(3), &sync_model_); + AddEntry(GetSampleEntry(4), &sync_model_); + + EntryMap expected_model(db_model_); + AddEntry(GetSampleEntry(3), &expected_model); + AddEntry(GetSampleEntry(4), &expected_model); + + CreateStore(); + + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + StartSyncing(); + + SyncDataList data = store_->GetAllSyncData(kDomDistillerModelType); + EntryVector entries; + for (SyncDataList::iterator it = data.begin(); it != data.end(); ++it) { + entries.push_back(EntryFromSpecifics(it->GetSpecifics())); + } + EXPECT_TRUE(AreEntriesEqual(entries, expected_model)); +} + +TEST_F(DomDistillerStoreTest, TestProcessSyncChanges) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + sync_model_ = db_model_; + + EntryMap expected_model(db_model_); + AddEntry(GetSampleEntry(2), &expected_model); + AddEntry(GetSampleEntry(3), &expected_model); + + CreateStore(); + + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + StartSyncing(); + + SyncChangeList changes; + changes.push_back(SyncChange( + FROM_HERE, SyncChange::ACTION_ADD, CreateSyncData(GetSampleEntry(2)))); + changes.push_back(SyncChange( + FROM_HERE, SyncChange::ACTION_ADD, CreateSyncData(GetSampleEntry(3)))); + + store_->ProcessSyncChanges(FROM_HERE, changes); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_TRUE(AreEntryMapsEqual(db_model_, expected_model)); +} + +TEST_F(DomDistillerStoreTest, TestSyncMergeWithSecondDomDistillerStore) { + AddEntry(GetSampleEntry(0), &db_model_); + AddEntry(GetSampleEntry(1), &db_model_); + AddEntry(GetSampleEntry(2), &db_model_); + + EntryMap other_db_model; + AddEntry(GetSampleEntry(2), &other_db_model); + AddEntry(GetSampleEntry(3), &other_db_model); + AddEntry(GetSampleEntry(4), &other_db_model); + + EntryMap expected_model(db_model_); + AddEntry(GetSampleEntry(3), &expected_model); + AddEntry(GetSampleEntry(4), &expected_model); + + CreateStore(); + + fake_db_->InitCallback(true); + fake_db_->LoadCallback(true); + + FakeDB* other_fake_db = new FakeDB(&other_db_model); + scoped_ptr owned_other_store(new DomDistillerStore( + scoped_ptr(other_fake_db), + EntryMap(), + base::FilePath(FILE_PATH_LITERAL("/fake/other/path")))); + DomDistillerStore* other_store = owned_other_store.get(); + other_fake_db->InitCallback(true); + other_fake_db->LoadCallback(true); + + EXPECT_FALSE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_FALSE(AreEntriesEqual(other_store->GetEntries(), expected_model)); + ASSERT_TRUE(AreEntriesEqual(other_store->GetEntries(), other_db_model)); + + FakeSyncErrorFactory* other_error_factory = new FakeSyncErrorFactory(); + store_->MergeDataAndStartSyncing( + kDomDistillerModelType, + SyncDataFromEntryMap(other_db_model), + owned_other_store.PassAs(), + make_scoped_ptr(other_error_factory)); + + EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model)); + EXPECT_TRUE(AreEntriesEqual(other_store->GetEntries(), expected_model)); +} + +} // namespace dom_distiller -- cgit v1.1