summaryrefslogtreecommitdiffstats
path: root/components/dom_distiller
diff options
context:
space:
mode:
authorcjhopman@chromium.org <cjhopman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-10-03 20:28:52 +0000
committercjhopman@chromium.org <cjhopman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-10-03 20:28:52 +0000
commit9cff665aba533449ee287975ec01f68c1a3e4138 (patch)
treea2a3f05b90a4cb38025e2c2d217d29f30ccef62a /components/dom_distiller
parent2cff9f233362b325ca8dc285c025f167d91f9472 (diff)
downloadchromium_src-9cff665aba533449ee287975ec01f68c1a3e4138.zip
chromium_src-9cff665aba533449ee287975ec01f68c1a3e4138.tar.gz
chromium_src-9cff665aba533449ee287975ec01f68c1a3e4138.tar.bz2
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
Diffstat (limited to 'components/dom_distiller')
-rw-r--r--components/dom_distiller/DEPS1
-rw-r--r--components/dom_distiller/core/dom_distiller_database.h12
-rw-r--r--components/dom_distiller/core/dom_distiller_store.cc356
-rw-r--r--components/dom_distiller/core/dom_distiller_store.h141
-rw-r--r--components/dom_distiller/core/dom_distiller_store_unittest.cc472
5 files changed, 975 insertions, 7 deletions
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<void(bool success, scoped_ptr<EntryVector>)>
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<base::SequencedTaskRunner> 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<DomDistillerDatabaseInterface> 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<DomDistillerDatabaseInterface> 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<ArticleEntry> DomDistillerStore::GetEntries() const {
+ std::vector<ArticleEntry> 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<syncer::SyncChangeProcessor> sync_processor,
+ scoped_ptr<syncer::SyncErrorFactory> 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<EntryVector> 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<EntryVector> 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<std::string> StringSet;
+ StringSet entries_to_change;
+ for (SyncDataList::const_iterator it = data.begin(); it != data.end(); ++it) {
+ std::string entry_id = GetEntryIdFromSyncData(*it);
+ std::pair<StringSet::iterator, bool> 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 <vector>
+
+#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<ArticleEntry> 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<std::string, ArticleEntry> EntryMap;
+
+ // Creates storage using the given database for local storage. Initializes the
+ // database with |database_dir|.
+ DomDistillerStore(scoped_ptr<DomDistillerDatabaseInterface> 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<DomDistillerDatabaseInterface> database,
+ const EntryMap& initial_model,
+ const base::FilePath& database_dir);
+
+ virtual ~DomDistillerStore();
+
+ // DomDistillerStoreInterface implementation.
+ virtual bool AddEntry(const ArticleEntry& entry) OVERRIDE;
+ virtual std::vector<ArticleEntry> GetEntries() const OVERRIDE;
+
+ // syncer::SyncableService implementation.
+ virtual syncer::SyncMergeResult MergeDataAndStartSyncing(
+ syncer::ModelType type,
+ const syncer::SyncDataList& initial_sync_data,
+ scoped_ptr<syncer::SyncChangeProcessor> sync_processor,
+ scoped_ptr<syncer::SyncErrorFactory> 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<EntryVector> 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<syncer::SyncChangeProcessor> sync_processor_;
+ scoped_ptr<syncer::SyncErrorFactory> error_factory_;
+ scoped_ptr<DomDistillerDatabaseInterface> database_;
+ bool database_loaded_;
+
+ EntryMap model_;
+
+ base::WeakPtrFactory<DomDistillerStore> 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<void(bool)> 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<EntryVector> 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<EntryVector> 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<EntryVector> 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<DomDistillerDatabaseInterface>(fake_db_),
+ store_model_,
+ db_dir_));
+ }
+
+ void StartSyncing() {
+ fake_sync_processor_ = new FakeSyncChangeProcessor(&sync_model_);
+
+ store_->MergeDataAndStartSyncing(
+ kDomDistillerModelType,
+ SyncDataFromEntryMap(sync_model_),
+ make_scoped_ptr<SyncChangeProcessor>(fake_sync_processor_),
+ scoped_ptr<SyncErrorFactory>(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<DomDistillerStore> 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<DomDistillerStore> owned_other_store(new DomDistillerStore(
+ scoped_ptr<DomDistillerDatabaseInterface>(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<SyncChangeProcessor>(),
+ make_scoped_ptr<SyncErrorFactory>(other_error_factory));
+
+ EXPECT_TRUE(AreEntriesEqual(store_->GetEntries(), expected_model));
+ EXPECT_TRUE(AreEntriesEqual(other_store->GetEntries(), expected_model));
+}
+
+} // namespace dom_distiller