chromium/chrome/browser/ash/app_mode/auto_sleep/repeating_time_interval_task_executor_unittest.cc

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

#include "chrome/browser/ash/app_mode/auto_sleep/fake_repeating_time_interval_task_executor.h"

#include <memory>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/test/repeating_test_future.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_test_policy_builder.h"
#include "chromeos/ash/components/policy/weekly_time/weekly_time.h"
#include "chromeos/ash/components/policy/weekly_time/weekly_time_interval.h"
#include "chromeos/ash/components/settings/scoped_timezone_settings.h"
#include "chromeos/ash/components/settings/timezone_settings.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "chromeos/dbus/power/native_timer.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

using DayOfWeek = DeviceWeeklyScheduledSuspendTestPolicyBuilder::DayOfWeek;

struct IntervalTestFutures {
  base::test::TestFuture<base::TimeDelta> interval_start;
  base::test::TestFuture<void> interval_end;
};

}  // namespace

class RepeatingTimeIntervalTaskExecutorTest : public testing::Test {
 public:
  RepeatingTimeIntervalTaskExecutorTest() = default;

  RepeatingTimeIntervalTaskExecutorTest(
      RepeatingTimeIntervalTaskExecutorTest&) = delete;
  RepeatingTimeIntervalTaskExecutorTest& operator=(
      RepeatingTimeIntervalTaskExecutorTest&) = delete;

  ~RepeatingTimeIntervalTaskExecutorTest() override = default;

  // testing::Test:
  void SetUp() override {
    chromeos::PowerManagerClient::InitializeFake();
    chromeos::FakePowerManagerClient::Get()->set_tick_clock(
        task_environment_.GetMockTickClock());
  }

  void TearDown() override {
    chromeos::PowerManagerClient::Shutdown();
  }

  void FastForwardTimeTo(const policy::WeeklyTime& weekly_time,
                         base::TimeDelta delta = base::TimeDelta()) {
    base::Time current_time = task_environment_.GetMockClock()->Now();
    policy::WeeklyTime current_weekly_time =
        policy::WeeklyTime::GetLocalWeeklyTime(current_time);

    base::TimeDelta duration = current_weekly_time.GetDurationTo(weekly_time);
    task_environment_.FastForwardBy(duration + delta);
  }

  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> CreateTestTaskExecutor(
      const policy::WeeklyTimeInterval& interval,
      base::RepeatingCallback<void(base::TimeDelta)> on_interval_start_callback,
      base::RepeatingClosure on_interval_end_future) {
    return std::make_unique<FakeRepeatingTimeIntervalTaskExecutor>(
        interval, on_interval_start_callback, on_interval_end_future,
        task_environment_.GetMockClock(), task_environment_.GetMockTickClock());
  }

  policy::WeeklyTimeInterval CreateWeeklyTimeInterval(
      DayOfWeek start_day_of_week,
      const base::TimeDelta& start_time_of_day,
      DayOfWeek end_day_of_week,
      const base::TimeDelta& end_time_of_day) {
    policy::WeeklyTime start{
        start_day_of_week, static_cast<int>(start_time_of_day.InMilliseconds()),
        std::nullopt};
    policy::WeeklyTime end{end_day_of_week,
                           static_cast<int>(end_time_of_day.InMilliseconds()),
                           std::nullopt};
    policy::WeeklyTimeInterval interval{start, end};

    return interval;
  }

  base::test::TaskEnvironment* task_environment() { return &task_environment_; }

 private:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::IO,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       TaskExecutorWorksWhenTimeFallsInCurrentInterval) {
  IntervalTestFutures future;

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::FRIDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::SATURDAY,
                               base::Hours(6)  // 6:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  // Confirm that interval start callback is executed when the current time is
  // at the start of the interval.
  EXPECT_FALSE(future.interval_start.IsReady());
  FastForwardTimeTo(interval.start());
  task_executor->ScheduleTimer();
  EXPECT_TRUE(!future.interval_start.Take().is_zero());

  // Confirm that interval end callback is executed when the timer is
  // finished.
  EXPECT_FALSE(future.interval_end.IsReady());
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       TaskExecutorWorksWhenTimeFallsInMiddleOfInterval) {
  IntervalTestFutures future;

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::FRIDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::SATURDAY,
                               base::Hours(6)  // 6:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());
  // Confirm that interval start callback is executed when the current time is
  // at the middle of the interval.
  auto delta = interval.start().GetDurationTo(interval.end()) / 2;
  EXPECT_FALSE(future.interval_start.IsReady());

  FastForwardTimeTo(interval.start());
  task_environment()->FastForwardBy(delta);
  task_executor->ScheduleTimer();
  EXPECT_TRUE(!future.interval_start.Take().is_zero());

  EXPECT_FALSE(future.interval_end.IsReady());
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest, TaskExecutorRunsInTheFuture) {
  IntervalTestFutures future;

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::FRIDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::SATURDAY,
                               base::Hours(6)  // 6:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  // Confirm that when you're outside an interval and then start the
  // timer, it schedules the timer for the future.
  EXPECT_FALSE(future.interval_start.IsReady());
  FastForwardTimeTo(interval.start(), -base::Minutes(5));
  task_executor->ScheduleTimer();
  // Run until idle to make sure that any native timer tasks are scheduled.
  task_environment()->RunUntilIdle();

  EXPECT_FALSE(future.interval_start.IsReady());

  FastForwardTimeTo(interval.start());
  EXPECT_TRUE(!future.interval_start.Take().is_zero());

  EXPECT_FALSE(future.interval_end.IsReady());
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest, TaskExecutorRunsEveryWeek) {
  IntervalTestFutures future;
  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::WEDNESDAY,
                               base::Hours(7),  // 7:00 AM
                               DayOfWeek::WEDNESDAY,
                               base::Hours(21)  // 9:00 PM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  // Move time to before interval start so that test expectations are met and we
  // do not get multiple values in a test future that expects only one.
  FastForwardTimeTo(interval.start(), -base::Minutes(5));

  task_executor->ScheduleTimer();

  for (size_t i = 0; i < 3; i++) {
    FastForwardTimeTo(interval.start());
    EXPECT_TRUE(!future.interval_start.Take().is_zero());

    EXPECT_FALSE(future.interval_end.IsReady());
    FastForwardTimeTo(interval.end());
    EXPECT_TRUE(future.interval_end.WaitAndClear());
  }
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       TwoTaskExecutorsWorkConcurrently) {
  IntervalTestFutures future;
  policy::WeeklyTimeInterval interval_1 =
      CreateWeeklyTimeInterval(DayOfWeek::WEDNESDAY,
                               base::Hours(7),  // 7:00 AM
                               DayOfWeek::WEDNESDAY,
                               base::Hours(21)  // 9:00 PM
      );

  policy::WeeklyTimeInterval interval_2 =
      CreateWeeklyTimeInterval(DayOfWeek::WEDNESDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::WEDNESDAY,
                               base::Hours(7)  // 7:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor_1 =
      CreateTestTaskExecutor(interval_1,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor_2 =
      CreateTestTaskExecutor(interval_2,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  FastForwardTimeTo(interval_1.start());
  task_executor_1->ScheduleTimer();
  task_executor_2->ScheduleTimer();

  EXPECT_TRUE(!future.interval_start.Take().is_zero());
  FastForwardTimeTo(interval_1.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());

  // Do not need to fast forward the time, since the end of the first interval
  // overlaps with the start of the second.
  EXPECT_TRUE(!future.interval_start.Take().is_zero());
  FastForwardTimeTo(interval_2.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest, TimezoneChangesReprogramTimer) {
  auto scoped_timezone_settings =
      std::make_unique<system::ScopedTimezoneSettings>(u"PST");

  IntervalTestFutures future;

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::FRIDAY,
                               base::Hours(12),  // 12:00 PM
                               DayOfWeek::SATURDAY,
                               base::Hours(8)  // 8:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  FastForwardTimeTo(interval.start(), -base::Hours(8));
  task_executor->ScheduleTimer();
  // Change the time zone to CET which is 8 hours ahead of PST and confirm that
  // interval should start immediately as the timer should have been
  // reprogrammed.
  scoped_timezone_settings->SetTimezoneFromID(u"CET");
  // Confirm that the timer is executed as you're in a timezone where the
  // current time is already inside the interval.
  EXPECT_TRUE(!future.interval_start.Take().is_zero());
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest, TimezoneChangesRestartTimer) {
  auto scoped_timezone_settings =
      std::make_unique<system::ScopedTimezoneSettings>(u"GMT");

  IntervalTestFutures future;

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::MONDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::TUESDAY,
                               base::Hours(8)  // 8:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             future.interval_start.GetRepeatingCallback(),
                             future.interval_end.GetRepeatingCallback());

  FastForwardTimeTo(interval.start());
  task_executor->ScheduleTimer();

  EXPECT_TRUE(!future.interval_start.Take().is_zero());
  // Change the time zone to GMT+1 and confirm that
  // interval should start immediately as the timer should have been
  // reprogrammed.
  scoped_timezone_settings->SetTimezoneFromID(u"GMT+1");
  EXPECT_TRUE(future.interval_end.WaitAndClear());

  // Confirm that the timer is executed as you're in a timezone where the
  // current time is already inside the interval.
  EXPECT_TRUE(!future.interval_start.Take().is_zero());
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(future.interval_end.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       TimezoneChangeToSameTimezoneDoesNotRestartTimer) {
  base::test::TestFuture<void> interval_end_future;
  auto scoped_timezone_settings =
      std::make_unique<system::ScopedTimezoneSettings>(u"GMT");
  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::MONDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::TUESDAY,
                               base::Hours(8)  // 8:00 AM
      );
  int interval_start_callback_count = 0;
  base::RepeatingCallback<void(base::TimeDelta)> interval_start_callback =
      base::BindRepeating(
          [](int& interval_start_callback_count, base::TimeDelta delta) {
            interval_start_callback_count++;
          },
          std::ref(interval_start_callback_count));

  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval, interval_start_callback,
                             interval_end_future.GetRepeatingCallback());

  FastForwardTimeTo(interval.start());
  EXPECT_EQ(interval_start_callback_count, 0);
  task_executor->ScheduleTimer();
  EXPECT_EQ(interval_start_callback_count, 1);
  scoped_timezone_settings->SetTimezoneFromID(u"GMT");
  task_environment()->RunUntilIdle();

  // Confirm that the interval doesn't start again when the callback is called
  // for the same timezone.
  EXPECT_EQ(interval_start_callback_count, 1);
  FastForwardTimeTo(interval.end());
  EXPECT_TRUE(interval_end_future.WaitAndClear());
  EXPECT_EQ(interval_start_callback_count, 1);
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       TimezoneChangesSendsNotifyUserNotification) {
  auto scoped_timezone_settings =
      std::make_unique<system::ScopedTimezoneSettings>(u"GMT");

  base::test::TestFuture<void> user_activity_future;
  chromeos::FakePowerManagerClient::Get()->set_user_activity_callback(
      user_activity_future.GetRepeatingCallback());

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::MONDAY,
                               base::Hours(21),  // 9:00 PM
                               DayOfWeek::TUESDAY,
                               base::Hours(8)  // 8:00 AM
      );
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(interval,
                             base::BindRepeating([](base::TimeDelta delta) {}),
                             base::BindRepeating([]() {}));

  FastForwardTimeTo(interval.start());
  task_executor->ScheduleTimer();
  // Confirm that changing the timezone fires a user activity callback to cancel
  // any pending suspend calls.
  scoped_timezone_settings->SetTimezoneFromID(u"GMT-1");
  EXPECT_TRUE(user_activity_future.WaitAndClear());
}

TEST_F(RepeatingTimeIntervalTaskExecutorTest,
       ChangingTimeZoneWithoutStartingExecutorIsNoOp) {
  auto scoped_timezone_settings =
      std::make_unique<system::ScopedTimezoneSettings>(u"PST");

  policy::WeeklyTimeInterval interval =
      CreateWeeklyTimeInterval(DayOfWeek::MONDAY,
                               base::Hours(12),  // 12:00 PM
                               DayOfWeek::TUESDAY,
                               base::Hours(8)  // 8:00 AM
      );
  // Bind lambdas with `FAIL` which will ensure that if the callbacks get
  // called, the test will fail.
  std::unique_ptr<FakeRepeatingTimeIntervalTaskExecutor> task_executor =
      CreateTestTaskExecutor(
          interval, base::BindRepeating([](base::TimeDelta) { FAIL(); }),
          base::BindRepeating([]() { FAIL(); }));
  FastForwardTimeTo(interval.start(), -base::Hours(8));
  scoped_timezone_settings->SetTimezoneFromID(u"CET");
  FastForwardTimeTo(interval.start());
  task_environment()->RunUntilIdle();
  FastForwardTimeTo(interval.end());
  task_environment()->RunUntilIdle();
}

}  // namespace ash