From 6d8d843ee029a3e6785e153e3b344773db5f7102 Mon Sep 17 00:00:00 2001 From: Copilot Date: Mon, 20 Jul 2026 18:23:40 +0200 Subject: [PATCH] tests(dds): enable GTest smoke-tests and add async-capable realistic MockDDS with GTest async subscription test --- .../DdsUnitTestsPlaceholder/CMakeLists.txt | 9 ++- .../ddsGenericDataTests_gtest.cpp | 34 +++++++++ .../DdsUnitTestsPlaceholder/mocks/MockDDS.cpp | 73 ++++++++++++++++++- .../DdsUnitTestsPlaceholder/mocks/MockDDS.h | 7 +- 4 files changed, 115 insertions(+), 8 deletions(-) diff --git a/Lts/DDS/DdsUnitTestsPlaceholder/CMakeLists.txt b/Lts/DDS/DdsUnitTestsPlaceholder/CMakeLists.txt index fb537aa4e..00d5401ae 100644 --- a/Lts/DDS/DdsUnitTestsPlaceholder/CMakeLists.txt +++ b/Lts/DDS/DdsUnitTestsPlaceholder/CMakeLists.txt @@ -6,7 +6,8 @@ # 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_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 if(BUILD_GTEST_SMOKE) @@ -21,21 +22,21 @@ if(BUILD_TESTING) # Generic data tests smoke (QtTest) 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) add_test(NAME ddsGenericDataTests COMMAND $) endif() # Data topic tests smoke (QtTest) 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) add_test(NAME ddsDataTopicTests COMMAND $) endif() # Function topic tests smoke (QtTest) 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) add_test(NAME ddsFunctionTopicTests COMMAND $) endif() diff --git a/Lts/DDS/DdsUnitTestsPlaceholder/ddsGenericDataTests_gtest.cpp b/Lts/DDS/DdsUnitTestsPlaceholder/ddsGenericDataTests_gtest.cpp index eb37a782f..49e36510a 100644 --- a/Lts/DDS/DdsUnitTestsPlaceholder/ddsGenericDataTests_gtest.cpp +++ b/Lts/DDS/DdsUnitTestsPlaceholder/ddsGenericDataTests_gtest.cpp @@ -1,5 +1,7 @@ #include #include "mocks/MockDDS.h" +#include +#include TEST(GenericSmokeGTest, BasicStringOps) { std::string s = "MiXeD"; @@ -19,3 +21,35 @@ TEST(GenericSmokeGTest, MockDDSPublishReceive) { EXPECT_EQ(payload, "payload42"); 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 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 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"); +} diff --git a/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.cpp b/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.cpp index 7ea96039d..2a0c6d626 100644 --- a/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.cpp +++ b/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.cpp @@ -1,6 +1,8 @@ #include "MockDDS.h" #include #include +#include +#include namespace MockDDS { @@ -14,6 +16,51 @@ struct TopicState { static std::map g_topics; static std::mutex g_mtx; +// Background dispatcher for async publishes +static std::vector> 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 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 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 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) { std::lock_guard lk(g_mtx); 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 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) { std::lock_guard lk(g_mtx); auto &ts = g_topics[topic]; @@ -58,8 +114,21 @@ std::vector get_queue(const std::string &topic) { } void clear_all() { - std::lock_guard lk(g_mtx); - g_topics.clear(); + { + std::lock_guard lk(g_mtx); + g_topics.clear(); + } + // shutdown async worker gracefully + { + std::lock_guard 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 lk(g_async_mtx); + g_async_queue.clear(); + } } } // namespace MockDDS diff --git a/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.h b/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.h index d7ae1b9e1..edddab53b 100644 --- a/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.h +++ b/Lts/DDS/DdsUnitTestsPlaceholder/mocks/MockDDS.h @@ -2,21 +2,24 @@ #include #include #include +#include namespace MockDDS { // 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 // - unsubscribe(topic, subscription id) // - take_last(topic), get_queue(topic) // - 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; using SubscriptionId = unsigned long; 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); void unsubscribe(const std::string &topic, SubscriptionId id); std::string take_last(const std::string &topic);