| /** |
| * @file test_MctpUtil.cpp |
| * @brief Unit tests for MctpUtil.cpp |
| * |
| * These tests verify the deferred state handling in MctpUtil when processing |
| * MCTP endpoint discovery events. Specifically, it tests that: |
| * 1. A single Add event populates the map. |
| * 2. A Remove event arriving while a query is in progress correctly cancels |
| * or rolls back the operation, leaving the map empty. |
| * 3. An Add -> Remove -> Add sequence in rapid succession correctly schedules |
| * a new query and leaves the map populated at the end. |
| * |
| * Synchronization Design: |
| * To avoid impacting production code, the tests use custom D-Bus signals |
| * emitted by the mock server (mock_mctpd.py) to know exactly when a query |
| * starts and completes. This allows precise orchestration of events without |
| * sleeps or production code hooks. |
| * |
| * The tests run in a single thread using `io.poll()` to match the production |
| * execution model. |
| */ |
| #include "MctpUtil.hpp" |
| #include "Utils.hpp" |
| |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include <boost/asio/io_context.hpp> |
| #include <sdbusplus/asio/connection.hpp> |
| #include <sdbusplus/asio/object_server.hpp> |
| #include <sdbusplus/bus/match.hpp> |
| |
| #include <array> |
| #include <cstdlib> |
| #include <iostream> |
| #include <memory> |
| #include <string> |
| #include <thread> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| |
| #define xstr(s) str(s) // NOLINT |
| #define str(s) #s // NOLINT |
| |
| static void runCmd(const std::string& cmd) |
| { |
| std::string finalCmd = cmd; |
| const char* dbusAddr = std::getenv("DBUS_SESSION_BUS_ADDRESS"); |
| if (dbusAddr != nullptr) |
| { |
| size_t pos = finalCmd.find("busctl "); |
| if (pos != std::string::npos) |
| { |
| finalCmd.replace(pos, 7, |
| "busctl --address=" + std::string(dbusAddr) + " "); |
| } |
| } |
| int rc = system(finalCmd.c_str()); // NOLINT(cert-env33-c) |
| ASSERT_EQ(rc, 0); |
| } |
| |
| class MctpUtilTest : public ::testing::Test |
| { |
| protected: |
| boost::asio::io_context io; |
| std::shared_ptr<sdbusplus::asio::connection> conn; |
| pid_t mockPid = -1; |
| |
| void SetUp() override |
| { |
| io.restart(); |
| conn = std::make_shared<sdbusplus::asio::connection>(io); |
| |
| // Clear global map before each test |
| mctpEndpointConfigMap.clear(); |
| std::cout << "--- Starting Python Mock ---\n"; |
| mockPid = fork(); |
| if (mockPid == 0) |
| { |
| // Child process |
| // clang-format off |
| std::string scriptPath = TEST_SRC_DIR "/mock_mctpd.py"; |
| execlp("python3", "python3", scriptPath.c_str(), nullptr); // NOLINT(cppcoreguidelines-pro-type-vararg) |
| // clang-format on |
| exit(1); |
| } |
| // Give mock some time to start and claim names! |
| sleep(3); // NOLINT |
| } |
| |
| void TearDown() override |
| { |
| cleanupMctpEndpointListener(); |
| if (mockPid > 0) |
| { |
| kill(mockPid, SIGTERM); |
| waitpid(mockPid, nullptr, 0); |
| } |
| } |
| |
| static bool checkPathInMapper(const std::string& path) |
| { |
| std::string cmd = "busctl tree xyz.openbmc_project.ObjectMapper"; |
| const char* dbusAddr = std::getenv("DBUS_SESSION_BUS_ADDRESS"); |
| if (dbusAddr != nullptr) |
| { |
| cmd = "busctl --address=" + std::string(dbusAddr) + |
| " tree xyz.openbmc_project.ObjectMapper"; |
| } |
| FILE* pipe = popen(cmd.c_str(), "r"); // NOLINT(cert-env33-c) |
| if (pipe == nullptr) |
| { |
| return false; |
| } |
| std::array<char, 128> buffer{}; |
| std::string result; |
| while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) |
| { |
| result += buffer.data(); |
| } |
| pclose(pipe); |
| return result.find(path) != std::string::npos; |
| } |
| }; |
| |
| // 1. A single Add event will trigger only one O-M query. And add the correct |
| // result in mctpEndpointConfigMap |
| TEST_F(MctpUtilTest, SingleAdd_PopulatesMap) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test1"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| // 2.a setupMctpEndpointListener() first in the test. |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| // 2.b setup the busctl event listener to all O-M event. |
| // We use a C++ match listener for all InterfacesAdded signals! |
| int queryCompletedCount = 0; |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { queryCompletedCount++; }); |
| |
| // Wait for priming loop to complete (finding nothing) |
| io.poll(); |
| usleep(100000); // 100ms |
| io.poll(); |
| |
| // Trigger Add event via D-Bus! |
| std::string addCmd = |
| "busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" + |
| endpointPath + "\" \"" + emConfigPath + "\""; |
| runCmd(addCmd); |
| |
| // Wait for query to complete with a timeout (up to 5 seconds) |
| for (int i = 0; i < 50; i++) |
| { |
| io.poll(); |
| usleep(100000); // NOLINT(cert-api50-c) |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| |
| // Wait a bit more to see if any extra signals arrive |
| usleep(500000); // 500ms |
| io.poll(); |
| |
| EXPECT_EQ(queryCompletedCount, 1); |
| |
| // Wait a bit more for MctpUtil to process the reply! |
| usleep(100000); // 100ms |
| |
| // Verify map populated |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it, mctpEndpointConfigMap.end()); |
| EXPECT_FALSE(it->second.empty()); |
| } |
| |
| // 2. Add -> Remove during query |
| TEST_F(MctpUtilTest, AddRemove_ClearsMap) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test2"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { queryStarted = true; }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { queryCompletedCount++; }); |
| |
| // Wait for priming loop to complete (finding nothing) |
| io.poll(); |
| usleep(100000); // 100ms |
| io.poll(); |
| |
| // Trigger Add event |
| std::string addCmd = |
| "busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" + |
| endpointPath + "\" \"" + emConfigPath + "\""; |
| runCmd(addCmd); |
| |
| // Wait for query to start |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| // IMMEDIATELY trigger Remove event (while query is in progress!) |
| std::string removeCmd = |
| "busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerRemove s \"" + |
| endpointPath + "\""; |
| runCmd(removeCmd); |
| |
| // Wait for query to complete with a timeout (up to 5 seconds) |
| for (int i = 0; i < 50; i++) |
| { |
| io.poll(); |
| usleep(100000); // NOLINT(cert-api50-c) |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| |
| // Wait a bit more to see if any extra signals arrive |
| usleep(500000); // 500ms |
| io.poll(); |
| |
| EXPECT_EQ(queryCompletedCount, 1); |
| |
| // Verify map is empty |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| EXPECT_TRUE(it == mctpEndpointConfigMap.end()); |
| } |
| |
| // 3. Add -> Remove -> Add during query |
| TEST_F(MctpUtilTest, AddRemoveAdd_PopulatesMap) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test3"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { queryStarted = true; }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { queryCompletedCount++; }); |
| |
| // Wait for priming loop to complete (finding nothing) |
| io.poll(); |
| usleep(100000); // 100ms |
| io.poll(); |
| |
| // Trigger Add event |
| std::string addCmd = |
| "busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" + |
| endpointPath + "\" \"" + emConfigPath + "\""; |
| runCmd(addCmd); |
| |
| // Wait for query to start |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| // IMMEDIATELY trigger Remove event |
| std::string removeCmd = |
| "busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerRemove s \"" + |
| endpointPath + "\""; |
| runCmd(removeCmd); |
| |
| // IMMEDIATELY trigger Add event again! |
| runCmd(addCmd); |
| |
| // Wait for queries to complete with a timeout (up to 5 seconds) |
| for (int i = 0; i < 70; i++) |
| { |
| io.poll(); |
| usleep(100000); // NOLINT(cert-api50-c) |
| if (queryCompletedCount >= 2) |
| { |
| break; |
| } |
| } |
| |
| // Wait a bit more to see if any extra signals arrive |
| usleep(500000); // 500ms |
| io.poll(); |
| |
| EXPECT_EQ(queryCompletedCount, 2); |
| |
| // Verify map is populated at the end |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it, mctpEndpointConfigMap.end()); |
| EXPECT_FALSE(it->second.empty()); |
| } |
| |
| // 4. Race between priming query and new event |
| TEST_F(MctpUtilTest, PrimeQueryRace_TriggersDeferredQuery) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test4"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| // Pre-populate association by triggering it BEFORE listener starts! |
| auto m = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd"); |
| m.append(endpointPath, emConfigPath); |
| conn->call(m); |
| |
| // Wait for mapperx to see it |
| bool found = false; |
| for (int i = 0; i < 300; i++) |
| { |
| if (checkPathInMapper(endpointPath + "/configured_by")) |
| { |
| found = true; |
| break; |
| } |
| usleep(100000); // 100ms |
| } |
| ASSERT_TRUE(found); |
| |
| // Now setup listener (this starts priming!) |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received\n"; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received\n"; |
| queryCompletedCount++; |
| }); |
| |
| // Wait for query to start (triggered by priming!) |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| std::string endpointPath5 = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test5"; |
| std::cout << "[DEBUG] Triggering Add for test5 (racing!)\n"; |
| // Trigger Add event for a DIFFERENT endpoint while priming query is in |
| // progress! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << "\n"; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath5, emConfigPath); |
| |
| // Wait for queries to complete (we expect 2!) |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 2) |
| { |
| break; |
| } |
| } |
| |
| // Wait a bit more to see if any extra signals arrive |
| usleep(500000); // 500ms |
| io.poll(); |
| |
| EXPECT_EQ(queryCompletedCount, 2); |
| |
| // Verify map populated for both endpoints! |
| EXPECT_EQ(mctpEndpointConfigMap.size(), 2); |
| |
| auto it4 = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it4, mctpEndpointConfigMap.end()); |
| EXPECT_FALSE(it4->second.empty()); |
| |
| auto it5 = mctpEndpointConfigMap.find(endpointPath5); |
| ASSERT_NE(it5, mctpEndpointConfigMap.end()); |
| EXPECT_FALSE(it5->second.empty()); |
| } |
| |
| // 5. During Priming, Remove on the single endpoint |
| TEST_F(MctpUtilTest, PrimeQueryRace_RemovePostponed) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test5"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| // Pre-populate association by triggering it BEFORE listener starts! |
| auto m = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd"); |
| m.append(endpointPath, emConfigPath); |
| conn->call(m); |
| |
| // Wait for mapperx to see it |
| bool found = false; |
| for (int i = 0; i < 300; i++) |
| { |
| if (checkPathInMapper(endpointPath + "/configured_by")) |
| { |
| found = true; |
| break; |
| } |
| usleep(100000); // 100ms |
| } |
| ASSERT_TRUE(found); |
| |
| // Now setup listener (this starts priming!) |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received\n"; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received\n"; |
| queryCompletedCount++; |
| }); |
| |
| // Wait for query to start (triggered by priming!) |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| std::cout << "[DEBUG] Triggering Remove (racing!)\n"; |
| // Trigger Remove event while priming query is in progress! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << "\n"; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| // Wait for queries to complete (we expect 1!) |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| |
| EXPECT_EQ(queryCompletedCount, 1); |
| |
| // Verify map is EMPTY (because it was removed!) |
| EXPECT_EQ(mctpEndpointConfigMap.size(), 0); |
| |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| EXPECT_EQ(it, mctpEndpointConfigMap.end()); |
| } |
| |
| // 6. During Priming, Remove -> Add on the single endpoint |
| TEST_F(MctpUtilTest, PrimeQueryRace_RemoveAddPostponed) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test6"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| // Pre-populate association by triggering it BEFORE listener starts! |
| auto m = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd"); |
| m.append(endpointPath, emConfigPath); |
| conn->call(m); |
| |
| // Wait for mapperx to see it |
| bool found = false; |
| for (int i = 0; i < 300; i++) |
| { |
| if (checkPathInMapper(endpointPath + "/configured_by")) |
| { |
| found = true; |
| break; |
| } |
| usleep(100000); // 100ms |
| } |
| ASSERT_TRUE(found); |
| |
| // Now setup listener (this starts priming!) |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received\n"; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received\n"; |
| queryCompletedCount++; |
| }); |
| |
| // Wait for query to start (triggered by priming!) |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| std::cout << "[DEBUG] Triggering Remove (racing!)\n"; |
| // Trigger Remove event! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << "\n"; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| std::cout << "[DEBUG] Triggering Add again (racing!)\n"; |
| // Trigger Add event AGAIN! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << "\n"; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Wait for queries to complete (we expect 2!) |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 2) |
| { |
| break; |
| } |
| } |
| |
| EXPECT_EQ(queryCompletedCount, 2); |
| |
| // Verify map populated |
| EXPECT_EQ(mctpEndpointConfigMap.size(), 1); |
| |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it, mctpEndpointConfigMap.end()); |
| EXPECT_FALSE(it->second.empty()); |
| } |
| |
| // 7. Add delay for a single ep Add to Add event. i.e. Add -> Remove -> Add |
| // (this add should be cooled down). |
| TEST_F(MctpUtilTest, Cooldown_AddRemoveAdd_IsCooledDown) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test7"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received" << '\n'; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received" << '\n'; |
| queryCompletedCount++; |
| }); |
| |
| // Trigger FIRST Add! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Wait for FIRST query to complete! |
| for (int i = 0; i < 50; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| ASSERT_EQ(queryCompletedCount, 1); |
| |
| std::cout << "[DEBUG] Triggering Remove and Add (racing!)" << '\n'; |
| // Trigger Remove! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| // Trigger SECOND Add! (Immediately after!). |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Wait for SECOND query to complete! |
| // We expect it to be DELAYED by 5 seconds! |
| auto startTime = std::chrono::steady_clock::now(); |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 2) |
| { |
| break; |
| } |
| } |
| auto endTime = std::chrono::steady_clock::now(); |
| auto duration = |
| std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime) |
| .count(); |
| |
| EXPECT_EQ(queryCompletedCount, 2); |
| EXPECT_GE(duration, 4); // Expect at least 4-5 seconds delay! |
| |
| // Verify map populated |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it, mctpEndpointConfigMap.end()); |
| } |
| |
| // 8. Priming scan does block its immediate following Add. i.e. Priming -> |
| // Remove -> Add (not cooldown) |
| TEST_F(MctpUtilTest, Cooldown_PrimingRemoveAdd_IsNormalized) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test8"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| // Pre-populate association by triggering it BEFORE listener starts! |
| auto m = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd"); |
| m.append(endpointPath, emConfigPath); |
| conn->call(m); |
| |
| // Wait for mapperx to see it |
| bool found = false; |
| for (int i = 0; i < 300; i++) |
| { |
| if (checkPathInMapper(endpointPath + "/configured_by")) |
| { |
| found = true; |
| break; |
| } |
| usleep(100000); // 100ms |
| } |
| ASSERT_TRUE(found); |
| |
| // Now setup listener (this starts priming!) |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received" << '\n'; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received" << '\n'; |
| queryCompletedCount++; |
| }); |
| |
| // Wait for query to start (triggered by priming!) |
| while (!queryStarted) |
| { |
| io.poll(); |
| usleep(10000); // 10ms |
| } |
| |
| std::cout << "[DEBUG] Triggering Remove and Add (racing with priming!)" |
| << '\n'; |
| // Trigger Remove event! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| // Trigger Add event! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Wait for queries to complete! |
| auto startTime = std::chrono::steady_clock::now(); |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 2) |
| { |
| break; |
| } |
| } |
| auto endTime = std::chrono::steady_clock::now(); |
| auto duration = |
| std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime) |
| .count(); |
| |
| EXPECT_EQ(queryCompletedCount, 2); |
| EXPECT_LT(duration, |
| 8); // Expect it to complete without additional 5s cooldown! |
| |
| // Verify map populated |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| ASSERT_NE(it, mctpEndpointConfigMap.end()); |
| } |
| |
| // 9. Remove has no cooldown (i.e. Remove -> Add -> Remove (no cooldown). |
| TEST_F(MctpUtilTest, Cooldown_RemoveAddRemove_NoCooldown) |
| { |
| std::string endpointPath = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test9"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| bool queryStarted = false; |
| int queryCompletedCount = 0; |
| |
| const std::string queryStartedSpec = |
| "type='signal',interface='com.example.Control',member='QueryStarted'"; |
| auto queryStartedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryStartedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryStarted received" << '\n'; |
| queryStarted = true; |
| }); |
| |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { |
| std::cout << "[TEST] QueryCompleted received" << '\n'; |
| queryCompletedCount++; |
| }); |
| |
| // Trigger FIRST Add! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Wait for FIRST query to complete! |
| for (int i = 0; i < 50; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| ASSERT_EQ(queryCompletedCount, 1); |
| |
| std::cout << "[DEBUG] Triggering Remove -> Add -> Remove" << '\n'; |
| // Trigger Remove! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| // Trigger Add! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerAdd failed: " << ec.message() << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAdd", endpointPath, emConfigPath); |
| |
| // Trigger Remove! |
| conn->async_method_call( |
| [](const boost::system::error_code& ec, const std::string&) { |
| if (ec) |
| { |
| std::cout << "[TEST] TriggerRemove failed: " << ec.message() |
| << '\n'; |
| } |
| }, "au.com.codeconstruct.MCTP1", "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerRemove", endpointPath); |
| |
| // Wait for queries to complete! |
| for (int i = 0; i < 100; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| } |
| |
| EXPECT_EQ(queryCompletedCount, 1); |
| |
| // Verify map is EMPTY! |
| auto it = mctpEndpointConfigMap.find(endpointPath); |
| EXPECT_EQ(it, mctpEndpointConfigMap.end()); |
| } |
| |
| // 10. Filter unsupported message types |
| TEST_F(MctpUtilTest, Filter_UnsupportedMessageType_IsIgnored) |
| { |
| std::string endpointPathSupported = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/test10_supported"; |
| std::string endpointPathUnsupported = |
| "/au/com/codeconstruct/mctp1/networks/1/endpoints/" |
| "test10_unsupported"; |
| std::string emConfigPath = |
| "/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device"; |
| |
| setupMctpEndpointListener(conn, MctpMessageType::NVME_MI); |
| |
| int queryCompletedCount = 0; |
| const std::string queryCompletedSpec = |
| "type='signal',interface='com.example.Control',member='QueryCompleted'"; |
| auto queryCompletedMatch = std::make_unique<sdbusplus::bus::match_t>( |
| static_cast<sdbusplus::bus_t&>(*conn), queryCompletedSpec, |
| [&](sdbusplus::message_t&) { queryCompletedCount++; }); |
| |
| // 1. Trigger Add for SUPPORTED endpoint (type 4 included) |
| std::vector<uint8_t> msgTypesSupported = {2, 0, 4}; |
| auto m1 = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAddCustom"); |
| m1.append(endpointPathSupported, emConfigPath, msgTypesSupported); |
| conn->call(m1); |
| |
| // 2. Trigger Add for UNSUPPORTED endpoint (no type 4) |
| std::vector<uint8_t> msgTypesUnsupported = {2}; |
| auto m2 = conn->new_method_call("au.com.codeconstruct.MCTP1", |
| "/au/com/codeconstruct/mctp1", |
| "com.example.Control", "TriggerAddCustom"); |
| m2.append(endpointPathUnsupported, emConfigPath, msgTypesUnsupported); |
| conn->call(m2); |
| |
| // Wait for queries to complete (we expect exactly 1 to complete!) |
| for (int i = 0; i < 50; i++) |
| { |
| while (io.poll() > 0) |
| {} // Drain all ready handlers! |
| usleep(100000); // 100ms |
| if (queryCompletedCount >= 1) |
| { |
| break; |
| } |
| } |
| |
| EXPECT_EQ(queryCompletedCount, 1); |
| |
| // Verify map size is 1 |
| EXPECT_EQ(mctpEndpointConfigMap.size(), 1); |
| |
| // Verify supported endpoint is present |
| auto itSupported = mctpEndpointConfigMap.find(endpointPathSupported); |
| EXPECT_NE(itSupported, mctpEndpointConfigMap.end()); |
| |
| // Verify unsupported endpoint is NOT present |
| auto itUnsupported = mctpEndpointConfigMap.find(endpointPathUnsupported); |
| EXPECT_EQ(itUnsupported, mctpEndpointConfigMap.end()); |
| } |
| |
| int main(int argc, char** argv) |
| { |
| ::testing::InitGoogleTest(&argc, argv); |
| return RUN_ALL_TESTS(); |
| } |