blob: a9509411f666d34ceeb740ca8a639dc94bc1cd06 [file]
#include <cstddef>
#include <cstdint>
#include <filesystem> // NOLINT
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <system_error> // NOLINT
#include <tuple>
#include <vector>
#include <gtest/gtest.h>
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "boottime_api/boottime_api_common.h"
#include "boottime_api/node_config.h"
#include "http_response.hpp"
#include "async_resp.hpp"
#include "boottime.hpp"
#include "test/redfish-core/lib/snapshot_fixture.hpp"
#include <nlohmann/json.hpp>
namespace redfish::boottime {
namespace {
void prepareTestDir(std::string_view testDir) {
std::filesystem::remove_all(testDir);
std::filesystem::create_directories(testDir);
}
void writeCheckpoint(const std::string& filename,
const std::vector<Checkpoint>& data) {
std::ofstream file(filename);
if (!file.is_open()) {
FAIL() << "Error: Could not open the file " << filename;
return;
}
for (const auto& entry : data) {
file << std::get<0>(entry) << "," << std::get<1>(entry) << ","
<< std::get<2>(entry) << "\n";
}
}
void writeDuration(const std::string& filename,
const std::vector<Duration>& data) {
std::ofstream file(filename);
if (!file.is_open()) {
FAIL() << "Error: Could not open the file " << filename;
return;
}
for (const auto& entry : data) {
file << std::get<0>(entry) << "," << std::get<1>(entry) << "\n";
}
}
class BootTimeTest : public SnapshotFixture {
protected:
static void SetUpTestSuite() {
kLockFile = testLockFile;
kDataDir = testDir;
prepareTestDir(testDir);
}
static void TearDownTestSuite() {
std::error_code ec;
// remove directories
std::filesystem::remove_all(testDir, ec);
if (ec) {
std::cerr << "Remove all test files failed in TearDownTestSuite() "
<< ec.message() << '\n';
}
}
static inline const std::string testDir = "/tmp/boot_time_test/";
static inline const std::string testLockFile = absl::StrCat(testDir, "flock");
static inline const std::string testHostFile =
absl::StrCat(testDir, "host0", boot_time_monitor::def::kCheckpointFile);
static inline const std::string testBmcFile =
absl::StrCat(testDir, "bmc", boot_time_monitor::def::kCheckpointFile);
};
struct BootTimeTestCase {
std::string_view name;
std::vector<Checkpoint> checkpoints;
std::vector<Duration> durations;
std::map<std::string, double> expectedBreakdown;
std::map<std::string, double> expectedDurations;
std::vector<std::tuple<std::string, std::string, int64_t>> expectedRebootFlow;
std::tuple<std::string, bool, nlohmann::json, nlohmann::json> expectedStat;
nlohmann::json expectedStatus;
double expectedTotal;
absl::StatusCode expectedStatusCode;
};
void BootTimeTestResponseCheck(
BootTimeTestCase testCase,
std::shared_ptr<bmcweb::AsyncResp>& share_async_resp_) {
EXPECT_EQ(share_async_resp_->res.jsonValue["@odata.type"],
"#GoogleBootTime.v1_0_0.GoogleBootTime");
EXPECT_EQ(share_async_resp_->res.jsonValue["Id"], "BootTimeData");
EXPECT_EQ(share_async_resp_->res.jsonValue["Name"], "Boot time data");
// Breakdown
EXPECT_TRUE(share_async_resp_->res.jsonValue.contains("Breakdown"));
std::map<std::string, double> expectedBreakdown(testCase.expectedBreakdown);
for (auto item : share_async_resp_->res.jsonValue["Breakdown"]) {
EXPECT_TRUE(item.contains("Stage"));
EXPECT_TRUE(item.contains("Duration"));
auto expected = expectedBreakdown.find(item["Stage"]);
if (expected == expectedBreakdown.end()) {
FAIL() << "Could not find " << item["Stage"]
<< " in the expected Breakdown." << '\n';
}
EXPECT_NEAR(expected->second, item["Duration"], 1e-1);
expectedBreakdown.erase(expected);
}
EXPECT_EQ(expectedBreakdown.size(), 0)
<< "Not all of the expected breakdowns are found in the response";
// Duration
EXPECT_TRUE(share_async_resp_->res.jsonValue.contains("Durations"));
std::map<std::string, double> expectedDurations(testCase.expectedDurations);
for (auto item : share_async_resp_->res.jsonValue["Durations"]) {
EXPECT_TRUE(item.contains("Name"));
EXPECT_TRUE(item.contains("Duration"));
auto expected = expectedDurations.find(item["Name"]);
if (expected == expectedDurations.end()) {
FAIL() << "Could not find " << item["Name"]
<< " in the expected durations." << '\n';
}
EXPECT_NEAR(expected->second, item["Duration"], 1e-1);
expectedDurations.erase(expected);
}
EXPECT_EQ(expectedDurations.size(), 0)
<< "Not all of the expected durations are found in the response";
// RebootFlow
EXPECT_TRUE(share_async_resp_->res.jsonValue.contains("RebootFlow"));
std::vector<std::tuple<std::string, std::string, int64_t>> expectedRebootFlow(
testCase.expectedRebootFlow.begin(), testCase.expectedRebootFlow.end());
size_t index = 0;
for (auto item : share_async_resp_->res.jsonValue["RebootFlow"]) {
EXPECT_TRUE(item.contains("Stage"));
EXPECT_TRUE(item.contains("StartTime"));
EXPECT_TRUE(item.contains("MonotonicStartTime"));
if (index > expectedRebootFlow.size()) {
FAIL() << "More reboot flow than expected.";
break;
}
auto [stage, startTime, monoStartTime] = expectedRebootFlow[index++];
EXPECT_EQ(stage, static_cast<std::string>(item["Stage"]));
EXPECT_EQ(startTime, static_cast<std::string>(item["StartTime"]));
EXPECT_EQ(monoStartTime, item["MonotonicStartTime"]);
}
}
TEST_F(BootTimeTest, HostBootTime) {
std::vector<struct BootTimeTestCase> testCases = {
BootTimeTestCase{
.name = "happy path",
.checkpoints =
{
Checkpoint({"Shutdown:BEGIN", 1731001381770, 794420}),
Checkpoint({"Shutdown", 1731001430000, 842650}),
Checkpoint({"HostState:Off", 1731001439805, 852210}),
Checkpoint({"OSStatus:Inactive", 1731001439808, 852210}),
Checkpoint({"TrayPowerCycle", 1731001558516, 970920}),
Checkpoint({"HostState:Off", 1731001625214, 67710}),
Checkpoint({"OSStatus:Inactive", 1731001625249, 67750}),
Checkpoint({"HostState:Running", 1731001725571, 168070}),
Checkpoint({"OSStatus:Standby", 1731001838682, 281180}),
Checkpoint({"Loader", 1731002038682, 460860}),
Checkpoint({"Userspace:BEGIN", 1731002185380, 517730}),
Checkpoint({"Userspace", 1731002295000, 627350}),
},
.durations =
{
Duration({"NerfKernel", 143000}),
Duration({"NerfUser", 36000}),
Duration({"NerfDHCP", 34000}),
Duration({"TimeOffset", 17650}),
Duration({"Kernel", 36870}),
},
.expectedBreakdown = {{"Firmware", 113.11},
{"Kernel", 36.87},
{"Loader", 179.68},
{"Off", 259.8},
{"OffToPowerCycle", 118.71},
{"Shutdown", 48.23},
{"TransitioningToKernel", 20},
{"TransitioningToOff", 9.56},
{"Userspace", 109.62}},
.expectedDurations = {{"NerfKernel", 143},
{"NerfUser", 36},
{"NerfDHCP", 34},
{"TimeOffset", 17.65},
{"Kernel", 36.87}},
.expectedRebootFlow =
{{"Shutdown:BEGIN", "2024-11-07T17:43:01.770+00:00", 794420},
{"Shutdown", "2024-11-07T17:43:50.000+00:00", 842650},
{"HostState:Off", "2024-11-07T17:43:59.805+00:00", 852210},
{"OSStatus:Inactive", "2024-11-07T17:43:59.808+00:00", 852210},
{"TrayPowerCycle", "2024-11-07T17:45:58.516+00:00", 970920},
{"HostState:Off", "2024-11-07T17:47:05.214+00:00", 67710},
{"OSStatus:Inactive", "2024-11-07T17:47:05.249+00:00", 67750},
{"HostState:Running", "2024-11-07T17:48:45.571+00:00", 168070},
{"OSStatus:Standby", "2024-11-07T17:50:38.682+00:00", 281180},
{"Loader", "2024-11-07T17:53:58.682+00:00", 460860},
{"Userspace:BEGIN", "2024-11-07T17:56:25.380+00:00", 517730},
{"Userspace", "2024-11-07T17:58:15.000+00:00", 627350}},
.expectedStat = {"AC", true, nlohmann::json(),
nlohmann::json(1731002038682)},
.expectedStatus = {{"Health", "Ok"}, {"State", "Enabled"}},
.expectedTotal = 895.58,
.expectedStatusCode = absl::StatusCode::kOk,
},
BootTimeTestCase{
.name = "empty",
.checkpoints = {},
.durations = {},
.expectedBreakdown = {},
.expectedDurations = {},
.expectedRebootFlow = {},
.expectedStat = {"Invalid", true, nlohmann::json(), nlohmann::json()},
.expectedStatus = {{"Health", "Warning"}, {"State", "Enabled"}},
.expectedTotal = 0.0,
.expectedStatusCode = absl::StatusCode::kOk,
},
};
for (auto testCase : testCases) {
prepareTestDir(testDir);
writeCheckpoint(
absl::StrCat(testDir, "host0", boot_time_monitor::def::kCheckpointFile),
testCase.checkpoints);
writeDuration(
absl::StrCat(testDir, "host0", boot_time_monitor::def::kDurationFile),
testCase.durations);
EXPECT_EQ(
PopulateHostData(share_async_resp_, "system", {"object"}).code(),
testCase.expectedStatusCode);
RunIoUntilDone();
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
EXPECT_EQ(share_async_resp_->res.jsonValue["@odata.id"],
"/redfish/v1/Systems/system/Oem/Google/BootTime");
EXPECT_EQ(share_async_resp_->res.jsonValue["Status"]["Health"],
testCase.expectedStatus["Health"]);
EXPECT_EQ(share_async_resp_->res.jsonValue["Status"]["State"],
testCase.expectedStatus["State"]);
// Response is only meaningful in ok status.
if (testCase.expectedStatusCode == absl::StatusCode::kOk) {
BootTimeTestResponseCheck(testCase, share_async_resp_);
}
}
}
TEST_F(BootTimeTest, BmcBootTime) {
std::vector<struct BootTimeTestCase> testCases = {
BootTimeTestCase{
.name = "happy path",
.checkpoints =
{
Checkpoint({"RebootStart", 1731086409343, 948870}),
Checkpoint({"TrayPowerCycle", 1731086409499, 949020}),
Checkpoint({"HostReleased", 1731086574782, 166280}),
Checkpoint({"UserspaceEnd", 1731086967609, 448680}),
},
.durations =
{
Duration({"BootToHost", 166230}),
Duration({"Firmware", 44330}),
Duration({"Loader", 0}),
Duration({"Kernel", 8231}),
Duration({"InitRD", 0}),
Duration({"Userspace", 438132}),
},
.expectedBreakdown = {{"BootToHost", 166.3}, {"Shutdown", 0.15}},
.expectedDurations =
{
{"BootToHost", 166.3},
{"Firmware", 44.3},
{"Loader", 0},
{"Kernel", 8.231},
{"InitRD", 0},
{"Userspace", 438.1},
},
.expectedRebootFlow =
{
{"RebootStart", "2024-11-08T17:20:09.343+00:00", 948870},
{"TrayPowerCycle", "2024-11-08T17:20:09.499+00:00", 949020},
{"HostReleased", "2024-11-08T17:22:54.782+00:00", 166280},
{"UserspaceEnd", "2024-11-08T17:29:27.609+00:00", 448680},
},
.expectedStat = {"AC", true, nlohmann::json(), nlohmann::json()},
.expectedStatus = {{"Health", "Ok"}, {"State", "Enabled"}},
.expectedTotal = 558.266,
.expectedStatusCode = absl::StatusCode::kOk,
},
BootTimeTestCase{
.name = "empty",
.checkpoints = {},
.durations = {},
.expectedBreakdown = {},
.expectedDurations = {},
.expectedRebootFlow = {},
.expectedStat = {"Invalid", true, nlohmann::json(), nlohmann::json()},
.expectedStatus = {{"Health", "Warning"}, {"State", "Enabled"}},
.expectedTotal = 0.0,
.expectedStatusCode = absl::StatusCode::kOk,
},
};
for (auto testCase : testCases) {
prepareTestDir(testDir);
writeCheckpoint(
absl::StrCat(testDir, "bmc", boot_time_monitor::def::kCheckpointFile),
testCase.checkpoints);
writeDuration(
absl::StrCat(testDir, "bmc", boot_time_monitor::def::kDurationFile),
testCase.durations);
boot_time_monitor::NodeConfig nc{
.node_name = "bmc",
.tag = std::string(boot_time_monitor::kBootTimeTagBMC)};
EXPECT_EQ(PopulateBmcData(share_async_resp_).code(),
testCase.expectedStatusCode);
RunIoUntilDone();
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
EXPECT_EQ(share_async_resp_->res.jsonValue["@odata.id"],
"/redfish/v1/Managers/bmc/Oem/Google/BootTime");
EXPECT_EQ(share_async_resp_->res.jsonValue["Status"]["Health"],
testCase.expectedStatus["Health"]);
EXPECT_EQ(share_async_resp_->res.jsonValue["Status"]["State"],
testCase.expectedStatus["State"]);
// Response is only meaningful in ok status.
if (testCase.expectedStatusCode == absl::StatusCode::kOk) {
BootTimeTestResponseCheck(testCase, share_async_resp_);
}
}
}
} // namespace
} // namespace redfish::boottime