// The same call-error channel, over the DEFAULT transport. // // LogosProtocol::LocalSocket (qt_remote / QtRO) is the default in // LogosTransportConfig, so it is what an in-process module host actually uses. // test_call_error_after_acquire.cpp proves the channel over plain TCP; if the // fix stopped there, the transport most calls go over would still be reporting // a timed-out call as a method that returned null. // // The failure exercised here is a DEFERRED ("multi") call whose completion // event never arrives: RemoteLogosObject answers the pending sentinel, arms its // bounded wait, and gives up. That branch used to deliver a bare QVariant(); // it now delivers a "timeout" CallError. Using the deferred path rather than a // sleeping provider is deliberate — QtRO dispatches the source call on this // same event loop, so a provider that blocked would stall the very loop the // consumer needs, and the test would be measuring the harness. // // The assertions are made at LogosAPIConsumer::invokeRemoteMethodAsync, which // is the site that used to hard-code `logos::CallError{}` next to every result. // lp_invoke_async is a thin renderer over that CallError (proved end-to-end in // test_call_error_after_acquire.cpp), so pinning it here pins the C ABI too. #include #include "logos_api_consumer.h" #include "logos_async_dispatch.h" #include "logos_instance.h" #include "logos_object.h" #include "logos_provider_interface.h" #include "logos_transport_config.h" #include "module_proxy.h" #include "remote_transport.h" #include "token_manager.h" #include #include #include #include #include #include #include #include #include namespace { QCoreApplication* ensureApp() { static int argc = 0; static char* argv[] = { nullptr }; if (!QCoreApplication::instance()) new QCoreApplication(argc, argv); return QCoreApplication::instance(); } // compute() answers straight away; stall() answers the pending sentinel and // then never pushes the completion event, so the consumer's bounded wait is // the only thing that ends the call. class StallProvider : public LogosProviderObject { public: QVariant callMethod(const QString& method, const QVariantList& /*args*/) override { if (method == QLatin1String("compute")) return QVariant(7); if (method == QLatin1String("stall")) { QVariantMap sentinel; sentinel[logos::pendingCallKey()] = QStringLiteral("never-completes"); return sentinel; } return QVariant(); } bool informModuleToken(const QString&, const QString&) override { return true; } QJsonArray getMethods() override { return QJsonArray{}; } void setEventListener(EventCallback) override {} void init(void*) override {} QString providerName() const override { return QStringLiteral("qtro_module"); } QString providerVersion() const override { return QStringLiteral("1.0.0"); } }; } // namespace class QtRemoteCallErrorTest : public ::testing::Test { protected: void SetUp() override { ensureApp(); } void pumpEventLoop(int ms) { auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); while (std::chrono::steady_clock::now() < end) { QCoreApplication::processEvents(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } }; // ── control: a successful QtRO async call reports its value and NO error ──── TEST_F(QtRemoteCallErrorTest, AsyncSuccessCarriesAnEmptyError) { const QString registryUrl = LogosInstance::id("qtro_ok_module"); RemoteTransportHost host(registryUrl); StallProvider provider; ModuleProxy proxy(&provider); ASSERT_TRUE(proxy.saveToken(QStringLiteral("origin"), QStringLiteral("tok-1"))); ASSERT_TRUE(host.publishObject("qtro_ok_module", &proxy)); LogosAPIConsumer consumer(QStringLiteral("qtro_ok_module"), QStringLiteral("origin"), &TokenManager::instance()); ASSERT_TRUE(consumer.isConnected()); std::atomic delivered{0}; QVariant got; logos::CallError err; consumer.invokeRemoteMethodAsync( QStringLiteral("tok-1"), QStringLiteral("qtro_ok_module"), QStringLiteral("compute"), QVariantList{}, [&](QVariant v, const logos::CallError& e) { got = std::move(v); err = e; delivered.fetch_add(1); }, Timeout(3000)); for (int i = 0; i < 60 && delivered.load() == 0; ++i) pumpEventLoop(50); std::cout << " QtRO success -> ok=" << err.ok() << " value=" << got.toInt() << std::endl; ASSERT_EQ(delivered.load(), 1) << "async callback never fired"; EXPECT_TRUE(err.ok()) << "a successful QtRO call reported error " << err.code; EXPECT_EQ(got.toInt(), 7); } // ── the deadline elapsed on the default transport ─────────────────────────── TEST_F(QtRemoteCallErrorTest, AsyncTimeoutCarriesTheCanonicalError) { const QString registryUrl = LogosInstance::id("qtro_stall_module"); RemoteTransportHost host(registryUrl); StallProvider provider; ModuleProxy proxy(&provider); ASSERT_TRUE(proxy.saveToken(QStringLiteral("origin"), QStringLiteral("tok-1"))); ASSERT_TRUE(host.publishObject("qtro_stall_module", &proxy)); LogosAPIConsumer consumer(QStringLiteral("qtro_stall_module"), QStringLiteral("origin"), &TokenManager::instance()); ASSERT_TRUE(consumer.isConnected()); std::atomic delivered{0}; QVariant got; logos::CallError err; consumer.invokeRemoteMethodAsync( QStringLiteral("tok-1"), QStringLiteral("qtro_stall_module"), QStringLiteral("stall"), QVariantList{}, [&](QVariant v, const logos::CallError& e) { got = std::move(v); err = e; delivered.fetch_add(1); }, Timeout(400)); for (int i = 0; i < 100 && delivered.load() == 0; ++i) pumpEventLoop(50); std::cout << " QtRO timeout -> code='" << err.code << "' origin='" << err.origin << "' message='" << err.message << "'" << std::endl; ASSERT_EQ(delivered.load(), 1) << "async callback never fired"; EXPECT_EQ(err.code, "timeout") << "a timed-out QtRO call reported success"; EXPECT_EQ(err.origin, "qtro_stall_module"); EXPECT_FALSE(err.message.empty()); EXPECT_FALSE(got.isValid()); }