blob: 88c58f1614e2c4bdae1b636631ab408f3e76157a [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/variations/variations_test_utils.h"
#include "base/base64.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/time/time.h"
#include "components/metrics/clean_exit_beacon.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/variations/active_field_trials.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/field_trial_config/fieldtrial_testing_config.h"
#include "components/variations/hashing.h"
#include "components/variations/pref_names.h"
#include "components/variations/proto/client_variations.pb.h"
#include "components/variations/synthetic_trial_registry.h"
#include "components/variations/variations_associated_data.h"
#include "components/variations/variations_switches.h"
#include "third_party/zlib/google/compression_utils.h"
namespace variations {
namespace {
// kTestSeed is a simple VariationsSeed containing:
// serial_number: "test"
// study: {
// name: "UMA-Uniformity-Trial-50-Percent"
// consistency: PERMANENT
// experiment: {
// name: "default"
// probability_weight: 1
// }
// experiment: {
// name: "group_01"
// probability_weight: 1
// }
// }
const char* kTestSeed_StudyNames[] = {"UMA-Uniformity-Trial-50-Percent"};
const char kTestSeed_Base64UncompressedData[] =
"CgR0ZXN0Ej4KH1VNQS1Vbmlmb3JtaXR5LVRyaWFsLTUwLVBlcmNlbnQ4AUoLCgdkZWZhdWx0EA"
"FKDAoIZ3JvdXBfMDEQAQ==";
const char kTestSeed_Base64CompressedData[] =
"H4sIAAAAAAAA/+JiKUktLhGy45IP9XXUDc3LTMsvys0sqdQNKcpMzNE1NdANSC1KTs0rsWD04u"
"ZiT0lNSyzNKRFg9OLh4kgvyi8tiDcwFGAEBAAA//90/JgERgAAAA==";
// The compressed data is the result of decoding the base64 encoded compressed
// data above and showing the data as hex:
// echo -n base64_compressed_data | base64 -d | hexdump -e '8 1 ", 0x%x"'
const uint8_t kTestSeed_CompressedData[] = {
0x1f, 0x8b, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xe2, 0x62,
0x29, 0x49, 0x2d, 0x2e, 0x11, 0xb2, 0xe3, 0x92, 0xf, 0xf5, 0x75, 0xd4,
0xd, 0xcd, 0xcb, 0x4c, 0xcb, 0x2f, 0xca, 0xcd, 0x2c, 0xa9, 0xd4, 0xd,
0x29, 0xca, 0x4c, 0xcc, 0xd1, 0x35, 0x35, 0xd0, 0xd, 0x48, 0x2d, 0x4a,
0x4e, 0xcd, 0x2b, 0xb1, 0x60, 0xf4, 0xe2, 0xe6, 0x62, 0x4f, 0x49, 0x4d,
0x4b, 0x2c, 0xcd, 0x29, 0x11, 0x60, 0xf4, 0xe2, 0xe1, 0xe2, 0x48, 0x2f,
0xca, 0x2f, 0x2d, 0x88, 0x37, 0x30, 0x14, 0x60, 0x4, 0x4, 0x0, 0x0,
0xff, 0xff, 0x74, 0xfc, 0x98, 0x4, 0x46, 0x0, 0x0, 0x0};
const char kTestSeed_Base64Signature[] =
"MEUCIQD5AEAzk5qEuE3xOZl+xSZR15Ac1RJpsXMiou7i5W0sMAIgRn++ngh03HaMGC+Pjl9NOu"
"Doxf83qsSwycF2PSS1nYQ=";
const char* kCrashingSeed_StudyNames[] = {"CrashingStudy"};
// kCrashingSeed is a VariationsSeed that triggers a crash for testing:
// serial_number: "35ed2d9e354b414befdf930a734094019c0162f1"
// study: {
// name: "CrashingStudy"
// consistency: PERMANENT
// experiment: {
// name: "EnabledLaunch"
// probability_weight: 100
// feature_association: {
// enable_feature: "ForceFieldTrialSetupCrashForTesting"
// }
// }
// experiment: {
// name: "ForcedOn_ForceFieldTrialSetupCrashForTesting"
// probability_weight: 0
// feature_association: {
// forcing_feature_on: "ForceFieldTrialSetupCrashForTesting"
// }
// }
// experiment: {
// name: "ForcedOff_ForceFieldTrialSetupCrashForTesting"
// probability_weight: 0
// feature_association: {
// forcing_feature_off: "ForceFieldTrialSetupCrashForTesting"
// }
// }
// filter: {
// min_version: "91.*"
// channel: CANARY
// channel: DEV
// channel: BETA
// channel: STABLE
// platform: PLATFORM_ANDROID
// platform: PLATFORM_IOS
// platform: PLATFORM_ANDROID_WEBVIEW
// platform: PLATFORM_WINDOWS
// platform: PLATFORM_MAC
// platform: PLATFORM_LINUX
// platform: PLATFORM_CHROMEOS
// platform: PLATFORM_CHROMEOS_LACROS
// }
// }
// version: "hash/4aa56a1dc30dfc767615248d6fee29830198b276"
const char kCrashingSeed_Base64UncompressedData[] =
"CigzNWVkMmQ5ZTM1NGI0MTRiZWZkZjkzMGE3MzQwOTQwMTljMDE2MmYxEp4CCg1DcmFzaGluZ1"
"N0dWR5OAFKOAoNRW5hYmxlZExhdW5jaBBkYiUKI0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jhc2hG"
"b3JUZXN0aW5nSlcKLEZvcmNlZE9uX0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jhc2hGb3JUZXN0aW"
"5nEABiJRojRm9yY2VGaWVsZFRyaWFsU2V0dXBDcmFzaEZvclRlc3RpbmdKWAotRm9yY2VkT2Zm"
"X0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jhc2hGb3JUZXN0aW5nEABiJSIjRm9yY2VGaWVsZFRyaW"
"FsU2V0dXBDcmFzaEZvclRlc3RpbmdSHhIEOTEuKiAAIAEgAiADKAQoBSgGKAAoASgCKAMoCSIt"
"aGFzaC80YWE1NmExZGMzMGRmYzc2NzYxNTI0OGQ2ZmVlMjk4MzAxOThiMjc2";
const char kCrashingSeed_Base64CompressedData[] =
"H4sIAAAAAAAAAI3QwUvDMBTH8babwgKDsaMHKZNBEKdJk6bJWbbDEAQ30JskeS+2UKp07cF/"
"Zn+rZfgH9Py+73P4ESpyhAwMilw6yaXDAMEIZgshmZGMG8+4ygJfnhMyf27tqayar0PXw6+"
"O95rMt411NcKL7RtfLsCtyd3uu/W4q7CGY1vZ+oBd/"
"3P5HA5HPHUDsH8nD5cMXpvPEf0icuubUfAH2fzDIYyVV2Pkt9vl1PDH+zRK4zRJJ3RKr+"
"g1jWhMEzqhs9WmHPonaW2uLAcvGARfqELxPJMaVEDMjBbDotplhfoDs9NLbnoBAAA=";
// The compressed data is the result of decoding the base64 encoded compressed
// data above and showing the data as hex:
// echo -n base64_compressed_data | base64 -d | hexdump -e '8 1 ", 0x%x"'
const uint8_t kCrashingSeed_CompressedData[] = {
0x1f, 0x8b, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8d, 0xd0,
0xc1, 0x4b, 0xc3, 0x30, 0x14, 0xc7, 0xf1, 0xb6, 0x9b, 0xc2, 0x2, 0x83,
0xb1, 0xa3, 0x7, 0x29, 0x93, 0x41, 0x10, 0xa7, 0x49, 0x93, 0xa6, 0xc9,
0x59, 0xb6, 0xc3, 0x10, 0x4, 0x37, 0xd0, 0x9b, 0x24, 0x79, 0x2f, 0xb6,
0x50, 0xaa, 0x74, 0xed, 0xc1, 0x7f, 0x66, 0x7f, 0xab, 0x65, 0xf8, 0x7,
0xf4, 0xfc, 0xbe, 0xef, 0x73, 0xf8, 0x11, 0x2a, 0x72, 0x84, 0xc, 0xc,
0x8a, 0x5c, 0x3a, 0xc9, 0xa5, 0xc3, 0x0, 0xc1, 0x8, 0x66, 0xb, 0x21,
0x99, 0x91, 0x8c, 0x1b, 0xcf, 0xb8, 0xca, 0x2, 0x5f, 0x9e, 0x13, 0x32,
0x7f, 0x6e, 0xed, 0xa9, 0xac, 0x9a, 0xaf, 0x43, 0xd7, 0xc3, 0xaf, 0x8e,
0xf7, 0x9a, 0xcc, 0xb7, 0x8d, 0x75, 0x35, 0xc2, 0x8b, 0xed, 0x1b, 0x5f,
0x2e, 0xc0, 0xad, 0xc9, 0xdd, 0xee, 0xbb, 0xf5, 0xb8, 0xab, 0xb0, 0x86,
0x63, 0x5b, 0xd9, 0xfa, 0x80, 0x5d, 0xff, 0x73, 0xf9, 0x1c, 0xe, 0x47,
0x3c, 0x75, 0x3, 0xb0, 0x7f, 0x27, 0xf, 0x97, 0xc, 0x5e, 0x9b, 0xcf,
0x11, 0xfd, 0x22, 0x72, 0xeb, 0x9b, 0x51, 0xf0, 0x7, 0xd9, 0xfc, 0xc3,
0x21, 0x8c, 0x95, 0x57, 0x63, 0xe4, 0xb7, 0xdb, 0xe5, 0xd4, 0xf0, 0xc7,
0xfb, 0x34, 0x4a, 0xe3, 0x34, 0x49, 0x27, 0x74, 0x4a, 0xaf, 0xe8, 0x35,
0x8d, 0x68, 0x4c, 0x13, 0x3a, 0xa1, 0xb3, 0xd5, 0xa6, 0x1c, 0xfa, 0x27,
0x69, 0x6d, 0xae, 0x2c, 0x7, 0x2f, 0x18, 0x4, 0x5f, 0xa8, 0x42, 0xf1,
0x3c, 0x93, 0x1a, 0x54, 0x40, 0xcc, 0x8c, 0x16, 0xc3, 0xa2, 0xda, 0x65,
0x85, 0xfa, 0x3, 0xb3, 0xd3, 0x4b, 0x6e, 0x7a, 0x1, 0x0, 0x0};
const char kCrashingSeed_Base64Signature[] =
"MEQCIEn1+VsBfNA93dxzpk+BLhdO91kMQnofxfTK5Uo8vDi8AiAnTCFCIPgEGWNOKzuKfNWn6"
"emB6pnGWjSTbI/pvfxHnw==";
// Create mock testing config equivalent to:
// {
// "UnitTest": [
// {
// "platforms": [
// "android",
// "android_weblayer",
// "android_webview",
// "chromeos",
// "chromeos_lacros",
// "fuchsia",
// "ios",
// "linux",
// "mac",
// "windows"
// ],
// "experiments": [
// {
// "name": "Enabled",
// "params": {
// "x": "1"
// },
// "enable_features": [
// "UnitTestEnabled"
// ]
// }
// ]
// }
// ]
// }
const Study::Platform array_kFieldTrialConfig_platforms_0[] = {
Study::PLATFORM_ANDROID,
Study::PLATFORM_ANDROID_WEBLAYER,
Study::PLATFORM_ANDROID_WEBVIEW,
Study::PLATFORM_CHROMEOS,
Study::PLATFORM_CHROMEOS_LACROS,
Study::PLATFORM_FUCHSIA,
Study::PLATFORM_IOS,
Study::PLATFORM_LINUX,
Study::PLATFORM_MAC,
Study::PLATFORM_WINDOWS,
};
const char* enable_features_0[] = {"UnitTestEnabled"};
const FieldTrialTestingExperimentParams array_kFieldTrialConfig_params_0[] = {
{
"x",
"1",
},
};
const FieldTrialTestingExperiment array_kFieldTrialConfig_experiments_0[] = {
{/*name=*/"Enabled",
/*platforms=*/array_kFieldTrialConfig_platforms_0,
/*form_factors=*/{},
/*is_low_end_device=*/std::nullopt,
/*min_os_version=*/nullptr,
/*params=*/array_kFieldTrialConfig_params_0,
/*enable_features=*/enable_features_0,
/*disable_features=*/{},
/*forcing_flag=*/nullptr,
/*override_ui_string=*/{}},
};
const FieldTrialTestingStudy array_kFieldTrialConfig_studies[] = {
{/*name=*/"UnitTest",
/*experiments=*/array_kFieldTrialConfig_experiments_0},
};
} // namespace
const SignedSeedData kTestSeedData{
kTestSeed_StudyNames, kTestSeed_Base64UncompressedData,
kTestSeed_Base64CompressedData, kTestSeed_Base64Signature,
kTestSeed_CompressedData, sizeof(kTestSeed_CompressedData)};
const SignedSeedData kCrashingSeedData{
kCrashingSeed_StudyNames, kCrashingSeed_Base64UncompressedData,
kCrashingSeed_Base64CompressedData, kCrashingSeed_Base64Signature,
kCrashingSeed_CompressedData, sizeof(kCrashingSeed_CompressedData)};
const SignedSeedPrefKeys kSafeSeedPrefKeys{prefs::kVariationsSafeCompressedSeed,
prefs::kVariationsSafeSeedSignature};
const SignedSeedPrefKeys kRegularSeedPrefKeys{prefs::kVariationsCompressedSeed,
prefs::kVariationsSeedSignature};
SignedSeedData::SignedSeedData(base::span<const char*> in_study_names,
const char* in_base64_uncompressed_data,
const char* in_base64_compressed_data,
const char* in_base64_signature,
const uint8_t* in_compressed_data,
size_t in_compressed_data_size)
: study_names(std::move(in_study_names)),
base64_uncompressed_data(in_base64_uncompressed_data),
base64_compressed_data(in_base64_compressed_data),
base64_signature(in_base64_signature),
compressed_data(in_compressed_data),
compressed_data_size(in_compressed_data_size) {}
SignedSeedData::~SignedSeedData() = default;
SignedSeedData::SignedSeedData(const SignedSeedData&) = default;
SignedSeedData::SignedSeedData(SignedSeedData&&) = default;
SignedSeedData& SignedSeedData::operator=(const SignedSeedData&) = default;
SignedSeedData& SignedSeedData::operator=(SignedSeedData&&) = default;
std::string_view SignedSeedData::GetCompressedData() const {
return std::string_view(reinterpret_cast<const char*>(compressed_data),
compressed_data_size);
}
void DisableTestingConfig() {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kDisableFieldTrialTestingConfig);
}
void EnableTestingConfig() {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kEnableFieldTrialTestingConfig);
}
bool ExtractVariationIds(const std::string& variations,
std::set<VariationID>* variation_ids,
std::set<VariationID>* trigger_ids) {
std::string serialized_proto;
if (!base::Base64Decode(variations, &serialized_proto)) {
return false;
}
ClientVariations proto;
if (!proto.ParseFromString(serialized_proto)) {
return false;
}
for (int i = 0; i < proto.variation_id_size(); ++i) {
variation_ids->insert(proto.variation_id(i));
}
for (int i = 0; i < proto.trigger_variation_id_size(); ++i) {
trigger_ids->insert(proto.trigger_variation_id(i));
}
return true;
}
scoped_refptr<base::FieldTrial> CreateTrialAndAssociateId(
const std::string& trial_name,
const std::string& default_group_name,
IDCollectionKey key,
VariationID id) {
AssociateGoogleVariationID(key, trial_name, default_group_name, id);
scoped_refptr<base::FieldTrial> trial(
base::FieldTrialList::CreateFieldTrial(trial_name, default_group_name));
DCHECK(trial);
if (trial) {
// Ensure the trial is registered under the correct key so we can look it
// up.
trial->Activate();
}
return trial;
}
void SimulateCrash(PrefService* local_state) {
local_state->SetBoolean(metrics::prefs::kStabilityExitedCleanly, false);
metrics::CleanExitBeacon::SkipCleanShutdownStepsForTesting();
}
void WriteSeedData(PrefService* local_state,
const SignedSeedData& seed_data,
const SignedSeedPrefKeys& pref_keys) {
local_state->SetString(pref_keys.base64_compressed_data_key,
seed_data.base64_compressed_data);
local_state->SetString(pref_keys.base64_signature_key,
seed_data.base64_signature);
local_state->CommitPendingWrite();
}
bool FieldTrialListHasAllStudiesFrom(const SignedSeedData& seed_data) {
return std::ranges::all_of(seed_data.study_names, [](const char* study) {
return base::FieldTrialList::TrialExists(study);
});
}
void ResetVariations() {
testing::ClearAllVariationIDs();
testing::ClearAllVariationParams();
}
const FieldTrialTestingConfig kTestingConfig = {
array_kFieldTrialConfig_studies};
std::unique_ptr<ClientFilterableState> CreateDummyClientFilterableState() {
auto client_state = std::make_unique<ClientFilterableState>(
base::BindOnce([] { return false; }),
base::BindOnce([] { return base::flat_set<uint64_t>(); }));
client_state->locale = "en-CA";
client_state->reference_date = base::Time::Now();
client_state->version = base::Version("20.0.0.0");
client_state->channel = Study::STABLE;
client_state->form_factor = Study::PHONE;
client_state->platform = Study::PLATFORM_ANDROID;
return client_state;
}
// Constructs mocked EntropyProviders.
MockEntropyProviders::MockEntropyProviders(
MockEntropyProviders::Results results,
uint32_t low_entropy_domain)
: EntropyProviders(results.high_entropy.has_value() ? "client_id" : "",
{0, low_entropy_domain},
results.limited_entropy.has_value()
? "limited_entropy_randomization_source"
: std::string_view()),
low_provider_(results.low_entropy),
high_provider_(results.high_entropy.value_or(0)),
limited_provider_(results.limited_entropy.value_or(0)) {}
MockEntropyProviders::~MockEntropyProviders() = default;
const base::FieldTrial::EntropyProvider& MockEntropyProviders::low_entropy()
const {
return low_provider_;
}
const base::FieldTrial::EntropyProvider& MockEntropyProviders::default_entropy()
const {
if (default_entropy_is_high_entropy()) {
return high_provider_;
}
return low_provider_;
}
const base::FieldTrial::EntropyProvider& MockEntropyProviders::limited_entropy()
const {
CHECK(has_limited_entropy());
return limited_provider_;
}
std::string GZipAndB64EncodeToHexString(const VariationsSeed& seed) {
auto serialized = seed.SerializeAsString();
std::string compressed;
compression::GzipCompress(serialized, &compressed);
return base::Base64Encode(compressed);
}
bool ContainsTrialName(const std::vector<ActiveGroupId>& active_group_ids,
std::string_view trial_name) {
auto hashed_name = HashName(trial_name);
for (const auto& trial : active_group_ids) {
if (trial.name == hashed_name) {
return true;
}
}
return false;
}
bool ContainsTrialAndGroupName(
const std::vector<ActiveGroupId>& active_group_ids,
std::string_view trial_name,
std::string_view group_name) {
auto hashed_trial_name = HashName(trial_name);
auto hashed_group_name = HashName(group_name);
for (const auto& trial : active_group_ids) {
if (trial.name == hashed_trial_name && trial.group == hashed_group_name) {
return true;
}
}
return false;
}
void SetUpSeedFileTrial(std::string group_name) {
if (group_name.empty()) {
return;
}
base::MockEntropyProvider entropy_provider(0.9);
scoped_refptr<base::FieldTrial> trial(
base::FieldTrialList::FactoryGetFieldTrial(
kSeedFileTrial, /*total_probability=*/100, kDefaultGroup,
entropy_provider));
trial->AppendGroup(group_name, /*group_probability=*/100);
trial->SetForced();
}
} // namespace variations