diff options
author | Hubert Chathi <uhoreg@debian.org> | 2019-06-25 16:33:24 -0400 |
---|---|---|
committer | Hubert Chathi <uhoreg@debian.org> | 2019-06-25 16:33:24 -0400 |
commit | 72d5660efd0755bb53a8699cd39865155400d288 (patch) | |
tree | ed7e7537e6a3eb7e8b92226c4015f9bfc8e11c5a /examples | |
parent | 52407a933bfe1fcc5f3aa1dccaa0b9a8279aa634 (diff) | |
parent | 681203f951d13e9e8eaf772435cac28c6d74cd42 (diff) | |
download | libquotient-72d5660efd0755bb53a8699cd39865155400d288.tar.gz libquotient-72d5660efd0755bb53a8699cd39865155400d288.zip |
Merge branch 'upstream' (v0.5.2)
Diffstat (limited to 'examples')
-rw-r--r-- | examples/CMakeLists.txt | 2 | ||||
-rw-r--r-- | examples/qmc-example.cpp | 480 |
2 files changed, 327 insertions, 155 deletions
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 49e0089a..cd5e15ed 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -45,7 +45,7 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu endif () endforeach () -find_package(Qt5 5.6 REQUIRED Network Gui) +find_package(Qt5 5.6 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) find_package(QMatrixClient REQUIRED) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 206501a5..bd9190b9 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -10,6 +10,8 @@ #include <QtCore/QCoreApplication> #include <QtCore/QStringBuilder> #include <QtCore/QTimer> +#include <QtCore/QTemporaryFile> +#include <QtCore/QFileInfo> #include <iostream> #include <functional> @@ -21,22 +23,26 @@ using namespace std::placeholders; class QMCTest : public QObject { public: - QMCTest(Connection* conn, const QString& testRoomName, QString source); + QMCTest(Connection* conn, QString testRoomName, QString source); private slots: - void setup(const QString& testRoomName); + void setupAndRun(); void onNewRoom(Room* r); - void startTests(); + void run(); + void doTests(); + void loadMembers(); void sendMessage(); + void sendFile(); + void checkFileSendingOutcome(const QString& txnId, + const QString& fileName); void setTopic(); void addAndRemoveTag(); void sendAndRedact(); - void checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events); + bool checkRedactionOutcome(const QString& evtIdToRedact); void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); - void leave(); + void conclude(); void finalize(); private: @@ -45,80 +51,79 @@ class QMCTest : public QObject QStringList succeeded; QStringList failed; QString origin; + QString targetRoomName; Room* targetRoom = nullptr; + + bool validatePendingEvent(const QString& txnId); }; #define QMC_CHECK(description, condition) \ { \ - const bool result = !!(condition); \ Q_ASSERT(running.removeOne(description)); \ - (result ? succeeded : failed).push_back(description); \ - cout << (description) << (result ? " successul" : " FAILED") << endl; \ - if (targetRoom) \ - targetRoom->postMessage(origin % ": " % QStringLiteral(description) % \ - (result ? QStringLiteral(" successful") : QStringLiteral(" FAILED")), \ - result ? MessageEventType::Notice : MessageEventType::Text); \ + if (!!(condition)) \ + { \ + succeeded.push_back(description); \ + cout << (description) << " successful" << endl; \ + if (targetRoom) \ + targetRoom->postMessage( \ + origin % ": " % (description) % " successful", \ + MessageEventType::Notice); \ + } else { \ + failed.push_back(description); \ + cout << (description) << " FAILED" << endl; \ + if (targetRoom) \ + targetRoom->postPlainText( \ + origin % ": " % (description) % " FAILED"); \ + } \ +} + +bool QMCTest::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() && + it->deliveryStatus() == EventStatus::Submitted && + (*it)->transactionId() == txnId; } -QMCTest::QMCTest(Connection* conn, const QString& testRoomName, QString source) - : c(conn), origin(std::move(source)) +QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) + : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) { if (!origin.isEmpty()) cout << "Origin for the test message: " << origin.toStdString() << endl; - if (!testRoomName.isEmpty()) - cout << "Test room name: " << testRoomName.toStdString() << endl; + if (!targetRoomName.isEmpty()) + cout << "Test room name: " << targetRoomName.toStdString() << endl; - connect(c.data(), &Connection::connected, - this, std::bind(&QMCTest::setup, this, testRoomName)); + connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog - QTimer::singleShot(180000, this, &QMCTest::leave); + QTimer::singleShot(180000, this, &QMCTest::conclude); } -void QMCTest::setup(const QString& testRoomName) +void QMCTest::setupAndRun() { + Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); + Q_ASSERT(c->domain() == c->userId().section(':', 1)); cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; - // Setting up sync loop - c->sync(); - connect(c.data(), &Connection::syncDone, c.data(), [this,testRoomName] { - cout << "Sync complete, " - << running.size() << " tests in the air" << endl; - if (!running.isEmpty()) - c->sync(10000); - else if (targetRoom) - { - targetRoom->postPlainText(origin % ": All tests finished"); - connect(targetRoom, &Room::pendingEventMerged, this, &QMCTest::leave); - } - else - finalize(); - }); - - // Join a testroom, if provided - if (!targetRoom && !testRoomName.isEmpty()) + if (!targetRoomName.isEmpty()) { - cout << "Joining " << testRoomName.toStdString() << endl; + cout << "Joining " << targetRoomName.toStdString() << endl; running.push_back("Join room"); - auto joinJob = c->joinRoom(testRoomName); + auto joinJob = c->joinRoom(targetRoomName); connect(joinJob, &BaseJob::failure, this, - [this] { QMC_CHECK("Join room", false); finalize(); }); - // As of BaseJob::success, a Room object is not guaranteed to even - // exist; it's a mere confirmation that the server processed - // the request. - connect(c.data(), &Connection::loadedRoomState, this, - [this,testRoomName] (Room* room) { - Q_ASSERT(room); // It's a grave failure if room is nullptr here - if (room->canonicalAlias() != testRoomName) - return; // Not our room - - targetRoom = room; - QMC_CHECK("Join room", true); - startTests(); - }); - } + [this] { QMC_CHECK("Join room", false); conclude(); }); + // Connection::joinRoom() creates a Room object upon JoinRoomJob::success + // but this object is empty until the first sync is done. + connect(joinJob, &BaseJob::success, this, [this,joinJob] { + targetRoom = c->room(joinJob->roomId(), JoinState::Join); + QMC_CHECK("Join room", targetRoom != nullptr); + + run(); + }); + } else + run(); } void QMCTest::onNewRoom(Room* r) @@ -141,14 +146,64 @@ void QMCTest::onNewRoom(Room* r) }); } -void QMCTest::startTests() +void QMCTest::run() +{ + c->setLazyLoading(true); + c->syncLoop(); + connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); + connect(c.data(), &Connection::syncDone, c.data(), [this] { + cout << "Sync complete, " << running.size() << " test(s) in the air: " + << running.join(", ").toStdString() << endl; + if (running.isEmpty()) + conclude(); + }); +} + +void QMCTest::doTests() { cout << "Starting tests" << endl; + + loadMembers(); + // Add here tests not requiring the test room + if (targetRoomName.isEmpty()) + return; + sendMessage(); + sendFile(); setTopic(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); + // Add here tests with the test room +} + +void QMCTest::loadMembers() +{ + running.push_back("Loading members"); + // The dedicated qmc-test room is too small to test + // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead. + // TODO: #264 + auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org")); + if (!r) + { + cout << "#test:matrix.org is not found in the test user's rooms" << endl; + QMC_CHECK("Loading members", false); + return; + } + // It's not exactly correct because an arbitrary server might not support + // lazy loading; but in the absence of capabilities framework we assume + // it does. + if (r->memberNames().size() >= r->joinedCount()) + { + cout << "Lazy loading doesn't seem to be enabled" << endl; + QMC_CHECK("Loading members", false); + return; + } + r->setDisplayed(); + connect(r, &Room::allMembersLoaded, [this,r] { + QMC_CHECK("Loading members", + r->memberNames().size() >= r->joinedCount()); + }); } void QMCTest::sendMessage() @@ -156,25 +211,134 @@ void QMCTest::sendMessage() running.push_back("Message sending"); cout << "Sending a message" << endl; auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); - auto& pending = targetRoom->pendingEvents(); - if (pending.empty()) + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("Message sending", false); return; } - auto it = std::find_if(pending.begin(), pending.end(), - [&txnId] (const auto& e) { - return e->transactionId() == txnId; + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + QMC_CHECK("Message sending", + is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() && + pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + return true; + }); +} + +void QMCTest::sendFile() +{ + running.push_back("File sending"); + cout << "Sending a file" << endl; + auto* tf = new QTemporaryFile; + if (!tf->open()) + { + cout << "Failed to create a temporary file" << endl; + QMC_CHECK("File sending", false); + return; + } + tf->write("Test"); + tf->close(); + // QFileInfo::fileName brings only the file name; QFile::fileName brings + // the full path + const auto tfName = QFileInfo(*tf).fileName(); + cout << "Sending file" << tfName.toStdString() << endl; + const auto txnId = targetRoom->postFile("Test file", + QUrl::fromLocalFile(tf->fileName())); + if (!validatePendingEvent(txnId)) + { + cout << "Invalid pending event right after submitting" << endl; + QMC_CHECK("File sending", false); + delete tf; + return; + } + + // FIXME: Clean away connections (connectUntil doesn't help here). + connect(targetRoom, &Room::fileTransferCompleted, this, + [this,txnId,tf,tfName] (const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return; + + delete tf; + + checkFileSendingOutcome(txnId, tfName); + }); + connect(targetRoom, &Room::fileTransferFailed, this, + [this,txnId,tf] + (const QString& id, const QString& error) { + if (id != txnId) + return; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + delete tf; + + QMC_CHECK("File sending", false); + }); +} + +void QMCTest::checkFileSendingOutcome(const QString& txnId, + const QString& fileName) +{ + auto it = targetRoom->findPendingEvent(txnId); + if (it == targetRoom->pendingEvents().end()) + { + cout << "Pending file event dropped before upload completion" + << endl; + QMC_CHECK("File sending", false); + return; + } + if (it->deliveryStatus() != EventStatus::FileUploaded) + { + cout << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + QMC_CHECK("File sending", false); + return; + } + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,txnId,fileName] (const RoomEvent* evt, int pendingIdx) { + const auto& pendingEvents = targetRoom->pendingEvents(); + Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); + + if (evt->transactionId() != txnId) + return false; + + cout << "File event " << txnId.toStdString() + << " arrived in the timeline" << endl; + visit(*evt, + [&] (const RoomMessageEvent& e) { + QMC_CHECK("File sending", + !e.id().isEmpty() && + pendingEvents[size_t(pendingIdx)] + ->transactionId() == txnId && + e.hasFileContent() && + e.content()->fileInfo()->originalName == fileName); + }, + [this] (const RoomEvent&) { + QMC_CHECK("File sending", false); }); - QMC_CHECK("Message sending", it != pending.end()); - // TODO: Wait when it actually gets sent; check that it obtained an id - // Independently, check when it shows up in the timeline. + return true; + }); } void QMCTest::setTopic() { - running.push_back("State setting test"); - running.push_back("Fake state event immunity test"); + static const char* const stateTestName = "State setting test"; + static const char* const fakeStateTestName = "Fake state event immunity test"; + running.push_back(stateTestName); + running.push_back(fakeStateTestName); auto initialTopic = targetRoom->topic(); const auto newTopic = c->generateTxnId(); @@ -183,35 +347,30 @@ void QMCTest::setTopic() targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event RoomTopicEvent(fakeTopic).contentJson()); - { - auto* context = new QObject; - connect(targetRoom, &Room::topicChanged, context, - [this,newTopic,fakeTopic,initialTopic,context] { - if (targetRoom->topic() == newTopic) - { - QMC_CHECK("State setting test", true); - // Don't reset the topic yet if the negative test still runs - if (!running.contains("Fake state event immunity test")) - targetRoom->setTopic(initialTopic); - - context->deleteLater(); - } - }); - } - - { - auto* context = new QObject; - connect(targetRoom, &Room::pendingEventAboutToMerge, context, - [this,fakeTopic,initialTopic,context] (const RoomEvent* e, int) { - if (e->contentJson().value("topic").toString() != fakeTopic) - return; // Wait on for the right event - - QMC_CHECK("Fake state event immunity test", !e->isStateEvent()); - if (!running.contains("State setting test")) + connectUntil(targetRoom, &Room::topicChanged, this, + [this,newTopic,fakeTopic,initialTopic] { + if (targetRoom->topic() == newTopic) + { + QMC_CHECK(stateTestName, true); + // Don't reset the topic yet if the negative test still runs + if (!running.contains(fakeStateTestName)) targetRoom->setTopic(initialTopic); - context->deleteLater(); - }); - } + + return true; + } + return false; + }); + + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this,fakeTopic,initialTopic] (const RoomEvent* e, int) { + if (e->contentJson().value("topic").toString() != fakeTopic) + return false; // Wait on for the right event + + QMC_CHECK(fakeStateTestName, !e->isStateEvent()); + if (!running.contains(fakeStateTestName)) + targetRoom->setTopic(initialTopic); + return true; + }); } void QMCTest::addAndRemoveTag() @@ -232,7 +391,7 @@ void QMCTest::addAndRemoveTag() cout << "Test tag set, removing it now" << endl; targetRoom->removeTag(TestTag); QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag)); - QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); + disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); } }); cout << "Adding a tag" << endl; @@ -243,70 +402,53 @@ void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; - if (auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": message to redact"))) + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) { - connect(job, &BaseJob::success, targetRoom, [job,this] { + QMC_CHECK("Redaction", false); + return; + } + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (const QString& tId, const QString& evtId) { + if (tId != txnId) + return; + cout << "Redacting the message" << endl; - targetRoom->redactEvent(job->eventId(), origin); - // Make sure to save the event id because the job is about to end. - connect(targetRoom, &Room::aboutToAddNewMessages, this, - std::bind(&QMCTest::checkRedactionOutcome, - this, job->eventId(), _1)); + targetRoom->redactEvent(evtId, origin); + + connectUntil(targetRoom, &Room::addedMessages, this, + [this,evtId] { return checkRedactionOutcome(evtId); }); }); - } else - QMC_CHECK("Redaction", false); } -void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, - RoomEventsRange events) +bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact) { - static bool checkSucceeded = false; // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. - auto it = std::find_if(events.begin(), events.end(), - [=] (const RoomEventPtr& e) { - return e->id() == evtIdToRedact; - }); - if (it == events.end()) - return; // Waiting for the next sync + auto it = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->timelineEdge()) + return false; // Waiting for the next sync if ((*it)->isRedacted()) { - if (checkSucceeded) - { - const auto msg = - "The redacted event came in with the sync again, ignoring"; - cout << msg << endl; - targetRoom->postPlainText(msg); - return; - } cout << "The sync brought already redacted message" << endl; QMC_CHECK("Redaction", true); - // Not disconnecting because there are other connections from this class - // to aboutToAddNewMessages - checkSucceeded = true; - return; - } - // The event is not redacted - if (checkSucceeded) - { - const auto msg = - "Warning: the redacted event came non-redacted with the sync!"; - cout << msg << endl; - targetRoom->postPlainText(msg); + } else { + cout << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this,evtIdToRedact] + (const RoomEvent* newEvent, const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + + QMC_CHECK("Redaction", newEvent->isRedacted() && + newEvent->redactionReason() == origin); + return true; + }); } - cout << "Message came non-redacted with the sync, waiting for redaction" << endl; - connect(targetRoom, &Room::replacedEvent, targetRoom, - [=] (const RoomEvent* newEvent, const RoomEvent* oldEvent) { - QMC_CHECK("Redaction", oldEvent->id() == evtIdToRedact && - newEvent->isRedacted() && - newEvent->redactionReason() == origin); - checkSucceeded = true; - disconnect(targetRoom, &Room::replacedEvent, nullptr, nullptr); - }); - + return true; } void QMCTest::markDirectChat() @@ -347,13 +489,48 @@ void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); } -void QMCTest::leave() +void QMCTest::conclude() { + c->stopSync(); + auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; + if (!failed.isEmpty() || !running.isEmpty()) + succeededRec += " of " % + QString::number(succeeded.size() + failed.size() + running.size()) % + " total"; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": <strong><font data-mx-color='#" % color % + "' color='#" % color % "'>Testing complete</font></strong>, " % + succeededRec; + if (!failed.isEmpty()) + { + plainReport += "\nFAILED: " % failed.join(", "); + htmlReport += "<br><strong>Failed:</strong> " % failed.join(", "); + } + if (!running.isEmpty()) + { + plainReport += "\nDID NOT FINISH: " % running.join(", "); + htmlReport += + "<br><strong>Did not finish:</strong> " % running.join(", "); + } + cout << plainReport.toStdString() << endl; + if (targetRoom) { - cout << "Leaving the room" << endl; - connect(targetRoom->leaveRoom(), &BaseJob::finished, - this, &QMCTest::finalize); + // TODO: Waiting for proper futures to come so that it could be: +// targetRoom->postHtmlText(...) +// .then(this, &QMCTest::finalize); // Qt-style or +// .then([this] { finalize(); }); // STL-style + auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); + connect(targetRoom, &Room::messageSent, this, + [this,txnId] (QString serverTxnId) { + if (txnId != serverTxnId) + return; + + cout << "Leaving the room" << endl; + connect(targetRoom->leaveRoom(), &BaseJob::finished, + this, &QMCTest::finalize); + }); } else finalize(); @@ -365,11 +542,6 @@ void QMCTest::finalize() c->logout(); connect(c.data(), &Connection::loggedOut, qApp, [this] { - if (!failed.isEmpty()) - cout << "FAILED: " << failed.join(", ").toStdString() << endl; - if (!running.isEmpty()) - cout << "DID NOT FINISH: " - << running.join(", ").toStdString() << endl; QCoreApplication::processEvents(); QCoreApplication::exit(failed.size() + running.size()); }); |