Home | History | Annotate | Download | only in variations
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "components/variations/study_filtering.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/strings/string_split.h"
     10 #include "components/variations/processed_study.h"
     11 #include "testing/gtest/include/gtest/gtest.h"
     12 
     13 namespace chrome_variations {
     14 
     15 namespace {
     16 
     17 // Converts |time| to Study proto format.
     18 int64 TimeToProtoTime(const base::Time& time) {
     19   return (time - base::Time::UnixEpoch()).InSeconds();
     20 }
     21 
     22 // Adds an experiment to |study| with the specified |name| and |probability|.
     23 Study_Experiment* AddExperiment(const std::string& name, int probability,
     24                                 Study* study) {
     25   Study_Experiment* experiment = study->add_experiment();
     26   experiment->set_name(name);
     27   experiment->set_probability_weight(probability);
     28   return experiment;
     29 }
     30 
     31 }  // namespace
     32 
     33 TEST(VariationsStudyFilteringTest, CheckStudyChannel) {
     34   const Study_Channel channels[] = {
     35     Study_Channel_CANARY,
     36     Study_Channel_DEV,
     37     Study_Channel_BETA,
     38     Study_Channel_STABLE,
     39   };
     40   bool channel_added[arraysize(channels)] = { 0 };
     41 
     42   Study_Filter filter;
     43 
     44   // Check in the forwarded order. The loop cond is <= arraysize(channels)
     45   // instead of < so that the result of adding the last channel gets checked.
     46   for (size_t i = 0; i <= arraysize(channels); ++i) {
     47     for (size_t j = 0; j < arraysize(channels); ++j) {
     48       const bool expected = channel_added[j] || filter.channel_size() == 0;
     49       const bool result = internal::CheckStudyChannel(filter, channels[j]);
     50       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
     51     }
     52 
     53     if (i < arraysize(channels)) {
     54       filter.add_channel(channels[i]);
     55       channel_added[i] = true;
     56     }
     57   }
     58 
     59   // Do the same check in the reverse order.
     60   filter.clear_channel();
     61   memset(&channel_added, 0, sizeof(channel_added));
     62   for (size_t i = 0; i <= arraysize(channels); ++i) {
     63     for (size_t j = 0; j < arraysize(channels); ++j) {
     64       const bool expected = channel_added[j] || filter.channel_size() == 0;
     65       const bool result = internal::CheckStudyChannel(filter, channels[j]);
     66       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
     67     }
     68 
     69     if (i < arraysize(channels)) {
     70       const int index = arraysize(channels) - i - 1;
     71       filter.add_channel(channels[index]);
     72       channel_added[index] = true;
     73     }
     74   }
     75 }
     76 
     77 TEST(VariationsStudyFilteringTest, CheckStudyFormFactor) {
     78   const Study_FormFactor form_factors[] = {
     79     Study_FormFactor_DESKTOP,
     80     Study_FormFactor_PHONE,
     81     Study_FormFactor_TABLET,
     82   };
     83 
     84   ASSERT_EQ(Study_FormFactor_FormFactor_ARRAYSIZE,
     85             static_cast<int>(arraysize(form_factors)));
     86 
     87   bool form_factor_added[arraysize(form_factors)] = { 0 };
     88   Study_Filter filter;
     89 
     90   for (size_t i = 0; i <= arraysize(form_factors); ++i) {
     91     for (size_t j = 0; j < arraysize(form_factors); ++j) {
     92       const bool expected = form_factor_added[j] ||
     93                             filter.form_factor_size() == 0;
     94       const bool result = internal::CheckStudyFormFactor(filter,
     95                                                          form_factors[j]);
     96       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
     97     }
     98 
     99     if (i < arraysize(form_factors)) {
    100       filter.add_form_factor(form_factors[i]);
    101       form_factor_added[i] = true;
    102     }
    103   }
    104 
    105   // Do the same check in the reverse order.
    106   filter.clear_form_factor();
    107   memset(&form_factor_added, 0, sizeof(form_factor_added));
    108   for (size_t i = 0; i <= arraysize(form_factors); ++i) {
    109     for (size_t j = 0; j < arraysize(form_factors); ++j) {
    110       const bool expected = form_factor_added[j] ||
    111                             filter.form_factor_size() == 0;
    112       const bool result = internal::CheckStudyFormFactor(filter,
    113                                                          form_factors[j]);
    114       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
    115     }
    116 
    117     if (i < arraysize(form_factors)) {
    118       const int index = arraysize(form_factors) - i - 1;;
    119       filter.add_form_factor(form_factors[index]);
    120       form_factor_added[index] = true;
    121     }
    122   }
    123 }
    124 
    125 TEST(VariationsStudyFilteringTest, CheckStudyLocale) {
    126   struct {
    127     const char* filter_locales;
    128     bool en_us_result;
    129     bool en_ca_result;
    130     bool fr_result;
    131   } test_cases[] = {
    132     {"en-US", true, false, false},
    133     {"en-US,en-CA,fr", true, true, true},
    134     {"en-US,en-CA,en-GB", true, true, false},
    135     {"en-GB,en-CA,en-US", true, true, false},
    136     {"ja,kr,vi", false, false, false},
    137     {"fr-CA", false, false, false},
    138     {"", true, true, true},
    139   };
    140 
    141   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) {
    142     std::vector<std::string> filter_locales;
    143     Study_Filter filter;
    144     base::SplitString(test_cases[i].filter_locales, ',', &filter_locales);
    145     for (size_t j = 0; j < filter_locales.size(); ++j)
    146       filter.add_locale(filter_locales[j]);
    147     EXPECT_EQ(test_cases[i].en_us_result,
    148               internal::CheckStudyLocale(filter, "en-US"));
    149     EXPECT_EQ(test_cases[i].en_ca_result,
    150               internal::CheckStudyLocale(filter, "en-CA"));
    151     EXPECT_EQ(test_cases[i].fr_result,
    152               internal::CheckStudyLocale(filter, "fr"));
    153   }
    154 }
    155 
    156 TEST(VariationsStudyFilteringTest, CheckStudyPlatform) {
    157   const Study_Platform platforms[] = {
    158     Study_Platform_PLATFORM_WINDOWS,
    159     Study_Platform_PLATFORM_MAC,
    160     Study_Platform_PLATFORM_LINUX,
    161     Study_Platform_PLATFORM_CHROMEOS,
    162     Study_Platform_PLATFORM_ANDROID,
    163     Study_Platform_PLATFORM_IOS,
    164   };
    165   ASSERT_EQ(Study_Platform_Platform_ARRAYSIZE,
    166             static_cast<int>(arraysize(platforms)));
    167   bool platform_added[arraysize(platforms)] = { 0 };
    168 
    169   Study_Filter filter;
    170 
    171   // Check in the forwarded order. The loop cond is <= arraysize(platforms)
    172   // instead of < so that the result of adding the last channel gets checked.
    173   for (size_t i = 0; i <= arraysize(platforms); ++i) {
    174     for (size_t j = 0; j < arraysize(platforms); ++j) {
    175       const bool expected = platform_added[j] || filter.platform_size() == 0;
    176       const bool result = internal::CheckStudyPlatform(filter, platforms[j]);
    177       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
    178     }
    179 
    180     if (i < arraysize(platforms)) {
    181       filter.add_platform(platforms[i]);
    182       platform_added[i] = true;
    183     }
    184   }
    185 
    186   // Do the same check in the reverse order.
    187   filter.clear_platform();
    188   memset(&platform_added, 0, sizeof(platform_added));
    189   for (size_t i = 0; i <= arraysize(platforms); ++i) {
    190     for (size_t j = 0; j < arraysize(platforms); ++j) {
    191       const bool expected = platform_added[j] || filter.platform_size() == 0;
    192       const bool result = internal::CheckStudyPlatform(filter, platforms[j]);
    193       EXPECT_EQ(expected, result) << "Case " << i << "," << j << " failed!";
    194     }
    195 
    196     if (i < arraysize(platforms)) {
    197       const int index = arraysize(platforms) - i - 1;
    198       filter.add_platform(platforms[index]);
    199       platform_added[index] = true;
    200     }
    201   }
    202 }
    203 
    204 TEST(VariationsStudyFilteringTest, CheckStudyStartDate) {
    205   const base::Time now = base::Time::Now();
    206   const base::TimeDelta delta = base::TimeDelta::FromHours(1);
    207   const struct {
    208     const base::Time start_date;
    209     bool expected_result;
    210   } start_test_cases[] = {
    211     { now - delta, true },
    212     { now, true },
    213     { now + delta, false },
    214   };
    215 
    216   Study_Filter filter;
    217 
    218   // Start date not set should result in true.
    219   EXPECT_TRUE(internal::CheckStudyStartDate(filter, now));
    220 
    221   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(start_test_cases); ++i) {
    222     filter.set_start_date(TimeToProtoTime(start_test_cases[i].start_date));
    223     const bool result = internal::CheckStudyStartDate(filter, now);
    224     EXPECT_EQ(start_test_cases[i].expected_result, result)
    225         << "Case " << i << " failed!";
    226   }
    227 }
    228 
    229 TEST(VariationsStudyFilteringTest, CheckStudyVersion) {
    230   const struct {
    231     const char* min_version;
    232     const char* version;
    233     bool expected_result;
    234   } min_test_cases[] = {
    235     { "1.2.2", "1.2.3", true },
    236     { "1.2.3", "1.2.3", true },
    237     { "1.2.4", "1.2.3", false },
    238     { "1.3.2", "1.2.3", false },
    239     { "2.1.2", "1.2.3", false },
    240     { "0.3.4", "1.2.3", true },
    241     // Wildcards.
    242     { "1.*", "1.2.3", true },
    243     { "1.2.*", "1.2.3", true },
    244     { "1.2.3.*", "1.2.3", true },
    245     { "1.2.4.*", "1.2.3", false },
    246     { "2.*", "1.2.3", false },
    247     { "0.3.*", "1.2.3", true },
    248   };
    249 
    250   const struct {
    251     const char* max_version;
    252     const char* version;
    253     bool expected_result;
    254   } max_test_cases[] = {
    255     { "1.2.2", "1.2.3", false },
    256     { "1.2.3", "1.2.3", true },
    257     { "1.2.4", "1.2.3", true },
    258     { "2.1.1", "1.2.3", true },
    259     { "2.1.1", "2.3.4", false },
    260     // Wildcards
    261     { "2.1.*", "2.3.4", false },
    262     { "2.*", "2.3.4", true },
    263     { "2.3.*", "2.3.4", true },
    264     { "2.3.4.*", "2.3.4", true },
    265     { "2.3.4.0.*", "2.3.4", true },
    266     { "2.4.*", "2.3.4", true },
    267     { "1.3.*", "2.3.4", false },
    268     { "1.*", "2.3.4", false },
    269   };
    270 
    271   Study_Filter filter;
    272 
    273   // Min/max version not set should result in true.
    274   EXPECT_TRUE(internal::CheckStudyVersion(filter, base::Version("1.2.3")));
    275 
    276   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(min_test_cases); ++i) {
    277     filter.set_min_version(min_test_cases[i].min_version);
    278     const bool result =
    279         internal::CheckStudyVersion(filter, Version(min_test_cases[i].version));
    280     EXPECT_EQ(min_test_cases[i].expected_result, result) <<
    281         "Min. version case " << i << " failed!";
    282   }
    283   filter.clear_min_version();
    284 
    285   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(max_test_cases); ++i) {
    286     filter.set_max_version(max_test_cases[i].max_version);
    287     const bool result =
    288         internal::CheckStudyVersion(filter, Version(max_test_cases[i].version));
    289     EXPECT_EQ(max_test_cases[i].expected_result, result) <<
    290         "Max version case " << i << " failed!";
    291   }
    292 
    293   // Check intersection semantics.
    294   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(min_test_cases); ++i) {
    295     for (size_t j = 0; j < ARRAYSIZE_UNSAFE(max_test_cases); ++j) {
    296       filter.set_min_version(min_test_cases[i].min_version);
    297       filter.set_max_version(max_test_cases[j].max_version);
    298 
    299       if (!min_test_cases[i].expected_result) {
    300         const bool result =
    301             internal::CheckStudyVersion(
    302                 filter, Version(min_test_cases[i].version));
    303         EXPECT_FALSE(result) << "Case " << i << "," << j << " failed!";
    304       }
    305 
    306       if (!max_test_cases[j].expected_result) {
    307         const bool result =
    308             internal::CheckStudyVersion(
    309                 filter, Version(max_test_cases[j].version));
    310         EXPECT_FALSE(result) << "Case " << i << "," << j << " failed!";
    311       }
    312     }
    313   }
    314 }
    315 
    316 TEST(VariationsStudyFilteringTest, CheckStudyHardwareClass) {
    317   struct {
    318     const char* hardware_class;
    319     const char* exclude_hardware_class;
    320     const char* actual_hardware_class;
    321     bool expected_result;
    322   } test_cases[] = {
    323     // Neither filtered nor excluded set:
    324     // True since empty is always a match.
    325     {"", "", "fancy INTEL pear device", true},
    326     {"", "", "", true},
    327 
    328     // Filtered set:
    329     {"apple,pear,orange", "", "apple", true},
    330     {"apple,pear,orange", "", "fancy INTEL pear device", true},
    331     {"apple,pear,orange", "", "fancy INTEL GRAPE device", false},
    332     // Somehow tagged as both, but still valid.
    333     {"apple,pear,orange", "", "fancy INTEL pear GRAPE device", true},
    334     // Substring, so still valid.
    335     {"apple,pear,orange", "", "fancy INTEL SNapple device", true},
    336     // No issues with doubling.
    337     {"apple,pear,orange", "", "fancy orange orange device", true},
    338     // Empty, which is what would happen for non ChromeOS platforms.
    339     {"apple,pear,orange", "", "", false},
    340 
    341     // Excluded set:
    342     {"", "apple,pear,orange", "apple", false},
    343     {"", "apple,pear,orange", "fancy INTEL pear device", false},
    344     {"", "apple,pear,orange", "fancy INTEL GRAPE device", true},
    345     // Somehow tagged as both. Very excluded!
    346     {"", "apple,pear,orange", "fancy INTEL pear GRAPE device", false},
    347     // Substring, so still invalid.
    348     {"", "apple,pear,orange", "fancy INTEL SNapple device", false},
    349     // Empty.
    350     {"", "apple,pear,orange", "", true},
    351 
    352     // Not testing when both are set as it should never occur and should be
    353     // considered undefined.
    354   };
    355 
    356   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) {
    357     Study_Filter filter;
    358     std::vector<std::string> hardware_class;
    359     base::SplitString(test_cases[i].hardware_class, ',', &hardware_class);
    360     for (size_t j = 0; j < hardware_class.size(); ++j)
    361       filter.add_hardware_class(hardware_class[j]);
    362 
    363     std::vector<std::string> exclude_hardware_class;
    364     base::SplitString(test_cases[i].exclude_hardware_class, ',',
    365                       &exclude_hardware_class);
    366     for (size_t j = 0; j < exclude_hardware_class.size(); ++j)
    367       filter.add_exclude_hardware_class(exclude_hardware_class[j]);
    368 
    369     EXPECT_EQ(test_cases[i].expected_result,
    370               internal::CheckStudyHardwareClass(
    371                   filter, test_cases[i].actual_hardware_class));
    372   }
    373 }
    374 
    375 TEST(VariationsStudyFilteringTest, FilterAndValidateStudies) {
    376   const std::string kTrial1Name = "A";
    377   const std::string kGroup1Name = "Group1";
    378   const std::string kTrial3Name = "B";
    379 
    380   VariationsSeed seed;
    381   Study* study1 = seed.add_study();
    382   study1->set_name(kTrial1Name);
    383   study1->set_default_experiment_name("Default");
    384   AddExperiment(kGroup1Name, 100, study1);
    385   AddExperiment("Default", 0, study1);
    386 
    387   Study* study2 = seed.add_study();
    388   *study2 = *study1;
    389   study2->mutable_experiment(0)->set_name("Bam");
    390   ASSERT_EQ(seed.study(0).name(), seed.study(1).name());
    391 
    392   Study* study3 = seed.add_study();
    393   study3->set_name(kTrial3Name);
    394   study3->set_default_experiment_name("Default");
    395   AddExperiment("A", 10, study3);
    396   AddExperiment("Default", 25, study3);
    397 
    398   std::vector<ProcessedStudy> processed_studies;
    399   FilterAndValidateStudies(
    400       seed, "en-CA", base::Time::Now(), base::Version("20.0.0.0"),
    401       Study_Channel_STABLE, Study_FormFactor_DESKTOP, "", &processed_studies);
    402 
    403   // Check that only the first kTrial1Name study was kept.
    404   ASSERT_EQ(2U, processed_studies.size());
    405   EXPECT_EQ(kTrial1Name, processed_studies[0].study()->name());
    406   EXPECT_EQ(kGroup1Name, processed_studies[0].study()->experiment(0).name());
    407   EXPECT_EQ(kTrial3Name, processed_studies[1].study()->name());
    408 }
    409 
    410 TEST(VariationsStudyFilteringTest, IsStudyExpired) {
    411   const base::Time now = base::Time::Now();
    412   const base::TimeDelta delta = base::TimeDelta::FromHours(1);
    413   const struct {
    414     const base::Time expiry_date;
    415     bool expected_result;
    416   } expiry_test_cases[] = {
    417     { now - delta, true },
    418     { now, true },
    419     { now + delta, false },
    420   };
    421 
    422   Study study;
    423 
    424   // Expiry date not set should result in false.
    425   EXPECT_FALSE(internal::IsStudyExpired(study, now));
    426 
    427   for (size_t i = 0; i < ARRAYSIZE_UNSAFE(expiry_test_cases); ++i) {
    428     study.set_expiry_date(TimeToProtoTime(expiry_test_cases[i].expiry_date));
    429     const bool result = internal::IsStudyExpired(study, now);
    430     EXPECT_EQ(expiry_test_cases[i].expected_result, result)
    431         << "Case " << i << " failed!";
    432   }
    433 }
    434 
    435 TEST(VariationsStudyFilteringTest, ValidateStudy) {
    436   Study study;
    437   study.set_default_experiment_name("def");
    438   AddExperiment("abc", 100, &study);
    439   Study_Experiment* default_group = AddExperiment("def", 200, &study);
    440 
    441   ProcessedStudy processed_study;
    442   EXPECT_TRUE(processed_study.Init(&study, false));
    443   EXPECT_EQ(300, processed_study.total_probability());
    444 
    445   // Min version checks.
    446   study.mutable_filter()->set_min_version("1.2.3.*");
    447   EXPECT_TRUE(processed_study.Init(&study, false));
    448   study.mutable_filter()->set_min_version("1.*.3");
    449   EXPECT_FALSE(processed_study.Init(&study, false));
    450   study.mutable_filter()->set_min_version("1.2.3");
    451   EXPECT_TRUE(processed_study.Init(&study, false));
    452 
    453   // Max version checks.
    454   study.mutable_filter()->set_max_version("2.3.4.*");
    455   EXPECT_TRUE(processed_study.Init(&study, false));
    456   study.mutable_filter()->set_max_version("*.3");
    457   EXPECT_FALSE(processed_study.Init(&study, false));
    458   study.mutable_filter()->set_max_version("2.3.4");
    459   EXPECT_TRUE(processed_study.Init(&study, false));
    460 
    461   study.clear_default_experiment_name();
    462   EXPECT_FALSE(processed_study.Init(&study, false));
    463 
    464   study.set_default_experiment_name("xyz");
    465   EXPECT_FALSE(processed_study.Init(&study, false));
    466 
    467   study.set_default_experiment_name("def");
    468   default_group->clear_name();
    469   EXPECT_FALSE(processed_study.Init(&study, false));
    470 
    471   default_group->set_name("def");
    472   EXPECT_TRUE(processed_study.Init(&study, false));
    473   Study_Experiment* repeated_group = study.add_experiment();
    474   repeated_group->set_name("abc");
    475   repeated_group->set_probability_weight(1);
    476   EXPECT_FALSE(processed_study.Init(&study, false));
    477 }
    478 
    479 }  // namespace chrome_variations
    480