shill: accelerate link monitoring after a suspend/resume event

When going into suspend, devices using the ath9k chipset do not disassociate
from the AP. Consequently, on networks like GoogleGuest, we are often able
to re-use the prior-to-suspend association following resume.

For some APs, however, this leads to the AP ending up in a confused state.
In particular, the Airport Extreme 802.11ac (A1521) sends DEAUTH frames
while we're sleeping, but it acknowledges our Nullfunc frames when we resume.
In fact, the AP even answers our ADDBA request with a Successful ADDBA respone.
When in this state, however, the AP does not deliver our frames to the IP
stack. E.g. ARPs fail.

Improve our post-suspend-resume behavior on these APs by running LinkMonitor
immediately on resume, and with a lower timeout. We use a per-probe timeout
of 200ms, which is at the 98.48%-ile of ARP response time observed in the
field last week. The total timeout is 1 second, and I believe we accept
late replies, so if the first ARP probe is replied too slowly, we'll still
accept it. A one-second ARP reply is at the 99.24%-ile.

Note that after LinkMonitor fails, it still takes ~10 seconds to establish
connectivity to this AP. That's because the Reassociate attempt times out.

While there:
- clarify a log message in Device::StopPortalDetection
- fix bad indent in LinkMonitor::StartInternal
- add some diagnostic infomation in cases where the IsArpRequest
  matcher fails to match expected values

BUG=chromium:244920
TEST=unit tests, manual (see below)

Manual testing
--------------
 1. grab a device which has an atheros wifi chip (e.g. lumpy, link)
 2. connect to "cros airport extreme wpa2", with password "chromeos"
 3. suspend device (e.g. close lid)
 4. wait >5 minutes.
 5. resume device (e.g. open lid)
 6. verify that the device detects a link failure in < 5 seconds
    "Link monitor has reached the failure threshold" should occur
    less than 5 seconds after "OnAfterResume" in /var/log/net.log
 7. wait 15-20 seconds
 8. verify that the device reconnects to the AP
 9. wait 45 seconds
10. verify that the device does not detect another link failure
11. switch to GoogleGuest.
12. suspend device
13. wait >5 minutes
14. resume device
15. check that device does not detect a link failure

Change-Id: I900ba45714875f5785bba3d47c73f37b863553fb
Reviewed-on: https://gerrit.chromium.org/gerrit/63126
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: mukesh agrawal <quiche@chromium.org>
Tested-by: mukesh agrawal <quiche@chromium.org>
diff --git a/device.cc b/device.cc
index d035735..6640c06 100644
--- a/device.cc
+++ b/device.cc
@@ -356,6 +356,10 @@
     SLOG(Device, 3) << "Renewing IP address on resume.";
     ipconfig_->RenewIP();
   }
+  if (link_monitor_) {
+    SLOG(Device, 3) << "Informing Link Monitor of resume.";
+    link_monitor_->OnAfterResume();
+  }
 }
 
 void Device::DropConnection() {
@@ -762,7 +766,7 @@
 
 void Device::StopPortalDetection() {
   SLOG(Device, 2) << "Device " << FriendlyName()
-                  << ": Portal detection has stopped.";
+                  << ": Portal detection stopping.";
   portal_detector_.reset();
 }
 
diff --git a/device_unittest.cc b/device_unittest.cc
index c053f4e..0ce8059 100644
--- a/device_unittest.cc
+++ b/device_unittest.cc
@@ -461,6 +461,19 @@
   device_->OnAfterResume();
 }
 
+TEST_F(DeviceTest, ResumeWithLinkMonitor) {
+  MockLinkMonitor *link_monitor = new StrictMock<MockLinkMonitor>();
+  SetLinkMonitor(link_monitor);  // Passes ownership.
+  EXPECT_CALL(*link_monitor, OnAfterResume());
+  device_->OnAfterResume();
+}
+
+TEST_F(DeviceTest, ResumeWithoutLinkMonitor) {
+  // Just test that we don't crash in this case.
+  EXPECT_FALSE(HasLinkMonitor());
+  device_->OnAfterResume();
+}
+
 TEST_F(DeviceTest, LinkMonitor) {
   scoped_refptr<MockConnection> connection(
       new StrictMock<MockConnection>(&device_info_));
diff --git a/link_monitor.cc b/link_monitor.cc
index 8bc7912..4fa7a15 100644
--- a/link_monitor.cc
+++ b/link_monitor.cc
@@ -28,9 +28,10 @@
 
 namespace shill {
 
-const int LinkMonitor::kTestPeriodMilliseconds = 5000;
+const int LinkMonitor::kDefaultTestPeriodMilliseconds = 5000;
 const char LinkMonitor::kDefaultLinkMonitorTechnologies[] = "wifi";
 const int LinkMonitor::kFailureThreshold = 5;
+const int LinkMonitor::kFastTestPeriodMilliseconds = 200;
 const int LinkMonitor::kMaxResponseSampleFilterDepth = 5;
 
 LinkMonitor::LinkMonitor(const ConnectionRefPtr &connection,
@@ -43,8 +44,11 @@
       metrics_(metrics),
       device_info_(device_info),
       failure_callback_(failure_callback),
+      test_period_milliseconds_(kDefaultTestPeriodMilliseconds),
       broadcast_failure_count_(0),
       unicast_failure_count_(0),
+      broadcast_success_count_(0),
+      unicast_success_count_(0),
       is_unicast_(false),
       response_sample_count_(0),
       response_sample_bucket_(0),
@@ -57,9 +61,17 @@
 
 bool LinkMonitor::Start() {
   Stop();
+  return StartInternal(kDefaultTestPeriodMilliseconds);
+}
+
+bool LinkMonitor::StartInternal(int probe_period_milliseconds) {
+  test_period_milliseconds_ = probe_period_milliseconds;
+  if (test_period_milliseconds_ > kDefaultTestPeriodMilliseconds) {
+    LOG(WARNING) << "Long test period; UMA stats will be truncated.";
+  }
 
   if (!device_info_->GetMACAddress(
-           connection_->interface_index(), &local_mac_address_)) {
+          connection_->interface_index(), &local_mac_address_)) {
     LOG(ERROR) << "Could not get local MAC address.";
     metrics_->NotifyLinkMonitorFailure(
         connection_->technology(),
@@ -68,7 +80,9 @@
     Stop();
     return false;
   }
-  gateway_mac_address_ = ByteString(local_mac_address_.GetLength());
+  if (gateway_mac_address_.IsEmpty()) {
+    gateway_mac_address_ = ByteString(local_mac_address_.GetLength());
+  }
   send_request_callback_.Reset(
       Bind(base::IgnoreResult(&LinkMonitor::SendRequest), Unretained(this)));
   time_->GetTimeMonotonic(&started_monitoring_at_);
@@ -82,6 +96,8 @@
   arp_client_.reset();
   broadcast_failure_count_ = 0;
   unicast_failure_count_ = 0;
+  broadcast_success_count_ = 0;
+  unicast_success_count_ = 0;
   is_unicast_ = false;
   response_sample_bucket_ = 0;
   response_sample_count_ = 0;
@@ -91,6 +107,13 @@
   timerclear(&sent_request_at_);
 }
 
+void LinkMonitor::OnAfterResume() {
+  ByteString prior_gateway_mac_address(gateway_mac_address_);
+  Stop();
+  gateway_mac_address_ = prior_gateway_mac_address;
+  StartInternal(kFastTestPeriodMilliseconds);
+}
+
 int LinkMonitor::GetResponseTimeMilliseconds() const {
   return response_sample_count_ ?
       response_sample_bucket_ / response_sample_count_ : 0;
@@ -139,12 +162,14 @@
 
 bool LinkMonitor::AddMissedResponse() {
   SLOG(Link, 2) << "In " << __func__ << ".";
-  AddResponseTimeSample(kTestPeriodMilliseconds);
+  AddResponseTimeSample(test_period_milliseconds_);
 
   if (is_unicast_) {
     ++unicast_failure_count_;
+    unicast_success_count_ = 0;
   } else {
     ++broadcast_failure_count_;
+    broadcast_success_count_ = 0;
   }
 
   if (unicast_failure_count_ + broadcast_failure_count_ >= kFailureThreshold) {
@@ -213,8 +238,10 @@
   arp_client_.reset();
 
   if (is_unicast_) {
+    ++unicast_success_count_;
     unicast_failure_count_ = 0;
   } else {
+    ++broadcast_success_count_;
     broadcast_failure_count_ = 0;
   }
 
@@ -230,6 +257,9 @@
   }
 
   is_unicast_ = !is_unicast_;
+  if (unicast_success_count_ && broadcast_success_count_) {
+    test_period_milliseconds_ = kDefaultTestPeriodMilliseconds;
+  }
 }
 
 bool LinkMonitor::SendRequest() {
@@ -284,7 +314,7 @@
   time_->GetTimeMonotonic(&sent_request_at_);
 
   dispatcher_->PostDelayedTask(send_request_callback_.callback(),
-                               kTestPeriodMilliseconds);
+                               test_period_milliseconds_);
   return true;
 }
 
diff --git a/link_monitor.h b/link_monitor.h
index 074e5a3..ff6785b 100644
--- a/link_monitor.h
+++ b/link_monitor.h
@@ -37,10 +37,11 @@
   // are reset, and the link monitoring quiesces.  Needed by Metrics.
   static const int kFailureThreshold;
 
-  // The number of milliseconds between ARP requests.  Needed by Metrics.
-  static const int kTestPeriodMilliseconds;
+  // The default number of milliseconds between ARP requests. Needed by Metrics.
+  static const int kDefaultTestPeriodMilliseconds;
 
   // The default list of technologies for which link monitoring is enabled.
+  // Needed by DefaultProfile.
   static const char kDefaultLinkMonitorTechnologies[];
 
   LinkMonitor(const ConnectionRefPtr &connection,
@@ -53,8 +54,15 @@
   // Starts link-monitoring on the selected connection.  Returns
   // true if successful, false otherwise.
   virtual bool Start();
+  // Stop link-monitoring on the selected connection. Clears any
+  // accumulated statistics.
   virtual void Stop();
 
+  // Inform LinkMonitor that the system is resuming from sleep.
+  // LinkMonitor will immediately probe the gateway, using a lower
+  // timeout than normal.
+  virtual void OnAfterResume();
+
   // Return modified cumulative average of the gateway ARP response
   // time.  Returns zero if no samples are available.  For each
   // missed ARP response, the sample is assumed to be the full
@@ -69,11 +77,20 @@
   friend class LinkMonitorForTest;
   friend class LinkMonitorTest;
 
+  // The number of milliseconds between ARP requests when running a quick test.
+  // Needed by unit tests.
+  static const int kFastTestPeriodMilliseconds;
+
   // The number of samples to compute a "strict" average over.  When
   // more samples than this number arrive, this determines how "slow"
   // our simple low-pass filter works.
   static const int kMaxResponseSampleFilterDepth;
 
+  // Similar to Start, except that the initial probes use
+  // |probe_period_milliseconds|. After successfully probing with both
+  // broadcast and unicast ARPs (at least one of each), LinkMonitor
+  // switches itself to kDefaultTestPeriodMilliseconds.
+  virtual bool StartInternal(int probe_period_milliseconds);
   // Add a response time sample to the buffer.
   void AddResponseTimeSample(int response_time_milliseconds);
   // Create an ArpClient instance so we can receive and transmit ARP
@@ -109,12 +126,21 @@
   // ArpClient instance used for performing link tests.
   scoped_ptr<ArpClient> arp_client_;
 
+  // How frequently we send an ARP request. This is also the timeout
+  // for a pending request.
+  int test_period_milliseconds_;
   // The number of consecutive times we have failed in receiving
   // responses to broadcast ARP requests.
   int broadcast_failure_count_;
   // The number of consecutive times we have failed in receiving
   // responses to unicast ARP requests.
   int unicast_failure_count_;
+  // The number of consecutive times we have succeeded in receiving
+  // responses to broadcast ARP requests.
+  int broadcast_success_count_;
+  // The number of consecutive times we have succeeded in receiving
+  // responses to unicast ARP requests.
+  int unicast_success_count_;
 
   // Whether this iteration of the test was a unicast request
   // to the gateway instead of broadcast.  The link monitor
diff --git a/link_monitor_unittest.cc b/link_monitor_unittest.cc
index 0bfae85..72db7f4 100644
--- a/link_monitor_unittest.cc
+++ b/link_monitor_unittest.cc
@@ -68,11 +68,33 @@
 };
 
 MATCHER_P4(IsArpRequest, local_ip, remote_ip, local_mac, remote_mac, "") {
-  return
-      local_ip.Equals(arg.local_ip_address()) &&
+  if (local_ip.Equals(arg.local_ip_address()) &&
       remote_ip.Equals(arg.remote_ip_address()) &&
       local_mac.Equals(arg.local_mac_address()) &&
-      remote_mac.Equals(arg.remote_mac_address());
+      remote_mac.Equals(arg.remote_mac_address()))
+    return true;
+
+  if (!local_ip.Equals(arg.local_ip_address())) {
+    *result_listener << "Local IP '" << arg.local_ip_address().ToString()
+                     << "' (wanted '" << local_ip.ToString() << "').";
+  }
+
+  if (!remote_ip.Equals(arg.remote_ip_address())) {
+    *result_listener << "Remote IP '" << arg.remote_ip_address().ToString()
+                     << "' (wanted '" << remote_ip.ToString() << "').";
+  }
+
+  if (!local_mac.Equals(arg.local_mac_address())) {
+    *result_listener << "Local MAC '" << arg.local_mac_address().HexEncode()
+                     << "' (wanted " << local_mac.HexEncode() << ")'.";
+  }
+
+  if (!remote_mac.Equals(arg.remote_mac_address())) {
+    *result_listener << "Remote MAC '" << arg.remote_mac_address().HexEncode()
+                     << "' (wanted " << remote_mac.HexEncode() << ")'.";
+  }
+
+  return false;
 }
 
 class LinkMonitorTest : public Test {
@@ -153,6 +175,8 @@
     EXPECT_TRUE(GetSendRequestCallback().IsCancelled());
     EXPECT_EQ(0, GetBroadcastFailureCount());
     EXPECT_EQ(0, GetUnicastFailureCount());
+    EXPECT_EQ(0, GetBroadcastSuccessCount());
+    EXPECT_EQ(0, GetUnicastSuccessCount());
     EXPECT_FALSE(IsUnicast());
   }
   const ArpClient *GetArpClient() { return monitor_.arp_client_.get(); }
@@ -168,17 +192,29 @@
   int GetUnicastFailureCount() {
     return monitor_.unicast_failure_count_;
   }
-  bool IsUnicast() { return monitor_.is_unicast_; }
-  int GetTestPeriodMilliseconds() {
-    return LinkMonitor::kTestPeriodMilliseconds;
+  int GetBroadcastSuccessCount() {
+    return monitor_.broadcast_success_count_;
   }
-  int GetFailureThreshold() {
+  int GetUnicastSuccessCount() {
+    return monitor_.unicast_success_count_;
+  }
+  bool IsUnicast() { return monitor_.is_unicast_; }
+  int GetCurrentTestPeriodMilliseconds() {
+    return monitor_.test_period_milliseconds_;
+  }
+  int GetDefaultTestPeriodMilliseconds() {
+    return LinkMonitor::kDefaultTestPeriodMilliseconds;
+  }
+  size_t GetFailureThreshold() {
     return LinkMonitor::kFailureThreshold;
   }
+  int GetFastTestPeriodMilliseconds() {
+    return LinkMonitor::kFastTestPeriodMilliseconds;
+  }
   int GetMaxResponseSampleFilterDepth() {
     return LinkMonitor::kMaxResponseSampleFilterDepth;
   }
-  void ExpectTransmit(bool is_unicast) {
+  void ExpectTransmit(bool is_unicast, int transmit_period_milliseconds) {
     const ByteString &destination_mac = is_unicast ? gateway_mac_ : zero_mac_;
     if (monitor_.arp_client_.get()) {
       EXPECT_EQ(client_, monitor_.arp_client_.get());
@@ -192,13 +228,15 @@
           IsArpRequest(local_ip_, gateway_ip_, local_mac_, destination_mac)))
           .WillOnce(Return(true));
     }
-    EXPECT_CALL(dispatcher_, PostDelayedTask(_, GetTestPeriodMilliseconds()));
+    EXPECT_CALL(dispatcher_,
+                PostDelayedTask(_, transmit_period_milliseconds));
   }
   void SendNextRequest() {
     EXPECT_CALL(monitor_, CreateClient())
         .WillOnce(Invoke(this, &LinkMonitorTest::CreateMockClient));
     EXPECT_CALL(*next_client_, TransmitRequest(_)).WillOnce(Return(true));
-    EXPECT_CALL(dispatcher_, PostDelayedTask(_, GetTestPeriodMilliseconds()));
+    EXPECT_CALL(dispatcher_,
+                PostDelayedTask(_, GetCurrentTestPeriodMilliseconds()));
     TriggerRequestTimer();
   }
   void ExpectNoTransmit() {
@@ -206,15 +244,31 @@
       monitor_.arp_client_.get() ? client_ : next_client_;
     EXPECT_CALL(*client, TransmitRequest(_)).Times(0);
   }
+  void ExpectRestart(int transmit_period_milliseconds) {
+    EXPECT_CALL(device_info_, GetMACAddress(0, _))
+        .WillOnce(DoAll(SetArgumentPointee<1>(local_mac_), Return(true)));
+    // Can't just use ExpectTransmit, because that depends on state
+    // that changes during Stop.
+    EXPECT_CALL(monitor_, CreateClient())
+        .WillOnce(Invoke(this, &LinkMonitorTest::CreateMockClient));
+    EXPECT_CALL(*next_client_, TransmitRequest(
+        IsArpRequest(local_ip_, gateway_ip_, local_mac_, zero_mac_)))
+        .WillOnce(Return(true));
+    EXPECT_CALL(dispatcher_,
+                PostDelayedTask(_, transmit_period_milliseconds));
+  }
   void StartMonitor() {
     EXPECT_CALL(device_info_, GetMACAddress(0, _))
         .WillOnce(DoAll(SetArgumentPointee<1>(local_mac_), Return(true)));
-    ExpectTransmit(false);
+    ExpectTransmit(false, GetDefaultTestPeriodMilliseconds());
     EXPECT_TRUE(monitor_.Start());
     EXPECT_TRUE(GetArpClient());
     EXPECT_FALSE(IsUnicast());
     EXPECT_FALSE(GetSendRequestCallback().IsCancelled());
   }
+  void ReportResume() {
+    monitor_.OnAfterResume();
+  }
   bool SimulateReceiveReply(ArpPacket *packet, ByteString *sender) {
     packet->set_local_ip_address(rx_packet_.local_ip_address());
     packet->set_remote_ip_address(rx_packet_.remote_ip_address());
@@ -238,6 +292,9 @@
   void ReceiveCorrectResponse() {
     ReceiveResponse(gateway_ip_, gateway_mac_, local_ip_, local_mac_);
   }
+  bool IsGatewayFound() {
+    return monitor_.IsGatewayFound();
+  }
 
   MockEventDispatcher dispatcher_;
   StrictMock<MockMetrics> metrics_;
@@ -359,22 +416,25 @@
 
 TEST_F(LinkMonitorTest, TimeoutBroadcast) {
   EXPECT_CALL(metrics_, SendToUMA(
-      HasSubstr("LinkMonitorResponseTimeSample"), GetTestPeriodMilliseconds(),
+      HasSubstr("LinkMonitorResponseTimeSample"),
+      GetDefaultTestPeriodMilliseconds(),
       _, _, _)).Times(GetFailureThreshold());
   StartMonitor();
-  // This value doesn't match real life (the timer in this scenario should
-  // advance by LinkMonitor::kTestPeriodMilliseconds), but this demonstrates
-  // the LinkMonitorSecondsToFailure independent from the response-time
-  // figures.
+  // This value doesn't match real life (the timer in this scenario
+  // should advance by LinkMonitor::kDefaultTestPeriodMilliseconds),
+  // but this demonstrates the LinkMonitorSecondsToFailure independent
+  // from the response-time figures.
   const int kTimeIncrement = 1000;
-  for (int i = 1; i < GetFailureThreshold(); ++i) {
-    ExpectTransmit(false);
+  for (size_t i = 1; i < GetFailureThreshold(); ++i) {
+    ExpectTransmit(false, GetDefaultTestPeriodMilliseconds());
     AdvanceTime(kTimeIncrement);
     TriggerRequestTimer();
     EXPECT_FALSE(IsUnicast());
     EXPECT_EQ(i, GetBroadcastFailureCount());
     EXPECT_EQ(0, GetUnicastFailureCount());
-    EXPECT_EQ(GetTestPeriodMilliseconds(),
+    EXPECT_EQ(0, GetBroadcastSuccessCount());
+    EXPECT_EQ(0, GetUnicastSuccessCount());
+    EXPECT_EQ(GetDefaultTestPeriodMilliseconds(),
               monitor_.GetResponseTimeMilliseconds());
   }
   ScopedMockLog log;
@@ -408,24 +468,27 @@
       .Times(GetFailureThreshold());
   // Unsuccessful unicast receptions.
   EXPECT_CALL(metrics_, SendToUMA(
-      HasSubstr("LinkMonitorResponseTimeSample"), GetTestPeriodMilliseconds(),
+      HasSubstr("LinkMonitorResponseTimeSample"),
+      GetDefaultTestPeriodMilliseconds(),
       _, _, _)).Times(GetFailureThreshold());
   ReceiveCorrectResponse();
-  for (int i = 1; i < GetFailureThreshold(); ++i) {
+  for (size_t i = 1; i < GetFailureThreshold(); ++i) {
     // Failed unicast ARP.
-    ExpectTransmit(true);
+    ExpectTransmit(true, GetDefaultTestPeriodMilliseconds());
     TriggerRequestTimer();
 
     // Successful broadcast ARP.
-    ExpectTransmit(false);
+    ExpectTransmit(false, GetDefaultTestPeriodMilliseconds());
     TriggerRequestTimer();
     ReceiveCorrectResponse();
 
     EXPECT_EQ(0, GetBroadcastFailureCount());
     EXPECT_EQ(i, GetUnicastFailureCount());
+    EXPECT_EQ(i+1, GetBroadcastSuccessCount());  // One before loop.
+    EXPECT_EQ(0, GetUnicastSuccessCount());
   }
   // Last unicast ARP transmission.
-  ExpectTransmit(true);
+  ExpectTransmit(true, GetDefaultTestPeriodMilliseconds());
   TriggerRequestTimer();
 
   ScopedMockLog log;
@@ -449,6 +512,83 @@
   ExpectReset();
 }
 
+TEST_F(LinkMonitorTest, OnAfterResume) {
+  const int kFastTestPeriodMilliseconds = GetFastTestPeriodMilliseconds();
+  StartMonitor();
+  Mock::VerifyAndClearExpectations(&monitor_);
+
+  // Resume should preserve the fact that we haven't resolved the gateway's MAC.
+  EXPECT_FALSE(IsGatewayFound());
+  ExpectRestart(kFastTestPeriodMilliseconds);
+  ReportResume();
+  EXPECT_FALSE(IsGatewayFound());
+
+  // After resume, we should use the fast test period...
+  ExpectRestart(kFastTestPeriodMilliseconds);
+  ReportResume();
+  EXPECT_EQ(kFastTestPeriodMilliseconds, GetCurrentTestPeriodMilliseconds());
+
+  // ...and the fast period should be used for reporting failure to UMA...
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("LinkMonitorResponseTimeSample"), kFastTestPeriodMilliseconds,
+      _, _, _));
+  ExpectTransmit(false, kFastTestPeriodMilliseconds);
+  TriggerRequestTimer();
+
+  // ...and the period should be reset after correct responses on both
+  // broadcast and unicast.
+  const int kResponseTime = 12;
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("LinkMonitorResponseTimeSample"), kResponseTime,  _, _, _))
+      .Times(2);
+  AdvanceTime(kResponseTime);
+  ReceiveCorrectResponse();
+  EXPECT_EQ(GetFastTestPeriodMilliseconds(),
+            GetCurrentTestPeriodMilliseconds());  // Don't change yet.
+  ExpectTransmit(true, kFastTestPeriodMilliseconds);
+  TriggerRequestTimer();
+  AdvanceTime(kResponseTime);
+  ReceiveCorrectResponse();
+  EXPECT_EQ(1, GetBroadcastSuccessCount());
+  EXPECT_EQ(1, GetUnicastSuccessCount());
+  EXPECT_EQ(GetDefaultTestPeriodMilliseconds(),
+            GetCurrentTestPeriodMilliseconds());
+
+  // Resume should preserve the fact that we _have_ resolved the gateway's MAC.
+  EXPECT_TRUE(IsGatewayFound());
+  ExpectRestart(kFastTestPeriodMilliseconds);
+  ReportResume();
+  EXPECT_TRUE(IsGatewayFound());
+
+  // Failure should happen just like normal.
+  ExpectRestart(kFastTestPeriodMilliseconds);
+  ReportResume();
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("LinkMonitorResponseTimeSample"), kFastTestPeriodMilliseconds,
+      _, _, _))
+      .Times(GetFailureThreshold());
+  EXPECT_CALL(metrics_, SendEnumToUMA(
+      HasSubstr("LinkMonitorFailure"),
+      Metrics::kLinkMonitorFailureThresholdReached, _));
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("LinkMonitorSecondsToFailure"), _, _, _, _));
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("BroadcastErrorsAtFailure"), GetFailureThreshold() / 2 + 1,
+      _, _, _));
+  EXPECT_CALL(metrics_, SendToUMA(
+      HasSubstr("UnicastErrorsAtFailure"), GetFailureThreshold() / 2,
+      _, _, _));
+  EXPECT_CALL(monitor_, FailureCallbackHandler());
+  bool unicast_probe = true;
+  for (size_t i = 1 ; i < GetFailureThreshold(); ++i) {
+    ExpectTransmit(unicast_probe, GetFastTestPeriodMilliseconds());
+    TriggerRequestTimer();
+    unicast_probe = !unicast_probe;
+  }
+  TriggerRequestTimer();
+  ExpectReset();
+}
+
 TEST_F(LinkMonitorTest, Average) {
   const int kSamples[] = { 200, 950, 1200, 4096, 5000,
                            86, 120, 3060, 842, 750 };
diff --git a/metrics.cc b/metrics.cc
index 903bd0d..41b6f9f 100644
--- a/metrics.cc
+++ b/metrics.cc
@@ -189,7 +189,7 @@
     "Network.Shill.%s.LinkMonitorResponseTimeSample";
 const int Metrics::kMetricLinkMonitorResponseTimeSampleMin = 0;
 const int Metrics::kMetricLinkMonitorResponseTimeSampleMax =
-    LinkMonitor::kTestPeriodMilliseconds;
+    LinkMonitor::kDefaultTestPeriodMilliseconds;
 const int Metrics::kMetricLinkMonitorResponseTimeSampleNumBuckets = 50;
 const char Metrics::kMetricLinkMonitorSecondsToFailure[] =
     "Network.Shill.%s.LinkMonitorSecondsToFailure";
diff --git a/mock_link_monitor.h b/mock_link_monitor.h
index 17f083f..690669d 100644
--- a/mock_link_monitor.h
+++ b/mock_link_monitor.h
@@ -18,6 +18,7 @@
 
   MOCK_METHOD0(Start, bool());
   MOCK_METHOD0(Stop, void());
+  MOCK_METHOD0(OnAfterResume, void());
   MOCK_CONST_METHOD0(GetResponseTimeMilliseconds, int());
   MOCK_CONST_METHOD0(IsGatewayFound, bool());