| // Copyright 2024 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "heartd/daemon/action_runner.h" |
| |
| #include <fcntl.h> |
| #include <string> |
| |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/test/task_environment.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "heartd/daemon/test_utils/mock_dbus_connector.h" |
| |
| namespace heartd { |
| |
| namespace { |
| |
| namespace mojom = ::ash::heartd::mojom; |
| |
| using ::testing::_; |
| using ::testing::Exactly; |
| |
| void InsertNormalRebootRecords(std::vector<BootRecord>& boot_records, |
| int count, |
| base::Time& time) { |
| for (int i = 0; i < count; ++i) { |
| boot_records.emplace_back("shutdown.123", time); |
| boot_records.emplace_back("Boot ID", time); |
| } |
| } |
| |
| void InsertAbnormalRebootRecords(std::vector<BootRecord>& boot_records, |
| int count, |
| base::Time& time) { |
| // Because two consecutive boot IDs indicate one abnormal reboot. |
| for (int i = 0; i < count + 1; ++i) { |
| boot_records.emplace_back("Boot ID", time); |
| } |
| } |
| |
| class ActionRunnerTest : public testing::Test { |
| public: |
| ActionRunnerTest() {} |
| ~ActionRunnerTest() override = default; |
| |
| void VerifyForceReboot(bool is_reboot) { |
| std::string content; |
| EXPECT_TRUE(ReadFileToString(fake_sysrq_path_, &content)); |
| if (is_reboot) { |
| EXPECT_EQ(content, "c"); |
| } else { |
| EXPECT_EQ(content, ""); |
| } |
| } |
| |
| protected: |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| fake_sysrq_path_ = temp_dir_.GetPath().Append("sysrq-trigger"); |
| fake_sysrq_fd_ = |
| open(fake_sysrq_path_.MaybeAsASCII().c_str(), O_CREAT | O_WRONLY, 0644); |
| EXPECT_NE(fake_sysrq_fd_, -1); |
| action_runner_.SetupSysrq(fake_sysrq_fd_); |
| } |
| void TearDown() override { ASSERT_TRUE(temp_dir_.Delete()); } |
| |
| protected: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| base::ScopedTempDir temp_dir_; |
| base::FilePath fake_sysrq_path_; |
| int fake_sysrq_fd_; |
| MockDbusConnector mock_dbus_connector_; |
| ActionRunner action_runner_{&mock_dbus_connector_}; |
| }; |
| |
| TEST_F(ActionRunnerTest, DefaultSetting) { |
| EXPECT_CALL(*mock_dbus_connector_.power_manager_proxy(), |
| RequestRestartAsync(_, _, _, _, _)) |
| .Times(Exactly(0)); |
| |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot); |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kForceReboot); |
| VerifyForceReboot(/* is_reboot= */ false); |
| } |
| |
| TEST_F(ActionRunnerTest, EnableNormalRebootActionForNormalReboot) { |
| EXPECT_CALL(*mock_dbus_connector_.power_manager_proxy(), |
| RequestRestartAsync(_, _, _, _, _)) |
| .Times(Exactly(1)); |
| |
| action_runner_.EnableNormalRebootAction(); |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot); |
| } |
| |
| TEST_F(ActionRunnerTest, EnableForceRebootActionForForceReboot) { |
| action_runner_.SetSyncFlag(false); |
| action_runner_.EnableForceRebootAction(); |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kForceReboot); |
| VerifyForceReboot(/* is_reboot= */ true); |
| } |
| |
| TEST_F(ActionRunnerTest, EnableNormalRebootActionForOtherAction) { |
| EXPECT_CALL(*mock_dbus_connector_.power_manager_proxy(), |
| RequestRestartAsync(_, _, _, _, _)) |
| .Times(Exactly(0)); |
| |
| action_runner_.SetSyncFlag(false); |
| action_runner_.EnableNormalRebootAction(); |
| auto start_action = mojom::ActionType::kNoOperation; |
| switch (start_action) { |
| case mojom::ActionType::kNoOperation: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNoOperation); |
| [[fallthrough]]; |
| case mojom::ActionType::kNormalReboot: |
| // It should reboot, it's tested in |
| // EnableNormalRebootActionForNormalReboot. |
| [[fallthrough]]; |
| case mojom::ActionType::kForceReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kForceReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kSyncData: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kSyncData); |
| [[fallthrough]]; |
| case mojom::ActionType::kUnmappedEnumField: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kUnmappedEnumField); |
| break; |
| } |
| } |
| |
| TEST_F(ActionRunnerTest, EnableForceRebootActionForOtherAction) { |
| action_runner_.SetSyncFlag(false); |
| action_runner_.EnableForceRebootAction(); |
| auto start_action = mojom::ActionType::kNoOperation; |
| switch (start_action) { |
| case mojom::ActionType::kNoOperation: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNoOperation); |
| [[fallthrough]]; |
| case mojom::ActionType::kNormalReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kForceReboot: |
| // It should reboot, it's tested in |
| // EnableForceRebootActionForForceReboot. |
| [[fallthrough]]; |
| case mojom::ActionType::kSyncData: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kSyncData); |
| [[fallthrough]]; |
| case mojom::ActionType::kUnmappedEnumField: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kUnmappedEnumField); |
| break; |
| } |
| |
| VerifyForceReboot(/* is_reboot= */ false); |
| } |
| |
| TEST_F(ActionRunnerTest, DisableNormalRebootAction) { |
| EXPECT_CALL(*mock_dbus_connector_.power_manager_proxy(), |
| RequestRestartAsync(_, _, _, _, _)) |
| .Times(Exactly(0)); |
| |
| action_runner_.SetSyncFlag(false); |
| action_runner_.EnableNormalRebootAction(); |
| action_runner_.DisableNormalRebootAction(); |
| auto start_action = mojom::ActionType::kNoOperation; |
| switch (start_action) { |
| case mojom::ActionType::kNoOperation: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNoOperation); |
| [[fallthrough]]; |
| case mojom::ActionType::kNormalReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kForceReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kForceReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kSyncData: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kSyncData); |
| [[fallthrough]]; |
| case mojom::ActionType::kUnmappedEnumField: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kUnmappedEnumField); |
| break; |
| } |
| } |
| |
| TEST_F(ActionRunnerTest, DisableForceRebootAction) { |
| action_runner_.SetSyncFlag(false); |
| action_runner_.EnableForceRebootAction(); |
| action_runner_.DisableForceRebootAction(); |
| auto start_action = mojom::ActionType::kNoOperation; |
| switch (start_action) { |
| case mojom::ActionType::kNoOperation: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNoOperation); |
| [[fallthrough]]; |
| case mojom::ActionType::kNormalReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kForceReboot: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kForceReboot); |
| [[fallthrough]]; |
| case mojom::ActionType::kSyncData: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kSyncData); |
| [[fallthrough]]; |
| case mojom::ActionType::kUnmappedEnumField: |
| action_runner_.Run(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kUnmappedEnumField); |
| break; |
| } |
| |
| VerifyForceReboot(/* is_reboot= */ false); |
| } |
| |
| TEST_F(ActionRunnerTest, NormalReboot12HoursThreshold) { |
| std::vector<BootRecord> boot_records; |
| auto start_time = base::Time().Now(); |
| task_environment_.FastForwardBy(base::Hours(12)); |
| |
| // 2 normal reboot records within 12 hours window. |
| InsertNormalRebootRecords(boot_records, 2, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_FALSE(action_runner_.IsNormalRebootTooManyTimes()); |
| |
| // 3 normal reboot records within 12 hours window. |
| InsertNormalRebootRecords(boot_records, 1, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_TRUE(action_runner_.IsNormalRebootTooManyTimes()); |
| |
| // If we move forward 1 second, we should be able to call the reboot action |
| // again. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| EXPECT_FALSE(action_runner_.IsNormalRebootTooManyTimes()); |
| } |
| |
| TEST_F(ActionRunnerTest, DryRunSuccess) { |
| action_runner_.EnableNormalRebootAction(); |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kSuccess); |
| } |
| |
| TEST_F(ActionRunnerTest, DryRunFailure) { |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kNotAllowed); |
| } |
| |
| TEST_F(ActionRunnerTest, DryRunRateLimit) { |
| std::vector<BootRecord> boot_records; |
| auto start_time = base::Time().Now(); |
| |
| action_runner_.EnableNormalRebootAction(); |
| task_environment_.FastForwardBy(base::Days(7)); |
| InsertNormalRebootRecords(boot_records, 10, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kRateLimit); |
| } |
| |
| TEST_F(ActionRunnerTest, NormalReboot7DaysThreshold) { |
| std::vector<BootRecord> boot_records; |
| auto start_time = base::Time().Now(); |
| task_environment_.FastForwardBy(base::Days(7)); |
| |
| // 9 normal reboot records within 7 days window. |
| InsertNormalRebootRecords(boot_records, 9, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_FALSE(action_runner_.IsNormalRebootTooManyTimes()); |
| action_runner_.EnableNormalRebootAction(); |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kSuccess); |
| |
| // 10 normal reboot records within 7 days window. |
| InsertNormalRebootRecords(boot_records, 1, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_TRUE(action_runner_.IsNormalRebootTooManyTimes()); |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kRateLimit); |
| |
| // If we move forward 1 second, we should be able to call the reboot action |
| // again. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| EXPECT_FALSE(action_runner_.IsNormalRebootTooManyTimes()); |
| EXPECT_EQ(action_runner_.DryRun(mojom::ServiceName::kKiosk, |
| mojom::ActionType::kNormalReboot), |
| mojom::HeartbeatResponse::kSuccess); |
| } |
| |
| TEST_F(ActionRunnerTest, ForceReboot12HoursThreshold) { |
| std::vector<BootRecord> boot_records; |
| auto start_time = base::Time().Now(); |
| task_environment_.FastForwardBy(base::Hours(12)); |
| |
| // 2 abnormal reboot records within 12 hours window. |
| InsertAbnormalRebootRecords(boot_records, 2, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_FALSE(action_runner_.IsForceRebootTooManyTimes()); |
| |
| // 3 abnormal reboot records within 12 hours window. |
| InsertAbnormalRebootRecords(boot_records, 1, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_TRUE(action_runner_.IsForceRebootTooManyTimes()); |
| |
| // If we move forward 1 second, we should be able to call the reboot action |
| // again. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| EXPECT_FALSE(action_runner_.IsForceRebootTooManyTimes()); |
| } |
| |
| TEST_F(ActionRunnerTest, ForceReboot7DaysThreshold) { |
| std::vector<BootRecord> boot_records; |
| auto start_time = base::Time().Now(); |
| task_environment_.FastForwardBy(base::Days(7)); |
| |
| // 9 abnormal reboot records within 7 days window. |
| InsertAbnormalRebootRecords(boot_records, 9, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_FALSE(action_runner_.IsForceRebootTooManyTimes()); |
| |
| // 10 abnormal reboot records within 7 days window. |
| InsertAbnormalRebootRecords(boot_records, 1, start_time); |
| action_runner_.CacheBootRecord(boot_records); |
| EXPECT_TRUE(action_runner_.IsForceRebootTooManyTimes()); |
| |
| // If we move forward 1 second, we should be able to call the reboot action |
| // again. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| EXPECT_FALSE(action_runner_.IsForceRebootTooManyTimes()); |
| } |
| |
| } // namespace |
| |
| } // namespace heartd |