diff --git a/cpp/implementations/mock/mock_store.cpp b/cpp/implementations/mock/mock_store.cpp index 03af7aa..bcdaf3d 100644 --- a/cpp/implementations/mock/mock_store.cpp +++ b/cpp/implementations/mock/mock_store.cpp @@ -13,6 +13,20 @@ void MockStore::reset() QMutexLocker lock(&m_mutex); m_expectations.clear(); m_calls.clear(); + m_mockObjectReleaseProbe = nullptr; +} + +void MockStore::setMockObjectReleaseProbe(std::atomic* counter) +{ + QMutexLocker lock(&m_mutex); + m_mockObjectReleaseProbe = counter; +} + +void MockStore::incrementMockObjectReleaseProbeIfSet() +{ + QMutexLocker lock(&m_mutex); + if (m_mockObjectReleaseProbe) + ++(*m_mockObjectReleaseProbe); } // ── ExpectationBuilder ─────────────────────────────────────────────────────── diff --git a/cpp/implementations/mock/mock_store.h b/cpp/implementations/mock/mock_store.h index d043e87..8c56c77 100644 --- a/cpp/implementations/mock/mock_store.h +++ b/cpp/implementations/mock/mock_store.h @@ -1,12 +1,15 @@ #ifndef MOCK_STORE_H #define MOCK_STORE_H +#include #include #include #include #include #include +class MockLogosObject; + /** * @brief Records a single intercepted call to a mocked module method. */ @@ -94,7 +97,18 @@ public: QVariantList lastArgs(const QString& module, const QString& method) const; QList allCalls() const; + /** + * @brief When non-null, MockLogosObject::release() increments *counter exactly once. + * + * Used by sdk_tests to assert LogosAPIConsumer invokes the async user callback before + * calling plugin->release(). Cleared by reset(). + */ + void setMockObjectReleaseProbe(std::atomic* counter); + private: + friend class MockLogosObject; + void incrementMockObjectReleaseProbeIfSet(); + MockStore() = default; MockStore(const MockStore&) = delete; MockStore& operator=(const MockStore&) = delete; @@ -102,6 +116,7 @@ private: mutable QMutex m_mutex; QList m_expectations; QList m_calls; + std::atomic* m_mockObjectReleaseProbe = nullptr; friend class ExpectationBuilder; }; diff --git a/cpp/implementations/mock/mock_transport.h b/cpp/implementations/mock/mock_transport.h index 47bcaa5..0131d79 100644 --- a/cpp/implementations/mock/mock_transport.h +++ b/cpp/implementations/mock/mock_transport.h @@ -58,7 +58,11 @@ public: QJsonArray getMethods() override { return QJsonArray(); } - void release() override { delete this; } + void release() override + { + MockStore::instance().incrementMockObjectReleaseProbeIfSet(); + delete this; + } quintptr id() const override { return reinterpret_cast(this); } diff --git a/cpp/logos_api_consumer.cpp b/cpp/logos_api_consumer.cpp index 7e13006..fcb23ab 100644 --- a/cpp/logos_api_consumer.cpp +++ b/cpp/logos_api_consumer.cpp @@ -106,9 +106,15 @@ void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const Q QPointer self(this); plugin->callMethodAsync(authToken, methodName, args, timeout.ms, [plugin, callback, self](QVariant result) { - plugin->release(); - if (!self) return; + // Deliver the result before release(): destroying the replica can + // invalidate storage that QVariant still references for some return + // types (matches sync invokeRemoteMethod: callMethod then release). + if (!self) { + plugin->release(); + return; + } callback(result); + plugin->release(); }); } diff --git a/tests/sdk/test_async_calls.cpp b/tests/sdk/test_async_calls.cpp index 44138a4..f31af2e 100644 --- a/tests/sdk/test_async_calls.cpp +++ b/tests/sdk/test_async_calls.cpp @@ -1,6 +1,8 @@ #include +#include #include #include "logos_mock.h" +#include "mock_store.h" #include "logos_api.h" #include "logos_api_client.h" @@ -96,6 +98,33 @@ TEST_F(AsyncCallsTest, AsyncCallWithVariantListResult) EXPECT_EQ(received.toList().size(), 3); } +TEST_F(AsyncCallsTest, AsyncUserCallbackRunsBeforeMockLogosObjectRelease) +{ + // Regression: LogosAPIConsumer must invoke the user callback before plugin->release(). + // Releasing the mock/replica first can invalidate QVariant payloads (e.g. remote lists), + // which manifested as crashes inside Qt when converting the async result. + std::atomic releaseCount{0}; + MockStore::instance().setMockObjectReleaseProbe(&releaseCount); + + QVariantList expected = {QStringLiteral("pkg_a"), QStringLiteral("pkg_b")}; + m_mock->when("mod", "getInstalledPackages").thenReturn(QVariant(expected)); + createApi(); + + bool userCallbackRan = false; + m_client->invokeRemoteMethodAsync("mod", "getInstalledPackages", QVariantList(), + [&](QVariant v) { + userCallbackRan = true; + EXPECT_EQ(releaseCount.load(), 0) + << "async user callback must run before MockLogosObject::release()"; + ASSERT_EQ(v.toList().size(), 2); + EXPECT_EQ(v.toList().at(0).toString(), QStringLiteral("pkg_a")); + }); + + QCoreApplication::processEvents(); + EXPECT_TRUE(userCallbackRan); + EXPECT_EQ(releaseCount.load(), 1); +} + TEST_F(AsyncCallsTest, AsyncCallWithVariantMapResult) { QVariantMap expected;