From d59ca7ac194e8d57177afb1ac89603e22b61b4ec Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Oct 2019 08:16:29 +0900 Subject: qmc-example -> quotest, QMCTest -> TestManager Also: some bits of refactoring in the test code to make it more extensible. Closes #352. --- tests/quotest.cpp | 577 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 tests/quotest.cpp (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp new file mode 100644 index 00000000..0a25dbc1 --- /dev/null +++ b/tests/quotest.cpp @@ -0,0 +1,577 @@ + +#include "connection.h" +#include "room.h" +#include "user.h" + +#include "csapi/joining.h" +#include "csapi/leaving.h" +#include "csapi/room_send.h" + +#include "events/reactionevent.h" +#include "events/simplestateevents.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace Quotient; +using std::cout, std::endl; + +class TestManager : public QObject { +public: + TestManager(Connection* conn, QString testRoomName, QString source); + +private slots: + // clang-format off + void setupAndRun(); + void onNewRoom(Room* r); + void run(); + void doTests(); + void loadMembers(); + void sendMessage(); + void sendReaction(const QString& targetEvtId); + void sendFile(); + void checkFileSendingOutcome(const QString& txnId, + const QString& fileName); + void setTopic(); + void sendAndRedact(); + bool checkRedactionOutcome(const QString& evtIdToRedact); + void addAndRemoveTag(); + void markDirectChat(); + void checkDirectChatOutcome( + const Connection::DirectChatsMap& added); + void conclude(); + void finalize(); + // clang-format on + +private: + QScopedPointer c; + QStringList running; + QStringList succeeded; + QStringList failed; + QString origin; + QString targetRoomName; + Room* targetRoom = nullptr; + +using TestToken = QByteArray; // return value of QMetaMethod::name +// For now, the token itself is the test name but that may change. +const char* testName(const TestToken& token) { return token.constData(); } + bool validatePendingEvent(const QString& txnId); + void finishTest(const TestToken& token, bool condition, const char* file, + int line);}; + +#define TEST_IMPL(Name) void TestManager::Name() + +#define FINISH_TEST(description, Condition) \ + finishTest(description, Condition, __FILE__, __LINE__) + +#define FAIL_TEST(description) FINISH_TEST(description, false) + +bool TestManager::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() + && it->deliveryStatus() == EventStatus::Submitted + && (*it)->transactionId() == txnId; +} + +void TestManager::finishTest(const TestToken& token, bool condition, + const char* file, int line) +{ + const auto& item = testName(token); + Q_ASSERT_X(running.contains(item), item, + "Trying to finish an item that's not running"); + running.removeOne(item); + if (condition) { + succeeded.push_back(item); + cout << item << " successful" << endl; + if (targetRoom) + targetRoom->postMessage(origin % ": " % item % " successful", + MessageEventType::Notice); + } else { + failed.push_back(item); + cout << item << " FAILED at " << file << ":" << line << endl; + if (targetRoom) + targetRoom->postPlainText(origin % ": " % item % " FAILED at " + % file % ", line " % QString::number(line)); + } +} + +TestManager::TestManager(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; + cout << "Test room name: " << targetRoomName.toStdString() << endl; + + connect(c.data(), &Connection::connected, this, &TestManager::setupAndRun); + connect(c.data(), &Connection::loadedRoomState, this, &TestManager::onNewRoom); + // Big countdown watchdog + QTimer::singleShot(180000, this, &TestManager::conclude); +} + +void TestManager::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; + + cout << "Joining " << targetRoomName.toStdString() << endl; + running.push_back("Join room"); + auto joinJob = c->joinRoom(targetRoomName); + connect(joinJob, &BaseJob::failure, this, [this] { + FAIL_TEST("Join room"); + 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); + FINISH_TEST("Join room", targetRoom != nullptr); + + run(); + }); +} + +void TestManager::onNewRoom(Room* r) +{ + cout << "New room: " << r->id().toStdString() << endl + << " Name: " << r->name().toStdString() << endl + << " Canonical alias: " << r->canonicalAlias().toStdString() << endl + << endl; + connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { + cout << timeline.size() << " new event(s) in room " + << r->canonicalAlias().toStdString() << endl; + // for (const auto& item: timeline) + // { + // cout << "From: " + // << r->roomMembername(item->senderId()).toStdString() + // << endl << "Timestamp:" + // << item->timestamp().toString().toStdString() << endl + // << "JSON:" << endl << + // item->originalJson().toStdString() << endl; + // } + }); +} + +void TestManager::run() +{ + c->setLazyLoading(true); + c->syncLoop(); + connectSingleShot(c.data(), &Connection::syncDone, this, &TestManager::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 TestManager::doTests() +{ + cout << "Starting tests" << endl; + + loadMembers(); + + sendMessage(); + sendFile(); + setTopic(); + addAndRemoveTag(); + sendAndRedact(); + markDirectChat(); + // Add here tests with the test room +} + +TEST_IMPL(loadMembers) +{ + running.push_back("Loading members"); + auto* r = c->roomByAlias(QStringLiteral("#quotient:matrix.org"), + JoinState::Join); + if (!r) { + cout << "#quotient:matrix.org is not found in the test user's rooms" + << endl; + FAIL_TEST("Loading members"); + 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; + FAIL_TEST("Loading members"); + return; + } + r->setDisplayed(); + connect(r, &Room::allMembersLoaded, [this, r] { + FINISH_TEST("Loading members", + r->memberNames().size() >= r->joinedCount()); + }); +} + +TEST_IMPL(sendMessage) +{ + running.push_back("Message sending"); + cout << "Sending a message" << endl; + auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; + FAIL_TEST("Message sending"); + return; + } + 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; + + FINISH_TEST("Message sending", + is(*evt) && !evt->id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + sendReaction(evt->id()); + return true; + }); +} + +void TestManager::sendReaction(const QString& targetEvtId) +{ + running.push_back("Reaction sending"); + cout << "Reacting to the newest message in the room" << endl; + Q_ASSERT(targetRoom->timelineSize() > 0); + const auto key = QStringLiteral("+1"); + const auto txnId = targetRoom->postReaction(targetEvtId, key); + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; + FAIL_TEST("Reaction sending"); + return; + } + + // TODO: Check that it came back as a reaction event and that it attached to + // the right event + connectUntil( + targetRoom, &Room::updatedEvent, this, + [this, txnId, key, targetEvtId](const QString& actualTargetEvtId) { + if (actualTargetEvtId != targetEvtId) + return false; + const auto reactions = targetRoom->relatedEvents( + targetEvtId, EventRelation::Annotation()); + // It's a test room, assuming no interference there should + // be exactly one reaction + if (reactions.size() != 1) { + FAIL_TEST("Reaction sending"); + } else { + const auto* evt = + eventCast(reactions.back()); + FINISH_TEST("Reaction sending", + is(*evt) && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); + } + return true; + }); +} + +TEST_IMPL(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; + FAIL_TEST("File sending"); + 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; + delete tf; + FAIL_TEST("File sending"); + 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; + + FAIL_TEST("File sending"); + }); +} + +void TestManager::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; + FAIL_TEST("File sending"); + return; + } + if (it->deliveryStatus() != EventStatus::FileUploaded) { + cout << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + FAIL_TEST("File sending"); + 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) { + FINISH_TEST( + "File sending", + !e.id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == txnId + && e.hasFileContent() + && e.content()->fileInfo()->originalName == fileName); + }, + [this](const RoomEvent&) { FAIL_TEST("File sending"); }); + return true; + }); +} + +TEST_IMPL(setTopic) +{ + running.push_back("State setting test"); + + const auto newTopic = c->generateTxnId(); // Just a way to get a unique id + targetRoom->setTopic(newTopic); + + connectUntil(targetRoom, &Room::topicChanged, this, + [this, newTopic] { + FINISH_TEST("State setting test", + targetRoom->topic() == newTopic); + return true; + }); +} + +TEST_IMPL(sendAndRedact) +{ + running.push_back("Redaction"); + cout << "Sending a message to redact" << endl; + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) { + FAIL_TEST("Redaction"); + 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(evtId, origin); + + connectUntil(targetRoom, &Room::addedMessages, this, + [this, evtId] { + return checkRedactionOutcome(evtId); + }); + }); +} + +bool TestManager::checkRedactionOutcome(const QString& evtIdToRedact) +{ + // 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 = targetRoom->findInTimeline(evtIdToRedact); + if (it == targetRoom->timelineEdge()) + return false; // Waiting for the next sync + + if ((*it)->isRedacted()) { + cout << "The sync brought already redacted message" << endl; + FINISH_TEST("Redaction", true); + } 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; + + FINISH_TEST("Redaction", + newEvent->isRedacted() + && newEvent->redactionReason() == origin); + return true; + }); + } + return true; +} + +TEST_IMPL(addAndRemoveTag) +{ + running.push_back("Tagging test"); + static const auto TestTag = QStringLiteral("org.quotient.test"); + // Pre-requisite + if (targetRoom->tags().contains(TestTag)) + targetRoom->removeTag(TestTag); + + // Connect first because the signal is emitted synchronously. + connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { + cout << "Room " << targetRoom->id().toStdString() + << ", tag(s) changed:" << endl + << " " << targetRoom->tagNames().join(", ").toStdString() << endl; + if (targetRoom->tags().contains(TestTag)) { + cout << "Test tag set, removing it now" << endl; + targetRoom->removeTag(TestTag); + FINISH_TEST("Tagging test", !targetRoom->tags().contains(TestTag)); + disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); + } + }); + cout << "Adding a tag" << endl; + targetRoom->addTag(TestTag); +} + +TEST_IMPL(markDirectChat) +{ + running.push_back("Direct chat test"); + if (targetRoom->directChatUsers().contains(c->user())) { + cout << "Warning: the room is already a direct chat," + " only unmarking will be tested" + << endl; + checkDirectChatOutcome({ { c->user(), targetRoom->id() } }); + return; + } + // Connect first because the signal is emitted synchronously. + connect(c.data(), &Connection::directChatsListChanged, this, + &TestManager::checkDirectChatOutcome); + cout << "Marking the room as a direct chat" << endl; + c->addToDirectChats(targetRoom, c->user()); +} + +void TestManager::checkDirectChatOutcome(const Connection::DirectChatsMap& added) +{ + disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); + if (!targetRoom->isDirectChat()) { + cout << "The room has not been marked as a direct chat" << endl; + FAIL_TEST("Direct chat test"); + return; + } + if (!added.contains(c->user(), targetRoom->id())) { + cout << "The room has not been listed in new direct chats" << endl; + FAIL_TEST("Direct chat test"); + return; + } + + cout << "Unmarking the direct chat" << endl; + c->removeFromDirectChats(targetRoom->id(), c->user()); + FINISH_TEST("Direct chat test", !c->isDirectChat(targetRoom->id())); +} + +void TestManager::conclude() +{ + c->stopSync(); + auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; + if (!failed.empty() || !running.empty()) + succeededRec += + " of " + % QString::number(succeeded.size() + failed.size() + running.size()) + % " total"; + QString plainReport = origin % ": Testing complete, " % succeededRec; + QString color = failed.empty() && running.empty() ? "00AA00" : "AA0000"; + QString htmlReport = origin % ": Testing complete, " % succeededRec; + if (!failed.empty()) { + plainReport += "\nFAILED: " % failed.join(", "); + htmlReport += "
Failed: " % failed.join(", "); + } + if (!running.empty()) { + plainReport += "\nDID NOT FINISH: " % running.join(", "); + htmlReport += "
Did not finish: " + % running.join(", "); + } + cout << plainReport.toStdString() << endl; + + if (targetRoom) { + // TODO: Waiting for proper futures to come so that it could be: + // targetRoom->postHtmlText(...) + // .then(this, &TestManager::finalize); // Qt-style or + // .then([this] { finalize(); }); // STL-style + auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); + connect(targetRoom, &Room::messageSent, this, + [this, txnId](const QString& serverTxnId) { + if (txnId != serverTxnId) + return; + + cout << "Leaving the room" << endl; + connect(targetRoom->leaveRoom(), &BaseJob::finished, this, + &TestManager::finalize); + }); + } else + finalize(); +} + +void TestManager::finalize() +{ + cout << "Logging out" << endl; + c->logout(); + connect(c.data(), &Connection::loggedOut, qApp, [this] { + QCoreApplication::processEvents(); + QCoreApplication::exit(failed.size() + running.size()); + }); +} + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + if (argc < 5) { + cout << "Usage: quotest [origin]" + << endl; + return -1; + } + + cout << "Connecting to the server as " << argv[1] << endl; + auto conn = new Connection; + conn->connectToServer(argv[1], argv[2], argv[3]); + TestManager test { conn, argv[4], argc >= 6 ? argv[5] : nullptr }; + return app.exec(); +} -- cgit v1.2.3 From ff78cd3370eccf593be789677dd8c4e2cdcea173 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 18 Oct 2019 09:03:06 +0900 Subject: TestManager::setTopic: add debug logging --- tests/quotest.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 0a25dbc1..f3a09e41 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -384,12 +384,16 @@ TEST_IMPL(setTopic) const auto newTopic = c->generateTxnId(); // Just a way to get a unique id targetRoom->setTopic(newTopic); - connectUntil(targetRoom, &Room::topicChanged, this, - [this, newTopic] { - FINISH_TEST("State setting test", - targetRoom->topic() == newTopic); - return true; - }); + connectUntil(targetRoom, &Room::topicChanged, this, [this, newTopic] { + if (targetRoom->topic() == newTopic) + FINISH_TEST("State setting test", true); + else { + cout << "Requested topic was " << newTopic.toStdString() << ", " + << targetRoom->topic().toStdString() << " arrived instead" + << endl; + } + return true; + }); } TEST_IMPL(sendAndRedact) -- cgit v1.2.3 From 3e153666542e903da420e405ca9f2f210b49475f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 20 Oct 2019 19:15:56 +0900 Subject: Quotest: factor out tests into TestSuite; rework sync tests Synchronous tests now use QSignalSpy instead of connecting to the signal just before its emission. --- tests/quotest.cpp | 570 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 327 insertions(+), 243 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp index f3a09e41..91f4f636 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -8,8 +8,10 @@ #include "csapi/room_send.h" #include "events/reactionevent.h" +#include "events/redactionevent.h" #include "events/simplestateevents.h" +#include #include #include #include @@ -22,57 +24,118 @@ using namespace Quotient; using std::cout, std::endl; +class TestSuite; + class TestManager : public QObject { public: TestManager(Connection* conn, QString testRoomName, QString source); -private slots: - // clang-format off +private: void setupAndRun(); void onNewRoom(Room* r); - void run(); void doTests(); - void loadMembers(); - void sendMessage(); - void sendReaction(const QString& targetEvtId); - void sendFile(); - void checkFileSendingOutcome(const QString& txnId, - const QString& fileName); - void setTopic(); - void sendAndRedact(); - bool checkRedactionOutcome(const QString& evtIdToRedact); - void addAndRemoveTag(); - void markDirectChat(); - void checkDirectChatOutcome( - const Connection::DirectChatsMap& added); void conclude(); void finalize(); - // clang-format on private: QScopedPointer c; - QStringList running; - QStringList succeeded; - QStringList failed; QString origin; QString targetRoomName; - Room* targetRoom = nullptr; + TestSuite* testSuite = nullptr; + QByteArrayList running {}, succeeded {}, failed {}; +}; using TestToken = QByteArray; // return value of QMetaMethod::name // For now, the token itself is the test name but that may change. const char* testName(const TestToken& token) { return token.constData(); } + +/// Test function declaration +/*! + * \return true, if the test finished (successfully or unsuccessfully); + * false, if the test went async and will complete later + */ +#define TEST_DECL(Name) bool Name(const TestToken& thisTest); + +/// The holder for the actual tests +/*! + * This class takes inspiration from Qt Test in terms of tests invocation; + * TestManager instantiates it and runs all public slots (cf. private slots in + * Qt Test) one after another. An important diversion from Qt Test is that + * the tests are assumed to by asynchronous rather than synchronous; so it's + * perfectly normal to have a few tests running at the same time. To avoid + * context clashes a special parameter with the name thisTest is passed to + * each test. Each test must conclude (synchronously or asynchronously) with + * an invocation of FINISH_TEST() macro (or FAIL_TEST() macro that expands to + * FINISH_TEST) that expects thisTest variable to be reachable. If FINISH_TEST() + * is invoked twice with the same thisTest, the second call will cause assertion + * failure; if FINISH_TEST() is not invoked at all, the test will be killed + * by a watchdog after a timeout and marked in the final report as not finished. + */ +class TestSuite : public QObject { + Q_OBJECT +public: + TestSuite(Connection* conn, QString testRoomAlias, QString source, + TestManager* parent) + : QObject(parent) + , targetConn(conn) + , targetRoomAlias(std::move(testRoomAlias)) + , origin(std::move(source)) + { + Q_ASSERT(conn && parent); + Q_ASSERT(targetRoomAlias.startsWith('!') + || targetRoomAlias.startsWith('#')); + } + + TEST_DECL(joinRoom) + +signals: + void finishedItem(QByteArray /*name*/, bool /*condition*/); + +public slots: + TEST_DECL(loadMembers) + TEST_DECL(sendMessage) + TEST_DECL(sendReaction) + TEST_DECL(sendFile) + TEST_DECL(setTopic) + TEST_DECL(sendAndRedact) + TEST_DECL(addAndRemoveTag) + TEST_DECL(markDirectChat) + // Add more tests above here + +public: + Room* room() const { return targetRoom; } + Connection* connection() const { return targetConn; } + +private slots: + bool checkFileSendingOutcome(const TestToken& thisTest, + const QString& txnId, const QString& fileName); + bool checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact); + +private: bool validatePendingEvent(const QString& txnId); + bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, - int line);}; + int line); -#define TEST_IMPL(Name) void TestManager::Name() +private: + Connection* targetConn; + QString targetRoomAlias; + QString origin; + Room* targetRoom = nullptr; +}; -#define FINISH_TEST(description, Condition) \ - finishTest(description, Condition, __FILE__, __LINE__) +#define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) -#define FAIL_TEST(description) FINISH_TEST(description, false) +// Returning true (rather than a void) allows to reuse the convention with +// connectUntil() to break the QMetaObject::Connection upon finishing the test +// item. +#define FINISH_TEST(Condition) \ + return (finishTest(thisTest, Condition, __FILE__, __LINE__), true) -bool TestManager::validatePendingEvent(const QString& txnId) +#define FAIL_TEST() FINISH_TEST(false) + +bool TestSuite::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); return it != targetRoom->pendingEvents().end() @@ -80,26 +143,23 @@ bool TestManager::validatePendingEvent(const QString& txnId) && (*it)->transactionId() == txnId; } -void TestManager::finishTest(const TestToken& token, bool condition, +void TestSuite::finishTest(const TestToken& token, bool condition, const char* file, int line) { const auto& item = testName(token); - Q_ASSERT_X(running.contains(item), item, - "Trying to finish an item that's not running"); - running.removeOne(item); if (condition) { - succeeded.push_back(item); cout << item << " successful" << endl; if (targetRoom) targetRoom->postMessage(origin % ": " % item % " successful", MessageEventType::Notice); } else { - failed.push_back(item); cout << item << " FAILED at " << file << ":" << line << endl; if (targetRoom) targetRoom->postPlainText(origin % ": " % item % " FAILED at " % file % ", line " % QString::number(line)); } + + emit finishedItem(item, condition); } TestManager::TestManager(Connection* conn, QString testRoomName, QString source) @@ -123,21 +183,10 @@ void TestManager::setupAndRun() << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; - cout << "Joining " << targetRoomName.toStdString() << endl; - running.push_back("Join room"); - auto joinJob = c->joinRoom(targetRoomName); - connect(joinJob, &BaseJob::failure, this, [this] { - FAIL_TEST("Join room"); - 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); - FINISH_TEST("Join room", targetRoom != nullptr); - - run(); - }); + c->setLazyLoading(true); + c->syncLoop(); + connectSingleShot(c.data(), &Connection::syncDone, this, + &TestManager::doTests); } void TestManager::onNewRoom(Room* r) @@ -161,136 +210,167 @@ void TestManager::onNewRoom(Room* r) }); } -void TestManager::run() +void TestManager::doTests() { - c->setLazyLoading(true); - c->syncLoop(); - connectSingleShot(c.data(), &Connection::syncDone, this, &TestManager::doTests); + cout << "Starting tests" << endl; + Q_ASSERT(!targetRoomName.isEmpty()); + testSuite = new TestSuite(c.data(), targetRoomName, origin, this); + connect(testSuite, &TestSuite::finishedItem, this, + [this](const QByteArray& itemName, bool condition) { + if (auto i = running.indexOf(itemName); i != -1) + (condition ? succeeded : failed).push_back(running.takeAt(i)); + else + Q_ASSERT_X(false, itemName, + "Test item is not in running state"); + }); + + running.push_back("joinRoom"); + testSuite->joinRoom("joinRoom"); + connectSingleShot(testSuite, &TestSuite::finishedItem, this, + [this](const QByteArray&, bool condition) { + if (!condition) { + finalize(); + return; + } + const auto* metaObj = testSuite->metaObject(); + for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); + ++i) { + const auto metaMethod = metaObj->method(i); + if (metaMethod.access() != QMetaMethod::Public + || metaMethod.methodType() != QMetaMethod::Slot) + continue; + + // By now connectSingleShot() has already disconnected this + // slot so the tests below can emit finishedItem() without + // the risk of recursion. + cout << "Starting: " << metaMethod.name().constData() << endl; + running.push_back(metaMethod.name()); + metaMethod.invoke(testSuite, Qt::DirectConnection, + Q_ARG(QByteArray, metaMethod.name())); + } + }); 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()) + cout << "Sync complete, "; + if (running.empty()) { + cout << "all tests finished" << endl; conclude(); + return; + } + cout << running.size() << " test(s) in the air:"; + for (const auto& test: qAsConst(running)) + cout << " " << testName(test); + cout << endl; + if (auto* r = testSuite->room()) + cout << "Test room timeline size = " << r->timelineSize() + << ", pending size = " << r->pendingEvents().size() << endl; }); } -void TestManager::doTests() +TEST_IMPL(joinRoom) { - cout << "Starting tests" << endl; - - loadMembers(); - - sendMessage(); - sendFile(); - setTopic(); - addAndRemoveTag(); - sendAndRedact(); - markDirectChat(); - // Add here tests with the test room + cout << "Joining " << targetRoomAlias.toStdString() << endl; + auto joinJob = connection()->joinRoom(targetRoomAlias); + // Ensure, before this test is completed, that the room has been joined + // and filled with some events so that other tests could use that + connect(joinJob, &BaseJob::success, this, [this, joinJob, thisTest] { + targetRoom = connection()->room(joinJob->roomId()); + targetRoom->getPreviousContent(); + connectUntil(targetRoom, &Room::addedMessages, this, [this, thisTest] { + FINISH_TEST(targetRoom->memberJoinState(connection()->user()) + == JoinState::Join); + }); + }); + connect(joinJob, &BaseJob::failure, this, [this, thisTest] { FAIL_TEST(); }); + return false; } TEST_IMPL(loadMembers) { - running.push_back("Loading members"); - auto* r = c->roomByAlias(QStringLiteral("#quotient:matrix.org"), - JoinState::Join); + // Trying to load members from another (larger) room + auto* r = connection()->roomByAlias(QStringLiteral("#quotient:matrix.org"), + JoinState::Join); if (!r) { cout << "#quotient:matrix.org is not found in the test user's rooms" << endl; - FAIL_TEST("Loading members"); - return; + FAIL_TEST(); } // 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; - FAIL_TEST("Loading members"); - return; + FAIL_TEST(); } r->setDisplayed(); - connect(r, &Room::allMembersLoaded, [this, r] { - FINISH_TEST("Loading members", - r->memberNames().size() >= r->joinedCount()); + connect(r, &Room::allMembersLoaded, [this, thisTest, r] { + FINISH_TEST(r->memberNames().size() >= r->joinedCount()); }); + return false; } TEST_IMPL(sendMessage) { - running.push_back("Message sending"); - cout << "Sending a message" << endl; auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; - FAIL_TEST("Message sending"); - return; + FAIL_TEST(); } - connectUntil( - targetRoom, &Room::pendingEventAboutToMerge, this, - [this, txnId](const RoomEvent* evt, int pendingIdx) { + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, 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; - FINISH_TEST("Message sending", - is(*evt) && !evt->id().isEmpty() - && pendingEvents[size_t(pendingIdx)]->transactionId() - == evt->transactionId()); - sendReaction(evt->id()); - return true; + FINISH_TEST(is(*evt) && !evt->id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); }); + return false; } -void TestManager::sendReaction(const QString& targetEvtId) +TEST_IMPL(sendReaction) { - running.push_back("Reaction sending"); cout << "Reacting to the newest message in the room" << endl; Q_ASSERT(targetRoom->timelineSize() > 0); + const auto targetEvtId = targetRoom->messageEvents().back()->id(); const auto key = QStringLiteral("+1"); const auto txnId = targetRoom->postReaction(targetEvtId, key); if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; - FAIL_TEST("Reaction sending"); - return; + FAIL_TEST(); } // TODO: Check that it came back as a reaction event and that it attached to // the right event connectUntil( targetRoom, &Room::updatedEvent, this, - [this, txnId, key, targetEvtId](const QString& actualTargetEvtId) { + [this, thisTest, txnId, key, targetEvtId](const QString& actualTargetEvtId) { if (actualTargetEvtId != targetEvtId) return false; const auto reactions = targetRoom->relatedEvents( targetEvtId, EventRelation::Annotation()); // It's a test room, assuming no interference there should // be exactly one reaction - if (reactions.size() != 1) { - FAIL_TEST("Reaction sending"); - } else { - const auto* evt = - eventCast(reactions.back()); - FINISH_TEST("Reaction sending", - is(*evt) && !evt->id().isEmpty() - && evt->relation().key == key - && evt->transactionId() == txnId); - } - return true; + if (reactions.size() != 1) + FAIL_TEST(); + + const auto* evt = + eventCast(reactions.back()); + FINISH_TEST(is(*evt) && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); }); + return false; } TEST_IMPL(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; - FAIL_TEST("File sending"); - return; + FAIL_TEST(); } tf->write("Test"); tf->close(); @@ -303,56 +383,51 @@ TEST_IMPL(sendFile) if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; delete tf; - FAIL_TEST("File sending"); - return; + FAIL_TEST(); } // 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; + connectUntil(targetRoom, &Room::fileTransferCompleted, this, + [this, thisTest, txnId, tf, tfName](const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); - delete tf; - - checkFileSendingOutcome(txnId, tfName); - }); - connect(targetRoom, &Room::fileTransferFailed, this, - [this, txnId, tf](const QString& id, const QString& error) { - if (id != txnId) - return; + if (id != txnId) + return false; - targetRoom->postPlainText(origin % ": File upload failed: " - % error); - delete tf; + delete tf; + return checkFileSendingOutcome(thisTest, txnId, tfName); + }); + connectUntil(targetRoom, &Room::fileTransferFailed, this, + [this, thisTest, txnId, tf](const QString& id, const QString& error) { + if (id != txnId) + return false; - FAIL_TEST("File sending"); - }); + targetRoom->postPlainText(origin % ": File upload failed: " % error); + delete tf; + FAIL_TEST(); + }); + return false; } -void TestManager::checkFileSendingOutcome(const QString& txnId, - const QString& fileName) +bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, + 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; - FAIL_TEST("File sending"); - return; + FAIL_TEST(); } if (it->deliveryStatus() != EventStatus::FileUploaded) { cout << "Pending file event status upon upload completion is " << it->deliveryStatus() << " != FileUploaded(" << EventStatus::FileUploaded << ')' << endl; - FAIL_TEST("File sending"); - return; + FAIL_TEST(); } - connectUntil( - targetRoom, &Room::pendingEventAboutToMerge, this, - [this, txnId, fileName](const RoomEvent* evt, int pendingIdx) { + connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, + [this, thisTest, txnId, fileName](const RoomEvent* evt, int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); @@ -361,52 +436,49 @@ void TestManager::checkFileSendingOutcome(const QString& txnId, cout << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; - visit( + // This part tests visit() + return visit( *evt, [&](const RoomMessageEvent& e) { FINISH_TEST( - "File sending", !e.id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == txnId && e.hasFileContent() && e.content()->fileInfo()->originalName == fileName); }, - [this](const RoomEvent&) { FAIL_TEST("File sending"); }); - return true; + [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); }); + return true; } TEST_IMPL(setTopic) { - running.push_back("State setting test"); - - const auto newTopic = c->generateTxnId(); // Just a way to get a unique id + const auto newTopic = connection()->generateTxnId(); // Just a way to make + // a unique id targetRoom->setTopic(newTopic); + connectUntil(targetRoom, &Room::topicChanged, this, + [this, thisTest, newTopic] { + if (targetRoom->topic() == newTopic) + FINISH_TEST(true); - connectUntil(targetRoom, &Room::topicChanged, this, [this, newTopic] { - if (targetRoom->topic() == newTopic) - FINISH_TEST("State setting test", true); - else { cout << "Requested topic was " << newTopic.toStdString() << ", " << targetRoom->topic().toStdString() << " arrived instead" << endl; - } - return true; - }); + return false; + }); + return false; } TEST_IMPL(sendAndRedact) { - running.push_back("Redaction"); cout << "Sending a message to redact" << endl; auto txnId = targetRoom->postPlainText(origin % ": message to redact"); - if (txnId.isEmpty()) { - FAIL_TEST("Redaction"); - return; - } + if (txnId.isEmpty()) + FAIL_TEST(); + connect(targetRoom, &Room::messageSent, this, - [this, txnId](const QString& tId, const QString& evtId) { + [this, thisTest, txnId](const QString& tId, const QString& evtId) { if (tId != txnId) return; @@ -414,13 +486,15 @@ TEST_IMPL(sendAndRedact) targetRoom->redactEvent(evtId, origin); connectUntil(targetRoom, &Room::addedMessages, this, - [this, evtId] { - return checkRedactionOutcome(evtId); + [this, thisTest, evtId] { + return checkRedactionOutcome(thisTest, evtId); }); }); + return false; } -bool TestManager::checkRedactionOutcome(const QString& evtIdToRedact) +bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, + const QString& evtIdToRedact) { // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with @@ -431,83 +505,85 @@ bool TestManager::checkRedactionOutcome(const QString& evtIdToRedact) if ((*it)->isRedacted()) { cout << "The sync brought already redacted message" << endl; - FINISH_TEST("Redaction", true); - } 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; - - FINISH_TEST("Redaction", - newEvent->isRedacted() - && newEvent->redactionReason() == origin); - return true; - }); + FINISH_TEST(true); } + + cout << "Message came non-redacted with the sync, waiting for redaction" + << endl; + connectUntil(targetRoom, &Room::replacedEvent, this, + [this, thisTest, evtIdToRedact](const RoomEvent* newEvent, + const RoomEvent* oldEvent) { + if (oldEvent->id() != evtIdToRedact) + return false; + + FINISH_TEST(newEvent->isRedacted() + && newEvent->redactionReason() == origin); + }); return true; } TEST_IMPL(addAndRemoveTag) { - running.push_back("Tagging test"); - static const auto TestTag = QStringLiteral("org.quotient.test"); + static const auto TestTag = QStringLiteral("im.quotient.test"); // Pre-requisite if (targetRoom->tags().contains(TestTag)) targetRoom->removeTag(TestTag); - // Connect first because the signal is emitted synchronously. - connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { - cout << "Room " << targetRoom->id().toStdString() - << ", tag(s) changed:" << endl - << " " << targetRoom->tagNames().join(", ").toStdString() << endl; - if (targetRoom->tags().contains(TestTag)) { - cout << "Test tag set, removing it now" << endl; - targetRoom->removeTag(TestTag); - FINISH_TEST("Tagging test", !targetRoom->tags().contains(TestTag)); - disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); - } - }); - cout << "Adding a tag" << endl; + // Unlike for most of Quotient, tags are applied and tagsChanged is emitted + // synchronously, with the server being notified async. The test checks + // that the signal is emitted, not only that tags have changed; but there's + // (currently) no way to check that the server has been correctly notified + // of the tag change. + QSignalSpy spy(targetRoom, &Room::tagsChanged); targetRoom->addTag(TestTag); + if (spy.count() != 1 || !targetRoom->tags().contains(TestTag)) { + cout << "Tag adding failed" << endl; + FAIL_TEST(); + } + cout << "Test tag set, removing it now" << endl; + targetRoom->removeTag(TestTag); + FINISH_TEST(spy.count() == 2 && !targetRoom->tags().contains(TestTag)); } -TEST_IMPL(markDirectChat) +bool TestSuite::checkDirectChat() const { - running.push_back("Direct chat test"); - if (targetRoom->directChatUsers().contains(c->user())) { - cout << "Warning: the room is already a direct chat," - " only unmarking will be tested" - << endl; - checkDirectChatOutcome({ { c->user(), targetRoom->id() } }); - return; - } - // Connect first because the signal is emitted synchronously. - connect(c.data(), &Connection::directChatsListChanged, this, - &TestManager::checkDirectChatOutcome); - cout << "Marking the room as a direct chat" << endl; - c->addToDirectChats(targetRoom, c->user()); + return targetRoom->directChatUsers().contains(connection()->user()); } -void TestManager::checkDirectChatOutcome(const Connection::DirectChatsMap& added) +TEST_IMPL(markDirectChat) { - disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); - if (!targetRoom->isDirectChat()) { - cout << "The room has not been marked as a direct chat" << endl; - FAIL_TEST("Direct chat test"); - return; - } - if (!added.contains(c->user(), targetRoom->id())) { - cout << "The room has not been listed in new direct chats" << endl; - FAIL_TEST("Direct chat test"); - return; + if (checkDirectChat()) + connection()->removeFromDirectChats(targetRoom->id(), + connection()->user()); + + int id = qRegisterMetaType(); // For QSignalSpy + Q_ASSERT(id != -1); + + // Same as with tags (and unusual for the rest of Quotient), direct chat + // operations are synchronous. + QSignalSpy spy(connection(), &Connection::directChatsListChanged); + cout << "Marking the room as a direct chat" << endl; + connection()->addToDirectChats(targetRoom, connection()->user()); + if (spy.count() != 1 || !checkDirectChat()) + FAIL_TEST(); + + // Check that the first argument (added DCs) actually contains the room + const auto& addedDCs = spy.back().front().value(); + if (addedDCs.size() != 1 + || !addedDCs.contains(connection()->user(), targetRoom->id())) { + cout << "The room is not in added direct chats" << endl; + FAIL_TEST(); } cout << "Unmarking the direct chat" << endl; - c->removeFromDirectChats(targetRoom->id(), c->user()); - FINISH_TEST("Direct chat test", !c->isDirectChat(targetRoom->id())); + connection()->removeFromDirectChats(targetRoom->id(), connection()->user()); + if (spy.count() != 2 && checkDirectChat()) + FAIL_TEST(); + + // Check that the second argument (removed DCs) actually contains the room + const auto& removedDCs = spy.back().back().value(); + FINISH_TEST(removedDCs.size() == 1 + && removedDCs.contains(connection()->user(), targetRoom->id())); } void TestManager::conclude() @@ -525,33 +601,39 @@ void TestManager::conclude() % "' color='#" % color % "'>Testing complete, " % succeededRec; if (!failed.empty()) { - plainReport += "\nFAILED: " % failed.join(", "); - htmlReport += "
Failed: " % failed.join(", "); + QByteArray failedList; + for (const auto& f : failed) + failedList += ' ' + f; + plainReport += "\nFAILED:" + failedList; + htmlReport += "
Failed:" + failedList; } if (!running.empty()) { - plainReport += "\nDID NOT FINISH: " % running.join(", "); - htmlReport += "
Did not finish: " - % running.join(", "); + QByteArray dnfList; + for (const auto& r : running) + dnfList += ' ' + r; + plainReport += "\nDID NOT FINISH:" + dnfList; + htmlReport += "
Did not finish:" + dnfList; } cout << plainReport.toStdString() << endl; - if (targetRoom) { - // TODO: Waiting for proper futures to come so that it could be: - // targetRoom->postHtmlText(...) - // .then(this, &TestManager::finalize); // Qt-style or - // .then([this] { finalize(); }); // STL-style - auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); - connect(targetRoom, &Room::messageSent, this, - [this, txnId](const QString& serverTxnId) { - if (txnId != serverTxnId) - return; - - cout << "Leaving the room" << endl; - connect(targetRoom->leaveRoom(), &BaseJob::finished, this, - &TestManager::finalize); + // TODO: Waiting for proper futures to come so that it could be: + // targetRoom->postHtmlText(...) + // .then(this, &TestManager::finalize); // Qt-style or + // .then([this] { finalize(); }); // STL-style + auto* room = testSuite->room(); + auto txnId = room->postHtmlText(plainReport, htmlReport); + connect(room, &Room::messageSent, this, + [this, room, txnId](const QString& serverTxnId) { + if (txnId != serverTxnId) + return; + + cout << "Leaving the room" << endl; + auto* job = room->leaveRoom(); + connect(job, &BaseJob::finished, this, [this, job] { + Q_ASSERT(job->status().good()); + finalize(); }); - } else - finalize(); + }); } void TestManager::finalize() @@ -579,3 +661,5 @@ int main(int argc, char* argv[]) TestManager test { conn, argv[4], argc >= 6 ? argv[5] : nullptr }; return app.exec(); } + +#include "quotest.moc" -- cgit v1.2.3 From b4b2c7c4043368320a81759269945b08a5b81929 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 21 Oct 2019 16:01:25 +0900 Subject: Quotest: derive TestManager from QCoreApplication Also: joining the test room didn't survive being a test in its own right and has been reverted to be a part of setupAndRun(). --- tests/quotest.cpp | 201 +++++++++++++++++++++++++++--------------------------- 1 file changed, 100 insertions(+), 101 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 91f4f636..14afcf4b 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -26,9 +26,9 @@ using std::cout, std::endl; class TestSuite; -class TestManager : public QObject { +class TestManager : public QCoreApplication { public: - TestManager(Connection* conn, QString testRoomName, QString source); + TestManager(int& argc, char** argv); private: void setupAndRun(); @@ -38,7 +38,7 @@ private: void finalize(); private: - QScopedPointer c; + Connection* c = nullptr; QString origin; QString targetRoomName; TestSuite* testSuite = nullptr; @@ -74,20 +74,12 @@ const char* testName(const TestToken& token) { return token.constData(); } class TestSuite : public QObject { Q_OBJECT public: - TestSuite(Connection* conn, QString testRoomAlias, QString source, - TestManager* parent) - : QObject(parent) - , targetConn(conn) - , targetRoomAlias(std::move(testRoomAlias)) - , origin(std::move(source)) + TestSuite(Room* testRoom, QString source, TestManager* parent) + : QObject(parent), targetRoom(testRoom), origin(std::move(source)) { - Q_ASSERT(conn && parent); - Q_ASSERT(targetRoomAlias.startsWith('!') - || targetRoomAlias.startsWith('#')); + Q_ASSERT(testRoom && parent); } - TEST_DECL(joinRoom) - signals: void finishedItem(QByteArray /*name*/, bool /*condition*/); @@ -104,7 +96,7 @@ public slots: public: Room* room() const { return targetRoom; } - Connection* connection() const { return targetConn; } + Connection* connection() const { return targetRoom->connection(); } private slots: bool checkFileSendingOutcome(const TestToken& thisTest, @@ -119,10 +111,8 @@ private: int line); private: - Connection* targetConn; - QString targetRoomAlias; + Room* targetRoom; QString origin; - Room* targetRoom = nullptr; }; #define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) @@ -162,17 +152,46 @@ void TestSuite::finishTest(const TestToken& token, bool condition, emit finishedItem(item, condition); } -TestManager::TestManager(Connection* conn, QString testRoomName, QString source) - : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) +TestManager::TestManager(int& argc, char** argv) + : QCoreApplication(argc, argv), c(new Connection(this)) { - if (!origin.isEmpty()) + Q_ASSERT(argc >= 5); + cout << "Connecting to Matrix as " << argv[1] << endl; + c->connectToServer(argv[1], argv[2], argv[3]); + targetRoomName = argv[4]; + cout << "Test room name: " << argv[4] << endl; + if (argc > 5) { + origin = argv[5]; cout << "Origin for the test message: " << origin.toStdString() << endl; - cout << "Test room name: " << targetRoomName.toStdString() << endl; + } + + connect(c, &Connection::connected, this, &TestManager::setupAndRun); + connect(c, &Connection::resolveError, this, + [this](const QString& error) { + cout << "Failed to resolve the server: " << error.toStdString() + << endl; + this->exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loginError, this, + [this](const QString& message, const QString& details) { + cout << "Failed to login to " + << c->homeserver().toDisplayString().toStdString() << ": " + << message.toStdString() << endl + << "Details:" << endl + << details.toStdString() << endl; + this->exit(-2); + }, + Qt::QueuedConnection); + connect(c, &Connection::loadedRoomState, this, &TestManager::onNewRoom); - connect(c.data(), &Connection::connected, this, &TestManager::setupAndRun); - connect(c.data(), &Connection::loadedRoomState, this, &TestManager::onNewRoom); // Big countdown watchdog - QTimer::singleShot(180000, this, &TestManager::conclude); + QTimer::singleShot(180000, this, [this] { + if (testSuite) + conclude(); + else + finalize(); + }); } void TestManager::setupAndRun() @@ -185,8 +204,30 @@ void TestManager::setupAndRun() c->setLazyLoading(true); c->syncLoop(); - connectSingleShot(c.data(), &Connection::syncDone, this, - &TestManager::doTests); + + cout << "Joining " << targetRoomName.toStdString() << endl; + auto joinJob = c->joinRoom(targetRoomName); + // Ensure, before this test is completed, that the room has been joined + // and filled with some events so that other tests could use that + connect(joinJob, &BaseJob::success, this, [this, joinJob] { + testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this); + testSuite->room()->getPreviousContent(); + connectSingleShot(testSuite->room(), &Room::addedMessages, this, [this] { + connectSingleShot(c, &Connection::syncDone, this, + &TestManager::doTests); + connect(c, &Connection::syncDone, testSuite, [this] { + cout << "Sync complete" << endl; + if (auto* r = testSuite->room()) + cout << "Test room timeline size = " << r->timelineSize() + << ", pending size = " << r->pendingEvents().size() + << endl; + }); + }); + }); + connect(joinJob, &BaseJob::failure, this, [this] { + cout << "Failed to join the test room" << endl; + finalize(); + }); } void TestManager::onNewRoom(Room* r) @@ -198,23 +239,21 @@ void TestManager::onNewRoom(Room* r) connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { cout << timeline.size() << " new event(s) in room " << r->canonicalAlias().toStdString() << endl; - // for (const auto& item: timeline) - // { - // cout << "From: " - // << r->roomMembername(item->senderId()).toStdString() - // << endl << "Timestamp:" - // << item->timestamp().toString().toStdString() << endl - // << "JSON:" << endl << - // item->originalJson().toStdString() << endl; - // } }); } void TestManager::doTests() { + if (testSuite->room()->memberJoinState(c->user()) != JoinState::Join) { + cout << "Test room sanity check failed; after joining the test " + "user is still not in Join state" + << endl; + finalize(); + return; + } cout << "Starting tests" << endl; - Q_ASSERT(!targetRoomName.isEmpty()); - testSuite = new TestSuite(c.data(), targetRoomName, origin, this); + // Some tests below are synchronous; in order to analyse all test outcomes + // uniformly, connect to the signal in advance. connect(testSuite, &TestSuite::finishedItem, this, [this](const QByteArray& itemName, bool condition) { if (auto i = running.indexOf(itemName); i != -1) @@ -224,35 +263,23 @@ void TestManager::doTests() "Test item is not in running state"); }); - running.push_back("joinRoom"); - testSuite->joinRoom("joinRoom"); - connectSingleShot(testSuite, &TestSuite::finishedItem, this, - [this](const QByteArray&, bool condition) { - if (!condition) { - finalize(); - return; - } - const auto* metaObj = testSuite->metaObject(); - for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); - ++i) { - const auto metaMethod = metaObj->method(i); - if (metaMethod.access() != QMetaMethod::Public - || metaMethod.methodType() != QMetaMethod::Slot) - continue; - - // By now connectSingleShot() has already disconnected this - // slot so the tests below can emit finishedItem() without - // the risk of recursion. - cout << "Starting: " << metaMethod.name().constData() << endl; - running.push_back(metaMethod.name()); - metaMethod.invoke(testSuite, Qt::DirectConnection, - Q_ARG(QByteArray, metaMethod.name())); - } - }); - connect(c.data(), &Connection::syncDone, c.data(), [this] { - cout << "Sync complete, "; + const auto* metaObj = testSuite->metaObject(); + for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); + ++i) { + const auto metaMethod = metaObj->method(i); + if (metaMethod.access() != QMetaMethod::Public + || metaMethod.methodType() != QMetaMethod::Slot) + continue; + + cout << "Starting: " << metaMethod.name().constData() << endl; + running.push_back(metaMethod.name()); + metaMethod.invoke(testSuite, Qt::DirectConnection, + Q_ARG(QByteArray, metaMethod.name())); + } + // By now, sync tests have all completed; catch up with async ones + connect(testSuite, &TestSuite::finishedItem, this, [this] { if (running.empty()) { - cout << "all tests finished" << endl; + cout << "All tests finished" << endl; conclude(); return; } @@ -260,30 +287,9 @@ void TestManager::doTests() for (const auto& test: qAsConst(running)) cout << " " << testName(test); cout << endl; - if (auto* r = testSuite->room()) - cout << "Test room timeline size = " << r->timelineSize() - << ", pending size = " << r->pendingEvents().size() << endl; }); } -TEST_IMPL(joinRoom) -{ - cout << "Joining " << targetRoomAlias.toStdString() << endl; - auto joinJob = connection()->joinRoom(targetRoomAlias); - // Ensure, before this test is completed, that the room has been joined - // and filled with some events so that other tests could use that - connect(joinJob, &BaseJob::success, this, [this, joinJob, thisTest] { - targetRoom = connection()->room(joinJob->roomId()); - targetRoom->getPreviousContent(); - connectUntil(targetRoom, &Room::addedMessages, this, [this, thisTest] { - FINISH_TEST(targetRoom->memberJoinState(connection()->user()) - == JoinState::Join); - }); - }); - connect(joinJob, &BaseJob::failure, this, [this, thisTest] { FAIL_TEST(); }); - return false; -} - TEST_IMPL(loadMembers) { // Trying to load members from another (larger) room @@ -588,7 +594,6 @@ TEST_IMPL(markDirectChat) void TestManager::conclude() { - c->stopSync(); auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; if (!failed.empty() || !running.empty()) succeededRec += @@ -602,14 +607,14 @@ void TestManager::conclude() % "'>Testing complete, " % succeededRec; if (!failed.empty()) { QByteArray failedList; - for (const auto& f : failed) + for (const auto& f : qAsConst(failed)) failedList += ' ' + f; plainReport += "\nFAILED:" + failedList; htmlReport += "
Failed:" + failedList; } if (!running.empty()) { QByteArray dnfList; - for (const auto& r : running) + for (const auto& r : qAsConst(running)) dnfList += ' ' + r; plainReport += "\nDID NOT FINISH:" + dnfList; htmlReport += "
Did not finish:" + dnfList; @@ -640,26 +645,20 @@ void TestManager::finalize() { cout << "Logging out" << endl; c->logout(); - connect(c.data(), &Connection::loggedOut, qApp, [this] { - QCoreApplication::processEvents(); - QCoreApplication::exit(failed.size() + running.size()); - }); + connect(c, &Connection::loggedOut, + this, [this] { this->exit(failed.size() + running.size()); }, + Qt::QueuedConnection); } int main(int argc, char* argv[]) { - QCoreApplication app(argc, argv); + // TODO: use QCommandLineParser if (argc < 5) { cout << "Usage: quotest [origin]" << endl; return -1; } - - cout << "Connecting to the server as " << argv[1] << endl; - auto conn = new Connection; - conn->connectToServer(argv[1], argv[2], argv[3]); - TestManager test { conn, argv[4], argc >= 6 ? argv[5] : nullptr }; - return app.exec(); + return TestManager(argc, argv).exec(); } #include "quotest.moc" -- cgit v1.2.3 From 9b588b30505a71da8004965364a42d24f6f2f16d Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 21 Oct 2019 16:05:34 +0900 Subject: Quotest: use clog instead of cout Because cout is not synchronised with stderr and the library's debug printing is not aligned with that from quotest. qDebug()/qWarn() would probably be even better, for consistency; but require switching from std::string to QString. --- tests/quotest.cpp | 96 +++++++++++++++++++++++++++---------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 14afcf4b..81f2e1e0 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -22,7 +22,7 @@ #include using namespace Quotient; -using std::cout, std::endl; +using std::clog, std::endl; class TestSuite; @@ -138,12 +138,12 @@ void TestSuite::finishTest(const TestToken& token, bool condition, { const auto& item = testName(token); if (condition) { - cout << item << " successful" << endl; + clog << item << " successful" << endl; if (targetRoom) targetRoom->postMessage(origin % ": " % item % " successful", MessageEventType::Notice); } else { - cout << item << " FAILED at " << file << ":" << line << endl; + clog << item << " FAILED at " << file << ":" << line << endl; if (targetRoom) targetRoom->postPlainText(origin % ": " % item % " FAILED at " % file % ", line " % QString::number(line)); @@ -156,26 +156,26 @@ TestManager::TestManager(int& argc, char** argv) : QCoreApplication(argc, argv), c(new Connection(this)) { Q_ASSERT(argc >= 5); - cout << "Connecting to Matrix as " << argv[1] << endl; + clog << "Connecting to Matrix as " << argv[1] << endl; c->connectToServer(argv[1], argv[2], argv[3]); targetRoomName = argv[4]; - cout << "Test room name: " << argv[4] << endl; + clog << "Test room name: " << argv[4] << endl; if (argc > 5) { origin = argv[5]; - cout << "Origin for the test message: " << origin.toStdString() << endl; + clog << "Origin for the test message: " << origin.toStdString() << endl; } connect(c, &Connection::connected, this, &TestManager::setupAndRun); connect(c, &Connection::resolveError, this, [this](const QString& error) { - cout << "Failed to resolve the server: " << error.toStdString() + clog << "Failed to resolve the server: " << error.toStdString() << endl; this->exit(-2); }, Qt::QueuedConnection); connect(c, &Connection::loginError, this, [this](const QString& message, const QString& details) { - cout << "Failed to login to " + clog << "Failed to login to " << c->homeserver().toDisplayString().toStdString() << ": " << message.toStdString() << endl << "Details:" << endl @@ -198,14 +198,14 @@ void TestManager::setupAndRun() { Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); Q_ASSERT(c->domain() == c->userId().section(':', 1)); - cout << "Connected, server: " + clog << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; - cout << "Access token: " << c->accessToken().toStdString() << endl; + clog << "Access token: " << c->accessToken().toStdString() << endl; c->setLazyLoading(true); c->syncLoop(); - cout << "Joining " << targetRoomName.toStdString() << endl; + clog << "Joining " << targetRoomName.toStdString() << endl; auto joinJob = c->joinRoom(targetRoomName); // Ensure, before this test is completed, that the room has been joined // and filled with some events so that other tests could use that @@ -216,28 +216,28 @@ void TestManager::setupAndRun() connectSingleShot(c, &Connection::syncDone, this, &TestManager::doTests); connect(c, &Connection::syncDone, testSuite, [this] { - cout << "Sync complete" << endl; + clog << "Sync complete" << endl; if (auto* r = testSuite->room()) - cout << "Test room timeline size = " << r->timelineSize() + clog << "Test room timeline size = " << r->timelineSize() << ", pending size = " << r->pendingEvents().size() << endl; }); }); }); connect(joinJob, &BaseJob::failure, this, [this] { - cout << "Failed to join the test room" << endl; + clog << "Failed to join the test room" << endl; finalize(); }); } void TestManager::onNewRoom(Room* r) { - cout << "New room: " << r->id().toStdString() << endl + clog << "New room: " << r->id().toStdString() << endl << " Name: " << r->name().toStdString() << endl << " Canonical alias: " << r->canonicalAlias().toStdString() << endl << endl; connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { - cout << timeline.size() << " new event(s) in room " + clog << timeline.size() << " new event(s) in room " << r->canonicalAlias().toStdString() << endl; }); } @@ -245,13 +245,13 @@ void TestManager::onNewRoom(Room* r) void TestManager::doTests() { if (testSuite->room()->memberJoinState(c->user()) != JoinState::Join) { - cout << "Test room sanity check failed; after joining the test " + clog << "Test room sanity check failed; after joining the test " "user is still not in Join state" << endl; finalize(); return; } - cout << "Starting tests" << endl; + clog << "Starting tests" << endl; // Some tests below are synchronous; in order to analyse all test outcomes // uniformly, connect to the signal in advance. connect(testSuite, &TestSuite::finishedItem, this, @@ -271,7 +271,7 @@ void TestManager::doTests() || metaMethod.methodType() != QMetaMethod::Slot) continue; - cout << "Starting: " << metaMethod.name().constData() << endl; + clog << "Starting: " << metaMethod.name().constData() << endl; running.push_back(metaMethod.name()); metaMethod.invoke(testSuite, Qt::DirectConnection, Q_ARG(QByteArray, metaMethod.name())); @@ -279,14 +279,14 @@ void TestManager::doTests() // By now, sync tests have all completed; catch up with async ones connect(testSuite, &TestSuite::finishedItem, this, [this] { if (running.empty()) { - cout << "All tests finished" << endl; + clog << "All tests finished" << endl; conclude(); return; } - cout << running.size() << " test(s) in the air:"; + clog << running.size() << " test(s) in the air:"; for (const auto& test: qAsConst(running)) - cout << " " << testName(test); - cout << endl; + clog << " " << testName(test); + clog << endl; }); } @@ -296,7 +296,7 @@ TEST_IMPL(loadMembers) auto* r = connection()->roomByAlias(QStringLiteral("#quotient:matrix.org"), JoinState::Join); if (!r) { - cout << "#quotient:matrix.org is not found in the test user's rooms" + clog << "#quotient:matrix.org is not found in the test user's rooms" << endl; FAIL_TEST(); } @@ -304,7 +304,7 @@ TEST_IMPL(loadMembers) // 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; + clog << "Lazy loading doesn't seem to be enabled" << endl; FAIL_TEST(); } r->setDisplayed(); @@ -318,7 +318,7 @@ TEST_IMPL(sendMessage) { auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); if (!validatePendingEvent(txnId)) { - cout << "Invalid pending event right after submitting" << endl; + clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this, @@ -338,13 +338,13 @@ TEST_IMPL(sendMessage) TEST_IMPL(sendReaction) { - cout << "Reacting to the newest message in the room" << endl; + clog << "Reacting to the newest message in the room" << endl; Q_ASSERT(targetRoom->timelineSize() > 0); const auto targetEvtId = targetRoom->messageEvents().back()->id(); const auto key = QStringLiteral("+1"); const auto txnId = targetRoom->postReaction(targetEvtId, key); if (!validatePendingEvent(txnId)) { - cout << "Invalid pending event right after submitting" << endl; + clog << "Invalid pending event right after submitting" << endl; FAIL_TEST(); } @@ -375,7 +375,7 @@ TEST_IMPL(sendFile) { auto* tf = new QTemporaryFile; if (!tf->open()) { - cout << "Failed to create a temporary file" << endl; + clog << "Failed to create a temporary file" << endl; FAIL_TEST(); } tf->write("Test"); @@ -383,11 +383,11 @@ TEST_IMPL(sendFile) // 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; + clog << "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; + clog << "Invalid pending event right after submitting" << endl; delete tf; FAIL_TEST(); } @@ -422,11 +422,11 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, { auto it = targetRoom->findPendingEvent(txnId); if (it == targetRoom->pendingEvents().end()) { - cout << "Pending file event dropped before upload completion" << endl; + clog << "Pending file event dropped before upload completion" << endl; FAIL_TEST(); } if (it->deliveryStatus() != EventStatus::FileUploaded) { - cout << "Pending file event status upon upload completion is " + clog << "Pending file event status upon upload completion is " << it->deliveryStatus() << " != FileUploaded(" << EventStatus::FileUploaded << ')' << endl; FAIL_TEST(); @@ -440,7 +440,7 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, if (evt->transactionId() != txnId) return false; - cout << "File event " << txnId.toStdString() + clog << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; // This part tests visit() return visit( @@ -468,7 +468,7 @@ TEST_IMPL(setTopic) if (targetRoom->topic() == newTopic) FINISH_TEST(true); - cout << "Requested topic was " << newTopic.toStdString() << ", " + clog << "Requested topic was " << newTopic.toStdString() << ", " << targetRoom->topic().toStdString() << " arrived instead" << endl; return false; @@ -478,7 +478,7 @@ TEST_IMPL(setTopic) TEST_IMPL(sendAndRedact) { - cout << "Sending a message to redact" << endl; + clog << "Sending a message to redact" << endl; auto txnId = targetRoom->postPlainText(origin % ": message to redact"); if (txnId.isEmpty()) FAIL_TEST(); @@ -488,7 +488,7 @@ TEST_IMPL(sendAndRedact) if (tId != txnId) return; - cout << "Redacting the message" << endl; + clog << "Redacting the message" << endl; targetRoom->redactEvent(evtId, origin); connectUntil(targetRoom, &Room::addedMessages, this, @@ -510,11 +510,11 @@ bool TestSuite::checkRedactionOutcome(const QByteArray& thisTest, return false; // Waiting for the next sync if ((*it)->isRedacted()) { - cout << "The sync brought already redacted message" << endl; + clog << "The sync brought already redacted message" << endl; FINISH_TEST(true); } - cout << "Message came non-redacted with the sync, waiting for redaction" + clog << "Message came non-redacted with the sync, waiting for redaction" << endl; connectUntil(targetRoom, &Room::replacedEvent, this, [this, thisTest, evtIdToRedact](const RoomEvent* newEvent, @@ -543,10 +543,10 @@ TEST_IMPL(addAndRemoveTag) QSignalSpy spy(targetRoom, &Room::tagsChanged); targetRoom->addTag(TestTag); if (spy.count() != 1 || !targetRoom->tags().contains(TestTag)) { - cout << "Tag adding failed" << endl; + clog << "Tag adding failed" << endl; FAIL_TEST(); } - cout << "Test tag set, removing it now" << endl; + clog << "Test tag set, removing it now" << endl; targetRoom->removeTag(TestTag); FINISH_TEST(spy.count() == 2 && !targetRoom->tags().contains(TestTag)); } @@ -568,7 +568,7 @@ TEST_IMPL(markDirectChat) // Same as with tags (and unusual for the rest of Quotient), direct chat // operations are synchronous. QSignalSpy spy(connection(), &Connection::directChatsListChanged); - cout << "Marking the room as a direct chat" << endl; + clog << "Marking the room as a direct chat" << endl; connection()->addToDirectChats(targetRoom, connection()->user()); if (spy.count() != 1 || !checkDirectChat()) FAIL_TEST(); @@ -577,11 +577,11 @@ TEST_IMPL(markDirectChat) const auto& addedDCs = spy.back().front().value(); if (addedDCs.size() != 1 || !addedDCs.contains(connection()->user(), targetRoom->id())) { - cout << "The room is not in added direct chats" << endl; + clog << "The room is not in added direct chats" << endl; FAIL_TEST(); } - cout << "Unmarking the direct chat" << endl; + clog << "Unmarking the direct chat" << endl; connection()->removeFromDirectChats(targetRoom->id(), connection()->user()); if (spy.count() != 2 && checkDirectChat()) FAIL_TEST(); @@ -619,7 +619,7 @@ void TestManager::conclude() plainReport += "\nDID NOT FINISH:" + dnfList; htmlReport += "
Did not finish:" + dnfList; } - cout << plainReport.toStdString() << endl; + clog << plainReport.toStdString() << endl; // TODO: Waiting for proper futures to come so that it could be: // targetRoom->postHtmlText(...) @@ -632,7 +632,7 @@ void TestManager::conclude() if (txnId != serverTxnId) return; - cout << "Leaving the room" << endl; + clog << "Leaving the room" << endl; auto* job = room->leaveRoom(); connect(job, &BaseJob::finished, this, [this, job] { Q_ASSERT(job->status().good()); @@ -643,7 +643,7 @@ void TestManager::conclude() void TestManager::finalize() { - cout << "Logging out" << endl; + clog << "Logging out" << endl; c->logout(); connect(c, &Connection::loggedOut, this, [this] { this->exit(failed.size() + running.size()); }, @@ -654,7 +654,7 @@ int main(int argc, char* argv[]) { // TODO: use QCommandLineParser if (argc < 5) { - cout << "Usage: quotest [origin]" + clog << "Usage: quotest [origin]" << endl; return -1; } -- cgit v1.2.3 From b822e2e116c38c99fd9f05fbe0f3c0fd20103b29 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 21 Oct 2019 20:26:16 +0900 Subject: Quotest: more work on test invocation Tests should be private slots, not public slots; and they are invoked through a QueuedConnection so that both sync and async tests could be processed uniformly with clear code. --- CONTRIBUTING.md | 10 +++--- tests/quotest.cpp | 98 ++++++++++++++++++++++++++++++------------------------- 2 files changed, 58 insertions(+), 50 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88352e04..fd604621 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -249,14 +249,14 @@ be used eventually. The `tests/` directory contains a command-line program, quotest, used for automated functional testing. Any significant addition to the library API should be accompanied by a respective test in quotest. To add a test you should: -- Add a new test to the `TestSuite` class (technically, each test is a public +- Add a new test to the `TestSuite` class (technically, each test is a private slot and there are two macros, `TEST_DECL()` and `TEST_IMPL()`, that conceal - passing the testing context to the test method). + passing the testing handle in `thisTest` variable to the test method). - Add test logic to the slot, using `FINISH_TEST` macro to assert the test outcome and complete the test (`FINISH_TEST` contains `return`). ALL - (even failing) branches should conclude with a `FINISH_TEST` invocation, - unless you intend to have a "DID NOT FINISH" message in the logs - under certain conditions. + (even failing) branches should conclude with a `FINISH_TEST` (or `FAIL_TEST` + that is a shortcut for a failing `FINISH_TEST`) invocation, unless you + intend to have a "DID NOT FINISH" message in the logs in certain conditions. The `TestManager` class sets up some basic test fixture to help you with testing; notably, the tests can rely on having an initialised `Room` object for the test diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 81f2e1e0..12e19a97 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -46,6 +46,7 @@ private: }; using TestToken = QByteArray; // return value of QMetaMethod::name +Q_DECLARE_METATYPE(TestToken); // For now, the token itself is the test name but that may change. const char* testName(const TestToken& token) { return token.constData(); } @@ -77,6 +78,7 @@ public: TestSuite(Room* testRoom, QString source, TestManager* parent) : QObject(parent), targetRoom(testRoom), origin(std::move(source)) { + qRegisterMetaType(); Q_ASSERT(testRoom && parent); } @@ -84,6 +86,9 @@ signals: void finishedItem(QByteArray /*name*/, bool /*condition*/); public slots: + void doTest(const QByteArray& testName); + +private slots: TEST_DECL(loadMembers) TEST_DECL(sendMessage) TEST_DECL(sendReaction) @@ -98,13 +103,12 @@ public: Room* room() const { return targetRoom; } Connection* connection() const { return targetRoom->connection(); } -private slots: +private: bool checkFileSendingOutcome(const TestToken& thisTest, const QString& txnId, const QString& fileName); bool checkRedactionOutcome(const QByteArray& thisTest, const QString& evtIdToRedact); -private: bool validatePendingEvent(const QString& txnId); bool checkDirectChat() const; void finishTest(const TestToken& token, bool condition, const char* file, @@ -125,6 +129,13 @@ private: #define FAIL_TEST() FINISH_TEST(false) +void TestSuite::doTest(const QByteArray& testName) +{ + clog << "Starting: " << testName.constData() << endl; + QMetaObject::invokeMethod(this, testName, Qt::DirectConnection, + Q_ARG(TestToken, testName)); +} + bool TestSuite::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); @@ -211,17 +222,14 @@ void TestManager::setupAndRun() // and filled with some events so that other tests could use that connect(joinJob, &BaseJob::success, this, [this, joinJob] { testSuite = new TestSuite(c->room(joinJob->roomId()), origin, this); - testSuite->room()->getPreviousContent(); - connectSingleShot(testSuite->room(), &Room::addedMessages, this, [this] { - connectSingleShot(c, &Connection::syncDone, this, - &TestManager::doTests); - connect(c, &Connection::syncDone, testSuite, [this] { - clog << "Sync complete" << endl; - if (auto* r = testSuite->room()) - clog << "Test room timeline size = " << r->timelineSize() - << ", pending size = " << r->pendingEvents().size() - << endl; - }); + connectSingleShot(c, &Connection::syncDone, this, [this] { + if (testSuite->room()->timelineSize() > 0) + doTests(); + else { + testSuite->room()->getPreviousContent(); + connectSingleShot(testSuite->room(), &Room::addedMessages, this, + &TestManager::doTests); + } }); }); connect(joinJob, &BaseJob::failure, this, [this] { @@ -244,16 +252,24 @@ void TestManager::onNewRoom(Room* r) void TestManager::doTests() { - if (testSuite->room()->memberJoinState(c->user()) != JoinState::Join) { - clog << "Test room sanity check failed; after joining the test " - "user is still not in Join state" - << endl; - finalize(); - return; + const auto* metaObj = testSuite->metaObject(); + for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); ++i) { + const auto metaMethod = metaObj->method(i); + if (metaMethod.access() != QMetaMethod::Private + || metaMethod.methodType() != QMetaMethod::Slot) + continue; + + const auto testName = metaMethod.name(); + running.push_back(testName); + // Some tests return the result immediately, so queue everything + // so that we could process all tests asynchronously. + QMetaObject::invokeMethod(testSuite, "doTest", Qt::QueuedConnection, + Q_ARG(QByteArray, testName)); } - clog << "Starting tests" << endl; - // Some tests below are synchronous; in order to analyse all test outcomes - // uniformly, connect to the signal in advance. + clog << "Tests to do:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; connect(testSuite, &TestSuite::finishedItem, this, [this](const QByteArray& itemName, bool condition) { if (auto i = running.indexOf(itemName); i != -1) @@ -261,32 +277,24 @@ void TestManager::doTests() else Q_ASSERT_X(false, itemName, "Test item is not in running state"); + if (running.empty()) { + clog << "All tests finished" << endl; + conclude(); + } }); - const auto* metaObj = testSuite->metaObject(); - for (auto i = metaObj->methodOffset(); i < metaObj->methodCount(); - ++i) { - const auto metaMethod = metaObj->method(i); - if (metaMethod.access() != QMetaMethod::Public - || metaMethod.methodType() != QMetaMethod::Slot) - continue; - - clog << "Starting: " << metaMethod.name().constData() << endl; - running.push_back(metaMethod.name()); - metaMethod.invoke(testSuite, Qt::DirectConnection, - Q_ARG(QByteArray, metaMethod.name())); - } - // By now, sync tests have all completed; catch up with async ones - connect(testSuite, &TestSuite::finishedItem, this, [this] { - if (running.empty()) { - clog << "All tests finished" << endl; - conclude(); - return; + connect(c, &Connection::syncDone, this, [this] { + static int i = 0; + clog << "Sync " << ++i << " complete" << endl; + if (auto* r = testSuite->room()) + clog << "Test room timeline size = " << r->timelineSize() + << ", pending size = " << r->pendingEvents().size() << endl; + if (!running.empty()) { + clog << running.size() << " test(s) in the air:"; + for (const auto& test: qAsConst(running)) + clog << " " << testName(test); + clog << endl; } - clog << running.size() << " test(s) in the air:"; - for (const auto& test: qAsConst(running)) - clog << " " << testName(test); - clog << endl; }); } -- cgit v1.2.3 From 5a3f154b2795d90f2c7bc31dfead49ad0e746a8a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 13 Dec 2019 14:01:59 +0300 Subject: quotest: cleanup --- tests/quotest.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'tests/quotest.cpp') diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 12e19a97..194c6a69 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -400,8 +400,9 @@ TEST_IMPL(sendFile) FAIL_TEST(); } - // FIXME: Clean away connections (connectUntil doesn't help here). - connectUntil(targetRoom, &Room::fileTransferCompleted, this, + // Using tf as a context object to clean away both connections + // once either of them triggers. + connectUntil(targetRoom, &Room::fileTransferCompleted, tf, [this, thisTest, txnId, tf, tfName](const QString& id) { auto fti = targetRoom->fileTransferInfo(id); Q_ASSERT(fti.status == FileTransferInfo::Completed); @@ -409,16 +410,16 @@ TEST_IMPL(sendFile) if (id != txnId) return false; - delete tf; + tf->deleteLater(); return checkFileSendingOutcome(thisTest, txnId, tfName); }); - connectUntil(targetRoom, &Room::fileTransferFailed, this, + connectUntil(targetRoom, &Room::fileTransferFailed, tf, [this, thisTest, txnId, tf](const QString& id, const QString& error) { if (id != txnId) return false; targetRoom->postPlainText(origin % ": File upload failed: " % error); - delete tf; + tf->deleteLater(); FAIL_TEST(); }); return false; @@ -454,6 +455,8 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest, return visit( *evt, [&](const RoomMessageEvent& e) { + // TODO: actually try to download it to check, e.g., #366 + // (and #368 would help to test against bad file names). FINISH_TEST( !e.id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() -- cgit v1.2.3