Index: components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc |
diff --git a/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc b/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..70161311c587ee4a0189c193f53bcd1881237c4c |
--- /dev/null |
+++ b/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc |
@@ -0,0 +1,361 @@ |
+// Copyright 2014 The Chromium 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 <queue> |
+ |
+#include "base/callback.h" |
+#include "base/memory/ref_counted.h" |
+#include "base/run_loop.h" |
+#include "base/single_thread_task_runner.h" |
+#include "base/thread_task_runner_handle.h" |
+#include "base/time/tick_clock.h" |
+#include "base/time/time.h" |
+#include "components/password_manager/core/browser/affiliation_fetch_throttler.h" |
+#include "testing/gmock/include/gmock/gmock.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+ |
+namespace password_manager { |
+namespace { |
+ |
+// A SingleThreadTaskRunner that mocks the current time and allows it to be |
+// fast-forwarded. TODO(bartfab): Copies of this class exist in several tests. |
+// Consolidate them (crbug.com/329911). |
+class MockTimeSingleThreadTaskRunner : public base::SingleThreadTaskRunner { |
+ public: |
+ MockTimeSingleThreadTaskRunner(); |
+ |
+ // base::SingleThreadTaskRunner: |
+ virtual bool RunsTasksOnCurrentThread() const override; |
+ virtual bool PostDelayedTask(const tracked_objects::Location& from_here, |
+ const base::Closure& task, |
+ base::TimeDelta delay) override; |
+ virtual bool PostNonNestableDelayedTask( |
+ const tracked_objects::Location& from_here, |
+ const base::Closure& task, |
+ base::TimeDelta delay) override; |
+ |
+ const base::TimeTicks& GetCurrentTime() const; |
+ |
+ size_t task_queue_size() const { |
+ return tasks_.size(); |
+ } |
+ |
+ void FastForwardBy(base::TimeDelta delta); |
+ void FastForwardUntilNoTasksRemain(); |
+ |
+ private: |
+ // Strict weak temporal ordering of tasks. |
+ class TemporalOrder { |
+ public: |
+ bool operator()( |
+ const std::pair<base::TimeTicks, base::Closure>& first_task, |
+ const std::pair<base::TimeTicks, base::Closure>& second_task) const; |
+ }; |
+ |
+ virtual ~MockTimeSingleThreadTaskRunner(); |
+ |
+ base::TimeTicks now_; |
+ std::priority_queue<std::pair<base::TimeTicks, base::Closure>, |
+ std::vector<std::pair<base::TimeTicks, base::Closure>>, |
+ TemporalOrder> tasks_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(MockTimeSingleThreadTaskRunner); |
+}; |
+ |
+// A base::TickClock that uses a MockTimeSingleThreadTaskRunner as the source of |
+// the current time. |
+class MockClock : public base::TickClock { |
+ public: |
+ explicit MockClock(scoped_refptr<MockTimeSingleThreadTaskRunner> task_runner); |
+ virtual ~MockClock(); |
+ |
+ // base::TickClock: |
+ virtual base::TimeTicks NowTicks() override; |
+ |
+ private: |
+ scoped_refptr<MockTimeSingleThreadTaskRunner> task_runner_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(MockClock); |
+}; |
+ |
+MockTimeSingleThreadTaskRunner::MockTimeSingleThreadTaskRunner() { |
+} |
+ |
+bool MockTimeSingleThreadTaskRunner::RunsTasksOnCurrentThread() const { |
+ return true; |
+} |
+ |
+bool MockTimeSingleThreadTaskRunner::PostDelayedTask( |
+ const tracked_objects::Location& from_here, |
+ const base::Closure& task, |
+ base::TimeDelta delay) { |
+ tasks_.push(std::make_pair(now_ + delay, task)); |
+ return true; |
+} |
+ |
+bool MockTimeSingleThreadTaskRunner::PostNonNestableDelayedTask( |
+ const tracked_objects::Location& from_here, |
+ const base::Closure& task, |
+ base::TimeDelta delay) { |
+ NOTREACHED(); |
+ return false; |
+} |
+ |
+const base::TimeTicks& MockTimeSingleThreadTaskRunner::GetCurrentTime() const { |
+ return now_; |
+} |
+ |
+void MockTimeSingleThreadTaskRunner::FastForwardBy(base::TimeDelta delta) { |
+ const base::TimeTicks latest = now_ + delta; |
+ while (!tasks_.empty() && tasks_.top().first <= latest) { |
+ now_ = tasks_.top().first; |
+ base::Closure task = tasks_.top().second; |
+ tasks_.pop(); |
+ task.Run(); |
+ } |
+ now_ = latest; |
+} |
+ |
+void MockTimeSingleThreadTaskRunner::FastForwardUntilNoTasksRemain() { |
+ while (!tasks_.empty()) { |
+ now_ = tasks_.top().first; |
+ base::Closure task = tasks_.top().second; |
+ tasks_.pop(); |
+ task.Run(); |
+ } |
+} |
+ |
+bool MockTimeSingleThreadTaskRunner::TemporalOrder::operator()( |
+ const std::pair<base::TimeTicks, base::Closure>& first_task, |
+ const std::pair<base::TimeTicks, base::Closure>& second_task) const { |
+ return first_task.first > second_task.first; |
+} |
+ |
+MockTimeSingleThreadTaskRunner::~MockTimeSingleThreadTaskRunner() { |
+} |
+ |
+MockClock::MockClock(scoped_refptr<MockTimeSingleThreadTaskRunner> task_runner) |
+ : task_runner_(task_runner) { |
+} |
+ |
+MockClock::~MockClock() { |
+} |
+ |
+base::TimeTicks MockClock::NowTicks() { |
+ return task_runner_->GetCurrentTime(); |
+} |
+ |
+class MockAffiliationFetchThrottlerDelegate |
+ : public testing::StrictMock<AffiliationFetchThrottlerDelegate> { |
+ public: |
+ MockAffiliationFetchThrottlerDelegate() {} |
+ ~MockAffiliationFetchThrottlerDelegate() {} |
+ |
+ MOCK_METHOD0(OnCanSendNetworkRequest, bool()); |
+ |
+ private: |
+ DISALLOW_COPY_AND_ASSIGN(MockAffiliationFetchThrottlerDelegate); |
+}; |
+ |
+class MockNetworkChangeNotifier : public net::NetworkChangeNotifier { |
+ public: |
+ MockNetworkChangeNotifier() : connection_type_(CONNECTION_UNKNOWN) {} |
+ |
+ void set_connection_type(ConnectionType connection_type) { |
+ connection_type_ = connection_type; |
+ NotifyObserversOfConnectionTypeChangeForTests(connection_type_); |
+ } |
+ |
+ ConnectionType GetCurrentConnectionType() const override { |
+ return connection_type_; |
+ } |
+ |
+ private: |
+ ConnectionType connection_type_; |
+}; |
+ |
+} // namespace |
+ |
+class AffiliationFetchThrottlerTest : public testing::Test { |
+ public: |
+ AffiliationFetchThrottlerTest() |
+ : network_change_notifier_(new MockNetworkChangeNotifier), |
+ task_runner_(new MockTimeSingleThreadTaskRunner) {} |
+ ~AffiliationFetchThrottlerTest() override {} |
+ |
+ void SetUp() override { |
+ policy_.reset(new AffiliationFetchThrottler( |
+ &mock_delegate_, task_runner_, |
+ make_scoped_ptr(new MockClock(task_runner_)))); |
+ } |
+ |
+ void SimulateHasNetworkConnectivity(bool has_connectivity) { |
+ network_change_notifier_->set_connection_type(has_connectivity ? |
+ net::NetworkChangeNotifier::CONNECTION_UNKNOWN : |
+ net::NetworkChangeNotifier::CONNECTION_NONE); |
+ base::RunLoop().RunUntilIdle(); |
+ } |
+ |
+ // Forwards time, and given that MockAffiliationFetchThrottlerDelegate is a |
+ // strict mock, also ensures OnCanSendNetworkRequest() is not called back |
+ // during this period. |
+ void FastForwardTimeBySecs(double secs) { |
+ task_runner_->FastForwardBy(base::TimeDelta::FromSecondsD(secs)); |
+ } |
+ |
+ void FastForwardUntilNoTasksRemain() { |
+ task_runner_->FastForwardUntilNoTasksRemain(); |
+ } |
+ |
+ size_t NumberOfEnqueuedTasks() { |
+ return task_runner_->task_queue_size(); |
+ } |
+ |
+ // Forwards time until the next OnCanSendNetworkRequest() callback, and |
+ // verifies that it occurs between |min_delay_sec| and |max_delay_sec| seconds |
+ // from now, inclusive. |
+ void AssertReleaseBetweenSecs(double min_delay_sec, double max_delay_sec) { |
+ base::TimeTicks ticks_at_start = task_runner_->GetCurrentTime(); |
+ base::TimeDelta min_delay = base::TimeDelta::FromSecondsD(min_delay_sec); |
+ if (min_delay.ToInternalValue() > 0) { |
+ task_runner_->FastForwardBy(min_delay - |
+ base::TimeDelta::FromInternalValue(1)); |
+ } |
+ EXPECT_CALL(mock_delegate_, OnCanSendNetworkRequest()) |
+ .Times(1) |
+ .WillRepeatedly(testing::Return(true)); |
+ task_runner_->FastForwardUntilNoTasksRemain(); |
+ ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&mock_delegate_)); |
+ ASSERT_GE(base::TimeDelta::FromSecondsD(max_delay_sec), |
+ task_runner_->GetCurrentTime() - ticks_at_start); |
+ } |
+ |
+ AffiliationFetchThrottler& policy() { return *policy_.get(); } |
+ |
+ private: |
+ // Needed because NetworkChangeNotifier uses ObserverList, which notifies |
+ // observers on the MessageLoop that belongs to the thread from which they |
+ // have registered. |
+ base::MessageLoop message_loop_; |
+ scoped_ptr<MockNetworkChangeNotifier> network_change_notifier_; |
+ scoped_refptr<MockTimeSingleThreadTaskRunner> task_runner_; |
+ MockAffiliationFetchThrottlerDelegate mock_delegate_; |
+ scoped_ptr<AffiliationFetchThrottler> policy_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(AffiliationFetchThrottlerTest); |
+}; |
+ |
+TEST_F(AffiliationFetchThrottlerTest, SuccessfulRequests) { |
+ SimulateHasNetworkConnectivity(true); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ AssertReleaseBetweenSecs(0, 0); |
+ |
+ // Signal while request is in flight should be ignored. |
+ policy().SignalNetworkRequestNeeded(); |
+ FastForwardUntilNoTasksRemain(); |
+ policy().InformOfNetworkRequestComplete(true); |
+ FastForwardUntilNoTasksRemain(); |
+ |
+ // Duplicate the second signal 3 times: still only 1 callback should arrive. |
+ policy().SignalNetworkRequestNeeded(); |
+ policy().SignalNetworkRequestNeeded(); |
+ policy().SignalNetworkRequestNeeded(); |
+ AssertReleaseBetweenSecs(0, 0); |
+} |
+ |
+TEST_F(AffiliationFetchThrottlerTest, FailedRequests) { |
+ SimulateHasNetworkConnectivity(true); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(0, 0)); |
+ policy().InformOfNetworkRequestComplete(false); |
+ |
+ // Request after first failure should be delayed by 10 seconds % 50% fuzzing |
+ // factor. |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(5, 10)); |
+ policy().InformOfNetworkRequestComplete(false); |
+ |
+ // Request after first failure should be delayed by 40 seconds % 50% fuzzing |
+ // factor, applied twice. |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(10, 40)); |
+ policy().InformOfNetworkRequestComplete(false); |
+} |
+ |
+TEST_F(AffiliationFetchThrottlerTest, GracePeriodAfterConnectivityIsRestored) { |
+ SimulateHasNetworkConnectivity(false); |
+ |
+ // After connectivity is re-established, the first request should be delayed |
+ // by 10 seconds % 50% fuzzing factor. |
+ policy().SignalNetworkRequestNeeded(); |
+ SimulateHasNetworkConnectivity(true); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(5, 10)); |
+ policy().InformOfNetworkRequestComplete(true); |
+ |
+ // The next request should not be delayed. |
+ policy().SignalNetworkRequestNeeded(); |
+ AssertReleaseBetweenSecs(0, 0); |
+} |
+ |
+// Same as GracePeriodAfterConnectivityIsRestored, but the network comes back |
+// just before SignalNetworkRequestNeeded() is called. |
+TEST_F(AffiliationFetchThrottlerTest, |
+ GracePeriodAfterConnectivityIsRestored2) { |
+ SimulateHasNetworkConnectivity(false); |
+ |
+ SimulateHasNetworkConnectivity(true); |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(5, 10)); |
+ policy().InformOfNetworkRequestComplete(true); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ AssertReleaseBetweenSecs(0, 0); |
+} |
+ |
+// TODO(engedy): This test is flaky. Investigate. |
+TEST_F(AffiliationFetchThrottlerTest, ConnectivityLostDuringBackoff) { |
+ SimulateHasNetworkConnectivity(true); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(0, 0)); |
+ policy().InformOfNetworkRequestComplete(false); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ SimulateHasNetworkConnectivity(false); |
+ |
+ // Let the exponential backoff delay expire, and verify nothing happens. |
+ FastForwardUntilNoTasksRemain(); |
+ |
+ // Verify that the request is, however, sent after 10 seconds % 50 fuzzing |
+ // factor. |
+ SimulateHasNetworkConnectivity(true); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(5, 10)); |
+} |
+ |
+TEST_F(AffiliationFetchThrottlerTest, |
+ ConnectivityLostAndRestoredDuringBackoff) { |
+ SimulateHasNetworkConnectivity(true); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(0, 0)); |
+ policy().InformOfNetworkRequestComplete(false); |
+ |
+ policy().SignalNetworkRequestNeeded(); |
+ |
+ // Also verify that a flaky connection will not flood the task queue with lots |
+ // of tasks. |
+ for (size_t t = 1; t <= 5; ++t) { |
+ SimulateHasNetworkConnectivity(false); |
+ SimulateHasNetworkConnectivity(true); |
+ FastForwardTimeBySecs(1); |
+ } |
+ EXPECT_EQ(1u, NumberOfEnqueuedTasks()); |
+ |
+ ASSERT_NO_FATAL_FAILURE(AssertReleaseBetweenSecs(5, 10)); |
+} |
+ |
+ |
+} // namespace password_manager |