| // 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/variations/service/variations_field_trial_creator.h" |
| |
| #include <stddef.h> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/feature_list.h" |
| #include "base/macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/version.h" |
| #include "build/build_config.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "components/variations/platform_field_trials.h" |
| #include "components/variations/pref_names.h" |
| #include "components/variations/proto/variations_seed.pb.h" |
| #include "components/variations/service/safe_seed_manager.h" |
| #include "components/variations/service/variations_service.h" |
| #include "components/variations/service/variations_service_client.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if defined(OS_ANDROID) |
| #include "components/variations/android/variations_seed_bridge.h" |
| #include "components/variations/seed_response.h" |
| #endif // OS_ANDROID |
| |
| using testing::_; |
| using testing::Ge; |
| using testing::Return; |
| |
| namespace variations { |
| namespace { |
| |
| // Constants used to create the test seeds. |
| const char kTestSeedStudyName[] = "test"; |
| const char kTestSeedExperimentName[] = "abc"; |
| const char kTestSafeSeedExperimentName[] = "abc.safe"; |
| const int kTestSeedExperimentProbability = 100; |
| const char kTestSeedSerialNumber[] = "123"; |
| |
| // Constants used to mock the serialized seed state. |
| const char kTestSeedData[] = "a serialized seed, 100% realistic"; |
| const char kTestSeedSignature[] = "a totally valid signature, I swear!"; |
| |
| // Populates |seed| with simple test data. The resulting seed will contain one |
| // study called "test", which contains one experiment called "abc" with |
| // probability weight 100. |
| VariationsSeed CreateTestSeed() { |
| VariationsSeed seed; |
| Study* study = seed.add_study(); |
| study->set_name(kTestSeedStudyName); |
| study->set_default_experiment_name(kTestSeedExperimentName); |
| Study_Experiment* experiment = study->add_experiment(); |
| experiment->set_name(kTestSeedExperimentName); |
| experiment->set_probability_weight(kTestSeedExperimentProbability); |
| seed.set_serial_number(kTestSeedSerialNumber); |
| return seed; |
| } |
| |
| // Returns a seed containing simple test data. The resulting seed will contain |
| // one study called "test", which contains one experiment called "abc.safe" with |
| // probability weight 100. This is intended to be used whenever a "safe" seed is |
| // called for, so that test expectations can distinguish between a "safe" seed |
| // and a "latest" seed. |
| VariationsSeed CreateTestSafeSeed() { |
| VariationsSeed seed = CreateTestSeed(); |
| Study* study = seed.mutable_study(0); |
| study->set_default_experiment_name(kTestSafeSeedExperimentName); |
| study->mutable_experiment(0)->set_name(kTestSafeSeedExperimentName); |
| return seed; |
| } |
| |
| #if defined(OS_ANDROID) |
| const char kTestSeedCountry[] = "in"; |
| |
| // Populates |seed| with simple test data, targetting only users in a specific |
| // country. The resulting seed will contain one study called "test", which |
| // contains one experiment called "abc" with probability weight 100, restricted |
| // just to users in |kTestSeedCountry|. |
| VariationsSeed CreateTestSeedWithCountryFilter() { |
| VariationsSeed seed = CreateTestSeed(); |
| Study* study = seed.mutable_study(0); |
| Study::Filter* filter = study->mutable_filter(); |
| filter->add_country(kTestSeedCountry); |
| return seed; |
| } |
| |
| // Serializes |seed| to protobuf binary format. |
| std::string SerializeSeed(const VariationsSeed& seed) { |
| std::string serialized_seed; |
| seed.SerializeToString(&serialized_seed); |
| return serialized_seed; |
| } |
| |
| // Returns the |time| formatted as a UTC string. |
| std::string ToUTCString(base::Time time) { |
| base::Time::Exploded exploded; |
| time.UTCExplode(&exploded); |
| return base::StringPrintf("%d-%d-%d %d:%d:%d UTC", exploded.year, |
| exploded.month, exploded.day_of_month, |
| exploded.hour, exploded.minute, exploded.second); |
| } |
| #endif // OS_ANDROID |
| |
| class TestPlatformFieldTrials : public PlatformFieldTrials { |
| public: |
| TestPlatformFieldTrials() = default; |
| ~TestPlatformFieldTrials() override = default; |
| |
| // PlatformFieldTrials: |
| void SetupFieldTrials() override {} |
| void SetupFeatureControllingFieldTrials( |
| bool has_seed, |
| base::FeatureList* feature_list) override {} |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(TestPlatformFieldTrials); |
| }; |
| |
| class MockSafeSeedManager : public SafeSeedManager { |
| public: |
| explicit MockSafeSeedManager(PrefService* local_state) |
| : SafeSeedManager(true, local_state) {} |
| ~MockSafeSeedManager() override = default; |
| |
| MOCK_CONST_METHOD0(ShouldRunInSafeMode, bool()); |
| MOCK_METHOD4(DoSetActiveSeedState, |
| void(const std::string& seed_data, |
| const std::string& base64_seed_signature, |
| ClientFilterableState* client_filterable_state, |
| base::Time seed_fetch_time)); |
| |
| void SetActiveSeedState( |
| const std::string& seed_data, |
| const std::string& base64_seed_signature, |
| std::unique_ptr<ClientFilterableState> client_filterable_state, |
| base::Time seed_fetch_time) override { |
| DoSetActiveSeedState(seed_data, base64_seed_signature, |
| client_filterable_state.get(), seed_fetch_time); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockSafeSeedManager); |
| }; |
| |
| class TestVariationsServiceClient : public VariationsServiceClient { |
| public: |
| TestVariationsServiceClient() = default; |
| ~TestVariationsServiceClient() override = default; |
| |
| // VariationsServiceClient: |
| std::string GetApplicationLocale() override { return std::string(); } |
| base::Callback<base::Version(void)> GetVersionForSimulationCallback() |
| override { |
| return base::Callback<base::Version(void)>(); |
| } |
| scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory() |
| override { |
| return nullptr; |
| } |
| network_time::NetworkTimeTracker* GetNetworkTimeTracker() override { |
| return nullptr; |
| } |
| version_info::Channel GetChannel() override { |
| return version_info::Channel::UNKNOWN; |
| } |
| bool OverridesRestrictParameter(std::string* parameter) override { |
| if (restrict_parameter_.empty()) |
| return false; |
| *parameter = restrict_parameter_; |
| return true; |
| } |
| |
| void set_restrict_parameter(const std::string& value) { |
| restrict_parameter_ = value; |
| } |
| |
| private: |
| std::string restrict_parameter_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestVariationsServiceClient); |
| }; |
| |
| class TestVariationsSeedStore : public VariationsSeedStore { |
| public: |
| explicit TestVariationsSeedStore(PrefService* local_state) |
| : VariationsSeedStore(local_state), local_state_(local_state) {} |
| ~TestVariationsSeedStore() override = default; |
| |
| bool LoadSeed(VariationsSeed* seed, |
| std::string* seed_data, |
| std::string* base64_signature) override { |
| *seed = CreateTestSeed(); |
| *seed_data = kTestSeedData; |
| *base64_signature = kTestSeedSignature; |
| return true; |
| } |
| |
| bool LoadSafeSeed(VariationsSeed* seed, |
| ClientFilterableState* client_state, |
| base::Time* seed_fetch_time) override { |
| if (has_corrupted_safe_seed_) |
| return false; |
| |
| *seed = CreateTestSafeSeed(); |
| *seed_fetch_time = |
| local_state_->GetTime(prefs::kVariationsSafeSeedFetchTime); |
| return true; |
| } |
| |
| void set_has_corrupted_safe_seed(bool is_corrupted) { |
| has_corrupted_safe_seed_ = is_corrupted; |
| } |
| |
| private: |
| // Whether to simulate having a corrupted safe seed. |
| bool has_corrupted_safe_seed_ = false; |
| |
| PrefService* local_state_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestVariationsSeedStore); |
| }; |
| |
| class TestVariationsFieldTrialCreator : public VariationsFieldTrialCreator { |
| public: |
| TestVariationsFieldTrialCreator(PrefService* local_state, |
| TestVariationsServiceClient* client, |
| SafeSeedManager* safe_seed_manager) |
| : VariationsFieldTrialCreator(local_state, client, UIStringOverrider()), |
| seed_store_(local_state), |
| safe_seed_manager_(safe_seed_manager) {} |
| |
| ~TestVariationsFieldTrialCreator() override = default; |
| |
| // A convenience wrapper around SetupFieldTrials() which passes default values |
| // for uninteresting params. |
| bool SetupFieldTrials() { |
| TestPlatformFieldTrials platform_field_trials; |
| return VariationsFieldTrialCreator::SetupFieldTrials( |
| "", "", "", std::set<std::string>(), std::vector<std::string>(), |
| nullptr, std::make_unique<base::FeatureList>(), &platform_field_trials, |
| safe_seed_manager_); |
| } |
| |
| TestVariationsSeedStore* seed_store() { return &seed_store_; } |
| |
| private: |
| VariationsSeedStore* GetSeedStore() override { return &seed_store_; } |
| |
| TestVariationsSeedStore seed_store_; |
| SafeSeedManager* const safe_seed_manager_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestVariationsFieldTrialCreator); |
| }; |
| |
| } // namespace |
| |
| class FieldTrialCreatorTest : public ::testing::Test { |
| protected: |
| FieldTrialCreatorTest() : field_trial_list_(nullptr) { |
| VariationsService::RegisterPrefs(prefs_.registry()); |
| global_feature_list_ = base::FeatureList::ClearInstanceForTesting(); |
| } |
| |
| ~FieldTrialCreatorTest() override { |
| // Clear out any features created by tests in this suite, and restore the |
| // global feature list. |
| base::FeatureList::ClearInstanceForTesting(); |
| base::FeatureList::RestoreInstanceForTesting( |
| std::move(global_feature_list_)); |
| } |
| |
| protected: |
| TestingPrefServiceSimple prefs_; |
| |
| private: |
| // The global feature list, which is ignored by tests in this suite. |
| std::unique_ptr<base::FeatureList> global_feature_list_; |
| |
| // A local FieldTrialList to hold any field trials created in this suite. |
| base::FieldTrialList field_trial_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FieldTrialCreatorTest); |
| }; |
| |
| TEST_F(FieldTrialCreatorTest, SetupFieldTrials_ValidSeed) { |
| // With a valid seed, the safe seed manager should be informed of the active |
| // seed state. |
| const base::Time now = base::Time::Now(); |
| const base::Time recent_time = now - base::TimeDelta::FromMinutes(17); |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()) |
| .WillByDefault(Return(false)); |
| EXPECT_CALL( |
| safe_seed_manager, |
| DoSetActiveSeedState(kTestSeedData, kTestSeedSignature, _, recent_time)) |
| .Times(1); |
| |
| TestVariationsServiceClient variations_service_client; |
| TestVariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, &safe_seed_manager); |
| |
| // Simulate a seed having been stored recently. |
| prefs_.SetTime(prefs::kVariationsLastFetchTime, recent_time); |
| |
| // Check that field trials are created from the seed. Since the test study has |
| // only 1 experiment with 100% probability weight, we must be part of it. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(field_trial_creator.SetupFieldTrials()); |
| EXPECT_EQ(kTestSeedExperimentName, |
| base::FieldTrialList::FindFullName(kTestSeedStudyName)); |
| |
| // Verify metrics. |
| histogram_tester.ExpectUniqueSample("Variations.SeedFreshness", 17, 1); |
| histogram_tester.ExpectUniqueSample("Variations.SafeMode.FellBackToSafeMode2", |
| false, 1); |
| } |
| |
| TEST_F(FieldTrialCreatorTest, SetupFieldTrials_NoLastFetchTime) { |
| // With a valid seed on first run, the safe seed manager should be informed of |
| // the active seed state. The last fetch time in this case is expected to be |
| // inferred to be recent. |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()) |
| .WillByDefault(Return(false)); |
| const base::Time start_time = base::Time::Now(); |
| EXPECT_CALL(safe_seed_manager, |
| DoSetActiveSeedState(kTestSeedData, kTestSeedSignature, _, |
| Ge(start_time))) |
| .Times(1); |
| |
| TestVariationsServiceClient variations_service_client; |
| TestVariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, &safe_seed_manager); |
| |
| // Simulate a first run by leaving |prefs::kVariationsLastFetchTime| empty. |
| EXPECT_EQ(0, prefs_.GetInt64(prefs::kVariationsLastFetchTime)); |
| |
| // Check that field trials are created from the seed. Since the test study has |
| // only 1 experiment with 100% probability weight, we must be part of it. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(field_trial_creator.SetupFieldTrials()); |
| EXPECT_EQ(base::FieldTrialList::FindFullName(kTestSeedStudyName), |
| kTestSeedExperimentName); |
| |
| // Verify metrics. The seed freshness metric should not be recorded on first |
| // run. |
| histogram_tester.ExpectTotalCount("Variations.SeedFreshness", 0); |
| histogram_tester.ExpectUniqueSample("Variations.SafeMode.FellBackToSafeMode2", |
| false, 1); |
| } |
| |
| TEST_F(FieldTrialCreatorTest, SetupFieldTrials_ExpiredSeed) { |
| // With an expired seed, there should be no field trials created, and hence no |
| // active state should be passed to the safe seed manager. |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()) |
| .WillByDefault(Return(false)); |
| EXPECT_CALL(safe_seed_manager, DoSetActiveSeedState(_, _, _, _)).Times(0); |
| |
| TestVariationsServiceClient variations_service_client; |
| TestVariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, &safe_seed_manager); |
| |
| // Simulate an expired seed. |
| const base::Time seed_date = |
| base::Time::Now() - base::TimeDelta::FromDays(31); |
| prefs_.SetTime(prefs::kVariationsLastFetchTime, seed_date); |
| |
| // Check that field trials are not created from the expired seed. |
| base::HistogramTester histogram_tester; |
| EXPECT_FALSE(field_trial_creator.SetupFieldTrials()); |
| EXPECT_TRUE(base::FieldTrialList::FindFullName(kTestSeedStudyName).empty()); |
| |
| // Verify metrics. The seed freshness metric should not be recorded for an |
| // expired seed. |
| histogram_tester.ExpectTotalCount("Variations.SeedFreshness", 0); |
| histogram_tester.ExpectTotalCount("Variations.SafeMode.FellBackToSafeMode2", |
| 0); |
| } |
| |
| TEST_F(FieldTrialCreatorTest, SetupFieldTrials_ValidSafeSeed) { |
| // With a valid safe seed, the safe seed manager should *not* be informed of |
| // the active seed state. This is an optimization to avoid saving a safe seed |
| // when already running in safe mode. |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()).WillByDefault(Return(true)); |
| EXPECT_CALL(safe_seed_manager, DoSetActiveSeedState(_, _, _, _)).Times(0); |
| |
| const base::Time now = base::Time::Now(); |
| const base::Time earlier = now - base::TimeDelta::FromMinutes(17); |
| TestVariationsServiceClient variations_service_client; |
| TestVariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, &safe_seed_manager); |
| prefs_.SetTime(prefs::kVariationsLastFetchTime, now); |
| prefs_.SetTime(prefs::kVariationsSafeSeedFetchTime, earlier); |
| |
| // Check that field trials are created from the safe seed. Since the test |
| // study has only 1 experiment with 100% probability weight, we must be part |
| // of it. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(field_trial_creator.SetupFieldTrials()); |
| EXPECT_EQ(kTestSafeSeedExperimentName, |
| base::FieldTrialList::FindFullName(kTestSeedStudyName)); |
| |
| // Verify metrics. |
| histogram_tester.ExpectUniqueSample("Variations.SeedFreshness", 17, 1); |
| histogram_tester.ExpectUniqueSample("Variations.SafeMode.FellBackToSafeMode2", |
| true, 1); |
| } |
| |
| TEST_F(FieldTrialCreatorTest, |
| SetupFieldTrials_CorruptedSafeSeed_FallsBackToLatestSeed) { |
| // With a corrupted safe seed, the field trial creator should fall back to the |
| // latest seed. Hence, the safe seed manager *should* be informed of the |
| // active seed state. |
| const base::Time now = base::Time::Now(); |
| const base::Time recent_time = now - base::TimeDelta::FromMinutes(17); |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()).WillByDefault(Return(true)); |
| EXPECT_CALL( |
| safe_seed_manager, |
| DoSetActiveSeedState(kTestSeedData, kTestSeedSignature, _, recent_time)) |
| .Times(1); |
| |
| TestVariationsServiceClient variations_service_client; |
| TestVariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, &safe_seed_manager); |
| field_trial_creator.seed_store()->set_has_corrupted_safe_seed(true); |
| prefs_.SetTime(prefs::kVariationsLastFetchTime, recent_time); |
| prefs_.SetTime(prefs::kVariationsSafeSeedFetchTime, |
| now - base::TimeDelta::FromDays(4)); |
| |
| // Check that field trials are created from the latest seed. Since the test |
| // study has only 1 experiment with 100% probability weight, we must be part |
| // of it. |
| base::HistogramTester histogram_tester; |
| EXPECT_TRUE(field_trial_creator.SetupFieldTrials()); |
| EXPECT_EQ(kTestSeedExperimentName, |
| base::FieldTrialList::FindFullName(kTestSeedStudyName)); |
| |
| // Verify metrics. |
| histogram_tester.ExpectUniqueSample("Variations.SeedFreshness", 17, 1); |
| histogram_tester.ExpectUniqueSample("Variations.SafeMode.FellBackToSafeMode2", |
| false, 1); |
| } |
| |
| #if defined(OS_ANDROID) |
| // This is a regression test for https://crbug.com/829527 |
| TEST_F(FieldTrialCreatorTest, SetupFieldTrials_LoadsCountryOnFirstRun) { |
| // Simulate having received a seed in Java during First Run. |
| const base::Time one_day_ago = |
| base::Time::Now() - base::TimeDelta::FromDays(1); |
| auto initial_seed = std::make_unique<SeedResponse>(); |
| initial_seed->data = SerializeSeed(CreateTestSeedWithCountryFilter()); |
| initial_seed->signature = kTestSeedSignature; |
| initial_seed->country = kTestSeedCountry; |
| initial_seed->date = ToUTCString(one_day_ago); |
| initial_seed->is_gzip_compressed = false; |
| |
| TestVariationsServiceClient variations_service_client; |
| TestPlatformFieldTrials platform_field_trials; |
| testing::NiceMock<MockSafeSeedManager> safe_seed_manager(&prefs_); |
| ON_CALL(safe_seed_manager, ShouldRunInSafeMode()) |
| .WillByDefault(Return(false)); |
| |
| // Note: Unlike other tests, this test does not mock out the seed store, since |
| // the interaction between these two classes is what's being tested. |
| VariationsFieldTrialCreator field_trial_creator( |
| &prefs_, &variations_service_client, UIStringOverrider(), |
| std::move(initial_seed)); |
| |
| // Check that field trials are created from the seed. The test seed contains a |
| // single study with an experiment targeting 100% of users in India. Since |
| // |initial_seed| included the country code for India, this study should be |
| // active. |
| EXPECT_TRUE(field_trial_creator.SetupFieldTrials( |
| "", "", "", std::set<std::string>(), std::vector<std::string>(), nullptr, |
| std::make_unique<base::FeatureList>(), &platform_field_trials, |
| &safe_seed_manager)); |
| |
| EXPECT_EQ(kTestSeedExperimentName, |
| base::FieldTrialList::FindFullName(kTestSeedStudyName)); |
| } |
| #endif // OS_ANDROID |
| |
| } // namespace variations |