chromium/chromeos/ash/services/device_sync/sync_scheduler_impl_unittest.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/device_sync/sync_scheduler_impl.h"

#include <memory>
#include <utility>

#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "base/timer/mock_timer.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace device_sync {

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();

  const base::TimeDelta difference =
      (jittered_time_delta - base_time_delta).magnitude();
  return (difference / base_time_delta) < max_jitter_ratio;
}

// Test harness for the SyncSchedulerImpl to create MockOneShotTimers.
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(const TestSyncSchedulerImpl&) = delete;
  TestSyncSchedulerImpl& operator=(const TestSyncSchedulerImpl&) = delete;

  ~TestSyncSchedulerImpl() override {}

  base::MockOneShotTimer* timer() { return mock_timer_; }

 private:
  std::unique_ptr<base::OneShotTimer> CreateTimer() override {
    mock_timer_ = new base::MockOneShotTimer();
    return base::WrapUnique(mock_timer_.get());
  }

  // A timer instance for testing. Owned by the parent scheduler.
  raw_ptr<base::MockOneShotTimer, DanglingUntriaged> mock_timer_;
};

}  // namespace

class DeviceSyncSyncSchedulerImplTest : public testing::Test,
                                        public SyncSchedulerImpl::Delegate {
 protected:
  DeviceSyncSyncSchedulerImplTest()
      : refresh_period_(base::Days(kRefreshPeriodDays)),
        base_recovery_period_(base::Seconds(kRecoveryPeriodSeconds)),
        zero_elapsed_time_(base::Seconds(0)),
        scheduler_(new TestSyncSchedulerImpl(this,
                                             refresh_period_,
                                             base_recovery_period_,
                                             0)) {}

  DeviceSyncSyncSchedulerImplTest(const DeviceSyncSyncSchedulerImplTest&) =
      delete;
  DeviceSyncSyncSchedulerImplTest& operator=(
      const DeviceSyncSyncSchedulerImplTest&) = delete;

  ~DeviceSyncSyncSchedulerImplTest() override {}

  void OnSyncRequested(
      std::unique_ptr<SyncScheduler::SyncRequest> sync_request) override {
    sync_request_ = std::move(sync_request);
  }

  base::MockOneShotTimer* 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_;
};

TEST_F(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, AggressiveRecoveryFailure) {
  scheduler_->Start(zero_elapsed_time_, Strategy::AGGRESSIVE_RECOVERY);

  timer()->Fire();
  sync_request_->OnDidComplete(false);
  EXPECT_EQ(Strategy::AGGRESSIVE_RECOVERY, scheduler_->GetStrategy());
}

TEST_F(DeviceSyncSyncSchedulerImplTest, AggressiveRecoveryBackOff) {
  scheduler_->Start(zero_elapsed_time_, Strategy::AGGRESSIVE_RECOVERY);
  base::TimeDelta last_recovery_period = base::Seconds(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(DeviceSyncSyncSchedulerImplTest, 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(DeviceSyncSyncSchedulerImplTest, SyncImmediatelyForPeriodicRefresh) {
  scheduler_->Start(base::Days(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(DeviceSyncSyncSchedulerImplTest, SyncImmediatelyForAggressiveRecovery) {
  scheduler_->Start(base::Days(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(DeviceSyncSyncSchedulerImplTest, InitialSyncShorterByElapsedTime) {
  base::TimeDelta elapsed_time = base::Days(2);
  scheduler_->Start(elapsed_time, Strategy::PERIODIC_REFRESH);
  EXPECT_EQ(refresh_period_ - elapsed_time, scheduler_->GetTimeToNextSync());
  timer()->Fire();
  EXPECT_TRUE(sync_request_);
}

TEST_F(DeviceSyncSyncSchedulerImplTest, PeriodicRefreshJitter) {
  scheduler_ = std::make_unique<TestSyncSchedulerImpl>(
      this, refresh_period_, base_recovery_period_, kMaxJitterPercentage);

  scheduler_->Start(zero_elapsed_time_, Strategy::PERIODIC_REFRESH);

  base::TimeDelta cumulative_jitter = base::Seconds(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(DeviceSyncSyncSchedulerImplTest, JitteredTimeDeltaIsNonNegative) {
  base::TimeDelta zero_delta = base::Seconds(0);
  double max_jitter_ratio = 1;
  scheduler_ = std::make_unique<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(DeviceSyncSyncSchedulerImplTest, StartWithNegativeElapsedTime) {
  // This could happen in rare cases where the system clock changes.
  scheduler_->Start(base::Days(-1000), Strategy::PERIODIC_REFRESH);

  base::TimeDelta zero_delta = base::Seconds(0);
  EXPECT_EQ(zero_delta, scheduler_->GetTimeToNextSync());
  EXPECT_EQ(zero_delta, timer()->GetCurrentDelay());
}

}  // namespace device_sync

}  // namespace ash