Commit 45763c93 authored by maxbogue's avatar maxbogue Committed by Commit bot

[Sync] Add two more USS integration tests.

- Disabling and enabling the type, which revealed a bug in the DTC.
- Conflict resolution, which revealed a bug in the worker.

BUG=643269

Review-Url: https://codereview.chromium.org/2339403004
Cr-Commit-Position: refs/heads/master@{#419222}
parent 05bae669
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
#include "chrome/browser/sync/test/integration/sync_integration_test_util.h" #include "chrome/browser/sync/test/integration/sync_integration_test_util.h"
#include "chrome/browser/sync/test/integration/sync_test.h" #include "chrome/browser/sync/test/integration/sync_test.h"
#include "components/browser_sync/browser/profile_sync_components_factory_impl.h" #include "components/browser_sync/browser/profile_sync_components_factory_impl.h"
#include "components/browser_sync/browser/profile_sync_service.h"
#include "components/sync/api/fake_model_type_service.h" #include "components/sync/api/fake_model_type_service.h"
using browser_sync::ChromeSyncClient; using browser_sync::ChromeSyncClient;
...@@ -19,6 +20,11 @@ using syncer_v2::FakeModelTypeService; ...@@ -19,6 +20,11 @@ using syncer_v2::FakeModelTypeService;
using syncer_v2::ModelTypeService; using syncer_v2::ModelTypeService;
using syncer_v2::SharedModelTypeProcessor; using syncer_v2::SharedModelTypeProcessor;
const char kKey1[] = "key1";
const char kValue1[] = "value1";
const char kValue2[] = "value2";
const char kResolutionValue[] = "RESOLVED";
// A ChromeSyncClient that provides a ModelTypeService for PREFERENCES. // A ChromeSyncClient that provides a ModelTypeService for PREFERENCES.
class TestSyncClient : public ChromeSyncClient { class TestSyncClient : public ChromeSyncClient {
public: public:
...@@ -62,6 +68,13 @@ class TestModelTypeService : public FakeModelTypeService { ...@@ -62,6 +68,13 @@ class TestModelTypeService : public FakeModelTypeService {
db().CreateMetadataBatch()); db().CreateMetadataBatch());
} }
syncer_v2::ConflictResolution ResolveConflict(
const syncer_v2::EntityData& local_data,
const syncer_v2::EntityData& remote_data) const override {
return syncer_v2::ConflictResolution::UseNew(
GenerateEntityData(local_data.non_unique_name, kResolutionValue));
}
void AddObserver(Observer* observer) { observers_.insert(observer); } void AddObserver(Observer* observer) { observers_.insert(observer); }
void RemoveObserver(Observer* observer) { observers_.erase(observer); } void RemoveObserver(Observer* observer) { observers_.erase(observer); }
...@@ -103,35 +116,73 @@ class KeyChecker : public StatusChangeChecker, ...@@ -103,35 +116,73 @@ class KeyChecker : public StatusChangeChecker,
const std::string key_; const std::string key_;
}; };
// Wait for a key to be present. // Wait for data for a key to have a certain value.
class KeyPresentChecker : public KeyChecker { class DataChecker : public KeyChecker {
public: public:
KeyPresentChecker(TestModelTypeService* service, const std::string& key) DataChecker(TestModelTypeService* service,
: KeyChecker(service, key) {} const std::string& key,
~KeyPresentChecker() override {} const std::string& value)
: KeyChecker(service, key), value_(value) {}
~DataChecker() override {}
bool IsExitConditionSatisfied() override { bool IsExitConditionSatisfied() override {
return service_->db().HasData(key_); const auto& db = service_->db();
return db.HasData(key_) && db.GetValue(key_) == value_;
} }
std::string GetDebugMessage() const override { std::string GetDebugMessage() const override {
return "Waiting for key '" + key_ + "' to be present."; return "Waiting for data for key '" + key_ + "' to be '" + value_ + "'.";
} }
private:
const std::string value_;
}; };
// Wait for a key to be absent. // Wait for data for a key to be absent.
class KeyAbsentChecker : public KeyChecker { class DataAbsentChecker : public KeyChecker {
public: public:
KeyAbsentChecker(TestModelTypeService* service, const std::string& key) DataAbsentChecker(TestModelTypeService* service, const std::string& key)
: KeyChecker(service, key) {} : KeyChecker(service, key) {}
~KeyAbsentChecker() override {} ~DataAbsentChecker() override {}
bool IsExitConditionSatisfied() override { bool IsExitConditionSatisfied() override {
return !service_->db().HasData(key_); return !service_->db().HasData(key_);
} }
std::string GetDebugMessage() const override { std::string GetDebugMessage() const override {
return "Waiting for key '" + key_ + "' to be absent."; return "Waiting for data for key '" + key_ + "' to be absent.";
}
};
// Wait for metadata for a key to be present.
class MetadataPresentChecker : public KeyChecker {
public:
MetadataPresentChecker(TestModelTypeService* service, const std::string& key)
: KeyChecker(service, key) {}
~MetadataPresentChecker() override {}
bool IsExitConditionSatisfied() override {
return service_->db().HasMetadata(key_);
}
std::string GetDebugMessage() const override {
return "Waiting for metadata for key '" + key_ + "' to be present.";
}
};
// Wait for metadata for a key to be absent.
class MetadataAbsentChecker : public KeyChecker {
public:
MetadataAbsentChecker(TestModelTypeService* service, const std::string& key)
: KeyChecker(service, key) {}
~MetadataAbsentChecker() override {}
bool IsExitConditionSatisfied() override {
return !service_->db().HasMetadata(key_);
}
std::string GetDebugMessage() const override {
return "Waiting for metadata for key '" + key_ + "' to be absent.";
} }
}; };
...@@ -176,6 +227,7 @@ class TwoClientUssSyncTest : public SyncTest { ...@@ -176,6 +227,7 @@ class TwoClientUssSyncTest : public SyncTest {
std::vector<TestSyncClient*> clients_; std::vector<TestSyncClient*> clients_;
bool first_client_ignored_ = false; bool first_client_ignored_ = false;
private:
DISALLOW_COPY_AND_ASSIGN(TwoClientUssSyncTest); DISALLOW_COPY_AND_ASSIGN(TwoClientUssSyncTest);
}; };
...@@ -186,10 +238,67 @@ IN_PROC_BROWSER_TEST_F(TwoClientUssSyncTest, Sanity) { ...@@ -186,10 +238,67 @@ IN_PROC_BROWSER_TEST_F(TwoClientUssSyncTest, Sanity) {
TestModelTypeService* model1 = GetModelTypeService(0); TestModelTypeService* model1 = GetModelTypeService(0);
TestModelTypeService* model2 = GetModelTypeService(1); TestModelTypeService* model2 = GetModelTypeService(1);
model1->WriteItem("foo", "bar"); // Add an entity.
ASSERT_TRUE(KeyPresentChecker(model2, "foo").Wait()); model1->WriteItem(kKey1, kValue1);
EXPECT_EQ("bar", model2->db().GetValue("foo")); ASSERT_TRUE(DataChecker(model2, kKey1, kValue1).Wait());
// Update an entity.
model1->WriteItem(kKey1, kValue2);
ASSERT_TRUE(DataChecker(model2, kKey1, kValue2).Wait());
// Delete an entity.
model1->DeleteItem(kKey1);
ASSERT_TRUE(DataAbsentChecker(model2, kKey1).Wait());
}
IN_PROC_BROWSER_TEST_F(TwoClientUssSyncTest, DisableEnable) {
ASSERT_TRUE(SetupSync());
TestModelTypeService* model1 = GetModelTypeService(0);
TestModelTypeService* model2 = GetModelTypeService(1);
// Add an entity to test with.
model1->WriteItem(kKey1, kValue1);
ASSERT_TRUE(DataChecker(model2, kKey1, kValue1).Wait());
ASSERT_EQ(1U, model1->db().data_count());
ASSERT_EQ(1U, model1->db().metadata_count());
ASSERT_EQ(1U, model2->db().data_count());
ASSERT_EQ(1U, model2->db().metadata_count());
// Disable PREFERENCES.
syncer::ModelTypeSet types = syncer::UserSelectableTypes();
types.Remove(syncer::PREFERENCES);
GetSyncService(0)->OnUserChoseDatatypes(false, types);
// Wait for it to take effect and remove the metadata.
ASSERT_TRUE(MetadataAbsentChecker(model1, kKey1).Wait());
ASSERT_EQ(1U, model1->db().data_count());
ASSERT_EQ(0U, model1->db().metadata_count());
// Model 2 should not be affected.
ASSERT_EQ(1U, model2->db().data_count());
ASSERT_EQ(1U, model2->db().metadata_count());
// Re-enable PREFERENCES.
GetSyncService(0)->OnUserChoseDatatypes(true, syncer::UserSelectableTypes());
// Wait for metadata to be re-added.
ASSERT_TRUE(MetadataPresentChecker(model1, kKey1).Wait());
ASSERT_EQ(1U, model1->db().data_count());
ASSERT_EQ(1U, model1->db().metadata_count());
ASSERT_EQ(1U, model2->db().data_count());
ASSERT_EQ(1U, model2->db().metadata_count());
}
IN_PROC_BROWSER_TEST_F(TwoClientUssSyncTest, ConflictResolution) {
ASSERT_TRUE(SetupSync());
TestModelTypeService* model1 = GetModelTypeService(0);
TestModelTypeService* model2 = GetModelTypeService(1);
// Write conflicting entities.
model1->WriteItem(kKey1, kValue1);
model2->WriteItem(kKey1, kValue2);
model1->DeleteItem("foo"); // Wait for them to be resolved to kResolutionValue by the custom conflict
ASSERT_TRUE(KeyAbsentChecker(model2, "foo").Wait()); // resolution logic in TestModelTypeService.
ASSERT_TRUE(DataChecker(model1, kKey1, kResolutionValue).Wait());
ASSERT_TRUE(DataChecker(model2, kKey1, kResolutionValue).Wait());
} }
...@@ -131,11 +131,14 @@ void NonBlockingDataTypeController::OnProcessorStarted( ...@@ -131,11 +131,14 @@ void NonBlockingDataTypeController::OnProcessorStarted(
void NonBlockingDataTypeController::RegisterWithBackend( void NonBlockingDataTypeController::RegisterWithBackend(
sync_driver::BackendDataTypeConfigurer* configurer) { sync_driver::BackendDataTypeConfigurer* configurer) {
DCHECK(CalledOnValidThread()); DCHECK(CalledOnValidThread());
if (activated_)
return;
DCHECK(configurer); DCHECK(configurer);
DCHECK(activation_context_); DCHECK(activation_context_);
DCHECK_EQ(MODEL_LOADED, state_); DCHECK_EQ(MODEL_LOADED, state_);
configurer->ActivateNonBlockingDataType(type(), configurer->ActivateNonBlockingDataType(type(),
std::move(activation_context_)); std::move(activation_context_));
activated_ = true;
} }
void NonBlockingDataTypeController::StartAssociating( void NonBlockingDataTypeController::StartAssociating(
...@@ -165,7 +168,9 @@ void NonBlockingDataTypeController::DeactivateDataType( ...@@ -165,7 +168,9 @@ void NonBlockingDataTypeController::DeactivateDataType(
sync_driver::BackendDataTypeConfigurer* configurer) { sync_driver::BackendDataTypeConfigurer* configurer) {
DCHECK(CalledOnValidThread()); DCHECK(CalledOnValidThread());
DCHECK(configurer); DCHECK(configurer);
DCHECK(activated_);
configurer->DeactivateNonBlockingDataType(type()); configurer->DeactivateNonBlockingDataType(type());
activated_ = false;
} }
void NonBlockingDataTypeController::Stop() { void NonBlockingDataTypeController::Stop() {
......
...@@ -97,6 +97,12 @@ class NonBlockingDataTypeController : public sync_driver::DataTypeController { ...@@ -97,6 +97,12 @@ class NonBlockingDataTypeController : public sync_driver::DataTypeController {
// callback and must temporarily own it until ActivateDataType is called. // callback and must temporarily own it until ActivateDataType is called.
std::unique_ptr<syncer_v2::ActivationContext> activation_context_; std::unique_ptr<syncer_v2::ActivationContext> activation_context_;
// This is a hack to prevent reconfigurations from crashing, because USS
// activation is not idempotent. RegisterWithBackend only needs to actually do
// something the first time after the type is enabled.
// TODO(crbug.com/647505): Remove this once the DTM handles things better.
bool activated_ = false;
DISALLOW_COPY_AND_ASSIGN(NonBlockingDataTypeController); DISALLOW_COPY_AND_ASSIGN(NonBlockingDataTypeController);
}; };
......
...@@ -148,17 +148,17 @@ SyncerError ModelTypeWorker::ProcessGetUpdatesResponse( ...@@ -148,17 +148,17 @@ SyncerError ModelTypeWorker::ProcessGetUpdatesResponse(
// Check if specifics are encrypted and try to decrypt if so. // Check if specifics are encrypted and try to decrypt if so.
if (!specifics.has_encrypted()) { if (!specifics.has_encrypted()) {
// No encryption. // No encryption.
entity->ReceiveUpdate(update_entity->version());
data.specifics = specifics; data.specifics = specifics;
response_data.entity = data.PassToPtr(); response_data.entity = data.PassToPtr();
entity->ReceiveUpdate(response_data);
pending_updates_.push_back(response_data); pending_updates_.push_back(response_data);
} else if (specifics.has_encrypted() && cryptographer_ && } else if (specifics.has_encrypted() && cryptographer_ &&
cryptographer_->CanDecrypt(specifics.encrypted())) { cryptographer_->CanDecrypt(specifics.encrypted())) {
// Encrypted, but we know the key. // Encrypted, but we know the key.
if (DecryptSpecifics(cryptographer_.get(), specifics, &data.specifics)) { if (DecryptSpecifics(cryptographer_.get(), specifics, &data.specifics)) {
entity->ReceiveUpdate(update_entity->version());
response_data.entity = data.PassToPtr(); response_data.entity = data.PassToPtr();
response_data.encryption_key_name = specifics.encrypted().key_name(); response_data.encryption_key_name = specifics.encrypted().key_name();
entity->ReceiveUpdate(response_data);
pending_updates_.push_back(response_data); pending_updates_.push_back(response_data);
} }
} else if (specifics.has_encrypted() && } else if (specifics.has_encrypted() &&
......
...@@ -162,7 +162,7 @@ class ModelTypeWorker : public syncer::UpdateHandler, ...@@ -162,7 +162,7 @@ class ModelTypeWorker : public syncer::UpdateHandler,
// Interface used to access and send nudges to the sync scheduler. Not owned. // Interface used to access and send nudges to the sync scheduler. Not owned.
syncer::NudgeHandler* nudge_handler_; syncer::NudgeHandler* nudge_handler_;
// A map of per-entity information known to this object. // A map of per-entity information, keyed by client_tag_hash.
// //
// When commits are pending, their information is stored here. This // When commits are pending, their information is stored here. This
// information is dropped from memory when the commit succeeds or gets // information is dropped from memory when the commit succeeds or gets
...@@ -170,9 +170,7 @@ class ModelTypeWorker : public syncer::UpdateHandler, ...@@ -170,9 +170,7 @@ class ModelTypeWorker : public syncer::UpdateHandler,
// //
// This also stores some information related to received server state in // This also stores some information related to received server state in
// order to implement reflection blocking and conflict detection. This // order to implement reflection blocking and conflict detection. This
// information is kept in memory indefinitely. With a bit more coordination // information is kept in memory indefinitely.
// with the model thread, we could optimize this to reduce memory usage in
// the steady state.
EntityMap entities_; EntityMap entities_;
// Accumulates all the updates from a single GetUpdates cycle in memory so // Accumulates all the updates from a single GetUpdates cycle in memory so
......
...@@ -34,7 +34,6 @@ bool WorkerEntityTracker::HasPendingCommit() const { ...@@ -34,7 +34,6 @@ bool WorkerEntityTracker::HasPendingCommit() const {
void WorkerEntityTracker::PopulateCommitProto( void WorkerEntityTracker::PopulateCommitProto(
sync_pb::SyncEntity* commit_entity) const { sync_pb::SyncEntity* commit_entity) const {
DCHECK(HasPendingCommit()); DCHECK(HasPendingCommit());
DCHECK(!client_tag_hash_.empty());
if (!id_.empty()) { if (!id_.empty()) {
commit_entity->set_id_string(id_); commit_entity->set_id_string(id_);
...@@ -125,11 +124,12 @@ void WorkerEntityTracker::ReceiveCommitResponse(CommitResponseData* ack) { ...@@ -125,11 +124,12 @@ void WorkerEntityTracker::ReceiveCommitResponse(CommitResponseData* ack) {
ClearPendingCommit(); ClearPendingCommit();
} }
void WorkerEntityTracker::ReceiveUpdate(int64_t version) { void WorkerEntityTracker::ReceiveUpdate(const UpdateResponseData& update) {
if (version <= highest_gu_response_version_) if (update.response_version <= highest_gu_response_version_)
return; return;
highest_gu_response_version_ = version; highest_gu_response_version_ = update.response_version;
id_ = update.entity->id;
// Got an applicable update newer than any pending updates. It must be safe // Got an applicable update newer than any pending updates. It must be safe
// to discard the old encrypted update, if there was one. // to discard the old encrypted update, if there was one.
......
...@@ -56,7 +56,7 @@ class WorkerEntityTracker { ...@@ -56,7 +56,7 @@ class WorkerEntityTracker {
void ReceiveCommitResponse(CommitResponseData* ack); void ReceiveCommitResponse(CommitResponseData* ack);
// Handles receipt of an update from the server. // Handles receipt of an update from the server.
void ReceiveUpdate(int64_t version); void ReceiveUpdate(const UpdateResponseData& update);
// Handles the receipt of an encrypted update from the server. // Handles the receipt of an encrypted update from the server.
// //
...@@ -92,7 +92,7 @@ class WorkerEntityTracker { ...@@ -92,7 +92,7 @@ class WorkerEntityTracker {
std::string id_; std::string id_;
// The hashed client tag for this entry. // The hashed client tag for this entry.
std::string client_tag_hash_; const std::string client_tag_hash_;
// The highest version seen in a commit response for this entry. // The highest version seen in a commit response for this entry.
int64_t highest_commit_response_version_; int64_t highest_commit_response_version_;
......
...@@ -37,7 +37,7 @@ class WorkerEntityTrackerTest : public ::testing::Test { ...@@ -37,7 +37,7 @@ class WorkerEntityTrackerTest : public ::testing::Test {
kSpecificsHash("somehash"), kSpecificsHash("somehash"),
kCtime(base::Time::UnixEpoch() + base::TimeDelta::FromDays(10)), kCtime(base::Time::UnixEpoch() + base::TimeDelta::FromDays(10)),
kMtime(base::Time::UnixEpoch() + base::TimeDelta::FromDays(20)), kMtime(base::Time::UnixEpoch() + base::TimeDelta::FromDays(20)),
entity_(new WorkerEntityTracker(kServerId, kClientTagHash)) { entity_(new WorkerEntityTracker("", kClientTagHash)) {
specifics.mutable_preference()->set_name(kClientTag); specifics.mutable_preference()->set_name(kClientTag);
specifics.mutable_preference()->set_value("pref.value"); specifics.mutable_preference()->set_value("pref.value");
} }
...@@ -86,8 +86,10 @@ class WorkerEntityTrackerTest : public ::testing::Test { ...@@ -86,8 +86,10 @@ class WorkerEntityTrackerTest : public ::testing::Test {
// Construct a new entity from a server update. Then receive another update. // Construct a new entity from a server update. Then receive another update.
TEST_F(WorkerEntityTrackerTest, FromUpdateResponse) { TEST_F(WorkerEntityTrackerTest, FromUpdateResponse) {
EXPECT_FALSE(entity_->HasPendingCommit()); EXPECT_FALSE(entity_->HasPendingCommit());
entity_->ReceiveUpdate(20); EXPECT_EQ("", entity_->id());
entity_->ReceiveUpdate(MakeUpdateResponseData(20));
EXPECT_FALSE(entity_->HasPendingCommit()); EXPECT_FALSE(entity_->HasPendingCommit());
EXPECT_EQ(kServerId, entity_->id());
} }
// Construct a new entity from a commit request. Then serialize it. // Construct a new entity from a commit request. Then serialize it.
...@@ -96,11 +98,12 @@ TEST_F(WorkerEntityTrackerTest, FromCommitRequest) { ...@@ -96,11 +98,12 @@ TEST_F(WorkerEntityTrackerTest, FromCommitRequest) {
const int64_t kBaseVersion = 33; const int64_t kBaseVersion = 33;
CommitRequestData data = MakeCommitRequestData(kSequenceNumber, kBaseVersion); CommitRequestData data = MakeCommitRequestData(kSequenceNumber, kBaseVersion);
entity_->RequestCommit(data); entity_->RequestCommit(data);
EXPECT_EQ("", entity_->id());
ASSERT_TRUE(entity_->HasPendingCommit()); ASSERT_TRUE(entity_->HasPendingCommit());
sync_pb::SyncEntity pb_entity; sync_pb::SyncEntity pb_entity;
entity_->PopulateCommitProto(&pb_entity); entity_->PopulateCommitProto(&pb_entity);
EXPECT_EQ(kServerId, pb_entity.id_string()); EXPECT_EQ("", pb_entity.id_string());
EXPECT_EQ(kClientTagHash, pb_entity.client_defined_unique_tag()); EXPECT_EQ(kClientTagHash, pb_entity.client_defined_unique_tag());
EXPECT_EQ(kBaseVersion, pb_entity.version()); EXPECT_EQ(kBaseVersion, pb_entity.version());
EXPECT_EQ(kCtime, syncer::ProtoTimeToTime(pb_entity.ctime())); EXPECT_EQ(kCtime, syncer::ProtoTimeToTime(pb_entity.ctime()));
...@@ -119,6 +122,13 @@ TEST_F(WorkerEntityTrackerTest, FromCommitRequest) { ...@@ -119,6 +122,13 @@ TEST_F(WorkerEntityTrackerTest, FromCommitRequest) {
EXPECT_EQ(kSequenceNumber, ack.sequence_number); EXPECT_EQ(kSequenceNumber, ack.sequence_number);
EXPECT_EQ(kSpecificsHash, ack.specifics_hash); EXPECT_EQ(kSpecificsHash, ack.specifics_hash);
EXPECT_FALSE(entity_->HasPendingCommit()); EXPECT_FALSE(entity_->HasPendingCommit());
EXPECT_EQ(kServerId, entity_->id());
CommitRequestData data2 =
MakeCommitRequestData(kSequenceNumber + 1, ack.response_version);
entity_->RequestCommit(data2);
entity_->PopulateCommitProto(&pb_entity);
EXPECT_EQ(kServerId, pb_entity.id_string());
} }
// Start with a server initiated entity. Commit over top of it. // Start with a server initiated entity. Commit over top of it.
...@@ -130,7 +140,7 @@ TEST_F(WorkerEntityTrackerTest, RequestCommit) { ...@@ -130,7 +140,7 @@ TEST_F(WorkerEntityTrackerTest, RequestCommit) {
// Start with a server initiated entity. Fail to request a commit because of // Start with a server initiated entity. Fail to request a commit because of
// an out of date base version. // an out of date base version.
TEST_F(WorkerEntityTrackerTest, RequestCommitFailure) { TEST_F(WorkerEntityTrackerTest, RequestCommitFailure) {
entity_->ReceiveUpdate(10); entity_->ReceiveUpdate(MakeUpdateResponseData(10));
EXPECT_FALSE(entity_->HasPendingCommit()); EXPECT_FALSE(entity_->HasPendingCommit());
entity_->RequestCommit( entity_->RequestCommit(
MakeCommitRequestData(23, 5 /* base_version 5 < 10 */)); MakeCommitRequestData(23, 5 /* base_version 5 < 10 */));
...@@ -144,7 +154,7 @@ TEST_F(WorkerEntityTrackerTest, UpdateClobbersCommit) { ...@@ -144,7 +154,7 @@ TEST_F(WorkerEntityTrackerTest, UpdateClobbersCommit) {
EXPECT_TRUE(entity_->HasPendingCommit()); EXPECT_TRUE(entity_->HasPendingCommit());
entity_->ReceiveUpdate(400); // Version 400 > 33. entity_->ReceiveUpdate(MakeUpdateResponseData(400)); // Version 400 > 33.
EXPECT_FALSE(entity_->HasPendingCommit()); EXPECT_FALSE(entity_->HasPendingCommit());
} }
...@@ -156,7 +166,7 @@ TEST_F(WorkerEntityTrackerTest, ReflectedUpdateDoesntClobberCommit) { ...@@ -156,7 +166,7 @@ TEST_F(WorkerEntityTrackerTest, ReflectedUpdateDoesntClobberCommit) {
EXPECT_TRUE(entity_->HasPendingCommit()); EXPECT_TRUE(entity_->HasPendingCommit());
entity_->ReceiveUpdate(33); // Version 33 == 33. entity_->ReceiveUpdate(MakeUpdateResponseData(33)); // Version 33 == 33.
EXPECT_TRUE(entity_->HasPendingCommit()); EXPECT_TRUE(entity_->HasPendingCommit());
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment