mirror of
https://github.com/logos-co/logos-view-module-runtime.git
synced 2026-08-29 12:01:06 +00:00
Merge master into feat/sdk-codegen-b4-qt-host
Brings in the hot-reload fix (#24). module-builder's B4 branch pins this runtime for view-interface-abi, so without this merge that pin would walk master's bump backward and lose the fix.
This commit is contained in:
@@ -144,6 +144,16 @@ public:
|
||||
// True once the view module's replica is Valid (source meta received).
|
||||
Q_INVOKABLE bool isViewModuleReady(const QString& moduleName) const;
|
||||
|
||||
// Re-emit viewModuleReadyChanged for every replica that is already Valid.
|
||||
//
|
||||
// Replicas outlive the QML engine (they are parented here, with
|
||||
// CppOwnership), so a view rebuilt against a fresh engine — hot reload —
|
||||
// gets the cached replica back from module() without any state transition.
|
||||
// Its Connections were created after the original Valid edge and would
|
||||
// otherwise wait forever for a signal that already fired. Hosts call this
|
||||
// once the new object tree is complete.
|
||||
void replayViewModuleState();
|
||||
|
||||
// Watch a QRemoteObjectPendingCall returned by a replica slot call and
|
||||
// invoke callbacks with the result. Replaces QtRemoteObjects.watch() so
|
||||
// QML plugins don't need to import QtRemoteObjects (keeping the sandbox
|
||||
|
||||
@@ -401,6 +401,15 @@ bool LogosQmlBridge::isViewModuleReady(const QString& moduleName) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogosQmlBridge::replayViewModuleState()
|
||||
{
|
||||
for (auto it = m_replicas.cbegin(); it != m_replicas.cend(); ++it) {
|
||||
auto* rep = qobject_cast<QRemoteObjectReplica*>(it.value());
|
||||
if (rep && rep->state() == QRemoteObjectReplica::Valid)
|
||||
emit viewModuleReadyChanged(it.key(), true);
|
||||
}
|
||||
}
|
||||
|
||||
void LogosQmlBridge::setViewModuleSocket(const QString& moduleName,
|
||||
const QString& socketName)
|
||||
{
|
||||
|
||||
@@ -185,3 +185,40 @@ target_link_libraries(test_logos_qml_bridge_calls PRIVATE
|
||||
)
|
||||
|
||||
add_test(NAME LogosQmlBridgeCallsTests COMMAND test_logos_qml_bridge_calls)
|
||||
|
||||
# Readiness replay for QML hot reload. m_replicas is only ever populated through
|
||||
# module(), which loads a factory plugin — so covering the branch where replay
|
||||
# meets a genuinely Valid replica needs a real factory plugin and a real
|
||||
# published source, not just the empty/dropped maps the unit suite can build.
|
||||
add_library(test_replica_factory_plugin MODULE test_replica_factory_plugin.cpp)
|
||||
target_link_libraries(test_replica_factory_plugin PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::RemoteObjects
|
||||
)
|
||||
target_include_directories(test_replica_factory_plugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
||||
)
|
||||
|
||||
add_executable(test_logos_qml_bridge_replay
|
||||
test_logos_qml_bridge_replay.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_logos_qml_bridge_replay PRIVATE
|
||||
logos_view_module_runtime
|
||||
Qt6::Core
|
||||
Qt6::Test
|
||||
Qt6::Qml
|
||||
Qt6::RemoteObjects
|
||||
logos-qt-sdk::logos_qt_sdk
|
||||
logos-cpp-sdk::logos_headers
|
||||
logos-protocol::logos_protocol
|
||||
)
|
||||
|
||||
# The bridge takes a filesystem path to the factory plugin, so the test needs
|
||||
# to know where CMake put it.
|
||||
target_compile_definitions(test_logos_qml_bridge_replay PRIVATE
|
||||
TEST_REPLICA_FACTORY_PLUGIN="$<TARGET_FILE:test_replica_factory_plugin>"
|
||||
)
|
||||
add_dependencies(test_logos_qml_bridge_replay test_replica_factory_plugin)
|
||||
|
||||
add_test(NAME LogosQmlBridgeReplayTests COMMAND test_logos_qml_bridge_replay)
|
||||
|
||||
@@ -325,6 +325,27 @@ private slots:
|
||||
// still knows the socket so it can reacquire.
|
||||
QVERIFY(bridge.hasViewModuleSocket("foo"));
|
||||
}
|
||||
|
||||
// ── Readiness replay (hot reload) ────────────────────────────────────
|
||||
|
||||
void replayViewModuleState_noReplicas_emitsNothing()
|
||||
{
|
||||
LogosQmlBridge bridge(nullptr);
|
||||
QSignalSpy spy(&bridge, &LogosQmlBridge::viewModuleReadyChanged);
|
||||
bridge.replayViewModuleState();
|
||||
QCOMPARE(spy.count(), 0);
|
||||
}
|
||||
|
||||
void replayViewModuleState_afterCrash_staysSilent()
|
||||
{
|
||||
LogosQmlBridge bridge(nullptr);
|
||||
bridge.setViewModuleSocket("foo", "sock-1");
|
||||
bridge.notifyViewModuleCrashed("foo");
|
||||
|
||||
QSignalSpy spy(&bridge, &LogosQmlBridge::viewModuleReadyChanged);
|
||||
bridge.replayViewModuleState();
|
||||
QCOMPARE(spy.count(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestLogosQmlBridge)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Readiness replay against a REAL Valid replica.
|
||||
//
|
||||
// replayViewModuleState() exists for QML hot reload: the host rebuilds the view
|
||||
// on a fresh QQmlEngine while the backend process keeps running, so the new
|
||||
// object tree's Connections are created after viewModuleReadyChanged already
|
||||
// fired and the cached replica — Valid, and staying Valid — never transitions
|
||||
// again to give them a second chance. The view then waits forever for readiness
|
||||
// it missed.
|
||||
//
|
||||
// The unit suite can only reach the negative half of that (empty map, dropped
|
||||
// map), where the replay loop never meets a Valid replica and so cannot catch a
|
||||
// regression in the branch that matters. This one publishes a real source over
|
||||
// QtRO, acquires through the bridge's own factory-plugin path, waits for the
|
||||
// replica to actually reach Valid, and then pins what replay emits.
|
||||
|
||||
#include "LogosQmlBridge.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QRemoteObjectHost>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QUrl>
|
||||
|
||||
namespace {
|
||||
|
||||
// The remoted source. A property is not incidental: a source with no members
|
||||
// still publishes, but giving the replica something to receive keeps this
|
||||
// close to a real view module's backend.
|
||||
class TestViewSource : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int counter READ counter WRITE setCounter NOTIFY counterChanged)
|
||||
|
||||
public:
|
||||
int counter() const { return m_counter; }
|
||||
void setCounter(int value)
|
||||
{
|
||||
if (m_counter == value) return;
|
||||
m_counter = value;
|
||||
emit counterChanged(m_counter);
|
||||
}
|
||||
|
||||
signals:
|
||||
void counterChanged(int value);
|
||||
|
||||
private:
|
||||
int m_counter = 0;
|
||||
};
|
||||
|
||||
constexpr int kReadyTimeoutMs = 10000;
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestLogosQmlBridgeReplay : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void replayViewModuleState_validReplica_reemitsReady()
|
||||
{
|
||||
// Socket name carries the pid so a leftover socket from a crashed run
|
||||
// cannot make this test bind to — or acquire from — the wrong host.
|
||||
const QString socket =
|
||||
QStringLiteral("lvmr-replay-%1").arg(QCoreApplication::applicationPid());
|
||||
|
||||
TestViewSource source;
|
||||
QRemoteObjectHost host(QUrl(QStringLiteral("local:") + socket));
|
||||
QVERIFY(host.enableRemoting(&source, QStringLiteral("test_view")));
|
||||
|
||||
LogosQmlBridge bridge(nullptr);
|
||||
bridge.setViewModuleSocket(QStringLiteral("test_view"), socket);
|
||||
bridge.setViewReplicaPlugin(QStringLiteral("test_view"),
|
||||
QStringLiteral(TEST_REPLICA_FACTORY_PLUGIN));
|
||||
|
||||
QSignalSpy readySpy(&bridge, &LogosQmlBridge::viewModuleReadyChanged);
|
||||
|
||||
QObject* replica = bridge.module(QStringLiteral("test_view"));
|
||||
QVERIFY(replica != nullptr);
|
||||
|
||||
// Acquisition is asynchronous: the replica is Default until the source
|
||||
// meta arrives over the socket.
|
||||
QVERIFY(readySpy.wait(kReadyTimeoutMs));
|
||||
QVERIFY(bridge.isViewModuleReady(QStringLiteral("test_view")));
|
||||
|
||||
// ── The reload. A fresh engine asks for the same module and gets the
|
||||
// cached replica back; on its own that is silent, which is the bug.
|
||||
readySpy.clear();
|
||||
QCOMPARE(bridge.module(QStringLiteral("test_view")), replica);
|
||||
QCOMPARE(readySpy.count(), 0);
|
||||
|
||||
// ── What the host calls once the new object tree is complete.
|
||||
bridge.replayViewModuleState();
|
||||
|
||||
QCOMPARE(readySpy.count(), 1);
|
||||
QCOMPARE(readySpy.first().at(0).toString(), QStringLiteral("test_view"));
|
||||
QCOMPARE(readySpy.first().at(1).toBool(), true);
|
||||
|
||||
// Replay is a report of current state, not a one-shot: a view reloaded
|
||||
// twice must be told twice.
|
||||
readySpy.clear();
|
||||
bridge.replayViewModuleState();
|
||||
QCOMPARE(readySpy.count(), 1);
|
||||
QCOMPARE(readySpy.first().at(1).toBool(), true);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestLogosQmlBridgeReplay)
|
||||
#include "test_logos_qml_bridge_replay.moc"
|
||||
@@ -0,0 +1,37 @@
|
||||
// A minimal LogosViewReplicaFactory plugin for the replay test.
|
||||
//
|
||||
// The bridge only ever populates m_replicas through module(), which loads a
|
||||
// factory plugin via QPluginLoader and asks it for a replica — so a test that
|
||||
// needs a genuinely Valid replica in that map has to come in the same way a
|
||||
// real view module does. This stands in for the repc-generated factory a view
|
||||
// module ships: it hands back a dynamic replica, which reaches
|
||||
// QRemoteObjectReplica::Valid against a published source exactly like a typed
|
||||
// one does. Only the metaobject differs, and the replay loop does not look at
|
||||
// it — it qobject_casts to QRemoteObjectReplica and reads state().
|
||||
|
||||
#include "LogosViewReplicaFactory.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QRemoteObjectDynamicReplica>
|
||||
#include <QRemoteObjectNode>
|
||||
#include <QtPlugin>
|
||||
|
||||
class TestReplicaFactory : public QObject, public LogosViewReplicaFactory {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID LogosViewReplicaFactory_iid)
|
||||
Q_INTERFACES(LogosViewReplicaFactory)
|
||||
|
||||
public:
|
||||
QObject* acquire(QRemoteObjectNode* node) override
|
||||
{
|
||||
if (!node) return nullptr;
|
||||
return node->acquireDynamic(QStringLiteral("test_view"));
|
||||
}
|
||||
|
||||
const QMetaObject* replicaMetaObject() const override
|
||||
{
|
||||
return &QRemoteObjectDynamicReplica::staticMetaObject;
|
||||
}
|
||||
};
|
||||
|
||||
#include "test_replica_factory_plugin.moc"
|
||||
Reference in New Issue
Block a user