blob: 5189520b73b0a576b2d4e3b7a7c0f6be7e004a5b [file] [log] [blame]
// 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/containers/flat_map.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::InitFromCommandLine(). 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::InitWithFeatureStates(
const flat_map<FeatureRef, bool>& feature_states) {
std::vector<FeatureRef> enabled_features, disabled_features;
for (const auto& [feature, enabled] : feature_states) {
if (enabled) {
enabled_features.push_back(feature);
} else {
disabled_features.push_back(feature);
}
}
InitWithFeaturesImpl(enabled_features, {}, disabled_features);
}
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(&current_enabled_features,
&current_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::InitFromCommandLine() 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->InitFromCommandLine(enabled, disabled);
InitWithFeatureList(std::move(new_feature_list));
}
} // namespace test
} // namespace base