| // Copyright 2017 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 "components/ntp_snippets/user_classifier.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/time/time.h" |
| #include "components/ntp_snippets/features.h" |
| #include "components/ntp_snippets/ntp_snippets_constants.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::DoubleNear; |
| using testing::Eq; |
| using testing::Gt; |
| using testing::Lt; |
| using testing::SizeIs; |
| |
| namespace ntp_snippets { |
| namespace { |
| |
| char kNowString[] = "2017-03-01 10:45"; |
| |
| class UserClassifierTest : public testing::Test { |
| public: |
| UserClassifierTest() { |
| UserClassifier::RegisterProfilePrefs(test_prefs_.registry()); |
| } |
| |
| UserClassifier* CreateUserClassifier() { |
| base::Time now; |
| CHECK(base::Time::FromUTCString(kNowString, &now)); |
| test_clock_.SetNow(now); |
| |
| user_classifier_ = |
| std::make_unique<UserClassifier>(&test_prefs_, &test_clock_); |
| return user_classifier_.get(); |
| } |
| |
| base::SimpleTestClock* test_clock() { return &test_clock_; } |
| |
| private: |
| TestingPrefServiceSimple test_prefs_; |
| std::unique_ptr<UserClassifier> user_classifier_; |
| base::SimpleTestClock test_clock_; |
| |
| DISALLOW_COPY_AND_ASSIGN(UserClassifierTest); |
| }; |
| |
| TEST_F(UserClassifierTest, ShouldBeActiveNtpUserInitially) { |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_NTP_USER)); |
| } |
| |
| TEST_F(UserClassifierTest, |
| ShouldBecomeActiveSuggestionsConsumerByClickingOften) { |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // After one click still only an active user. |
| user_classifier->OnEvent(UserClassifier::Metric::SUGGESTIONS_USED); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_NTP_USER)); |
| |
| // After a few more clicks, become an active consumer. |
| for (int i = 0; i < 5; i++) { |
| test_clock()->Advance(base::TimeDelta::FromHours(1)); |
| user_classifier->OnEvent(UserClassifier::Metric::SUGGESTIONS_USED); |
| } |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER)); |
| } |
| |
| TEST_F(UserClassifierTest, |
| ShouldBecomeActiveSuggestionsConsumerByClickingOftenWithDecreasedParam) { |
| // Increase the param to one half. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| kArticleSuggestionsFeature, |
| {{"user_classifier_active_consumer_clicks_at_least_once_per_hours", |
| "36"}}); |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // After two clicks still only an active user. |
| user_classifier->OnEvent(UserClassifier::Metric::SUGGESTIONS_USED); |
| test_clock()->Advance(base::TimeDelta::FromHours(1)); |
| user_classifier->OnEvent(UserClassifier::Metric::SUGGESTIONS_USED); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_NTP_USER)); |
| |
| // One more click to become an active consumer. |
| test_clock()->Advance(base::TimeDelta::FromHours(1)); |
| user_classifier->OnEvent(UserClassifier::Metric::SUGGESTIONS_USED); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER)); |
| } |
| |
| TEST_F(UserClassifierTest, ShouldBecomeRareNtpUserByNoActivity) { |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // After two days of waiting still an active user. |
| test_clock()->Advance(base::TimeDelta::FromDays(2)); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_NTP_USER)); |
| |
| // Two more days to become a rare user. |
| test_clock()->Advance(base::TimeDelta::FromDays(2)); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::RARE_NTP_USER)); |
| } |
| |
| TEST_F(UserClassifierTest, |
| ShouldBecomeRareNtpUserByNoActivityWithDecreasedParam) { |
| // Decrease the param to one half. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| kArticleSuggestionsFeature, |
| {{"user_classifier_rare_user_opens_ntp_at_most_once_per_hours", "48"}}); |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // After one days of waiting still an active user. |
| test_clock()->Advance(base::TimeDelta::FromDays(1)); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::ACTIVE_NTP_USER)); |
| |
| // One more day to become a rare user. |
| test_clock()->Advance(base::TimeDelta::FromDays(1)); |
| EXPECT_THAT(user_classifier->GetUserClass(), |
| Eq(UserClassifier::UserClass::RARE_NTP_USER)); |
| } |
| |
| class UserClassifierMetricTest |
| : public UserClassifierTest, |
| public ::testing::WithParamInterface< |
| std::pair<UserClassifier::Metric, std::string>> { |
| public: |
| UserClassifierMetricTest() {} |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(UserClassifierMetricTest); |
| }; |
| |
| TEST_P(UserClassifierMetricTest, ShouldDecreaseEstimateAfterEvent) { |
| UserClassifier::Metric metric = GetParam().first; |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // The initial event does not decrease the estimate. |
| user_classifier->OnEvent(metric); |
| |
| for (int i = 0; i < 10; i++) { |
| test_clock()->Advance(base::TimeDelta::FromHours(1)); |
| double old_metric = user_classifier->GetEstimatedAvgTime(metric); |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Lt(old_metric)); |
| } |
| } |
| |
| TEST_P(UserClassifierMetricTest, ShouldReportToUmaOnEvent) { |
| UserClassifier::Metric metric = GetParam().first; |
| const std::string& histogram_name = GetParam().second; |
| base::HistogramTester histogram_tester; |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(histogram_tester.GetAllSamples(histogram_name), SizeIs(1)); |
| } |
| |
| TEST_P(UserClassifierMetricTest, ShouldConvergeTowardsPattern) { |
| UserClassifier::Metric metric = GetParam().first; |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // Have the pattern of an event every five hours and start changing it towards |
| // an event every 10 hours. |
| for (int i = 0; i < 100; i++) { |
| test_clock()->Advance(base::TimeDelta::FromHours(5)); |
| user_classifier->OnEvent(metric); |
| } |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), |
| DoubleNear(5.0, 0.1)); |
| for (int i = 0; i < 3; i++) { |
| test_clock()->Advance(base::TimeDelta::FromHours(10)); |
| user_classifier->OnEvent(metric); |
| } |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Gt(5.5)); |
| for (int i = 0; i < 100; i++) { |
| test_clock()->Advance(base::TimeDelta::FromHours(10)); |
| user_classifier->OnEvent(metric); |
| } |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), |
| DoubleNear(10.0, 0.1)); |
| } |
| |
| TEST_P(UserClassifierMetricTest, ShouldIgnoreSubsequentEventsForHalfAnHour) { |
| UserClassifier::Metric metric = GetParam().first; |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // The initial event |
| user_classifier->OnEvent(metric); |
| // Subsequent events get ignored for the next 30 minutes. |
| for (int i = 0; i < 5; i++) { |
| test_clock()->Advance(base::TimeDelta::FromMinutes(5)); |
| double old_metric = user_classifier->GetEstimatedAvgTime(metric); |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Eq(old_metric)); |
| } |
| // An event 30 minutes after the initial event is finally not ignored. |
| test_clock()->Advance(base::TimeDelta::FromMinutes(5)); |
| double old_metric = user_classifier->GetEstimatedAvgTime(metric); |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Lt(old_metric)); |
| } |
| |
| TEST_P(UserClassifierMetricTest, |
| ShouldIgnoreSubsequentEventsWithIncreasedLimit) { |
| UserClassifier::Metric metric = GetParam().first; |
| // Increase the min_hours to 1.0, i.e. 60 minutes. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| kArticleSuggestionsFeature, {{"user_classifier_min_hours", "1.0"}}); |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // The initial event |
| user_classifier->OnEvent(metric); |
| // Subsequent events get ignored for the next 60 minutes. |
| for (int i = 0; i < 11; i++) { |
| test_clock()->Advance(base::TimeDelta::FromMinutes(5)); |
| double old_metric = user_classifier->GetEstimatedAvgTime(metric); |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Eq(old_metric)); |
| } |
| // An event 60 minutes after the initial event is finally not ignored. |
| test_clock()->Advance(base::TimeDelta::FromMinutes(5)); |
| double old_metric = user_classifier->GetEstimatedAvgTime(metric); |
| user_classifier->OnEvent(metric); |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), Lt(old_metric)); |
| } |
| |
| TEST_P(UserClassifierMetricTest, ShouldCapDelayBetweenEvents) { |
| UserClassifier::Metric metric = GetParam().first; |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // The initial event |
| user_classifier->OnEvent(metric); |
| // Wait for an insane amount of time |
| test_clock()->Advance(base::TimeDelta::FromDays(365)); |
| user_classifier->OnEvent(metric); |
| double metric_after_a_year = user_classifier->GetEstimatedAvgTime(metric); |
| |
| // Now repeat the same with s/one year/one week. |
| user_classifier->ClearClassificationForDebugging(); |
| user_classifier->OnEvent(metric); |
| test_clock()->Advance(base::TimeDelta::FromDays(7)); |
| user_classifier->OnEvent(metric); |
| |
| // The results should be the same. |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), |
| Eq(metric_after_a_year)); |
| } |
| |
| TEST_P(UserClassifierMetricTest, |
| ShouldCapDelayBetweenEventsWithDecreasedLimit) { |
| UserClassifier::Metric metric = GetParam().first; |
| // Decrease the max_hours to 72, i.e. 3 days. |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| kArticleSuggestionsFeature, {{"user_classifier_max_hours", "72"}}); |
| UserClassifier* user_classifier = CreateUserClassifier(); |
| |
| // The initial event |
| user_classifier->OnEvent(metric); |
| // Wait for an insane amount of time |
| test_clock()->Advance(base::TimeDelta::FromDays(365)); |
| user_classifier->OnEvent(metric); |
| double metric_after_a_year = user_classifier->GetEstimatedAvgTime(metric); |
| |
| // Now repeat the same with s/one year/two days. |
| user_classifier->ClearClassificationForDebugging(); |
| user_classifier->OnEvent(metric); |
| test_clock()->Advance(base::TimeDelta::FromDays(3)); |
| user_classifier->OnEvent(metric); |
| |
| // The results should be the same. |
| EXPECT_THAT(user_classifier->GetEstimatedAvgTime(metric), |
| Eq(metric_after_a_year)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| NTP, |
| UserClassifierMetricTest, |
| testing::Values( |
| std::make_pair(UserClassifier::Metric::NTP_OPENED, |
| "NewTabPage.UserClassifier.AverageHoursToOpenNTP"), |
| std::make_pair( |
| UserClassifier::Metric::SUGGESTIONS_SHOWN, |
| "NewTabPage.UserClassifier.AverageHoursToShowSuggestions"), |
| std::make_pair( |
| UserClassifier::Metric::SUGGESTIONS_USED, |
| "NewTabPage.UserClassifier.AverageHoursToUseSuggestions"))); |
| |
| } // namespace |
| } // namespace ntp_snippets |