blob: 7726c4f9cd5bcfacbb127a0f3a11802612d6977f [file]
#include "health.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include "http_response.hpp"
#include "dbus_utility.hpp"
#include "test/redfish-core/lib/snapshot_fixture.hpp"
#include <nlohmann/json.hpp>
#include "managed_store.hpp"
#include "managed_store_types.hpp"
#include "test/g3/mock_managed_store.hpp"
#include "sdbusplus/message/native_types.hpp"
namespace redfish {
namespace {
using ::dbus::utility::DBusInteracesMap;
using ::dbus::utility::DBusPropertiesMap;
using ::dbus::utility::DbusVariantType;
using ::dbus::utility::ManagedObjectType;
using ::dbus::utility::MapperGetSubTreeResponse;
using ::dbus::utility::MapperServiceMap;
using ::managedStore::KeyType;
using ::managedStore::ManagedType;
TEST_F(SnapshotFixture, HealthPopulateFillsOkStatusWhenNoErrors) {
dbus::utility::MapperGetSubTreePathsResponse healthPaths = {
"/xyz/openbmc_project/inventory/system/board/LeakDetector_0/"
"EMMC_PRSNT"};
sdbusplus::message::object_path healthPathObj(healthPaths[0]);
DBusPropertiesMap configProperties{
{"Name", DbusVariantType("EMMC_PRSNT")},
{"Severity", DbusVariantType("Critical")},
{"BoardName", DbusVariantType("LeakDetector_0")},
{"FailureMessageId",
DbusVariantType("ResourceEvent.1.0.ResourceErrorsDetected")},
{"OkMessageId",
DbusVariantType("ResourceEvent.1.0.ResourceErrorsCorrected")},
{"MessageArgs",
DbusVariantType(std::vector<std::string>{"Fan0", "Efuse failure"})},
{"Resolution", DbusVariantType("Replace")},
{"RelatedEntityName", DbusVariantType("LeakDetector_0")},
{"RelatedEntityType", DbusVariantType("Board")},
{"ODataID", DbusVariantType("/redfish/v1/Chassis/LeakDetector_0")}};
ASSERT_TRUE(
dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.EntityManager", healthPathObj,
"xyz.openbmc_project.Configuration.HealthMonitor"),
CreateValueType(configProperties))
.ok());
DBusPropertiesMap statusProperties{
{"healthy", DbusVariantType(true)},
{"toggle_count", DbusVariantType(uint64_t{0})}};
sdbusplus::message::object_path statusPath(
"/xyz/openbmc_project/HealthMonitor/EMMC_PRSNT");
ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.HealthMonitor", statusPath,
"xyz.openbmc_project.HealthMonitor.GetStatus"),
CreateValueType(statusProperties))
.ok());
auto health = std::make_shared<HealthPopulate>(share_async_resp_);
health->populate();
RunIoUntilDone();
health.reset(); // Destroy HealthPopulate to trigger destructor
RunIoUntilDone(); // Drain any callbacks posted by destructor
nlohmann::json& json = share_async_resp_->res.jsonValue;
EXPECT_EQ(json["Status"]["Health"], "OK");
EXPECT_EQ(json["Status"]["HealthRollup"], "OK");
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
}
TEST_F(SnapshotFixture, HealthPopulateConditions) {
dbus::utility::MapperGetSubTreePathsResponse healthPaths = {
"/xyz/openbmc_project/inventory/system/board/LeakDetector_0/"
"EMMC_PRSNT"};
sdbusplus::message::object_path healthPathObj(healthPaths[0]);
ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedSubtreePaths,
"/xyz/openbmc_project/inventory", 0,
{"xyz.openbmc_project.Configuration."
"HealthMonitor"}),
CreateValueType(healthPaths))
.ok());
DBusPropertiesMap configProperties{
{"Name", DbusVariantType("EMMC_PRSNT")},
{"Severity", DbusVariantType("Critical")},
{"BoardName", DbusVariantType("LeakDetector_0")},
{"FailureMessageId",
DbusVariantType("ResourceEvent.1.0.ResourceErrorsDetected")},
{"OkMessageId",
DbusVariantType("ResourceEvent.1.0.ResourceErrorsCorrected")},
{"MessageArgs",
DbusVariantType(std::vector<std::string>{"Fan0", "Efuse failure"})},
{"Resolution", DbusVariantType("Replace")},
{"RelatedEntityName", DbusVariantType("LeakDetector_0")},
{"RelatedEntityType", DbusVariantType("Board")},
{"ODataID", DbusVariantType("/redfish/v1/Chassis/LeakDetector_0")}};
ASSERT_TRUE(
dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.EntityManager", healthPathObj,
"xyz.openbmc_project.Configuration.HealthMonitor"),
CreateValueType(configProperties))
.ok());
DBusPropertiesMap statusProperties{
{"healthy", DbusVariantType(false)},
{"toggle_count", DbusVariantType(uint64_t{1})}};
sdbusplus::message::object_path statusPath(
"/xyz/openbmc_project/HealthMonitor/EMMC_PRSNT");
ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.HealthMonitor", statusPath,
"xyz.openbmc_project.HealthMonitor.GetStatus"),
CreateValueType(statusProperties))
.ok());
HealthStatusCache::getInstance().getHealthAttributes(healthPaths[0]);
RunIoUntilDone();
auto health = std::make_shared<HealthPopulate>(
share_async_resp_, "/redfish/v1/Chassis/LeakDetector_0",
"/xyz/openbmc_project/inventory/system/board/LeakDetector_0", "Board",
"LeakDetector_0");
health->populate();
RunIoUntilDone();
health.reset(); // Destroy HealthPopulate to trigger destructor
nlohmann::json& json = share_async_resp_->res.jsonValue;
EXPECT_EQ(json["Status"]["Health"], "Critical");
EXPECT_EQ(json["Status"]["HealthRollup"], "Critical");
ASSERT_EQ(json["Status"]["Conditions"].size(), 1);
EXPECT_EQ(json["Status"]["Conditions"][0]["MessageId"],
"ResourceEvent.1.0.ResourceErrorsDetected");
EXPECT_EQ(json["Status"]["Conditions"][0]["Severity"], "Critical");
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
}
TEST_F(SnapshotFixture,
HealthPopulateCorrectlyProcessesChildrenAndAssociations) {
// This test is designed to improve code coverage by exercising complex
// logic in the HealthPopulate destructor. It verifies the handling of:
// - Direct child inventory items.
// - Items associated with child inventory via 'endpoints'.
// - A 'self' path health status.
// - An item that is not a child and has no relevant association (should be
// skipped).
// - Malformed association data.
auto health = std::make_shared<HealthPopulate>(share_async_resp_);
// 1. Define inventory, including 'self' and 'child' items.
const std::string selfPath = "/xyz/openbmc_project/inventory/system/chassis";
const std::string childPath1 = "/xyz/openbmc_project/inventory/system/cpu0";
const std::string childPath2 = "/xyz/openbmc_project/inventory/system/dimm4";
health->selfPath = selfPath;
health->inventory = {childPath1, childPath2};
health->isManagersHealth = false;
// 2. Build the 'statuses' map to trigger different logic paths.
// The types are vector<pair<...>>, so we use emplace_back.
const std::string criticalAssocPath =
"/xyz/openbmc_project/associations/cpu0_thermals/status/critical";
const std::string selfWarningPath = selfPath + "/status/warning";
const std::string skippedAssocPath =
"/xyz/openbmc_project/associations/unrelated/status/critical";
const std::string malformedAssocPath =
"/xyz/openbmc_project/associations/malformed/status/critical";
// Case A: A critical status on a path associated with a child inventory
// item.
DBusPropertiesMap criticalAssocProperties;
criticalAssocProperties.emplace_back(
"endpoints", DbusVariantType(std::vector<std::string>{childPath1}));
DBusInteracesMap criticalAssocInterfaces;
criticalAssocInterfaces.emplace_back("xyz.openbmc_project.Association",
criticalAssocProperties);
// Case B: A warning status directly on the 'self' path.
DBusInteracesMap selfWarningInterfaces; // Empty is sufficient
// Case C: An association pointing to a non-child item.
DBusPropertiesMap skippedAssocProperties;
skippedAssocProperties.emplace_back(
"endpoints", DbusVariantType(std::vector<std::string>{
"/xyz/openbmc_project/inventory/system/fan1"}));
DBusInteracesMap skippedAssocInterfaces;
skippedAssocInterfaces.emplace_back("xyz.openbmc_project.Association",
skippedAssocProperties);
// Case D: An association with a malformed 'endpoints' property.
DBusPropertiesMap malformedAssocProperties;
malformedAssocProperties.emplace_back(
"endpoints", DbusVariantType(std::string("this is not a vector")));
DBusInteracesMap malformedAssocInterfaces;
malformedAssocInterfaces.emplace_back("xyz.openbmc_project.Association",
malformedAssocProperties);
// Construct the final ManagedObjectType vector
health->statuses.emplace_back(
sdbusplus::message::object_path(criticalAssocPath),
criticalAssocInterfaces);
health->statuses.emplace_back(
sdbusplus::message::object_path(selfWarningPath), selfWarningInterfaces);
health->statuses.emplace_back(
sdbusplus::message::object_path(skippedAssocPath),
skippedAssocInterfaces);
health->statuses.emplace_back(
sdbusplus::message::object_path(malformedAssocPath),
malformedAssocInterfaces);
// 3. Trigger the destructor where the health calculation logic resides.
health.reset();
// 4. Verify the final calculated health status.
nlohmann::json& json = share_async_resp_->res.jsonValue;
// The main 'Health' is "Warning" because of Case B (direct self path).
EXPECT_EQ(json["Status"]["Health"], "Warning");
// The 'HealthRollup' is "Critical" because of Case A, which has higher
// severity.
EXPECT_EQ(json["Status"]["HealthRollup"], "Critical");
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
}
TEST_F(SnapshotFixture, ProcessHealthStatusChange) {
// 1. Setup
HealthStatusCache& cache = HealthStatusCache::getInstance();
cache.clear();
// Define D-Bus paths and properties for a mock health criteria
const std::string criteriaPath =
"/xyz/openbmc_project/inventory/system/board/TestBoard/TestCriteria";
sdbusplus::message::object_path criteriaPathObj(criteriaPath);
const DBusPropertiesMap configProperties{
{"Name", DbusVariantType("TestCriteria")},
{"Severity", DbusVariantType("Warning")},
{"FailureMessageId", DbusVariantType("Test.Failure.Message")},
{"OkMessageId", DbusVariantType("Test.OK.Message")},
{"MessageArgs",
DbusVariantType(std::vector<std::string>{"Power", "Fault"})},
{"Resolution", DbusVariantType("Replace the unit.")},
{"RelatedEntityName", DbusVariantType("TestBoard")},
{"RelatedEntityType", DbusVariantType("Board")}};
ASSERT_TRUE(
dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.EntityManager", criteriaPathObj,
"xyz.openbmc_project.Configuration.HealthMonitor"),
CreateValueType(configProperties))
.ok());
const DBusPropertiesMap initialStatusProperties{
{"healthy", DbusVariantType(true)},
{"toggle_count", DbusVariantType(uint64_t{0})}};
sdbusplus::message::object_path statusPath(
"/xyz/openbmc_project/HealthMonitor/TestCriteria");
ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>(
managedStore::GetManagedObjectStore())
->upsertMockObjectIntoManagedStore(
KeyType(ManagedType::kManagedPropertyMap,
"xyz.openbmc_project.HealthMonitor", statusPath,
"xyz.openbmc_project.HealthMonitor.GetStatus"),
CreateValueType(initialStatusProperties))
.ok());
// Populate the cache by calling the method that reads from mocked D-Bus
cache.getHealthAttributes(criteriaPath);
RunIoUntilDone();
// 2. First call: Simulates a health change from Healthy to Unhealthy
cache.processHealthStatusChange(criteriaPath);
// 3. Verification 1: Check the generated log entry
nlohmann::json logEntryArray;
cache.fillLogEntries(logEntryArray, "system");
ASSERT_EQ(logEntryArray.size(), 1);
const nlohmann::json& log1 = logEntryArray[0];
EXPECT_EQ(log1["MessageId"], "Test.Failure.Message");
EXPECT_EQ(log1["Severity"], "Warning");
EXPECT_EQ(log1["Resolved"], false);
// toggleCount becomes 1, so ErrorCount is (1+1)/2 = 1
EXPECT_EQ(log1["ErrorCount"], 1);
// 4. Second call: Simulates a health change from Unhealthy to Healthy
cache.processHealthStatusChange(criteriaPath);
// 5. Verification 2: Check the new (resolved) log entry
cache.fillLogEntries(logEntryArray, "system");
ASSERT_EQ(logEntryArray.size(), 2);
const nlohmann::json& log2 = logEntryArray[1];
EXPECT_EQ(log2["MessageId"], "Test.OK.Message");
EXPECT_EQ(log2["Severity"], "OK");
EXPECT_EQ(log2["Resolved"], true);
EXPECT_EQ(log2["ResolvedEntryId"], "0");
// ErrorCount should not be present in a resolution log entry
EXPECT_FALSE(log2.contains("ErrorCount"));
}
TEST_F(SnapshotFixture, HealthPopulateCorrectlyHandlesAllStatusPathTypes) {
// This test covers the complex branching logic in the HealthPopulate
// destructor by checking the precedence of different status path types.
// The expected outcome is that a 'self' warning sets the main Health,
// but a non-global, non-self critical status sets the final Rollup.
auto health = std::make_shared<HealthPopulate>(share_async_resp_);
// 1. Setup paths to simulate different inventory types.
health->globalInventoryPath = "/global/inventory";
health->selfPath = "/self/path";
health->isManagersHealth = false;
health->inventory = {}; // Keep inventory empty to isolate path logic.
// 2. Define various status paths to trigger all logic branches.
// This path should set rollup to "Warning" initially.
const std::string nonGlobalWarningPath = "/some/other/path/warning";
// This path should set health to "Warning" because isSelf is true.
const std::string selfWarningPath = "/self/path/status/warning";
// This path should upgrade rollup to "Critical". Health remains "Warning".
const std::string nonGlobalCriticalPath = "/another/unrelated/path/critical";
// This path would set health to "Warning", but the critical rollup above
// should prevent the rollup from being downgraded.
const std::string globalWarningPath = "/global/inventory/item/warning";
// 3. Populate the statuses map. Order matters for testing precedence.
health->statuses.emplace_back(
sdbusplus::message::object_path(nonGlobalWarningPath),
DBusInteracesMap{});
health->statuses.emplace_back(
sdbusplus::message::object_path(selfWarningPath), DBusInteracesMap{});
health->statuses.emplace_back(
sdbusplus::message::object_path(nonGlobalCriticalPath),
DBusInteracesMap{});
health->statuses.emplace_back(
sdbusplus::message::object_path(globalWarningPath), DBusInteracesMap{});
// 4. Trigger the destructor to run the health calculation.
health.reset();
// 5. Verify the final computed health status.
nlohmann::json& json = share_async_resp_->res.jsonValue;
// Health is "Warning" due to the selfWarningPath.
EXPECT_EQ(json["Status"]["Health"], "Warning");
// HealthRollup is "Critical" due to the nonGlobalCriticalPath.
EXPECT_EQ(json["Status"]["HealthRollup"], "Critical");
EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);
}
TEST_F(SnapshotFixture, HealthPopulateReturnsEarlyOnCriticalPaths) {
// This test verifies that the health calculation loop terminates early
// when a critical status is encountered, as specified by the 'return'
// statements in the logic.
// Scenario 1: Global Critical Path
{
auto health = std::make_shared<HealthPopulate>(share_async_resp_);
health->globalInventoryPath = "/global/inventory";
// The first path is global and critical, which should cause an
// immediate return. The second warning path should never be processed.
health->statuses.emplace_back(
sdbusplus::message::object_path("/global/inventory/item/critical"),
DBusInteracesMap{});
health->statuses.emplace_back(
sdbusplus::message::object_path("/some/other/path/warning"),
DBusInteracesMap{});
health.reset(); // Trigger destructor
nlohmann::json& json = share_async_resp_->res.jsonValue;
EXPECT_EQ(json["Status"]["Health"], "Critical");
EXPECT_EQ(json["Status"]["HealthRollup"], "Critical");
}
// Reset response for the next scenario
SetUp();
// Scenario 2: Self Critical Path
{
auto health = std::make_shared<HealthPopulate>(share_async_resp_);
health->selfPath = "/self/path";
// The first path is a 'self' critical path, which should cause an
// immediate return. The following warning path should be ignored.
health->statuses.emplace_back(
sdbusplus::message::object_path("/self/path/status/critical"),
DBusInteracesMap{});
health->statuses.emplace_back(
sdbusplus::message::object_path("/self/path/status/warning"),
DBusInteracesMap{});
health.reset(); // Trigger destructor
nlohmann::json& json = share_async_resp_->res.jsonValue;
EXPECT_EQ(json["Status"]["Health"], "Critical");
EXPECT_EQ(json["Status"]["HealthRollup"], "Critical");
}
}
} // namespace
} // namespace redfish