From 1353cc7b5386fca0240debd6acbb2c426b82d0a1 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Wed, 8 Apr 2026 18:00:46 -0300 Subject: [PATCH] make async method calls truly async --- cpp/implementations/mock/mock_transport.h | 15 + .../qt_local/local_transport.cpp | 19 ++ .../qt_remote/remote_transport.cpp | 64 ++++ cpp/logos_api_consumer.cpp | 10 +- cpp/logos_object.h | 20 ++ tests/sdk/CMakeLists.txt | 1 + tests/sdk/test_async_calls.cpp | 288 ++++++++++++++++++ 7 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 tests/sdk/test_async_calls.cpp diff --git a/cpp/implementations/mock/mock_transport.h b/cpp/implementations/mock/mock_transport.h index 6b92dab..47bcaa5 100644 --- a/cpp/implementations/mock/mock_transport.h +++ b/cpp/implementations/mock/mock_transport.h @@ -6,6 +6,7 @@ #include "mock_store.h" #include #include +#include /** * @brief LogosObject implementation for mock mode. @@ -28,6 +29,20 @@ public: return MockStore::instance().recordAndReturn(m_moduleName, methodName, args); } + void callMethodAsync(const QString& /*authToken*/, + const QString& methodName, + const QVariantList& args, + int /*timeoutMs*/, + AsyncResultCallback callback) override + { + if (!callback) return; + QString mod = m_moduleName; + QTimer::singleShot(0, [mod, methodName, args, callback]() { + QVariant result = MockStore::instance().recordAndReturn(mod, methodName, args); + callback(result); + }); + } + bool informModuleToken(const QString& /*authToken*/, const QString& moduleName, const QString& /*token*/, diff --git a/cpp/implementations/qt_local/local_transport.cpp b/cpp/implementations/qt_local/local_transport.cpp index a257e1c..b2eaacf 100644 --- a/cpp/implementations/qt_local/local_transport.cpp +++ b/cpp/implementations/qt_local/local_transport.cpp @@ -3,6 +3,7 @@ #include "../../module_proxy.h" #include #include +#include // ── LocalLogosObject ───────────────────────────────────────────────────────── @@ -57,6 +58,24 @@ public: return m_proxy->callRemoteMethod(authToken, methodName, args); } + void callMethodAsync(const QString& authToken, + const QString& methodName, + const QVariantList& args, + int /*timeoutMs*/, + AsyncResultCallback callback) override + { + if (!callback) return; + if (!m_proxy) { + QTimer::singleShot(0, [callback]() { callback(QVariant()); }); + return; + } + ModuleProxy* proxy = m_proxy; + QTimer::singleShot(0, [proxy, authToken, methodName, args, callback]() { + QVariant result = proxy->callRemoteMethod(authToken, methodName, args); + callback(result); + }); + } + bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token, diff --git a/cpp/implementations/qt_remote/remote_transport.cpp b/cpp/implementations/qt_remote/remote_transport.cpp index 9f22da5..72a6d11 100644 --- a/cpp/implementations/qt_remote/remote_transport.cpp +++ b/cpp/implementations/qt_remote/remote_transport.cpp @@ -3,11 +3,15 @@ #include #include #include +#include +#include +#include #include #include #include #include #include +#include // ── RemoteLogosObject ──────────────────────────────────────────────────────── @@ -89,6 +93,66 @@ public: return pendingCall.returnValue(); } + void callMethodAsync(const QString& authToken, + const QString& methodName, + const QVariantList& args, + int timeoutMs, + AsyncResultCallback callback) override + { + if (!callback) return; + if (!m_replica) { + QTimer::singleShot(0, [callback]() { callback(QVariant()); }); + return; + } + + qDebug() << "[LogosObject] RemoteLogosObject::callMethodAsync" << methodName << "args:" << args.size(); + + QRemoteObjectPendingCall pendingCall; + bool success = QMetaObject::invokeMethod( + m_replica, + "callRemoteMethod", + Qt::DirectConnection, + Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall), + Q_ARG(QString, authToken), + Q_ARG(QString, methodName), + Q_ARG(QVariantList, args) + ); + + if (!success) { + qWarning() << "RemoteLogosObject: Failed to invoke callRemoteMethod on replica (async)"; + QTimer::singleShot(0, [callback]() { callback(QVariant()); }); + return; + } + + auto handled = std::make_shared(false); + auto* watcher = new QRemoteObjectPendingCallWatcher(pendingCall); + + // Success handler -- delivers result on the consumer's thread + QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished, + watcher, [callback, handled](QRemoteObjectPendingCallWatcher* w) { + if (*handled) { w->deleteLater(); return; } + *handled = true; + QVariant result; + if (w->error() == QRemoteObjectPendingCall::NoError) { + result = w->returnValue(); + } else { + qWarning() << "RemoteLogosObject: async callMethod error:" << w->error(); + } + callback(result); + w->deleteLater(); + }, Qt::QueuedConnection); + + // Timeout handler -- fires if the watcher hasn't finished in time + QPointer weakWatcher(watcher); + QTimer::singleShot(timeoutMs, [weakWatcher, callback, handled]() { + if (*handled) return; + *handled = true; + qWarning() << "RemoteLogosObject: async callMethod timed out"; + callback(QVariant()); + if (!weakWatcher.isNull()) weakWatcher->deleteLater(); + }); + } + bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token, diff --git a/cpp/logos_api_consumer.cpp b/cpp/logos_api_consumer.cpp index 26f480b..fac2cdb 100644 --- a/cpp/logos_api_consumer.cpp +++ b/cpp/logos_api_consumer.cpp @@ -97,10 +97,12 @@ void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const Q return; } - qDebug() << "[LogosObject] LogosAPIConsumer: async calling via LogosObject::callMethod" << methodName; - QVariant result = plugin->callMethod(authToken, methodName, args, timeout.ms); - plugin->release(); - QTimer::singleShot(0, this, [callback, result]() { callback(result); }); + qDebug() << "[LogosObject] LogosAPIConsumer: async calling via LogosObject::callMethodAsync" << methodName; + plugin->callMethodAsync(authToken, methodName, args, timeout.ms, + [plugin, callback](QVariant result) { + plugin->release(); + callback(result); + }); } void LogosAPIConsumer::onEvent(LogosObject* originObject, const QString& eventName, std::function callback) diff --git a/cpp/logos_object.h b/cpp/logos_object.h index 7e1126d..cedaddd 100644 --- a/cpp/logos_object.h +++ b/cpp/logos_object.h @@ -33,6 +33,26 @@ public: const QVariantList& args, int timeoutMs) = 0; + using AsyncResultCallback = std::function; + + /** + * @brief Invoke a method asynchronously; result is delivered via callback. + * + * Returns immediately. The callback is always invoked on a subsequent + * event-loop iteration, never synchronously inside this call. + * + * @param authToken Authentication token for the operation + * @param methodName Method to call on the underlying module + * @param args Arguments for the method + * @param timeoutMs Maximum time to wait for the result + * @param callback Called with the result (invalid QVariant on failure/timeout) + */ + virtual void callMethodAsync(const QString& authToken, + const QString& methodName, + const QVariantList& args, + int timeoutMs, + AsyncResultCallback callback) = 0; + /** * @brief Deliver a module token to the underlying module. * @param authToken Authentication token for the operation diff --git a/tests/sdk/CMakeLists.txt b/tests/sdk/CMakeLists.txt index 5058607..4aeda45 100644 --- a/tests/sdk/CMakeLists.txt +++ b/tests/sdk/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(sdk_tests test_mock_transport.cpp test_local_transport_integration.cpp test_event_system.cpp + test_async_calls.cpp test_provider_dispatch.cpp fixtures/sample_provider.cpp ${GENERATED_DISPATCH} diff --git a/tests/sdk/test_async_calls.cpp b/tests/sdk/test_async_calls.cpp new file mode 100644 index 0000000..44138a4 --- /dev/null +++ b/tests/sdk/test_async_calls.cpp @@ -0,0 +1,288 @@ +#include +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" + +class AsyncCallsTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_api; + delete m_mock; + } + + void createApi(const QString& targetModule = "mod") + { + m_api = new LogosAPI("origin"); + m_client = m_api->getClient(targetModule); + } + + LogosMockSetup* m_mock = nullptr; + LogosAPI* m_api = nullptr; + LogosAPIClient* m_client = nullptr; +}; + +TEST_F(AsyncCallsTest, BasicAsyncCallReturnsCorrectResult) +{ + m_mock->when("mod", "getValue").thenReturn(QVariant(42)); + createApi(); + + bool called = false; + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "getValue", QVariantList(), + [&](QVariant v) { called = true; received = v; }); + + EXPECT_FALSE(called); // callback must not fire synchronously + QCoreApplication::processEvents(); + EXPECT_TRUE(called); + EXPECT_EQ(received.toInt(), 42); +} + +TEST_F(AsyncCallsTest, AsyncCallWithStringResult) +{ + m_mock->when("mod", "getName").thenReturn(QVariant("hello")); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "getName", QVariantList(), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toString(), "hello"); +} + +TEST_F(AsyncCallsTest, AsyncCallWithBoolResult) +{ + m_mock->when("mod", "isReady").thenReturn(QVariant(true)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "isReady", QVariantList(), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_TRUE(received.toBool()); +} + +TEST_F(AsyncCallsTest, AsyncCallWithDoubleResult) +{ + m_mock->when("mod", "getPrice").thenReturn(QVariant(3.14)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "getPrice", QVariantList(), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_DOUBLE_EQ(received.toDouble(), 3.14); +} + +TEST_F(AsyncCallsTest, AsyncCallWithVariantListResult) +{ + QVariantList expected = {1, 2, 3}; + m_mock->when("mod", "getItems").thenReturn(QVariant(expected)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "getItems", QVariantList(), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toList().size(), 3); +} + +TEST_F(AsyncCallsTest, AsyncCallWithVariantMapResult) +{ + QVariantMap expected; + expected["key"] = "value"; + expected["count"] = 5; + m_mock->when("mod", "getData").thenReturn(QVariant(expected)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "getData", QVariantList(), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + QVariantMap result = received.toMap(); + EXPECT_EQ(result["key"].toString(), "value"); + EXPECT_EQ(result["count"].toInt(), 5); +} + +TEST_F(AsyncCallsTest, AsyncCallForwardsArguments) +{ + m_mock->when("mod", "add").thenReturn(QVariant(30)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "add", + QVariantList() << 10 << 20, + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 30); + EXPECT_TRUE(m_mock->wasCalledWith("mod", "add", QVariantList() << 10 << 20)); +} + +TEST_F(AsyncCallsTest, AsyncCallTracksCallCount) +{ + m_mock->when("mod", "ping").thenReturn(QVariant("pong")); + createApi(); + + int callCount = 0; + auto cb = [&](QVariant) { callCount++; }; + + m_client->invokeRemoteMethodAsync("mod", "ping", QVariantList(), cb); + m_client->invokeRemoteMethodAsync("mod", "ping", QVariantList(), cb); + m_client->invokeRemoteMethodAsync("mod", "ping", QVariantList(), cb); + + QCoreApplication::processEvents(); + EXPECT_EQ(callCount, 3); + EXPECT_EQ(m_mock->callCount("mod", "ping"), 3); +} + +TEST_F(AsyncCallsTest, AsyncNullCallbackDoesNotCrash) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(1)); + createApi(); + + m_client->invokeRemoteMethodAsync("mod", "fn", QVariantList(), nullptr); + QCoreApplication::processEvents(); + // no crash = pass +} + +TEST_F(AsyncCallsTest, AsyncCallNoExpectationReturnsInvalidVariant) +{ + createApi("other"); + + bool called = false; + QVariant received(42); // pre-set to non-default + m_client->invokeRemoteMethodAsync("other", "nonexistent", QVariantList(), + [&](QVariant v) { called = true; received = v; }); + + QCoreApplication::processEvents(); + EXPECT_TRUE(called); + EXPECT_FALSE(received.isValid()); +} + +TEST_F(AsyncCallsTest, MultipleConcurrentAsyncCalls) +{ + m_mock->when("mod", "a").thenReturn(QVariant(1)); + m_mock->when("mod", "b").thenReturn(QVariant(2)); + m_mock->when("mod", "c").thenReturn(QVariant(3)); + createApi(); + + QVariant ra, rb, rc; + m_client->invokeRemoteMethodAsync("mod", "a", QVariantList(), [&](QVariant v) { ra = v; }); + m_client->invokeRemoteMethodAsync("mod", "b", QVariantList(), [&](QVariant v) { rb = v; }); + m_client->invokeRemoteMethodAsync("mod", "c", QVariantList(), [&](QVariant v) { rc = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(ra.toInt(), 1); + EXPECT_EQ(rb.toInt(), 2); + EXPECT_EQ(rc.toInt(), 3); +} + +TEST_F(AsyncCallsTest, AsyncCallbackIsNeverSynchronous) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(99)); + createApi(); + + bool calledDuringInvoke = false; + bool calledAfterProcessEvents = false; + + m_client->invokeRemoteMethodAsync("mod", "fn", QVariantList(), + [&](QVariant) { + calledAfterProcessEvents = true; + }); + + calledDuringInvoke = calledAfterProcessEvents; + EXPECT_FALSE(calledDuringInvoke); // must not fire synchronously + + QCoreApplication::processEvents(); + EXPECT_TRUE(calledAfterProcessEvents); +} + +TEST_F(AsyncCallsTest, AsyncOneArgOverload) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(10)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant("arg1"), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 10); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 1); + EXPECT_EQ(last[0].toString(), "arg1"); +} + +TEST_F(AsyncCallsTest, AsyncTwoArgOverload) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(20)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant(1), QVariant(2), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 20); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 2); +} + +TEST_F(AsyncCallsTest, AsyncThreeArgOverload) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(30)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", + QVariant(1), QVariant(2), QVariant(3), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 30); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 3); +} + +TEST_F(AsyncCallsTest, AsyncFourArgOverload) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(40)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", + QVariant(1), QVariant(2), QVariant(3), QVariant(4), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 40); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 4); +} + +TEST_F(AsyncCallsTest, AsyncFiveArgOverload) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(50)); + createApi(); + + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", + QVariant(1), QVariant(2), QVariant(3), QVariant(4), QVariant(5), + [&](QVariant v) { received = v; }); + + QCoreApplication::processEvents(); + EXPECT_EQ(received.toInt(), 50); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 5); +}