diff options
Diffstat (limited to 'chrome/browser/sync/engine/sync_scheduler_unittest.cc')
-rw-r--r-- | chrome/browser/sync/engine/sync_scheduler_unittest.cc | 1035 |
1 files changed, 1035 insertions, 0 deletions
diff --git a/chrome/browser/sync/engine/sync_scheduler_unittest.cc b/chrome/browser/sync/engine/sync_scheduler_unittest.cc new file mode 100644 index 0000000..d7310d5 --- /dev/null +++ b/chrome/browser/sync/engine/sync_scheduler_unittest.cc @@ -0,0 +1,1035 @@ +// Copyright (c) 2011 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 "base/bind.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_callback_factory.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "base/test/test_timeouts.h" +#include "chrome/browser/sync/engine/mock_model_safe_workers.h" +#include "chrome/browser/sync/engine/configure_reason.h" +#include "chrome/browser/sync/engine/sync_scheduler.h" +#include "chrome/browser/sync/engine/syncer.h" +#include "chrome/browser/sync/sessions/test_util.h" +#include "chrome/test/sync/engine/mock_connection_manager.h" +#include "chrome/test/sync/engine/test_directory_setter_upper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gmock/include/gmock/gmock.h" + +using base::TimeDelta; +using base::TimeTicks; +using testing::_; +using testing::AtLeast; +using testing::DoAll; +using testing::Eq; +using testing::Invoke; +using testing::Mock; +using testing::Return; +using testing::WithArg; + +namespace browser_sync { +using sessions::SyncSession; +using sessions::SyncSessionContext; +using sessions::SyncSessionSnapshot; +using syncable::ModelTypeBitSet; +using sync_pb::GetUpdatesCallerInfo; + +class MockSyncer : public Syncer { + public: + MOCK_METHOD3(SyncShare, void(sessions::SyncSession*, SyncerStep, + SyncerStep)); +}; + +// Used when tests want to record syncing activity to examine later. +struct SyncShareRecords { + std::vector<TimeTicks> times; + std::vector<linked_ptr<SyncSessionSnapshot> > snapshots; +}; + +void QuitLoopNow() { + // We use QuitNow() instead of Quit() as the latter may get stalled + // indefinitely in the presence of repeated timers with low delays + // and a slow test (e.g., ThrottlingDoesThrottle [which has a poll + // delay of 5ms] run under TSAN on the trybots). + MessageLoop::current()->QuitNow(); +} + +void RunLoop() { + MessageLoop::current()->Run(); +} + +void PumpLoop() { + // Do it this way instead of RunAllPending to pump loop exactly once + // (necessary in the presence of timers; see comment in + // QuitLoopNow). + MessageLoop::current()->PostTask(FROM_HERE, base::Bind(&QuitLoopNow)); + RunLoop(); +} + +// Convenient to use in tests wishing to analyze SyncShare calls over time. +static const size_t kMinNumSamples = 5; +class SyncSchedulerTest : public testing::Test { + public: + SyncSchedulerTest() + : callback_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)), + context_(NULL), + syncer_(NULL), + delay_(NULL) {} + + class MockDelayProvider : public SyncScheduler::DelayProvider { + public: + MOCK_METHOD1(GetDelay, TimeDelta(const TimeDelta&)); + }; + + virtual void SetUp() { + syncable::ModelTypeBitSet model_types; + model_types[syncable::BOOKMARKS] = true; + model_types[syncable::AUTOFILL] = true; + model_types[syncable::THEMES] = true; + + syncdb_.SetUp(); + syncer_ = new MockSyncer(); + delay_ = NULL; + registrar_.reset(MockModelSafeWorkerRegistrar::PassiveForTypes( + model_types)); + connection_.reset(new MockConnectionManager(syncdb_.manager(), "Test")); + connection_->SetServerReachable(); + context_ = new SyncSessionContext(connection_.get(), syncdb_.manager(), + registrar_.get(), std::vector<SyncEngineEventListener*>()); + context_->set_notifications_enabled(true); + context_->set_account_name("Test"); + scheduler_.reset( + new SyncScheduler("TestSyncScheduler", context_, syncer_)); + } + + SyncScheduler* scheduler() { return scheduler_.get(); } + MockSyncer* syncer() { return syncer_; } + MockDelayProvider* delay() { return delay_; } + MockConnectionManager* connection() { return connection_.get(); } + TimeDelta zero() { return TimeDelta::FromSeconds(0); } + TimeDelta timeout() { + return TimeDelta::FromMilliseconds(TestTimeouts::action_timeout_ms()); + } + + virtual void TearDown() { + PumpLoop(); + scheduler_.reset(); + PumpLoop(); + syncdb_.TearDown(); + } + + void AnalyzePollRun(const SyncShareRecords& records, size_t min_num_samples, + const TimeTicks& optimal_start, const TimeDelta& poll_interval) { + const std::vector<TimeTicks>& data(records.times); + EXPECT_GE(data.size(), min_num_samples); + for (size_t i = 0; i < data.size(); i++) { + SCOPED_TRACE(testing::Message() << "SyncShare # (" << i << ")"); + TimeTicks optimal_next_sync = optimal_start + poll_interval * i; + EXPECT_GE(data[i], optimal_next_sync); + EXPECT_EQ(GetUpdatesCallerInfo::PERIODIC, + records.snapshots[i]->source.updates_source); + } + } + + void DoQuitLoopNow() { + QuitLoopNow(); + } + + void StartSyncScheduler(SyncScheduler::Mode mode) { + scheduler()->Start( + mode, + callback_factory_.NewCallback(&SyncSchedulerTest::DoQuitLoopNow)); + } + + bool GetBackoffAndResetTest() { + syncable::ModelTypeBitSet nudge_types; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, nudge_types, FROM_HERE); + RunLoop(); + + bool backing_off = scheduler()->IsBackingOff(); + scheduler()->Stop(); + syncdb_.TearDown(); + + Mock::VerifyAndClearExpectations(syncer()); + + TearDown(); + SetUp(); + UseMockDelayProvider(); + EXPECT_CALL(*delay(), GetDelay(_)) + .WillRepeatedly(Return(TimeDelta::FromMilliseconds(1))); + return backing_off; + } + + void UseMockDelayProvider() { + delay_ = new MockDelayProvider(); + scheduler_->delay_provider_.reset(delay_); + } + + // Compare a ModelTypeBitSet to a ModelTypePayloadMap, ignoring + // payload values. + bool CompareModelTypeBitSetToModelTypePayloadMap( + const syncable::ModelTypeBitSet& lhs, + const syncable::ModelTypePayloadMap& rhs) { + size_t count = 0; + for (syncable::ModelTypePayloadMap::const_iterator i = rhs.begin(); + i != rhs.end(); ++i, ++count) { + if (!lhs.test(i->first)) + return false; + } + if (lhs.count() != count) + return false; + return true; + } + + SyncSessionContext* context() { return context_; } + + private: + base::ScopedCallbackFactory<SyncSchedulerTest> callback_factory_; + MessageLoop message_loop_; + scoped_ptr<SyncScheduler> scheduler_; + scoped_ptr<MockConnectionManager> connection_; + SyncSessionContext* context_; + MockSyncer* syncer_; + MockDelayProvider* delay_; + scoped_ptr<MockModelSafeWorkerRegistrar> registrar_; + MockDirectorySetterUpper syncdb_; +}; + +void RecordSyncShareImpl(SyncSession* s, SyncShareRecords* record) { + record->times.push_back(TimeTicks::Now()); + record->snapshots.push_back(make_linked_ptr(new SyncSessionSnapshot( + s->TakeSnapshot()))); +} + +ACTION_P(RecordSyncShare, record) { + RecordSyncShareImpl(arg0, record); + QuitLoopNow(); +} + +ACTION_P2(RecordSyncShareMultiple, record, quit_after) { + RecordSyncShareImpl(arg0, record); + EXPECT_LE(record->times.size(), quit_after); + if (record->times.size() >= quit_after) { + QuitLoopNow(); + } +} + +ACTION(AddFailureAndQuitLoopNow) { + ADD_FAILURE(); + QuitLoopNow(); +} + +ACTION(QuitLoopNowAction) { + QuitLoopNow(); +} + +// Test nudge scheduling. +TEST_F(SyncSchedulerTest, Nudge) { + SyncShareRecords records; + syncable::ModelTypeBitSet model_types; + model_types[syncable::BOOKMARKS] = true; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))) + .RetiresOnSaturation(); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, model_types, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records.snapshots[0]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + records.snapshots[0]->source.updates_source); + + Mock::VerifyAndClearExpectations(syncer()); + + // Make sure a second, later, nudge is unaffected by first (no coalescing). + SyncShareRecords records2; + model_types[syncable::BOOKMARKS] = false; + model_types[syncable::AUTOFILL] = true; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records2)))); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, model_types, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, records2.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records2.snapshots[0]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + records2.snapshots[0]->source.updates_source); +} + +// Make sure a regular config command is scheduled fine in the absence of any +// errors. +TEST_F(SyncSchedulerTest, Config) { + SyncShareRecords records; + syncable::ModelTypeBitSet model_types; + model_types[syncable::BOOKMARKS] = true; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + scheduler()->ScheduleConfig( + model_types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records.snapshots[0]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::RECONFIGURATION, + records.snapshots[0]->source.updates_source); +} + +// Simulate a failure and make sure the config request is retried. +TEST_F(SyncSchedulerTest, ConfigWithBackingOff) { + UseMockDelayProvider(); + EXPECT_CALL(*delay(), GetDelay(_)) + .WillRepeatedly(Return(TimeDelta::FromMilliseconds(1))); + SyncShareRecords records; + syncable::ModelTypeBitSet model_types; + model_types[syncable::BOOKMARKS] = true; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + ASSERT_EQ(0U, records.snapshots.size()); + scheduler()->ScheduleConfig( + model_types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + RunLoop(); + + ASSERT_EQ(2U, records.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records.snapshots[1]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::SYNC_CYCLE_CONTINUATION, + records.snapshots[1]->source.updates_source); +} + +// Issue 2 config commands. Second one right after the first has failed +// and make sure LATEST is executed. +TEST_F(SyncSchedulerTest, MultipleConfigWithBackingOff) { + syncable::ModelTypeBitSet model_types1, model_types2; + model_types1[syncable::BOOKMARKS] = true; + model_types2[syncable::AUTOFILL] = true; + UseMockDelayProvider(); + EXPECT_CALL(*delay(), GetDelay(_)) + .WillRepeatedly(Return(TimeDelta::FromMilliseconds(30))); + SyncShareRecords records; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + ASSERT_EQ(0U, records.snapshots.size()); + scheduler()->ScheduleConfig( + model_types1, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + scheduler()->ScheduleConfig( + model_types2, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(2U, records.snapshots.size()); + RunLoop(); + + ASSERT_EQ(3U, records.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types2, + records.snapshots[2]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::RECONFIGURATION, + records.snapshots[2]->source.updates_source); +} + +// Issue a nudge when the config has failed. Make sure both the config and +// nudge are executed. +TEST_F(SyncSchedulerTest, NudgeWithConfigWithBackingOff) { + syncable::ModelTypeBitSet model_types; + model_types[syncable::BOOKMARKS] = true; + UseMockDelayProvider(); + EXPECT_CALL(*delay(), GetDelay(_)) + .WillRepeatedly(Return(TimeDelta::FromMilliseconds(50))); + SyncShareRecords records; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + ASSERT_EQ(0U, records.snapshots.size()); + scheduler()->ScheduleConfig( + model_types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, model_types, FROM_HERE); + RunLoop(); + + ASSERT_EQ(2U, records.snapshots.size()); + RunLoop(); + + // Now change the mode so nudge can execute. + ASSERT_EQ(3U, records.snapshots.size()); + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + ASSERT_EQ(4U, records.snapshots.size()); + + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records.snapshots[2]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::SYNC_CYCLE_CONTINUATION, + records.snapshots[2]->source.updates_source); + + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(model_types, + records.snapshots[3]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + records.snapshots[3]->source.updates_source); + +} + +// Test that nudges are coalesced. +TEST_F(SyncSchedulerTest, NudgeCoalescing) { + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + SyncShareRecords r; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&r)))); + syncable::ModelTypeBitSet types1, types2, types3; + types1[syncable::BOOKMARKS] = true; + types2[syncable::AUTOFILL] = true; + types3[syncable::THEMES] = true; + TimeDelta delay = zero(); + TimeTicks optimal_time = TimeTicks::Now() + delay; + scheduler()->ScheduleNudge( + delay, NUDGE_SOURCE_UNKNOWN, types1, FROM_HERE); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, types2, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, r.snapshots.size()); + EXPECT_GE(r.times[0], optimal_time); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap( + types1 | types2, r.snapshots[0]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + r.snapshots[0]->source.updates_source); + + Mock::VerifyAndClearExpectations(syncer()); + + SyncShareRecords r2; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&r2)))); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_NOTIFICATION, types3, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, r2.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(types3, + r2.snapshots[0]->source.types)); + EXPECT_EQ(GetUpdatesCallerInfo::NOTIFICATION, + r2.snapshots[0]->source.updates_source); +} + +// Test nudge scheduling. +TEST_F(SyncSchedulerTest, NudgeWithPayloads) { + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + SyncShareRecords records; + syncable::ModelTypePayloadMap model_types_with_payloads; + model_types_with_payloads[syncable::BOOKMARKS] = "test"; + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))) + .RetiresOnSaturation(); + scheduler()->ScheduleNudgeWithPayloads( + zero(), NUDGE_SOURCE_LOCAL, model_types_with_payloads, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + EXPECT_EQ(model_types_with_payloads, records.snapshots[0]->source.types); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + records.snapshots[0]->source.updates_source); + + Mock::VerifyAndClearExpectations(syncer()); + + // Make sure a second, later, nudge is unaffected by first (no coalescing). + SyncShareRecords records2; + model_types_with_payloads.erase(syncable::BOOKMARKS); + model_types_with_payloads[syncable::AUTOFILL] = "test2"; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records2)))); + scheduler()->ScheduleNudgeWithPayloads( + zero(), NUDGE_SOURCE_LOCAL, model_types_with_payloads, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, records2.snapshots.size()); + EXPECT_EQ(model_types_with_payloads, records2.snapshots[0]->source.types); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + records2.snapshots[0]->source.updates_source); +} + +// Test that nudges are coalesced. +TEST_F(SyncSchedulerTest, NudgeWithPayloadsCoalescing) { + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + SyncShareRecords r; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&r)))); + syncable::ModelTypePayloadMap types1, types2, types3; + types1[syncable::BOOKMARKS] = "test1"; + types2[syncable::AUTOFILL] = "test2"; + types3[syncable::THEMES] = "test3"; + TimeDelta delay = zero(); + TimeTicks optimal_time = TimeTicks::Now() + delay; + scheduler()->ScheduleNudgeWithPayloads( + delay, NUDGE_SOURCE_UNKNOWN, types1, FROM_HERE); + scheduler()->ScheduleNudgeWithPayloads( + zero(), NUDGE_SOURCE_LOCAL, types2, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, r.snapshots.size()); + EXPECT_GE(r.times[0], optimal_time); + syncable::ModelTypePayloadMap coalesced_types; + syncable::CoalescePayloads(&coalesced_types, types1); + syncable::CoalescePayloads(&coalesced_types, types2); + EXPECT_EQ(coalesced_types, r.snapshots[0]->source.types); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + r.snapshots[0]->source.updates_source); + + Mock::VerifyAndClearExpectations(syncer()); + + SyncShareRecords r2; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&r2)))); + scheduler()->ScheduleNudgeWithPayloads( + zero(), NUDGE_SOURCE_NOTIFICATION, types3, FROM_HERE); + RunLoop(); + + ASSERT_EQ(1U, r2.snapshots.size()); + EXPECT_EQ(types3, r2.snapshots[0]->source.types); + EXPECT_EQ(GetUpdatesCallerInfo::NOTIFICATION, + r2.snapshots[0]->source.updates_source); +} + +// Test that polling works as expected. +TEST_F(SyncSchedulerTest, Polling) { + SyncShareRecords records; + TimeDelta poll_interval(TimeDelta::FromMilliseconds(30)); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(AtLeast(kMinNumSamples)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShareMultiple(&records, kMinNumSamples)))); + + scheduler()->OnReceivedLongPollIntervalUpdate(poll_interval); + + TimeTicks optimal_start = TimeTicks::Now() + poll_interval; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + AnalyzePollRun(records, kMinNumSamples, optimal_start, poll_interval); +} + +// Test that the short poll interval is used. +TEST_F(SyncSchedulerTest, PollNotificationsDisabled) { + SyncShareRecords records; + TimeDelta poll_interval(TimeDelta::FromMilliseconds(30)); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(AtLeast(kMinNumSamples)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShareMultiple(&records, kMinNumSamples)))); + + scheduler()->OnReceivedShortPollIntervalUpdate(poll_interval); + scheduler()->set_notifications_enabled(false); + + TimeTicks optimal_start = TimeTicks::Now() + poll_interval; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + AnalyzePollRun(records, kMinNumSamples, optimal_start, poll_interval); +} + +// Test that polling intervals are updated when needed. +TEST_F(SyncSchedulerTest, PollIntervalUpdate) { + SyncShareRecords records; + TimeDelta poll1(TimeDelta::FromMilliseconds(120)); + TimeDelta poll2(TimeDelta::FromMilliseconds(30)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll1); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(AtLeast(kMinNumSamples)) + .WillOnce(WithArg<0>( + sessions::test_util::SimulatePollIntervalUpdate(poll2))) + .WillRepeatedly( + DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>( + RecordSyncShareMultiple(&records, kMinNumSamples)))); + + TimeTicks optimal_start = TimeTicks::Now() + poll1 + poll2; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + AnalyzePollRun(records, kMinNumSamples, optimal_start, poll2); +} + +// Test that a sync session is run through to completion. +TEST_F(SyncSchedulerTest, HasMoreToSync) { + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateHasMoreToSync)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + QuitLoopNowAction())); + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, ModelTypeBitSet(), FROM_HERE); + RunLoop(); + // If more nudges are scheduled, they'll be waited on by TearDown, and would + // cause our expectation to break. +} + +// Test that no syncing occurs when throttled. +TEST_F(SyncSchedulerTest, ThrottlingDoesThrottle) { + syncable::ModelTypeBitSet types; + types[syncable::BOOKMARKS] = true; + TimeDelta poll(TimeDelta::FromMilliseconds(5)); + TimeDelta throttle(TimeDelta::FromMinutes(10)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(WithArg<0>(sessions::test_util::SimulateThrottled(throttle))) + .WillRepeatedly(AddFailureAndQuitLoopNow()); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, types, FROM_HERE); + PumpLoop(); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + scheduler()->ScheduleConfig( + types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + PumpLoop(); +} + +TEST_F(SyncSchedulerTest, ThrottlingExpires) { + SyncShareRecords records; + TimeDelta poll(TimeDelta::FromMilliseconds(15)); + TimeDelta throttle1(TimeDelta::FromMilliseconds(150)); + TimeDelta throttle2(TimeDelta::FromMinutes(10)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + + ::testing::InSequence seq; + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(WithArg<0>(sessions::test_util::SimulateThrottled(throttle1))) + .RetiresOnSaturation(); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShareMultiple(&records, kMinNumSamples)))); + + TimeTicks optimal_start = TimeTicks::Now() + poll + throttle1; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + AnalyzePollRun(records, kMinNumSamples, optimal_start, poll); +} + +// Test nudges / polls don't run in config mode and config tasks do. +TEST_F(SyncSchedulerTest, ConfigurationMode) { + TimeDelta poll(TimeDelta::FromMilliseconds(15)); + SyncShareRecords records; + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce((Invoke(sessions::test_util::SimulateSuccess), + WithArg<0>(RecordSyncShare(&records)))); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + syncable::ModelTypeBitSet nudge_types; + nudge_types[syncable::AUTOFILL] = true; + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, nudge_types, FROM_HERE); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, nudge_types, FROM_HERE); + + syncable::ModelTypeBitSet config_types; + config_types[syncable::BOOKMARKS] = true; + + scheduler()->ScheduleConfig( + config_types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + RunLoop(); + + ASSERT_EQ(1U, records.snapshots.size()); + EXPECT_TRUE(CompareModelTypeBitSetToModelTypePayloadMap(config_types, + records.snapshots[0]->source.types)); +} + +// Test that exponential backoff is properly triggered. +TEST_F(SyncSchedulerTest, BackoffTriggers) { + UseMockDelayProvider(); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateDownloadUpdatesFailed)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + QuitLoopNowAction())); + EXPECT_FALSE(GetBackoffAndResetTest()); + // Note GetBackoffAndResetTest clears mocks and re-instantiates the syncer. + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateSuccess), + QuitLoopNowAction())); + EXPECT_FALSE(GetBackoffAndResetTest()); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateDownloadUpdatesFailed)) + .WillRepeatedly(DoAll(Invoke( + sessions::test_util::SimulateDownloadUpdatesFailed), + QuitLoopNowAction())); + EXPECT_TRUE(GetBackoffAndResetTest()); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + QuitLoopNowAction())); + EXPECT_TRUE(GetBackoffAndResetTest()); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateDownloadUpdatesFailed)) + .WillOnce(Invoke(sessions::test_util::SimulateDownloadUpdatesFailed)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + QuitLoopNowAction())); + EXPECT_FALSE(GetBackoffAndResetTest()); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + QuitLoopNowAction())); + EXPECT_FALSE(GetBackoffAndResetTest()); +} + +// Test that no polls or extraneous nudges occur when in backoff. +TEST_F(SyncSchedulerTest, BackoffDropsJobs) { + SyncShareRecords r; + TimeDelta poll(TimeDelta::FromMilliseconds(5)); + syncable::ModelTypeBitSet types; + types[syncable::BOOKMARKS] = true; + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + UseMockDelayProvider(); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(2) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + RecordSyncShareMultiple(&r, 2U))); + EXPECT_CALL(*delay(), GetDelay(_)). + WillRepeatedly(Return(TimeDelta::FromDays(1))); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + // Pump loop to get rid of nudge. + PumpLoop(); + + Mock::VerifyAndClearExpectations(syncer()); + ASSERT_EQ(2U, r.snapshots.size()); + EXPECT_EQ(GetUpdatesCallerInfo::PERIODIC, + r.snapshots[0]->source.updates_source); + EXPECT_EQ(GetUpdatesCallerInfo::SYNC_CYCLE_CONTINUATION, + r.snapshots[1]->source.updates_source); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(1) + .WillOnce(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + RecordSyncShare(&r))); + + // We schedule a nudge with enough delay (10X poll interval) that at least + // one or two polls would have taken place. The nudge should succeed. + scheduler()->ScheduleNudge( + poll * 10, NUDGE_SOURCE_LOCAL, types, FROM_HERE); + RunLoop(); + + Mock::VerifyAndClearExpectations(syncer()); + Mock::VerifyAndClearExpectations(delay()); + ASSERT_EQ(3U, r.snapshots.size()); + EXPECT_EQ(GetUpdatesCallerInfo::LOCAL, + r.snapshots[2]->source.updates_source); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(0); + EXPECT_CALL(*delay(), GetDelay(_)).Times(0); + + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + scheduler()->ScheduleConfig( + types, sync_api::CONFIGURE_REASON_RECONFIGURATION); + PumpLoop(); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, types, FROM_HERE); + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, types, FROM_HERE); + PumpLoop(); +} + +// Test that backoff is shaping traffic properly with consecutive errors. +TEST_F(SyncSchedulerTest, BackoffElevation) { + SyncShareRecords r; + const TimeDelta poll(TimeDelta::FromMilliseconds(10)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + UseMockDelayProvider(); + + const TimeDelta first = TimeDelta::FromSeconds(1); + const TimeDelta second = TimeDelta::FromMilliseconds(10); + const TimeDelta third = TimeDelta::FromMilliseconds(20); + const TimeDelta fourth = TimeDelta::FromMilliseconds(30); + const TimeDelta fifth = TimeDelta::FromDays(1); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(kMinNumSamples) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateCommitFailed), + RecordSyncShareMultiple(&r, kMinNumSamples))); + + EXPECT_CALL(*delay(), GetDelay(Eq(first))).WillOnce(Return(second)) + .RetiresOnSaturation(); + EXPECT_CALL(*delay(), GetDelay(Eq(second))).WillOnce(Return(third)) + .RetiresOnSaturation(); + EXPECT_CALL(*delay(), GetDelay(Eq(third))).WillOnce(Return(fourth)) + .RetiresOnSaturation(); + EXPECT_CALL(*delay(), GetDelay(Eq(fourth))).WillOnce(Return(fifth)); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + ASSERT_EQ(kMinNumSamples, r.snapshots.size()); + EXPECT_GE(r.times[2] - r.times[1], second); + EXPECT_GE(r.times[3] - r.times[2], third); + EXPECT_GE(r.times[4] - r.times[3], fourth); +} + +// Test that things go back to normal once a canary task makes forward progress +// following a succession of failures. +TEST_F(SyncSchedulerTest, BackoffRelief) { + SyncShareRecords r; + const TimeDelta poll(TimeDelta::FromMilliseconds(10)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + UseMockDelayProvider(); + + const TimeDelta backoff = TimeDelta::FromMilliseconds(100); + + EXPECT_CALL(*syncer(), SyncShare(_,_,_)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillOnce(Invoke(sessions::test_util::SimulateCommitFailed)) + .WillRepeatedly(DoAll(Invoke(sessions::test_util::SimulateSuccess), + RecordSyncShareMultiple(&r, kMinNumSamples))); + EXPECT_CALL(*delay(), GetDelay(_)).WillOnce(Return(backoff)); + + // Optimal start for the post-backoff poll party. + TimeTicks optimal_start = TimeTicks::Now() + poll + backoff; + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + + // Check for healthy polling after backoff is relieved. + // Can't use AnalyzePollRun because first sync is a continuation. Bleh. + for (size_t i = 0; i < r.times.size(); i++) { + SCOPED_TRACE(testing::Message() << "SyncShare # (" << i << ")"); + TimeTicks optimal_next_sync = optimal_start + poll * i; + EXPECT_GE(r.times[i], optimal_next_sync); + EXPECT_EQ(i == 0 ? GetUpdatesCallerInfo::SYNC_CYCLE_CONTINUATION + : GetUpdatesCallerInfo::PERIODIC, + r.snapshots[i]->source.updates_source); + } +} + +TEST_F(SyncSchedulerTest, GetRecommendedDelay) { + EXPECT_LE(TimeDelta::FromSeconds(0), + SyncScheduler::GetRecommendedDelay(TimeDelta::FromSeconds(0))); + EXPECT_LE(TimeDelta::FromSeconds(1), + SyncScheduler::GetRecommendedDelay(TimeDelta::FromSeconds(1))); + EXPECT_LE(TimeDelta::FromSeconds(50), + SyncScheduler::GetRecommendedDelay(TimeDelta::FromSeconds(50))); + EXPECT_LE(TimeDelta::FromSeconds(10), + SyncScheduler::GetRecommendedDelay(TimeDelta::FromSeconds(10))); + EXPECT_EQ(TimeDelta::FromSeconds(kMaxBackoffSeconds), + SyncScheduler::GetRecommendedDelay( + TimeDelta::FromSeconds(kMaxBackoffSeconds))); + EXPECT_EQ(TimeDelta::FromSeconds(kMaxBackoffSeconds), + SyncScheduler::GetRecommendedDelay( + TimeDelta::FromSeconds(kMaxBackoffSeconds + 1))); +} + +// Test that appropriate syncer steps are requested for each job type. +TEST_F(SyncSchedulerTest, SyncerSteps) { + // Nudges. + EXPECT_CALL(*syncer(), SyncShare(_, SYNCER_BEGIN, SYNCER_END)) + .Times(1); + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, ModelTypeBitSet(), FROM_HERE); + PumpLoop(); + // Pump again to run job. + PumpLoop(); + + scheduler()->Stop(); + Mock::VerifyAndClearExpectations(syncer()); + + // ClearUserData. + EXPECT_CALL(*syncer(), SyncShare(_, CLEAR_PRIVATE_DATA, SYNCER_END)) + .Times(1); + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleClearUserData(); + PumpLoop(); + PumpLoop(); + + scheduler()->Stop(); + + Mock::VerifyAndClearExpectations(syncer()); + // Configuration. + EXPECT_CALL(*syncer(), SyncShare(_, DOWNLOAD_UPDATES, APPLY_UPDATES)); + StartSyncScheduler(SyncScheduler::CONFIGURATION_MODE); + RunLoop(); + + scheduler()->ScheduleConfig( + ModelTypeBitSet(), sync_api::CONFIGURE_REASON_RECONFIGURATION); + PumpLoop(); + PumpLoop(); + + scheduler()->Stop(); + Mock::VerifyAndClearExpectations(syncer()); + + // Poll. + EXPECT_CALL(*syncer(), SyncShare(_, SYNCER_BEGIN, SYNCER_END)) + .Times(AtLeast(1)) + .WillRepeatedly(QuitLoopNowAction()); + const TimeDelta poll(TimeDelta::FromMilliseconds(10)); + scheduler()->OnReceivedLongPollIntervalUpdate(poll); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + // Run again to wait for polling. + RunLoop(); + + scheduler()->Stop(); + Mock::VerifyAndClearExpectations(syncer()); +} + +// Test config tasks don't run during normal mode. +// TODO(tim): Implement this test and then the functionality! +TEST_F(SyncSchedulerTest, DISABLED_NoConfigDuringNormal) { +} + +// Test that starting the syncer thread without a valid connection doesn't +// break things when a connection is detected. +TEST_F(SyncSchedulerTest, StartWhenNotConnected) { + connection()->SetServerNotReachable(); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).WillOnce(QuitLoopNowAction()); + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, ModelTypeBitSet(), FROM_HERE); + // Should save the nudge for until after the server is reachable. + PumpLoop(); + + connection()->SetServerReachable(); + PumpLoop(); +} + +TEST_F(SyncSchedulerTest, SetsPreviousRoutingInfo) { + ModelSafeRoutingInfo info; + EXPECT_TRUE(info == context()->previous_session_routing_info()); + ModelSafeRoutingInfo expected; + context()->registrar()->GetModelSafeRoutingInfo(&expected); + ASSERT_FALSE(expected.empty()); + EXPECT_CALL(*syncer(), SyncShare(_,_,_)).Times(1); + + StartSyncScheduler(SyncScheduler::NORMAL_MODE); + RunLoop(); + + scheduler()->ScheduleNudge( + zero(), NUDGE_SOURCE_LOCAL, ModelTypeBitSet(), FROM_HERE); + PumpLoop(); + // Pump again to run job. + PumpLoop(); + + scheduler()->Stop(); + + EXPECT_TRUE(expected == context()->previous_session_routing_info()); +} + +} // namespace browser_sync |