summaryrefslogtreecommitdiffstats
path: root/sync/engine/non_blocking_type_processor_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'sync/engine/non_blocking_type_processor_unittest.cc')
-rw-r--r--sync/engine/non_blocking_type_processor_unittest.cc524
1 files changed, 524 insertions, 0 deletions
diff --git a/sync/engine/non_blocking_type_processor_unittest.cc b/sync/engine/non_blocking_type_processor_unittest.cc
new file mode 100644
index 0000000..9ab6ca3
--- /dev/null
+++ b/sync/engine/non_blocking_type_processor_unittest.cc
@@ -0,0 +1,524 @@
+// 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/non_blocking_type_processor.h"
+
+#include "sync/engine/non_blocking_sync_common.h"
+#include "sync/engine/non_blocking_type_processor_core_interface.h"
+#include "sync/internal_api/public/base/model_type.h"
+#include "sync/internal_api/public/sync_core_proxy.h"
+#include "sync/protocol/sync.pb.h"
+#include "sync/syncable/syncable_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace syncer {
+
+namespace {
+
+class MockNonBlockingTypeProcessorCore
+ : public NonBlockingTypeProcessorCoreInterface {
+ public:
+ MockNonBlockingTypeProcessorCore();
+ virtual ~MockNonBlockingTypeProcessorCore();
+
+ virtual void RequestCommits(const CommitRequestDataList& list) OVERRIDE;
+
+ bool is_connected_;
+
+ std::vector<CommitRequestDataList> commit_request_lists_;
+};
+
+MockNonBlockingTypeProcessorCore::MockNonBlockingTypeProcessorCore()
+ : is_connected_(false) {
+}
+
+MockNonBlockingTypeProcessorCore::~MockNonBlockingTypeProcessorCore() {
+}
+
+void MockNonBlockingTypeProcessorCore::RequestCommits(
+ const CommitRequestDataList& list) {
+ commit_request_lists_.push_back(list);
+}
+
+class MockSyncCoreProxy : public syncer::SyncCoreProxy {
+ public:
+ MockSyncCoreProxy();
+ virtual ~MockSyncCoreProxy();
+
+ virtual void ConnectTypeToCore(
+ syncer::ModelType type,
+ const DataTypeState& data_type_state,
+ base::WeakPtr<syncer::NonBlockingTypeProcessor> type_processor) OVERRIDE;
+ virtual void Disconnect(syncer::ModelType type) OVERRIDE;
+ virtual scoped_ptr<SyncCoreProxy> Clone() const OVERRIDE;
+
+ MockNonBlockingTypeProcessorCore* GetMockProcessorCore();
+
+ private:
+ explicit MockSyncCoreProxy(MockNonBlockingTypeProcessorCore*);
+
+ // The NonBlockingTypeProcessor's contract expects that it gets to own this
+ // object, so we can retain only a non-owned pointer to it.
+ //
+ // This is very unsafe, but we can get away with it since these tests are not
+ // exercising the processor <-> processor_core connection code.
+ MockNonBlockingTypeProcessorCore* mock_core_;
+};
+
+MockSyncCoreProxy::MockSyncCoreProxy()
+ : mock_core_(new MockNonBlockingTypeProcessorCore) {
+}
+
+MockSyncCoreProxy::MockSyncCoreProxy(MockNonBlockingTypeProcessorCore* core)
+ : mock_core_(core) {
+}
+
+MockSyncCoreProxy::~MockSyncCoreProxy() {
+}
+
+void MockSyncCoreProxy::ConnectTypeToCore(
+ syncer::ModelType type,
+ const DataTypeState& data_type_state,
+ base::WeakPtr<syncer::NonBlockingTypeProcessor> type_processor) {
+ // This class is allowed to participate in only one connection.
+ DCHECK(!mock_core_->is_connected_);
+ mock_core_->is_connected_ = true;
+
+ // Hands off ownership of our member to the type_processor, while keeping
+ // an unsafe pointer to it. This is why we can only connect once.
+ scoped_ptr<NonBlockingTypeProcessorCoreInterface> core(mock_core_);
+
+ type_processor->OnConnect(core.Pass());
+}
+
+void MockSyncCoreProxy::Disconnect(syncer::ModelType type) {
+ // This mock object is not meant for connect and disconnect tests.
+ NOTREACHED() << "Not implemented";
+}
+
+scoped_ptr<SyncCoreProxy> MockSyncCoreProxy::Clone() const {
+ // There's no sensible way to clone this MockSyncCoreProxy.
+ return scoped_ptr<SyncCoreProxy>(new MockSyncCoreProxy(mock_core_));
+}
+
+MockNonBlockingTypeProcessorCore* MockSyncCoreProxy::GetMockProcessorCore() {
+ return mock_core_;
+}
+
+} // namespace
+
+// Tests the sync engine parts of NonBlockingTypeProcessor.
+//
+// The NonBlockingTypeProcessor 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 processor responds to the model's
+// requests to make changes to its data, the messages incoming fro 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 MockNonBlockingTypeProcessorCore.
+class NonBlockingTypeProcessorTest : public ::testing::Test {
+ public:
+ NonBlockingTypeProcessorTest();
+ virtual ~NonBlockingTypeProcessorTest();
+
+ // Explicit initialization step. Kept separate to allow tests to inject
+ // on-disk state before the test begins.
+ void Initialize();
+
+ // 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);
+
+ // 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 processor a successful commit response.
+ void SuccessfulCommitResponse(const CommitRequestData& request_data);
+
+ private:
+ std::string GenerateId(const std::string& tag) const;
+ std::string GenerateTagHash(const std::string& tag) const;
+ sync_pb::EntitySpecifics GenerateSpecifics(const std::string& tag,
+ const std::string& value) const;
+
+ int64 GetServerVersion(const std::string& tag);
+ void SetServerVersion(const std::string& tag, int64 version);
+
+ const ModelType type_;
+
+ scoped_ptr<MockSyncCoreProxy> mock_sync_core_proxy_;
+ scoped_ptr<NonBlockingTypeProcessor> processor_;
+ MockNonBlockingTypeProcessorCore* mock_processor_core_;
+
+ DataTypeState data_type_state_;
+
+ std::map<const std::string, int64> server_versions_;
+};
+
+NonBlockingTypeProcessorTest::NonBlockingTypeProcessorTest()
+ : type_(PREFERENCES),
+ mock_sync_core_proxy_(new MockSyncCoreProxy()),
+ processor_(new NonBlockingTypeProcessor(type_)) {
+}
+
+NonBlockingTypeProcessorTest::~NonBlockingTypeProcessorTest() {
+}
+
+void NonBlockingTypeProcessorTest::Initialize() {
+ processor_->Enable(mock_sync_core_proxy_->Clone());
+ mock_processor_core_ = mock_sync_core_proxy_->GetMockProcessorCore();
+}
+
+void NonBlockingTypeProcessorTest::WriteItem(const std::string& tag,
+ const std::string& value) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ processor_->Put(tag, GenerateSpecifics(tag, value));
+}
+
+void NonBlockingTypeProcessorTest::DeleteItem(const std::string& tag) {
+ processor_->Delete(tag);
+}
+
+void NonBlockingTypeProcessorTest::UpdateFromServer(int64 version_offset,
+ const std::string& tag,
+ const std::string& value) {
+ const std::string tag_hash = GenerateTagHash(tag);
+
+ // Overwrite the existing server version if this is the new highest version.
+ int64 old_version = GetServerVersion(tag_hash);
+ int64 version = old_version + version_offset;
+ if (version > old_version) {
+ SetServerVersion(tag_hash, version);
+ }
+
+ UpdateResponseData data;
+ data.id = GenerateId(tag_hash);
+ data.client_tag_hash = tag_hash;
+ data.response_version = version;
+ data.ctime = base::Time::UnixEpoch() + base::TimeDelta::FromDays(1);
+ data.mtime = data.ctime + base::TimeDelta::FromSeconds(version);
+ data.non_unique_name = tag;
+ data.deleted = false;
+ data.specifics = GenerateSpecifics(tag, value);
+
+ UpdateResponseDataList list;
+ list.push_back(data);
+
+ processor_->OnUpdateReceived(data_type_state_, list);
+}
+
+void NonBlockingTypeProcessorTest::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);
+ int64 old_version = GetServerVersion(tag_hash);
+ int64 version = old_version + version_offset;
+ if (version > old_version) {
+ SetServerVersion(tag_hash, version);
+ }
+
+ UpdateResponseData data;
+ data.id = GenerateId(tag_hash);
+ data.client_tag_hash = tag_hash;
+ data.response_version = version;
+ data.ctime = base::Time::UnixEpoch() + base::TimeDelta::FromDays(1);
+ data.mtime = data.ctime + base::TimeDelta::FromSeconds(version);
+ data.non_unique_name = tag;
+ data.deleted = true;
+
+ UpdateResponseDataList list;
+ list.push_back(data);
+
+ processor_->OnUpdateReceived(data_type_state_, list);
+}
+
+void NonBlockingTypeProcessorTest::SuccessfulCommitResponse(
+ const CommitRequestData& request_data) {
+ const std::string& client_tag_hash = request_data.client_tag_hash;
+ CommitResponseData response_data;
+
+ if (request_data.base_version == 0) {
+ // Server assigns new ID to newly committed items.
+ DCHECK(request_data.id.empty());
+ response_data.id = request_data.id;
+ } else {
+ // Otherwise we reuse the ID from the request.
+ response_data.id = GenerateId(client_tag_hash);
+ }
+
+ response_data.client_tag_hash = client_tag_hash;
+ response_data.sequence_number = request_data.sequence_number;
+
+ // Increment the server version on successful commit.
+ int64 version = GetServerVersion(client_tag_hash);
+ version++;
+ SetServerVersion(client_tag_hash, version);
+
+ response_data.response_version = version;
+
+ CommitResponseDataList list;
+ list.push_back(response_data);
+
+ processor_->OnCommitCompletion(data_type_state_, list);
+}
+
+int64 NonBlockingTypeProcessorTest::GetServerVersion(const std::string& tag) {
+ std::map<const std::string, int64>::const_iterator it;
+ it = server_versions_.find(tag);
+ if (it == server_versions_.end()) {
+ return 0;
+ } else {
+ return it->second;
+ }
+}
+
+void NonBlockingTypeProcessorTest::SetServerVersion(const std::string& tag_hash,
+ int64 version) {
+ server_versions_[tag_hash] = version;
+}
+
+std::string NonBlockingTypeProcessorTest::GenerateId(
+ const std::string& tag) const {
+ return "FakeId:" + tag;
+}
+
+std::string NonBlockingTypeProcessorTest::GenerateTagHash(
+ const std::string& tag) const {
+ return syncable::GenerateSyncableHash(type_, tag);
+}
+
+sync_pb::EntitySpecifics NonBlockingTypeProcessorTest::GenerateSpecifics(
+ const std::string& tag,
+ const std::string& value) const {
+ sync_pb::EntitySpecifics specifics;
+ specifics.mutable_preference()->set_name(tag);
+ specifics.mutable_preference()->set_value(value);
+ return specifics;
+}
+
+size_t NonBlockingTypeProcessorTest::GetNumCommitRequestLists() {
+ return mock_processor_core_->commit_request_lists_.size();
+}
+
+CommitRequestDataList NonBlockingTypeProcessorTest::GetNthCommitRequestList(
+ size_t n) {
+ DCHECK_LT(n, GetNumCommitRequestLists());
+ return mock_processor_core_->commit_request_lists_[n];
+}
+
+bool NonBlockingTypeProcessorTest::HasCommitRequestForTag(
+ const std::string& tag) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ const std::vector<CommitRequestDataList>& lists =
+ mock_processor_core_->commit_request_lists_;
+
+ // Iterate backward through the sets of commit requests to find the most
+ // recent one that applies to the specified tag.
+ for (std::vector<CommitRequestDataList>::const_reverse_iterator lists_it =
+ lists.rbegin();
+ lists_it != lists.rend();
+ ++lists_it) {
+ for (CommitRequestDataList::const_iterator it = lists_it->begin();
+ it != lists_it->end();
+ ++it) {
+ if (it->client_tag_hash == tag_hash) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+CommitRequestData NonBlockingTypeProcessorTest::GetLatestCommitRequestForTag(
+ const std::string& tag) {
+ const std::string tag_hash = GenerateTagHash(tag);
+ const std::vector<CommitRequestDataList>& lists =
+ mock_processor_core_->commit_request_lists_;
+
+ // Iterate backward through the sets of commit requests to find the most
+ // recent one that applies to the specified tag.
+ for (std::vector<CommitRequestDataList>::const_reverse_iterator lists_it =
+ lists.rbegin();
+ lists_it != lists.rend();
+ ++lists_it) {
+ for (CommitRequestDataList::const_iterator it = lists_it->begin();
+ it != lists_it->end();
+ ++it) {
+ if (it->client_tag_hash == tag_hash) {
+ return *it;
+ }
+ }
+ }
+
+ NOTREACHED() << "Could not find any commits for given tag " << tag << ". "
+ << "Test should have checked HasCommitRequestForTag() first.";
+ return CommitRequestData();
+}
+
+// Creates a new item locally.
+// Thoroughly tests the data generated by a local item creation.
+TEST_F(NonBlockingTypeProcessorTest, CreateLocalItem) {
+ Initialize();
+ 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(0, 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(NonBlockingTypeProcessorTest, CreateAndModifyLocalItem) {
+ Initialize();
+ 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(0, 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(NonBlockingTypeProcessorTest, DeleteUnknown) {
+ Initialize();
+
+ 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(NonBlockingTypeProcessorTest, DeleteServerUnknown) {
+ Initialize();
+
+ 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(0, 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(NonBlockingTypeProcessorTest, DeleteServerUnknown_RacyCommitResponse) {
+ Initialize();
+
+ 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(NonBlockingTypeProcessorTest, TwoIndependentItems) {
+ Initialize();
+ 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"));
+}
+
+// TODO(rlarocque): Add more testing of non_unique_name fields.
+
+} // namespace syncer