| // Copyright (c) 2012 The Chromium OS 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 "login_manager/liveness_checker_impl.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include <base/files/file.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/memory/ref_counted.h> |
| #include <base/time/time.h> |
| #include <brillo/message_loops/fake_message_loop.h> |
| #include <brillo/syslog_logging.h> |
| #include <dbus/message.h> |
| #include <dbus/mock_object_proxy.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "login_manager/mock_metrics.h" |
| #include "login_manager/mock_process_manager_service.h" |
| |
| using ::base::TimeDelta; |
| using ::testing::_; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::Return; |
| using ::testing::StrictMock; |
| |
| namespace login_manager { |
| |
| class LivenessCheckerImplTest : public ::testing::Test { |
| public: |
| LivenessCheckerImplTest() {} |
| LivenessCheckerImplTest(const LivenessCheckerImplTest&) = delete; |
| LivenessCheckerImplTest& operator=(const LivenessCheckerImplTest&) = delete; |
| |
| ~LivenessCheckerImplTest() override {} |
| |
| void SetUp() override { |
| fake_loop_.SetAsCurrent(); |
| manager_.reset(new StrictMock<MockProcessManagerService>); |
| object_proxy_ = |
| new dbus::MockObjectProxy(nullptr, "", dbus::ObjectPath("/fake/path")); |
| |
| ASSERT_TRUE(tmpdir_.CreateUniqueTempDir()); |
| |
| metrics_.reset(new MockMetrics()); |
| |
| checker_.reset(new LivenessCheckerImpl(manager_.get(), object_proxy_.get(), |
| true, base::Seconds(10), |
| metrics_.get())); |
| base::FilePath fake_proc_path(tmpdir_.GetPath()); |
| checker_->SetProcForTests(std::move(fake_proc_path)); |
| } |
| |
| void ExpectUnAckedLivenessPing() { |
| EXPECT_CALL(*object_proxy_.get(), DoCallMethod(_, _, _)).Times(1); |
| } |
| |
| // Expect two pings, the first with a response. |
| void ExpectLivenessPingResponsePing() { |
| EXPECT_CALL(*object_proxy_.get(), DoCallMethod(_, _, _)) |
| .WillOnce(Invoke(this, &LivenessCheckerImplTest::Respond)) |
| .WillOnce(Return()); |
| } |
| |
| // Expect three runs through CheckAndSendLivenessPing(): |
| // 1) No ping has been sent before, so expect initial ping and ACK it. |
| // 2) Last ping was ACK'd, so expect a no-op during this run. |
| // 3) Caller should expect action during this run; Quit after it. |
| void ExpectPingResponsePingCheckPingAndQuit() { |
| EXPECT_CALL(*object_proxy_.get(), DoCallMethod(_, _, _)) |
| .WillOnce(Invoke(this, &LivenessCheckerImplTest::Respond)) |
| .WillOnce(Return()) |
| .WillOnce(InvokeWithoutArgs(brillo::MessageLoop::current(), |
| &brillo::MessageLoop::BreakLoop)); |
| } |
| |
| brillo::FakeMessageLoop fake_loop_{nullptr}; |
| scoped_refptr<dbus::MockObjectProxy> object_proxy_; |
| std::unique_ptr<StrictMock<MockProcessManagerService>> manager_; |
| |
| std::unique_ptr<LivenessCheckerImpl> checker_; |
| |
| base::ScopedTempDir tmpdir_; |
| std::unique_ptr<MockMetrics> metrics_; |
| |
| private: |
| void Respond(dbus::MethodCall* method_call, |
| int timeout_ms, |
| dbus::ObjectProxy::ResponseCallback* callback) { |
| std::move(*callback).Run(dbus::Response::CreateEmpty().get()); |
| } |
| }; |
| |
| TEST_F(LivenessCheckerImplTest, CheckAndSendOutstandingPing) { |
| ExpectUnAckedLivenessPing(); |
| |
| // Expects one failure for the un-acked ping. |
| EXPECT_CALL(*metrics_, SendLivenessPingResult(/*succeess=*/false)).Times(1); |
| |
| EXPECT_CALL(*manager_.get(), AbortBrowserForHang()).Times(1); |
| EXPECT_CALL(*manager_.get(), GetBrowserPid()) |
| .WillRepeatedly(Return(std::nullopt)); |
| EXPECT_CALL(*metrics_, RecordStateForLivenessTimeout( |
| LoginMetrics::BrowserState::kErrorGettingState)) |
| .Times(1); |
| checker_->CheckAndSendLivenessPing(TimeDelta()); |
| fake_loop_.Run(); // Runs until the message loop is empty. |
| } |
| |
| TEST_F(LivenessCheckerImplTest, CheckAndSendAckedThenOutstandingPing) { |
| ExpectLivenessPingResponsePing(); |
| |
| // Expects one success for acked ping and one failure for the un-acked one. |
| EXPECT_CALL(*metrics_, SendLivenessPingResult(/*succeess=*/true)).Times(1); |
| EXPECT_CALL(*metrics_, SendLivenessPingResult(/*succeess=*/false)).Times(1); |
| |
| EXPECT_CALL(*manager_.get(), AbortBrowserForHang()).Times(1); |
| EXPECT_CALL(*manager_.get(), GetBrowserPid()) |
| .WillRepeatedly(Return(std::nullopt)); |
| EXPECT_CALL(*metrics_, RecordStateForLivenessTimeout( |
| LoginMetrics::BrowserState::kErrorGettingState)) |
| .Times(1); |
| checker_->CheckAndSendLivenessPing(TimeDelta()); |
| fake_loop_.Run(); // Runs until the message loop is empty. |
| } |
| |
| TEST_F(LivenessCheckerImplTest, CheckAndSendAckedThenOutstandingPingNeutered) { |
| checker_.reset(new LivenessCheckerImpl(manager_.get(), object_proxy_.get(), |
| false, // Disable aborting |
| base::Seconds(10), metrics_.get())); |
| base::FilePath fake_proc_path(tmpdir_.GetPath()); |
| checker_->SetProcForTests(std::move(fake_proc_path)); |
| |
| ExpectPingResponsePingCheckPingAndQuit(); |
| |
| // Expects one success for acked ping and one failure for the un-acked one. |
| EXPECT_CALL(*metrics_, SendLivenessPingResult(/*succeess=*/true)).Times(1); |
| EXPECT_CALL(*metrics_, SendLivenessPingResult(/*succeess=*/false)).Times(1); |
| |
| // Expect _no_ browser abort! |
| EXPECT_CALL(*manager_.get(), AbortBrowserForHang()).Times(0); |
| // But we still record the UMA. |
| EXPECT_CALL(*manager_.get(), GetBrowserPid()) |
| .WillRepeatedly(Return(std::nullopt)); |
| EXPECT_CALL(*metrics_, RecordStateForLivenessTimeout( |
| LoginMetrics::BrowserState::kErrorGettingState)) |
| .Times(1); |
| checker_->CheckAndSendLivenessPing(base::Seconds(1)); |
| fake_loop_.Run(); // Runs until the message loop is empty. |
| } |
| |
| TEST_F(LivenessCheckerImplTest, StartStop) { |
| checker_->Start(); |
| EXPECT_TRUE(checker_->IsRunning()); |
| checker_->Stop(); // Should cancel ping, so... |
| EXPECT_FALSE(checker_->IsRunning()); |
| } |
| |
| struct TestFileAndStatus { |
| const char* const test_name; |
| const char* const file_name; |
| LoginMetrics::BrowserState expected_state; |
| const char* const expected_log_message; |
| }; |
| |
| const TestFileAndStatus kTestFilesAndStatuses[] = { |
| {"Running", "TEST_STATUS_RUNNING", LoginMetrics::BrowserState::kRunning, |
| nullptr}, |
| {"Sleeping", "TEST_STATUS_SLEEPING", LoginMetrics::BrowserState::kSleeping, |
| nullptr}, |
| {"Stopped", "TEST_STATUS_STOPPED", |
| LoginMetrics::BrowserState::kTracedOrStopped, nullptr}, |
| {"UninterruptibleWait", "TEST_STATUS_UNINTERRUPTIBLE_WAIT", |
| LoginMetrics::BrowserState::kUninterruptibleWait, nullptr}, |
| {"Zombie", "TEST_STATUS_ZOMBIE", LoginMetrics::BrowserState::kZombie, |
| nullptr}, |
| {"UnknownState", "TEST_STATUS_UNKNOWN_STATE", |
| LoginMetrics::BrowserState::kUnknown, "Unknown browser state X"}, |
| {"MissingStateLine", "TEST_STATUS_MISSING_STATE", |
| LoginMetrics::BrowserState::kErrorGettingState, |
| "Could not find '\\nState:\\t'"}, |
| {"StateAtEnd", "TEST_STATUS_STATE_IS_LAST_CHARACTER", |
| LoginMetrics::BrowserState::kErrorGettingState, |
| "State:\\t at very end of file"}, |
| {"MissingStatusFile", nullptr, |
| LoginMetrics::BrowserState::kErrorGettingState, "Could not open "}}; |
| |
| class LivenessCheckerImplParamTest |
| : public LivenessCheckerImplTest, |
| public testing::WithParamInterface<TestFileAndStatus> {}; |
| |
| TEST_P(LivenessCheckerImplParamTest, BrowserStatusToUMA) { |
| brillo::InitLog(brillo::kLogToStderr); |
| if (GetParam().file_name != nullptr) { |
| base::FilePath fake_status_path = tmpdir_.GetPath().Append("123"); |
| base::File::Error error; |
| ASSERT_TRUE(base::CreateDirectoryAndGetError(fake_status_path, &error)) |
| << base::File::ErrorToString(error); |
| base::FilePath fake_status_file_name = fake_status_path.Append("status"); |
| base::FilePath test_data_file_name = |
| base::FilePath("testdata").Append(GetParam().file_name); |
| ASSERT_TRUE(base::CopyFile(test_data_file_name, fake_status_file_name)) |
| << "Could not copy " << test_data_file_name.value() << " to " |
| << fake_status_file_name.value(); |
| } |
| |
| if (GetParam().expected_log_message != nullptr) { |
| brillo::ClearLog(); |
| brillo::LogToString(true); |
| } |
| |
| ExpectUnAckedLivenessPing(); |
| EXPECT_CALL(*manager_.get(), AbortBrowserForHang()).Times(1); |
| EXPECT_CALL(*manager_.get(), GetBrowserPid()).WillRepeatedly(Return(123)); |
| EXPECT_CALL(*metrics_, |
| RecordStateForLivenessTimeout(GetParam().expected_state)) |
| .Times(1); |
| checker_->CheckAndSendLivenessPing(TimeDelta()); |
| fake_loop_.Run(); // Runs until the message loop is empty. |
| |
| if (GetParam().expected_log_message != nullptr) { |
| EXPECT_TRUE(brillo::FindLog(GetParam().expected_log_message)) |
| << "Did not find '" << GetParam().expected_log_message << "' in logs"; |
| brillo::LogToString(false); |
| brillo::ClearLog(); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| LivenessChecker, |
| LivenessCheckerImplParamTest, |
| testing::ValuesIn(kTestFilesAndStatuses), |
| [](const ::testing::TestParamInfo<LivenessCheckerImplParamTest::ParamType>& |
| info) { return std::string(info.param.test_name); }); |
| |
| } // namespace login_manager |