aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml14
-rw-r--r--CMakeLists.txt18
-rw-r--r--lib/connection.cpp6
-rw-r--r--lib/connection.h82
-rw-r--r--lib/csapi/gtad.yaml4
-rw-r--r--lib/jobs/basejob.h19
-rw-r--r--lib/room.h64
-rw-r--r--lib/user.h13
-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.cpp665
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);
diff --git a/lib/room.h b/lib/room.h
index cded7eb9..80e305f0 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -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);
diff --git a/lib/user.h b/lib/user.h
index c9e3dbc1..28ec841b 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -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"