blob: a531f8948c341bddc4e26712e3d6f872cbcc597d [file] [log] [blame]
#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