blob: a1a0a305e96f9a03fef60ab8f79f00ffee66510b [file]
#include "persistent_storage_impl.h"
#include <unistd.h>
#include <cstdint>
#include <filesystem> // NOLINT(build/c++17)
#include <fstream>
#include <ios>
#include <iterator>
#include <string>
#include <vector>
#include "google/protobuf/timestamp.pb.h"
#include "action.pb.h"
#include "safepower_agent.pb.h"
#include "safepower_agent_config.pb.h"
#include "state_persistence.pb.h"
#include "utils.pb.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "google/protobuf/message.h"
#include "test_util.h"
namespace persistent_storage {
namespace {
using safepower_agent_persistence_proto::SavedAction;
using safepower_agent_persistence_proto::SavedActions;
using borg_mgmt::node_proxy::safepower::utils::FlightRecordRequest;
using safepower_agent_persistence_proto::SavedActionRecord;
using ::testing::IsEmpty;
constexpr uint64_t kNEW_EPOCH = 987654321;
constexpr absl::string_view gkey = "default_flight_id";
SavedActions GetSavedActionsWhole(std::string flight_id = std::string(gkey)) {
SavedActions saved_actions;
SavedAction saved_action;
safepower_agent_proto::StartActionRequest* start_action_request =
saved_action.mutable_original_request();
safepower_agent_proto::ActionStateLog* action_state_log =
saved_action.mutable_action_state_log();
action_state_log->set_epoch_ms(123456789);
action_state_log->set_current_state(
safepower_agent_proto::ACTION_STATE_RUNNING_ACTION);
safepower_agent_proto::Action* action =
start_action_request->mutable_action();
action->set_action_type(safepower_agent_proto::ACTION_TYPE_ON);
start_action_request->set_caller_cookie("test_cookie");
FlightRecordRequest test_flight_record;
test_flight_record.set_flight_name(flight_id);
test_flight_record.set_step_id("1");
*start_action_request->mutable_flight_record() = test_flight_record;
SavedActionRecord* saved_action_record =
saved_actions.add_saved_action_records();
*(saved_action_record->mutable_actions()) = saved_action;
return saved_actions;
}
SavedActions GetSavedActionsUpdate(std::string key = std::string(gkey))
{
// append ONLY a change to example saved actions
// Only the time and the Action state will be different
// status: running -> success
// time: smaller -> larger
SavedActions saved_actions_diff;
SavedAction saved_action_diff;
safepower_agent_proto::ActionStateLog* action_state_log_diff =
saved_action_diff.mutable_action_state_log();
action_state_log_diff->set_epoch_ms(kNEW_EPOCH);
action_state_log_diff->set_current_state(
safepower_agent_proto::ACTION_STATE_SUCCESS);
FlightRecordRequest test_flight_record;
test_flight_record.set_flight_name(key);
test_flight_record.set_step_id("1");
*saved_action_diff.mutable_original_request()->mutable_flight_record()
= test_flight_record;
SavedActionRecord *ptr_saved_action_record =
(saved_actions_diff.add_saved_action_records());
*(ptr_saved_action_record->mutable_actions()) = saved_action_diff;
return saved_actions_diff;
}
void evalChangeOn(SavedActions result, std::string key = std::string(gkey)) {
for (const auto& i : result.saved_action_records()) {
if (i.actions().original_request().flight_record().flight_name() == key) {
auto result_saved_action = i.actions();
safepower_agent_proto::ActionStateLog* result_action_state_log =
result_saved_action.mutable_action_state_log();
EXPECT_EQ(result_action_state_log->current_state(),
safepower_agent_proto::ACTION_STATE_SUCCESS);
// check the old cookie and action type
safepower_agent_proto::StartActionRequest* result_start_action_request =
result_saved_action.mutable_original_request();
safepower_agent_proto::Action* action =
result_start_action_request->mutable_action();
EXPECT_EQ(action->action_type(), safepower_agent_proto::ACTION_TYPE_ON);
EXPECT_EQ(result_start_action_request->caller_cookie(), "test_cookie");
}
}
}
std::string CreateNewTestDir() {
static int counter = 0;
std::string dir_path = absl::StrCat(testing::TempDir(), "/test_",
counter++, "/persistent_storage");
std::filesystem::create_directories(dir_path);
return dir_path;
}
TEST(PersistentStorageBMC, WriteStateAndRead) {
auto saved_actions = GetSavedActionsWhole();
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->sequence_number(), 1);
result->clear_sequence_number();
std::string result_str, orig_str;
result->SerializeToString(&result_str);
saved_actions.SerializeToString(&orig_str);
EXPECT_EQ(result_str, orig_str);
}
TEST(PersistentStorageBMC, ReadBeforeWriteReturnsEmpty) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 0);
}
TEST(PersistentStorageBMC, WriteStateAndModifyAndRead) {
std::string dir_name = CreateNewTestDir();
auto saved_actions = GetSavedActionsWhole();
// save example saved actions
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto update = GetSavedActionsUpdate();
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
// check the new time, and the new state
evalChangeOn(*result);
}
TEST(PersistentStorageBMC, WriteStateAndModifyAndReadManyTimes) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto special = GetSavedActionsWhole("special");
EXPECT_EQ(persist.WriteSavedActionsChange(special), absl::OkStatus());
uint32_t written_size = 0;
while (written_size < kMaxFileSize * 2) {
auto saved_actions = GetSavedActionsWhole();
written_size += saved_actions.ByteSizeLong();
// save example saved actions
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
}
written_size = 0;
while (written_size < kMaxFileSize * 2) {
auto update = GetSavedActionsUpdate();
written_size += update.ByteSizeLong();
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
// check the new time, and the new state
evalChangeOn(*result);
// check special
SavedAction result_special;
for (const auto& i : result->saved_action_records()) {
if (i.actions().original_request().flight_record().flight_name()
== "special") {
result_special = i.actions();
break;
}
}
EXPECT_TRUE(result_special.has_action_state_log());
EXPECT_EQ((result_special.action_state_log().epoch_ms()), 123456789);
}
}
TEST(PersistentStorageBMC, DropOldestProto) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
std::vector<uint32_t> counts;
uint32_t written_size = 0;
uint32_t count = 0;
while (written_size < kMaxFileSize * 2) {
SavedActions saved_actions;
SavedActionRecord* record = saved_actions.add_saved_action_records();
record->mutable_actions()->mutable_original_request()
->mutable_flight_record()->set_flight_name(std::to_string(count));
record->mutable_actions()->mutable_original_request()
->mutable_flight_record()->set_step_id("step_id");
record->mutable_actions()->mutable_action_state_log()->add_history()
->mutable_changed_at()->set_seconds(count);
counts.push_back(count);
count++;
written_size += saved_actions.ByteSizeLong();
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
}
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
// we expect protos to be dropped, because we wrote 2 X max filesize
EXPECT_LT(result->saved_action_records().size(), count);
// Create a map of flight names to change at time from the result
absl::flat_hash_map<std::string, uint64_t> result_map;
for (auto& record : result->saved_action_records()) {
uint64_t time = record.actions().action_state_log().history().rbegin()->
changed_at().seconds();
std::string name(
record.actions().original_request().flight_record().flight_name());
result_map[name] = time;
}
// we expect (at least) the last kProtoKeepNumber
// and we can check the count value equals the epoch_ms set above
for (auto i = counts.rbegin();
i != std::next(counts.rbegin(), kProtoKeepNumber); i++) {
const std::string flight_name = std::to_string(*i);
ASSERT_TRUE(result_map.count(flight_name)) << "Missing flight name: ";
EXPECT_EQ(result_map.at(flight_name), *i);
}
}
TEST(PersistentStorageBMC, ReadWithoutPruning) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
// Ensure size limit is large enough to not trigger rotation for 15 records.
config.set_max_file_size(kMaxFileSize * 10);
persistent_storage::PersistentStorageManagerImpl persist(config);
// Write 15 unique records.
for (int i = 0; i < 15; ++i) {
auto actions = GetSavedActionsWhole(std::to_string(i));
EXPECT_EQ(persist.WriteSavedActionsChange(actions), absl::OkStatus());
}
// Read back and verify we have exactly 15 records (not pruned to 10).
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records().size(), 15);
}
TEST(PersistentStorageBMC, InitializeSavedActions) {
std::string dir_name = CreateNewTestDir();
std::ofstream out_file(absl::StrCat(dir_name, "/test_file"));
ASSERT_TRUE(out_file.is_open());
out_file.close();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
EXPECT_EQ(persist.InitializeSavedActions(), absl::OkStatus());
EXPECT_EQ(std::filesystem::exists(dir_name), true);
std::filesystem::directory_iterator it(dir_name);
std::vector<std::filesystem::path> files(std::filesystem::begin(it),
std::filesystem::end(it));
EXPECT_THAT(files, IsEmpty());
}
void WriteActionsToFile(absl::string_view file_path,
const SavedActions& actions) {
std::ofstream ofs(std::string(file_path),
std::ios_base::binary | std::ios_base::out);
ASSERT_TRUE(ofs.is_open());
ASSERT_TRUE(actions.SerializeToOstream(&ofs));
ofs.close();
ASSERT_FALSE(ofs.fail());
}
TEST(PersistentStorageBMC, ReadSavedActionsPreservesSequenceNumber) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions = GetSavedActionsWhole();
actions.set_sequence_number(42);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_0"), actions);
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->sequence_number(), 42);
}
TEST(PersistentStorageBMC,
WriteSavedActionsChangeIncrementsSequenceNumberOnRotation) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
// Force rotation on next write.
config.set_max_file_size(1);
persistent_storage::PersistentStorageManagerImpl persist(config);
// First write: goes to savedactions_0, sequence number 1.
auto actions1 = GetSavedActionsWhole("actions1");
ASSERT_EQ(persist.WriteSavedActionsChange(actions1), absl::OkStatus());
// Verify active file has sequence 2 (rotated immediately because size > 1).
auto result1 = persist.ReadSavedActions();
ASSERT_OK(result1.status());
EXPECT_EQ(result1->sequence_number(), 2);
// Second write: triggers rotation to savedactions_0, sequence number 3.
auto actions2 = GetSavedActionsWhole("actions2");
ASSERT_EQ(persist.WriteSavedActionsChange(actions2), absl::OkStatus());
// Verify active file is now sequence 3 and has merged content.
auto result2 = persist.ReadSavedActions();
ASSERT_OK(result2.status());
EXPECT_EQ(result2->sequence_number(), 3);
EXPECT_EQ(result2->saved_action_records_size(), 2);
}
TEST(PersistentStorageBMC, FindLargestFileIdIgnoresInvalidFileNames) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions1 = GetSavedActionsWhole("actions1");
auto actions2 = GetSavedActionsWhole("actions2");
auto actions_other = GetSavedActionsWhole("other");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_2"), actions1);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_foo"),
actions_other);
WriteActionsToFile(absl::StrCat(dir_name, "/notsavedactions_4"),
actions_other);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions"), actions_other);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_"), actions_other);
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"actions1");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_3"), actions2);
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"actions2");
}
TEST(PersistentStorageBMC, ReadSavedActionsIgnoresInvalidFileContent) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions1 = GetSavedActionsWhole("actions1");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_1"), actions1);
// Overwrite the file with invalid content.
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_1"),
std::ios_base::binary | std::ios_base::out);
ASSERT_TRUE(ofs.is_open());
ofs << "invalid proto content";
ofs.close();
ASSERT_FALSE(ofs.fail());
auto result = persist.ReadSavedActions();
ASSERT_FALSE(result.ok());
}
TEST(PersistentStorageBMC, ReadSavedActionsFailsIfSingleFileIsCorrupt) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
// Write a corrupt file with ID 0.
// This ensures id.value() == 0, preventing fallback attempts.
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_0"),
std::ios_base::binary | std::ios_base::out);
ASSERT_TRUE(ofs.is_open());
ofs << "invalid proto content";
ofs.close();
ASSERT_FALSE(ofs.fail());
auto result = persist.ReadSavedActions();
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kDataLoss);
EXPECT_THAT(result.status().message(),
::testing::HasSubstr("Savedactions file 0 is corrupt"));
}
TEST(PersistentStorageBMC, BootCounterAndIdPreservedAcrossPartialUpdate) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto saved_actions = GetSavedActionsWhole();
saved_actions.set_boot_counter(42);
saved_actions.set_boot_id("boot-id-1");
// First write
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->boot_counter(), 42);
EXPECT_EQ(result->boot_id(), "boot-id-1");
// Update without setting boot_counter/boot_id
auto update = GetSavedActionsUpdate();
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->boot_counter(), 42);
EXPECT_EQ(result->boot_id(), "boot-id-1");
}
TEST(PersistentStorageBMC, BootCounterAndIdUpdated) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto saved_actions = GetSavedActionsWhole();
saved_actions.set_boot_counter(42);
saved_actions.set_boot_id("boot-id-1");
// First write
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->boot_counter(), 42);
EXPECT_EQ(result->boot_id(), "boot-id-1");
// Update WITH setting boot_counter/boot_id
saved_actions.set_boot_counter(43);
saved_actions.set_boot_id("boot-id-2");
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->boot_counter(), 43);
EXPECT_EQ(result->boot_id(), "boot-id-2");
}
TEST(PersistentStorageBMC, BootCounterPreservedAcrossMerge) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
// Set small size to force merge
config.set_max_file_size(10);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto saved_actions = GetSavedActionsWhole();
saved_actions.set_boot_counter(100);
saved_actions.set_boot_id("persistent-boot-id");
// This should trigger merge immediately or on second write depending on size
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
// Write more to ensure multiple files / merge happens
auto update = GetSavedActionsUpdate();
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->boot_counter(), 100);
EXPECT_EQ(result->boot_id(), "persistent-boot-id");
}
TEST(PersistentStorageBMC, BootCounterAndIdUpdatePreservesActions) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto saved_actions = GetSavedActionsWhole();
saved_actions.set_boot_counter(10);
saved_actions.set_boot_id("boot-id-10");
// First write
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->boot_counter(), 10);
EXPECT_EQ(result->boot_id(), "boot-id-10");
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"default_flight_id");
// Update ONLY boot counter and id
SavedActions update;
update.set_boot_counter(20);
update.set_boot_id("boot-id-20");
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
// Verify actions are preserved (still 1)
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"default_flight_id");
// Verify boot info is updated
EXPECT_EQ(result->boot_counter(), 20);
EXPECT_EQ(result->boot_id(), "boot-id-20");
}
TEST(PersistentStorageBMC, BootCounterAndIdAdditionPreservesActions) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto saved_actions = GetSavedActionsWhole();
// Ensure no boot info initially
saved_actions.clear_boot_counter();
saved_actions.clear_boot_id();
// First write
EXPECT_EQ(persist.WriteSavedActionsChange(saved_actions), absl::OkStatus());
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->boot_counter(), 0);
EXPECT_TRUE(result->boot_id().empty());
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"default_flight_id");
// Update adding boot info only
SavedActions update;
update.set_boot_counter(55);
update.set_boot_id("boot-id-55");
EXPECT_EQ(persist.WriteSavedActionsChange(update), absl::OkStatus());
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
// Verify actions are preserved
ASSERT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"default_flight_id");
// Verify boot info is added
EXPECT_EQ(result->boot_counter(), 55);
EXPECT_EQ(result->boot_id(), "boot-id-55");
}
TEST(PersistentStorageBMC, ReadSavedActionsPermissionDenied) {
// This test is run as root in meson, which bypasses normal filesystem
// permissions (perms::none). Skip it if running as root.
if (getuid() == 0) {
GTEST_SKIP() << "Skipping permission test as root.";
}
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
// Remove read and execute permissions from the directory.
std::filesystem::permissions(dir_name, std::filesystem::perms::none);
auto result = persist.ReadSavedActions();
EXPECT_FALSE(result.ok());
// The error comes from listFiles or exists check.
EXPECT_EQ(result.status().code(), absl::StatusCode::kInternal);
// Restore permissions so cleanup (TempDir removal) can work.
std::filesystem::permissions(dir_name, std::filesystem::perms::owner_all |
std::filesystem::perms::group_all);
}
TEST(PersistentStorageBMC, ReadSavedActionsNotADirectory) {
std::string file_path = absl::StrCat(testing::TempDir(), "/not_a_dir");
std::ofstream ofs(file_path);
ofs << "just a file";
ofs.close();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(file_path);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto result = persist.ReadSavedActions();
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kInternal);
}
TEST(PersistentStorageBMC, WriteSavedActionsChangeWriteFailure) {
// This test is run as root in meson, which bypasses normal filesystem
// permissions (non-writable). Skip it if running as root.
if (getuid() == 0) {
GTEST_SKIP() << "Skipping permission test as root.";
}
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
// Set small size to force merge/cleanup
config.set_max_file_size(1);
persistent_storage::PersistentStorageManagerImpl persist(config);
// Create active file (savedactions_1) and inactive (savedactions_0)
auto actions1 = GetSavedActionsWhole("actions1");
EXPECT_EQ(persist.WriteSavedActionsChange(actions1), absl::OkStatus());
// Now make the inactive file (savedactions_0) non-writable.
// When we write update, it will append to savedactions_1, exceed size,
// and try to write compressed state to savedactions_0, which should fail.
std::filesystem::permissions(absl::StrCat(dir_name, "/savedactions_0"),
std::filesystem::perms::owner_read);
auto update = GetSavedActionsUpdate();
auto status = persist.WriteSavedActionsChange(update);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kUnavailable);
// Restore permissions for cleanup.
std::filesystem::permissions(absl::StrCat(dir_name, "/savedactions_0"),
std::filesystem::perms::owner_all);
}
TEST(PersistentStorageBMC, InitializeSavedActionsDeletionFailure) {
std::string dir_name = CreateNewTestDir();
std::string file_path = absl::StrCat(dir_name, "/cant_delete");
// Create a directory instead of a file.
std::filesystem::create_directories(file_path);
// Create a file inside it to make it non-empty.
std::ofstream ofs(absl::StrCat(file_path, "/sub_file"));
ofs << "content";
ofs.close();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
// InitializeSavedActions iterates over files in the directory and deletes
// them using std::filesystem::remove. If it encounters a non-empty directory,
// remove will fail regardless of whether the user is root or not.
auto status = persist.InitializeSavedActions();
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kUnavailable);
}
TEST(PersistentStorageBMC, MigrationOnWrite) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions1 = GetSavedActionsWhole("actions1");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_2"), actions1);
// Write new actions, which should trigger migration.
auto actions2 = GetSavedActionsWhole("actions2");
EXPECT_EQ(persist.WriteSavedActionsChange(actions2), absl::OkStatus());
// Verify that savedactions_2 is deleted.
EXPECT_FALSE(
std::filesystem::exists(absl::StrCat(dir_name, "/savedactions_2")));
// Verify that we migrated to savedactions_0.
EXPECT_TRUE(
std::filesystem::exists(absl::StrCat(dir_name, "/savedactions_0")));
// Verify the content. It should contain both actions1 and actions2.
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 2);
}
TEST(PersistentStorageBMC,
ReadFromSavedActions1WhenSavedActions0IsMissingOrCorrupt) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions1 = GetSavedActionsWhole("actions1");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_1"), actions1);
// Verify we can read when savedactions_0 is missing.
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 1);
// Now write corrupt data to savedactions_0.
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_0"));
ofs << "corrupt data";
ofs.close();
// Verify we can still read from savedactions_1.
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 1);
}
TEST(PersistentStorageBMC, MigrationFallbackWhenLatestOldFileIsCorrupt) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions1 = GetSavedActionsWhole("actions1");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_2"), actions1);
// Write corrupt data to savedactions_3 (latest old file).
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_3"));
ofs << "corrupt data";
ofs.close();
// Read should fallback to savedactions_2.
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->saved_action_records(0)
.actions()
.original_request()
.flight_record()
.flight_name(),
"actions1");
}
TEST(PersistentStorageBMC, RecoverWhenOneFileIsCorrupt) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions0 = GetSavedActionsWhole("actions0");
actions0.set_sequence_number(10);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_0"), actions0);
// Write corrupt data to savedactions_1.
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_1"));
ofs << "corrupt data";
ofs.close();
// Read should succeed using savedactions_0.
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->sequence_number(), 10);
// Test the other way: savedactions_0 corrupt, savedactions_1 valid.
std::ofstream ofs0(absl::StrCat(dir_name, "/savedactions_0"));
ofs0 << "corrupt data";
ofs0.close();
auto actions1 = GetSavedActionsWhole("actions1");
actions1.set_sequence_number(11);
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_1"), actions1);
result = persist.ReadSavedActions();
ASSERT_OK(result.status());
EXPECT_EQ(result->saved_action_records_size(), 1);
EXPECT_EQ(result->sequence_number(), 11);
}
TEST(PersistentStorageBMC, CorruptActiveFileDoesNotRollbackToOldFiles) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
persistent_storage::PersistentStorageManagerImpl persist(config);
auto actions_old = GetSavedActionsWhole("old_action");
WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_2"), actions_old);
// Create a corrupt savedactions_0.
std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_0"));
ofs << "corrupt data";
ofs.close();
// Read should fail with DataLossError, and NOT rollback to savedactions_2.
auto result = persist.ReadSavedActions();
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kDataLoss);
EXPECT_THAT(result.status().message(),
::testing::HasSubstr("Savedactions file 0 is corrupt"));
}
TEST(PersistentStorageBMC, PruningKeepsNewest) {
std::string dir_name = CreateNewTestDir();
safepower_agent_config::PersistentStorageConfig config;
config.set_dir_path(dir_name);
config.set_max_file_size(1); // Force rotation/pruning on every write
config.set_proto_keep_number(3); // Only keep 3 records
persistent_storage::PersistentStorageManagerImpl persist(config);
// Write 5 records with distinct timestamps (oldest to newest)
for (int i = 1; i <= 5; ++i) {
SavedActions actions;
SavedActionRecord* record = actions.add_saved_action_records();
record->mutable_actions()
->mutable_original_request()
->mutable_flight_record()
->set_flight_name(absl::StrCat("flight_", i));
record->mutable_actions()
->mutable_action_state_log()
->add_history()
->mutable_changed_at()
->set_seconds(i * 10); // Timestamps: 10, 20, 30, 40, 50
ASSERT_OK(persist.WriteSavedActionsChange(actions));
}
// Read back and verify we kept the 3 newest records (flight_3, flight_4,
// flight_5)
auto result = persist.ReadSavedActions();
ASSERT_OK(result.status());
ASSERT_EQ(result->saved_action_records_size(), 3);
std::vector<std::string> kept_flights;
for (const auto& record : result->saved_action_records()) {
kept_flights.emplace_back(
record.actions().original_request().flight_record().flight_name());
}
EXPECT_THAT(kept_flights, ::testing::UnorderedElementsAre(
"flight_3", "flight_4", "flight_5"));
}
} // namespace
} // namespace persistent_storage