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