blob: 6beb63d0063c9b03a199551a076ee2fcd22989bb [file] [log] [blame] [edit]
// 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