blob: 15f128db0989bb887a96e67ef188bd6751d5a897 [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/seed_reader_writer.h"
#include "base/base64.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_entropy_provider.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/timer/mock_timer.h"
#include "base/version_info/channel.h"
#include "components/prefs/testing_pref_service.h"
#include "components/variations/pref_names.h"
#include "components/variations/variations_seed_store.h"
#include "components/variations/variations_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/compression_utils.h"
namespace variations {
namespace {
using ::testing::IsEmpty;
using ::testing::TestWithParam;
using ::testing::Values;
const base::FilePath::CharType kSeedFilename[] = FILE_PATH_LITERAL("TestSeed");
// Used for clients that do not participate in SeedFiles experiment.
constexpr char kNoGroup[] = "";
// Compresses `data` using Gzip compression.
std::string Gzip(const std::string& data) {
std::string compressed;
CHECK(compression::GzipCompress(data, &compressed));
return compressed;
}
// Creates, serializes, and then Gzip compresses a test seed.
std::string CreateCompressedVariationsSeed() {
VariationsSeed seed;
seed.add_study()->set_name("TestStudy");
std::string serialized_seed;
seed.SerializeToString(&serialized_seed);
return Gzip(serialized_seed);
}
struct SeedReaderWriterTestParams {
using TupleT =
std::tuple<std::string_view, std::string_view, version_info::Channel>;
SeedReaderWriterTestParams(std::string_view seed_pref,
std::string_view field_trial_group,
version_info::Channel channel)
: seed_pref(seed_pref),
field_trial_group(field_trial_group),
channel(channel) {}
explicit SeedReaderWriterTestParams(const TupleT& t)
: SeedReaderWriterTestParams(std::get<0>(t),
std::get<1>(t),
std::get<2>(t)) {}
std::string_view seed_pref;
std::string_view field_trial_group;
version_info::Channel channel;
};
struct ExpectedFieldTrialGroupTestParams {
using TupleT = std::tuple<std::string_view, version_info::Channel>;
ExpectedFieldTrialGroupTestParams(std::string_view seed_pref,
version_info::Channel channel)
: seed_pref(seed_pref), channel(channel) {}
explicit ExpectedFieldTrialGroupTestParams(const TupleT& t)
: ExpectedFieldTrialGroupTestParams(std::get<0>(t), std::get<1>(t)) {}
std::string_view seed_pref;
version_info::Channel channel;
};
class SeedReaderWriterTestBase {
public:
SeedReaderWriterTestBase()
: file_writer_thread_("SeedReaderWriter Test thread"),
entropy_providers_(std::make_unique<const MockEntropyProviders>(
MockEntropyProviders::Results{.low_entropy =
kAlwaysUseLastGroup})) {
scoped_feature_list_.InitWithEmptyFeatureAndFieldTrialLists();
file_writer_thread_.Start();
CHECK(temp_dir_.CreateUniqueTempDir());
temp_seed_file_path_ = temp_dir_.GetPath().Append(kSeedFilename);
VariationsSeedStore::RegisterPrefs(local_state_.registry());
}
~SeedReaderWriterTestBase() = default;
protected:
base::test::ScopedFeatureList scoped_feature_list_;
base::test::TaskEnvironment task_environment_;
base::FilePath temp_seed_file_path_;
base::Thread file_writer_thread_;
base::ScopedTempDir temp_dir_;
TestingPrefServiceSimple local_state_;
base::MockOneShotTimer timer_;
std::unique_ptr<const MockEntropyProviders> entropy_providers_;
};
class ExpectedFieldTrialGroupChannelsTest
: public SeedReaderWriterTestBase,
public TestWithParam<ExpectedFieldTrialGroupTestParams> {};
class ExpectedFieldTrialGroupAllChannelsTest
: public ExpectedFieldTrialGroupChannelsTest {};
class ExpectedFieldTrialGroupPreStableTest
: public ExpectedFieldTrialGroupChannelsTest {};
class ExpectedFieldTrialGroupStableTest
: public SeedReaderWriterTestBase,
public TestWithParam<std::string_view> {};
class ExpectedFieldTrialGroupUnknownTest
: public SeedReaderWriterTestBase,
public TestWithParam<std::string_view> {};
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupAllChannelsTest,
::testing::ConvertGenerator<ExpectedFieldTrialGroupTestParams::TupleT>(
::testing::Combine(
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed),
::testing::Values(version_info::Channel::UNKNOWN,
version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA,
version_info::Channel::STABLE))));
// If empty seed file dir given, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupAllChannelsTest, NoSeedFileDir) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
// If no entropy provider given, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupAllChannelsTest, NoEntropyProvider) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, /*entropy_providers=*/nullptr,
file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupPreStableTest,
::testing::ConvertGenerator<ExpectedFieldTrialGroupTestParams::TupleT>(
::testing::Combine(
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed),
::testing::Values(version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA))));
// If channel is pre-stable, client is assigned a group.
TEST_P(ExpectedFieldTrialGroupPreStableTest, PreStable) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial),
::testing::AnyOf(kControlGroup, kSeedFilesGroup));
}
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupStableTest,
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed));
// If channel is stable, trial has been registered.
TEST_P(ExpectedFieldTrialGroupStableTest, Stable) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam(), version_info::Channel::STABLE, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_TRUE(base::FieldTrialList::TrialExists(kSeedFileTrial));
}
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupUnknownTest,
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed));
// If channel is unknown, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupUnknownTest, Unknown) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam(), version_info::Channel::UNKNOWN, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
class SeedReaderWriterGroupTest
: public SeedReaderWriterTestBase,
public TestWithParam<SeedReaderWriterTestParams> {
public:
SeedReaderWriterGroupTest() {
SetUpSeedFileTrial(std::string(GetParam().field_trial_group));
}
};
class SeedReaderWriterSeedFilesGroupTest : public SeedReaderWriterGroupTest {};
class SeedReaderWriterLocalStateGroupsTest : public SeedReaderWriterGroupTest {
};
class SeedReaderWriterAllGroupsTest : public SeedReaderWriterGroupTest {};
// Verifies clients in SeedFiles group write seeds to a seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, WriteSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
seed_reader_writer.StoreValidatedSeed(compressed_seed,
base64_compressed_seed);
// Force write.
timer_.Fire();
file_writer_thread_.FlushForTesting();
// Verify that a seed was written to a seed file.
std::string seed_file_data;
ASSERT_TRUE(base::ReadFileToString(temp_seed_file_path_, &seed_file_data));
EXPECT_EQ(seed_file_data, compressed_seed);
}
// Verifies that a seed is cleared from a seed file for clients in the SeedFiles
// group.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ClearSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
// Clear seed and force write.
seed_reader_writer.ClearSeed();
timer_.Fire();
file_writer_thread_.FlushForTesting();
// Verify seed cleared correctly in a seed file.
std::string seed_file_data;
ASSERT_TRUE(base::ReadFileToString(temp_seed_file_path_, &seed_file_data));
EXPECT_THAT(seed_file_data, IsEmpty());
}
// Verifies clients in SeedFiles group read seeds from the seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadSeedFileBasedSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
local_state_.SetString(GetParam().seed_pref, "unused seed");
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
// Ensure seed data loaded from seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(compressed_seed, seed_reader_writer.GetSeedData().data);
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(GetParam().seed_pref, "Safe") ? "Safe" : "Latest"}),
/*sample=*/1, /*expected_bucket_count=*/1);
}
// Verifies clients in SeedFiles group do not crash if reading empty seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadEmptySeedFile) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, ""));
local_state_.SetString(GetParam().seed_pref, "unused seed");
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(GetParam().seed_pref, "Safe") ? "Safe" : "Latest"}),
/*sample=*/1, /*expected_bucket_count=*/1);
// Ensure seed data loaded from seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ("", seed_reader_writer.GetSeedData().data);
}
// Verifies clients in SeedFiles group read seeds from local state prefs if no
// seed file found.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadMissingSeedFile) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
local_state_.SetString(GetParam().seed_pref,
base::Base64Encode(compressed_seed));
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
// Ensure read failed due to seed file not existing.
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(GetParam().seed_pref, "Safe") ? "Safe" : "Latest"}),
/*sample=*/0, /*expected_bucket_count=*/1);
// Ensure seed data from local state prefs is loaded and decoded.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(compressed_seed, seed_reader_writer.GetSeedData().data);
}
INSTANTIATE_TEST_SUITE_P(
All,
SeedReaderWriterSeedFilesGroupTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed),
::testing::Values(kSeedFilesGroup),
::testing::Values(version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA))));
// Verifies clients using local state to store seeds write seeds to Local State.
TEST_P(SeedReaderWriterLocalStateGroupsTest, WriteSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
seed_reader_writer.StoreValidatedSeed(compressed_seed,
base64_compressed_seed);
// Ensure there's no pending write.
EXPECT_FALSE(timer_.IsRunning());
// Verify seed stored correctly, should only be found in Local State prefs.
EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
EXPECT_EQ(local_state_.GetString(GetParam().seed_pref),
base64_compressed_seed);
}
// Verifies that a seed is cleared from Local State and that seed file is
// deleted if present for clients using local state to store seeds.
TEST_P(SeedReaderWriterLocalStateGroupsTest, ClearSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
local_state_.SetString(GetParam().seed_pref,
base::Base64Encode(compressed_seed));
// Clear seed and force file delete.
seed_reader_writer.ClearSeed();
file_writer_thread_.FlushForTesting();
// Verify seed cleared correctly in Local State prefs and that seed file is
// deleted.
EXPECT_THAT(local_state_.GetString(GetParam().seed_pref), IsEmpty());
EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
}
// Verifies clients using local state to store seeds read seeds from local
// state.
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadLocalStateBasedSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, "unused seed"));
local_state_.SetString(GetParam().seed_pref,
base::Base64Encode(CreateCompressedVariationsSeed()));
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_pref, GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
// Ensure seed data loaded from prefs, not seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressedAndBase64Encoded,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(local_state_.GetString(GetParam().seed_pref),
seed_reader_writer.GetSeedData().data);
histogram_tester.ExpectTotalCount(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(GetParam().seed_pref, "Safe") ? "Safe" : "Latest"}),
/*expected_count=*/0);
}
// Verifies that writing seeds with an empty path for `seed_file_dir` does not
// cause a crash.
TEST_P(SeedReaderWriterLocalStateGroupsTest, EmptySeedFilePathIsValid) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename, GetParam().seed_pref,
GetParam().channel, entropy_providers_.get(),
file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
seed_reader_writer.StoreValidatedSeed(compressed_seed,
base64_compressed_seed);
// Ensure there's no pending write.
EXPECT_FALSE(timer_.IsRunning());
// Verify seed stored correctly, should only be found in Local State prefs.
EXPECT_EQ(local_state_.GetString(GetParam().seed_pref),
base64_compressed_seed);
}
INSTANTIATE_TEST_SUITE_P(
NoGroup,
SeedReaderWriterLocalStateGroupsTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed),
::testing::Values(kNoGroup),
::testing::Values(version_info::Channel::UNKNOWN))));
INSTANTIATE_TEST_SUITE_P(
ControlAndDefaultGroup,
SeedReaderWriterLocalStateGroupsTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(
::testing::Values(prefs::kVariationsCompressedSeed,
prefs::kVariationsSafeCompressedSeed),
::testing::Values(kControlGroup, kDefaultGroup),
::testing::Values(version_info::Channel::UNKNOWN,
version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA,
version_info::Channel::STABLE))));
} // namespace
} // namespace variations