|  | // Copyright 2015 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 "components/cryptauth/sync_scheduler_impl.h" | 
|  |  | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/macros.h" | 
|  | #include "base/memory/ptr_util.h" | 
|  | #include "base/timer/mock_timer.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace cryptauth { | 
|  |  | 
|  | using Strategy = SyncScheduler::Strategy; | 
|  | using SyncState = SyncScheduler::SyncState; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // Constants configuring the the scheduler. | 
|  | const int kElapsedTimeDays = 40; | 
|  | const int kRefreshPeriodDays = 30; | 
|  | const int kRecoveryPeriodSeconds = 10; | 
|  | const double kMaxJitterPercentage = 0.1; | 
|  | const char kTestSchedulerName[] = "TestSyncSchedulerImpl"; | 
|  |  | 
|  | // Returns true if |jittered_time_delta| is within the range of a jittered | 
|  | // |base_time_delta| with a maximum of |max_jitter_ratio|. | 
|  | bool IsTimeDeltaWithinJitter(const base::TimeDelta& base_time_delta, | 
|  | const base::TimeDelta& jittered_time_delta, | 
|  | double max_jitter_ratio) { | 
|  | if (base_time_delta.is_zero()) | 
|  | return jittered_time_delta.is_zero(); | 
|  |  | 
|  | base::TimeDelta difference = | 
|  | (jittered_time_delta - base_time_delta).magnitude(); | 
|  | double percentage_of_base = | 
|  | difference.InMillisecondsF() / base_time_delta.InMillisecondsF(); | 
|  | return percentage_of_base < max_jitter_ratio; | 
|  | } | 
|  |  | 
|  | // Test harness for the SyncSchedulerImpl to create MockTimers. | 
|  | class TestSyncSchedulerImpl : public SyncSchedulerImpl { | 
|  | public: | 
|  | TestSyncSchedulerImpl(Delegate* delegate, | 
|  | base::TimeDelta refresh_period, | 
|  | base::TimeDelta recovery_period, | 
|  | double max_jitter_ratio) | 
|  | : SyncSchedulerImpl(delegate, | 
|  | refresh_period, | 
|  | recovery_period, | 
|  | max_jitter_ratio, | 
|  | kTestSchedulerName) {} | 
|  |  | 
|  | ~TestSyncSchedulerImpl() override {} | 
|  |  | 
|  | base::MockTimer* timer() { return mock_timer_; } | 
|  |  | 
|  | private: | 
|  | std::unique_ptr<base::Timer> CreateTimer() override { | 
|  | bool retain_user_task = false; | 
|  | bool is_repeating = false; | 
|  | mock_timer_ = new base::MockTimer(retain_user_task, is_repeating); | 
|  | return base::WrapUnique(mock_timer_); | 
|  | } | 
|  |  | 
|  | // A timer instance for testing. Owned by the parent scheduler. | 
|  | base::MockTimer* mock_timer_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(TestSyncSchedulerImpl); | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class CryptAuthSyncSchedulerImplTest : public testing::Test, | 
|  | public SyncSchedulerImpl::Delegate { | 
|  | protected: | 
|  | CryptAuthSyncSchedulerImplTest() | 
|  | : refresh_period_(base::TimeDelta::FromDays(kRefreshPeriodDays)), | 
|  | base_recovery_period_( | 
|  | base::TimeDelta::FromSeconds(kRecoveryPeriodSeconds)), | 
|  | zero_elapsed_time_(base::TimeDelta::FromSeconds(0)), | 
|  | scheduler_(new TestSyncSchedulerImpl(this, | 
|  | refresh_period_, | 
|  | base_recovery_period_, | 
|  | 0)) {} | 
|  |  | 
|  | ~CryptAuthSyncSchedulerImplTest() override {} | 
|  |  | 
|  | void OnSyncRequested( | 
|  | std::unique_ptr<SyncScheduler::SyncRequest> sync_request) override { | 
|  | sync_request_ = std::move(sync_request); | 
|  | } | 
|  |  | 
|  | base::MockTimer* timer() { return scheduler_->timer(); } | 
|  |  | 
|  | // The time deltas used to configure |scheduler_|. | 
|  | base::TimeDelta refresh_period_; | 
|  | base::TimeDelta base_recovery_period_; | 
|  | base::TimeDelta zero_elapsed_time_; | 
|  |  | 
|  | // The scheduler instance under test. | 
|  | std::unique_ptr<TestSyncSchedulerImpl> scheduler_; | 
|  |  | 
|  | std::unique_ptr<SyncScheduler::SyncRequest> sync_request_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(CryptAuthSyncSchedulerImplTest); | 
|  | }; | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, ForceSyncSuccess) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | EXPECT_EQ(SyncState::WAITING_FOR_REFRESH, scheduler_->GetSyncState()); | 
|  |  | 
|  | scheduler_->ForceSync(); | 
|  | EXPECT_EQ(SyncState::SYNC_IN_PROGRESS, scheduler_->GetSyncState()); | 
|  | EXPECT_TRUE(sync_request_); | 
|  | sync_request_->OnDidComplete(true); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | EXPECT_EQ(SyncState::WAITING_FOR_REFRESH, scheduler_->GetSyncState()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, ForceSyncFailure) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  |  | 
|  | scheduler_->ForceSync(); | 
|  | EXPECT_TRUE(sync_request_); | 
|  | sync_request_->OnDidComplete(false); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, PeriodicRefreshSuccess) { | 
|  | EXPECT_EQ(SyncState::NOT_STARTED, scheduler_->GetSyncState()); | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  |  | 
|  | EXPECT_EQ(refresh_period_, timer()->GetCurrentDelay()); | 
|  | timer()->Fire(); | 
|  | EXPECT_EQ(SyncState::SYNC_IN_PROGRESS, scheduler_->GetSyncState()); | 
|  | ASSERT_TRUE(sync_request_.get()); | 
|  |  | 
|  | sync_request_->OnDidComplete(true); | 
|  | EXPECT_EQ(SyncState::WAITING_FOR_REFRESH, scheduler_->GetSyncState()); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, PeriodicRefreshFailure) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(false); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, AggressiveRecoverySuccess) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::AGGRESSIVE_RECOVERY); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  |  | 
|  | EXPECT_EQ(base_recovery_period_, timer()->GetCurrentDelay()); | 
|  | timer()->Fire(); | 
|  | EXPECT_EQ(SyncState::SYNC_IN_PROGRESS, scheduler_->GetSyncState()); | 
|  | ASSERT_TRUE(sync_request_.get()); | 
|  |  | 
|  | sync_request_->OnDidComplete(true); | 
|  | EXPECT_EQ(SyncState::WAITING_FOR_REFRESH, scheduler_->GetSyncState()); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, AggressiveRecoveryFailure) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::AGGRESSIVE_RECOVERY); | 
|  |  | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(false); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, AggressiveRecoveryBackOff) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::AGGRESSIVE_RECOVERY); | 
|  | base::TimeDelta last_recovery_period = base::TimeDelta::FromSeconds(0); | 
|  |  | 
|  | for (int i = 0; i < 20; ++i) { | 
|  | timer()->Fire(); | 
|  | EXPECT_EQ(SyncState::SYNC_IN_PROGRESS, scheduler_->GetSyncState()); | 
|  | sync_request_->OnDidComplete(false); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  | EXPECT_EQ(SyncState::WAITING_FOR_REFRESH, scheduler_->GetSyncState()); | 
|  |  | 
|  | base::TimeDelta recovery_period = scheduler_->GetTimeToNextSync(); | 
|  | EXPECT_LE(last_recovery_period, recovery_period); | 
|  | last_recovery_period = recovery_period; | 
|  | } | 
|  |  | 
|  | // Backoffs should rapidly converge to the normal refresh period. | 
|  | EXPECT_EQ(refresh_period_, last_recovery_period); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, RefreshFailureRecoverySuccess) { | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  |  | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(false); | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  |  | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(true); | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, SyncImmediatelyForPeriodicRefresh) { | 
|  | scheduler_->Start(base::TimeDelta::FromDays(kElapsedTimeDays), | 
|  | Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_TRUE(scheduler_->GetTimeToNextSync().is_zero()); | 
|  | EXPECT_TRUE(timer()->GetCurrentDelay().is_zero()); | 
|  | timer()->Fire(); | 
|  | EXPECT_TRUE(sync_request_); | 
|  |  | 
|  | EXPECT_EQ(Strategy::PERIODIC_REFRESH, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, | 
|  | SyncImmediatelyForAggressiveRecovery) { | 
|  | scheduler_->Start(base::TimeDelta::FromDays(kElapsedTimeDays), | 
|  | Strategy::AGGRESSIVE_RECOVERY); | 
|  | EXPECT_TRUE(scheduler_->GetTimeToNextSync().is_zero()); | 
|  | EXPECT_TRUE(timer()->GetCurrentDelay().is_zero()); | 
|  | timer()->Fire(); | 
|  | EXPECT_TRUE(sync_request_); | 
|  |  | 
|  | EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, InitialSyncShorterByElapsedTime) { | 
|  | base::TimeDelta elapsed_time = base::TimeDelta::FromDays(2); | 
|  | scheduler_->Start(elapsed_time, Strategy::PERIODIC_REFRESH); | 
|  | EXPECT_EQ(refresh_period_ - elapsed_time, scheduler_->GetTimeToNextSync()); | 
|  | timer()->Fire(); | 
|  | EXPECT_TRUE(sync_request_); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, PeriodicRefreshJitter) { | 
|  | scheduler_.reset(new TestSyncSchedulerImpl( | 
|  | this, refresh_period_, base_recovery_period_, kMaxJitterPercentage)); | 
|  |  | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  |  | 
|  | base::TimeDelta cumulative_jitter = base::TimeDelta::FromSeconds(0); | 
|  | for (int i = 0; i < 10; ++i) { | 
|  | base::TimeDelta next_sync_delta = scheduler_->GetTimeToNextSync(); | 
|  | cumulative_jitter += (next_sync_delta - refresh_period_).magnitude(); | 
|  | EXPECT_TRUE(IsTimeDeltaWithinJitter(refresh_period_, next_sync_delta, | 
|  | kMaxJitterPercentage)); | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(true); | 
|  | } | 
|  |  | 
|  | // The probablility that all periods are randomly equal to |refresh_period_| | 
|  | // is so low that we would expect the heat death of the universe before this | 
|  | // test flakes. | 
|  | EXPECT_FALSE(cumulative_jitter.is_zero()); | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, JitteredTimeDeltaIsNonNegative) { | 
|  | base::TimeDelta zero_delta = base::TimeDelta::FromSeconds(0); | 
|  | double max_jitter_ratio = 1; | 
|  | scheduler_.reset(new TestSyncSchedulerImpl(this, zero_delta, zero_delta, | 
|  | max_jitter_ratio)); | 
|  | scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH); | 
|  |  | 
|  | for (int i = 0; i < 10; ++i) { | 
|  | base::TimeDelta next_sync_delta = scheduler_->GetTimeToNextSync(); | 
|  | EXPECT_GE(zero_delta, next_sync_delta); | 
|  | EXPECT_TRUE( | 
|  | IsTimeDeltaWithinJitter(zero_delta, next_sync_delta, max_jitter_ratio)); | 
|  | timer()->Fire(); | 
|  | sync_request_->OnDidComplete(true); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(CryptAuthSyncSchedulerImplTest, StartWithNegativeElapsedTime) { | 
|  | // This could happen in rare cases where the system clock changes. | 
|  | scheduler_->Start(base::TimeDelta::FromDays(-1000), | 
|  | Strategy::PERIODIC_REFRESH); | 
|  |  | 
|  | base::TimeDelta zero_delta = base::TimeDelta::FromSeconds(0); | 
|  | EXPECT_EQ(zero_delta, scheduler_->GetTimeToNextSync()); | 
|  | EXPECT_EQ(zero_delta, timer()->GetCurrentDelay()); | 
|  | } | 
|  |  | 
|  | }  // namespace cryptauth |