blob: 0854d5f48f263d6bd7afe926e6ba8ffec0b543c6 [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 "MctpReactorDevice.hpp"
#include "MctpUtil.hpp"
#include "Utils.hpp"
#include <sys/prctl.h>
#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 <csignal>
#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
// Ask the kernel to automatically send SIGTERM to this process
// if the parent (the test suite) dies for any reason, including a
// segfault.
// clang-format off
prctl(PR_SET_PDEATHSIG, SIGTERM); // NOLINT(cppcoreguidelines-pro-type-vararg)
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);
}
// Wait for mock to claim names
bool ready = false;
for (int i = 0; i < 50; i++)
{
std::string cmd =
"busctl call org.freedesktop.DBus /org/freedesktop/DBus "
"org.freedesktop.DBus GetNameOwner s "
"au.com.codeconstruct.MCTP1 > /dev/null 2>&1";
const char* dbusAddr = std::getenv("DBUS_SESSION_BUS_ADDRESS");
if (dbusAddr != nullptr)
{
size_t pos = cmd.find("busctl ");
if (pos != std::string::npos)
{
cmd.replace(pos, 7,
"busctl --address=" + std::string(dbusAddr) +
" ");
}
}
int rc = system(cmd.c_str()); // NOLINT(cert-env33-c)
if (rc != -1 && WIFEXITED(rc) && WEXITSTATUS(rc) == 0)
{
ready = true;
break;
}
usleep(100000); // 100ms
}
ASSERT_TRUE(ready)
<< "Mock mctpd service au.com.codeconstruct.MCTP1 not ready!";
}
void TearDown() override
{
cleanupMctpEndpointListener();
if (mockPid > 0)
{
kill(mockPid, SIGTERM);
waitpid(mockPid, nullptr, 0);
}
// First, process any pending D-Bus replies (like GetSubTree) while conn
// is still externally referenced. This prevents conn from destructing
// inside sd_bus_process if a callback drops the last reference.
while (io.poll() > 0)
{}
// Now safely destroy the connection
conn.reset();
// Drain any cancellation handlers (e.g. async_read) queued by conn's
// destructor
while (io.poll() > 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());
}
// 11. MctpReactorDevice creation BEFORE mctpEndpointConfigMap population
// (Deferred Setup)
TEST_F(MctpUtilTest, MctpReactorDevice_DeferredSetup_USB)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/11";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
setupMctpEndpointListener(conn, MctpMessageType::NVME_MI);
// Create BusInfo for target device (matching what mock_mctpd.py returns for
// USB)
BusInfo targetBusInfo;
targetBusInfo["BusType"] = "USB";
targetBusInfo["Port"] = "1.5.1";
targetBusInfo["Configuration"] = "1";
targetBusInfo["InterfaceNum"] = "4";
targetBusInfo["RootHubPath"] = "/sys/devices/platform/ahb/f0806000.usb";
bool setupCompleted = false;
// Create MctpReactorDevice
auto mctpDev = std::make_shared<MctpReactorDevice>(
conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
std::nullopt);
mctpDev->setup([&](const std::error_code& ec,
const std::shared_ptr<MctpEndpoint>& ep) {
setupCompleted = true;
EXPECT_FALSE(ec);
EXPECT_NE(ep, nullptr);
});
// Verify it didn't complete immediately
io.poll();
EXPECT_FALSE(setupCompleted);
// 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 setup to complete (up to 5 seconds)
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (setupCompleted)
{
break;
}
}
EXPECT_TRUE(setupCompleted);
}
// 12. MctpReactorDevice creation AFTER mctpEndpointConfigMap population
// (Immediate Setup)
TEST_F(MctpUtilTest, MctpReactorDevice_ImmediateSetup_USB)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/12";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
setupMctpEndpointListener(conn, MctpMessageType::NVME_MI);
// Trigger Add event FIRST!
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addCmd);
// Wait for map to be populated (up to 5 seconds)
bool populated = false;
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (mctpEndpointConfigMap.find(endpointPath) !=
mctpEndpointConfigMap.end())
{
populated = true;
break;
}
}
ASSERT_TRUE(populated);
// Create BusInfo for target device
BusInfo targetBusInfo;
targetBusInfo["BusType"] = "USB";
targetBusInfo["Port"] = "1.5.1";
targetBusInfo["Configuration"] = "1";
targetBusInfo["InterfaceNum"] = "4";
targetBusInfo["RootHubPath"] = "/sys/devices/platform/ahb/f0806000.usb";
bool setupCompleted = false;
// Create MctpReactorDevice
auto mctpDev = std::make_shared<MctpReactorDevice>(
conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
std::nullopt);
mctpDev->setup([&](const std::error_code& ec,
const std::shared_ptr<MctpEndpoint>& ep) {
setupCompleted = true;
EXPECT_FALSE(ec);
EXPECT_NE(ep, nullptr);
});
// Verify it completes IMMEDIATELY
io.poll();
EXPECT_TRUE(setupCompleted);
}
// 13. MctpReactorDevice removal notification (Case C)
TEST_F(MctpUtilTest, MctpReactorDevice_RemovalNotification_USB)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/13";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
setupMctpEndpointListener(conn, MctpMessageType::NVME_MI);
usleep(100000); // 100ms for match rules to register
// Trigger Add event FIRST!
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addCmd);
// Wait for map to be populated
bool populated = false;
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (mctpEndpointConfigMap.find(endpointPath) !=
mctpEndpointConfigMap.end())
{
populated = true;
break;
}
}
ASSERT_TRUE(populated);
bool sensorNotified = false;
// Create BusInfo for target device
BusInfo targetBusInfo;
targetBusInfo["BusType"] = "USB";
targetBusInfo["Port"] = "1.5.1";
targetBusInfo["Configuration"] = "1";
targetBusInfo["InterfaceNum"] = "4";
targetBusInfo["RootHubPath"] = "/sys/devices/platform/ahb/f0806000.usb";
bool setupCompleted = false;
std::shared_ptr<MctpEndpoint> endpoint;
// Create MctpReactorDevice
auto mctpDev = std::make_shared<MctpReactorDevice>(
conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
std::nullopt);
mctpDev->setup([&](const std::error_code& ec,
const std::shared_ptr<MctpEndpoint>& ep) {
setupCompleted = true;
endpoint = ep;
EXPECT_FALSE(ec);
EXPECT_NE(ep, nullptr);
});
io.poll();
ASSERT_TRUE(setupCompleted);
ASSERT_NE(endpoint, nullptr);
endpoint->subscribe(
nullptr, nullptr,
[&](const std::shared_ptr<MctpEndpoint>&) { sensorNotified = true; });
// Trigger Remove event!
std::string removeCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerRemove s \"" +
endpointPath + "\"";
runCmd(removeCmd);
// Wait for removal notification (up to 5 seconds)
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (sensorNotified)
{
break;
}
}
EXPECT_TRUE(sensorNotified);
}
// 14. Cancellation on Device Removal (Case D)
TEST_F(MctpUtilTest, MctpReactorDevice_Cancellation_USB)
{
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
setupMctpEndpointListener(conn, MctpMessageType::NVME_MI);
// Create BusInfo for target device
BusInfo targetBusInfo;
targetBusInfo["BusType"] = "USB";
targetBusInfo["Port"] = "1.5.1";
targetBusInfo["Configuration"] = "1";
targetBusInfo["InterfaceNum"] = "4";
targetBusInfo["RootHubPath"] = "/sys/devices/platform/ahb/f0806000.usb";
bool setupCompleted = false;
std::error_code errorResult;
// Create MctpReactorDevice
auto mctpDev = std::make_shared<MctpReactorDevice>(
conn, emConfigPath, std::vector<BusInfo>{targetBusInfo}, std::nullopt,
std::nullopt);
mctpDev->setup([&](const std::error_code& ec,
const std::shared_ptr<MctpEndpoint>& ep) {
setupCompleted = true;
errorResult = ec;
EXPECT_EQ(ep, nullptr);
});
// Verify it didn't complete immediately
io.poll();
EXPECT_FALSE(setupCompleted);
// Call remove() to trigger cancellation!
mctpDev->remove();
// Verify it completed with operation_canceled!
EXPECT_TRUE(setupCompleted);
EXPECT_EQ(errorResult, std::make_error_code(std::errc::operation_canceled));
}
// 15. Immediate Notification on Device Destruction (Case E)
TEST_F(MctpUtilTest, MctpReactorDevice_DestructionNotification_USB)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/14";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
setupMctpEndpointListener(conn, MctpMessageType::NVME_MI);
usleep(100000); // 100ms for match rules to register
// Trigger Add event FIRST!
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addCmd);
// Wait for map to be populated
bool populated = false;
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (mctpEndpointConfigMap.find(endpointPath) !=
mctpEndpointConfigMap.end())
{
populated = true;
break;
}
}
ASSERT_TRUE(populated);
// Create BusInfo for target device
BusInfo targetBusInfo;
targetBusInfo["BusType"] = "USB";
targetBusInfo["Port"] = "1.5.1";
targetBusInfo["Configuration"] = "1";
targetBusInfo["InterfaceNum"] = "4";
targetBusInfo["RootHubPath"] = "/sys/devices/platform/ahb/f0806000.usb";
bool sensorNotified = false;
auto cb = [&](const std::shared_ptr<MctpEndpoint>&) {
sensorNotified = true;
};
{
bool setupCompleted = false;
std::shared_ptr<MctpEndpoint> endpoint;
// Create MctpReactorDevice
auto mctpDev = std::make_shared<MctpReactorDevice>(
conn, emConfigPath, std::vector<BusInfo>{targetBusInfo},
std::nullopt, std::nullopt);
mctpDev->setup([&](const std::error_code&,
const std::shared_ptr<MctpEndpoint>& ep) {
setupCompleted = true;
endpoint = ep;
});
io.poll();
ASSERT_TRUE(setupCompleted);
ASSERT_NE(endpoint, nullptr);
endpoint->subscribe(nullptr, nullptr, cb);
}
// mctpDev is destroyed here. Wait for the removal notification to be
// processed.
int i = 0;
for (; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (sensorNotified)
{
break;
}
}
ASSERT_LT(i, 50) << "Timeout: Destruction notification was never fired!";
EXPECT_TRUE(sensorNotified);
}
// 16. Bridged Endpoint Resolution
TEST_F(MctpUtilTest, BridgedEndpoint_ResolvesParentBridge)
{
std::string bridgePath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/mctp_device";
std::string bridgeEmPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_bridged";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/bridged_endpoint_config";
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++; });
// Wait for priming loop to complete (finding nothing)
io.poll();
usleep(100000); // 100ms
io.poll();
std::string addBridgeCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
bridgePath + "\" \"" + bridgeEmPath + "\"";
runCmd(addBridgeCmd);
// Wait for bridge query to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 1)
{
break;
}
}
ASSERT_EQ(queryCompletedCount, 1);
// Wait for mapperx to create association for bridge
bool found = false;
for (int i = 0; i < 50; i++)
{
if (checkPathInMapper(bridgePath + "/configured_by"))
{
found = true;
break;
}
io.poll();
usleep(100000); // 100ms
}
ASSERT_TRUE(found) << "Timed out waiting for bridge association in mapper";
// 2. Trigger Add for Endpoint!
std::string addEpCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addEpCmd);
// Wait for endpoint query to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 2)
{
break;
}
}
EXPECT_EQ(queryCompletedCount, 2);
auto it = mctpEndpointConfigMap.find(endpointPath);
ASSERT_NE(it, mctpEndpointConfigMap.end());
EXPECT_EQ(it->second.size(), 2);
MctpReactorBusInfo busInfo(it->second);
EXPECT_TRUE(busInfo.isValid());
EXPECT_EQ(busInfo.getType().front(), "USB");
EXPECT_EQ(busInfo.getProperties().front().at("Port"), "1.5.1");
}
// 17. Broken Bridge Chain (Non-existent bridge)
TEST_F(MctpUtilTest, BridgedEndpoint_BrokenChain_Nonexistent_Fails)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_broken_nonexistent";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/broken_endpoint_nonexistent";
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++; });
// Wait for priming loop to complete (finding nothing)
io.poll();
usleep(100000); // 100ms
io.poll();
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addCmd);
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 1)
{
break;
}
}
EXPECT_EQ(queryCompletedCount, 1);
auto it = mctpEndpointConfigMap.find(endpointPath);
EXPECT_EQ(it, mctpEndpointConfigMap.end());
}
// 18. Multi-Level Bridging
TEST_F(MctpUtilTest, BridgedEndpoint_MultiLevel_ResolvesRootBridge)
{
std::string rootBridgePath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/mctp_device";
std::string rootBridgeEmPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
std::string interBridgePath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/intermediate_bridge";
std::string interBridgeEmPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/intermediate_bridge";
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_multilevel";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/multilevel_endpoint";
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++; });
// Wait for priming loop to complete (finding nothing)
io.poll();
usleep(100000); // 100ms
io.poll();
// 1. Trigger Add for Root Bridge!
std::string addRootCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
rootBridgePath + "\" \"" + rootBridgeEmPath + "\"";
runCmd(addRootCmd);
// Wait for root bridge query to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 1)
{
break;
}
}
ASSERT_EQ(queryCompletedCount, 1);
// 2. Trigger Add for Intermediate Bridge!
std::string addInterCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
interBridgePath + "\" \"" + interBridgeEmPath + "\"";
runCmd(addInterCmd);
// Wait for intermediate bridge query to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 2)
{
break;
}
}
ASSERT_EQ(queryCompletedCount, 2);
// 3. Trigger Add for Endpoint!
std::string addEpCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addEpCmd);
// Wait for endpoint query to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 3)
{
break;
}
}
EXPECT_EQ(queryCompletedCount, 3);
auto it = mctpEndpointConfigMap.find(endpointPath);
ASSERT_NE(it, mctpEndpointConfigMap.end());
EXPECT_EQ(it->second.size(), 3);
MctpReactorBusInfo busInfo(it->second);
EXPECT_TRUE(busInfo.isValid());
EXPECT_EQ(busInfo.getType().front(), "USB");
EXPECT_EQ(busInfo.getProperties().front().at("Port"), "1.5.1");
}
// 19. Out-of-Order Triggers (Endpoint before Bridge) - Expect Success after
// Retry
TEST_F(MctpUtilTest, BridgedEndpoint_OutOfOrder_SucceedsAfterRetry)
{
std::string bridgePath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/mctp_device";
std::string bridgeEmPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_out_of_order";
std::string emConfigPath =
"/xyz/openbmc_project/inventory/system/board/MockBoard/bridged_endpoint_config";
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++; });
// Wait for priming loop to complete
io.poll();
usleep(100000); // 100ms
io.poll();
// Wait 5 seconds to ensure cooldown expires!
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
}
// 1. Trigger Add for Endpoint FIRST!
std::string addEpCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addEpCmd);
// Trigger Add for Bridge SECOND (after 2s)!
for (int i = 0; i < 20; i++)
{
io.poll();
usleep(100000); // 100ms
}
std::string addBridgeCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
bridgePath + "\" \"" + bridgeEmPath + "\"";
runCmd(addBridgeCmd);
// Wait for all to complete!
// B retries after 5 seconds, so it should complete around 6-7 seconds!
for (int i = 0; i < 100; i++)
{ // 10 seconds max
io.poll();
usleep(100000); // 100ms
}
// Expect 3 queries: B (fail) -> Bridge A (success) -> B (success retry)
EXPECT_EQ(queryCompletedCount, 3);
auto it = mctpEndpointConfigMap.find(endpointPath);
EXPECT_NE(it, mctpEndpointConfigMap.end());
}
// 20. performEmConfigQuery Failure Triggers Retry - Expect Failure with Current
// Code
TEST_F(MctpUtilTest, performEmConfigQuery_Failure_TriggersRetry)
{
std::string endpointPath =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_retry";
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++; });
// Wait for priming loop to complete
for (int i = 0; i < 50; i++)
{
io.poll();
usleep(100000); // 100ms
}
// Enable failure mode in mock!
runCmd(
"busctl call xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory com.example.Control SetFailMode b true");
// Trigger Add for Endpoint!
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPath + "\" \"" + emConfigPath + "\"";
runCmd(addCmd);
// Wait for first query to complete (it should fail!)
for (int i = 0; i < 20; i++)
{
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 1)
{
break;
}
}
EXPECT_EQ(queryCompletedCount, 1);
// Disable failure mode in mock!
runCmd(
"busctl call xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory com.example.Control SetFailMode b false");
// Wait for retry!
// With current code, it won't retry on failure! So count will stay 1!
// With fix, it should retry after 5 seconds and succeed!
// So count should reach 2!
for (int i = 0; i < 80; i++)
{ // 8 seconds max
io.poll();
usleep(100000); // 100ms
if (queryCompletedCount >= 2)
{
break;
}
}
// Expect success (count=2) after fix, but FAIL with current code!
EXPECT_EQ(queryCompletedCount, 2);
auto it = mctpEndpointConfigMap.find(endpointPath);
EXPECT_NE(it, mctpEndpointConfigMap.end());
}
// 21. Priming with failing endpoint does not block others
TEST_F(MctpUtilTest, Priming_FailingEndpoint_DoesNotBlockOthers)
{
std::string endpointPathFail =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_prime_fail";
std::string emConfigPathFail =
"/xyz/openbmc_project/inventory/system/board/MockBoard/broken_endpoint_nonexistent";
std::string endpointPathSuccess =
"/au/com/codeconstruct/mctp1/networks/1/endpoints/test_prime_success";
std::string emConfigPathSuccess =
"/xyz/openbmc_project/inventory/system/board/MockBoard/mctp_device";
// Pre-populate association for failing endpoint BEFORE listener starts!
auto m = conn->new_method_call("au.com.codeconstruct.MCTP1",
"/au/com/codeconstruct/mctp1",
"com.example.Control", "TriggerAdd");
m.append(endpointPathFail, emConfigPathFail);
conn->call(m);
// Wait for mapperx to see it
bool found = false;
for (int i = 0; i < 300; i++)
{
if (checkPathInMapper(endpointPathFail + "/configured_by"))
{
found = true;
break;
}
usleep(100000); // 100ms
}
ASSERT_TRUE(found);
// Now setup listener (this starts priming!)
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++; });
// Wait for priming query to start for failing endpoint
usleep(500000); // 500ms
io.poll();
// Trigger Add for a VALID endpoint while priming is in progress!
std::string addCmd =
"busctl call au.com.codeconstruct.MCTP1 /au/com/codeconstruct/mctp1 com.example.Control TriggerAdd ss \"" +
endpointPathSuccess + "\" \"" + emConfigPathSuccess + "\"";
runCmd(addCmd);
// Wait for queries to complete.
// With fix: failing one fails and defers, valid one succeeds! Total = 2
// queries. Without fix: failing one hangs, valid one never starts! Total =
// 1 query.
for (int i = 0; i < 150; i++)
{ // 15 seconds max
io.poll();
usleep(100000); // 100ms
if (mctpEndpointConfigMap.find(endpointPathSuccess) !=
mctpEndpointConfigMap.end())
{
break;
}
}
auto it = mctpEndpointConfigMap.find(endpointPathSuccess);
EXPECT_NE(it, mctpEndpointConfigMap.end());
}
// 22. MctpReactorBusInfo chain parsing and operator== matching
TEST_F(MctpUtilTest, MctpReactorBusInfo_ChainParsingAndMatching)
{
std::vector<SensorData> chain;
// Leaf (endpoint)
SensorBaseConfigMap epProps;
epProps["EIDOffset"] = uint64_t(2);
SensorData epData;
epData["xyz.openbmc_project.Configuration.MCTPBridgeDownstreamDevice"] =
epProps;
chain.push_back(epData);
// Bridge
SensorBaseConfigMap bridgeProps;
bridgeProps["BusType"] = std::string("USB");
bridgeProps["Port"] = std::string("1.5.1");
bridgeProps["Configuration"] =
std::string("1"); // Extra property not in target
SensorData bridgeData;
bridgeData["xyz.openbmc_project.Configuration.MCTPUSBDevice"] = bridgeProps;
chain.push_back(bridgeData);
MctpReactorBusInfo busInfo(chain);
EXPECT_TRUE(busInfo.isValid());
auto propsList = busInfo.getProperties();
ASSERT_EQ(propsList.size(), 2);
// Verify order is root-to-leaf (from Test 22)
EXPECT_EQ(propsList[0].at("BusType"), "USB");
EXPECT_EQ(propsList[0].at("Port"), "1.5.1");
EXPECT_EQ(propsList[1].at("BusType"), "MCTP Bridge");
EXPECT_EQ(propsList[1].at("EIDOffset"), "2");
// Verify operator== subset matching (from Test 23)
std::vector<BusInfo> targetConfig;
// Root level
BusInfo targetBridge;
targetBridge["BusType"] = "USB";
targetBridge["Port"] = "1.5.1";
targetConfig.push_back(targetBridge);
// Leaf level
BusInfo targetEp;
targetEp["EIDOffset"] = "2";
targetConfig.push_back(targetEp);
// Should match because target is a subset of extracted properties
EXPECT_TRUE(busInfo == targetConfig);
// Test mismatch in value
targetConfig[1]["EIDOffset"] = "3";
EXPECT_FALSE(busInfo == targetConfig);
// Test missing property in extracted props
targetConfig[1]["EIDOffset"] = "2";
targetConfig[1]["ExtraProp"] = "value";
EXPECT_FALSE(busInfo == targetConfig);
// Test size mismatch
targetConfig.pop_back();
EXPECT_FALSE(busInfo == targetConfig);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}