| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/location.h" |
| #include "base/process/launch.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/login/test/session_manager_state_waiter.h" |
| #include "chrome/test/base/chromeos/crosier/ash_integration_test.h" |
| #include "chrome/test/base/chromeos/crosier/chromeos_integration_login_mixin.h" |
| #include "chrome/test/base/chromeos/crosier/chromeos_test_definition.pb.h" |
| #include "chrome/test/base/chromeos/crosier/crosier_util.h" |
| #include "chrome/test/base/chromeos/crosier/helper/test_sudo_helper_client.h" |
| #include "chrome/test/base/chromeos/crosier/upstart.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace ash { |
| namespace { |
| |
| constexpr char kHelperBinary[] = |
| "/usr/local/libexec/tast/helpers/local/cros/" |
| "featured.FeatureLibraryLateBoot.check"; |
| |
| // The tests start the chromeos_integration_tests binary with two features |
| // enabled or disabled, with or without parameters, simulating a Finch |
| // experiment. One feature is default-enabled; the other feature is |
| // default-disabled. The helper binary (a simulation of a platform binary) |
| // returns its view of the feature state as a JSON object. The test cases have |
| // expected output for the default-enabled and default-disabled features. |
| struct TestCase { |
| const char* test_name = ""; |
| const char* enabled_features = ""; |
| const char* disabled_features = ""; |
| const char* expected_default_enabled = ""; |
| const char* expected_default_disabled = ""; |
| }; |
| |
| class FeaturedIntegrationTest : public AshIntegrationTest, |
| public ::testing::WithParamInterface<TestCase> { |
| public: |
| FeaturedIntegrationTest() { |
| feature_list_.InitFromCommandLine(GetParam().enabled_features, |
| GetParam().disabled_features); |
| } |
| |
| // AshIntegrationTest: |
| void SetUpOnMainThread() override { |
| AshIntegrationTest::SetUpOnMainThread(); |
| |
| chrome_test_base_chromeos_crosier::TestInfo info; |
| info.set_description( |
| "Verifies features are enabled/disabled as expected and parameters are " |
| "unchanged"); |
| info.set_team_email("chromeos-data-eng@google.com"); |
| info.add_contacts("jamescook@google.com"); // Ported from Tast to Crosier. |
| info.set_buganizer("1096648"); |
| crosier_util::AddTestInfo(info); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| constexpr TestCase kTestCases[] = { |
| {.test_name = "experiment_enabled_without_params", |
| .enabled_features = |
| "CrOSLateBootTestDefaultEnabled,CrOSLateBootTestDefaultDisabled", |
| .expected_default_enabled = R"( |
| { |
| "EnabledCallbackEnabledResult": true, |
| "FeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackEnabledResult": true, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )", |
| .expected_default_disabled = R"( |
| { |
| "EnabledCallbackEnabledResult": true, |
| "FeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackEnabledResult": true, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )"}, |
| {.test_name = "experiment_disabled_without_params", |
| .disabled_features = |
| "CrOSLateBootTestDefaultEnabled,CrOSLateBootTestDefaultDisabled", |
| .expected_default_enabled = R"( |
| { |
| "EnabledCallbackEnabledResult": false, |
| "FeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackEnabledResult": false, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )", |
| .expected_default_disabled = R"( |
| { |
| "EnabledCallbackEnabledResult": false, |
| "FeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackEnabledResult": false, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )"}, |
| {.test_name = "experiment_enabled_with_params", |
| .enabled_features = "CrOSLateBootTestDefaultEnabled:k1/v1/k2/v2," |
| "CrOSLateBootTestDefaultDisabled:k3/v3/k4/v4", |
| .expected_default_enabled = R"( |
| { |
| "EnabledCallbackEnabledResult": true, |
| "FeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackEnabledResult": true, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackParamsResult": { |
| "k1":"v1", |
| "k2":"v2" |
| } |
| } |
| )", |
| .expected_default_disabled = R"( |
| { |
| "EnabledCallbackEnabledResult": true, |
| "FeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackEnabledResult": true, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackParamsResult": { |
| "k3":"v3", |
| "k4":"v4" |
| } |
| } |
| )"}, |
| {.test_name = "experiment_default", |
| .enabled_features = "", |
| .expected_default_enabled = R"( |
| { |
| "EnabledCallbackEnabledResult": true, |
| "FeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackEnabledResult": true, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultEnabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )", |
| .expected_default_disabled = R"( |
| { |
| "EnabledCallbackEnabledResult": false, |
| "FeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackEnabledResult": false, |
| "ParamsCallbackFeatureName": "CrOSLateBootTestDefaultDisabled", |
| "ParamsCallbackParamsResult": {} |
| } |
| )"}, |
| |
| }; |
| |
| const char* NameFromTestCase(::testing::TestParamInfo<TestCase> info) { |
| return info.param.test_name; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| FeaturedIntegrationTest, |
| ::testing::ValuesIn(kTestCases), |
| NameFromTestCase); |
| |
| IN_PROC_BROWSER_TEST_P(FeaturedIntegrationTest, FeatureLibraryLateBoot) { |
| // Collect stdout from the helper binary. The binary will call back via D-Bus |
| // into chromeos_integration_tests, so collect the binary's output off the |
| // main thread. |
| std::string output; |
| base::RunLoop run_loop; |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce( |
| [](std::string* output, base::RunLoop* run_loop) { |
| // Run the helper binary to get its feature state. |
| ASSERT_TRUE(base::GetAppOutput({kHelperBinary}, output)); |
| run_loop->Quit(); |
| }, |
| &output, &run_loop)); |
| run_loop.Run(); |
| |
| // The helper binary returns two lines of output. |
| std::vector<std::string> split_out = base::SplitString( |
| output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| ASSERT_EQ(split_out.size(), 2u); |
| |
| // The first line of the output is the state of the default enabled feature. |
| std::optional<base::Value> default_enabled = |
| base::JSONReader::Read(split_out[0]); |
| ASSERT_TRUE(default_enabled.has_value()); |
| |
| // The test case has the expected JSON for the default enabled feature. |
| std::optional<base::Value> expected_default_enabled = |
| base::JSONReader::Read(GetParam().expected_default_enabled); |
| ASSERT_TRUE(expected_default_enabled.has_value()); |
| EXPECT_EQ(default_enabled, expected_default_enabled); |
| |
| // The second line of the output is the default disabled feature. |
| std::optional<base::Value> default_disabled = |
| base::JSONReader::Read(split_out[1]); |
| ASSERT_TRUE(default_disabled.has_value()); |
| |
| // The test case has the expected JSON for the default disabled feature. |
| std::optional<base::Value> expected_default_disabled = |
| base::JSONReader::Read(GetParam().expected_default_disabled); |
| ASSERT_TRUE(expected_default_disabled.has_value()); |
| EXPECT_EQ(default_disabled, expected_default_disabled); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // FeaturedLatePlatformIntegrationTest |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // kTestFeature is the name of the test feature in platform-features.json. |
| // It should always match the name in that file exactly. |
| constexpr char kTestFeature[] = "CrOSLateBootTestFeature"; |
| |
| // kDirPath is the path to the directory this test uses. |
| constexpr char kDirPath[] = "/run/featured_test"; |
| |
| // kFilePath is the file whose existence gates the behavior of featured. |
| // If it exists and the experiment is enabled, featured should write a string to |
| // it. Otherwise, it should do nothing. |
| constexpr char kFilePath[] = "/run/featured_test/test_write"; |
| |
| // kExpectedContents is the expected contents of the filePath after featured |
| // writes to it. |
| constexpr char kExpectedContents[] = "test_featured"; |
| |
| class FeaturedLatePlatformIntegrationTest |
| : public AshIntegrationTest, |
| public testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| // Whether the test file should be created by the test. |
| static bool TestFileExists() { return std::get<0>(GetParam()); } |
| |
| // Whether the experimental feature should be enabled in chrome. |
| static bool TestFeatureEnabled() { return std::get<1>(GetParam()); } |
| |
| FeaturedLatePlatformIntegrationTest() { |
| // Don't enable or disable the test feature in the PRE_ test stage. Only do |
| // it in the main test stage. Ditto for login. |
| if (base::StartsWith( |
| testing::UnitTest::GetInstance()->current_test_info()->name(), |
| "PRE_")) { |
| return; |
| } |
| if (TestFeatureEnabled()) { |
| feature_list_.InitFromCommandLine(/*enable_features=*/kTestFeature, |
| /*disable_features=*/""); |
| } else { |
| feature_list_.InitFromCommandLine(/*enable_features=*/"", |
| /*disable_features=*/kTestFeature); |
| } |
| set_exit_when_last_browser_closes(false); |
| login_mixin().SetMode(ChromeOSIntegrationLoginMixin::Mode::kTestLogin); |
| } |
| |
| void TearDownOnMainThread() override { |
| // Don't clean up files in the PRE_ test stage because they must continue to |
| // exist for the main test stage. |
| if (base::StartsWith( |
| testing::UnitTest::GetInstance()->current_test_info()->name(), |
| "PRE_")) { |
| return; |
| } |
| |
| // The directory is owned by root, so use the sudo helper. |
| auto result = TestSudoHelperClient().RunCommand( |
| base::StringPrintf("rm -rf %s", kDirPath)); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| |
| AshIntegrationTest::TearDownOnMainThread(); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(FileFeature, |
| FeaturedLatePlatformIntegrationTest, |
| testing::Combine(testing::Bool(), testing::Bool())); |
| |
| IN_PROC_BROWSER_TEST_P(FeaturedLatePlatformIntegrationTest, |
| PRE_LatePlatformFeatures) { |
| // Use sudo helper because /run is owned by root. |
| TestSudoHelperClient sudo; |
| auto result = sudo.RunCommand(base::StringPrintf("mkdir -p %s", kDirPath)); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| result = sudo.RunCommand(base::StringPrintf("chmod 755 %s", kDirPath)); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| |
| base::FilePath file_path(kFilePath); |
| if (TestFileExists()) { |
| // Create a zero-sized file. |
| result = sudo.RunCommand(base::StringPrintf("truncate -s 0 %s", kFilePath)); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| |
| result = sudo.RunCommand(base::StringPrintf("chmod 664 %s", kFilePath)); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| } else { |
| // Ensure the file does not exist. |
| result = sudo.RunCommand("rm -f /run/featured_test/test_write"); |
| ASSERT_EQ(result.return_code, 0) << result.output; |
| } |
| |
| // Restart featured. |
| ASSERT_TRUE(upstart::RestartJob("featured")); |
| ASSERT_TRUE(upstart::WaitForJobStatus("featured", upstart::Goal::kStart, |
| upstart::State::kRunning, |
| upstart::WrongGoalPolicy::kReject)); |
| |
| // Restart chrome (by ending this PRE_ test). |
| } |
| |
| IN_PROC_BROWSER_TEST_P(FeaturedLatePlatformIntegrationTest, |
| LatePlatformFeatures) { |
| login_mixin().Login(); |
| ash::test::WaitForPrimaryUserSessionStart(); |
| EXPECT_TRUE(login_mixin().IsCryptohomeMounted()); |
| |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| // Poll for files for up to 20 seconds. |
| bool passed = false; |
| for (int i = 0; i < 20; ++i) { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), base::Seconds(1)); |
| run_loop.Run(); |
| |
| base::FilePath file_path(kFilePath); |
| if (TestFileExists()) { |
| // If testing the file exists case, the file should exist. Featured should |
| // not have removed the file. |
| ASSERT_TRUE(base::PathExists(file_path)); |
| std::string contents; |
| ASSERT_TRUE(base::ReadFileToString(file_path, &contents)); |
| if (TestFeatureEnabled()) { |
| // If the file contains the expected string, exit the loop. If not, keep |
| // polling. |
| if (contents == kExpectedContents) { |
| passed = true; |
| break; |
| } |
| } else { |
| // In the feature disabled case the File should be empty. If it is, |
| // exit the loop. Otherwise keep polling. |
| if (contents.empty()) { |
| passed = true; |
| break; |
| } |
| } |
| } else { |
| // If testing the file does-not-exist case, featured should not have |
| // created the file. |
| ASSERT_FALSE(base::PathExists(file_path)); |
| passed = true; |
| // Continue to poll, to ensure featured doesn't create the file later. |
| } |
| } |
| EXPECT_TRUE(passed) << "Did not pass after 20 iterations."; |
| } |
| |
| } // namespace |
| } // namespace ash |