aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2019-10-03 08:16:29 +0900
committerKitsune Ral <Kitsune-Ral@users.sf.net>2019-10-18 10:25:14 +0900
commitd59ca7ac194e8d57177afb1ac89603e22b61b4ec (patch)
tree75a7224c04891512dfa318617f6630b94c199210
parent0a82e68f86a3b4716ebada39c041a6c8673d2999 (diff)
downloadlibquotient-d59ca7ac194e8d57177afb1ac89603e22b61b4ec.tar.gz
libquotient-d59ca7ac194e8d57177afb1ac89603e22b61b4ec.zip
qmc-example -> quotest, QMCTest -> TestManager
Also: some bits of refactoring in the test code to make it more extensible. Closes #352.
-rw-r--r--.travis.yml14
-rw-r--r--CMakeLists.txt18
-rw-r--r--lib/csapi/gtad.yaml4
-rw-r--r--quotest.pro (renamed from qmc-example.pro)5
-rw-r--r--tests/.valgrind.supp (renamed from .valgrind.qmc-example.supp)0
-rw-r--r--tests/CMakeLists.txt (renamed from examples/CMakeLists.txt)14
-rw-r--r--tests/quotest.cpp577
7 files changed, 606 insertions, 26 deletions
diff --git a/.travis.yml b/.travis.yml
index 21b2fd64..3f2759bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,7 @@ env:
global:
- DESTDIR="$TRAVIS_BUILD_DIR/install"
- CMAKE_ARGS="-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=$DESTDIR/usr"
- - VALGRIND="valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=.valgrind.qmc-example.supp $VALGRIND_OPTIONS"
+ - VALGRIND="valgrind --tool=memcheck --leak-check=yes --gen-suppressions=all --suppressions=tests/.valgrind.supp $VALGRIND_OPTIONS"
matrix:
include:
@@ -65,14 +65,14 @@ before_script:
script:
- _cmake_build --target install
-# Build qmc-example with the installed libQuotient
-- cmake $CMAKE_ARGS examples -Bbuild-example -DOlm_DIR=olm/build
-- cmake --build build-example --target all
+# Build quotest with the installed libQuotient
+- cmake $CMAKE_ARGS tests -Bbuild-test -DOlm_DIR=olm/build
+- cmake --build build-test --target all
# Build with qmake
-- qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/lib" "LIBS += -Lolm/build"
+- qmake quotest.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/lib" "LIBS += -Lolm/build"
- make all
-# Run the qmake-compiled qmc-example under valgrind
-- if [ "$QMC_TEST_USER" != "" ]; then LD_LIBRARY_PATH="olm/build" $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi
+# Run the qmake-compiled quotest under valgrind
+- if [ "$TEST_USER" != "" ]; then LD_LIBRARY_PATH="olm/build" $VALGRIND ./quotest "$QMC_TEST_USER" "$QMC_TEST_PWD" quotest-travis '#quotest:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi
notifications:
webhooks:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ce4af9a8..58509eae 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,7 +6,7 @@ endif()
set(API_VERSION "0.6")
project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX)
-option(QUOTIENT_INSTALL_EXAMPLE "install qmc-example application" ON)
+option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON)
include(CheckCXXCompilerFlag)
if (NOT WIN32)
@@ -52,7 +52,7 @@ else()
endforeach ()
endif()
-find_package(Qt5 5.9 REQUIRED Network Gui Multimedia)
+find_package(Qt5 5.9 REQUIRED Network Gui Multimedia Test)
get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE)
if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM)
@@ -218,7 +218,7 @@ if (MATRIX_DOC_PATH AND GTAD_PATH)
endif()
endif()
-set(example_SRCS examples/qmc-example.cpp)
+set(tests_SRCS tests/quotest.cpp)
add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS})
set_target_properties(${PROJECT_NAME} PROPERTIES
@@ -237,8 +237,10 @@ target_include_directories(${PROJECT_NAME} PUBLIC
)
target_link_libraries(${PROJECT_NAME} QtOlm Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia)
-add_executable(qmc-example ${example_SRCS})
-target_link_libraries(qmc-example Qt5::Core Quotient)
+set(TEST_BINARY quotest)
+add_executable(${TEST_BINARY} ${tests_SRCS})
+target_link_libraries(${TEST_BINARY} Qt5::Core Qt5::Test Quotient)
+
configure_file(Quotient.pc.in ${CMAKE_CURRENT_BINARY_DIR}/Quotient.pc @ONLY NEWLINE_STYLE UNIX)
# Installation
@@ -281,9 +283,9 @@ if (WIN32)
install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages)
endif (WIN32)
-if (QUOTIENT_INSTALL_EXAMPLE)
- install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
-endif (QUOTIENT_INSTALL_EXAMPLE)
+if (QUOTIENT_INSTALL_TESTS)
+ install(TARGETS ${TEST_BINARY} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+endif ()
if (UNIX AND NOT APPLE)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/Quotient.pc
diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml
index 301ee0b6..6d4e080f 100644
--- a/lib/csapi/gtad.yaml
+++ b/lib/csapi/gtad.yaml
@@ -95,9 +95,9 @@ analyzer:
- //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> }
- map: # `additionalProperties` in OpenAPI
- RoomState:
- type: "std::unordered_map<QString, {{1}}>"
+ type: "UnorderedMap<QString, {{1}}>"
moveOnly:
- imports: <unordered_map>
+ imports: '"util.h"'
- /.+/:
type: "QHash<QString, {{1}}>"
imports: <QtCore/QHash>
diff --git a/qmc-example.pro b/quotest.pro
index a9548df9..433a2ccc 100644
--- a/qmc-example.pro
+++ b/quotest.pro
@@ -1,12 +1,13 @@
TEMPLATE = app
+QT += testlib
CONFIG *= c++1z warn_on object_parallel_to_source
windows { CONFIG *= console }
include(libquotient.pri)
-SOURCES += examples/qmc-example.cpp
+SOURCES += tests/quotest.cpp
DISTFILES += \
- .valgrind.qmc-example.supp
+ .valgrind.supp
diff --git a/.valgrind.qmc-example.supp b/tests/.valgrind.supp
index d65fb52e..d65fb52e 100644
--- a/.valgrind.qmc-example.supp
+++ b/tests/.valgrind.supp
diff --git a/examples/CMakeLists.txt b/tests/CMakeLists.txt
index 1f512958..b6ba0f18 100644
--- a/examples/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.1)
# This CMakeLists file assumes that the library is installed to CMAKE_INSTALL_PREFIX
# and ignores the in-tree library code. You can use this to start work on your own client.
-project(qmc-example CXX)
+project(quotest CXX)
include(CheckCXXCompilerFlag)
if (NOT WIN32)
@@ -45,13 +45,13 @@ foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu
endif ()
endforeach ()
-find_package(Qt5 5.6 REQUIRED Network Gui Multimedia)
+find_package(Qt5 5.9 REQUIRED Network Gui Multimedia)
get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE)
find_package(Quotient REQUIRED)
get_filename_component(QMC_Prefix "${Quotient_DIR}/../.." ABSOLUTE)
-message( STATUS "qmc-example configuration:" )
+message( STATUS "${PROJECT_NAME} configuration:" )
if (CMAKE_BUILD_TYPE)
message( STATUS " Build type: ${CMAKE_BUILD_TYPE}")
endif(CMAKE_BUILD_TYPE)
@@ -59,11 +59,11 @@ message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSI
message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" )
message( STATUS " Quotient: ${Quotient_VERSION} at ${QMC_Prefix}" )
-set(example_SRCS qmc-example.cpp)
+set(example_SRCS quotest.cpp)
-add_executable(qmc-example ${example_SRCS})
-target_link_libraries(qmc-example Qt5::Core Quotient)
+add_executable(${PROJECT_NAME} ${example_SRCS})
+target_link_libraries(${PROJECT_NAME} Qt5::Core Quotient)
# Installation
-install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+install (TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
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 <QtCore/QCoreApplication>
+#include <QtCore/QFileInfo>
+#include <QtCore/QStringBuilder>
+#include <QtCore/QTemporaryFile>
+#include <QtCore/QTimer>
+
+#include <functional>
+#include <iostream>
+
+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<Connection, QScopedPointerDeleteLater> 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<RoomMessageEvent>(*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<const ReactionEvent>(reactions.back());
+ FINISH_TEST("Reaction sending",
+ is<ReactionEvent>(*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 % ": <strong><font data-mx-color='#" % color
+ % "' color='#" % color
+ % "'>Testing complete</font></strong>, " % succeededRec;
+ if (!failed.empty()) {
+ plainReport += "\nFAILED: " % failed.join(", ");
+ htmlReport += "<br><strong>Failed:</strong> " % failed.join(", ");
+ }
+ if (!running.empty()) {
+ plainReport += "\nDID NOT FINISH: " % running.join(", ");
+ htmlReport += "<br><strong>Did not finish:</strong> "
+ % 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 <user> <passwd> <device_name> <room_alias> [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();
+}