summaryrefslogtreecommitdiffstats
path: root/sync/engine/model_type_sync_proxy_impl_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'sync/engine/model_type_sync_proxy_impl_unittest.cc')
-rw-r--r--sync/engine/model_type_sync_proxy_impl_unittest.cc463
1 files changed, 463 insertions, 0 deletions
diff --git a/sync/engine/model_type_sync_proxy_impl_unittest.cc b/sync/engine/model_type_sync_proxy_impl_unittest.cc
new file mode 100644
index 0000000..810c6b6
--- /dev/null
+++ b/sync/engine/model_type_sync_proxy_impl_unittest.cc
@@ -0,0 +1,463 @@
+// 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 "sync/engine/model_type_sync_proxy_impl.h"
+
+#include "sync/engine/model_type_sync_worker.h"
+#include "sync/engine/non_blocking_sync_common.h"
+#include "sync/internal_api/public/base/model_type.h"
+#include "sync/internal_api/public/sync_context_proxy.h"
+#include "sync/protocol/sync.pb.h"
+#include "sync/syncable/syncable_util.h"
+#include "sync/test/engine/injectable_sync_context_proxy.h"
+#include "sync/test/engine/mock_model_type_sync_worker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace syncer {
+
+static const ModelType kModelType = PREFERENCES;
+
+// Tests the sync engine parts of ModelTypeSyncProxyImpl.
+//
+// The ModelTypeSyncProxyImpl contains a non-trivial amount of code dedicated
+// to turning the sync engine on and off again. That code is fairly well
+// tested in the NonBlockingDataTypeController unit tests and it doesn't need
+// to be re-tested here.
+//
+// These tests skip past initialization and focus on steady state sync engine
+// behvior. This is where we test how the type sync proxy responds to the
+// model's requests to make changes to its data, the messages incoming from the
+// sync server, and what happens when the two conflict.
+//
+// Inputs:
+// - Initial state from permanent storage. (TODO)
+// - Create, update or delete requests from the model.
+// - Update responses and commit responses from the server.
+//
+// Outputs:
+// - Writes to permanent storage. (TODO)
+// - Callbacks into the model. (TODO)
+// - Requests to the sync thread. Tested with MockModelTypeSyncWorker.
+class ModelTypeSyncProxyImplTest : public ::testing::Test {
+ public:
+ ModelTypeSyncProxyImplTest();
+ virtual ~ModelTypeSyncProxyImplTest();
+
+ // Initialize with no local state. The type sync proxy will be unable to
+ // commit until it receives notification that initial sync has completed.
+ void FirstTimeInitialize();
+
+ // Initialize to a "ready-to-commit" state.
+ void InitializeToReadyState();
+
+ // Disconnect the ModelTypeSyncWorker from our ModelTypeSyncProxyImpl.
+ void Disconnect();
+
+ // Disable sync for this ModelTypeSyncProxyImpl. Should cause sync state to
+ // be discarded.
+ void Disable();
+
+ // Re-enable sync after Disconnect() or Disable().
+ void ReEnable();
+
+ // Local data modification. Emulates signals from the model thread.
+ void WriteItem(const std::string& tag, const std::string& value);
+ void DeleteItem(const std::string& tag);
+
+ // Emulates an "initial sync done" message from the
+ // ModelTypeSyncWorker.
+ void OnInitialSyncDone();
+
+ // Emulate updates from the server.
+ // This harness has some functionality to help emulate server behavior.
+ // See the definitions of these methods for more information.
+ void UpdateFromServer(int64 version_offset,
+ const std::string& tag,
+ const std::string& value);
+ void TombstoneFromServer(int64 version_offset, const std::string& tag);
+
+ // Read emitted commit requests as batches.
+ size_t GetNumCommitRequestLists();
+ CommitRequestDataList GetNthCommitRequestList(size_t n);
+
+ // Read emitted commit requests by tag, most recent only.
+ bool HasCommitRequestForTag(const std::string& tag);
+ CommitRequestData GetLatestCommitRequestForTag(const std::string& tag);
+
+ // Sends the type sync proxy a successful commit response.
+ void SuccessfulCommitResponse(const CommitRequestData& request_data);
+
+ private:
+ static std::string GenerateTagHash(const std::string& tag);
+ static sync_pb::EntitySpecifics GenerateSpecifics(const std::string& tag,
+ const std::string& value);
+
+ int64 GetServerVersion(const std::string& tag);
+ void SetServerVersion(const std::string& tag, int64 version);
+
+ MockModelTypeSyncWorker* mock_worker_;
+ scoped_ptr<InjectableSyncContextProxy> injectable_sync_context_proxy_;
+ scoped_ptr<ModelTypeSyncProxyImpl> type_sync_proxy_;
+
+ DataTypeState data_type_state_;
+};
+
+ModelTypeSyncProxyImplTest::ModelTypeSyncProxyImplTest()
+ : mock_worker_(new MockModelTypeSyncWorker()),
+ injectable_sync_context_proxy_(
+ new InjectableSyncContextProxy(mock_worker_)),
+ type_sync_proxy_(new ModelTypeSyncProxyImpl(kModelType)) {
+}
+
+ModelTypeSyncProxyImplTest::~ModelTypeSyncProxyImplTest() {
+}
+
+void ModelTypeSyncProxyImplTest::FirstTimeInitialize() {
+ type_sync_proxy_->Enable(injectable_sync_context_proxy_->Clone());
+}
+
+void ModelTypeSyncProxyImplTest::InitializeToReadyState() {
+ // TODO(rlarocque): This should be updated to inject on-disk state.
+ // At the time this code was written, there was no support for on-disk
+ // state so this was the only way to inject a data_type_state into
+ // the |type_sync_proxy_|.
+ FirstTimeInitialize();
+ OnInitialSyncDone();
+}
+
+void ModelTypeSyncProxyImplTest::Disconnect() {
+ type_sync_proxy_->Disconnect();
+ injectable_sync_context_proxy_.reset();
+ mock_worker_ = NULL;
+}
+
+void ModelTypeSyncProxyImplTest::Disable() {
+ type_sync_proxy_->Disable();
+ injectable_sync_context_proxy_.reset();
+ mock_worker_ = NULL;
+}
+
+void ModelTypeSyncProxyImplTest::ReEnable() {
+ DCHECK(!type_sync_proxy_->IsConnected());
+
+ // Prepare a new NonBlockingTypeProcesorCore instance, just as we would
+ // if this happened in the real world.
+ mock_worker_ = new MockModelTypeSyncWorker();
+ injectable_sync_context_proxy_.reset(
+ new InjectableSyncContextProxy(mock_worker_));
+
+ // Re-enable sync with the new ModelTypeSyncWorker.
+ type_sync_proxy_->Enable(injectable_sync_context_proxy_->Clone());
+}
+
+void ModelTypeSyncProxyImplTest::WriteItem(const std::string& tag,
+ const std::string& value) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ type_sync_proxy_->Put(tag, GenerateSpecifics(tag, value));
+}
+
+void ModelTypeSyncProxyImplTest::DeleteItem(const std::string& tag) {
+ type_sync_proxy_->Delete(tag);
+}
+
+void ModelTypeSyncProxyImplTest::OnInitialSyncDone() {
+ data_type_state_.initial_sync_done = true;
+ UpdateResponseDataList empty_update_list;
+
+ type_sync_proxy_->OnUpdateReceived(data_type_state_, empty_update_list);
+}
+
+void ModelTypeSyncProxyImplTest::UpdateFromServer(int64 version_offset,
+ const std::string& tag,
+ const std::string& value) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ UpdateResponseData data = mock_worker_->UpdateFromServer(
+ version_offset, tag_hash, GenerateSpecifics(tag, value));
+
+ UpdateResponseDataList list;
+ list.push_back(data);
+ type_sync_proxy_->OnUpdateReceived(data_type_state_, list);
+}
+
+void ModelTypeSyncProxyImplTest::TombstoneFromServer(int64 version_offset,
+ const std::string& tag) {
+ // Overwrite the existing server version if this is the new highest version.
+ std::string tag_hash = GenerateTagHash(tag);
+
+ UpdateResponseData data =
+ mock_worker_->TombstoneFromServer(version_offset, tag_hash);
+
+ UpdateResponseDataList list;
+ list.push_back(data);
+ type_sync_proxy_->OnUpdateReceived(data_type_state_, list);
+}
+
+void ModelTypeSyncProxyImplTest::SuccessfulCommitResponse(
+ const CommitRequestData& request_data) {
+ CommitResponseDataList list;
+ list.push_back(mock_worker_->SuccessfulCommitResponse(request_data));
+ type_sync_proxy_->OnCommitCompletion(data_type_state_, list);
+}
+
+std::string ModelTypeSyncProxyImplTest::GenerateTagHash(
+ const std::string& tag) {
+ return syncable::GenerateSyncableHash(kModelType, tag);
+}
+
+sync_pb::EntitySpecifics ModelTypeSyncProxyImplTest::GenerateSpecifics(
+ const std::string& tag,
+ const std::string& value) {
+ sync_pb::EntitySpecifics specifics;
+ specifics.mutable_preference()->set_name(tag);
+ specifics.mutable_preference()->set_value(value);
+ return specifics;
+}
+
+size_t ModelTypeSyncProxyImplTest::GetNumCommitRequestLists() {
+ return mock_worker_->GetNumCommitRequestLists();
+}
+
+CommitRequestDataList ModelTypeSyncProxyImplTest::GetNthCommitRequestList(
+ size_t n) {
+ return mock_worker_->GetNthCommitRequestList(n);
+}
+
+bool ModelTypeSyncProxyImplTest::HasCommitRequestForTag(
+ const std::string& tag) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ return mock_worker_->HasCommitRequestForTagHash(tag_hash);
+}
+
+CommitRequestData ModelTypeSyncProxyImplTest::GetLatestCommitRequestForTag(
+ const std::string& tag) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ return mock_worker_->GetLatestCommitRequestForTagHash(tag_hash);
+}
+
+// Creates a new item locally.
+// Thoroughly tests the data generated by a local item creation.
+TEST_F(ModelTypeSyncProxyImplTest, CreateLocalItem) {
+ InitializeToReadyState();
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+
+ WriteItem("tag1", "value1");
+
+ // Verify the commit request this operation has triggered.
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_data = GetLatestCommitRequestForTag("tag1");
+
+ EXPECT_TRUE(tag1_data.id.empty());
+ EXPECT_EQ(kUncommittedVersion, tag1_data.base_version);
+ EXPECT_FALSE(tag1_data.ctime.is_null());
+ EXPECT_FALSE(tag1_data.mtime.is_null());
+ EXPECT_EQ("tag1", tag1_data.non_unique_name);
+ EXPECT_FALSE(tag1_data.deleted);
+ EXPECT_EQ("tag1", tag1_data.specifics.preference().name());
+ EXPECT_EQ("value1", tag1_data.specifics.preference().value());
+}
+
+// Creates a new local item then modifies it.
+// Thoroughly tests data generated by modification of server-unknown item.
+TEST_F(ModelTypeSyncProxyImplTest, CreateAndModifyLocalItem) {
+ InitializeToReadyState();
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+
+ WriteItem("tag1", "value1");
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1");
+
+ WriteItem("tag1", "value2");
+ EXPECT_EQ(2U, GetNumCommitRequestLists());
+
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_v2_data = GetLatestCommitRequestForTag("tag1");
+
+ // Test some of the relations between old and new commit requests.
+ EXPECT_EQ(tag1_v1_data.specifics.preference().value(), "value1");
+ EXPECT_GT(tag1_v2_data.sequence_number, tag1_v1_data.sequence_number);
+
+ // Perform a thorough examination of the update-generated request.
+ EXPECT_TRUE(tag1_v2_data.id.empty());
+ EXPECT_EQ(kUncommittedVersion, tag1_v2_data.base_version);
+ EXPECT_FALSE(tag1_v2_data.ctime.is_null());
+ EXPECT_FALSE(tag1_v2_data.mtime.is_null());
+ EXPECT_EQ("tag1", tag1_v2_data.non_unique_name);
+ EXPECT_FALSE(tag1_v2_data.deleted);
+ EXPECT_EQ("tag1", tag1_v2_data.specifics.preference().name());
+ EXPECT_EQ("value2", tag1_v2_data.specifics.preference().value());
+}
+
+// Deletes an item we've never seen before.
+// Should have no effect and not crash.
+TEST_F(ModelTypeSyncProxyImplTest, DeleteUnknown) {
+ InitializeToReadyState();
+
+ DeleteItem("tag1");
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+}
+
+// Creates an item locally then deletes it.
+//
+// In this test, no commit responses are received, so the deleted item is
+// server-unknown as far as the model thread is concerned. That behavior
+// is race-dependent; other tests are used to test other races.
+TEST_F(ModelTypeSyncProxyImplTest, DeleteServerUnknown) {
+ InitializeToReadyState();
+
+ WriteItem("tag1", "value1");
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1");
+
+ DeleteItem("tag1");
+ EXPECT_EQ(2U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_v2_data = GetLatestCommitRequestForTag("tag1");
+
+ EXPECT_GT(tag1_v2_data.sequence_number, tag1_v1_data.sequence_number);
+
+ EXPECT_TRUE(tag1_v2_data.id.empty());
+ EXPECT_EQ(kUncommittedVersion, tag1_v2_data.base_version);
+ EXPECT_TRUE(tag1_v2_data.deleted);
+}
+
+// Creates an item locally then deletes it.
+//
+// The item is created locally then enqueued for commit. The sync thread
+// successfully commits it, but, before the commit response is picked up
+// by the model thread, the item is deleted by the model thread.
+TEST_F(ModelTypeSyncProxyImplTest, DeleteServerUnknown_RacyCommitResponse) {
+ InitializeToReadyState();
+
+ WriteItem("tag1", "value1");
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1");
+
+ DeleteItem("tag1");
+ EXPECT_EQ(2U, GetNumCommitRequestLists());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+
+ // This commit happened while the deletion was in progress, but the commit
+ // response didn't arrive on our thread until after the delete was issued to
+ // the sync thread. It will update some metadata, but won't do much else.
+ SuccessfulCommitResponse(tag1_v1_data);
+
+ // TODO(rlarocque): Verify the state of the item is correct once we get
+ // storage hooked up in these tests. For example, verify the item is still
+ // marked as deleted.
+}
+
+// Creates two different sync items.
+// Verifies that the second has no effect on the first.
+TEST_F(ModelTypeSyncProxyImplTest, TwoIndependentItems) {
+ InitializeToReadyState();
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+
+ WriteItem("tag1", "value1");
+
+ // There should be one commit request for this item only.
+ ASSERT_EQ(1U, GetNumCommitRequestLists());
+ EXPECT_EQ(1U, GetNthCommitRequestList(0).size());
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+
+ WriteItem("tag2", "value2");
+
+ // The second write should trigger another single-item commit request.
+ ASSERT_EQ(2U, GetNumCommitRequestLists());
+ EXPECT_EQ(1U, GetNthCommitRequestList(1).size());
+ ASSERT_TRUE(HasCommitRequestForTag("tag2"));
+}
+
+// Starts the type sync proxy with no local state.
+// Verify that it waits until initial sync is complete before requesting
+// commits.
+TEST_F(ModelTypeSyncProxyImplTest, NoCommitsUntilInitialSyncDone) {
+ FirstTimeInitialize();
+
+ WriteItem("tag1", "value1");
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+
+ OnInitialSyncDone();
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ EXPECT_TRUE(HasCommitRequestForTag("tag1"));
+}
+
+// Test proper handling of disconnect and reconnect.
+//
+// Creates items in various states of commit and verifies they re-attempt to
+// commit on reconnect.
+TEST_F(ModelTypeSyncProxyImplTest, Disconnect) {
+ InitializeToReadyState();
+
+ // The first item is fully committed.
+ WriteItem("tag1", "value1");
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ SuccessfulCommitResponse(GetLatestCommitRequestForTag("tag1"));
+
+ // The second item has a commit request in progress.
+ WriteItem("tag2", "value2");
+ EXPECT_TRUE(HasCommitRequestForTag("tag2"));
+
+ Disconnect();
+
+ // The third item is added after disconnection.
+ WriteItem("tag3", "value3");
+
+ ReEnable();
+
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ EXPECT_EQ(2U, GetNthCommitRequestList(0).size());
+
+ // The first item was already in sync.
+ EXPECT_FALSE(HasCommitRequestForTag("tag1"));
+
+ // The second item's commit was interrupted and should be retried.
+ EXPECT_TRUE(HasCommitRequestForTag("tag2"));
+
+ // The third item's commit was not started until the reconnect.
+ EXPECT_TRUE(HasCommitRequestForTag("tag3"));
+}
+
+// Test proper handling of disable and re-enable.
+//
+// Creates items in various states of commit and verifies they re-attempt to
+// commit on re-enable.
+TEST_F(ModelTypeSyncProxyImplTest, Disable) {
+ InitializeToReadyState();
+
+ // The first item is fully committed.
+ WriteItem("tag1", "value1");
+ ASSERT_TRUE(HasCommitRequestForTag("tag1"));
+ SuccessfulCommitResponse(GetLatestCommitRequestForTag("tag1"));
+
+ // The second item has a commit request in progress.
+ WriteItem("tag2", "value2");
+ EXPECT_TRUE(HasCommitRequestForTag("tag2"));
+
+ Disable();
+
+ // The third item is added after disable.
+ WriteItem("tag3", "value3");
+
+ // Now we re-enable.
+ ReEnable();
+
+ // There should be nothing to commit right away, since we need to
+ // re-initialize the client state first.
+ EXPECT_EQ(0U, GetNumCommitRequestLists());
+
+ // Once we're ready to commit, all three local items should consider
+ // themselves uncommitted and pending for commit.
+ OnInitialSyncDone();
+ EXPECT_EQ(1U, GetNumCommitRequestLists());
+ EXPECT_EQ(3U, GetNthCommitRequestList(0).size());
+ EXPECT_TRUE(HasCommitRequestForTag("tag1"));
+ EXPECT_TRUE(HasCommitRequestForTag("tag2"));
+ EXPECT_TRUE(HasCommitRequestForTag("tag3"));
+}
+
+} // namespace syncer