blob: f643a1aac49994459964fe5489725e8114a54375 [file]
/**
* @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
std::string mockScript = std::string(xstr(BUILDDIR)) +
"/../../tests/mock_mctpd.py";
// clang-format off
execlp("python3", "python3", mockScript.c_str(), nullptr); // NOLINT(cppcoreguidelines-pro-type-vararg)
// clang-format on
exit(1);
}
// Give mock some time to start and claim names!
sleep(1); // 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);
// 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 xyz.openbmc_project.Mctp /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);
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 xyz.openbmc_project.Mctp /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 xyz.openbmc_project.Mctp /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);
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 xyz.openbmc_project.Mctp /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 xyz.openbmc_project.Mctp /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 < 50; 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());
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}