tests(dds): enable GTest smoke-tests and add async-capable realistic MockDDS with GTest async subscription test
This commit is contained in:
1 parent
14988660cd
commit
6d8d843ee0
4 files changed
+115
-8
No files matched your search
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
# Option to force-building smoke-tests even when real test targets exist.
|
# Option to force-building smoke-tests even when real test targets exist.
|
||||||
option(BUILD_SMOKE_TESTS "Build smoke-tests even if real test executables are present" OFF)
|
option(BUILD_SMOKE_TESTS "Build smoke-tests even if real test executables are present" OFF)
|
||||||
option(BUILD_GTEST_SMOKE "Build GoogleTest-based smoke-tests for core logic" OFF)
|
# Enable GTest smoke-tests during migration to exercise realistic DDS mocks
|
||||||
|
option(BUILD_GTEST_SMOKE "Build GoogleTest-based smoke-tests for core logic" ON)
|
||||||
|
|
||||||
# Try to find GoogleTest if requested
|
# Try to find GoogleTest if requested
|
||||||
if(BUILD_GTEST_SMOKE)
|
if(BUILD_GTEST_SMOKE)
|
||||||
@@ -21,21 +22,21 @@ if(BUILD_TESTING)
|
|||||||
|
|
||||||
# Generic data tests smoke (QtTest)
|
# Generic data tests smoke (QtTest)
|
||||||
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsGenericDataTests)
|
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsGenericDataTests)
|
||||||
add_executable(ddsGenericDataTests_smoke ddsGenericDataTests_smoke.cpp)
|
add_executable(ddsGenericDataTests_smoke ddsGenericDataTests_smoke.cpp mocks/MockDDS.cpp)
|
||||||
target_link_libraries(ddsGenericDataTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
target_link_libraries(ddsGenericDataTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
||||||
add_test(NAME ddsGenericDataTests COMMAND $<TARGET_FILE:ddsGenericDataTests_smoke>)
|
add_test(NAME ddsGenericDataTests COMMAND $<TARGET_FILE:ddsGenericDataTests_smoke>)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Data topic tests smoke (QtTest)
|
# Data topic tests smoke (QtTest)
|
||||||
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsDataTopicTests)
|
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsDataTopicTests)
|
||||||
add_executable(ddsDataTopicTests_smoke ddsDataTopicTests_smoke.cpp)
|
add_executable(ddsDataTopicTests_smoke ddsDataTopicTests_smoke.cpp mocks/MockDDS.cpp)
|
||||||
target_link_libraries(ddsDataTopicTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
target_link_libraries(ddsDataTopicTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
||||||
add_test(NAME ddsDataTopicTests COMMAND $<TARGET_FILE:ddsDataTopicTests_smoke>)
|
add_test(NAME ddsDataTopicTests COMMAND $<TARGET_FILE:ddsDataTopicTests_smoke>)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Function topic tests smoke (QtTest)
|
# Function topic tests smoke (QtTest)
|
||||||
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsFunctionTopicTests)
|
if(BUILD_SMOKE_TESTS OR NOT TARGET ddsFunctionTopicTests)
|
||||||
add_executable(ddsFunctionTopicTests_smoke ddsFunctionTopicTests_smoke.cpp)
|
add_executable(ddsFunctionTopicTests_smoke ddsFunctionTopicTests_smoke.cpp mocks/MockDDS.cpp)
|
||||||
target_link_libraries(ddsFunctionTopicTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
target_link_libraries(ddsFunctionTopicTests_smoke PRIVATE Qt5::Core Qt5::Test)
|
||||||
add_test(NAME ddsFunctionTopicTests COMMAND $<TARGET_FILE:ddsFunctionTopicTests_smoke>)
|
add_test(NAME ddsFunctionTopicTests COMMAND $<TARGET_FILE:ddsFunctionTopicTests_smoke>)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include "mocks/MockDDS.h"
|
#include "mocks/MockDDS.h"
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
TEST(GenericSmokeGTest, BasicStringOps) {
|
TEST(GenericSmokeGTest, BasicStringOps) {
|
||||||
std::string s = "MiXeD";
|
std::string s = "MiXeD";
|
||||||
@@ -19,3 +21,35 @@ TEST(GenericSmokeGTest, MockDDSPublishReceive) {
|
|||||||
EXPECT_EQ(payload, "payload42");
|
EXPECT_EQ(payload, "payload42");
|
||||||
EXPECT_EQ(seq, 1UL);
|
EXPECT_EQ(seq, 1UL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(GenericSmokeGTest, MockDDSAsyncDelivery) {
|
||||||
|
MockDDS::clear_all();
|
||||||
|
std::mutex mtx;
|
||||||
|
std::condition_variable cv;
|
||||||
|
bool received = false;
|
||||||
|
std::string last;
|
||||||
|
|
||||||
|
auto cb = [&](const std::string &payload){
|
||||||
|
std::lock_guard<std::mutex> lk(mtx);
|
||||||
|
last = payload;
|
||||||
|
received = true;
|
||||||
|
cv.notify_one();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sub = MockDDS::subscribe("topic-async", cb);
|
||||||
|
// publish asynchronously with small delay; callback should be invoked
|
||||||
|
MockDDS::publish_async("topic-async", "async-payload", 20);
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lk(mtx);
|
||||||
|
if (!cv.wait_for(lk, std::chrono::seconds(2), [&]{ return received; })) {
|
||||||
|
// timeout
|
||||||
|
MockDDS::unsubscribe("topic-async", sub);
|
||||||
|
FAIL() << "Did not receive async callback in time";
|
||||||
|
}
|
||||||
|
MockDDS::unsubscribe("topic-async", sub);
|
||||||
|
// parse annotation
|
||||||
|
auto pos = last.find(':');
|
||||||
|
ASSERT_NE(pos, std::string::npos);
|
||||||
|
auto payload = last.substr(pos + 1);
|
||||||
|
EXPECT_EQ(payload, "async-payload");
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "MockDDS.h"
|
#include "MockDDS.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
namespace MockDDS {
|
namespace MockDDS {
|
||||||
|
|
||||||
@@ -14,6 +16,51 @@ struct TopicState {
|
|||||||
static std::map<std::string, TopicState> g_topics;
|
static std::map<std::string, TopicState> g_topics;
|
||||||
static std::mutex g_mtx;
|
static std::mutex g_mtx;
|
||||||
|
|
||||||
|
// Background dispatcher for async publishes
|
||||||
|
static std::vector<std::tuple<std::string, std::string, unsigned>> g_async_queue;
|
||||||
|
static std::mutex g_async_mtx;
|
||||||
|
static std::condition_variable g_async_cv;
|
||||||
|
static bool g_async_running = false;
|
||||||
|
static std::thread g_async_thread;
|
||||||
|
|
||||||
|
static void async_worker() {
|
||||||
|
std::unique_lock<std::mutex> lk(g_async_mtx);
|
||||||
|
while (g_async_running) {
|
||||||
|
if (g_async_queue.empty()) {
|
||||||
|
g_async_cv.wait(lk, [] { return !g_async_queue.empty() || !g_async_running; });
|
||||||
|
if (!g_async_running) break;
|
||||||
|
}
|
||||||
|
auto task = g_async_queue.front();
|
||||||
|
g_async_queue.erase(g_async_queue.begin());
|
||||||
|
lk.unlock();
|
||||||
|
auto &topic = std::get<0>(task);
|
||||||
|
auto &msg = std::get<1>(task);
|
||||||
|
unsigned delay_ms = std::get<2>(task);
|
||||||
|
if (delay_ms) std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
|
||||||
|
// perform synchronous publish semantics for delivered message
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk2(g_mtx);
|
||||||
|
auto &ts = g_topics[topic];
|
||||||
|
ts.seq++;
|
||||||
|
std::string annotated = std::to_string(ts.seq) + ":" + msg;
|
||||||
|
ts.queue.push_back(annotated);
|
||||||
|
for (auto &p : ts.subs) {
|
||||||
|
auto &cb = p.second;
|
||||||
|
if (cb) cb(annotated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lk.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensure_async_thread() {
|
||||||
|
std::lock_guard<std::mutex> lk(g_async_mtx);
|
||||||
|
if (!g_async_running) {
|
||||||
|
g_async_running = true;
|
||||||
|
g_async_thread = std::thread(async_worker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void publish(const std::string &topic, const std::string &msg) {
|
void publish(const std::string &topic, const std::string &msg) {
|
||||||
std::lock_guard<std::mutex> lk(g_mtx);
|
std::lock_guard<std::mutex> lk(g_mtx);
|
||||||
auto &ts = g_topics[topic];
|
auto &ts = g_topics[topic];
|
||||||
@@ -28,6 +75,15 @@ void publish(const std::string &topic, const std::string &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void publish_async(const std::string &topic, const std::string &msg, unsigned delay_ms) {
|
||||||
|
ensure_async_thread();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_async_mtx);
|
||||||
|
g_async_queue.emplace_back(topic, msg, delay_ms);
|
||||||
|
}
|
||||||
|
g_async_cv.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
MockDDS::SubscriptionId subscribe(const std::string &topic, Subscriber cb) {
|
MockDDS::SubscriptionId subscribe(const std::string &topic, Subscriber cb) {
|
||||||
std::lock_guard<std::mutex> lk(g_mtx);
|
std::lock_guard<std::mutex> lk(g_mtx);
|
||||||
auto &ts = g_topics[topic];
|
auto &ts = g_topics[topic];
|
||||||
@@ -58,8 +114,21 @@ std::vector<std::string> get_queue(const std::string &topic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clear_all() {
|
void clear_all() {
|
||||||
std::lock_guard<std::mutex> lk(g_mtx);
|
{
|
||||||
g_topics.clear();
|
std::lock_guard<std::mutex> lk(g_mtx);
|
||||||
|
g_topics.clear();
|
||||||
|
}
|
||||||
|
// shutdown async worker gracefully
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_async_mtx);
|
||||||
|
g_async_running = false;
|
||||||
|
}
|
||||||
|
g_async_cv.notify_all();
|
||||||
|
if (g_async_thread.joinable()) g_async_thread.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_async_mtx);
|
||||||
|
g_async_queue.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace MockDDS
|
} // namespace MockDDS
|
||||||
@@ -2,21 +2,24 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace MockDDS {
|
namespace MockDDS {
|
||||||
|
|
||||||
// Richer mock for migration smoke-tests:
|
// Richer mock for migration smoke-tests:
|
||||||
// - publish(topic, payload)
|
// - publish(topic, payload) : synchronous publish
|
||||||
|
// - publish_async(topic, payload, delay_ms) : asynchronous publish (background dispatcher)
|
||||||
// - subscribe(topic, callback) -> subscription id
|
// - subscribe(topic, callback) -> subscription id
|
||||||
// - unsubscribe(topic, subscription id)
|
// - unsubscribe(topic, subscription id)
|
||||||
// - take_last(topic), get_queue(topic)
|
// - take_last(topic), get_queue(topic)
|
||||||
// - clear_all()
|
// - clear_all()
|
||||||
// - sequence numbers per topic (as simple integer appended to payload meta)
|
// - sequence numbers per topic (integer appended to payload meta)
|
||||||
|
|
||||||
using Subscriber = std::function<void(const std::string &payload)>;
|
using Subscriber = std::function<void(const std::string &payload)>;
|
||||||
using SubscriptionId = unsigned long;
|
using SubscriptionId = unsigned long;
|
||||||
|
|
||||||
void publish(const std::string &topic, const std::string &msg);
|
void publish(const std::string &topic, const std::string &msg);
|
||||||
|
void publish_async(const std::string &topic, const std::string &msg, unsigned delay_ms = 0);
|
||||||
SubscriptionId subscribe(const std::string &topic, Subscriber cb);
|
SubscriptionId subscribe(const std::string &topic, Subscriber cb);
|
||||||
void unsubscribe(const std::string &topic, SubscriptionId id);
|
void unsubscribe(const std::string &topic, SubscriptionId id);
|
||||||
std::string take_last(const std::string &topic);
|
std::string take_last(const std::string &topic);
|
||||||
|
|||||||
Reference in new issue
Block a user