mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-27 10:11:12 +00:00
test(ipc-new-api): cover async consumption from a Qt-free module
test_ipc_module was the only module asserting async inter-module calls, and it
is a Qt consumer — so migrating it off the legacy interface would have deleted
the repo's only async coverage. Its universal successor covered zero async.
This closes that hole, which is the precondition for retiring the Qt-consumer
original rather than a change to it.
Four methods mirroring the originals by name. On the Qt consumer three were RAW
invokeRemoteMethodAsync and one went through a generated wrapper; here all four
go through wrappers, because on the lp surface `<name>Async` IS the generated
wrapper and bottoms out in lp_invoke_async. So these assert something the Qt
ones cannot: async delivery into a module with no Qt in its own TUs.
WHY concurrency: "multi" IS REQUIRED, and is not incidental here. A Qt-affine
client is bound to the Qt main thread, and its completions are marshalled back
to that thread. Under `single`, dispatch runs on the main thread too — so a
method that blocks waiting for its own completion is blocking the exact thread
that has to deliver it, and every call burns its full timeout and returns a
default-constructed value. That is logos_thread_marshal.h's documented hazard
("only pumps events while it happens to be blocked inside a call") reached from
the other side. The Qt consumer never hit it because QEventLoop PUMPS while it
waits; std::future::wait_for does not. Under `multi` the handler runs on a
worker QThread and the main thread stays free to marshal.
Measured, not assumed: under `single` seven of the eight assertions failed with
empty/zero results. Note the eighth, asyncCallBasicAddInts(0, 0), PASSED —
the timeout sentinel is 0, which is also the expected answer. It is kept
deliberately, next to (3, 4), so the pair cannot both go green on a stall.
The promise is held by shared_ptr rather than by reference into the frame: on
the timeout path the frame is gone while the call is still outstanding, and a
late completion would otherwise write through a dangling reference — corrupting
memory precisely when something has already gone wrong. The wait is bounded so
a stall names the method that stalled instead of surfacing as the harness's own
timeout.
Requires the qt-host-generator capture-list fix: this module has a void method
and no result method, the combination that did not compile under multi.
Verified: 31 passed, 0 failed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c221e1128f
commit
bc942e2a37
@@ -6,6 +6,7 @@
|
||||
"description": "Test module exercising inter-module communication via the new LogosProviderBase API",
|
||||
"main": "test_ipc_new_api_module_plugin",
|
||||
"interface": "universal",
|
||||
"concurrency": "multi",
|
||||
"codegen": {
|
||||
"impl_header": "src/test_ipc_new_api_impl.h",
|
||||
"impl_class": "TestIpcNewApiImpl"
|
||||
|
||||
@@ -1,7 +1,39 @@
|
||||
#include "test_ipc_new_api_impl.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include <logos_sdk.h> // generated: modules().<dep> typed wrappers
|
||||
|
||||
namespace {
|
||||
|
||||
// Drive one `<name>Async(args..., callback)` wrapper and block until it fires.
|
||||
//
|
||||
// The promise is held by SHARED pointer, not by reference into this frame. On
|
||||
// the timeout path below the frame goes away while the call is still
|
||||
// outstanding, and a late completion then writes through whatever it was given
|
||||
// — a reference would make that a write to a dead promise, i.e. the timeout
|
||||
// path would corrupt memory precisely when something is already wrong.
|
||||
//
|
||||
// Bounded rather than infinite. An async wrapper that never fires would
|
||||
// otherwise surface as the harness's own 30s timeout with no indication of
|
||||
// WHICH call stalled; returning the sentinel turns it into an ordinary failed
|
||||
// assertion naming the method.
|
||||
template <typename T, typename Start>
|
||||
T awaitAsync(Start&& start)
|
||||
{
|
||||
auto box = std::make_shared<std::promise<T>>();
|
||||
auto fut = box->get_future();
|
||||
start([box](T v) { box->set_value(std::move(v)); });
|
||||
if (fut.wait_for(std::chrono::seconds(10)) != std::future_status::ready)
|
||||
return T{};
|
||||
return fut.get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Every method here forwards through the generated type-safe wrappers. The
|
||||
// previous implementation used LogosAPIClient::invokeRemoteMethod with a
|
||||
// module name and method name as strings; a typo in either was a runtime
|
||||
@@ -102,6 +134,43 @@ std::string TestIpcNewApiImpl::wrapperExtlibReverse(const std::string& input)
|
||||
return modules().test_extlib_module.reverseString(input);
|
||||
}
|
||||
|
||||
// ── Async calls ──────────────────────────────────────────────────────────────
|
||||
// The async half of the SAME generated wrappers the sync methods above use;
|
||||
// on this (lp) surface `<name>Async` bottoms out in lp_invoke_async, so these
|
||||
// exercise Qt-free async delivery end to end rather than only its signature.
|
||||
|
||||
std::string TestIpcNewApiImpl::asyncCallBasicEcho(const std::string& input)
|
||||
{
|
||||
return awaitAsync<std::string>([&](auto cb) {
|
||||
modules().test_basic_module.echoAsync(input, std::move(cb));
|
||||
});
|
||||
}
|
||||
|
||||
int64_t TestIpcNewApiImpl::asyncCallBasicAddInts(int64_t a, int64_t b)
|
||||
{
|
||||
return awaitAsync<int64_t>([&](auto cb) {
|
||||
modules().test_basic_module.addIntsAsync(a, b, std::move(cb));
|
||||
});
|
||||
}
|
||||
|
||||
std::string TestIpcNewApiImpl::asyncCallExtlibReverse(const std::string& input)
|
||||
{
|
||||
return awaitAsync<std::string>([&](auto cb) {
|
||||
modules().test_extlib_module.reverseStringAsync(input, std::move(cb));
|
||||
});
|
||||
}
|
||||
|
||||
std::string TestIpcNewApiImpl::asyncWrapperBasicEcho(const std::string& input)
|
||||
{
|
||||
// Identical to asyncCallBasicEcho by construction — see the header. Kept as
|
||||
// its own method because the ipc test group calls it by name, and because
|
||||
// its Qt-consumer predecessor was the one async case that ALREADY went
|
||||
// through a generated wrapper; keeping the name keeps that lineage visible.
|
||||
return awaitAsync<std::string>([&](auto cb) {
|
||||
modules().test_basic_module.echoAsync(input, std::move(cb));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────────
|
||||
|
||||
void TestIpcNewApiImpl::triggerBasicEvent(const std::string& data)
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
//
|
||||
// The method surface is deliberately UNCHANGED across that migration (same
|
||||
// names, same arity, same wire types) so the ipc test group keeps asserting
|
||||
// exactly what it asserted before.
|
||||
// exactly what it asserted before. That includes the async four: on the Qt
|
||||
// consumer three of them were RAW invokeRemoteMethodAsync and only the fourth
|
||||
// went through a generated wrapper, whereas here all four do — the same
|
||||
// collapse the sync `wrapper*` pair below already went through. The names are
|
||||
// kept so the distinction stays legible in the harness output.
|
||||
//
|
||||
// NO trailing `// comments` on declaration lines (the parser needs a `;`).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -61,6 +65,23 @@ public:
|
||||
std::string wrapperBasicEcho(const std::string& input);
|
||||
std::string wrapperExtlibReverse(const std::string& input);
|
||||
|
||||
// ── Async calls ──────────────────────────────────────────────────────────
|
||||
// The async half of the consumer surface. `<name>Async(args..., callback)`
|
||||
// is emitted by the SAME generator pass as the sync wrappers above, on the
|
||||
// lp path, and bottoms out in lp_invoke_async — so this exercises the
|
||||
// Qt-free transport's async delivery, not just the API's shape.
|
||||
//
|
||||
// Each blocks until the callback fires and returns the value, so the ipc
|
||||
// test group can assert on a return exactly as it did against the Qt
|
||||
// consumer that preceded this module. Blocking is safe here specifically
|
||||
// because plain-transport completions are pinned to arrive off both the
|
||||
// caller's thread and the io thread (logos-protocol's
|
||||
// test_delivery_without_qt), so the wait cannot starve its own completion.
|
||||
std::string asyncCallBasicEcho(const std::string& input);
|
||||
int64_t asyncCallBasicAddInts(int64_t a, int64_t b);
|
||||
std::string asyncCallExtlibReverse(const std::string& input);
|
||||
std::string asyncWrapperBasicEcho(const std::string& input);
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
// Asks test_basic_module to emit its own event, then emits our own.
|
||||
void triggerBasicEvent(const std::string& data);
|
||||
|
||||
@@ -1061,6 +1061,21 @@ test_ipc_new_api "wrapperBasicEcho(test123)" "Result: test123"
|
||||
test_ipc_new_api "wrapperExtlibReverse(hello)" "Result: olleh" "test_ipc_new_api_module.wrapperExtlibReverse(hello)"
|
||||
test_ipc_new_api "wrapperExtlibReverse(abc)" "Result: cba" "test_ipc_new_api_module.wrapperExtlibReverse(abc)"
|
||||
|
||||
echo ""
|
||||
echo " -- IPC new-API: async over the lp transport --"
|
||||
# The async half of the same generated wrappers used above. On this surface
|
||||
# `<name>Async` bottoms out in lp_invoke_async, so unlike the `async` group
|
||||
# (which exercises the QT consumer through test_ipc_module) these assert async
|
||||
# delivery into a module with NO Qt in its own translation units.
|
||||
test_ipc_new_api "asyncCallBasicEcho(hello)" "Result: hello" "test_ipc_new_api_module.asyncCallBasicEcho(hello)"
|
||||
test_ipc_new_api "asyncCallBasicEcho(world)" "Result: world" "test_ipc_new_api_module.asyncCallBasicEcho(world)"
|
||||
test_ipc_new_api "asyncCallBasicAddInts(3, 4)" "Result: 7" "test_ipc_new_api_module.asyncCallBasicAddInts(3, 4)"
|
||||
test_ipc_new_api "asyncCallBasicAddInts(0, 0)" "Result: 0" "test_ipc_new_api_module.asyncCallBasicAddInts(0, 0)"
|
||||
test_ipc_new_api "asyncCallExtlibReverse(hello)" "Result: olleh" "test_ipc_new_api_module.asyncCallExtlibReverse(hello)"
|
||||
test_ipc_new_api "asyncCallExtlibReverse(abc)" "Result: cba" "test_ipc_new_api_module.asyncCallExtlibReverse(abc)"
|
||||
test_ipc_new_api "asyncWrapperBasicEcho(hello)" "Result: hello" "test_ipc_new_api_module.asyncWrapperBasicEcho(hello)"
|
||||
test_ipc_new_api "asyncWrapperBasicEcho(test123)" "Result: test123" "test_ipc_new_api_module.asyncWrapperBasicEcho(test123)"
|
||||
|
||||
echo ""
|
||||
echo " -- IPC new-API: events --"
|
||||
skip_test "triggerBasicEvent(data)" "void return → invalid QVariant → logoscore exit 1"
|
||||
|
||||
Reference in New Issue
Block a user