chromium/chrome/browser/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_controller_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/device_weekly_scheduled_suspend_controller.h"

#include <functional>
#include <memory>
#include <optional>
#include <string>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_test_policy_builder.h"
#include "chrome/browser/ash/app_mode/auto_sleep/fake_repeating_time_interval_task_executor.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chromeos/ash/components/policy/weekly_time/weekly_time.h"
#include "chromeos/ash/components/policy/weekly_time/weekly_time_interval.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

using policy::WeeklyTimeInterval;
using WeeklyTimeIntervals = std::vector<std::unique_ptr<WeeklyTimeInterval>>;

using DayOfWeek = DeviceWeeklyScheduledSuspendTestPolicyBuilder::DayOfWeek;

class DeviceWeeklyScheduledSuspendControllerTest : public testing::Test {
 protected:
  DeviceWeeklyScheduledSuspendControllerTest()
      : testing_local_state_(TestingBrowserProcess::GetGlobal()) {}

  // testing::Test:
  void SetUp() override {
    chromeos::PowerManagerClient::InitializeFake();
    InitController();
    chromeos::FakePowerManagerClient::Get()->set_tick_clock(
        task_environment_.GetMockTickClock());
    device_weekly_scheduled_suspend_controller_
        ->SetTaskExecutorFactoryForTesting(
            std::make_unique<FakeRepeatingTimeIntervalTaskExecutor::Factory>(
                task_environment_.GetMockClock()));
    user_activity_calls_ = 0;
    chromeos::FakePowerManagerClient::Get()->set_user_activity_callback(
        base::BindRepeating([](int& count) { ++count; },
                            std::ref(user_activity_calls_)));
  }

  void TearDown() override {
    // Clear the policy so that task executors can be cleaned up before shutting
    // down the fake power manager.
    UpdatePolicyPref({});
    device_weekly_scheduled_suspend_controller_.reset();
    chromeos::FakePowerManagerClient::Get()->set_tick_clock(nullptr);
    chromeos::FakePowerManagerClient::Get()->set_user_activity_callback(
        base::NullCallback());
    chromeos::PowerManagerClient::Shutdown();
  }

  void UpdatePolicyPref(base::Value::List schedule_list) {
    testing_local_state_.Get()->SetList(prefs::kDeviceWeeklyScheduledSuspend,
                                        std::move(schedule_list));
  }

  void UpdatePolicyAndCheckIntervals(
      const DeviceWeeklyScheduledSuspendTestPolicyBuilder& policy_builder) {
    UpdatePolicyPref(policy_builder.GetAsPrefValue());
    CheckIntervalsInController(policy_builder.GetAsWeeklyTimeIntervals());
  }

  void CheckIntervalsInController(
      const WeeklyTimeIntervals& expected_intervals) {
    const RepeatingTimeIntervalTaskExecutors& interval_executors =
        device_weekly_scheduled_suspend_controller_
            ->GetIntervalExecutorsForTesting();
    ASSERT_EQ(expected_intervals.size(), interval_executors.size());
    for (size_t i = 0; i < expected_intervals.size(); ++i) {
      ASSERT_TRUE(interval_executors[i]);
      EXPECT_EQ(*expected_intervals[i], interval_executors[i]->time_interval());
    }
  }

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

    auto delta_to_future =
        current_weekly_time.GetDurationTo(future_weekly_time);
    auto delta_to_future_plus_dt = delta_to_future + dt;
    ASSERT_TRUE(delta_to_future_plus_dt.is_positive());
    task_environment_.FastForwardBy(delta_to_future_plus_dt);
  }

  DeviceWeeklyScheduledSuspendController*
  device_weekly_scheduled_suspend_controller() {
    return device_weekly_scheduled_suspend_controller_.get();
  }

  void InitController() {
    device_weekly_scheduled_suspend_controller_ =
        std::make_unique<DeviceWeeklyScheduledSuspendController>(
            testing_local_state_.Get());
  }

  int user_activity_calls() const { return user_activity_calls_; }

 private:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::IO,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  ScopedTestingLocalState testing_local_state_;
  std::unique_ptr<DeviceWeeklyScheduledSuspendController>
      device_weekly_scheduled_suspend_controller_;
  int user_activity_calls_;
};

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       HandlesEmptyIntervalsWithNoPolicy) {
  WeeklyTimeIntervals empty_intervals;
  CheckIntervalsInController(empty_intervals);
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest, HandlesEmptyPolicy) {
  DeviceWeeklyScheduledSuspendTestPolicyBuilder empty_policy;
  UpdatePolicyAndCheckIntervals(empty_policy);
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       HandlesValidPolicyWithMultipleIntervals) {
  UpdatePolicyAndCheckIntervals(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(12),
                                    DayOfWeek::WEDNESDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::MONDAY, base::Hours(8)));
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       UpdatesIntervalsOnPolicyChange) {
  UpdatePolicyAndCheckIntervals(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(12),
                                    DayOfWeek::WEDNESDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::MONDAY, base::Hours(8)));

  UpdatePolicyAndCheckIntervals(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::FRIDAY, base::Hours(20), DayOfWeek::MONDAY,
          base::Hours(8)));
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       ClearsIntervalsOnEmptyPolicy) {
  UpdatePolicyAndCheckIntervals(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(12),
                                    DayOfWeek::WEDNESDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::MONDAY, base::Hours(8)));

  UpdatePolicyPref(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().GetAsPrefValue());
  CheckIntervalsInController({});
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       HandlesInvalidPolicyWithMissingStartTime) {
  UpdatePolicyPref(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddInvalidScheduleMissingStart(DayOfWeek::FRIDAY, base::Hours(6))
          .GetAsPrefValue());

  CheckIntervalsInController({});
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       HandlesInvalidPolicyWithMissingEndTime) {
  UpdatePolicyPref(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddInvalidScheduleMissingEnd(DayOfWeek::MONDAY, base::Hours(20))
          .GetAsPrefValue());

  CheckIntervalsInController({});
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       HandlesInvalidPolicyWithOverlappingIntervals) {
  UpdatePolicyPref(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::SATURDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::SATURDAY, base::Hours(6),
                                    DayOfWeek::SUNDAY, base::Hours(20))
          .GetAsPrefValue());

  CheckIntervalsInController({});
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       DoesNotCallSuspendWhenOutsideOfInterval) {
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::FRIDAY, base::Hours(0), DayOfWeek::FRIDAY, base::Hours(9));
  auto intervals = builder.GetAsWeeklyTimeIntervals();
  FastForwardTimeTo(intervals[0]->start(), -base::Hours(5));
  UpdatePolicyPref(builder.GetAsPrefValue());
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);
  FastForwardTimeTo(intervals[0]->start(), -base::Hours(1));
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       CallsSuspendOnIntervalStartEveryWeek) {
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::MONDAY, base::Hours(21), DayOfWeek::TUESDAY,
          base::Hours(7));
  auto intervals = builder.GetAsWeeklyTimeIntervals();
  FastForwardTimeTo(intervals[0]->start(), -base::Minutes(5));
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);
  UpdatePolicyPref(builder.GetAsPrefValue());
  FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);
  FastForwardTimeTo(intervals[0]->end());
  FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 2);
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       CallsSuspendForEverySuspendInterval) {
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::MONDAY, base::Hours(0),
                                    DayOfWeek::MONDAY, base::Hours(9))
          .AddWeeklySuspendInterval(DayOfWeek::TUESDAY, base::Hours(0),
                                    DayOfWeek::TUESDAY, base::Hours(9))
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(0),
                                    DayOfWeek::WEDNESDAY, base::Hours(9));
  auto intervals = builder.GetAsWeeklyTimeIntervals();
  FastForwardTimeTo(intervals[0]->start(), -base::Minutes(5));
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);
  UpdatePolicyPref(builder.GetAsPrefValue());
  int expected_suspend_calls = 0;
  for (auto& interval : intervals) {
    FastForwardTimeTo(interval->start());
    EXPECT_EQ(
        chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(),
        ++expected_suspend_calls);
    FastForwardTimeTo(interval->end());
  }
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       DeviceFullyResumesWhenIntervalEnds) {
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::THURSDAY, base::Hours(21), DayOfWeek::FRIDAY,
          base::Hours(7));
  auto intervals = builder.GetAsWeeklyTimeIntervals();
  UpdatePolicyPref(builder.GetAsPrefValue());

  // Assert the device is not sleeping before the interval starts.
  FastForwardTimeTo(intervals[0]->start(), -base::Seconds(5));
  ASSERT_EQ(user_activity_calls(), 0);
  ASSERT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);

  // Assert the device sleeps when the interval starts.
  FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(user_activity_calls(), 0);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);

  // Device should fully resume (as signaled by user activity) when the interval
  // ends.
  FastForwardTimeTo(intervals[0]->end());
  chromeos::FakePowerManagerClient::Get()->SendDarkSuspendImminent();
  EXPECT_EQ(user_activity_calls(), 1);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);
}

TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
       DeviceDoesNotFullyResumeBeforeIntervalEnds) {
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::THURSDAY, base::Hours(21), DayOfWeek::FRIDAY,
          base::Hours(7));
  auto intervals = builder.GetAsWeeklyTimeIntervals();
  UpdatePolicyPref(builder.GetAsPrefValue());

  // Assert the device is not sleeping before the interval starts.
  FastForwardTimeTo(intervals[0]->start(), -base::Seconds(5));
  ASSERT_EQ(user_activity_calls(), 0);
  ASSERT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 0);

  // Assert the device sleeps when the interval starts.
  FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(user_activity_calls(), 0);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);

  // Dark resumes should not trigger a full resume before the interval ends.
  FastForwardTimeTo(policy::WeeklyTime(DayOfWeek::FRIDAY, 0, std::nullopt));
  chromeos::FakePowerManagerClient::Get()->SendDarkSuspendImminent();
  EXPECT_EQ(user_activity_calls(), 0);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);

  FastForwardTimeTo(intervals[0]->end(), -base::Seconds(5));
  chromeos::FakePowerManagerClient::Get()->SendDarkSuspendImminent();
  EXPECT_EQ(user_activity_calls(), 0);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);

  // Device should fully resume (as signaled by user activity) when the interval
  // ends.
  FastForwardTimeTo(intervals[0]->end());
  chromeos::FakePowerManagerClient::Get()->SendDarkSuspendImminent();
  EXPECT_EQ(user_activity_calls(), 1);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_suspend_calls(), 1);
}

class DeviceWeeklyScheduledSuspendControllerPowerServiceTest
    : public DeviceWeeklyScheduledSuspendControllerTest {
 public:
  // testing::Test:
  void SetUp() override {
    DeviceWeeklyScheduledSuspendControllerTest::SetUp();
    chromeos::FakePowerManagerClient::Get()->SetServiceAvailability(false);
    // Recreate the controller after changing service availability to apply it.
    InitController();
  }
};

TEST_F(DeviceWeeklyScheduledSuspendControllerPowerServiceTest,
       PolicyNotSetWhenPowerServiceIsNotAvailable) {
  UpdatePolicyPref(
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(12),
                                    DayOfWeek::WEDNESDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::MONDAY, base::Hours(8))
          .GetAsPrefValue());
  CheckIntervalsInController({});
}

TEST_F(DeviceWeeklyScheduledSuspendControllerPowerServiceTest,
       PolicyGetsSetWhenPowerManagerIsAvailable) {
  auto policy_builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(12),
                                    DayOfWeek::WEDNESDAY, base::Hours(15))
          .AddWeeklySuspendInterval(DayOfWeek::FRIDAY, base::Hours(20),
                                    DayOfWeek::MONDAY, base::Hours(8));
  UpdatePolicyPref(policy_builder.GetAsPrefValue());
  CheckIntervalsInController({});
  chromeos::FakePowerManagerClient::Get()->SetServiceAvailability(true);
  CheckIntervalsInController(policy_builder.GetAsWeeklyTimeIntervals());
}

}  // namespace ash