blob: 384b571c82659cda3c08712b52bf4a93d308a718 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <utility>
#include <base/dcheck_is_on.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/logging.h>
#include <base/run_loop.h>
#include <base/strings/strcat.h>
#include <base/task/single_thread_task_runner.h>
#include <base/test/bind.h>
#include <base/test/task_environment.h>
#include <chromeos/dbus/service_constants.h>
#include <dbus/message.h>
#include <dbus/mock_bus.h>
#include <dbus/mock_object_proxy.h>
#include <featured/proto_bindings/featured.pb.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "featured/feature_library.h"
namespace {
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
} // namespace
namespace feature {
class FeatureLibraryTest : public testing::Test {
protected:
FeatureLibraryTest()
: mock_bus_(new dbus::MockBus{dbus::Bus::Options{}}),
mock_chrome_proxy_(new dbus::MockObjectProxy(
mock_bus_.get(),
chromeos::kChromeFeaturesServiceName,
dbus::ObjectPath(chromeos::kChromeFeaturesServicePath))),
mock_feature_proxy_(new dbus::MockObjectProxy(
mock_bus_.get(),
kFeatureLibInterface,
dbus::ObjectPath(kFeatureLibServicePath))) {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
active_trials_dir_ = dir_.GetPath().Append("active_trials");
EXPECT_TRUE(base::CreateDirectory(active_trials_dir_));
}
void SetUp() override {
PlatformFeatures::InitializeForTesting(mock_bus_, mock_chrome_proxy_.get(),
mock_feature_proxy_.get());
features_ = PlatformFeatures::Get();
features_->SetActiveTrialFileDirectoryForTesting(active_trials_dir_);
}
void TearDown() override { PlatformFeatures::ShutdownForTesting(); }
base::expected<std::unique_ptr<dbus::Response>, dbus::Error>
CreateIsEnabledResponse(dbus::MethodCall* call, bool enabled) {
if (call->GetInterface() == "org.chromium.ChromeFeaturesServiceInterface" &&
call->GetMember() == "IsFeatureEnabled") {
std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(enabled);
return base::ok(std::move(response));
}
LOG(ERROR) << "Unexpected method call " << call->ToString();
return base::unexpected(dbus::Error());
}
base::expected<std::unique_ptr<dbus::Response>, dbus::Error>
CreateGetParamsResponse(
dbus::MethodCall* call,
std::map<std::string, std::map<std::string, std::string>> params_map,
std::map<std::string, bool> enabled_map) {
if (call->GetInterface() == "org.chromium.ChromeFeaturesServiceInterface" &&
call->GetMember() == "GetFeatureParams") {
dbus::MessageReader reader(call);
dbus::MessageReader array_reader(nullptr);
if (!reader.PopArray(&array_reader)) {
LOG(ERROR) << "Failed to read array of feature names.";
return base::unexpected(dbus::Error());
}
std::vector<std::string> input_features;
while (array_reader.HasMoreData()) {
std::string feature_name;
if (!array_reader.PopString(&feature_name)) {
LOG(ERROR) << "Failed to pop feature_name from array.";
return base::unexpected(dbus::Error());
}
input_features.push_back(feature_name);
}
std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
// Copied from chrome_features_service_provider.cc.
dbus::MessageWriter array_writer(nullptr);
// A map from feature name to:
// * two booleans:
// * Whether to use the override (or the default),
// * What the override state is (only valid if we should use the
// override value).
// * Another map, from parameter name to value.
writer.OpenArray("{s(bba{ss})}", &array_writer);
for (const auto& feature_name : input_features) {
dbus::MessageWriter feature_dict_writer(nullptr);
array_writer.OpenDictEntry(&feature_dict_writer);
feature_dict_writer.AppendString(feature_name);
dbus::MessageWriter struct_writer(nullptr);
feature_dict_writer.OpenStruct(&struct_writer);
if (enabled_map.find(feature_name) != enabled_map.end()) {
struct_writer.AppendBool(true); // Use override
struct_writer.AppendBool(enabled_map[feature_name]);
} else {
struct_writer.AppendBool(false); // Ignore override
struct_writer.AppendBool(false); // Arbitrary choice
}
dbus::MessageWriter sub_array_writer(nullptr);
struct_writer.OpenArray("{ss}", &sub_array_writer);
if (params_map.find(feature_name) != params_map.end()) {
const auto& submap = params_map[feature_name];
for (const auto& [key, value] : submap) {
dbus::MessageWriter dict_writer(nullptr);
sub_array_writer.OpenDictEntry(&dict_writer);
dict_writer.AppendString(key);
dict_writer.AppendString(value);
sub_array_writer.CloseContainer(&dict_writer);
}
}
struct_writer.CloseContainer(&sub_array_writer);
feature_dict_writer.CloseContainer(&struct_writer);
array_writer.CloseContainer(&feature_dict_writer);
}
writer.CloseContainer(&array_writer);
return base::ok(std::move(response));
}
LOG(ERROR) << "Unexpected method call " << call->ToString();
return base::unexpected(dbus::Error());
}
base::test::SingleThreadTaskEnvironment task_environment_;
scoped_refptr<dbus::MockBus> mock_bus_;
scoped_refptr<dbus::MockObjectProxy> mock_chrome_proxy_;
scoped_refptr<dbus::MockObjectProxy> mock_feature_proxy_;
PlatformFeatures* features_;
std::unique_ptr<base::RunLoop> run_loop_;
base::FilePath active_trials_dir_;
private:
base::ScopedTempDir dir_;
};
// Parameterized tests, with a boolean indicating whether the feature should be
// enabled.
class FeatureLibraryParameterizedTest
: public FeatureLibraryTest,
public ::testing::WithParamInterface<bool> {};
INSTANTIATE_TEST_SUITE_P(FeatureLibraryParameterizedTest,
FeatureLibraryParameterizedTest,
testing::Values(true, false));
TEST_P(FeatureLibraryParameterizedTest, IsEnabled_Success) {
bool enabled = GetParam();
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke(
[this, enabled](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::unique_ptr<dbus::Response> resp =
CreateIsEnabledResponse(call, enabled).value_or(nullptr);
std::move(*callback).Run(resp.get());
}));
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f{"Feature", FEATURE_DISABLED_BY_DEFAULT};
features_->IsEnabled(f,
base::BindLambdaForTesting([this, enabled](bool actual) {
EXPECT_EQ(enabled, actual);
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabled_Failure_WaitForService) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(false); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.Times(0);
run_loop_ = std::make_unique<base::RunLoop>();
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
features_->IsEnabled(f,
base::BindLambdaForTesting([this, enabled](bool actual) {
EXPECT_EQ(enabled, actual);
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabled_Failure_NullResponse) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::move(*callback).Run(nullptr);
}));
run_loop_ = std::make_unique<base::RunLoop>();
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
features_->IsEnabled(f,
base::BindLambdaForTesting([this, enabled](bool actual) {
EXPECT_EQ(enabled, actual);
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabled_Failure_EmptyResponse) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke(
[&response](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::move(*callback).Run(response.get());
}));
run_loop_ = std::make_unique<base::RunLoop>();
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
features_->IsEnabled(f,
base::BindLambdaForTesting([this, enabled](bool actual) {
EXPECT_EQ(enabled, actual);
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabledBlocking_Success) {
bool enabled = GetParam();
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([this, enabled](dbus::MethodCall* call, int timeout_ms) {
return CreateIsEnabledResponse(call, enabled);
}));
VariationsFeature f{"Feature", FEATURE_DISABLED_BY_DEFAULT};
EXPECT_EQ(enabled, features_->IsEnabledBlocking(f));
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabledBlocking_Failure_Null) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return base::unexpected(dbus::Error());
}));
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
EXPECT_EQ(enabled, features_->IsEnabledBlocking(f));
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabledBlocking_Failure_Empty) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return dbus::Response::CreateEmpty();
}));
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
EXPECT_EQ(enabled, features_->IsEnabledBlocking(f));
}
TEST_P(FeatureLibraryParameterizedTest, IsEnabledBlockingWithTimeout_Success) {
int timeout = 100;
bool enabled = GetParam();
EXPECT_CALL(*mock_chrome_proxy_, CallMethodAndBlock(_, timeout))
.WillOnce(Invoke([this, enabled](dbus::MethodCall* call, int timeout_ms) {
return CreateIsEnabledResponse(call, enabled);
}));
VariationsFeature f{"Feature", FEATURE_DISABLED_BY_DEFAULT};
EXPECT_EQ(enabled, features_->IsEnabledBlockingWithTimeout(f, timeout));
}
TEST_P(FeatureLibraryParameterizedTest,
IsEnabledBlockingWithTimeout_Failure_Null) {
int timeout = 100;
EXPECT_CALL(*mock_chrome_proxy_, CallMethodAndBlock(_, timeout))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return base::unexpected(dbus::Error());
}));
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
EXPECT_EQ(enabled, features_->IsEnabledBlockingWithTimeout(f, timeout));
}
TEST_P(FeatureLibraryParameterizedTest,
IsEnabledBlockingWithTimeout_Failure_Empty) {
int timeout = 100;
EXPECT_CALL(*mock_chrome_proxy_, CallMethodAndBlock(_, timeout))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return base::ok(dbus::Response::CreateEmpty());
}));
bool enabled = GetParam();
FeatureState feature_state =
GetParam() ? FEATURE_ENABLED_BY_DEFAULT : FEATURE_DISABLED_BY_DEFAULT;
VariationsFeature f{"Feature", feature_state};
EXPECT_EQ(enabled, features_->IsEnabledBlockingWithTimeout(f, timeout));
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabled_Success) {
// Will be enabled with params.
VariationsFeature f1{"Feature1", FEATURE_DISABLED_BY_DEFAULT};
// Will be explicitly disabled (and hence no params).
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
// Will be default state (and hence no params).
VariationsFeature f3{"Feature3", FEATURE_DISABLED_BY_DEFAULT};
// Will be explicitly disabled (and hence no params).
VariationsFeature f4{"Feature4", FEATURE_ENABLED_BY_DEFAULT};
// Will be enabled with *no* params
VariationsFeature f5{"Feature5", FEATURE_DISABLED_BY_DEFAULT};
// Will be enabled by default with *no* params
VariationsFeature f6{"Feature6", FEATURE_ENABLED_BY_DEFAULT};
std::map<std::string, std::map<std::string, std::string>> params_map{
{f1.name, {{"key", "value"}, {"anotherkey", "anothervalue"}}},
};
std::map<std::string, bool> enabled_map{
{f1.name, true},
{f2.name, false},
// f3 is default
{f4.name, false},
{f5.name, true},
// f6 is default
};
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke([this, enabled_map, params_map](
dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::unique_ptr<dbus::Response> resp =
CreateGetParamsResponse(call, params_map, enabled_map)
.value_or(nullptr);
std::move(*callback).Run(resp.get());
}));
run_loop_ = std::make_unique<base::RunLoop>();
PlatformFeaturesInterface::ParamsResult expected{
{
f1.name,
{
.enabled = true,
.params = params_map[f1.name],
},
},
{
f2.name,
{
.enabled = false,
},
},
{
f3.name,
{
.enabled = false,
},
},
{
f4.name,
{
.enabled = false,
},
},
{
f5.name,
{
.enabled = true,
},
},
{
f6.name,
{
.enabled = true,
},
}};
features_->GetParamsAndEnabled(
{&f1, &f2, &f3, &f4, &f5, &f6},
base::BindLambdaForTesting(
[this, expected](PlatformFeaturesInterface::ParamsResult actual) {
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabled_Failure_WaitForService) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(false); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.Times(0);
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
features_->GetParamsAndEnabled(
{&f1, &f2},
base::BindLambdaForTesting(
[this, expected](PlatformFeaturesInterface::ParamsResult actual) {
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabled_Failure_Null) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::move(*callback).Run(nullptr);
}));
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
features_->GetParamsAndEnabled(
{&f1, &f2},
base::BindLambdaForTesting(
[this, expected](PlatformFeaturesInterface::ParamsResult actual) {
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabled_Failure_Empty) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke(
[&response](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::move(*callback).Run(response.get());
}));
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
features_->GetParamsAndEnabled(
{&f1, &f2},
base::BindLambdaForTesting(
[this, expected](PlatformFeaturesInterface::ParamsResult actual) {
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
run_loop_->Quit();
}));
run_loop_->Run();
}
// Invalid response should result in default values.
TEST_F(FeatureLibraryTest, GetParamsAndEnabled_Failure_Invalid) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(true); }));
std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
writer.AppendBool(true);
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.WillOnce(Invoke(
[&response](dbus::MethodCall* call, int timeout_ms,
dbus::MockObjectProxy::ResponseCallback* callback) {
std::move(*callback).Run(response.get());
}));
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
features_->GetParamsAndEnabled(
{&f1, &f2},
base::BindLambdaForTesting(
[this, expected](PlatformFeaturesInterface::ParamsResult actual) {
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
run_loop_->Quit();
}));
run_loop_->Run();
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabledBlocking) {
// Will be enabled with params.
VariationsFeature f1{"Feature1", FEATURE_DISABLED_BY_DEFAULT};
// Will be explicitly disabled (and hence no params).
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
// Will be default state (and hence no params).
VariationsFeature f3{"Feature3", FEATURE_DISABLED_BY_DEFAULT};
// Will be explicitly disabled (and hence no params).
VariationsFeature f4{"Feature4", FEATURE_ENABLED_BY_DEFAULT};
// Will be enabled with *no* params
VariationsFeature f5{"Feature5", FEATURE_DISABLED_BY_DEFAULT};
// Will be enabled by default with *no* params
VariationsFeature f6{"Feature6", FEATURE_ENABLED_BY_DEFAULT};
std::map<std::string, std::map<std::string, std::string>> params_map{
{f1.name, {{"key", "value"}, {"anotherkey", "anothervalue"}}},
};
std::map<std::string, bool> enabled_map{
{f1.name, true},
{f2.name, false},
// f3 is default
{f4.name, false},
{f5.name, true},
// f6 is default
};
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([this, enabled_map, params_map](dbus::MethodCall* call,
int timeout_ms) {
return CreateGetParamsResponse(call, params_map, enabled_map);
}));
PlatformFeaturesInterface::ParamsResult expected{
{
f1.name,
{
.enabled = true,
.params = params_map[f1.name],
},
},
{
f2.name,
{
.enabled = false,
},
},
{
f3.name,
{
.enabled = false,
},
},
{
f4.name,
{
.enabled = false,
},
},
{
f5.name,
{
.enabled = true,
},
},
{
f6.name,
{
.enabled = true,
},
}};
auto actual =
features_->GetParamsAndEnabledBlocking({&f1, &f2, &f3, &f4, &f5, &f6});
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabledBlocking_Failure_Null) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return base::unexpected(dbus::Error());
}));
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
auto actual = features_->GetParamsAndEnabledBlocking({&f1, &f2});
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
}
TEST_F(FeatureLibraryTest, GetParamsAndEnabledBlocking_Failure_Empty) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
return base::ok(dbus::Response::CreateEmpty());
}));
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
auto actual = features_->GetParamsAndEnabledBlocking({&f1, &f2});
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
}
// Invalid response should result in default values.
TEST_F(FeatureLibraryTest, GetParamsAndEnabledBlocking_Failure_Invalid) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.WillOnce(Invoke([](dbus::MethodCall* call, int timeout_ms) {
std::unique_ptr<dbus::Response> response =
dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
writer.AppendBool(true);
return base::ok(std::move(response));
}));
VariationsFeature f1{"Feature1", FEATURE_ENABLED_BY_DEFAULT};
VariationsFeature f2{"Feature2", FEATURE_DISABLED_BY_DEFAULT};
PlatformFeaturesInterface::ParamsResult expected{{
f1.name,
{
.enabled = true,
},
},
{
f2.name,
{
.enabled = false,
},
}};
auto actual = features_->GetParamsAndEnabledBlocking({&f1, &f2});
EXPECT_EQ(actual.size(), expected.size());
for (const auto& [name, entry] : actual) {
auto it = expected.find(name);
ASSERT_NE(it, expected.end()) << name;
EXPECT_EQ(entry.enabled, it->second.enabled) << name;
EXPECT_EQ(entry.params, it->second.params) << name;
}
}
TEST_F(FeatureLibraryTest, CheckFeatureIdentity) {
VariationsFeature f1{"Feature", FEATURE_ENABLED_BY_DEFAULT};
// A new, unseen feature should pass the check.
EXPECT_TRUE(features_->CheckFeatureIdentity(f1));
// As should a feature seen a second time.
EXPECT_TRUE(features_->CheckFeatureIdentity(f1));
VariationsFeature f2{"Feature", FEATURE_ENABLED_BY_DEFAULT};
// A separate feature with the same name should fail.
EXPECT_FALSE(features_->CheckFeatureIdentity(f2));
VariationsFeature f3{"Feature3", FEATURE_ENABLED_BY_DEFAULT};
// A distinct feature with a distinct name should pass.
EXPECT_TRUE(features_->CheckFeatureIdentity(f3));
EXPECT_TRUE(features_->CheckFeatureIdentity(f3));
}
// Test that registering the signal handler registers the right signal handler,
// and that success is appropriately reported.
TEST_F(FeatureLibraryTest, RegisterSignalHandler_Success) {
EXPECT_CALL(*mock_feature_proxy_,
DoConnectToSignal(kFeatureLibInterface, kRefetchSignal, _, _))
.WillOnce([](const std::string& interface, const std::string& signal,
dbus::ObjectProxy::SignalCallback signal_cb,
dbus::ObjectProxy::OnConnectedCallback* on_connected) {
std::move(*on_connected).Run(interface, signal, true);
});
bool ran = false;
bool result = false;
features_->ListenForRefetchNeeded(
base::DoNothing(),
base::BindLambdaForTesting([&ran, &result](bool success) {
result = success;
ran = true;
}));
EXPECT_TRUE(ran);
EXPECT_TRUE(result);
}
// Test that registering the signal handler registers the right signal handler,
// and that failure is appropriately reported.
TEST_F(FeatureLibraryTest, RegisterSignalHandler_Failure) {
EXPECT_CALL(*mock_feature_proxy_,
DoConnectToSignal(kFeatureLibInterface, kRefetchSignal, _, _))
.WillOnce([](const std::string& interface, const std::string& signal,
dbus::ObjectProxy::SignalCallback signal_cb,
dbus::ObjectProxy::OnConnectedCallback* on_connected) {
std::move(*on_connected).Run(interface, signal, false);
});
bool ran = false;
bool result = false;
features_->ListenForRefetchNeeded(
base::DoNothing(),
base::BindLambdaForTesting([&ran, &result](bool success) {
result = success;
ran = true;
}));
EXPECT_TRUE(ran);
EXPECT_FALSE(result);
}
// Test that an active trial file is written.
TEST_F(FeatureLibraryTest, RecordSingleActiveTrial) {
featured::FeatureOverride trial;
trial.set_trial_name("test_trial");
trial.set_group_name("test_group");
features_->RecordActiveTrial(trial);
size_t num_found_files = 0;
base::FileEnumerator e(active_trials_dir_, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath file = e.Next(); !file.empty(); file = e.Next()) {
EXPECT_EQ(file.BaseName(), base::FilePath("test_trial,test_group"));
++num_found_files;
}
EXPECT_EQ(num_found_files, 1);
}
// Test that multiple active feature files are written for non-duplicate
// features.
TEST_F(FeatureLibraryTest, RecordMultipleActiveTrials) {
featured::FeatureOverride f1;
f1.set_trial_name("test_trial_1");
f1.set_group_name("test_group_1");
features_->RecordActiveTrial(f1);
featured::FeatureOverride f2;
f2.set_trial_name("test_trial_2");
f2.set_group_name("test_group_2");
features_->RecordActiveTrial(f2);
std::vector<base::FilePath> expected_files;
expected_files.push_back(base::FilePath("test_trial_1,test_group_1"));
expected_files.push_back(base::FilePath("test_trial_2,test_group_2"));
size_t num_found_files = 0;
base::FileEnumerator e(active_trials_dir_, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath file = e.Next(); !file.empty(); file = e.Next()) {
EXPECT_TRUE(base::Contains(expected_files, file.BaseName()));
++num_found_files;
}
EXPECT_EQ(num_found_files, expected_files.size());
}
// Test that only one active feature file is written when recording duplicate
// active features.
TEST_F(FeatureLibraryTest, RecordDuplicateActiveTrialOnlyOnce) {
featured::FeatureOverride trial;
trial.set_trial_name("test_trial");
trial.set_group_name("test_group");
features_->RecordActiveTrial(trial);
features_->RecordActiveTrial(trial);
size_t num_found_files = 0;
base::FileEnumerator e(active_trials_dir_, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath file = e.Next(); !file.empty(); file = e.Next()) {
EXPECT_EQ(file.BaseName(), base::FilePath("test_trial,test_group"));
++num_found_files;
}
EXPECT_EQ(num_found_files, 1);
}
// Test that trial names with kTrialGroupSeparator get escaped.
TEST_F(FeatureLibraryTest, EscapeTrialNameWithSeparator) {
featured::FeatureOverride trial;
trial.set_trial_name("test_trial,");
trial.set_group_name("test_group");
features_->RecordActiveTrial(trial);
size_t num_found_files = 0;
base::FileEnumerator e(active_trials_dir_, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath file = e.Next(); !file.empty(); file = e.Next()) {
EXPECT_EQ(file.BaseName(), base::FilePath("test_trial%2C,test_group"));
++num_found_files;
}
EXPECT_EQ(num_found_files, 1);
}
// Test that trial names with forward slashes are escaped and created as files,
// not as subdirectories.
TEST_F(FeatureLibraryTest, EscapeTrialNameWithForwardSlash) {
featured::FeatureOverride trial;
trial.set_trial_name("test_trial/");
trial.set_group_name("test_group");
features_->RecordActiveTrial(trial);
size_t num_found_files = 0;
base::FileEnumerator e(active_trials_dir_, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath file = e.Next(); !file.empty(); file = e.Next()) {
EXPECT_EQ(file.BaseName(), base::FilePath("test_trial%2F,test_group"));
++num_found_files;
}
EXPECT_EQ(num_found_files, 1);
}
#if DCHECK_IS_ON()
using FeatureLibraryDeathTest = FeatureLibraryTest;
TEST_F(FeatureLibraryDeathTest, IsEnabledDistinctFeatureDefs) {
EXPECT_CALL(*mock_chrome_proxy_, DoWaitForServiceToBeAvailable(_))
.WillOnce(
Invoke([](dbus::MockObjectProxy::WaitForServiceToBeAvailableCallback*
callback) { std::move(*callback).Run(false); }));
EXPECT_CALL(*mock_chrome_proxy_,
DoCallMethod(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, _))
.Times(0);
run_loop_ = std::make_unique<base::RunLoop>();
VariationsFeature f{"Feature", FEATURE_ENABLED_BY_DEFAULT};
features_->IsEnabled(f, base::BindLambdaForTesting([this](bool enabled) {
EXPECT_TRUE(enabled); // Default value
run_loop_->Quit();
}));
run_loop_->Run();
VariationsFeature f2{"Feature", FEATURE_ENABLED_BY_DEFAULT};
EXPECT_DEATH(
features_->IsEnabled(f2, base::BindLambdaForTesting([this](bool enabled) {
EXPECT_TRUE(enabled); // Default value
run_loop_->Quit();
})),
"Feature");
}
TEST_F(FeatureLibraryDeathTest, IsEnabledBlockingDistinctFeatureDefs) {
EXPECT_CALL(*mock_chrome_proxy_,
CallMethodAndBlock(_, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT))
.Times(1);
VariationsFeature f{"Feature", FEATURE_ENABLED_BY_DEFAULT};
features_->IsEnabledBlocking(f);
VariationsFeature f2{"Feature", FEATURE_ENABLED_BY_DEFAULT};
EXPECT_DEATH(features_->IsEnabledBlocking(f2), "Feature");
}
#endif // DCHECK_IS_ON()
} // namespace feature