| #include "absl/cleanup/cleanup.h" |
| #include "bmcweb_config.h" |
| #include "boottime_api/boottime_api.h" |
| #include "boottime_api/resource.h" |
| |
| #include "app.hpp" |
| #include "async_resp.hpp" |
| #include "boottime.hpp" |
| #include "http_response.hpp" |
| #include "managers.hpp" |
| #include "nlohmann/json.hpp" |
| #include "snapshot_fixture.hpp" |
| #include "systems.hpp" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include <gmock/gmock.h> // IWYU pragma: keep |
| #include <gtest/gtest.h> // IWYU pragma: keep |
| |
| // IWYU pragma: no_include <gtest/gtest-message.h> |
| // IWYU pragma: no_include <gtest/gtest-test-part.h> |
| // IWYU pragma: no_include "gtest/gtest_pred_impl.h" |
| // IWYU pragma: no_include <gmock/gmock-matchers.h> |
| // IWYU pragma: no_include <gtest/gtest-matchers.h> |
| |
| namespace redfish::boottime |
| { |
| namespace |
| { |
| |
| using ::dbus::utility::DbusVariantType; |
| using ::managedStore::KeyType; |
| using ::managedStore::ManagedType; |
| using ::managedStore::SimulateFailedAsyncPostDbusCallThreadSafeAction; |
| using ::managedStore:: |
| SimulateFailedAsyncPostDbusCallThreadSafeWithEmptyValueAction; |
| using ::managedStore:: |
| SimulateSuccessfulAsyncPostDbusCallThreadSafeWithValueAction; |
| using ::managedStore::ValueType; |
| using ::testing::_; |
| using ::testing::An; |
| |
| 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()) |
| { |
| LOG(ERROR) << "Error: Could not open the file " << filename; |
| FAIL(); |
| 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()) |
| { |
| LOG(ERROR) << "Error: Could not open the file " << filename; |
| FAIL(); |
| 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) |
| { |
| LOG(WARNING) |
| << "Remove all test files failed in TearDownTestSuite() " |
| << ec.message(); |
| } |
| } |
| |
| 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'; |
| continue; |
| } |
| 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'; |
| continue; |
| } |
| 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"]); |
| } |
| EXPECT_EQ(expectedRebootFlow.size(), index) |
| << "Not all of the expected RebootFlow are found in the response"; |
| |
| // Stat |
| EXPECT_TRUE(share_async_resp_->res.jsonValue.contains("Stat")); |
| auto stat = share_async_resp_->res.jsonValue["Stat"]; |
| EXPECT_TRUE(stat.contains("PowerSource")); |
| EXPECT_TRUE(stat.contains("IsRebooting")); |
| auto [powerSource, rebooting, upTimestamp, loaderTimestamp] = |
| testCase.expectedStat; |
| |
| EXPECT_EQ(stat["PowerSource"], powerSource); |
| EXPECT_EQ(stat["IsRebooting"], rebooting); |
| EXPECT_EQ(stat["UpTime"], nlohmann::json()); |
| EXPECT_EQ(stat["UpTimestamp"], upTimestamp); |
| EXPECT_EQ(stat["LoaderReadyTimestamp"], loaderTimestamp); |
| |
| // Total |
| EXPECT_EQ(testCase.expectedTotal, |
| share_async_resp_->res.jsonValue["Total"]); |
| } |
| |
| TEST_F(BootTimeTest, SystemBootTime) |
| { |
| 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_TRUE( |
| 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"]); |
| |
| std::cout << nlohmann::json(share_async_resp_->res.jsonValue).dump() |
| << '\n' |
| << '\n'; |
| |
| // 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_TRUE(PopulateBmcData(share_async_resp_).code() == |
| testCase.expectedStatusCode); |
| |
| RunIoUntilDone(); |
| |
| std::cout << nlohmann::json(share_async_resp_->res.jsonValue).dump() |
| << '\n' |
| << '\n'; |
| 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_); |
| } |
| } |
| } |
| |
| TEST_F(BootTimeTest, ParseSystemNameToIndex) |
| { |
| // Single host |
| EXPECT_TRUE(parseSystemNameToIndex("system", false).ok()); |
| EXPECT_EQ(parseSystemNameToIndex("system", false).value_or(-1), 0); |
| |
| EXPECT_FALSE(parseSystemNameToIndex("system0", false).ok()); |
| EXPECT_FALSE(parseSystemNameToIndex("wrongName", false).ok()); |
| EXPECT_FALSE(parseSystemNameToIndex("system1", false).ok()); |
| EXPECT_FALSE(parseSystemNameToIndex("system2", false).ok()); |
| |
| // Multiple hosts |
| EXPECT_FALSE(parseSystemNameToIndex("system", true).ok()); |
| EXPECT_FALSE(parseSystemNameToIndex("system0", true).ok()); |
| EXPECT_FALSE(parseSystemNameToIndex("wrongName", true).ok()); |
| |
| EXPECT_TRUE(parseSystemNameToIndex("system1", true).ok()); |
| EXPECT_EQ(parseSystemNameToIndex("system1", true).value_or(-1), 0); |
| |
| EXPECT_TRUE(parseSystemNameToIndex("system2", true).ok()); |
| EXPECT_EQ(parseSystemNameToIndex("system2", true).value_or(-1), 1); |
| } |
| |
| } // namespace |
| } // namespace redfish::boottime |