AU: Implement exponential back off for 500 and 503 HTTP response codes.

Also refactors the automatic update checks into a separate
scheduler class and adds unit tests for them.

update_check_scheduler.cc                  59 /   59: 100.0%
update_check_scheduler.h                    1 /    1: 100.0%

Note: because the unit tests for this CL use the
UpdateAttempter class, the CL brings in several untested
modules into the test report reducing the overall test
coverage to ~82%.

BUG=2394
TEST=unit tests, gmerged on device and inspected logs

Change-Id: I078b1727b5338f6fc34e51f5e04a375518d63cef

Review URL: http://codereview.chromium.org/3215006
diff --git a/SConstruct b/SConstruct
index 4df1395..abf5253 100644
--- a/SConstruct
+++ b/SConstruct
@@ -223,6 +223,7 @@
                    tarjan.cc
                    topological_sort.cc
                    update_attempter.cc
+                   update_check_scheduler.cc
                    update_metadata.pb.cc
                    utils.cc""")
 main = ['main.cc']
@@ -258,6 +259,7 @@
                             tarjan_unittest.cc
                             test_utils.cc
                             topological_sort_unittest.cc
+                            update_check_scheduler_unittest.cc
                             utils_unittest.cc
                             zip_unittest.cc""")
 unittest_main = ['testrunner.cc']
diff --git a/download_action.h b/download_action.h
index a46124f..189121b 100644
--- a/download_action.h
+++ b/download_action.h
@@ -76,6 +76,8 @@
     writer_ = writer;
   }
 
+  int GetHTTPResponseCode() { return http_fetcher_->http_response_code(); }
+
   // Debugging/logging
   static std::string StaticType() { return "DownloadAction"; }
   std::string Type() const { return StaticType(); }
diff --git a/main.cc b/main.cc
index 64c6aba..f027033 100644
--- a/main.cc
+++ b/main.cc
@@ -6,20 +6,20 @@
 #include <tr1/memory>
 #include <vector>
 
+#include <base/at_exit.h>
+#include <base/command_line.h>
+#include <base/logging.h>
+#include <base/string_util.h>
 #include <gflags/gflags.h>
 #include <glib.h>
+#include <metrics/metrics_library.h>
 
-#include "base/at_exit.h"
-#include "base/command_line.h"
-#include "base/logging.h"
-#include "base/string_util.h"
-#include "metrics/metrics_library.h"
 #include "update_engine/dbus_constants.h"
 #include "update_engine/dbus_service.h"
 #include "update_engine/prefs.h"
 #include "update_engine/subprocess.h"
 #include "update_engine/update_attempter.h"
-#include "update_engine/utils.h"
+#include "update_engine/update_check_scheduler.h"
 
 extern "C" {
 #include "update_engine/update_engine.dbusserver.h"
@@ -128,7 +128,9 @@
   update_attempter.set_dbus_service(service);
   chromeos_update_engine::SetupDbusService(service);
 
-  update_attempter.InitiatePeriodicUpdateChecks();
+  // Schedule periodic update checks.
+  chromeos_update_engine::UpdateCheckScheduler scheduler(&update_attempter);
+  scheduler.Run();
 
   // Update boot flags after 45 seconds
   g_timeout_add_seconds(45, &chromeos_update_engine::UpdateBootFlags, NULL);
diff --git a/omaha_request_action.h b/omaha_request_action.h
index 6f30a32..7533d14 100644
--- a/omaha_request_action.h
+++ b/omaha_request_action.h
@@ -127,6 +127,8 @@
   void PerformAction();
   void TerminateProcessing();
 
+  int GetHTTPResponseCode() { return http_fetcher_->http_response_code(); }
+
   // Debugging/logging
   static std::string StaticType() { return "OmahaRequestAction"; }
   std::string Type() const { return StaticType(); }
diff --git a/update_attempter.cc b/update_attempter.cc
index b0c470b..3614cad 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -15,8 +15,8 @@
 #include <vector>
 
 #include <glib.h>
+#include <metrics/metrics_library.h>
 
-#include "metrics/metrics_library.h"
 #include "update_engine/dbus_service.h"
 #include "update_engine/download_action.h"
 #include "update_engine/filesystem_copier_action.h"
@@ -26,6 +26,7 @@
 #include "update_engine/omaha_response_handler_action.h"
 #include "update_engine/postinstall_runner_action.h"
 #include "update_engine/set_bootable_flag_action.h"
+#include "update_engine/update_check_scheduler.h"
 
 using base::TimeDelta;
 using base::TimeTicks;
@@ -35,21 +36,6 @@
 
 namespace chromeos_update_engine {
 
-namespace {
-
-const int kTimeoutOnce = 7 * 60;  // at 7 minutes
-const int kTimeoutPeriodic = 45 * 60;  // every 45 minutes
-const int kTimeoutFuzz = 10 * 60;  // +/- 5 minutes
-
-gboolean CheckForUpdatePeriodically(void* arg) {
-  UpdateAttempter* update_attempter = reinterpret_cast<UpdateAttempter*>(arg);
-  update_attempter->Update("", "");
-  update_attempter->SchedulePeriodicUpdateCheck(kTimeoutPeriodic);
-  return FALSE;  // Don't run again.
-}
-
-}  // namespace {}
-
 const char* kUpdateCompletedMarker = "/tmp/update_engine_autoupdate_completed";
 
 const char* UpdateStatusToString(UpdateStatus status) {
@@ -104,6 +90,8 @@
     : dbus_service_(NULL),
       prefs_(prefs),
       metrics_lib_(metrics_lib),
+      update_check_scheduler_(NULL),
+      http_response_code_(0),
       priority_(utils::kProcessPriorityNormal),
       manage_priority_source_(NULL),
       download_active_(false),
@@ -131,6 +119,7 @@
     // Update in progress. Do nothing
     return;
   }
+  http_response_code_ = 0;
   if (!omaha_request_params_.Init(app_version, omaha_url)) {
     LOG(ERROR) << "Unable to initialize Omaha request device params.";
     return;
@@ -271,9 +260,10 @@
     return;
   }
 
-  LOG(INFO) << "Update failed.";
-  if (ScheduleErrorEventAction())
+  if (ScheduleErrorEventAction()) {
     return;
+  }
+  LOG(INFO) << "No update.";
   SetStatusAndNotify(UPDATE_STATUS_IDLE);
 }
 
@@ -291,11 +281,23 @@
 void UpdateAttempter::ActionCompleted(ActionProcessor* processor,
                                       AbstractAction* action,
                                       ActionExitCode code) {
-  // Reset download progress regardless of whether or not the download action
-  // succeeded.
+  // Reset download progress regardless of whether or not the download
+  // action succeeded. Also, get the response code from HTTP request
+  // actions (update download as well as the initial update check
+  // actions).
   const string type = action->Type();
-  if (type == DownloadAction::StaticType())
+  if (type == DownloadAction::StaticType()) {
     download_progress_ = 0.0;
+    DownloadAction* download_action = dynamic_cast<DownloadAction*>(action);
+    http_response_code_ = download_action->GetHTTPResponseCode();
+  } else if (type == OmahaRequestAction::StaticType()) {
+    OmahaRequestAction* omaha_request_action =
+        dynamic_cast<OmahaRequestAction*>(action);
+    // If the request is not an event, then it's the update-check.
+    if (!omaha_request_action->IsEvent()) {
+      http_response_code_ = omaha_request_action->GetHTTPResponseCode();
+    }
+  }
   if (code != kActionCodeSuccess) {
     // On failure, schedule an error event to be sent to Omaha.
     CreatePendingErrorEvent(action, code);
@@ -374,6 +376,9 @@
 
 void UpdateAttempter::SetStatusAndNotify(UpdateStatus status) {
   status_ = status;
+  if (update_check_scheduler_) {
+    update_check_scheduler_->SetUpdateStatus(status_);
+  }
   if (!dbus_service_)
     return;
   last_notify_time_ = TimeTicks::Now();
@@ -414,6 +419,7 @@
   if (error_event_.get() == NULL)
     return false;
 
+  LOG(INFO) << "Update failed -- reporting the error event.";
   shared_ptr<OmahaRequestAction> error_event_action(
       new OmahaRequestAction(prefs_,
                              omaha_request_params_,
@@ -426,27 +432,6 @@
   return true;
 }
 
-void UpdateAttempter::InitiatePeriodicUpdateChecks() {
-  if (!utils::IsOfficialBuild()) {
-    LOG(WARNING) << "Non-official build: periodic update checks disabled.";
-    return;
-  }
-  if (utils::IsRemovableDevice(utils::RootDevice(utils::BootDevice()))) {
-    LOG(WARNING) << "Removable device boot: periodic update checks disabled.";
-    return;
-  }
-  // Kick off periodic update checks. The first check is scheduled
-  // |kTimeoutOnce| seconds from now. Subsequent checks are scheduled
-  // at |kTimeoutPeriodic|-second intervals.
-  SchedulePeriodicUpdateCheck(kTimeoutOnce);
-}
-
-void UpdateAttempter::SchedulePeriodicUpdateCheck(int seconds) {
-  seconds = utils::FuzzInt(seconds, kTimeoutFuzz);
-  g_timeout_add_seconds(seconds, CheckForUpdatePeriodically, this);
-  LOG(INFO) << "Next update check in " << seconds << " seconds.";
-}
-
 void UpdateAttempter::SetPriority(utils::ProcessPriority priority) {
   if (priority_ == priority) {
     return;
diff --git a/update_attempter.h b/update_attempter.h
index 1c9ad40..fd9bc2a 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -24,9 +24,7 @@
 
 namespace chromeos_update_engine {
 
-namespace utils {
-enum ProcessPriority;
-};
+class UpdateCheckScheduler;
 
 extern const char* kUpdateCompletedMarker;
 
@@ -47,12 +45,13 @@
                         public DownloadActionDelegate {
  public:
   UpdateAttempter(PrefsInterface* prefs, MetricsLibraryInterface* metrics_lib);
-  ~UpdateAttempter();
+  virtual ~UpdateAttempter();
 
   // Checks for update and, if a newer version is available, attempts
   // to update the system. Non-empty |in_app_version| or
   // |in_update_url| prevents automatic detection of the parameter.
-  void Update(const std::string& app_version, const std::string& omaha_url);
+  virtual void Update(const std::string& app_version,
+                      const std::string& omaha_url);
 
   // ActionProcessorDelegate methods:
   void ProcessingDone(const ActionProcessor* processor, ActionExitCode code);
@@ -75,10 +74,22 @@
                  std::string* new_version,
                  int64_t* new_size);
 
+  UpdateStatus status() const { return status_; }
+
+  int http_response_code() const { return http_response_code_; }
+  void set_http_response_code(int code) { http_response_code_ = code; }
+
   void set_dbus_service(struct UpdateEngineService* dbus_service) {
     dbus_service_ = dbus_service;
   }
 
+  UpdateCheckScheduler* update_check_scheduler() const {
+    return update_check_scheduler_;
+  }
+  void set_update_check_scheduler(UpdateCheckScheduler* scheduler) {
+    update_check_scheduler_ = scheduler;
+  }
+
   // This is the D-Bus service entry point for going through an
   // update. If the current status is idle invokes Update.
   void CheckForUpdate(const std::string& app_version,
@@ -88,13 +99,6 @@
   // UPDATED_NEED_REBOOT. Returns true on sucess, false otherwise.
   bool RebootIfNeeded();
 
-  // Kicks off the periodic update checks, if necessary.
-  void InitiatePeriodicUpdateChecks();
-
-  // Schedules the next periodic update check |seconds| from now. Note
-  // that the actual timeout will be fuzzed.
-  void SchedulePeriodicUpdateCheck(int seconds);
-
   // DownloadActionDelegate methods
   void SetDownloadStatus(bool active);
   void BytesReceived(uint64_t bytes_received, uint64_t total);
@@ -155,9 +159,14 @@
   // Pointer to the UMA metrics collection library.
   MetricsLibraryInterface* metrics_lib_;
 
+  // The current UpdateCheckScheduler to notify of state transitions.
+  UpdateCheckScheduler* update_check_scheduler_;
+
   // Pending error event, if any.
   scoped_ptr<OmahaEvent> error_event_;
 
+  int http_response_code_;
+
   // Current process priority.
   utils::ProcessPriority priority_;
 
diff --git a/update_attempter_mock.h b/update_attempter_mock.h
new file mode 100644
index 0000000..56a0b42
--- /dev/null
+++ b/update_attempter_mock.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_PLATFORM_UPDATE_ENGINE_UPDATE_ATTEMPTER_MOCK_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_UPDATE_ATTEMPTER_MOCK_H__
+
+#include <gmock/gmock.h>
+
+#include "update_engine/update_attempter.h"
+
+namespace chromeos_update_engine {
+
+class UpdateAttempterMock : public UpdateAttempter {
+ public:
+  UpdateAttempterMock() : UpdateAttempter(NULL, NULL) {}
+
+  MOCK_METHOD2(Update, void(const std::string& app_version,
+                            const std::string& omaha_url));
+};
+
+}  // namespace chromeos_update_engine
+
+#endif  // CHROMEOS_PLATFORM_UPDATE_ENGINE_UPDATE_ATTEMPTER_MOCK_H__
diff --git a/update_check_scheduler.cc b/update_check_scheduler.cc
new file mode 100644
index 0000000..c0a52ab
--- /dev/null
+++ b/update_check_scheduler.cc
@@ -0,0 +1,128 @@
+// Copyright (c) 2010 The Chromium OS 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 "update_engine/update_check_scheduler.h"
+
+#include "update_engine/utils.h"
+
+namespace chromeos_update_engine {
+
+const int UpdateCheckScheduler::kTimeoutOnce = 7 * 60;  // at 7 minutes
+const int UpdateCheckScheduler::kTimeoutPeriodic = 45 * 60;  // every 45 minutes
+const int UpdateCheckScheduler::kTimeoutRegularFuzz = 10 * 60;  // +/- 5 minutes
+const int UpdateCheckScheduler::kTimeoutMaxBackoff = 4 * 60 * 60;  // 4 hours
+
+UpdateCheckScheduler::UpdateCheckScheduler(UpdateAttempter* update_attempter)
+    : update_attempter_(update_attempter),
+      enabled_(false),
+      scheduled_(false),
+      last_interval_(0) {}
+
+UpdateCheckScheduler::~UpdateCheckScheduler() {}
+
+void UpdateCheckScheduler::Run() {
+  enabled_ = false;
+  update_attempter_->set_update_check_scheduler(NULL);
+
+  if (!IsOfficialBuild()) {
+    LOG(WARNING) << "Non-official build: periodic update checks disabled.";
+    return;
+  }
+  if (IsBootDeviceRemovable()) {
+    LOG(WARNING) << "Removable device boot: periodic update checks disabled.";
+    return;
+  }
+  enabled_ = true;
+
+  // Registers this scheduler with the update attempter so that scheduler can be
+  // notified of update status changes.
+  update_attempter_->set_update_check_scheduler(this);
+
+  // Kicks off periodic update checks. The first check is scheduled
+  // |kTimeoutOnce| seconds from now. Subsequent checks are scheduled by
+  // ScheduleNextCheck, normally at |kTimeoutPeriodic|-second intervals.
+  ScheduleCheck(kTimeoutOnce, kTimeoutRegularFuzz);
+}
+
+bool UpdateCheckScheduler::IsBootDeviceRemovable() {
+  return utils::IsRemovableDevice(utils::RootDevice(utils::BootDevice()));
+}
+
+bool UpdateCheckScheduler::IsOfficialBuild() {
+  return utils::IsOfficialBuild();
+}
+
+guint UpdateCheckScheduler::GTimeoutAddSeconds(guint interval,
+                                               GSourceFunc function) {
+  return g_timeout_add_seconds(interval, function, this);
+}
+
+void UpdateCheckScheduler::ScheduleCheck(int interval, int fuzz) {
+  if (!CanSchedule()) {
+    return;
+  }
+  last_interval_ = interval;
+  interval = utils::FuzzInt(interval, fuzz);
+  if (interval < 0) {
+    interval = 0;
+  }
+  GTimeoutAddSeconds(interval, StaticCheck);
+  scheduled_ = true;
+  LOG(INFO) << "Next update check in " << interval << " seconds.";
+}
+
+gboolean UpdateCheckScheduler::StaticCheck(void* scheduler) {
+  UpdateCheckScheduler* me = reinterpret_cast<UpdateCheckScheduler*>(scheduler);
+  CHECK(me->scheduled_);
+  me->scheduled_ = false;
+  me->update_attempter_->Update("", "");
+  // This check ensures that future update checks will be or are already
+  // scheduled. The check should never fail. A check failure means that there's
+  // a bug that will most likely prevent further automatic update checks. It
+  // seems better to crash in such cases and restart the update_engine daemon
+  // into, hopefully, a known good state.
+  CHECK(me->update_attempter_->status() != UPDATE_STATUS_IDLE ||
+        !me->CanSchedule());
+  return FALSE;  // Don't run again.
+}
+
+void UpdateCheckScheduler::ComputeNextIntervalAndFuzz(int* next_interval,
+                                                      int* next_fuzz) {
+  int interval = 0;
+  int fuzz = 0;
+  // Implements exponential back off on 500 (Internal Server Error) and 503
+  // (Service Unavailable) HTTP response codes.
+  if (update_attempter_->http_response_code() == 500 ||
+      update_attempter_->http_response_code() == 503) {
+    interval = 2 * last_interval_;
+    if (interval > kTimeoutMaxBackoff) {
+      interval = kTimeoutMaxBackoff;
+    }
+    // Exponential back off is fuzzed by +/- |interval|/2.
+    fuzz = interval;
+  }
+  // Ensures that under normal conditions the regular update check interval and
+  // fuzz are used. Also covers the case where back off is required based on the
+  // initial update check.
+  if (interval < kTimeoutPeriodic) {
+    interval = kTimeoutPeriodic;
+    fuzz = kTimeoutRegularFuzz;
+  }
+  *next_interval = interval;
+  *next_fuzz = fuzz;
+}
+
+void UpdateCheckScheduler::ScheduleNextCheck() {
+  int interval, fuzz;
+  ComputeNextIntervalAndFuzz(&interval, &fuzz);
+  ScheduleCheck(interval, fuzz);
+}
+
+void UpdateCheckScheduler::SetUpdateStatus(UpdateStatus status) {
+  if (status == UPDATE_STATUS_IDLE) {
+    ScheduleNextCheck();
+  }
+}
+
+}  // namespace chromeos_update_engine
diff --git a/update_check_scheduler.h b/update_check_scheduler.h
new file mode 100644
index 0000000..7253c5a
--- /dev/null
+++ b/update_check_scheduler.h
@@ -0,0 +1,118 @@
+// Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_PLATFORM_UPDATE_ENGINE_UPDATE_CHECK_SCHEDULER_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_UPDATE_CHECK_SCHEDULER_H__
+
+#include <base/basictypes.h>
+#include <glib.h>
+#include <gtest/gtest_prod.h>  // for FRIEND_TEST
+
+#include "update_engine/update_attempter.h"
+
+namespace chromeos_update_engine {
+
+// UpdateCheckScheduler manages the periodic background update checks. This is
+// the basic update check cycle:
+//
+//    Run
+//     |
+//     v
+// /->ScheduleCheck
+// |   |
+// |   v
+// |  StaticCheck (invoked through a GLib timeout source)
+// |   |
+// |   v
+// |  UpdateAttempter::Update
+// |   |
+// |   v
+// |  SetUpdateStatus (invoked by UpdateAttempter on state transitions)
+// |   |
+// |   v
+// |  ScheduleNextCheck (invoked when UpdateAttempter becomes idle)
+// \---/
+class UpdateCheckScheduler {
+ public:
+  static const int kTimeoutOnce;
+  static const int kTimeoutPeriodic;
+  static const int kTimeoutRegularFuzz;
+  static const int kTimeoutMaxBackoff;
+
+  UpdateCheckScheduler(UpdateAttempter* update_attempter);
+  virtual ~UpdateCheckScheduler();
+
+  // Initiates the periodic update checks, if necessary.
+  void Run();
+
+  // Sets the new update status. This is invoked by UpdateAttempter.
+  void SetUpdateStatus(UpdateStatus status);
+
+ private:
+  friend class UpdateCheckSchedulerTest;
+  FRIEND_TEST(UpdateCheckSchedulerTest, CanScheduleTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ComputeNextIntervalAndFuzzBackoffTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ComputeNextIntervalAndFuzzTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, GTimeoutAddSecondsTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, IsBootDeviceRemovableTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, IsOfficialBuildTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, RunBootDeviceRemovableTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, RunNonOfficialBuildTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, RunTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ScheduleCheckDisabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ScheduleCheckEnabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ScheduleCheckNegativeIntervalTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ScheduleNextCheckDisabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, ScheduleNextCheckEnabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, SetUpdateStatusIdleDisabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, SetUpdateStatusIdleEnabledTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, SetUpdateStatusNonIdleTest);
+  FRIEND_TEST(UpdateCheckSchedulerTest, StaticCheckTest);
+
+  // Wraps GLib's g_timeout_add_seconds so that it can be mocked in tests.
+  virtual guint GTimeoutAddSeconds(guint interval, GSourceFunc function);
+
+  // Wrappers for utils functions so that they can be mocked in tests.
+  virtual bool IsBootDeviceRemovable();
+  virtual bool IsOfficialBuild();
+
+  // Returns true if an update check can be scheduled. An update check should
+  // not be scheduled if periodic update checks are disabled or if one is
+  // already scheduled.
+  bool CanSchedule() { return enabled_ && !scheduled_; }
+
+  // Schedules the next periodic update check |interval| seconds from now
+  // randomized by +/- |fuzz|/2.
+  void ScheduleCheck(int interval, int fuzz);
+
+  // GLib timeout source callback. Initiates an update check through the update
+  // attempter.
+  static gboolean StaticCheck(void* scheduler);
+
+  // Schedules the next update check by setting up a timeout source.
+  void ScheduleNextCheck();
+
+  // Computes the timeout interval along with its random fuzz range for the next
+  // update check by taking into account the last timeout interval as well as
+  // the last update status.
+  void ComputeNextIntervalAndFuzz(int* next_interval, int* next_fuzz);
+
+  // The UpdateAttempter to use for update checks.
+  UpdateAttempter* update_attempter_;
+
+  // True if automatic update checks should be scheduled, false otherwise.
+  bool enabled_;
+
+  // True if there's an update check scheduled already, false otherwise.
+  bool scheduled_;
+
+  // The timeout interval (before fuzzing) for the last update check.
+  int last_interval_;
+
+  DISALLOW_COPY_AND_ASSIGN(UpdateCheckScheduler);
+};
+
+}  // namespace chromeos_update_engine
+
+#endif  // CHROMEOS_PLATFORM_UPDATE_ENGINE_CHECK_SCHEDULER_H__
diff --git a/update_check_scheduler_unittest.cc b/update_check_scheduler_unittest.cc
new file mode 100644
index 0000000..8a0ca19
--- /dev/null
+++ b/update_check_scheduler_unittest.cc
@@ -0,0 +1,238 @@
+// Copyright (c) 2010 The Chromium OS 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 <gtest/gtest.h>
+
+#include "update_engine/update_attempter_mock.h"
+#include "update_engine/update_check_scheduler.h"
+
+using std::string;
+using testing::_;
+using testing::AllOf;
+using testing::Ge;
+using testing::Le;
+using testing::MockFunction;
+using testing::Return;
+
+namespace chromeos_update_engine {
+
+namespace {
+void FuzzRange(int interval, int fuzz, int* interval_min, int* interval_max) {
+  *interval_min = interval - fuzz / 2;
+  *interval_max = interval + fuzz - fuzz / 2;
+}
+}  // namespace {}
+
+// Test a subclass rather than the main class directly so that we can mock out
+// GLib and utils in tests. There're explicit unit test for the wrapper methods.
+class UpdateCheckSchedulerUnderTest : public UpdateCheckScheduler {
+ public:
+  UpdateCheckSchedulerUnderTest(UpdateAttempter* update_attempter)
+      : UpdateCheckScheduler(update_attempter) {}
+
+  MOCK_METHOD2(GTimeoutAddSeconds, guint(guint seconds, GSourceFunc function));
+  MOCK_METHOD0(IsBootDeviceRemovable, bool());
+  MOCK_METHOD0(IsOfficialBuild, bool());
+};
+
+class UpdateCheckSchedulerTest : public ::testing::Test {
+ protected:
+  virtual void SetUp() {
+    test_ = this;
+    loop_ = NULL;
+    scheduler_.reset(new UpdateCheckSchedulerUnderTest(&attempter_));
+    EXPECT_EQ(&attempter_, scheduler_->update_attempter_);
+    EXPECT_FALSE(scheduler_->enabled_);
+    EXPECT_FALSE(scheduler_->scheduled_);
+    EXPECT_EQ(0, scheduler_->last_interval_);
+  }
+
+  virtual void TearDown() {
+    test_ = NULL;
+    loop_ = NULL;
+    scheduler_.reset(NULL);
+  }
+
+  static gboolean SourceCallback(gpointer data) {
+    g_main_loop_quit(test_->loop_);
+    // Forwards the call to the function mock so that expectations can be set.
+    return test_->source_callback_.Call(data);
+  }
+
+  scoped_ptr<UpdateCheckSchedulerUnderTest> scheduler_;
+  UpdateAttempterMock attempter_;
+  MockFunction<gboolean(gpointer data)> source_callback_;
+  GMainLoop* loop_;
+  static UpdateCheckSchedulerTest* test_;
+};
+
+UpdateCheckSchedulerTest* UpdateCheckSchedulerTest::test_ = NULL;
+
+TEST_F(UpdateCheckSchedulerTest, CanScheduleTest) {
+  EXPECT_FALSE(scheduler_->CanSchedule());
+  scheduler_->enabled_ = true;
+  EXPECT_TRUE(scheduler_->CanSchedule());
+  scheduler_->scheduled_ = true;
+  EXPECT_FALSE(scheduler_->CanSchedule());
+  scheduler_->enabled_ = false;
+  EXPECT_FALSE(scheduler_->CanSchedule());
+}
+
+TEST_F(UpdateCheckSchedulerTest, ComputeNextIntervalAndFuzzBackoffTest) {
+  int interval, fuzz;
+  attempter_.set_http_response_code(500);
+  int last_interval = UpdateCheckScheduler::kTimeoutPeriodic + 50;
+  scheduler_->last_interval_ = last_interval;
+  scheduler_->ComputeNextIntervalAndFuzz(&interval, &fuzz);
+  EXPECT_EQ(2 * last_interval, interval);
+  EXPECT_EQ(2 * last_interval, fuzz);
+
+  attempter_.set_http_response_code(503);
+  last_interval = UpdateCheckScheduler::kTimeoutMaxBackoff / 2 + 1;
+  scheduler_->last_interval_ = last_interval;
+  scheduler_->ComputeNextIntervalAndFuzz(&interval, &fuzz);
+  EXPECT_EQ(UpdateCheckScheduler::kTimeoutMaxBackoff, interval);
+  EXPECT_EQ(UpdateCheckScheduler::kTimeoutMaxBackoff, fuzz);
+}
+
+TEST_F(UpdateCheckSchedulerTest, ComputeNextIntervalAndFuzzTest) {
+  int interval, fuzz;
+  scheduler_->ComputeNextIntervalAndFuzz(&interval, &fuzz);
+  EXPECT_EQ(UpdateCheckScheduler::kTimeoutPeriodic, interval);
+  EXPECT_EQ(UpdateCheckScheduler::kTimeoutRegularFuzz, fuzz);
+}
+
+TEST_F(UpdateCheckSchedulerTest, GTimeoutAddSecondsTest) {
+  loop_ = g_main_loop_new(g_main_context_default(), FALSE);
+  // Invokes the actual GLib wrapper method rather than the subclass mock.
+  scheduler_->UpdateCheckScheduler::GTimeoutAddSeconds(0, SourceCallback);
+  EXPECT_CALL(source_callback_, Call(scheduler_.get())).Times(1);
+  g_main_loop_run(loop_);
+  g_main_loop_unref(loop_);
+}
+
+TEST_F(UpdateCheckSchedulerTest, IsBootDeviceRemovableTest) {
+  // Invokes the actual utils wrapper method rather than the subclass mock.
+  EXPECT_FALSE(scheduler_->UpdateCheckScheduler::IsBootDeviceRemovable());
+}
+
+TEST_F(UpdateCheckSchedulerTest, IsOfficialBuildTest) {
+  // Invokes the actual utils wrapper method rather than the subclass mock.
+  EXPECT_TRUE(scheduler_->UpdateCheckScheduler::IsOfficialBuild());
+}
+
+TEST_F(UpdateCheckSchedulerTest, RunBootDeviceRemovableTest) {
+  scheduler_->enabled_ = true;
+  EXPECT_CALL(*scheduler_, IsOfficialBuild()).Times(1).WillOnce(Return(true));
+  EXPECT_CALL(*scheduler_, IsBootDeviceRemovable())
+      .Times(1)
+      .WillOnce(Return(true));
+  scheduler_->Run();
+  EXPECT_FALSE(scheduler_->enabled_);
+  EXPECT_EQ(NULL, attempter_.update_check_scheduler());
+}
+
+TEST_F(UpdateCheckSchedulerTest, RunNonOfficialBuildTest) {
+  scheduler_->enabled_ = true;
+  EXPECT_CALL(*scheduler_, IsOfficialBuild()).Times(1).WillOnce(Return(false));
+  scheduler_->Run();
+  EXPECT_FALSE(scheduler_->enabled_);
+  EXPECT_EQ(NULL, attempter_.update_check_scheduler());
+}
+
+TEST_F(UpdateCheckSchedulerTest, RunTest) {
+  int interval_min, interval_max;
+  FuzzRange(UpdateCheckScheduler::kTimeoutOnce,
+            UpdateCheckScheduler::kTimeoutRegularFuzz,
+            &interval_min,
+            &interval_max);
+  EXPECT_CALL(*scheduler_, IsOfficialBuild()).Times(1).WillOnce(Return(true));
+  EXPECT_CALL(*scheduler_, IsBootDeviceRemovable())
+      .Times(1)
+      .WillOnce(Return(false));
+  EXPECT_CALL(*scheduler_,
+              GTimeoutAddSeconds(AllOf(Ge(interval_min), Le(interval_max)),
+                                 scheduler_->StaticCheck)).Times(1);
+  scheduler_->Run();
+  EXPECT_TRUE(scheduler_->enabled_);
+  EXPECT_EQ(scheduler_.get(), attempter_.update_check_scheduler());
+}
+
+TEST_F(UpdateCheckSchedulerTest, ScheduleCheckDisabledTest) {
+  EXPECT_CALL(*scheduler_, GTimeoutAddSeconds(_, _)).Times(0);
+  scheduler_->ScheduleCheck(250, 30);
+  EXPECT_EQ(0, scheduler_->last_interval_);
+  EXPECT_FALSE(scheduler_->scheduled_);
+}
+
+TEST_F(UpdateCheckSchedulerTest, ScheduleCheckEnabledTest) {
+  int interval_min, interval_max;
+  FuzzRange(100, 10, &interval_min,&interval_max);
+  EXPECT_CALL(*scheduler_,
+              GTimeoutAddSeconds(AllOf(Ge(interval_min), Le(interval_max)),
+                                 scheduler_->StaticCheck)).Times(1);
+  scheduler_->enabled_ = true;
+  scheduler_->ScheduleCheck(100, 10);
+  EXPECT_EQ(100, scheduler_->last_interval_);
+  EXPECT_TRUE(scheduler_->scheduled_);
+}
+
+TEST_F(UpdateCheckSchedulerTest, ScheduleCheckNegativeIntervalTest) {
+  EXPECT_CALL(*scheduler_, GTimeoutAddSeconds(0, scheduler_->StaticCheck))
+      .Times(1);
+  scheduler_->enabled_ = true;
+  scheduler_->ScheduleCheck(-50, 20);
+  EXPECT_TRUE(scheduler_->scheduled_);
+}
+
+TEST_F(UpdateCheckSchedulerTest, ScheduleNextCheckDisabledTest) {
+  EXPECT_CALL(*scheduler_, GTimeoutAddSeconds(_, _)).Times(0);
+  scheduler_->ScheduleNextCheck();
+}
+
+TEST_F(UpdateCheckSchedulerTest, ScheduleNextCheckEnabledTest) {
+  int interval_min, interval_max;
+  FuzzRange(UpdateCheckScheduler::kTimeoutPeriodic,
+            UpdateCheckScheduler::kTimeoutRegularFuzz,
+            &interval_min,
+            &interval_max);
+  EXPECT_CALL(*scheduler_,
+              GTimeoutAddSeconds(AllOf(Ge(interval_min), Le(interval_max)),
+                                 scheduler_->StaticCheck)).Times(1);
+  scheduler_->enabled_ = true;
+  scheduler_->ScheduleNextCheck();
+}
+
+TEST_F(UpdateCheckSchedulerTest, SetUpdateStatusIdleDisabledTest) {
+  EXPECT_CALL(*scheduler_, GTimeoutAddSeconds(_, _)).Times(0);
+  scheduler_->SetUpdateStatus(UPDATE_STATUS_IDLE);
+}
+
+TEST_F(UpdateCheckSchedulerTest, SetUpdateStatusIdleEnabledTest) {
+  int interval_min, interval_max;
+  FuzzRange(UpdateCheckScheduler::kTimeoutPeriodic,
+            UpdateCheckScheduler::kTimeoutRegularFuzz,
+            &interval_min,
+            &interval_max);
+  EXPECT_CALL(*scheduler_,
+              GTimeoutAddSeconds(AllOf(Ge(interval_min), Le(interval_max)),
+                                 scheduler_->StaticCheck)).Times(1);
+  scheduler_->enabled_ = true;
+  scheduler_->SetUpdateStatus(UPDATE_STATUS_IDLE);
+}
+
+TEST_F(UpdateCheckSchedulerTest, SetUpdateStatusNonIdleTest) {
+  EXPECT_CALL(*scheduler_, GTimeoutAddSeconds(_, _)).Times(0);
+  scheduler_->SetUpdateStatus(UPDATE_STATUS_DOWNLOADING);
+  scheduler_->enabled_ = true;
+  scheduler_->SetUpdateStatus(UPDATE_STATUS_DOWNLOADING);
+}
+
+TEST_F(UpdateCheckSchedulerTest, StaticCheckTest) {
+  scheduler_->scheduled_ = true;
+  EXPECT_CALL(attempter_, Update("", "")).Times(1);
+  UpdateCheckSchedulerUnderTest::StaticCheck(scheduler_.get());
+}
+
+}  // namespace chromeos_update_engine