shill: Create ConnectivityTrial class to hold logic for connection tests

Create a new class, ConnectivityTrial, that contains the logic and handling
for each attempt in portal detection.  This separates the higher level logic
of retries and maximum number of attempts from the individual connection tests.

This architectural change enables code-reuse for an upcoming feature for on
demand connection testing.

BUG=chromium:402888
TEST=ran shill unit tests and tested on peppy

Change-Id: I90d2bca4cd0778c3aeac82677b675247769a5597
Reviewed-on: https://chromium-review.googlesource.com/216505
Commit-Queue: Rebecca Silberstein <silberst@chromium.org>
Tested-by: Rebecca Silberstein <silberst@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
diff --git a/connectivity_trial_unittest.cc b/connectivity_trial_unittest.cc
new file mode 100644
index 0000000..39c125c
--- /dev/null
+++ b/connectivity_trial_unittest.cc
@@ -0,0 +1,511 @@
+// Copyright (c) 2012 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 "shill/connectivity_trial.h"
+
+#include <string>
+
+#include <base/bind.h>
+#include <base/memory/scoped_ptr.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "shill/mock_connection.h"
+#include "shill/mock_control.h"
+#include "shill/mock_device_info.h"
+#include "shill/mock_event_dispatcher.h"
+#include "shill/mock_http_request.h"
+#include "shill/mock_time.h"
+
+using base::Bind;
+using base::Callback;
+using base::Unretained;
+using std::string;
+using std::vector;
+using testing::_;
+using testing::AtLeast;
+using testing::DoAll;
+using testing::InSequence;
+using testing::Mock;
+using testing::NiceMock;
+using testing::Return;
+using testing::ReturnRef;
+using testing::SetArgumentPointee;
+using testing::StrictMock;
+using testing::Test;
+
+namespace shill {
+
+namespace {
+const char kBadURL[] = "badurl";
+const char kInterfaceName[] = "int0";
+const char kURL[] = "http://www.chromium.org";
+const char kDNSServer0[] = "8.8.8.8";
+const char kDNSServer1[] = "8.8.4.4";
+const char *kDNSServers[] = { kDNSServer0, kDNSServer1 };
+}  // namespace
+
+MATCHER_P(IsResult, result, "") {
+  return (result.phase == arg.phase &&
+          result.status == arg.status);
+}
+
+class ConnectivityTrialTest : public Test {
+ public:
+  ConnectivityTrialTest()
+      : device_info_(new NiceMock<MockDeviceInfo>(
+          &control_,
+          reinterpret_cast<EventDispatcher *>(NULL),
+          reinterpret_cast<Metrics *>(NULL),
+          reinterpret_cast<Manager *>(NULL))),
+        connection_(new StrictMock<MockConnection>(device_info_.get())),
+        connectivity_trial_(new ConnectivityTrial(
+            connection_.get(),
+            &dispatcher_,
+            kTrialTimeout,
+            callback_target_.result_callback())),
+        interface_name_(kInterfaceName),
+        dns_servers_(kDNSServers, kDNSServers + 2),
+        http_request_(NULL) {
+    current_time_.tv_sec = current_time_.tv_usec = 0;
+  }
+
+  virtual void SetUp() {
+    EXPECT_CALL(*connection_.get(), IsIPv6())
+                .WillRepeatedly(Return(false));
+    EXPECT_CALL(*connection_.get(), interface_name())
+        .WillRepeatedly(ReturnRef(interface_name_));
+    EXPECT_CALL(time_, GetTimeMonotonic(_))
+        .WillRepeatedly(Invoke(this, &ConnectivityTrialTest::GetTimeMonotonic));
+    EXPECT_CALL(*connection_.get(), dns_servers())
+        .WillRepeatedly(ReturnRef(dns_servers_));
+    EXPECT_FALSE(connectivity_trial_->request_.get());
+  }
+
+  virtual void TearDown() {
+    Mock::VerifyAndClearExpectations(&http_request_);
+    if (connectivity_trial_->request_.get()) {
+      EXPECT_CALL(*http_request(), Stop());
+
+      // Delete the ConnectivityTrial while expectations still exist.
+      connectivity_trial_.reset();
+    }
+  }
+
+ protected:
+  static const int kNumAttempts;
+  static const int kTrialTimeout;
+
+  class CallbackTarget {
+   public:
+    CallbackTarget()
+        : result_callback_(Bind(&CallbackTarget::ResultCallback,
+                                Unretained(this))) {
+    }
+
+    MOCK_METHOD1(ResultCallback, void(ConnectivityTrial::Result result));
+    Callback<void(ConnectivityTrial::Result)> &result_callback() {
+      return result_callback_;
+    }
+
+   private:
+    Callback<void(ConnectivityTrial::Result)> result_callback_;
+  };
+
+  void AssignHTTPRequest() {
+    http_request_ = new StrictMock<MockHTTPRequest>(connection_);
+    connectivity_trial_->request_.reset(http_request_);  // Passes ownership.
+  }
+
+  bool StartTrialWithDelay(const string &url_string, int delay) {
+    bool ret = connectivity_trial_->Start(url_string, delay);
+    if (ret) {
+      AssignHTTPRequest();
+    }
+    return ret;
+  }
+
+  bool StartTrial(const string &url_string) {
+    return StartTrialWithDelay(url_string, 0);
+  }
+
+  void StartTrialTask() {
+    AssignHTTPRequest();
+    EXPECT_CALL(*http_request(), Start(_, _, _))
+        .WillOnce(Return(HTTPRequest::kResultInProgress));
+    EXPECT_CALL(dispatcher(), PostDelayedTask(_, kTrialTimeout * 1000));
+    connectivity_trial()->StartTrialTask();
+  }
+
+  void ExpectTrialReturn(const ConnectivityTrial::Result &result) {
+    EXPECT_CALL(callback_target(), ResultCallback(IsResult(result)));
+
+    // Expect the PortalDetector to stop the current request.
+    EXPECT_CALL(*http_request(), Stop());
+  }
+
+  void TimeoutTrial() {
+    connectivity_trial_->TimeoutTrialTask();
+  }
+
+  MockHTTPRequest *http_request() { return http_request_; }
+  ConnectivityTrial *connectivity_trial() { return connectivity_trial_.get(); }
+  MockEventDispatcher &dispatcher() { return dispatcher_; }
+  CallbackTarget &callback_target() { return callback_target_; }
+  ByteString &response_data() { return response_data_; }
+
+  void ExpectReset() {
+    EXPECT_TRUE(callback_target_.result_callback().
+                Equals(connectivity_trial_->trial_callback_));
+    EXPECT_FALSE(connectivity_trial_->request_.get());
+  }
+
+  void ExpectTrialRetry(const ConnectivityTrial::Result &result, int delay) {
+    EXPECT_CALL(callback_target(), ResultCallback(IsResult(result)));
+
+    // Expect the ConnectivityTrial to stop the current request.
+    EXPECT_CALL(*http_request(), Stop());
+
+    // Expect the ConnectivityTrial to schedule the next attempt.
+    EXPECT_CALL(dispatcher(), PostDelayedTask(_, delay));
+  }
+
+  void AdvanceTime(int milliseconds) {
+    struct timeval tv = { milliseconds / 1000, (milliseconds % 1000) * 1000 };
+    timeradd(&current_time_, &tv, &current_time_);
+  }
+
+  void StartTrial() {
+    EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+    EXPECT_TRUE(StartTrial(kURL));
+
+    // Expect that the request will be started -- return failure.
+    EXPECT_CALL(*http_request(), Start(_, _, _))
+        .WillOnce(Return(HTTPRequest::kResultInProgress));
+    EXPECT_CALL(dispatcher(), PostDelayedTask(
+        _, kTrialTimeout * 1000));
+
+    connectivity_trial()->StartTrialTask();
+  }
+
+  void AppendReadData(const string &read_data) {
+    response_data_.Append(ByteString(read_data, false));
+    connectivity_trial_->RequestReadCallback(response_data_);
+  }
+
+ private:
+  int GetTimeMonotonic(struct timeval *tv) {
+    *tv = current_time_;
+    return 0;
+  }
+
+  StrictMock<MockEventDispatcher> dispatcher_;
+  MockControl control_;
+  scoped_ptr<MockDeviceInfo> device_info_;
+  scoped_refptr<MockConnection> connection_;
+  CallbackTarget callback_target_;
+  scoped_ptr<ConnectivityTrial> connectivity_trial_;
+  StrictMock<MockTime> time_;
+  struct timeval current_time_;
+  const string interface_name_;
+  vector<string> dns_servers_;
+  ByteString response_data_;
+  MockHTTPRequest *http_request_;
+};
+
+// static
+const int ConnectivityTrialTest::kNumAttempts = 0;
+const int ConnectivityTrialTest::kTrialTimeout = 4;
+
+TEST_F(ConnectivityTrialTest, Constructor) {
+  ExpectReset();
+}
+
+TEST_F(ConnectivityTrialTest, InvalidURL) {
+  EXPECT_FALSE(connectivity_trial()->IsActive());
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0)).Times(0);
+  EXPECT_FALSE(StartTrial(kBadURL));
+  ExpectReset();
+
+  EXPECT_FALSE(connectivity_trial()->Retry(0));
+  EXPECT_FALSE(connectivity_trial()->IsActive());
+}
+
+TEST_F(ConnectivityTrialTest, IsActive) {
+  // Before the trial is started, should not be active.
+  EXPECT_FALSE(connectivity_trial()->IsActive());
+
+  // Once the trial is started, IsActive should return true.
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+  StartTrialTask();
+  EXPECT_TRUE(connectivity_trial()->IsActive());
+
+  // Finish the trial, IsActive should return false.
+  EXPECT_CALL(*http_request(), Stop());
+  connectivity_trial()->CompleteTrial(
+      ConnectivityTrial::Result(ConnectivityTrial::kPhaseContent,
+                                ConnectivityTrial::kStatusFailure));
+  EXPECT_FALSE(connectivity_trial()->IsActive());
+}
+
+TEST_F(ConnectivityTrialTest, StartAttemptFailed) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  // Expect that the request will be started -- return failure.
+  EXPECT_CALL(*http_request(), Start(_, _, _))
+      .WillOnce(Return(HTTPRequest::kResultConnectionFailure));
+  // Expect a failure to be relayed to the caller.
+  EXPECT_CALL(callback_target(),
+              ResultCallback(IsResult(
+                  ConnectivityTrial::Result(
+                      ConnectivityTrial::kPhaseConnection,
+                      ConnectivityTrial::kStatusFailure))))
+      .Times(1);
+
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0)).Times(0);
+  EXPECT_CALL(*http_request(), Stop());
+
+  connectivity_trial()->StartTrialTask();
+}
+
+TEST_F(ConnectivityTrialTest, StartRepeated) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0)).Times(1);
+  EXPECT_TRUE(StartTrial(kURL));
+
+  // A second call should cancel the existing trial and set up the new one.
+  EXPECT_CALL(*http_request(), Stop());
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 10)).Times(1);
+  EXPECT_TRUE(StartTrialWithDelay(kURL, 10));
+}
+
+TEST_F(ConnectivityTrialTest, StartTrialAfterDelay) {
+  const int kDelaySeconds = 123;
+  // The trial should be delayed by kDelaySeconds.
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, kDelaySeconds));
+  EXPECT_TRUE(StartTrialWithDelay(kURL, kDelaySeconds));
+}
+
+TEST_F(ConnectivityTrialTest, TrialRetry) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  // Expect that the request will be started -- return failure.
+  EXPECT_CALL(*http_request(), Start(_, _, _))
+      .WillOnce(Return(HTTPRequest::kResultConnectionFailure));
+  EXPECT_CALL(*http_request(), Stop());
+  connectivity_trial()->StartTrialTask();
+
+  const int kRetryDelay = 7;
+  EXPECT_CALL(*http_request(), Stop());
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, kRetryDelay)).Times(1);
+  EXPECT_TRUE(connectivity_trial()->Retry(kRetryDelay));
+}
+
+TEST_F(ConnectivityTrialTest, TrialRetryFail) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  EXPECT_CALL(*http_request(), Stop());
+  connectivity_trial()->Stop();
+
+  EXPECT_FALSE(connectivity_trial()->Retry(0));
+}
+
+// Exactly like AttemptCount, except that the termination conditions are
+// different because we're triggering a different sort of error.
+TEST_F(ConnectivityTrialTest, ReadBadHeadersRetry) {
+  int num_failures = 3;
+  int sec_between_attempts = 3;
+
+  // Expect ConnectivityTrial to immediately post a task for the each attempt.
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  // Expect that the request will be started and return the in progress status.
+  EXPECT_CALL(*http_request(), Start(_, _, _))
+      .Times(num_failures).WillRepeatedly(
+          Return(HTTPRequest::kResultInProgress));
+
+  // Each HTTP request that gets started will have a request timeout.
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, kTrialTimeout * 1000))
+      .Times(num_failures);
+
+  // Expect failures for all attempts but the last.
+  EXPECT_CALL(callback_target(),
+              ResultCallback(IsResult(
+                  ConnectivityTrial::Result(
+                      ConnectivityTrial::kPhaseContent,
+                      ConnectivityTrial::kStatusFailure))))
+      .Times(num_failures);
+
+  // Expect the ConnectivityTrial to stop the current request each time, plus
+  // an extra time in ConnectivityTrial::Stop().
+  ByteString response_data("X", 1);
+
+  for (int i = 0; i < num_failures; ++i) {
+    connectivity_trial()->StartTrialTask();
+    AdvanceTime(sec_between_attempts * 1000);
+    EXPECT_CALL(*http_request(), Stop()).Times(2);
+    EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0)).Times(1);
+    connectivity_trial()->RequestReadCallback(response_data);
+    EXPECT_TRUE(connectivity_trial()->Retry(0));
+  }
+}
+
+
+TEST_F(ConnectivityTrialTest, ReadBadHeader) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  StartTrialTask();
+
+  ExpectTrialReturn(ConnectivityTrial::Result(
+      ConnectivityTrial::kPhaseContent,
+      ConnectivityTrial::kStatusFailure));
+  AppendReadData("X");
+}
+
+TEST_F(ConnectivityTrialTest, RequestTimeout) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  StartTrialTask();
+
+  ExpectTrialReturn(ConnectivityTrial::Result(
+      ConnectivityTrial::kPhaseUnknown,
+      ConnectivityTrial::kStatusTimeout));
+
+  EXPECT_CALL(*http_request(), response_data())
+      .WillOnce(ReturnRef(response_data()));
+
+  TimeoutTrial();
+}
+
+TEST_F(ConnectivityTrialTest, ReadPartialHeaderTimeout) {
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  StartTrialTask();
+
+
+  const string response_expected(ConnectivityTrial::kResponseExpected);
+  const size_t partial_size = response_expected.length() / 2;
+  AppendReadData(response_expected.substr(0, partial_size));
+
+  ExpectTrialReturn(ConnectivityTrial::Result(
+      ConnectivityTrial::kPhaseContent,
+      ConnectivityTrial::kStatusTimeout));
+
+  EXPECT_CALL(*http_request(), response_data())
+      .WillOnce(ReturnRef(response_data()));
+
+  TimeoutTrial();
+}
+
+TEST_F(ConnectivityTrialTest, ReadCompleteHeader) {
+  const string response_expected(ConnectivityTrial::kResponseExpected);
+  const size_t partial_size = response_expected.length() / 2;
+
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  StartTrialTask();
+
+  AppendReadData(response_expected.substr(0, partial_size));
+
+  ExpectTrialReturn(ConnectivityTrial::Result(
+      ConnectivityTrial::kPhaseContent,
+      ConnectivityTrial::kStatusSuccess));
+
+  AppendReadData(response_expected.substr(partial_size));
+}
+
+TEST_F(ConnectivityTrialTest, ReadMatchingHeader) {
+  const string kResponse("HTTP/9.8 204");
+
+  EXPECT_CALL(dispatcher(), PostDelayedTask(_, 0));
+  EXPECT_TRUE(StartTrial(kURL));
+
+  StartTrialTask();
+
+  ExpectTrialReturn(ConnectivityTrial::Result(
+      ConnectivityTrial::kPhaseContent,
+      ConnectivityTrial::kStatusSuccess));
+
+  AppendReadData(kResponse);
+}
+
+struct ResultMapping {
+  ResultMapping() : http_result(HTTPRequest::kResultUnknown), trial_result() {}
+  ResultMapping(HTTPRequest::Result in_http_result,
+                const ConnectivityTrial::Result &in_trial_result)
+      : http_result(in_http_result),
+        trial_result(in_trial_result) {}
+  HTTPRequest::Result http_result;
+  ConnectivityTrial::Result trial_result;
+};
+
+class ConnectivityTrialResultMappingTest
+    : public testing::TestWithParam<ResultMapping> {};
+
+TEST_P(ConnectivityTrialResultMappingTest, MapResult) {
+  ConnectivityTrial::Result trial_result =
+      ConnectivityTrial::GetPortalResultForRequestResult(
+          GetParam().http_result);
+  EXPECT_EQ(trial_result.phase, GetParam().trial_result.phase);
+  EXPECT_EQ(trial_result.status, GetParam().trial_result.status);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    TrialResultMappingTest,
+    ConnectivityTrialResultMappingTest,
+    ::testing::Values(
+        ResultMapping(
+            HTTPRequest::kResultUnknown,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseUnknown,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultInProgress,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseUnknown,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultDNSFailure,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseDNS,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultDNSTimeout,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseDNS,
+                                      ConnectivityTrial::kStatusTimeout)),
+        ResultMapping(
+            HTTPRequest::kResultConnectionFailure,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseConnection,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultConnectionTimeout,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseConnection,
+                                      ConnectivityTrial::kStatusTimeout)),
+        ResultMapping(
+            HTTPRequest::kResultRequestFailure,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseHTTP,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultRequestTimeout,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseHTTP,
+                                      ConnectivityTrial::kStatusTimeout)),
+        ResultMapping(
+            HTTPRequest::kResultResponseFailure,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseHTTP,
+                                      ConnectivityTrial::kStatusFailure)),
+        ResultMapping(
+            HTTPRequest::kResultResponseTimeout,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseHTTP,
+                                      ConnectivityTrial::kStatusTimeout)),
+        ResultMapping(
+            HTTPRequest::kResultSuccess,
+            ConnectivityTrial::Result(ConnectivityTrial::kPhaseContent,
+                                      ConnectivityTrial::kStatusFailure))));
+
+}  // namespace shill