| #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 |