Skip to content

Commit 09ead45

Browse files
committed
feat(tests): add integration tests for Awaitable
Introduce new awaitable methods in TestProxy for asynchronous operations and add corresponding integration tests in DBusAwaitableMethodsTests.cpp to validate functionality. The test uses a simple coroutine task type which uses a promise/future pair to pass data between the callback and the coroutine, leveraging the fact that the tests are all multithreaded. This approach would not work in a single-threaded scenario, as the future's .get() method would block the thread.
1 parent 64b6173 commit 09ead45

4 files changed

Lines changed: 284 additions & 0 deletions

File tree

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ set(INTEGRATIONTESTS_SRCS
6060
${INTEGRATIONTESTS_SOURCE_DIR}/DBusGeneralTests.cpp
6161
${INTEGRATIONTESTS_SOURCE_DIR}/DBusMethodsTests.cpp
6262
${INTEGRATIONTESTS_SOURCE_DIR}/DBusAsyncMethodsTests.cpp
63+
${INTEGRATIONTESTS_SOURCE_DIR}/DBusAwaitableMethodsTests.cpp
6364
${INTEGRATIONTESTS_SOURCE_DIR}/DBusSignalsTests.cpp
6465
${INTEGRATIONTESTS_SOURCE_DIR}/DBusPropertiesTests.cpp
6566
${INTEGRATIONTESTS_SOURCE_DIR}/DBusStandardInterfacesTests.cpp
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* (C) 2016 - 2021 KISTLER INSTRUMENTE AG, Winterthur, Switzerland
3+
* (C) 2016 - 2026 Stanislav Angelovic <stanislav.angelovic@protonmail.com>
4+
* (C) 2026 - Alex Cani <alexcani109@gmail.com>
5+
*
6+
* @file DBusAwaitableMethodsTests.cpp
7+
*
8+
* Created on: Mar 1, 2026
9+
* Project: sdbus-c++
10+
* Description: High-level D-Bus IPC C++ library based on sd-bus
11+
*
12+
* This file is part of sdbus-c++.
13+
*
14+
* sdbus-c++ is free software; you can redistribute it and/or modify it
15+
* under the terms of the GNU Lesser General Public License as published by
16+
* the Free Software Foundation, either version 2.1 of the License, or
17+
* (at your option) any later version.
18+
*
19+
* sdbus-c++ is distributed in the hope that it will be useful,
20+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
* GNU Lesser General Public License for more details.
23+
*
24+
* You should have received a copy of the GNU Lesser General Public License
25+
* along with sdbus-c++. If not, see <http://www.gnu.org/licenses/>.
26+
*/
27+
28+
#include <coroutine>
29+
#include <cstdint>
30+
#include <exception>
31+
#include <future>
32+
#include <map>
33+
#include <string>
34+
#include <type_traits>
35+
#include <utility>
36+
37+
#include <sdbus-c++/sdbus-c++.h>
38+
#include <gtest/gtest.h>
39+
#include <gmock/gmock.h>
40+
41+
#include "TestFixture.h"
42+
#include "TestProxy.h"
43+
#include "Defs.h"
44+
45+
using ::testing::Eq;
46+
using namespace std::chrono_literals;
47+
using namespace sdbus::test;
48+
49+
// Simple coroutine task type for testing purposes
50+
// Uses a promise/future pair to communicate results and exceptions
51+
// between the coroutine and the test code. Do not mistake this for
52+
// the std::future-based async API of sdbus-c++.
53+
template<typename T>
54+
struct Task {
55+
struct promise_type {
56+
T value;
57+
std::exception_ptr exception;
58+
std::promise<T> completion;
59+
std::future<T> future;
60+
61+
promise_type() : future(completion.get_future()) {}
62+
63+
Task get_return_object() {
64+
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
65+
}
66+
67+
// Lazy coroutine
68+
std::suspend_always initial_suspend() noexcept { return {}; }
69+
70+
// final_suspend suspends so that the test code can retrieve the result
71+
// or exception from the promise before the coroutine is destroyed
72+
std::suspend_always final_suspend() noexcept
73+
{
74+
if (exception)
75+
{
76+
completion.set_exception(exception);
77+
}
78+
else
79+
{
80+
completion.set_value(std::move(value));
81+
}
82+
return {};
83+
}
84+
85+
void return_value(T v) { value = std::move(v); }
86+
void unhandled_exception() { exception = std::current_exception(); }
87+
};
88+
89+
std::coroutine_handle<promise_type> handle;
90+
91+
// Ctor and rule of 5 for proper handle management
92+
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
93+
Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {}
94+
Task& operator=(Task&& other) noexcept {
95+
if (this != &other) {
96+
if (handle) handle.destroy();
97+
handle = std::exchange(other.handle, {});
98+
}
99+
return *this;
100+
}
101+
Task(const Task&) = delete;
102+
Task& operator=(const Task&) = delete;
103+
~Task() { if (handle) handle.destroy(); }
104+
105+
// "User API" for the test code, allows starting the task and retrieving the result or exception
106+
void resume() { if (handle && !handle.done()) handle.resume(); }
107+
T get() { return handle.promise().future.get(); }
108+
};
109+
110+
// Specialization for void
111+
template<>
112+
struct Task<void> {
113+
struct promise_type {
114+
std::exception_ptr exception;
115+
std::promise<void> completion;
116+
std::future<void> future;
117+
118+
promise_type() : future(completion.get_future()) {}
119+
120+
Task get_return_object() {
121+
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
122+
}
123+
124+
std::suspend_always initial_suspend() noexcept { return {}; }
125+
126+
std::suspend_always final_suspend() noexcept
127+
{
128+
if (exception)
129+
{
130+
completion.set_exception(exception);
131+
}
132+
else
133+
{
134+
completion.set_value();
135+
}
136+
return {};
137+
}
138+
139+
void return_void() {}
140+
void unhandled_exception() { exception = std::current_exception(); }
141+
};
142+
143+
std::coroutine_handle<promise_type> handle;
144+
145+
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
146+
Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {}
147+
Task& operator=(Task&& other) noexcept {
148+
if (this != &other) {
149+
if (handle) handle.destroy();
150+
handle = std::exchange(other.handle, {});
151+
}
152+
return *this;
153+
}
154+
Task(const Task&) = delete;
155+
Task& operator=(const Task&) = delete;
156+
157+
~Task() { if (handle) handle.destroy(); }
158+
159+
void resume() { if (handle && !handle.done()) handle.resume(); }
160+
void get() { handle.promise().future.get(); }
161+
};
162+
163+
/*-------------------------------------*/
164+
/* -- TEST CASES -- */
165+
/*-------------------------------------*/
166+
167+
TYPED_TEST(AsyncSdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithAwaitable)
168+
{
169+
auto task = [](TestProxy* proxy) -> Task<uint32_t> {
170+
co_return co_await proxy->doOperationClientSideAsync(100, sdbus::with_awaitable);
171+
}(this->m_proxy.get());
172+
173+
task.resume();
174+
175+
ASSERT_THAT(task.get(), Eq(100));
176+
}
177+
178+
TYPED_TEST(AsyncSdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithAwaitableOnBasicAPILevel)
179+
{
180+
auto task = [](TestProxy* proxy) -> Task<uint32_t> {
181+
auto methodReply = co_await proxy->doOperationClientSideAsyncOnBasicAPILevel(100, sdbus::with_awaitable);
182+
uint32_t returnValue{};
183+
methodReply >> returnValue;
184+
co_return returnValue;
185+
}(this->m_proxy.get());
186+
187+
task.resume();
188+
189+
ASSERT_THAT(task.get(), Eq(100));
190+
}
191+
192+
TYPED_TEST(AsyncSdbusTestObject, InvokesMethodWithLargeDataAsynchronouslyOnClientSideWithAwaitable)
193+
{
194+
std::map<int32_t, std::string> largeMap;
195+
for (int32_t i = 0; i < 40'000; ++i)
196+
largeMap.emplace(i, "This is string nr. " + std::to_string(i+1));
197+
198+
auto task = [&largeMap, this]() -> Task<std::map<int32_t, std::string>> {
199+
co_return co_await this->m_proxy->doOperationWithLargeDataClientSideAsync(largeMap, sdbus::with_awaitable);
200+
}();
201+
202+
task.resume();
203+
204+
ASSERT_THAT(task.get(), Eq(largeMap));
205+
}
206+
207+
TYPED_TEST(AsyncSdbusTestObject, ThrowsErrorWhenClientSideAsynchronousMethodCallWithAwaitableFails)
208+
{
209+
auto task = [](TestProxy* proxy) -> Task<void> {
210+
co_await proxy->doErroneousOperationClientSideAsync(sdbus::with_awaitable);
211+
}(this->m_proxy.get());
212+
213+
task.resume();
214+
215+
ASSERT_THROW(task.get(), sdbus::Error);
216+
}
217+
218+
TYPED_TEST(AsyncSdbusTestObject, AwaitableSupportsMultipleSequentialCalls)
219+
{
220+
auto task = [](TestProxy* proxy) -> Task<uint32_t> {
221+
auto result1 = co_await proxy->doOperationClientSideAsync(10, sdbus::with_awaitable);
222+
auto result2 = co_await proxy->doOperationClientSideAsync(20, sdbus::with_awaitable);
223+
auto result3 = co_await proxy->doOperationClientSideAsync(30, sdbus::with_awaitable);
224+
co_return result1 + result2 + result3;
225+
}(this->m_proxy.get());
226+
227+
task.resume();
228+
229+
ASSERT_THAT(task.get(), Eq(60));
230+
}
231+
232+
TYPED_TEST(AsyncSdbusTestObject, AwaitablePropagatesExceptionsCorrectly)
233+
{
234+
auto task = [](TestProxy* proxy) -> Task<std::string> {
235+
try {
236+
co_await proxy->doErroneousOperationClientSideAsync(sdbus::with_awaitable);
237+
co_return "FAILED";
238+
} catch (const sdbus::Error& e) {
239+
// Verify we can inspect the exception
240+
co_return std::string(e.getName());
241+
}
242+
}(this->m_proxy.get());
243+
244+
task.resume();
245+
246+
ASSERT_THAT(task.get(), ::testing::HasSubstr("Error"));
247+
}

tests/integrationtests/TestProxy.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,38 @@ std::future<void> TestProxy::doErroneousOperationClientSideAsync(with_future_t)
196196
.getResultAsFuture<>();
197197
}
198198

199+
sdbus::Awaitable<uint32_t> TestProxy::doOperationClientSideAsync(uint32_t param, sdbus::with_awaitable_t)
200+
{
201+
return getProxy().callMethodAsync("doOperation")
202+
.onInterface(sdbus::test::INTERFACE_NAME)
203+
.withArguments(param)
204+
.getResultAsAwaitable<uint32_t>();
205+
}
206+
207+
sdbus::Awaitable<std::map<int32_t, std::string>> TestProxy::doOperationWithLargeDataClientSideAsync(const std::map<int32_t, std::string>& largeParam, sdbus::with_awaitable_t)
208+
{
209+
return getProxy().callMethodAsync("doOperationWithLargeData")
210+
.onInterface(sdbus::test::INTERFACE_NAME)
211+
.withArguments(largeParam)
212+
.getResultAsAwaitable<std::map<int32_t, std::string>>();
213+
}
214+
215+
sdbus::Awaitable<MethodReply> TestProxy::doOperationClientSideAsyncOnBasicAPILevel(uint32_t param, sdbus::with_awaitable_t)
216+
{
217+
auto methodCall = getProxy().createMethodCall(sdbus::test::INTERFACE_NAME, sdbus::MethodName{"doOperation"});
218+
methodCall << param;
219+
220+
return getProxy().callMethodAsync(methodCall, sdbus::with_awaitable);
221+
}
222+
223+
224+
sdbus::Awaitable<void> TestProxy::doErroneousOperationClientSideAsync(sdbus::with_awaitable_t)
225+
{
226+
return getProxy().callMethodAsync("throwError")
227+
.onInterface(sdbus::test::INTERFACE_NAME)
228+
.getResultAsAwaitable<>();
229+
}
230+
199231
void TestProxy::doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param)
200232
{
201233
using namespace std::chrono_literals;

tests/integrationtests/TestProxy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ class TestProxy final : public sdbus::ProxyInterfaces< org::sdbuscpp::integratio
111111
std::future<std::map<int32_t, std::string>> doOperationWithLargeDataClientSideAsync(const std::map<int32_t, std::string>& largeParam, with_future_t);
112112
std::future<MethodReply> doOperationClientSideAsyncOnBasicAPILevel(uint32_t param);
113113
std::future<void> doErroneousOperationClientSideAsync(with_future_t);
114+
sdbus::Awaitable<uint32_t> doOperationClientSideAsync(uint32_t param, sdbus::with_awaitable_t);
115+
sdbus::Awaitable<std::map<int32_t, std::string>> doOperationWithLargeDataClientSideAsync(const std::map<int32_t, std::string>& largeParam, sdbus::with_awaitable_t);
116+
sdbus::Awaitable<MethodReply> doOperationClientSideAsyncOnBasicAPILevel(uint32_t param, sdbus::with_awaitable_t);
117+
sdbus::Awaitable<void> doErroneousOperationClientSideAsync(sdbus::with_awaitable_t);
114118
void doErroneousOperationClientSideAsync();
115119
void doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param);
116120
int32_t callNonexistentMethod();

0 commit comments

Comments
 (0)