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 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 variations 480