| // 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/variations_layers.h" |
| |
| #include <sys/types.h> |
| |
| #include <cstdint> |
| #include <limits> |
| #include <vector> |
| |
| #include "base/test/metrics/histogram_tester.h" |
| #include "components/variations/entropy_provider.h" |
| #include "components/variations/processed_study.h" |
| #include "components/variations/proto/layer.pb.h" |
| #include "components/variations/proto/study.pb.h" |
| #include "components/variations/proto/variations_seed.pb.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace variations { |
| |
| namespace { |
| |
| const uint32_t kLayerId = 101; |
| const uint32_t kLayerMemberId = 201; |
| |
| // If CreateSeedWithLimitedLayer() is used to constructed the layer and the |
| // seed, the following values are picked so that a particular slot can be |
| // selected. This is used to catch any error if the entropy provider is not |
| // selected based on the entropy mode. |
| const uint32_t kTestLowEntropySource = 502; // Will select slot 49. |
| const char kTestClientID[] = "client_id_809"; // Will select slot 99. |
| const char kTestLimitedEntropyRandomizationSource[] = |
| "limited_entropy_randomization_source_964"; // Will select slot 0. |
| |
| struct LayerMemberSpec { |
| uint32_t id; |
| uint32_t start; |
| uint32_t end; |
| }; |
| |
| struct LayerSpec { |
| uint32_t id; |
| uint32_t num_slots; |
| Layer::EntropyMode entropy_mode; |
| std::vector<LayerMemberSpec> layer_members; |
| }; |
| |
| struct StudySpec { |
| uint32_t layer_id; |
| uint32_t layer_member_id; |
| }; |
| |
| struct SeedSpec { |
| std::vector<Layer> layers; |
| std::vector<Study> studies; |
| }; |
| |
| const LayerMemberSpec kSingleSlotLayerMember = {.id = kLayerMemberId, |
| .start = 0u, |
| .end = 0u}; |
| |
| Layer CreateLayer(const LayerSpec spec) { |
| Layer layer; |
| layer.set_id(spec.id); |
| layer.set_num_slots(spec.num_slots); |
| layer.set_entropy_mode(spec.entropy_mode); |
| |
| for (const auto& layer_member_spec : spec.layer_members) { |
| auto* layer_member = layer.add_members(); |
| layer_member->set_id(layer_member_spec.id); |
| auto* slot = layer_member->add_slots(); |
| slot->set_start(layer_member_spec.start); |
| slot->set_end(layer_member_spec.end); |
| } |
| |
| return layer; |
| } |
| |
| // Creates a layer with a single 100% layer member. |
| Layer CreateSimpleLayer(Layer::EntropyMode entropy_mode) { |
| return CreateLayer( |
| {.id = kLayerId, |
| .num_slots = 100, |
| .entropy_mode = entropy_mode, |
| .layer_members = {{.id = kLayerMemberId, .start = 0, .end = 99}}}); |
| } |
| |
| // Adds an experiment with the given name and probability to a study. |
| Study::Experiment* AddExperiment(const std::string& name, |
| uint32_t probability, |
| Study* study) { |
| Study::Experiment* experiment = study->add_experiment(); |
| experiment->set_name(name); |
| experiment->set_probability_weight(probability); |
| return experiment; |
| } |
| |
| Study CreateStudy(const StudySpec& spec) { |
| Study study; |
| study.set_name("TestStudy"); |
| AddExperiment("Experiment", 100, &study); |
| |
| auto* layer_member_reference = study.mutable_layer(); |
| layer_member_reference->set_layer_id(spec.layer_id); |
| layer_member_reference->add_layer_member_ids(spec.layer_member_id); |
| |
| return study; |
| } |
| |
| LayerMemberReference CreateLayerMemberReference( |
| uint32_t layer_id, |
| const std::vector<uint32_t>& layer_member_ids) { |
| LayerMemberReference reference; |
| reference.set_layer_id(layer_id); |
| for (uint32_t layer_member_id : layer_member_ids) { |
| reference.add_layer_member_ids(layer_member_id); |
| } |
| return reference; |
| } |
| |
| // Creates a study with the given `consistency`, and two 50% groups. |
| Study CreateTwoArmStudy(Study_Consistency consistency) { |
| Study study; |
| study.set_name("TestStudy"); |
| study.set_consistency(consistency); |
| |
| AddExperiment("Group1", 50, &study); |
| AddExperiment("Group2", 50, &study); |
| return study; |
| } |
| |
| // Adds a google_web_experiment_id to each experiment in the given `study`. The |
| // first experiments will use 100001, the second 100002, and so on. |
| void AddGoogleExperimentIds(Study* study) { |
| for (int i = 0; i < study->experiment_size(); ++i) { |
| Study::Experiment* experiment = study->mutable_experiment(i); |
| experiment->set_google_web_experiment_id(100001 + i); |
| } |
| } |
| |
| void ConstrainToLayer(Study* study, |
| const LayerMemberReference& layer_member_reference) { |
| LayerMemberReference* reference_to_fill = study->mutable_layer(); |
| reference_to_fill->MergeFrom(layer_member_reference); |
| } |
| |
| VariationsSeed CreateSeed(const SeedSpec& spec) { |
| VariationsSeed seed; |
| for (const auto& layer : spec.layers) { |
| auto* layer_to_add = seed.add_layers(); |
| layer_to_add->MergeFrom(layer); |
| } |
| for (const auto& study : spec.studies) { |
| auto* study_to_add = seed.add_study(); |
| study_to_add->MergeFrom(study); |
| } |
| return seed; |
| } |
| |
| // Creates a seed that contains the given study constrained to a layer with the |
| // given entropy mode. This will modify the given study to be layer constrained, |
| // but returned seed will have a copy of the modified study. |
| VariationsSeed CreateSeedWithLayerConstrainedStudy( |
| Layer::EntropyMode layer_entropy_mode, |
| Study* study_to_be_constrained) { |
| Layer layer = CreateSimpleLayer(layer_entropy_mode); |
| ConstrainToLayer(study_to_be_constrained, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| return CreateSeed({.layers = {layer}, .studies = {*study_to_be_constrained}}); |
| } |
| |
| VariationsSeed CreateSeedWithLimitedLayer() { |
| return CreateSeed( |
| {.layers = {CreateLayer({.id = kLayerId, |
| .num_slots = 100u, |
| .entropy_mode = Layer::LIMITED, |
| .layer_members = {kSingleSlotLayerMember}})}, |
| .studies = {CreateStudy( |
| {.layer_id = kLayerId, .layer_member_id = kLayerMemberId})}}); |
| } |
| |
| enum EntropyProviderSelection { |
| NOT_SELECTED = 0, |
| SESSION = 1, |
| DEFAULT = 2, |
| LOW = 3, |
| LIMITED = 4, |
| }; |
| |
| class FakeEntropyProviders : public EntropyProviders { |
| public: |
| explicit FakeEntropyProviders( |
| std::string_view high_entropy_value, |
| std::string_view limited_entropy_randomization_source = |
| kTestLimitedEntropyRandomizationSource) |
| : EntropyProviders(high_entropy_value, |
| {.value = 0, .range = 100}, |
| limited_entropy_randomization_source) {} |
| ~FakeEntropyProviders() override = default; |
| |
| const base::FieldTrial::EntropyProvider& default_entropy() const override { |
| selection_ = EntropyProviderSelection::DEFAULT; |
| return EntropyProviders::default_entropy(); |
| } |
| const base::FieldTrial::EntropyProvider& low_entropy() const override { |
| selection_ = EntropyProviderSelection::LOW; |
| return EntropyProviders::low_entropy(); |
| } |
| const base::FieldTrial::EntropyProvider& session_entropy() const override { |
| selection_ = EntropyProviderSelection::SESSION; |
| return EntropyProviders::session_entropy(); |
| } |
| const base::FieldTrial::EntropyProvider& limited_entropy() const override { |
| selection_ = EntropyProviderSelection::LIMITED; |
| return EntropyProviders::limited_entropy(); |
| } |
| EntropyProviderSelection selection() { return selection_; } |
| |
| private: |
| // The "mutable" keyword allows `selection_` to be updated in const functions. |
| mutable EntropyProviderSelection selection_ = |
| EntropyProviderSelection::NOT_SELECTED; |
| }; |
| |
| } // namespace |
| |
| class VariationsLayersTest : public ::testing::Test { |
| public: |
| VariationsLayersTest() |
| : entropy_providers_(kTestClientID, |
| {kTestLowEntropySource, 8000}, |
| kTestLimitedEntropyRandomizationSource) {} |
| |
| protected: |
| const EntropyProviders entropy_providers_; |
| base::HistogramTester histogram_tester_; |
| }; |
| |
| TEST_F(VariationsLayersTest, LayersHaveDuplicatedID) { |
| auto layer = CreateLayer({.id = kLayerId, |
| .num_slots = 100u, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = {kSingleSlotLayerMember}}); |
| auto study = |
| CreateStudy({.layer_id = kLayerId, .layer_member_id = kLayerMemberId}); |
| // Creating a seed with 3 layers using the same ID. |
| auto seed = CreateSeed({.layers = {layer, layer, layer}, .studies = {study}}); |
| |
| VariationsLayers layers(seed, entropy_providers_); |
| |
| EXPECT_FALSE(layers.IsLayerMemberActive( |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId}))); |
| // InvalidLayerReason::LayerIDNotUnique. Assert on the integer enum value in |
| // case the semantics change over time. |
| const int expected_bucket = 7; |
| // The metric should only be reported once. |
| histogram_tester_.ExpectUniqueSample("Variations.InvalidLayerReason", |
| expected_bucket, 1); |
| } |
| |
| TEST_F(VariationsLayersTest, LayersAllHaveUniqueIDs) { |
| const uint32_t layer_id_1 = kLayerId; |
| const uint32_t layer_id_2 = kLayerId + 1; |
| |
| const auto layer_1 = CreateLayer( |
| {.id = layer_id_1, |
| .num_slots = 100u, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = {{.id = kLayerMemberId, .start = 0u, .end = 99u}}}); |
| const auto layer_2 = CreateLayer( |
| {.id = layer_id_2, |
| .num_slots = 100u, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = {{.id = kLayerMemberId, .start = 0u, .end = 99u}}}); |
| |
| const auto study_1 = |
| CreateStudy({.layer_id = layer_id_1, .layer_member_id = kLayerMemberId}); |
| const auto study_2 = |
| CreateStudy({.layer_id = layer_id_2, .layer_member_id = kLayerMemberId}); |
| |
| auto seed = |
| CreateSeed({.layers = {layer_1, layer_2}, .studies = {study_1, study_2}}); |
| |
| VariationsLayers layers(seed, entropy_providers_); |
| |
| EXPECT_TRUE(layers.IsLayerMemberActive( |
| CreateLayerMemberReference(layer_id_1, {kLayerMemberId}))); |
| EXPECT_TRUE(layers.IsLayerMemberActive( |
| CreateLayerMemberReference(layer_id_2, {kLayerMemberId}))); |
| histogram_tester_.ExpectTotalCount("Variations.InvalidLayerReason", 0); |
| } |
| |
| TEST_F(VariationsLayersTest, ValidLimitedLayer) { |
| VariationsLayers layers(CreateSeedWithLimitedLayer(), entropy_providers_); |
| |
| EXPECT_TRUE(layers.IsLayerActive(kLayerId)); |
| EXPECT_TRUE(layers.IsLayerMemberActive( |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId}))); |
| histogram_tester_.ExpectTotalCount("Variations.InvalidLayerReason", 0); |
| } |
| |
| TEST_F(VariationsLayersTest, InvalidLayer_LimitedLayerDropped) { |
| // An empty limited entropy randomization indicates that limited entropy |
| // randomization is not supported on this platform, or that the client is not |
| // in the enabled group of the limited entropy synthetic trial. |
| const EntropyProviders entropy_providers( |
| kTestClientID, {kTestLowEntropySource, 8000}, |
| /*limited_entropy_randomization_source=*/std::string_view()); |
| |
| VariationsLayers layers(CreateSeedWithLimitedLayer(), entropy_providers); |
| |
| EXPECT_FALSE(layers.IsLayerActive(kLayerId)); |
| EXPECT_FALSE(layers.IsLayerMemberActive( |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId}))); |
| // InvalidLayerReason::kLimitedLayerDropped. Assert on the |
| // integer enum value in case the semantics change over time. |
| const int expected_bucket = 8; |
| histogram_tester_.ExpectUniqueSample("Variations.InvalidLayerReason", |
| expected_bucket, 1); |
| } |
| |
| TEST_F(VariationsLayersTest, ValidSlotBounds) { |
| auto representable_max = std::numeric_limits<uint32_t>::max(); |
| auto study = CreateStudy({.layer_id = 1u, .layer_member_id = 1u}); |
| auto layer = |
| CreateLayer({.id = 1u, |
| .num_slots = representable_max, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = { |
| {.id = 1u, .start = 0u, .end = representable_max - 1}}}); |
| EXPECT_TRUE(VariationsLayers::AreSlotBoundsValid(layer)); |
| } |
| |
| TEST_F(VariationsLayersTest, InvalidSlotBounds_ReferringToOutOfBoundsSlot) { |
| auto representable_max = std::numeric_limits<uint32_t>::max(); |
| auto study = CreateStudy({.layer_id = 1u, .layer_member_id = 1u}); |
| auto layer = CreateLayer( |
| {.id = 1u, |
| .num_slots = representable_max, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = { |
| {.id = 1u, .start = 0u, .end = representable_max - 1}, |
| // The last slot has index `representable_max - 1` so |
| // `representable_max` is out of bound. |
| {.id = 2u, .start = representable_max, .end = representable_max}}}); |
| EXPECT_FALSE(VariationsLayers::AreSlotBoundsValid(layer)); |
| } |
| |
| TEST_F(VariationsLayersTest, UniqueLayerMemberIDs) { |
| const auto layer = |
| CreateLayer({.id = 1u, |
| .num_slots = 10u, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = {{.id = 1u, .start = 0u, .end = 4u}, |
| {.id = 2u, .start = 5u, .end = 9u}}}); |
| auto seed = CreateSeed({.layers = {layer}, .studies = {}}); |
| |
| VariationsLayers layers(seed, entropy_providers_); |
| |
| // One of the two layer members must be active since together they are |
| // covering all slots. |
| EXPECT_TRUE( |
| layers.IsLayerMemberActive(CreateLayerMemberReference(1u, {1u})) || |
| layers.IsLayerMemberActive(CreateLayerMemberReference(1u, {2u}))); |
| histogram_tester_.ExpectTotalCount("Variations.InvalidLayerReason", 0); |
| } |
| |
| TEST_F(VariationsLayersTest, DuplicatedLayerMemberIDs) { |
| // There are two layer members with ID=1 in this layer. |
| const auto layer = |
| CreateLayer({.id = 1u, |
| .num_slots = 10u, |
| .entropy_mode = Layer::DEFAULT, |
| .layer_members = {{.id = 1u, .start = 0u, .end = 4u}, |
| {.id = 1u, .start = 5u, .end = 9u}}}); |
| auto seed = CreateSeed({.layers = {layer}, .studies = {}}); |
| |
| VariationsLayers layers(seed, entropy_providers_); |
| |
| EXPECT_FALSE( |
| layers.IsLayerMemberActive(CreateLayerMemberReference(1u, {1u}))); |
| const int expected_bucket = 9; // kDuplicatedLayerMemberID |
| histogram_tester_.ExpectUniqueSample("Variations.InvalidLayerReason", |
| expected_bucket, 1); |
| } |
| |
| TEST_F(VariationsLayersTest, LowEntropyStudy) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| AddGoogleExperimentIds(&study); |
| EXPECT_FALSE(VariationsLayers::AllowsHighEntropy(study)); |
| } |
| |
| TEST_F(VariationsLayersTest, HighEntropyStudy) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| EXPECT_TRUE(VariationsLayers::AllowsHighEntropy(study)); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyConstrainedToLowEntropyLayer) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| auto seed = CreateSeedWithLayerConstrainedStudy(Layer::LOW, &study); |
| VariationsLayers layers(seed, entropy_providers_); |
| EXPECT_FALSE( |
| layers.ActiveLayerMemberDependsOnHighEntropy(study.layer().layer_id())); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyConstrainedToLimitedEntropyLayer) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| auto seed = CreateSeedWithLayerConstrainedStudy(Layer::LIMITED, &study); |
| VariationsLayers layers(seed, entropy_providers_); |
| EXPECT_FALSE( |
| layers.ActiveLayerMemberDependsOnHighEntropy(study.layer().layer_id())); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyConstrainedToDefaultEntropyLayer) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| auto seed = CreateSeedWithLayerConstrainedStudy(Layer::DEFAULT, &study); |
| VariationsLayers layers(seed, entropy_providers_); |
| EXPECT_TRUE( |
| layers.ActiveLayerMemberDependsOnHighEntropy(study.layer().layer_id())); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyEntropyProviderSelection_SelectLowEntropy) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| AddGoogleExperimentIds(&study); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); // No layers. |
| |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // Permanently consistent, non layer constrained studies with Google |
| // experiment IDs should use the low entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), EntropyProviderSelection::LOW); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_SelectSessionEntropy) { |
| Study study = CreateTwoArmStudy(Study_Consistency_SESSION); |
| AddGoogleExperimentIds(&study); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); // No layers. |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // Session consistent, non layer constrained studies with Google |
| // experiment IDs should use the session entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::SESSION); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_SelectDefaultEntropy) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); // No layers. |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // Permanently consistent, non layer constrained studies without Google |
| // experiment IDs should use the default entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::DEFAULT); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyEntropyProviderSelection_NoHighEntropyValue) { |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| FakeEntropyProviders slot_entropy_providers(/*high_entropy_value=*/""); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); // No layers. |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers(/*high_entropy_value=*/""); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // Without an high entropy value, low entropy provider should be used although |
| // the study is eligible for high entropy. |
| EXPECT_EQ(study_entropy_providers.selection(), EntropyProviderSelection::LOW); |
| } |
| |
| TEST_F( |
| VariationsLayersTest, |
| StudyEntropyProviderSelection_NoHighEntropy_ConstrainedToHighEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::DEFAULT); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers(/*high_entropy_value=*/""); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| // Default entropy is low entropy when there is no high entropy value. |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::DEFAULT); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers(/*high_entropy_value=*/""); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // Without a high entropy value, the slot selection should use the default |
| // entropy (which is low entropy in this case). A study that allows high |
| // entropy should be also randomized with low entropy. Because the de facto |
| // low entropy study is constrained to the de facto low entropy layer, the |
| // study should use the remainder entropy. The following shows that |
| // `study_entropy_providers` is not selected since the remainder entropy from |
| // slot selection is used. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_HighEntropyStudyInHighEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::DEFAULT); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::DEFAULT); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // A study that is eligible to use high entropy and constrained to a layer |
| // with `EntropyMode.DEFAULT` should use the default entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::DEFAULT); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_LowEntropyStudyInLowEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::LOW); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| AddGoogleExperimentIds(&study); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), EntropyProviderSelection::LOW); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // A study that is NOT eligible to use high entropy and constrained to a layer |
| // with `EntropyMode.LOW` should use the remainder entropy from slot |
| // randomization. Therefore a selection is not made on |
| // `study_entropy_providers`. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_HighEntropyStudyInLowEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::LOW); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), EntropyProviderSelection::LOW); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // A study that is eligible to use high entropy AND constrained to a layer |
| // with `EntropyMode.LOW` should use the default entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::DEFAULT); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| StudyEntropyProviderSelection_LimitedEntropyStudyInLimitedEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::LIMITED); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| AddGoogleExperimentIds(&study); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::LIMITED); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // A study with Google web experiment ID and is constrained to a layer with |
| // `EntropyMode.LIMITED` should use the limited entropy provider. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::LIMITED); |
| } |
| |
| TEST_F( |
| VariationsLayersTest, |
| StudyEntropyProviderSelection_NonLimitedEntropyStudyInLimitedEntropyLayer) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::LIMITED); |
| // Constructs a study without Google web experiment ID. |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers("high_entropy_value"); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::LIMITED); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers("high_entropy_value"); |
| layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| |
| // A study constrained to a layer with `EntropyMode.LIMITED` should use the |
| // limited entropy provider, regardless of whether it has Google web |
| // experiment IDs or not. |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::LIMITED); |
| } |
| |
| TEST_F(VariationsLayersTest, StudyEntropyProviderSelection_NoLimitedSource) { |
| Layer layer = CreateSimpleLayer(/*entropy_mode=*/Layer::LIMITED); |
| Study study = CreateTwoArmStudy(Study_Consistency_PERMANENT); |
| AddGoogleExperimentIds(&study); |
| ConstrainToLayer(&study, |
| CreateLayerMemberReference(kLayerId, {kLayerMemberId})); |
| |
| VariationsSeed seed; |
| *seed.add_study() = study; |
| *seed.add_layers() = layer; |
| FakeEntropyProviders slot_entropy_providers( |
| "high_entropy_value", |
| /*limited_entropy_randomization_source=*/std::string_view()); |
| VariationsLayers layers(seed, slot_entropy_providers); |
| EXPECT_EQ(slot_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); |
| EXPECT_FALSE(layers.IsLayerActive(kLayerId)); |
| |
| ProcessedStudy processed_study; |
| EXPECT_TRUE(processed_study.Init(&study)); |
| FakeEntropyProviders study_entropy_providers( |
| "high_entropy_value", |
| /*limited_entropy_randomization_source=*/std::string_view()); |
| auto selected = layers.SelectEntropyProviderForStudy(processed_study, |
| study_entropy_providers); |
| EXPECT_FALSE(selected.has_value()); |
| EXPECT_EQ(study_entropy_providers.selection(), |
| EntropyProviderSelection::NOT_SELECTED); |
| } |
| |
| TEST_F(VariationsLayersTest, IsReferencingLayerMemberId_IncludeLayerMembers) { |
| LayerMemberReference layer_member_reference = |
| CreateLayerMemberReference(kLayerId, {42, 43}); |
| EXPECT_FALSE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 41)); |
| EXPECT_TRUE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 42)); |
| EXPECT_TRUE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 43)); |
| } |
| |
| TEST_F(VariationsLayersTest, |
| IsReferencingLayerMemberId_IncludeLayerMembers_LegacyField) { |
| LayerMemberReference layer_member_reference; |
| layer_member_reference.set_layer_id(1); |
| // `layer_member_id` is a legacy field that should be replaced with |
| // `layer_member_ids`. |
| // TODO(crbug.com/TBA): remove this test after the legacy field is deprecated. |
| layer_member_reference.set_layer_member_id(42); |
| |
| EXPECT_FALSE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 41)); |
| EXPECT_TRUE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 42)); |
| } |
| |
| TEST_F(VariationsLayersTest, IsReferencingLayerMemberId_NoLayerMembers) { |
| LayerMemberReference layer_member_reference = |
| CreateLayerMemberReference(kLayerId, {}); |
| EXPECT_FALSE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 42)); |
| EXPECT_FALSE( |
| VariationsLayers::IsReferencingLayerMemberId(layer_member_reference, 43)); |
| } |
| |
| } // namespace variations |