| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/test/scoped_feature_list.h" |
| |
| #include <atomic> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase_vector.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/field_trial_param_associator.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/mock_entropy_provider.h" |
| #include "base/test/task_environment.h" |
| |
| namespace base { |
| namespace test { |
| namespace { |
| |
| // A monotonically increasing id, passed to `FeatureList`s as they are created |
| // to invalidate the cache member of `base::Feature` objects that were queried |
| // with a different `FeatureList` installed. |
| uint16_t g_current_caching_context = 1; |
| |
| } // namespace |
| |
| // A struct describes ParsedEnableFeatures()' result. |
| struct ScopedFeatureList::FeatureWithStudyGroup { |
| FeatureWithStudyGroup(const std::string& feature_name, |
| const std::string& study_name, |
| const std::string& group_name, |
| const std::string& params) |
| : feature_name(feature_name), |
| study_name(study_name), |
| group_name(group_name), |
| params(params) { |
| DCHECK(IsValidFeatureName(feature_name)); |
| DCHECK(IsValidFeatureOrFieldTrialName(study_name)); |
| DCHECK(IsValidFeatureOrFieldTrialName(group_name)); |
| } |
| |
| explicit FeatureWithStudyGroup(const std::string& feature_name) |
| : feature_name(feature_name) { |
| DCHECK(IsValidFeatureName(feature_name)); |
| } |
| |
| ~FeatureWithStudyGroup() = default; |
| FeatureWithStudyGroup(const FeatureWithStudyGroup& other) = default; |
| |
| bool operator==(const FeatureWithStudyGroup& other) const { |
| return feature_name == other.feature_name && |
| StudyNameOrDefault() == other.StudyNameOrDefault() && |
| GroupNameOrDefault() == other.GroupNameOrDefault(); |
| } |
| |
| std::string FeatureName() const { |
| return StartsWith(feature_name, "*") ? feature_name.substr(1) |
| : feature_name; |
| } |
| |
| // If |study_name| is empty, returns a default study name for |feature_name|. |
| // Otherwise, just return |study_name|. |
| std::string StudyNameOrDefault() const { |
| return study_name.empty() ? "Study" + FeatureName() : study_name; |
| } |
| |
| // If |group_name| is empty, returns a default group name for |feature_name|. |
| // Otherwise, just return |group_name|. |
| std::string GroupNameOrDefault() const { |
| return group_name.empty() ? "Group" + FeatureName() : group_name; |
| } |
| |
| bool has_params() const { return !params.empty(); } |
| |
| std::string ParamsForFeatureList() const { |
| if (params.empty()) |
| return ""; |
| return ":" + params; |
| } |
| |
| static bool IsValidFeatureOrFieldTrialName(const StringPiece& name) { |
| return IsStringASCII(name) && |
| name.find_first_of(",<*") == std::string::npos; |
| } |
| |
| static bool IsValidFeatureName(const StringPiece& feature_name) { |
| return IsValidFeatureOrFieldTrialName( |
| StartsWith(feature_name, "*") ? feature_name.substr(1) : feature_name); |
| } |
| |
| // When ParseEnableFeatures() gets |
| // "FeatureName<StudyName.GroupName:Param1/Value1/Param2/Value2", |
| // a new FeatureWithStudyGroup with: |
| // - feature_name = "FeatureName" |
| // - study_name = "StudyName" |
| // - group_name = "GroupName" |
| // - params = "Param1/Value1/Param2/Value2" |
| // will be created and be returned. |
| const std::string feature_name; |
| const std::string study_name; |
| const std::string group_name; |
| const std::string params; |
| }; |
| |
| struct ScopedFeatureList::Features { |
| std::vector<FeatureWithStudyGroup> enabled_feature_list; |
| std::vector<FeatureWithStudyGroup> disabled_feature_list; |
| }; |
| |
| namespace { |
| |
| constexpr char kTrialGroup[] = "scoped_feature_list_trial_group"; |
| |
| // Checks and parses the |enable_features| flag and appends each parsed |
| // feature, an instance of FeatureWithStudyGroup, to |parsed_enable_features|. |
| // Returns true if |enable_features| is parsable, otherwise false. |
| // The difference between this function and ParseEnabledFeatures() defined in |
| // feature_list.cc is: |
| // if "Feature1<Study1.Group1:Param1/Value1/Param2/Value2," + |
| // "Feature2<Study2.Group2" is given, |
| // feature_list.cc's one returns strings: |
| // parsed_enable_features = "Feature1<Study1,Feature2<Study2" |
| // force_field_trials = "Study1/Group1" |
| // force_fieldtrial_params = "Study1<Group1:Param1/Value1/Param2/Value2" |
| // this function returns a vector: |
| // [0] FeatureWithStudyGroup("Feature1", "Study1", "Group1", |
| // "Param1/Value1/Param2/Value2") |
| // [1] FeatureWithStudyGroup("Feature2", "Study2", "Group2", "") |
| bool ParseEnableFeatures(const std::string& enable_features, |
| std::vector<ScopedFeatureList::FeatureWithStudyGroup>& |
| parsed_enable_features) { |
| for (const auto& enable_feature : |
| FeatureList::SplitFeatureListString(enable_features)) { |
| std::string feature_name; |
| std::string study; |
| std::string group; |
| std::string feature_params; |
| if (!FeatureList::ParseEnableFeatureString( |
| enable_feature, &feature_name, &study, &group, &feature_params)) { |
| return false; |
| } |
| |
| parsed_enable_features.emplace_back(feature_name, study, group, |
| feature_params); |
| } |
| return true; |
| } |
| |
| // Escapes separators used by enable-features command line. |
| // E.g. Feature '<' Study '.' Group ':' param1 '/' value1 ',' |
| // ('*' is not a separator. No need to escape it.) |
| std::string EscapeValue(const std::string& value) { |
| std::string escaped_str; |
| for (const auto ch : value) { |
| if (ch == ',' || ch == '/' || ch == ':' || ch == '<' || ch == '.') { |
| escaped_str.append(base::StringPrintf("%%%02X", ch)); |
| } else { |
| escaped_str.append(1, ch); |
| } |
| } |
| return escaped_str; |
| } |
| |
| // Extracts a feature name from a feature state string. For example, given |
| // the input "*MyLovelyFeature<SomeFieldTrial", returns "MyLovelyFeature". |
| StringPiece GetFeatureName(StringPiece feature) { |
| StringPiece feature_name = feature; |
| |
| // Remove default info. |
| if (StartsWith(feature_name, "*")) |
| feature_name = feature_name.substr(1); |
| |
| // Remove field_trial info. |
| std::size_t index = feature_name.find("<"); |
| if (index != std::string::npos) |
| feature_name = feature_name.substr(0, index); |
| |
| return feature_name; |
| } |
| |
| // Features in |feature_vector| came from |merged_features| in |
| // OverrideFeatures() and contains linkage with field trial is case when they |
| // have parameters (with '<' simbol). In |feature_name| name is already cleared |
| // with GetFeatureName() and also could be without parameters. |
| bool ContainsFeature( |
| const std::vector<ScopedFeatureList::FeatureWithStudyGroup>& feature_vector, |
| StringPiece feature_name) { |
| return Contains(feature_vector, feature_name, |
| [](const ScopedFeatureList::FeatureWithStudyGroup& a) { |
| return a.feature_name; |
| }); |
| } |
| |
| // Merges previously-specified feature overrides with those passed into one of |
| // the Init() methods. |features| should be a list of features previously |
| // overridden to be in the |override_state|. |merged_features| should contain |
| // the enabled and disabled features passed into the Init() method, plus any |
| // overrides merged as a result of previous calls to this function. |
| void OverrideFeatures( |
| const std::vector<ScopedFeatureList::FeatureWithStudyGroup>& features_list, |
| FeatureList::OverrideState override_state, |
| ScopedFeatureList::Features* merged_features) { |
| for (const auto& feature : features_list) { |
| StringPiece feature_name = GetFeatureName(feature.feature_name); |
| |
| if (ContainsFeature(merged_features->enabled_feature_list, feature_name) || |
| ContainsFeature(merged_features->disabled_feature_list, feature_name)) { |
| continue; |
| } |
| |
| if (override_state == FeatureList::OverrideState::OVERRIDE_ENABLE_FEATURE) { |
| merged_features->enabled_feature_list.push_back(feature); |
| } else { |
| DCHECK_EQ(override_state, |
| FeatureList::OverrideState::OVERRIDE_DISABLE_FEATURE); |
| merged_features->disabled_feature_list.push_back(feature); |
| } |
| } |
| } |
| |
| // Merges previously-specified feature overrides with those passed into one of |
| // the Init() methods. |feature_list| should be a string whose format is the |
| // same as --enable-features or --disable-features command line flag, and |
| // specifies features overridden to be in the |override_state|. |
| // |merged_features| should contain the enabled and disabled features passed in |
| // to the Init() method, plus any overrides merged as a result of previous |
| // calls to this function. |
| void OverrideFeatures(const std::string& features_list, |
| FeatureList::OverrideState override_state, |
| ScopedFeatureList::Features* merged_features) { |
| std::vector<ScopedFeatureList::FeatureWithStudyGroup> parsed_features; |
| bool parse_enable_features_result = |
| ParseEnableFeatures(features_list, parsed_features); |
| DCHECK(parse_enable_features_result); |
| OverrideFeatures(parsed_features, override_state, merged_features); |
| } |
| |
| // Hex encode params so that special characters do not break formatting. |
| std::string HexEncodeString(const std::string& input) { |
| return HexEncode(input.data(), input.size()); |
| } |
| |
| // Inverse of HexEncodeString(). |
| std::string HexDecodeString(const std::string& input) { |
| if (input.empty()) |
| return std::string(); |
| std::string bytes; |
| bool result = HexStringToString(input, &bytes); |
| DCHECK(result); |
| return bytes; |
| } |
| |
| // Returns a command line string suitable to pass to |
| // FeatureList::InitializeFromCommandLine(). For example, |
| // {{"Feature1", "Study1", "Group1", "Param1/Value1/"}, {"Feature2"}} returns: |
| // - |enabled_feature|=true -> "Feature1<Study1.Group1:Param1/Value1/,Feature2" |
| // - |enabled_feature|=false -> "Feature1<Study1.Group1,Feature2" |
| std::string CreateCommandLineArgumentFromFeatureList( |
| const std::vector<ScopedFeatureList::FeatureWithStudyGroup>& feature_list, |
| bool enable_features) { |
| std::vector<std::string> features; |
| for (const auto& feature : feature_list) { |
| std::string feature_with_study_group = feature.feature_name; |
| if (feature.has_params() || !feature.study_name.empty()) { |
| feature_with_study_group += "<"; |
| feature_with_study_group += feature.StudyNameOrDefault(); |
| if (feature.has_params() || !feature.group_name.empty()) { |
| feature_with_study_group += "."; |
| feature_with_study_group += feature.GroupNameOrDefault(); |
| } |
| if (feature.has_params() && enable_features) { |
| feature_with_study_group += feature.ParamsForFeatureList(); |
| } |
| } |
| features.push_back(feature_with_study_group); |
| } |
| return JoinString(features, ","); |
| } |
| |
| } // namespace |
| |
| FeatureRefAndParams::FeatureRefAndParams(const Feature& feature, |
| const FieldTrialParams& params) |
| : feature(feature), params(params) {} |
| |
| FeatureRefAndParams::FeatureRefAndParams(const FeatureRefAndParams& other) = |
| default; |
| |
| FeatureRefAndParams::~FeatureRefAndParams() = default; |
| |
| ScopedFeatureList::ScopedFeatureList() = default; |
| |
| ScopedFeatureList::ScopedFeatureList(const Feature& enable_feature) { |
| InitAndEnableFeature(enable_feature); |
| } |
| |
| ScopedFeatureList::~ScopedFeatureList() { |
| Reset(); |
| } |
| |
| void ScopedFeatureList::Reset() { |
| // If one of the Init() functions was never called, don't reset anything. |
| if (!init_called_) |
| return; |
| |
| init_called_ = false; |
| |
| // ThreadPool tasks racily probing FeatureList while it's initialized/reset |
| // are problematic and while callers should ideally set up ScopedFeatureList |
| // before TaskEnvironment, that's not always possible. Fencing execution here |
| // avoids an entire class of bugs by making sure no ThreadPool task queries |
| // FeatureList while it's being modified. This local action is preferred to |
| // requiring all such callers to manually flush all tasks before each |
| // ScopedFeatureList Init/Reset: crbug.com/1275502#c45 |
| // |
| // All FeatureList modifications in this file should have this as well. |
| TaskEnvironment::ParallelExecutionFence fence( |
| "ScopedFeatureList must be Reset from the test main thread"); |
| |
| FeatureList::ClearInstanceForTesting(); |
| |
| if (field_trial_list_) { |
| field_trial_list_.reset(); |
| } |
| |
| // Restore params to how they were before. |
| FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting(); |
| if (!original_params_.empty()) { |
| // Before restoring params, we need to make all field trials in-active, |
| // because FieldTrialParamAssociator checks whether the given field trial |
| // is active or not, and associates no parameters if the trial is active. |
| // So temporarily restore field trial list to be nullptr. |
| FieldTrialList::RestoreInstanceForTesting(nullptr); |
| AssociateFieldTrialParamsFromString(original_params_, &HexDecodeString); |
| } |
| |
| if (original_field_trial_list_) { |
| FieldTrialList::RestoreInstanceForTesting(original_field_trial_list_); |
| original_field_trial_list_ = nullptr; |
| } |
| |
| if (original_feature_list_) |
| FeatureList::RestoreInstanceForTesting(std::move(original_feature_list_)); |
| } |
| |
| void ScopedFeatureList::Init() { |
| InitWithFeaturesImpl({}, {}, {}, /*keep_existing_states=*/true); |
| } |
| |
| void ScopedFeatureList::InitWithEmptyFeatureAndFieldTrialLists() { |
| InitWithFeaturesImpl({}, {}, {}, /*keep_existing_states=*/false); |
| } |
| |
| void ScopedFeatureList::InitWithNullFeatureAndFieldTrialLists() { |
| DCHECK(!init_called_); |
| |
| // Back up the current field trial parameters to be restored in Reset(). |
| original_params_ = FieldTrialList::AllParamsToString(&HexEncodeString); |
| |
| // Back up the current field trial list, to be restored in Reset(). |
| original_field_trial_list_ = FieldTrialList::BackupInstanceForTesting(); |
| |
| auto* field_trial_param_associator = FieldTrialParamAssociator::GetInstance(); |
| field_trial_param_associator->ClearAllParamsForTesting(); |
| field_trial_list_ = nullptr; |
| |
| DCHECK(!original_feature_list_); |
| |
| // Execution fence required while modifying FeatureList, as in Reset. |
| TaskEnvironment::ParallelExecutionFence fence( |
| "ScopedFeatureList must be Init from the test main thread"); |
| |
| // Back up the current feature list, to be restored in Reset(). |
| original_feature_list_ = FeatureList::ClearInstanceForTesting(); |
| init_called_ = true; |
| } |
| |
| void ScopedFeatureList::InitWithFeatureList( |
| std::unique_ptr<FeatureList> feature_list) { |
| DCHECK(!original_feature_list_); |
| |
| // Execution fence required while modifying FeatureList, as in Reset. |
| TaskEnvironment::ParallelExecutionFence fence( |
| "ScopedFeatureList must be Init from the test main thread"); |
| |
| original_feature_list_ = FeatureList::ClearInstanceForTesting(); |
| feature_list->SetCachingContextForTesting(++g_current_caching_context); |
| FeatureList::SetInstance(std::move(feature_list)); |
| init_called_ = true; |
| } |
| |
| void ScopedFeatureList::InitFromCommandLine( |
| const std::string& enable_features, |
| const std::string& disable_features) { |
| Features merged_features; |
| bool parse_enable_features_result = |
| ParseEnableFeatures(enable_features, |
| merged_features.enabled_feature_list) && |
| ParseEnableFeatures(disable_features, |
| merged_features.disabled_feature_list); |
| DCHECK(parse_enable_features_result); |
| return InitWithMergedFeatures(std::move(merged_features), |
| /*create_associated_field_trials=*/false, |
| /*keep_existing_states=*/true); |
| } |
| |
| void ScopedFeatureList::InitWithFeatures( |
| const std::vector<FeatureRef>& enabled_features, |
| const std::vector<FeatureRef>& disabled_features) { |
| InitWithFeaturesImpl(enabled_features, {}, disabled_features); |
| } |
| |
| void ScopedFeatureList::InitAndEnableFeature(const Feature& feature) { |
| InitWithFeaturesImpl({feature}, {}, {}); |
| } |
| |
| void ScopedFeatureList::InitAndDisableFeature(const Feature& feature) { |
| InitWithFeaturesImpl({}, {}, {feature}); |
| } |
| |
| void ScopedFeatureList::InitWithFeatureState(const Feature& feature, |
| bool enabled) { |
| if (enabled) { |
| InitAndEnableFeature(feature); |
| } else { |
| InitAndDisableFeature(feature); |
| } |
| } |
| |
| void ScopedFeatureList::InitWithFeaturesImpl( |
| const std::vector<FeatureRef>& enabled_features, |
| const std::vector<FeatureRefAndParams>& enabled_features_and_params, |
| const std::vector<FeatureRef>& disabled_features, |
| bool keep_existing_states) { |
| DCHECK(!init_called_); |
| DCHECK(enabled_features.empty() || enabled_features_and_params.empty()); |
| |
| Features merged_features; |
| bool create_associated_field_trials = false; |
| if (!enabled_features_and_params.empty()) { |
| for (const auto& feature : enabled_features_and_params) { |
| std::string trial_name = "scoped_feature_list_trial_for_"; |
| trial_name += feature.feature->name; |
| |
| // If features.params has 2 params whose values are value1 and value2, |
| // |params| will be "param1/value1/param2/value2/". |
| std::string params; |
| for (const auto& param : feature.params) { |
| // Add separator from previous param information if it exists. |
| if (!params.empty()) |
| params.append(1, '/'); |
| params.append(EscapeValue(param.first)); |
| params.append(1, '/'); |
| params.append(EscapeValue(param.second)); |
| } |
| |
| merged_features.enabled_feature_list.emplace_back( |
| feature.feature->name, trial_name, kTrialGroup, params); |
| } |
| create_associated_field_trials = true; |
| } else { |
| for (const auto& feature : enabled_features) |
| merged_features.enabled_feature_list.emplace_back(feature->name); |
| } |
| for (const auto& feature : disabled_features) |
| merged_features.disabled_feature_list.emplace_back(feature->name); |
| |
| InitWithMergedFeatures(std::move(merged_features), |
| create_associated_field_trials, keep_existing_states); |
| } |
| |
| void ScopedFeatureList::InitAndEnableFeatureWithParameters( |
| const Feature& feature, |
| const FieldTrialParams& feature_parameters) { |
| InitWithFeaturesAndParameters({{feature, feature_parameters}}, {}); |
| } |
| |
| void ScopedFeatureList::InitWithFeaturesAndParameters( |
| const std::vector<FeatureRefAndParams>& enabled_features, |
| const std::vector<FeatureRef>& disabled_features) { |
| InitWithFeaturesImpl({}, enabled_features, disabled_features); |
| } |
| |
| void ScopedFeatureList::InitWithMergedFeatures( |
| Features&& merged_features, |
| bool create_associated_field_trials, |
| bool keep_existing_states) { |
| DCHECK(!init_called_); |
| |
| std::string current_enabled_features; |
| std::string current_disabled_features; |
| const FeatureList* feature_list = FeatureList::GetInstance(); |
| if (feature_list && keep_existing_states) { |
| feature_list->GetFeatureOverrides(¤t_enabled_features, |
| ¤t_disabled_features); |
| } |
| |
| std::vector<FieldTrial::State> all_states = |
| FieldTrialList::GetAllFieldTrialStates(PassKey()); |
| original_params_ = FieldTrialList::AllParamsToString(&HexEncodeString); |
| |
| std::vector<ScopedFeatureList::FeatureWithStudyGroup> |
| parsed_current_enabled_features; |
| // Check relationship between current enabled features and field trials. |
| bool parse_enable_features_result = ParseEnableFeatures( |
| current_enabled_features, parsed_current_enabled_features); |
| DCHECK(parse_enable_features_result); |
| |
| // Back up the current field trial list, to be restored in Reset(). |
| original_field_trial_list_ = FieldTrialList::BackupInstanceForTesting(); |
| |
| // Create a field trial list, to which we'll add trials corresponding to the |
| // features that have params, before restoring the field trial state from the |
| // previous instance, further down in this function. |
| field_trial_list_ = std::make_unique<FieldTrialList>(); |
| |
| auto* field_trial_param_associator = FieldTrialParamAssociator::GetInstance(); |
| for (const auto& feature : merged_features.enabled_feature_list) { |
| // If we don't need to create any field trials for the |feature| (i.e. |
| // unless |create_associated_field_trials|=true or |feature| has any |
| // params), we can skip the code: EraseIf()...ClearParamsForTesting(). |
| if (!(create_associated_field_trials || feature.has_params())) |
| continue; |
| |
| // |all_states| contains the existing field trials, and is used to |
| // restore the field trials into a newly created field trial list with |
| // FieldTrialList::CreateTrialsFromFieldTrialStates(). |
| // However |all_states| may have a field trial that's being explicitly |
| // set through |merged_features.enabled_feature_list|. In this case, |
| // FieldTrialParamAssociator::AssociateFieldTrialParams() will fail. |
| // So remove such field trials from |all_states| here. |
| EraseIf(all_states, [feature](const auto& state) { |
| return state.trial_name == feature.StudyNameOrDefault(); |
| }); |
| |
| // If |create_associated_field_trials| is true, we want to match the |
| // behavior of VariationsFieldTrialCreator to always associate a field |
| // trial, even when there no params. Since |
| // FeatureList::InitializeFromCommandLine() doesn't associate a field trial |
| // when there are no params, we do it here. |
| if (!feature.has_params()) { |
| scoped_refptr<FieldTrial> field_trial_without_params = |
| FieldTrialList::CreateFieldTrial(feature.StudyNameOrDefault(), |
| feature.GroupNameOrDefault()); |
| DCHECK(field_trial_without_params); |
| } |
| |
| // Re-assigning field trial parameters is not allowed. Clear |
| // all field trial parameters. |
| field_trial_param_associator->ClearParamsForTesting( |
| feature.StudyNameOrDefault(), feature.GroupNameOrDefault()); |
| } |
| |
| if (keep_existing_states) { |
| // Restore other field trials. Note: We don't need to do anything for params |
| // here because the param associator already has the right state for these |
| // restored trials, which has been backed up via |original_params_| to be |
| // restored later. |
| FieldTrialList::CreateTrialsFromFieldTrialStates(PassKey(), all_states); |
| } else { |
| // No need to keep existing field trials. Instead, clear all parameters. |
| field_trial_param_associator->ClearAllParamsForTesting(); |
| } |
| |
| // Create enable-features and disable-features arguments. |
| OverrideFeatures(parsed_current_enabled_features, |
| FeatureList::OverrideState::OVERRIDE_ENABLE_FEATURE, |
| &merged_features); |
| OverrideFeatures(current_disabled_features, |
| FeatureList::OverrideState::OVERRIDE_DISABLE_FEATURE, |
| &merged_features); |
| |
| std::string enabled = CreateCommandLineArgumentFromFeatureList( |
| merged_features.enabled_feature_list, /*enable_features=*/true); |
| std::string disabled = CreateCommandLineArgumentFromFeatureList( |
| merged_features.disabled_feature_list, /*enable_features=*/false); |
| |
| std::unique_ptr<FeatureList> new_feature_list(new FeatureList); |
| new_feature_list->InitializeFromCommandLine(enabled, disabled); |
| InitWithFeatureList(std::move(new_feature_list)); |
| } |
| |
| } // namespace test |
| } // namespace base |