blob: f8bb7b0b43ca415f5488a2689f3902df926df6b9 [file] [log] [blame]
// Copyright 2024 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/service/limited_entropy_randomization.h"
#include <cstdint>
#include <utility>
#include <vector>
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/version_info/version_info.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/entropy_provider.h"
#include "components/variations/proto/layer.pb.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/variations_layers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace variations {
namespace {
constexpr int kTestLayerId = 101;
constexpr int kTestLayerMemberId = 201;
constexpr int kLowEntropyLayerId = 301;
constexpr int kLimitedEntropyLayerId = 401;
// The entropy limits, in bits, to use for various tests.
constexpr double kEntropyLimit_10bits = 10.0;
constexpr double kEntropyLimit_2bits = 2.0;
// Hard code the enum bucket values, and use these in tests so that any
// renumbering can be detected.
// LimitedEntropySeedRejectionReason::kHighEntropyUsage
constexpr int kHighEntropyUsageBucket = 0;
// LimitedEntropySeedRejectionReason::kMoreThenOneLimitedLayer
constexpr int kMoreThenOneLimitedLayerBucket = 1;
// LimitedEntropySeedRejectionReason::kLayerHasInvalidSlotBounds
constexpr int kLayerHasInvalidSlotBoundsBucket = 2;
// LimitedEntropySeedRejectionReason::kLayerDoesNotContainSlots
constexpr int kLayerDoesNotContainSlotsBucket = 3;
// LimitedEntropySeedRejectionReason::kInvalidId
constexpr int kInvalidLayerIdBucket = 4;
// LimitedEntropySeedRejectionReason::kDuplicatedLayerId
constexpr int kDuplicatedLayerIdBucket = 5;
// LimitedEntropySeedRejectionReason::kInvalidLayerReference
constexpr int kInvalidLayerReferenceBucket = 6;
// LimitedEntropySeedRejectionReason::kDanglingLayerReference
constexpr int kDanglingLayerReferenceBucket = 7;
// LimitedEntropySeedRejectionReason::kDanglingLayerMemberReference
constexpr int kDanglingLayerMemberReferenceBucket = 8;
// LimitedEntropySeedRejectionReason::kEmptyLayerReference
constexpr int kEmptyLayerReferenceBucket = 9;
// LimitedEntropySeedRejectionReason::kInvalidLayerConfiguration
[[maybe_unused]] constexpr int kInvalidLayerConfigurationBucket = 10;
// LimitedEntropySeedRejectionReason::kActiveLowAndLimitedLayers
constexpr int kActiveLowAndLimitedLayersBucket = 11;
Study::Experiment CreateExperiment(int weight) {
Study::Experiment experiment;
experiment.set_probability_weight(weight);
return experiment;
}
Study::Experiment CreateGoogleWebExperiment(int weight,
int google_web_experiment_id) {
auto experiment = CreateExperiment(weight);
experiment.set_google_web_experiment_id(google_web_experiment_id);
return experiment;
}
std::vector<Study::Experiment> CreateExperimentsWithTwoBitsOfEntropy() {
// Create 3 experiments with a total probability weight of 100. Only the 25%
// group has a google_web_experiment_id so the entropy used should be
// -log2(1/4) = 2.
std::vector<Study::Experiment> experiments = {
CreateExperiment(10), CreateGoogleWebExperiment(25, 100001),
CreateExperiment(65)};
return experiments;
}
LayerMemberReference CreateLayerMemberReference(
uint32_t layer_id,
const std::vector<uint32_t>& layer_member_ids) {
LayerMemberReference layer_member_reference;
layer_member_reference.set_layer_id(layer_id);
for (uint32_t layer_member_id : layer_member_ids) {
layer_member_reference.add_layer_member_ids(layer_member_id);
}
return layer_member_reference;
}
// Creates a test study.
Study CreateTestStudy(const std::vector<Study::Experiment>& experiments) {
Study study;
study.set_name("test_study");
study.set_consistency(Study::PERMANENT);
std::vector<Study::Experiment> copied_experiments(experiments);
for (size_t i = 0; i < copied_experiments.size(); ++i) {
copied_experiments[i].set_name(
base::StrCat({"test_experiment_", base::NumberToString(i)}));
Study_Experiment* experiment_to_add = study.add_experiment();
experiment_to_add->MergeFrom(copied_experiments[i]);
}
// Add all platforms to the study filter.
auto* platforms = study.mutable_filter()->mutable_platform();
for (int p = static_cast<int>(Study_Platform_Platform_MIN);
p <= static_cast<int>(Study_Platform_Platform_MAX); ++p) {
platforms->Add(static_cast<Study_Platform>(p));
}
return study;
}
Study CreateTestStudy(const std::vector<Study::Experiment>& experiments,
const LayerMemberReference& layer_member_reference) {
auto study = CreateTestStudy(experiments);
auto* layer_member_reference_to_add = study.mutable_layer();
layer_member_reference_to_add->MergeFrom(layer_member_reference);
return study;
}
// Creates a test layer member with slot ranges given as <start, end> pairs.
Layer::LayerMember CreateLayerMember(
int layer_member_id,
const std::vector<std::pair<int, int>>& slot_ranges) {
Layer::LayerMember layer_member;
layer_member.set_id(layer_member_id);
for (const auto& slot_range : slot_ranges) {
auto* slot = layer_member.add_slots();
slot->set_start(slot_range.first);
slot->set_end(slot_range.second);
}
return layer_member;
}
Layer CreateLayer(int layer_id,
int num_slots,
Layer::EntropyMode entropy_mode,
const std::vector<Layer::LayerMember>& layer_members) {
Layer layer;
layer.set_id(layer_id);
layer.set_num_slots(num_slots);
layer.set_entropy_mode(entropy_mode);
for (const auto& layer_member : layer_members) {
auto* member_to_add = layer.add_members();
member_to_add->MergeFrom(layer_member);
}
return layer;
}
VariationsSeed CreateTestSeed(const std::vector<Layer>& layers,
const std::vector<Study>& studies) {
VariationsSeed seed;
for (const auto& study : studies) {
auto* study_to_add = seed.add_study();
study_to_add->MergeFrom(study);
}
for (const auto& layer : layers) {
auto* layer_to_add = seed.add_layers();
layer_to_add->MergeFrom(layer);
}
return seed;
}
} // namespace
class LimitedEntropyRandomizationTest : public ::testing::Test {
public:
LimitedEntropyRandomizationTest()
: client_state_(
/*is_enterprise_function=*/base::BindOnce([] { return false; }),
/*google_groups_function=*/base::BindOnce(
[] { return base::flat_set<uint64_t>(); })) {
client_state_.platform = ClientFilterableState::GetCurrentPlatform();
client_state_.channel = Study::STABLE;
client_state_.version = version_info::GetVersion();
}
protected:
base::HistogramTester histogram_tester_;
ClientFilterableState client_state_;
};
TEST_F(LimitedEntropyRandomizationTest,
ValidConfiguration_WithValidEntropyUse) {
std::vector<Layer> test_layers;
for (int i = 1; i <= 4; ++i) {
test_layers.push_back(CreateLayer(
/*layer_id=*/i, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
}
auto test_seed = CreateTestSeed(
test_layers,
{CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(2, {kTestLayerMemberId})),
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(2, {kTestLayerMemberId}))});
const MisconfiguredEntropyResult result = SeedHasMisconfiguredEntropy(
client_state_, test_seed, kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
EXPECT_TRUE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
ValidConfiguration_UseDeprecatedLayerMemberIdField) {
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::DEFAULT,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
LayerMemberReference layer_member_reference;
layer_member_reference.set_layer_id(kTestLayerId);
layer_member_reference.set_layer_member_id(kTestLayerMemberId);
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
layer_member_reference)});
EXPECT_FALSE(SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits)
.is_misconfigured);
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
ValidConfiguration_NoLimitedLayerReferences) {
// Creates a layer with DEFAULT entropy mode. It would have taken 1 bit of
// entropy if it is using LIMITED entropy mode.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::DEFAULT,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId, {kTestLayerMemberId}))});
// Seed should not be rejected since it's not using LIMITED entropy mode.
const MisconfiguredEntropyResult result = SeedHasMisconfiguredEntropy(
client_state_, test_seed, kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
EXPECT_FALSE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
// This is exactly the same test as SeedRejection_EntropyOveruse, below, except
// that the study has session consistency so it does not consume entropy.
TEST_F(LimitedEntropyRandomizationTest, SessionConsistency) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_study = CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kTestLayerId, {kTestLayerMemberId}));
test_study.set_consistency(Study::SESSION);
auto test_seed = CreateTestSeed({test_layer}, {test_study});
// Seed should not be rejected since the study is session consistency, which
// does not consume entropy.
EXPECT_FALSE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_EntropyOveruse) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId, {kTestLayerMemberId}))});
// The total entropy used should be 3 bits which is over the limit.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kHighEntropyUsageBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_InvalidLayerId) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/0, // Zero is not a valid layer id.
/*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
/*layers=*/{test_layer},
/*studies=*/{});
// Rejected because of invalid layer id.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kInvalidLayerIdBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_DuplicatedLayerId) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer, test_layer}, // Add same layer twice to test duplicated id.
{CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kTestLayerId, {kTestLayerMemberId}))});
// Rejected because of duplicated layer id.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kDuplicatedLayerIdBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_InvalidLayerReference) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer},
{CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(0, // Invalid layer id.
{kTestLayerMemberId}))});
// Rejected because of duplicated layer id.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kInvalidLayerReferenceBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_EmptyLayerReference) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId,
/*layer_member_ids=*/{}))}); // Empty.
// Rejected because of empty layer member reference.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kEmptyLayerReferenceBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_DanglingLayerReference) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.f
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer},
{CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId + 1, // Layer id + 1 is not defined above.
{kTestLayerMemberId}))});
// Rejected because of dangling layer member reference.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kDanglingLayerReferenceBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_DanglingLayerMemberReference) {
// Creates a layer with LIMITED entropy mode that takes 1 bit of entropy from
// the layer member.f
auto test_layer = CreateLayer(
/*layer_id=*/kTestLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})});
auto test_seed = CreateTestSeed(
{test_layer},
{CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId, {kTestLayerMemberId +
1}))}); // Layer member id + 1 is not defined.
// Rejected because of dangling layer member reference.
EXPECT_TRUE(
SeedHasMisconfiguredEntropy(client_state_, test_seed, kEntropyLimit_2bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kDanglingLayerMemberReferenceBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_MultipleActiveReferencedLimitedLayer) {
std::vector<Layer> test_layers;
for (int i = 1; i <= 4; ++i) {
test_layers.push_back(CreateLayer(
/*layer_id=*/i, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
}
auto test_seed = CreateTestSeed(
test_layers,
{CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(2, {kTestLayerMemberId})),
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(3, {kTestLayerMemberId}))});
EXPECT_TRUE(SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kMoreThenOneLimitedLayerBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_MultipleDisjointReferencedLimitedLayer) {
std::vector<Layer> test_layers;
for (int i = 1; i <= 4; ++i) {
test_layers.push_back(CreateLayer(
/*layer_id=*/i, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
}
// Find the current version value used by this binary, and increment its
// last component. This way we can have study1 end at the current version,
// and study2 start one incremented version later. That way, the studies have
// disjoint client populations.
//
// Example versions:
//
// * The client is running version 140.0.1111.0
// * study1 has max_version 140.0.1111.3
// * study2 has min_version 140.0.1111.4
auto study1_max_version = client_state_.version;
auto version_components = client_state_.version.components();
ASSERT_FALSE(version_components.empty());
version_components.back() += 1;
auto study2_min_version = base::Version(version_components);
auto study1 =
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(2, // Not the same as study2.
{kTestLayerMemberId}));
study1.mutable_filter()->set_max_version(study1_max_version.GetString());
auto study2 =
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(3, // Not the same as study1.
{kTestLayerMemberId}));
study2.mutable_filter()->set_min_version(study2_min_version.GetString());
auto test_seed = CreateTestSeed(test_layers, {study1, study2});
EXPECT_FALSE(SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits)
.is_misconfigured);
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_InvalidSlotBounds) {
// A test layer with overlapping layer members.
auto test_layer =
CreateLayer(kTestLayerId, /*num_slots=*/100, Layer::LIMITED,
{CreateLayerMember(kTestLayerMemberId, {{0, 49}, {25, 49}})});
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId, {kTestLayerMemberId}))});
// Seed should be rejected since the actively referenced LIMITED layer is
// invalid.
EXPECT_TRUE(SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kLayerHasInvalidSlotBoundsBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest, SeedRejection_NoSlots) {
// A test layer with no slots.
auto test_layer = CreateLayer(kTestLayerId, /*num_slots=*/0, Layer::LIMITED,
{CreateLayerMember(kTestLayerMemberId, {})});
auto test_seed = CreateTestSeed(
{test_layer}, {CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(
kTestLayerId, {kTestLayerMemberId}))});
// Seed should be rejected since the LIMITED layer is invalid.
EXPECT_TRUE(SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits)
.is_misconfigured);
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kLayerDoesNotContainSlotsBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_SimultaneousLowAndLimitedLayers) {
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create an entropy consuming study that refers to the LOW entropy layer.
test_studies.push_back(CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId})));
// Create an entropy consuming study that refers to the LIMITED entropy layer.
test_studies.push_back(
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId})));
// The seed is rejected because both layers would be active and simultaneously
// active LOW and LIMITED layers are not allowed.
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_TRUE(result.is_misconfigured);
EXPECT_FALSE(result.seed_has_active_low_layer.has_value());
EXPECT_FALSE(result.seed_has_active_limited_layer.has_value());
histogram_tester_.ExpectUniqueSample(kSeedRejectionReasonHistogram,
kActiveLowAndLimitedLayersBucket, 1);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_AllowNonActiveSimultaneousLowAndLimitedLayers_LOW) {
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create multiple entropy consuming studies only referencing the LOW
// entropy layer.
test_studies.push_back(CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId})));
test_studies.push_back(CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId})));
// The seed is not rejected, only the LOW layer is active.
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
ASSERT_TRUE(result.seed_has_active_low_layer.has_value());
EXPECT_TRUE(result.seed_has_active_low_layer.value());
EXPECT_TRUE(result.seed_has_active_limited_layer.has_value());
EXPECT_FALSE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
SeedRejection_AllowNonActiveSimultaneousLowAndLimitedLayers_LIMITED) {
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create multiple entropy consuming studies only referencing the LIMITED
// entropy layer.
test_studies.push_back(
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId})));
test_studies.push_back(
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId})));
// The seed is not rejected, only the LIMITED layer is active.
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
ASSERT_TRUE(result.seed_has_active_low_layer.has_value());
EXPECT_FALSE(result.seed_has_active_low_layer.value());
EXPECT_TRUE(result.seed_has_active_limited_layer.has_value());
EXPECT_TRUE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
SimultaneousLowAndLimitedLayers_LowLayerExperimentIsNotWebVisible) {
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create a non-web-visible study that refers to the LOW entropy layer.
test_studies.push_back(CreateTestStudy(
{CreateExperiment(50), CreateExperiment(50)},
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId})));
// Create an entropy-consuming study that refers to the LIMITED entropy layer.
test_studies.push_back(
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId})));
// Verify that the seed is not rejected. The study in the LOW entropy layer
// is not entropy-consuming. Only the limited layer is active.
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
ASSERT_TRUE(result.seed_has_active_low_layer.has_value());
EXPECT_FALSE(result.seed_has_active_low_layer.value());
EXPECT_TRUE(result.seed_has_active_limited_layer.has_value());
EXPECT_TRUE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
SimultaneousLowAndLimitedLayers_LowLayerExperimentIsNotPermanent) {
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create a session-randomized study that refers to the LOW entropy layer.
{
auto study = CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId}));
study.set_consistency(Study::SESSION);
test_studies.push_back(std::move(study));
}
// Create an entropy-consuming study that refers to the LIMITED entropy layer.
test_studies.push_back(
CreateTestStudy(CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId})));
// Verify that the seed is not rejected. The study in the LOW entropy layer
// is not entropy-consuming. Only the limited layer is active.
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
ASSERT_TRUE(result.seed_has_active_low_layer.has_value());
EXPECT_FALSE(result.seed_has_active_low_layer.value());
EXPECT_TRUE(result.seed_has_active_limited_layer.has_value());
EXPECT_TRUE(result.seed_has_active_limited_layer.value());
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
TEST_F(LimitedEntropyRandomizationTest,
SimultaneousLowAndLimitedLayers_DifferentFormFactors) {
client_state_.form_factor = Study_FormFactor_DESKTOP;
std::vector<Layer> test_layers;
std::vector<Study> test_studies;
// Create LOW and LIMITED entropy layers.
test_layers.push_back(CreateLayer(
kLowEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LOW,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
test_layers.push_back(CreateLayer(
kLimitedEntropyLayerId, /*num_slots=*/100,
/*entropy_mode=*/Layer::LIMITED,
/*layer_members=*/{CreateLayerMember(kTestLayerMemberId, {{0, 49}})}));
// Create a DESKTOP-only entropy-consuming study that refers to the LOW
// entropy layer.
{
auto study = CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLowEntropyLayerId, {kTestLayerMemberId}));
study.mutable_filter()->add_form_factor(Study_FormFactor_DESKTOP);
test_studies.push_back(std::move(study));
}
// Create a PHONE-only entropy-consuming study that refers to the LIMITED
// entropy layer.
{
auto study = CreateTestStudy(
CreateExperimentsWithTwoBitsOfEntropy(),
CreateLayerMemberReference(kLimitedEntropyLayerId,
{kTestLayerMemberId}));
study.mutable_filter()->add_form_factor(Study_FormFactor_PHONE);
test_studies.push_back(std::move(study));
}
// Verify that the seed is not rejected for a client with either form factor,
// since the study targeting the other form factor does not apply. Similarly,
// a client with neither form factor has no active layers and should not
// reject the seed.
for (auto form_factor : {Study_FormFactor_DESKTOP, Study_FormFactor_PHONE,
Study_FormFactor_TABLET}) {
client_state_.form_factor = form_factor;
auto test_seed = CreateTestSeed(test_layers, test_studies);
auto result = SeedHasMisconfiguredEntropy(client_state_, test_seed,
kEntropyLimit_10bits);
EXPECT_FALSE(result.is_misconfigured);
ASSERT_TRUE(result.seed_has_active_low_layer.has_value());
EXPECT_EQ(result.seed_has_active_low_layer.value(),
form_factor == Study_FormFactor_DESKTOP);
EXPECT_TRUE(result.seed_has_active_limited_layer.has_value());
EXPECT_EQ(result.seed_has_active_limited_layer.value(),
form_factor == Study_FormFactor_PHONE);
histogram_tester_.ExpectTotalCount(kSeedRejectionReasonHistogram, 0);
}
}
} // namespace variations