| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/crostini/crostini_installer.h" |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/ash/crostini/ansible/ansible_management_test_helper.h" |
| #include "chrome/browser/ash/crostini/crostini_installer_ui_delegate.h" |
| #include "chrome/browser/ash/crostini/crostini_test_helper.h" |
| #include "chrome/browser/ash/crostini/crostini_types.mojom.h" |
| #include "chrome/browser/component_updater/fake_cros_component_manager.h" |
| #include "chrome/browser/notifications/system_notification_helper.h" |
| #include "chrome/test/base/browser_process_platform_part_test_api_chromeos.h" |
| #include "chrome/test/base/scoped_testing_local_state.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chromeos/dbus/cicerone/cicerone_client.h" |
| #include "chromeos/dbus/cicerone/fake_cicerone_client.h" |
| #include "chromeos/dbus/concierge/concierge_client.h" |
| #include "chromeos/dbus/concierge/concierge_service.pb.h" |
| #include "chromeos/dbus/concierge/fake_concierge_client.h" |
| #include "chromeos/dbus/dbus_thread_manager.h" |
| #include "chromeos/dbus/dlcservice/dlcservice_client.h" |
| #include "chromeos/dbus/seneschal/seneschal_client.h" |
| #include "chromeos/disks/disk_mount_manager.h" |
| #include "chromeos/disks/mock_disk_mount_manager.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| using crostini::mojom::InstallerError; |
| using crostini::mojom::InstallerState; |
| using testing::_; |
| using testing::AnyNumber; |
| using testing::Expectation; |
| using testing::ExpectationSet; |
| using testing::Invoke; |
| using testing::Le; |
| using testing::MockFunction; |
| using testing::SaveArg; |
| using testing::Truly; |
| |
| namespace crostini { |
| |
| class CrostiniInstallerTest : public testing::Test { |
| public: |
| using ResultCallback = CrostiniInstallerUIDelegate::ResultCallback; |
| using ProgressCallback = CrostiniInstallerUIDelegate::ProgressCallback; |
| |
| class MockCallbacks { |
| public: |
| MOCK_METHOD2(OnProgress, |
| void(InstallerState state, double progress_fraction)); |
| MOCK_METHOD1(OnFinished, void(InstallerError error)); |
| MOCK_METHOD0(OnCanceled, void()); |
| }; |
| |
| class WaitingFakeConciergeClient : public chromeos::FakeConciergeClient { |
| public: |
| explicit WaitingFakeConciergeClient(chromeos::FakeCiceroneClient* client) |
| : chromeos::FakeConciergeClient(client) {} |
| |
| void StartTerminaVm( |
| const vm_tools::concierge::StartVmRequest& request, |
| chromeos::DBusMethodCallback<vm_tools::concierge::StartVmResponse> |
| callback) override { |
| chromeos::FakeConciergeClient::StartTerminaVm(request, |
| std::move(callback)); |
| if (quit_closure_) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, |
| std::move(quit_closure_)); |
| } |
| } |
| |
| void WaitForStartTerminaVmCalled() { |
| base::RunLoop loop; |
| quit_closure_ = loop.QuitClosure(); |
| loop.Run(); |
| EXPECT_GE(start_termina_vm_call_count(), 1); |
| } |
| |
| private: |
| base::OnceClosure quit_closure_; |
| }; |
| |
| CrostiniInstallerTest() |
| : local_state_(std::make_unique<ScopedTestingLocalState>( |
| TestingBrowserProcess::GetGlobal())), |
| browser_part_(g_browser_process->platform_part()) {} |
| |
| void SetUp() override { |
| component_manager_ = |
| base::MakeRefCounted<component_updater::FakeCrOSComponentManager>(); |
| component_manager_->set_supported_components({"cros-termina"}); |
| component_manager_->ResetComponentState( |
| "cros-termina", |
| component_updater::FakeCrOSComponentManager::ComponentInfo( |
| component_updater::CrOSComponentManager::Error::NONE, |
| base::FilePath("/install/path"), base::FilePath("/mount/path"))); |
| browser_part_.InitializeCrosComponentManager(component_manager_); |
| |
| chromeos::DlcserviceClient::InitializeFake(); |
| chromeos::DBusThreadManager::Initialize(); |
| chromeos::CiceroneClient::InitializeFake(); |
| waiting_fake_concierge_client_ = |
| new WaitingFakeConciergeClient(chromeos::FakeCiceroneClient::Get()); |
| chromeos::SeneschalClient::InitializeFake(); |
| |
| disk_mount_manager_mock_ = new chromeos::disks::MockDiskMountManager; |
| chromeos::disks::DiskMountManager::InitializeForTesting( |
| disk_mount_manager_mock_); |
| |
| profile_ = std::make_unique<TestingProfile>(); |
| // Needed at least for passing IsCrostiniUIAllowedForProfile() test in |
| // CrostiniManager. |
| crostini_test_helper_ = |
| std::make_unique<CrostiniTestHelper>(profile_.get()); |
| |
| crostini_installer_ = std::make_unique<CrostiniInstaller>(profile_.get()); |
| crostini_installer_->set_skip_launching_terminal_for_testing(); |
| |
| g_browser_process->platform_part() |
| ->InitializeSchedulerConfigurationManager(); |
| TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper( |
| std::make_unique<SystemNotificationHelper>()); |
| } |
| |
| void TearDown() override { |
| g_browser_process->platform_part()->ShutdownSchedulerConfigurationManager(); |
| crostini_installer_->Shutdown(); |
| crostini_installer_.reset(); |
| crostini_test_helper_.reset(); |
| profile_.reset(); |
| |
| chromeos::disks::MockDiskMountManager::Shutdown(); |
| chromeos::SeneschalClient::Shutdown(); |
| chromeos::ConciergeClient::Shutdown(); |
| chromeos::CiceroneClient::Shutdown(); |
| chromeos::DBusThreadManager::Shutdown(); |
| chromeos::DlcserviceClient::Shutdown(); |
| |
| browser_part_.ShutdownCrosComponentManager(); |
| component_manager_.reset(); |
| } |
| |
| void Install() { |
| CrostiniManager::GetForProfile(profile_.get()) |
| ->SetCrostiniDialogStatus(DialogType::INSTALLER, true); |
| crostini_installer_->Install( |
| CrostiniManager::RestartOptions{}, |
| base::BindRepeating(&MockCallbacks::OnProgress, |
| base::Unretained(&mock_callbacks_)), |
| base::BindOnce(&MockCallbacks::OnFinished, |
| base::Unretained(&mock_callbacks_))); |
| } |
| |
| void Cancel() { |
| crostini_installer_->Cancel(base::BindOnce( |
| &MockCallbacks::OnCanceled, base::Unretained(&mock_callbacks_))); |
| } |
| |
| protected: |
| MockCallbacks mock_callbacks_; |
| content::BrowserTaskEnvironment task_environment_{ |
| content::BrowserTaskEnvironment::REAL_IO_THREAD, |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| base::HistogramTester histogram_tester_; |
| |
| // Owned by DiskMountManager |
| chromeos::disks::MockDiskMountManager* disk_mount_manager_mock_ = nullptr; |
| |
| WaitingFakeConciergeClient* waiting_fake_concierge_client_ = nullptr; |
| |
| std::unique_ptr<TestingProfile> profile_; |
| std::unique_ptr<CrostiniTestHelper> crostini_test_helper_; |
| std::unique_ptr<CrostiniInstaller> crostini_installer_; |
| |
| private: |
| std::unique_ptr<ScopedTestingLocalState> local_state_; |
| scoped_refptr<component_updater::FakeCrOSComponentManager> component_manager_; |
| BrowserProcessPlatformPartTestApi browser_part_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CrostiniInstallerTest); |
| }; |
| |
| TEST_F(CrostiniInstallerTest, InstallFlow) { |
| double last_progress = 0.0; |
| auto greater_equal_last_progress = Truly( |
| [&last_progress](double progress) { return progress >= last_progress; }); |
| |
| ExpectationSet expectation_set; |
| expectation_set += |
| EXPECT_CALL(mock_callbacks_, |
| OnProgress(_, AllOf(greater_equal_last_progress, Le(1.0)))) |
| .WillRepeatedly(SaveArg<1>(&last_progress)); |
| // |OnProgress()| should not happens after |OnFinished()| |
| EXPECT_CALL(mock_callbacks_, OnFinished(InstallerError::kNone)) |
| .After(expectation_set); |
| |
| Install(); |
| |
| task_environment_.RunUntilIdle(); |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kSuccess), |
| 1); |
| histogram_tester_.ExpectTotalCount("Crostini.Setup.Started", 1); |
| histogram_tester_.ExpectTotalCount("Crostini.Restarter.Started", 0); |
| |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| TEST_F(CrostiniInstallerTest, InstallFlowWithAnsibleInfra) { |
| AnsibleManagementTestHelper test_helper(profile_.get()); |
| test_helper.SetUpAnsibleInfra(); |
| |
| double last_progress = 0.0; |
| auto greater_equal_last_progress = Truly( |
| [&last_progress](double progress) { return progress >= last_progress; }); |
| |
| ExpectationSet expectation_set; |
| expectation_set += |
| EXPECT_CALL(mock_callbacks_, |
| OnProgress(_, AllOf(greater_equal_last_progress, Le(1.0)))) |
| .WillRepeatedly(SaveArg<1>(&last_progress)); |
| // |OnProgress()| should not happens after |OnFinished()| |
| EXPECT_CALL(mock_callbacks_, OnFinished(InstallerError::kNone)) |
| .After(expectation_set); |
| |
| Install(); |
| |
| task_environment_.RunUntilIdle(); |
| test_helper.SendSucceededApplySignal(); |
| |
| task_environment_.RunUntilIdle(); |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kSuccess), |
| 1); |
| |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| TEST_F(CrostiniInstallerTest, CancelBeforeStart) { |
| crostini_installer_->CancelBeforeStart(); |
| |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kNotStarted), |
| 1); |
| } |
| |
| TEST_F(CrostiniInstallerTest, CancelAfterStart) { |
| MockFunction<void(std::string check_point_name)> check; |
| |
| Expectation expect_progresses = |
| EXPECT_CALL(mock_callbacks_, OnProgress(_, _)).Times(AnyNumber()); |
| // |OnProgress()| should not happen after |Cancel()| is called. |
| Expectation expect_calling_cancel = |
| EXPECT_CALL(check, Call("calling Cancel()")).After(expect_progresses); |
| EXPECT_CALL(mock_callbacks_, OnCanceled()).After(expect_calling_cancel); |
| |
| Install(); |
| |
| // It is too involved to also fake the cleanup process in CrostiniManager, so |
| // we will just skip it. |
| crostini_installer_->set_require_cleanup_for_testing(false); |
| |
| // Hang the installer flow waiting for Tremplin to start, so we get a chance |
| // to cancel it. |
| waiting_fake_concierge_client_->set_send_tremplin_started_signal_delay( |
| base::TimeDelta::FromDays(1)); |
| task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); |
| |
| check.Call("calling Cancel()"); |
| Cancel(); |
| task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); |
| |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kUserCancelledStartTerminaVm), |
| 1); |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| // Cancel before |OnAvailableDiskSpace()| happens is a special case, in which we |
| // haven't started the restarting process yet. |
| TEST_F(CrostiniInstallerTest, CancelAfterStartBeforeCheckDisk) { |
| EXPECT_CALL(mock_callbacks_, OnCanceled()); |
| |
| crostini_installer_->Install(CrostiniManager::RestartOptions{}, |
| base::DoNothing(), base::DoNothing()); |
| Cancel(); // Cancel immediately |
| |
| task_environment_.RunUntilIdle(); |
| |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kNotStarted), |
| 1); |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| TEST_F(CrostiniInstallerTest, InstallerError) { |
| Expectation expect_progresses = |
| EXPECT_CALL(mock_callbacks_, OnProgress(_, _)).Times(AnyNumber()); |
| // |OnProgress()| should not happens after |OnFinished()| |
| EXPECT_CALL(mock_callbacks_, |
| OnFinished(InstallerError::kErrorStartingTermina)) |
| .After(expect_progresses); |
| |
| // Fake a failure for starting vm. |
| vm_tools::concierge::StartVmResponse response; |
| response.set_status(vm_tools::concierge::VM_STATUS_FAILURE); |
| waiting_fake_concierge_client_->set_start_vm_response(std::move(response)); |
| |
| Install(); |
| waiting_fake_concierge_client_->WaitForStartTerminaVmCalled(); |
| |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kErrorStartingTermina), |
| 1); |
| histogram_tester_.ExpectTotalCount("Crostini.Setup.Started", 1); |
| histogram_tester_.ExpectTotalCount("Crostini.Restarter.Started", 0); |
| |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| TEST_F(CrostiniInstallerTest, InstallerErrorWhileConfiguring) { |
| AnsibleManagementTestHelper test_helper(profile_.get()); |
| test_helper.SetUpAnsibleInfra(); |
| test_helper.SetUpAnsibleInstallation( |
| vm_tools::cicerone::InstallLinuxPackageResponse::FAILED); |
| |
| Expectation expect_progresses = |
| EXPECT_CALL(mock_callbacks_, OnProgress(_, _)).Times(AnyNumber()); |
| // |OnProgress()| should not happens after |OnFinished()| |
| EXPECT_CALL(mock_callbacks_, |
| OnFinished(InstallerError::kErrorConfiguringContainer)) |
| .After(expect_progresses); |
| |
| Install(); |
| |
| task_environment_.RunUntilIdle(); |
| histogram_tester_.ExpectUniqueSample( |
| "Crostini.SetupResult", |
| static_cast<base::HistogramBase::Sample>( |
| CrostiniInstaller::SetupResult::kErrorConfiguringContainer), |
| 1); |
| |
| EXPECT_TRUE(crostini_installer_->CanInstall()) |
| << "Installer should recover to installable state"; |
| } |
| |
| } // namespace crostini |