update_engine: add utilities for update staging policy

Add staging_utils files that contain logic to decide what
to changes should be done to the state. This includes:
* Turn off staging if there is a forced update or if OOBE hasn't been
completed
* Keep the current staging state if no changes to the policy are
detected.
* Use a new waiting time if there have been changes to the policy.
* If there is a persisted value that is still valid, use it.

These changes aren't added to the update engine yet. The return value
of CalculateStagingCase will be used in update_attempter in
a switch statement to update the state based on the value.

BUG=chromium:858621
TEST=cros_workon_make update_engine --test
CQ-DEPEND=CL:1142480

Change-Id: Ib06365793618c3f2a357e3ace8c660fe51cdf950
Reviewed-on: https://chromium-review.googlesource.com/1138983
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Adolfo Higueros <adokar@google.com>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/Android.mk b/Android.mk
index 2fc6f46..d1d8488 100644
--- a/Android.mk
+++ b/Android.mk
@@ -335,6 +335,7 @@
     update_manager/real_system_provider.cc \
     update_manager/real_time_provider.cc \
     update_manager/real_updater_provider.cc \
+    update_manager/staging_utils.cc \
     update_manager/state_factory.cc \
     update_manager/update_manager.cc \
     update_manager/update_time_restrictions_policy_impl.cc \
@@ -1021,6 +1022,7 @@
     update_manager/real_system_provider_unittest.cc \
     update_manager/real_time_provider_unittest.cc \
     update_manager/real_updater_provider_unittest.cc \
+    update_manager/staging_utils_unittest.cc \
     update_manager/umtest_utils.cc \
     update_manager/update_manager_unittest.cc \
     update_manager/update_time_restrictions_policy_impl_unittest.cc \
diff --git a/update_engine.gyp b/update_engine.gyp
index 20fb282..cbdce2b 100644
--- a/update_engine.gyp
+++ b/update_engine.gyp
@@ -293,6 +293,7 @@
         'update_manager/real_system_provider.cc',
         'update_manager/real_time_provider.cc',
         'update_manager/real_updater_provider.cc',
+        'update_manager/staging_utils.cc',
         'update_manager/state_factory.cc',
         'update_manager/update_manager.cc',
         'update_manager/update_time_restrictions_policy_impl.cc',
@@ -581,6 +582,7 @@
             'update_manager/real_system_provider_unittest.cc',
             'update_manager/real_time_provider_unittest.cc',
             'update_manager/real_updater_provider_unittest.cc',
+            'update_manager/staging_utils_unittest.cc',
             'update_manager/umtest_utils.cc',
             'update_manager/update_manager_unittest.cc',
             'update_manager/update_time_restrictions_policy_impl_unittest.cc',
diff --git a/update_manager/staging_utils.cc b/update_manager/staging_utils.cc
new file mode 100644
index 0000000..4835ab2
--- /dev/null
+++ b/update_manager/staging_utils.cc
@@ -0,0 +1,142 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/update_manager/staging_utils.h"
+
+#include <utility>
+#include <vector>
+
+#include <base/logging.h>
+#include <base/rand_util.h>
+#include <base/time/time.h>
+#include <policy/device_policy.h>
+
+#include "update_engine/common/constants.h"
+#include "update_engine/common/hardware_interface.h"
+#include "update_engine/common/prefs_interface.h"
+#include "update_engine/system_state.h"
+
+using base::TimeDelta;
+using chromeos_update_engine::kPrefsWallClockStagingWaitPeriod;
+using chromeos_update_engine::PrefsInterface;
+using chromeos_update_engine::SystemState;
+using policy::DevicePolicy;
+
+namespace chromeos_update_manager {
+
+int GetStagingSchedule(const DevicePolicy* device_policy,
+                       StagingSchedule* staging_schedule_out) {
+  StagingSchedule staging_schedule;
+  if (!device_policy->GetDeviceUpdateStagingSchedule(&staging_schedule) ||
+      staging_schedule.empty()) {
+    return 0;
+  }
+
+  // Last percentage of the schedule should be 100.
+  if (staging_schedule.back().percentage != 100) {
+    LOG(ERROR) << "Last percentage of the schedule is not 100, it's: "
+               << staging_schedule.back().percentage;
+    return 0;
+  }
+
+  int previous_days = 0;
+  int previous_percentage = -1;
+  // Ensure that the schedule has a monotonically increasing set of percentages
+  // and that days are also monotonically increasing.
+  for (const auto& staging_pair : staging_schedule) {
+    int days = staging_pair.days;
+    if (previous_days >= days) {
+      LOG(ERROR) << "Days in staging schedule are not monotonically "
+                 << "increasing. Previous value: " << previous_days
+                 << " Current value: " << days;
+      return 0;
+    }
+    previous_days = days;
+    int percentage = staging_pair.percentage;
+    if (previous_percentage >= percentage) {
+      LOG(ERROR) << "Percentages in staging schedule are not monotonically "
+                 << "increasing. Previous value: " << previous_percentage
+                 << " Current value: " << percentage;
+      return 0;
+    }
+    previous_percentage = percentage;
+  }
+  // Modify staging schedule only if the schedule in the device policy is valid.
+  if (staging_schedule_out)
+    *staging_schedule_out = std::move(staging_schedule);
+
+  return previous_days;
+}
+
+int CalculateWaitTimeInDaysFromSchedule(
+    const StagingSchedule& staging_schedule) {
+  int prev_days = 0;
+  int percentage_position = base::RandInt(1, 100);
+  for (const auto& staging_pair : staging_schedule) {
+    int days = staging_pair.days;
+    if (percentage_position <= staging_pair.percentage) {
+      // Scatter between the start of the range and the end.
+      return prev_days + base::RandInt(1, days - prev_days);
+    }
+    prev_days = days;
+  }
+  // Something went wrong.
+  NOTREACHED();
+  return 0;
+}
+
+StagingCase CalculateStagingCase(const DevicePolicy* device_policy,
+                                 PrefsInterface* prefs,
+                                 TimeDelta* staging_wait_time,
+                                 StagingSchedule* staging_schedule) {
+  // Check that the schedule in the device policy is correct.
+  StagingSchedule new_staging_schedule;
+  int max_days = GetStagingSchedule(device_policy, &new_staging_schedule);
+  if (max_days == 0)
+    return StagingCase::kOff;
+
+  // Calculate the new wait time.
+  TimeDelta new_staging_wait_time = TimeDelta::FromDays(
+      CalculateWaitTimeInDaysFromSchedule(new_staging_schedule));
+  DCHECK_GT(new_staging_wait_time.InSeconds(), 0);
+  if (staging_wait_time->InSeconds() > 0) {
+    // If there hasn't been any changes to the schedule and there is a value
+    // set, don't change the waiting time.
+    if (new_staging_schedule == *staging_schedule) {
+      return StagingCase::kNoAction;
+    }
+    // Otherwise, update the schedule and wait time.
+    *staging_wait_time = new_staging_wait_time;
+    *staging_schedule = std::move(new_staging_schedule);
+    return StagingCase::kNoSavedValue;
+  }
+  // Getting this means the schedule changed, update the old schedule.
+  *staging_schedule = std::move(new_staging_schedule);
+
+  int64_t wait_period_in_days;
+  // There exists a persisted value that is valid. That is, it's smaller than
+  // the maximum amount of days of staging set by the user.
+  if (prefs->GetInt64(kPrefsWallClockStagingWaitPeriod, &wait_period_in_days) &&
+      wait_period_in_days > 0 && wait_period_in_days <= max_days) {
+    *staging_wait_time = TimeDelta::FromDays(wait_period_in_days);
+    return StagingCase::kSetStagingFromPref;
+  }
+
+  *staging_wait_time = new_staging_wait_time;
+  return StagingCase::kNoSavedValue;
+}
+
+}  // namespace chromeos_update_manager
diff --git a/update_manager/staging_utils.h b/update_manager/staging_utils.h
new file mode 100644
index 0000000..e91bfeb
--- /dev/null
+++ b/update_manager/staging_utils.h
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef UPDATE_ENGINE_UPDATE_MANAGER_STAGING_UTILS_H_
+#define UPDATE_ENGINE_UPDATE_MANAGER_STAGING_UTILS_H_
+
+#include <utility>
+#include <vector>
+
+#include <base/time/time.h>
+#include <policy/device_policy.h>
+
+#include "update_engine/common/prefs_interface.h"
+
+namespace chromeos_update_manager {
+
+using StagingSchedule = std::vector<policy::DevicePolicy::DayPercentagePair>;
+
+// Possible cases that staging might run into based on the inputs.
+enum class StagingCase {
+  // Staging is off, remove the persisted value.
+  kOff,
+  // Staging is enabled, but there is no valid persisted value, saved value or
+  // the value of the schedule has changed.
+  kNoSavedValue,
+  // Staging is enabled, and there is a valid persisted value.
+  kSetStagingFromPref,
+  // Staging is enabled, and there have been no changes to the schedule.
+  kNoAction
+};
+
+// Calculate the bucket in which the device belongs based on a given staging
+// schedule. |staging_schedule| is assumed to have already been validated.
+int CalculateWaitTimeInDaysFromSchedule(
+    const StagingSchedule& staging_schedule);
+
+// Verifies that |device_policy| contains a valid staging schedule. If
+// |device_policy| contains a valid staging schedule, move it into
+// |staging_schedule_out| and return the total number of days spanned by the
+// schedule. Otherwise, don't modify |staging_schedule_out| and return 0 (which
+// is an invalid value for the length of a schedule).
+int GetStagingSchedule(const policy::DevicePolicy* device_policy,
+                       StagingSchedule* staging_schedule_out);
+
+// Uses the given arguments to check whether staging is on, and whether the
+// state should be updated with a new waiting time or not. |staging_wait_time|
+// should contain the old value of the wait time, it will be replaced with the
+// new calculated wait time value if staging is on. |staging_schedule| should
+// contain the previous staging schedule, if there is a new schedule found, its
+// value will be replaced with the new one.
+StagingCase CalculateStagingCase(const policy::DevicePolicy* device_policy,
+                                 chromeos_update_engine::PrefsInterface* prefs,
+                                 base::TimeDelta* staging_wait_time,
+                                 StagingSchedule* staging_schedule);
+
+}  // namespace chromeos_update_manager
+
+#endif  // UPDATE_ENGINE_UPDATE_MANAGER_STAGING_UTILS_H_
diff --git a/update_manager/staging_utils_unittest.cc b/update_manager/staging_utils_unittest.cc
new file mode 100644
index 0000000..8d75acd
--- /dev/null
+++ b/update_manager/staging_utils_unittest.cc
@@ -0,0 +1,175 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/update_manager/staging_utils.h"
+
+#include <memory>
+#include <utility>
+
+#include <base/time/time.h>
+#include <gtest/gtest.h>
+#include <policy/mock_device_policy.h>
+
+#include "update_engine/common/constants.h"
+#include "update_engine/common/fake_prefs.h"
+
+using base::TimeDelta;
+using chromeos_update_engine::FakePrefs;
+using chromeos_update_engine::kPrefsWallClockStagingWaitPeriod;
+using testing::_;
+using testing::DoAll;
+using testing::Return;
+using testing::SetArgPointee;
+
+namespace chromeos_update_manager {
+
+constexpr TimeDelta kDay = TimeDelta::FromDays(1);
+constexpr int kMaxDays = 28;
+constexpr int kValidDaySum = 14;
+const StagingSchedule valid_schedule = {{2, 0}, {7, 50}, {9, 80}, {14, 100}};
+
+class StagingUtilsScheduleTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    test_wait_time_ = TimeDelta();
+    test_staging_schedule_ = StagingSchedule();
+  }
+
+  void SetStagingSchedule(const StagingSchedule& staging_schedule) {
+    EXPECT_CALL(device_policy_, GetDeviceUpdateStagingSchedule(_))
+        .WillRepeatedly(
+            DoAll(SetArgPointee<0>(staging_schedule), Return(true)));
+  }
+
+  void SetPersistedStagingVal(int64_t wait_time) {
+    EXPECT_TRUE(
+        fake_prefs_.SetInt64(kPrefsWallClockStagingWaitPeriod, wait_time));
+  }
+
+  void TestStagingCase(const StagingCase& expected) {
+    EXPECT_EQ(expected,
+              CalculateStagingCase(&device_policy_,
+                                   &fake_prefs_,
+                                   &test_wait_time_,
+                                   &test_staging_schedule_));
+  }
+
+  void ExpectNoChanges() {
+    EXPECT_EQ(TimeDelta(), test_wait_time_);
+    EXPECT_EQ(StagingSchedule(), test_staging_schedule_);
+  }
+
+  policy::MockDevicePolicy device_policy_;
+  TimeDelta test_wait_time_;
+  StagingSchedule test_staging_schedule_;
+  FakePrefs fake_prefs_;
+};
+
+// Last element should be 100, if not return false.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleInvalidLastElem) {
+  SetStagingSchedule(StagingSchedule{{2, 10}, {4, 20}, {5, 40}});
+  EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+  ExpectNoChanges();
+}
+
+// Percentage should be monotonically increasing.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleNonMonotonic) {
+  SetStagingSchedule(StagingSchedule{{2, 10}, {6, 20}, {11, 20}, {12, 100}});
+  EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+  ExpectNoChanges();
+}
+
+// The days should be monotonically increasing.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleOverMaxDays) {
+  SetStagingSchedule(StagingSchedule{{2, 10}, {4, 20}, {15, 30}, {10, 100}});
+  EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+  ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleValid) {
+  SetStagingSchedule(valid_schedule);
+  EXPECT_EQ(kValidDaySum,
+            GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+  EXPECT_EQ(test_staging_schedule_, valid_schedule);
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffNoSchedule) {
+  // If the function returns false, the schedule shouldn't get used.
+  EXPECT_CALL(device_policy_, GetDeviceUpdateStagingSchedule(_))
+      .WillRepeatedly(DoAll(SetArgPointee<0>(valid_schedule), Return(false)));
+  TestStagingCase(StagingCase::kOff);
+  ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffEmptySchedule) {
+  SetStagingSchedule(StagingSchedule());
+  TestStagingCase(StagingCase::kOff);
+  ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffInvalidSchedule) {
+  // Any invalid schedule should return |StagingCase::kOff|.
+  SetStagingSchedule(StagingSchedule{{3, 30}, {6, 40}});
+  TestStagingCase(StagingCase::kOff);
+  ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOnNoAction) {
+  test_wait_time_ = kDay;
+  // Same as valid schedule, just using std::pair types.
+  StagingSchedule valid_schedule_pairs = {{2, 0}, {7, 50}, {9, 80}, {14, 100}};
+  test_staging_schedule_ = valid_schedule_pairs;
+  SetStagingSchedule(valid_schedule);
+  TestStagingCase(StagingCase::kNoAction);
+  // Vars should not be changed.
+  EXPECT_EQ(kDay, test_wait_time_);
+  EXPECT_EQ(test_staging_schedule_, valid_schedule_pairs);
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingNoSavedValueChangePolicy) {
+  test_wait_time_ = kDay;
+  SetStagingSchedule(valid_schedule);
+  TestStagingCase(StagingCase::kNoSavedValue);
+  // Vars should change since < 2 days should not be possible due to
+  // valid_schedule's value.
+  EXPECT_NE(kDay, test_wait_time_);
+  EXPECT_EQ(test_staging_schedule_, valid_schedule);
+  EXPECT_LE(test_wait_time_, kDay * kMaxDays);
+}
+
+// Tests the case where there was a reboot and there is no persisted value.
+TEST_F(StagingUtilsScheduleTest, StagingNoSavedValueNoPersisted) {
+  SetStagingSchedule(valid_schedule);
+  TestStagingCase(StagingCase::kNoSavedValue);
+  // Vars should change since there are no preset values and there is a new
+  // staging schedule.
+  EXPECT_NE(TimeDelta(), test_wait_time_);
+  EXPECT_EQ(test_staging_schedule_, valid_schedule);
+  EXPECT_LE(test_wait_time_, kDay * kMaxDays);
+}
+
+// If there is a pref set and its value is less than the day count, use that
+// pref.
+TEST_F(StagingUtilsScheduleTest, StagingSetFromPref) {
+  SetStagingSchedule(valid_schedule);
+  SetPersistedStagingVal(5);
+  TestStagingCase(StagingCase::kSetStagingFromPref);
+  // Vars should change.
+  EXPECT_EQ(kDay * 5, test_wait_time_);
+  EXPECT_EQ(test_staging_schedule_, valid_schedule);
+}
+
+}  // namespace chromeos_update_manager