diff options
-rw-r--r-- | .travis.yml | 14 | ||||
-rw-r--r-- | CMakeLists.txt | 18 | ||||
-rw-r--r-- | lib/connection.cpp | 6 | ||||
-rw-r--r-- | lib/connection.h | 82 | ||||
-rw-r--r-- | lib/csapi/gtad.yaml | 4 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 19 | ||||
-rw-r--r-- | lib/room.h | 64 | ||||
-rw-r--r-- | lib/user.h | 13 | ||||
-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) | 18 | ||||
-rw-r--r-- | tests/quotest.cpp | 665 |
12 files changed, 793 insertions, 115 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/connection.cpp b/lib/connection.cpp index 25f1c3f6..af85d066 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -385,7 +385,7 @@ void Connection::syncLoop(int timeout) syncLoopIteration(); // initial sync to start the loop } -QJsonObject toJson(const Connection::DirectChatsMap& directChats) +QJsonObject toJson(const DirectChatsMap& directChats) { QJsonObject json; for (auto it = directChats.begin(); it != directChats.end();) { @@ -1050,7 +1050,7 @@ QVector<Room*> Connection::roomsWithTag(const QString& tagName) const return rooms; } -Connection::DirectChatsMap Connection::directChats() const +DirectChatsMap Connection::directChats() const { return d->directChats; } @@ -1117,7 +1117,7 @@ bool Connection::isIgnored(const User* user) const return ignoredUsers().contains(user->id()); } -Connection::IgnoredUsersList Connection::ignoredUsers() const +IgnoredUsersList Connection::ignoredUsers() const { const auto* event = d->unpackAccountData<IgnoredUsersEvent>(); return event ? event->ignored_users() : IgnoredUsersList(); diff --git a/lib/connection.h b/lib/connection.h index 1f1d4cd5..e4109fd4 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -97,6 +97,14 @@ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; Q_ENUM_NS(RunningPolicy) +// Room ids, rather than room pointers, are used in the direct chat +// map types because the library keeps Invite rooms separate from +// rooms in Join and Leave state; and direct chats in account data +// are stored with no regard to their state. +using DirectChatsMap = QMultiHash<const User*, QString>; +using DirectChatUsersMap = QMultiHash<QString, User*>; +using IgnoredUsersList = IgnoredUsersEvent::content_type; + class Connection : public QObject { Q_OBJECT @@ -115,14 +123,6 @@ class Connection : public QObject { lazyLoadingChanged) public: - // Room ids, rather than room pointers, are used in the direct chat - // map types because the library keeps Invite rooms separate from - // rooms in Join and Leave state; and direct chats in account data - // are stored with no regard to their state. - using DirectChatsMap = QMultiHash<const User*, QString>; - using DirectChatUsersMap = QMultiHash<QString, User*>; - using IgnoredUsersList = IgnoredUsersEvent::content_type; - using UsersToDevicesToEvents = UnorderedMap<QString, UnorderedMap<QString, const Event&>>; @@ -153,7 +153,7 @@ public: * from the server. * \sa rooms, room, roomsWithTag */ - Q_INVOKABLE QVector<Room*> allRooms() const; + Q_INVOKABLE QVector<Quotient::Room*> allRooms() const; /// Get rooms that have either of the given join state(s) /*! @@ -163,10 +163,11 @@ public: * Leave rooms from the server. * \sa allRooms, room, roomsWithTag */ - Q_INVOKABLE QVector<Room*> rooms(JoinStates joinStates) const; + Q_INVOKABLE QVector<Quotient::Room*> + rooms(Quotient::JoinStates joinStates) const; /// Get the total number of rooms in the given join state(s) - Q_INVOKABLE int roomsCount(JoinStates joinStates) const; + Q_INVOKABLE int roomsCount(Quotient::JoinStates joinStates) const; /** Check whether the account has data of the given type * Direct chats map is not supported by this method _yet_. @@ -253,10 +254,10 @@ public: QList<User*> directChatUsers(const Room* room) const; /** Check whether a particular user is in the ignore list */ - Q_INVOKABLE bool isIgnored(const User* user) const; + Q_INVOKABLE bool isIgnored(const Quotient::User* user) const; /** Get the whole list of ignored users */ - Q_INVOKABLE IgnoredUsersList ignoredUsers() const; + Q_INVOKABLE Quotient::IgnoredUsersList ignoredUsers() const; /** Add the user to the ignore list * The change signal is emitted synchronously, without waiting @@ -264,14 +265,14 @@ public: * * \sa ignoredUsersListChanged */ - Q_INVOKABLE void addToIgnoredUsers(const User* user); + Q_INVOKABLE void addToIgnoredUsers(const Quotient::User* user); /** Remove the user from the ignore list */ /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ - Q_INVOKABLE void removeFromIgnoredUsers(const User* user); + Q_INVOKABLE void removeFromIgnoredUsers(const Quotient::User* user); /** Get the full list of users known to this account */ QMap<QString, User*> users() const; @@ -281,13 +282,14 @@ public: /** Get the domain name used for ids/aliases on the server */ QString domain() const; /** Find a room by its id and a mask of applicable states */ - Q_INVOKABLE Room* room(const QString& roomId, - JoinStates states = JoinState::Invite - | JoinState::Join) const; + Q_INVOKABLE Quotient::Room* + room(const QString& roomId, + Quotient::JoinStates states = JoinState::Invite | JoinState::Join) const; /** Find a room by its alias and a mask of applicable states */ - Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, - JoinStates states = JoinState::Invite - | JoinState::Join) const; + Q_INVOKABLE Quotient::Room* + roomByAlias(const QString& roomAlias, + Quotient::JoinStates states = JoinState::Invite + | JoinState::Join) const; /** Update the internal map of room aliases to IDs */ /// This is used to maintain the internal index of room aliases. /// It does NOT change aliases on the server, @@ -295,15 +297,15 @@ public: void updateRoomAliases(const QString& roomId, const QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases); - Q_INVOKABLE Room* invitation(const QString& roomId) const; - Q_INVOKABLE User* user(const QString& userId); + Q_INVOKABLE Quotient::Room* invitation(const QString& roomId) const; + Q_INVOKABLE Quotient::User* user(const QString& userId); const User* user() const; User* user(); QString userId() const; QString deviceId() const; QByteArray accessToken() const; QtOlm::Account* olmAccount() const; - Q_INVOKABLE SyncJob* syncJob() const; + Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; Q_INVOKABLE void getTurnServers(); @@ -589,6 +591,7 @@ public slots: /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; + signals: /** * @deprecated @@ -622,7 +625,7 @@ signals: * * @param request - the pointer to the failed job */ - void requestFailed(BaseJob* request); + void requestFailed(Quotient::BaseJob* request); /** A network request (job) failed due to network problems * @@ -640,7 +643,7 @@ signals: void syncDone(); void syncError(QString message, QString details); - void newUser(User* user); + void newUser(Quotient::User* user); /** * \group Signals emitted on room transitions @@ -672,7 +675,7 @@ signals: */ /** A new room object has been created */ - void newRoom(Room* room); + void newRoom(Quotient::Room* room); /** A room invitation is seen for the first time * @@ -680,7 +683,7 @@ signals: * that initial sync will trigger this signal for all rooms in * Invite state. */ - void invitedRoom(Room* room, Room* prev); + void invitedRoom(Quotient::Room* room, Quotient::Room* prev); /** A joined room is seen for the first time * @@ -691,7 +694,7 @@ signals: * this room was in Invite state before, the respective object is * passed in prev (and it will be deleted shortly afterwards). */ - void joinedRoom(Room* room, Room* prev); + void joinedRoom(Quotient::Room* room, Quotient::Room* prev); /** A room has just been left * @@ -702,10 +705,10 @@ signals: * Left rooms upon initial sync (not only those that were left * right before the sync). */ - void leftRoom(Room* room, Room* prev); + void leftRoom(Quotient::Room* room, Quotient::Room* prev); /** The room object is about to be deleted */ - void aboutToDeleteRoom(Room* room); + void aboutToDeleteRoom(Quotient::Room* room); /** The room has just been created by createRoom or requestDirectChat * @@ -716,7 +719,7 @@ signals: * use directChatAvailable signal if you just need to obtain * a direct chat room. */ - void createdRoom(Room* room); + void createdRoom(Quotient::Room* room); /** The first sync for the room has been completed * @@ -726,7 +729,7 @@ signals: * signals (newRoom, joinedRoom etc.) come earlier, when the room * has just been created. */ - void loadedRoomState(Room* room); + void loadedRoomState(Quotient::Room* room); /** Account data (except direct chats) have changed */ void accountDataChanged(QString type); @@ -735,18 +738,18 @@ signals: * This signal is emitted upon any successful outcome from * requestDirectChat. */ - void directChatAvailable(Room* directChat); + void directChatAvailable(Quotient::Room* directChat); /** The list of direct chats has changed * This signal is emitted every time when the mapping of users * to direct chat rooms is changed (because of either local updates * or a different list arrived from the server). */ - void directChatsListChanged(DirectChatsMap additions, - DirectChatsMap removals); + void directChatsListChanged(Quotient::DirectChatsMap additions, + Quotient::DirectChatsMap removals); - void ignoredUsersListChanged(IgnoredUsersList additions, - IgnoredUsersList removals); + void ignoredUsersListChanged(Quotient::IgnoredUsersList additions, + Quotient::IgnoredUsersList removals); void cacheStateChanged(); void lazyLoadingChanged(); @@ -812,4 +815,5 @@ private: static user_factory_t _userFactory; }; } // namespace Quotient -Q_DECLARE_METATYPE(Quotient::Connection*) +Q_DECLARE_METATYPE(Quotient::DirectChatsMap) +Q_DECLARE_METATYPE(Quotient::IgnoredUsersList) 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/lib/jobs/basejob.h b/lib/jobs/basejob.h index 6c1b802c..c4da40f5 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -184,11 +184,11 @@ public: using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t std::chrono::seconds getCurrentTimeout() const; - Q_INVOKABLE duration_ms_t getCurrentTimeoutMs() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const; std::chrono::seconds getNextRetryInterval() const; - Q_INVOKABLE duration_ms_t getNextRetryMs() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const; std::chrono::milliseconds timeToRetry() const; - Q_INVOKABLE duration_ms_t millisToRetry() const; + Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const; friend QDebug operator<<(QDebug dbg, const BaseJob* j) { @@ -215,7 +215,7 @@ signals: void sentRequest(); /** The job has changed its status */ - void statusChanged(Status newStatus); + void statusChanged(Quotient::BaseJob::Status newStatus); /** * The previous network request has failed; the next attempt will @@ -225,7 +225,8 @@ signals: * @param inMilliseconds the interval after which the next attempt will be * taken */ - void retryScheduled(int nextAttempt, duration_ms_t inMilliseconds); + void retryScheduled(int nextAttempt, + Quotient::BaseJob::duration_ms_t inMilliseconds); /** * The previous network request has been rate-limited; the next attempt @@ -251,7 +252,7 @@ signals: * * @see result, success, failure */ - void finished(BaseJob* job); + void finished(Quotient::BaseJob* job); /** * Emitted when the job is finished (except when abandoned). @@ -262,14 +263,14 @@ signals: * * @see success, failure */ - void result(BaseJob* job); + void result(Quotient::BaseJob* job); /** * Emitted together with result() in case there's no error. * * @see result, failure */ - void success(BaseJob*); + void success(Quotient::BaseJob*); /** * Emitted together with result() if there's an error. @@ -277,7 +278,7 @@ signals: * * @see result, success */ - void failure(BaseJob*); + void failure(Quotient::BaseJob*); void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); void uploadProgress(qint64 bytesSent, qint64 bytesTotal); @@ -181,11 +181,11 @@ public: QString avatarMediaId() const; QUrl avatarUrl() const; const Avatar& avatarObject() const; - Q_INVOKABLE JoinState joinState() const; - Q_INVOKABLE QList<User*> usersTyping() const; + Q_INVOKABLE Quotient::JoinState joinState() const; + Q_INVOKABLE QList<Quotient::User*> usersTyping() const; QList<User*> membersLeft() const; - Q_INVOKABLE QList<User*> users() const; + Q_INVOKABLE QList<Quotient::User*> users() const; QStringList memberNames() const; [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; @@ -228,7 +228,7 @@ public: * \note The method will return a valid user regardless of * the membership. */ - Q_INVOKABLE User* user(const QString& userId) const; + Q_INVOKABLE Quotient::User* user(const QString& userId) const; /** * \brief Check the join state of a given user in this room @@ -236,16 +236,15 @@ public: * \note Banned and invited users are not tracked for now (Leave * will be returned for them). * - * \return either of Join, Leave, depending on the given - * user's state in the room + * \return Join if the user is a room member; Leave otherwise */ - Q_INVOKABLE JoinState memberJoinState(User* user) const; + Q_INVOKABLE Quotient::JoinState memberJoinState(Quotient::User* user) const; /** * Get a disambiguated name for a given user in * the context of the room */ - Q_INVOKABLE QString roomMembername(const User* u) const; + Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; /** * Get a disambiguated name for a user with this id in * the context of the room @@ -274,9 +273,10 @@ public: Timeline::const_iterator syncEdge() const; /// \deprecated Use historyEdge instead rev_iter_t timelineEdge() const; - Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; - Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; - Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; + Q_INVOKABLE Quotient::TimelineItem::index_t minTimelineIndex() const; + Q_INVOKABLE Quotient::TimelineItem::index_t maxTimelineIndex() const; + Q_INVOKABLE bool + isValidIndex(Quotient::TimelineItem::index_t timelineIndex) const; rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; @@ -414,7 +414,8 @@ public: * the event is even sent), while downloads are using * the normal event id for identifier. */ - Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + Q_INVOKABLE Quotient::FileTransferInfo + fileTransferInfo(const QString& id) const; /// Get the URL to the actual file source in a unified way /*! @@ -438,7 +439,7 @@ public: /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ - Q_INVOKABLE const StateEventBase* + Q_INVOKABLE const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; template <typename EvT> @@ -549,7 +550,8 @@ signals: /// The remote echo has arrived with the sync and will be merged /// with its local counterpart /** NB: Requires a sync loop to be emitted */ - void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex); + void pendingEventAboutToMerge(Quotient::RoomEvent* serverEvent, + int pendingEventIndex); /// The remote and local copies of the event have been merged /** NB: Requires a sync loop to be emitted */ void pendingEventMerged(); @@ -577,21 +579,21 @@ signals: * upon the last sync * \sa Changes */ - void changed(Changes changes); + void changed(Quotient::Room::Changes changes); /** * \brief The room name, the canonical alias or other aliases changed * * Not triggered when display name changes. */ - void namesChanged(Room* room); - void displaynameAboutToChange(Room* room); - void displaynameChanged(Room* room, QString oldName); + void namesChanged(Quotient::Room* room); + void displaynameAboutToChange(Quotient::Room* room); + void displaynameChanged(Quotient::Room* room, QString oldName); void topicChanged(); void avatarChanged(); - void userAdded(User* user); - void userRemoved(User* user); - void memberAboutToRename(User* user, QString newName); - void memberRenamed(User* user); + void userAdded(Quotient::User* user); + void userRemoved(Quotient::User* user); + void memberAboutToRename(Quotient::User* user, QString newName); + void memberRenamed(Quotient::User* user); /// The list of members has changed /** Emitted no more than once per sync, this is a good signal to * for cases when some action should be done upon any change in @@ -605,7 +607,8 @@ signals: void allMembersLoaded(); void encryption(); - void joinStateChanged(JoinState oldState, JoinState newState); + void joinStateChanged(Quotient::JoinState oldState, + Quotient::JoinState newState); void typingChanged(); void highlightCountChanged(); @@ -614,11 +617,11 @@ signals: void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); - void lastReadEventChanged(User* user); + void lastReadEventChanged(Quotient::User* user); void readMarkerMoved(QString fromEventId, QString toEventId); - void readMarkerForUserMoved(User* user, QString fromEventId, + void readMarkerForUserMoved(Quotient::User* user, QString fromEventId, QString toEventId); - void unreadMessagesChanged(Room* room); + void unreadMessagesChanged(Quotient::Room* room); void accountDataAboutToChange(QString type); void accountDataChanged(QString type); @@ -626,7 +629,8 @@ signals: void tagsChanged(); void updatedEvent(QString eventId); - void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); + void replacedEvent(const Quotient::RoomEvent* newEvent, + const Quotient::RoomEvent* oldEvent); void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); @@ -634,18 +638,18 @@ signals: void fileTransferFailed(QString id, QString errorMessage = {}); void fileTransferCancelled(QString id); - void callEvent(Room* room, const RoomEvent* event); + void callEvent(Quotient::Room* room, const Quotient::RoomEvent* event); /// The room's version stability may have changed void stabilityUpdated(QString recommendedDefault, QStringList stableVersions); /// This room has been upgraded and won't receive updates any more - void upgraded(QString serverMessage, Room* successor); + void upgraded(QString serverMessage, Quotient::Room* successor); /// An attempted room upgrade has failed void upgradeFailed(QString errorMessage); /// The room is about to be deleted - void beforeDestruction(Room*); + void beforeDestruction(Quotient::Room*); protected: virtual Changes processStateEvent(const RoomEvent& e); @@ -107,9 +107,10 @@ public: qreal hueF() const; const Avatar& avatarObject(const Room* room = nullptr) const; - Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); + Q_INVOKABLE QImage avatar(int dimension, + const Quotient::Room* room = nullptr); Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, - const Room* room = nullptr); + const Quotient::Room* room = nullptr); QImage avatar(int width, int height, const Room* room, const Avatar::get_callback_t& callback); @@ -145,9 +146,10 @@ public slots: signals: void nameAboutToChange(QString newName, QString oldName, - const Room* roomContext); - void nameChanged(QString newName, QString oldName, const Room* roomContext); - void avatarChanged(User* user, const Room* roomContext); + const Quotient::Room* roomContext); + void nameChanged(QString newName, QString oldName, + const Quotient::Room* roomContext); + void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext); private slots: void updateName(const QString& newName, const Room* room = nullptr); @@ -161,4 +163,3 @@ private: QScopedPointer<Private> d; }; } // namespace Quotient -Q_DECLARE_METATYPE(Quotient::User*) 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..490a2506 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,25 +45,25 @@ 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 Test) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) find_package(Quotient REQUIRED) -get_filename_component(QMC_Prefix "${Quotient_DIR}/../.." ABSOLUTE) +get_filename_component(Quotient_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) message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" ) -message( STATUS " Quotient: ${Quotient_VERSION} at ${QMC_Prefix}" ) +message( STATUS " Quotient: ${Quotient_VERSION} at ${Quotient_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 Qt5::Test 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..91f4f636 --- /dev/null +++ b/tests/quotest.cpp @@ -0,0 +1,665 @@ + +#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/redactionevent.h" +#include "events/simplestateevents.h" + +#include <QtTest/QSignalSpy> +#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 TestSuite; + +class TestManager : public QObject { +public: + TestManager(Connection* conn, QString testRoomName, QString source); + +private: + void setupAndRun(); + void onNewRoom(Room* r); + void doTests(); + void conclude(); + void finalize(); + +private: + QScopedPointer<Connection, QScopedPointerDeleteLater> c; + QString origin; + QString targetRoomName; + 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); + +private: + Connection* targetConn; + QString targetRoomAlias; + QString origin; + Room* targetRoom = nullptr; +}; + +#define TEST_IMPL(Name) bool TestSuite::Name(const TestToken& thisTest) + +// 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) + +#define FAIL_TEST() FINISH_TEST(false) + +bool TestSuite::validatePendingEvent(const QString& txnId) +{ + auto it = targetRoom->findPendingEvent(txnId); + return it != targetRoom->pendingEvents().end() + && it->deliveryStatus() == EventStatus::Submitted + && (*it)->transactionId() == txnId; +} + +void TestSuite::finishTest(const TestToken& token, bool condition, + const char* file, int line) +{ + const auto& item = testName(token); + if (condition) { + cout << item << " successful" << endl; + if (targetRoom) + targetRoom->postMessage(origin % ": " % item % " successful", + MessageEventType::Notice); + } else { + 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) + : 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; + + c->setLazyLoading(true); + c->syncLoop(); + connectSingleShot(c.data(), &Connection::syncDone, this, + &TestManager::doTests); +} + +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::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, "; + 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; + }); +} + +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 + 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(); + } + // 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(); + } + r->setDisplayed(); + connect(r, &Room::allMembersLoaded, [this, thisTest, r] { + FINISH_TEST(r->memberNames().size() >= r->joinedCount()); + }); + return false; +} + +TEST_IMPL(sendMessage) +{ + auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); + if (!validatePendingEvent(txnId)) { + cout << "Invalid pending event right after submitting" << endl; + FAIL_TEST(); + } + 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(is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == evt->transactionId()); + }); + return false; +} + +TEST_IMPL(sendReaction) +{ + 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(); + } + + // TODO: Check that it came back as a reaction event and that it attached to + // the right event + connectUntil( + targetRoom, &Room::updatedEvent, this, + [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(); + + const auto* evt = + eventCast<const ReactionEvent>(reactions.back()); + FINISH_TEST(is<ReactionEvent>(*evt) && !evt->id().isEmpty() + && evt->relation().key == key + && evt->transactionId() == txnId); + }); + return false; +} + +TEST_IMPL(sendFile) +{ + auto* tf = new QTemporaryFile; + if (!tf->open()) { + cout << "Failed to create a temporary file" << endl; + FAIL_TEST(); + } + 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(); + } + + // FIXME: Clean away connections (connectUntil doesn't help here). + connectUntil(targetRoom, &Room::fileTransferCompleted, this, + [this, thisTest, txnId, tf, tfName](const QString& id) { + auto fti = targetRoom->fileTransferInfo(id); + Q_ASSERT(fti.status == FileTransferInfo::Completed); + + if (id != txnId) + return false; + + 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; + + targetRoom->postPlainText(origin % ": File upload failed: " % error); + delete tf; + FAIL_TEST(); + }); + return false; +} + +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(); + } + if (it->deliveryStatus() != EventStatus::FileUploaded) { + cout << "Pending file event status upon upload completion is " + << it->deliveryStatus() << " != FileUploaded(" + << EventStatus::FileUploaded << ')' << endl; + FAIL_TEST(); + } + + 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())); + + if (evt->transactionId() != txnId) + return false; + + cout << "File event " << txnId.toStdString() + << " arrived in the timeline" << endl; + // This part tests visit() + return visit( + *evt, + [&](const RoomMessageEvent& e) { + FINISH_TEST( + !e.id().isEmpty() + && pendingEvents[size_t(pendingIdx)]->transactionId() + == txnId + && e.hasFileContent() + && e.content()->fileInfo()->originalName == fileName); + }, + [this, thisTest](const RoomEvent&) { FAIL_TEST(); }); + }); + return true; +} + +TEST_IMPL(setTopic) +{ + 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); + + cout << "Requested topic was " << newTopic.toStdString() << ", " + << targetRoom->topic().toStdString() << " arrived instead" + << endl; + return false; + }); + return false; +} + +TEST_IMPL(sendAndRedact) +{ + cout << "Sending a message to redact" << endl; + auto txnId = targetRoom->postPlainText(origin % ": message to redact"); + if (txnId.isEmpty()) + FAIL_TEST(); + + connect(targetRoom, &Room::messageSent, this, + [this, thisTest, 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, thisTest, evtId] { + return checkRedactionOutcome(thisTest, evtId); + }); + }); + return false; +} + +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 + // 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(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) +{ + static const auto TestTag = QStringLiteral("im.quotient.test"); + // Pre-requisite + if (targetRoom->tags().contains(TestTag)) + targetRoom->removeTag(TestTag); + + // 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)); +} + +bool TestSuite::checkDirectChat() const +{ + return targetRoom->directChatUsers().contains(connection()->user()); +} + +TEST_IMPL(markDirectChat) +{ + if (checkDirectChat()) + connection()->removeFromDirectChats(targetRoom->id(), + connection()->user()); + + int id = qRegisterMetaType<DirectChatsMap>(); // 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<DirectChatsMap>(); + 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; + 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<DirectChatsMap>(); + FINISH_TEST(removedDCs.size() == 1 + && removedDCs.contains(connection()->user(), 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()) { + QByteArray failedList; + for (const auto& f : failed) + failedList += ' ' + f; + plainReport += "\nFAILED:" + failedList; + htmlReport += "<br><strong>Failed:</strong>" + failedList; + } + if (!running.empty()) { + QByteArray dnfList; + for (const auto& r : running) + dnfList += ' ' + r; + plainReport += "\nDID NOT FINISH:" + dnfList; + htmlReport += "<br><strong>Did not finish:</strong>" + dnfList; + } + cout << plainReport.toStdString() << endl; + + // 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(); + }); + }); +} + +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(); +} + +#include "quotest.moc" |