Index: sync/engine/non_blocking_type_processor_unittest.cc |
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 0000000000000000000000000000000000000000..9ab6ca320f3828e8288d7a762bdfb5348567babb |
--- /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 |