// 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 #include #include #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(&test_prefs_, &test_clock_); return user_classifier_.get(); } base::SimpleTestClock* test_clock() { return &test_clock_; } private: TestingPrefServiceSimple test_prefs_; std::unique_ptr 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> { 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