From 1f30224578136434055783906f6224003bf8b82d Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 4 Mar 2019 13:58:01 +0100 Subject: [PATCH] Add support for creating promises from Qt signals (#25) Introduce a new `QtPromise::connect()` helper that allows to create a promise resolved from a single signal and optionally, rejected by another one (from a different object or not). The promise type is determined by the type of the first signal argument (other arguments are currently ignored). A `QPromise` is returned if the resolve signal doesn't provide any argument. If the rejection is emitted before the promise is resolved, the promise will be rejected with the value of the first argument (other arguments being ignored). If the rejection signal doesn't provide any argument, the promise will be rejected with `QPromiseUndefinedException` if the signal is emitted. Additionally, the promise will be automatically rejected with `QPromiseContextException` if the source object is destroyed before the promise is resolved. --- README.md | 3 +- docs/.vuepress/config.js | 3 + docs/qtpromise/api-reference.md | 1 + docs/qtpromise/exceptions/context.md | 17 ++ docs/qtpromise/helpers/connect.md | 48 ++++ docs/qtpromise/qtconcurrent.md | 2 +- docs/qtpromise/qtsignals.md | 89 ++++++++ include/QtPromise | 1 + src/qtpromise/qpromiseconnections.h | 48 ++++ src/qtpromise/qpromiseexceptions.h | 10 + src/qtpromise/qpromisehelpers.h | 39 ++++ src/qtpromise/qpromisehelpers_p.h | 91 ++++++++ src/qtpromise/qtpromise.pri | 2 + .../qtpromise/exceptions/tst_exceptions.cpp | 6 + .../qtpromise/helpers/connect/connect.pro | 4 + .../qtpromise/helpers/connect/tst_connect.cpp | 211 ++++++++++++++++++ tests/auto/qtpromise/helpers/helpers.pro | 1 + .../qpromiseconnections.pro | 4 + .../tst_qpromiseconnections.cpp | 81 +++++++ tests/auto/qtpromise/qtpromise.pri | 1 + tests/auto/qtpromise/qtpromise.pro | 1 + tests/auto/qtpromise/shared/object.h | 25 +++ 22 files changed, 686 insertions(+), 2 deletions(-) create mode 100644 docs/qtpromise/exceptions/context.md create mode 100644 docs/qtpromise/helpers/connect.md create mode 100644 docs/qtpromise/qtsignals.md create mode 100644 src/qtpromise/qpromiseconnections.h create mode 100644 src/qtpromise/qpromisehelpers_p.h create mode 100644 tests/auto/qtpromise/helpers/connect/connect.pro create mode 100644 tests/auto/qtpromise/helpers/connect/tst_connect.cpp create mode 100644 tests/auto/qtpromise/qpromiseconnections/qpromiseconnections.pro create mode 100644 tests/auto/qtpromise/qpromiseconnections/tst_qpromiseconnections.cpp create mode 100644 tests/auto/qtpromise/shared/object.h diff --git a/README.md b/README.md index 90b6b6e..63313cc 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Requires [Qt 5.6](https://www.qt.io/download/) (or later) with [C++11 support en ## Documentation * [Getting Started](https://qtpromise.netlify.com/qtpromise/getting-started.html) -* [QtConcurrent](https://qtpromise.netlify.com/qtpromise/qtconcurrent.html) +* [Qt Concurrent](https://qtpromise.netlify.com/qtpromise/qtconcurrent.html) +* [Qt Signals](https://qtpromise.netlify.com/qtpromise/qtsignals.html) * [Thread-Safety](https://qtpromise.netlify.com/qtpromise/thread-safety.html) * [API Reference](https://qtpromise.netlify.com/qtpromise/api-reference.html) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7996f50..d195861 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -17,6 +17,7 @@ module.exports = { sidebar: [ 'qtpromise/getting-started', 'qtpromise/qtconcurrent', + 'qtpromise/qtsignals', 'qtpromise/thread-safety', 'qtpromise/api-reference', { @@ -46,6 +47,7 @@ module.exports = { title: 'Helpers', children: [ 'qtpromise/helpers/attempt', + 'qtpromise/helpers/connect', 'qtpromise/helpers/each', 'qtpromise/helpers/filter', 'qtpromise/helpers/map', @@ -57,6 +59,7 @@ module.exports = { title: 'Exceptions', children: [ 'qtpromise/exceptions/canceled', + 'qtpromise/exceptions/context', 'qtpromise/exceptions/timeout', 'qtpromise/exceptions/undefined' ] diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md index 02afa58..f2429dc 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -27,6 +27,7 @@ ## Helpers * [`QtPromise::attempt`](helpers/attempt.md) +* [`QtPromise::connect`](helpers/connect.md) * [`QtPromise::each`](helpers/each.md) * [`QtPromise::filter`](helpers/filter.md) * [`QtPromise::map`](helpers/map.md) diff --git a/docs/qtpromise/exceptions/context.md b/docs/qtpromise/exceptions/context.md new file mode 100644 index 0000000..64d904d --- /dev/null +++ b/docs/qtpromise/exceptions/context.md @@ -0,0 +1,17 @@ +--- +title: QPromiseContextException +--- + +# QPromiseContextException + +*Since: 0.5.0* + +When a promise is created using [`QtPromise::connect()`](../helpers/connect.md), this exception is thrown when the `sender` object is destroyed, for example: + +```cpp +auto promise = QtPromise::connect(sender, &Object::finished, &Object::error); + +promise.fail([](const QPromiseContextException&) { + // 'sender' has been destroyed. +}) +``` diff --git a/docs/qtpromise/helpers/connect.md b/docs/qtpromise/helpers/connect.md new file mode 100644 index 0000000..3b4e8f0 --- /dev/null +++ b/docs/qtpromise/helpers/connect.md @@ -0,0 +1,48 @@ +--- +title: connect +--- + +# QtPromise::connect + +*Since: 0.5.0* + +```cpp +(1) QtPromise::connect(QObject* sender, Signal(T) resolver) -> QPromise +(2) QtPromise::connect(QObject* sender, Signal(T) resolver, Signal(R) rejecter) -> QPromise +(3) QtPromise::connect(QObject* sender, Signal(T) resolver, QObject* sender2, Signal(R) rejecter) -> QPromise +``` + +Creates a `QPromise` that will be fulfilled with the `resolver` signal's first argument, or a `QPromise` if `resolver` doesn't provide any argument. + +The second `(2)` and third `(3)` variants of this method will reject the `output` promise when the `rejecter` signal is emitted. The rejection reason is the value of the `rejecter` signal's first argument or [`QPromiseUndefinedException`](../exceptions/undefined) if `rejected` doesn't provide any argument. + +Additionally, the `output` promise will be automatically rejected with [`QPromiseContextException`](../exceptions/context.md) if `sender` is destroyed before the promise is resolved (that doesn't apply to `sender2`). + +```cpp +class Sender : public QObject +{ +Q_SIGNALS: + void finished(const QByteArray&); + void error(ErrorCode); +}; + +auto sender = new Sender(); +auto output = QtPromise::connect(sender, &Sender::finished, &Sender::error); + +// 'output' resolves as soon as one of the following events happens: +// - the 'sender' object is destroyed, the promise is rejected +// - the 'finished' signal is emitted, the promise is fulfilled +// - the 'error' signal is emitted, the promise is rejected + +// 'output' type: QPromise +output.then([](const QByteArray& res) { + // 'res' is the first argument of the 'finished' signal. +}).fail([](ErrorCode err) { + // 'err' is the first argument of the 'error' signal. +}).fail([](const QPromiseContextException& err) { + // the 'sender' object has been destroyed before any of + // the 'finished' or 'error' signals have been emitted. +}); +``` + +See also the [`Qt Signals`](../qtsignals.md) section for more examples. diff --git a/docs/qtpromise/qtconcurrent.md b/docs/qtpromise/qtconcurrent.md index f8858fa..4bb4ea1 100644 --- a/docs/qtpromise/qtconcurrent.md +++ b/docs/qtpromise/qtconcurrent.md @@ -1,4 +1,4 @@ -# QtConcurrent +# Qt Concurrent QtPromise integrates with [QtConcurrent](https://doc.qt.io/qt-5/qtconcurrent-index.html) to make easy chaining QFuture with QPromise. diff --git a/docs/qtpromise/qtsignals.md b/docs/qtpromise/qtsignals.md new file mode 100644 index 0000000..763ec74 --- /dev/null +++ b/docs/qtpromise/qtsignals.md @@ -0,0 +1,89 @@ +# Qt Signals + +QtPromise supports creating promises that are resolved or rejected by regular [Qt signals](https://doc.qt.io/qt-5/signalsandslots.html). + +::: warning IMPORTANT +A promise connected to a signal will be resolved (fulfilled or rejected) **only one time**, no matter if the signals are emitted multiple times. Internally, the promise is disconnected from all signals as soon as one signal is emitted. +::: + +## Resolve Signal + +The [`QtPromise::connect()`](helpers/connect.md) helper allows to create a promise resolved from a single signal: + +```cpp +// [signal] Object::finished(const QByteArray&) +auto output = QtPromise::connect(obj, &Object::finished); + +// output type: QPromise +output.then([](const QByteArray& data) { + // {...} +}); +``` + +If the signal doesn't provide any argument, a `QPromise` is returned: + +```cpp +// [signal] Object::done() +auto output = QtPromise::connect(obj, &Object::done); + +// output type: QPromise +output.then([]() { + // {...} +}); +``` + +::: tip NOTE +QtPromise currently only supports single argument signals, which means that only the first argument is used to fulfill or reject the connected promise, other arguments being ignored. +::: + +## Reject Signal + +The [`QtPromise::connect()`](helpers/connect.md) helper also allows to reject the promise from another signal: + +```cpp +// [signal] Object::finished(const QByteArray& data) +// [signal] Object::error(ObjectError error) +auto output = QtPromise::connect(obj, &Object::finished, &Object::error); + +// output type: QPromise +output.then([](const QByteArray& data) { + // {...} +}).fail(const ObjectError& error) { + // {...} +}); +``` + +If the rejection signal doesn't provide any argument, the promise will be rejected +with [`QPromiseUndefinedException`](../exceptions/undefined), for example: + +```cpp +// [signal] Object::finished() +// [signal] Object::error() +auto output = QtPromise::connect(obj, &Object::finished, &Object::error); + +// output type: QPromise +output.then([]() { + // {...} +}).fail(const QPromiseUndefinedException& error) { + // {...} +}); +``` + +A third variant allows to connect the resolve and reject signals from different objects: + +```cpp +// [signal] ObjectA::finished(const QByteArray& data) +// [signal] ObjectB::error(ObjectBError error) +auto output = QtPromise::connect(objA, &ObjectA::finished, objB, &ObjectB::error); + +// output type: QPromise +output.then([](const QByteArray& data) { + // {...} +}).fail(const ObjectBError& error) { + // {...} +}); +``` + +Additionally to the rejection signal, promises created using [`QtPromise::connect()`](helpers/connect.md) are automatically rejected with [`QPromiseContextException`](exceptions/context.md) if the sender is destroyed before fulfilling the promise. + +See [`QtPromise::connect()`](helpers/connect.md) for more details. diff --git a/include/QtPromise b/include/QtPromise index 658168a..64a999c 100644 --- a/include/QtPromise +++ b/include/QtPromise @@ -2,6 +2,7 @@ #define QTPROMISE_MODULE_H #include "../src/qtpromise/qpromise.h" +#include "../src/qtpromise/qpromiseconnections.h" #include "../src/qtpromise/qpromisefuture.h" #include "../src/qtpromise/qpromisehelpers.h" diff --git a/src/qtpromise/qpromiseconnections.h b/src/qtpromise/qpromiseconnections.h new file mode 100644 index 0000000..827e870 --- /dev/null +++ b/src/qtpromise/qpromiseconnections.h @@ -0,0 +1,48 @@ +#ifndef QTPROMISE_QPROMISECONNECTIONS_H +#define QTPROMISE_QPROMISECONNECTIONS_H + +// Qt +#include + +namespace QtPromise { + +class QPromiseConnections +{ +public: + QPromiseConnections() : m_d(new Data()) { } + + int count() const { return m_d->connections.count(); } + + void disconnect() const { m_d->disconnect(); } + + void operator<<(QMetaObject::Connection&& other) const + { + m_d->connections.append(std::move(other)); + } + +private: + struct Data + { + QVector connections; + + ~Data() { + if (!connections.empty()) { + qWarning("QPromiseConnections: destroyed with unhandled connections."); + disconnect(); + } + } + + void disconnect() { + for (const auto& connection: connections) { + QObject::disconnect(connection); + } + connections.clear(); + } + }; + + QSharedPointer m_d; +}; + +} // namespace QtPromise + +#endif // QTPROMISE_QPROMISECONNECTIONS_H diff --git a/src/qtpromise/qpromiseexceptions.h b/src/qtpromise/qpromiseexceptions.h index bbfaf5b..18dedad 100644 --- a/src/qtpromise/qpromiseexceptions.h +++ b/src/qtpromise/qpromiseexceptions.h @@ -19,6 +19,16 @@ public: } }; +class QPromiseContextException : public QException +{ +public: + void raise() const Q_DECL_OVERRIDE { throw *this; } + QPromiseContextException* clone() const Q_DECL_OVERRIDE + { + return new QPromiseContextException(*this); + } +}; + class QPromiseTimeoutException : public QException { public: diff --git a/src/qtpromise/qpromisehelpers.h b/src/qtpromise/qpromisehelpers.h index 85f3b1a..c1fc0a5 100644 --- a/src/qtpromise/qpromisehelpers.h +++ b/src/qtpromise/qpromisehelpers.h @@ -2,6 +2,7 @@ #define QTPROMISE_QPROMISEHELPERS_H #include "qpromise_p.h" +#include "qpromisehelpers_p.h" namespace QtPromise { @@ -63,6 +64,44 @@ attempt(Functor&& fn, Args&&... args) }); } +template +static inline typename QtPromisePrivate::PromiseFromSignal +connect(const Sender* sender, Signal signal) +{ + using namespace QtPromisePrivate; + using T = typename PromiseFromSignal::Type; + + return QPromise( + [&](const QPromiseResolve& resolve, const QPromiseReject& reject) { + QPromiseConnections connections; + connectSignalToResolver(connections, resolve, sender, signal); + connectDestroyedToReject(connections, reject, sender); + }); +} + +template +static inline typename QtPromisePrivate::PromiseFromSignal +connect(const FSender* fsender, FSignal fsignal, const RSender* rsender, RSignal rsignal) +{ + using namespace QtPromisePrivate; + using T = typename PromiseFromSignal::Type; + + return QPromise( + [&](const QPromiseResolve& resolve, const QPromiseReject& reject) { + QPromiseConnections connections; + connectSignalToResolver(connections, resolve, fsender, fsignal); + connectSignalToResolver(connections, reject, rsender, rsignal); + connectDestroyedToReject(connections, reject, fsender); + }); +} + +template +static inline typename QtPromisePrivate::PromiseFromSignal +connect(const Sender* sender, FSignal fsignal, RSignal rsignal) +{ + return connect(sender, fsignal, sender, rsignal); +} + template static inline QPromise each(const Sequence& values, Functor&& fn) { diff --git a/src/qtpromise/qpromisehelpers_p.h b/src/qtpromise/qpromisehelpers_p.h new file mode 100644 index 0000000..d10221a --- /dev/null +++ b/src/qtpromise/qpromisehelpers_p.h @@ -0,0 +1,91 @@ +#ifndef QTPROMISE_QPROMISEHELPERS_P_H +#define QTPROMISE_QPROMISEHELPERS_P_H + +#include "qpromiseconnections.h" +#include "qpromiseexceptions.h" + +namespace QtPromisePrivate { + +// TODO: Suppress QPrivateSignal trailing private signal args +// TODO: Support deducing tuple from args (might require MSVC2017) + +template +using PromiseFromSignal = typename QtPromise::QPromise::first>>; + +// Connect signal() to QPromiseResolve +template +typename std::enable_if<(ArgsOf::count == 0)>::type +connectSignalToResolver( + const QtPromise::QPromiseConnections& connections, + const QtPromise::QPromiseResolve& resolve, + const Sender* sender, + Signal signal) +{ + connections << QObject::connect(sender, signal, [=]() { + connections.disconnect(); + resolve(); + }); +} + +// Connect signal() to QPromiseReject +template +typename std::enable_if<(ArgsOf::count == 0)>::type +connectSignalToResolver( + const QtPromise::QPromiseConnections& connections, + const QtPromise::QPromiseReject& reject, + const Sender* sender, + Signal signal) +{ + connections << QObject::connect(sender, signal, [=]() { + connections.disconnect(); + reject(QtPromise::QPromiseUndefinedException()); + }); +} + +// Connect signal(args...) to QPromiseResolve +template +typename std::enable_if<(ArgsOf::count >= 1)>::type +connectSignalToResolver( + const QtPromise::QPromiseConnections& connections, + const QtPromise::QPromiseResolve& resolve, + const Sender* sender, + Signal signal) +{ + connections << QObject::connect(sender, signal, [=](const T& value) { + connections.disconnect(); + resolve(value); + }); +} + +// Connect signal(args...) to QPromiseReject +template +typename std::enable_if<(ArgsOf::count >= 1)>::type +connectSignalToResolver( + const QtPromise::QPromiseConnections& connections, + const QtPromise::QPromiseReject& reject, + const Sender* sender, + Signal signal) +{ + using V = Unqualified::first>; + connections << QObject::connect(sender, signal, [=](const V& value) { + connections.disconnect(); + reject(value); + }); +} + +// Connect QObject::destroyed signal to QPromiseReject +template +void connectDestroyedToReject( + const QtPromise::QPromiseConnections& connections, + const QtPromise::QPromiseReject& reject, + const Sender* sender) +{ + connections << QObject::connect(sender, &QObject::destroyed, [=]() { + connections.disconnect(); + reject(QtPromise::QPromiseContextException()); + }); +} + +} // namespace QtPromisePrivate + +#endif // QTPROMISE_QPROMISEHELPERS_P_H diff --git a/src/qtpromise/qtpromise.pri b/src/qtpromise/qtpromise.pri index 1d53240..8ed377f 100644 --- a/src/qtpromise/qtpromise.pri +++ b/src/qtpromise/qtpromise.pri @@ -2,8 +2,10 @@ HEADERS += \ $$PWD/qpromise.h \ $$PWD/qpromise.inl \ $$PWD/qpromise_p.h \ + $$PWD/qpromiseconnections.h \ $$PWD/qpromiseexceptions.h \ $$PWD/qpromisefuture.h \ $$PWD/qpromiseglobal.h \ $$PWD/qpromisehelpers.h \ + $$PWD/qpromisehelpers_p.h \ $$PWD/qpromiseresolver.h diff --git a/tests/auto/qtpromise/exceptions/tst_exceptions.cpp b/tests/auto/qtpromise/exceptions/tst_exceptions.cpp index 3fde13b..358f35e 100644 --- a/tests/auto/qtpromise/exceptions/tst_exceptions.cpp +++ b/tests/auto/qtpromise/exceptions/tst_exceptions.cpp @@ -15,6 +15,7 @@ class tst_exceptions : public QObject private Q_SLOTS: void canceled(); + void context(); void timeout(); void undefined(); @@ -41,6 +42,11 @@ void tst_exceptions::canceled() verify(); } +void tst_exceptions::context() +{ + verify(); +} + void tst_exceptions::timeout() { verify(); diff --git a/tests/auto/qtpromise/helpers/connect/connect.pro b/tests/auto/qtpromise/helpers/connect/connect.pro new file mode 100644 index 0000000..74bae78 --- /dev/null +++ b/tests/auto/qtpromise/helpers/connect/connect.pro @@ -0,0 +1,4 @@ +TARGET = tst_helpers_connect +SOURCES += $$PWD/tst_connect.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/helpers/connect/tst_connect.cpp b/tests/auto/qtpromise/helpers/connect/tst_connect.cpp new file mode 100644 index 0000000..0805be6 --- /dev/null +++ b/tests/auto/qtpromise/helpers/connect/tst_connect.cpp @@ -0,0 +1,211 @@ +#include "../../shared/object.h" +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_helpers_connect : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + // connect(QObject* sender, Signal resolver) + void resolveOneSenderNoArg(); + void resolveOneSenderOneArg(); + void resolveOneSenderManyArgs(); + + // connect(QObject* sender, Signal resolver, Signal rejecter) + void rejectOneSenderNoArg(); + void rejectOneSenderOneArg(); + void rejectOneSenderManyArgs(); + void rejectOneSenderDestroyed(); + + // connect(QObject* s0, Signal resolver, QObject* s1, Signal rejecter) + void rejectTwoSendersNoArg(); + void rejectTwoSendersOneArg(); + void rejectTwoSendersManyArgs(); + void rejectTwoSendersDestroyed(); +}; + +QTEST_MAIN(tst_helpers_connect) +#include "tst_connect.moc" + +void tst_helpers_connect::resolveOneSenderNoArg() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.noArgSignal(); + }); + + auto p = QtPromise::connect(&sender, &Object::noArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, -1, 42), 42); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::resolveOneSenderOneArg() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.oneArgSignal("foo"); + }); + + auto p = QtPromise::connect(&sender, &Object::oneArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, QString()), QString("foo")); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::resolveOneSenderManyArgs() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.twoArgsSignal(42, "foo"); + }); + + auto p = QtPromise::connect(&sender, &Object::twoArgsSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, -1), 42); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::rejectOneSenderNoArg() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.noArgSignal(); + }); + + auto p = QtPromise::connect(&sender, &Object::oneArgSignal, &Object::noArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForRejected(p), true); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::rejectOneSenderOneArg() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.oneArgSignal("bar"); + }); + + auto p = QtPromise::connect(&sender, &Object::noArgSignal, &Object::oneArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForError(p, QString()), QString("bar")); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::rejectOneSenderManyArgs() +{ + Object sender; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT sender.twoArgsSignal(42, "bar"); + }); + + auto p = QtPromise::connect(&sender, &Object::noArgSignal, &Object::twoArgsSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForError(p, -1), 42); + QCOMPARE(sender.hasConnections(), false); +} + +void tst_helpers_connect::rejectOneSenderDestroyed() +{ + Object* sender = new Object(); + QtPromisePrivate::qtpromise_defer([&]() { + sender->deleteLater(); + }); + + auto p = QtPromise::connect(sender, &Object::twoArgsSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForRejected(p), true); +} + +void tst_helpers_connect::rejectTwoSendersNoArg() +{ + Object s0, s1; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT s1.noArgSignal(); + }); + + auto p = QtPromise::connect(&s0, &Object::noArgSignal, &s1, &Object::noArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(s0.hasConnections(), true); + QCOMPARE(s1.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForRejected(p), true); + QCOMPARE(s0.hasConnections(), false); + QCOMPARE(s1.hasConnections(), false); +} + +void tst_helpers_connect::rejectTwoSendersOneArg() +{ + Object s0, s1; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT s1.oneArgSignal("bar"); + }); + + auto p = QtPromise::connect(&s0, &Object::noArgSignal, &s1, &Object::oneArgSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(s0.hasConnections(), true); + QCOMPARE(s1.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForError(p, QString()), QString("bar")); + QCOMPARE(s0.hasConnections(), false); + QCOMPARE(s1.hasConnections(), false); +} + +void tst_helpers_connect::rejectTwoSendersManyArgs() +{ + Object s0, s1; + QtPromisePrivate::qtpromise_defer([&]() { + Q_EMIT s1.twoArgsSignal(42, "bar"); + }); + + auto p = QtPromise::connect(&s0, &Object::noArgSignal, &s1, &Object::twoArgsSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(s0.hasConnections(), true); + QCOMPARE(s1.hasConnections(), true); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForError(p, -1), 42); + QCOMPARE(s0.hasConnections(), false); + QCOMPARE(s1.hasConnections(), false); +} + +void tst_helpers_connect::rejectTwoSendersDestroyed() +{ + Object* s0 = new Object(); + Object* s1 = new Object(); + + QtPromisePrivate::qtpromise_defer([&]() { + QObject::connect(s1, &QObject::destroyed, [&]() { + // Let's first delete s1, then resolve s0 and make sure + // we don't reject when the rejecter object is destroyed. + Q_EMIT s0->noArgSignal(); + }); + + s1->deleteLater(); + }); + + auto p = QtPromise::connect(s0, &Object::noArgSignal, s1, &Object::twoArgsSignal); + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, -1, 42), 42); +} diff --git a/tests/auto/qtpromise/helpers/helpers.pro b/tests/auto/qtpromise/helpers/helpers.pro index 6c1b222..e7a7bac 100644 --- a/tests/auto/qtpromise/helpers/helpers.pro +++ b/tests/auto/qtpromise/helpers/helpers.pro @@ -2,6 +2,7 @@ TEMPLATE = subdirs SUBDIRS += \ all \ attempt \ + connect \ each \ filter \ map \ diff --git a/tests/auto/qtpromise/qpromiseconnections/qpromiseconnections.pro b/tests/auto/qtpromise/qpromiseconnections/qpromiseconnections.pro new file mode 100644 index 0000000..99f5e72 --- /dev/null +++ b/tests/auto/qtpromise/qpromiseconnections/qpromiseconnections.pro @@ -0,0 +1,4 @@ +TARGET = tst_qpromiseconnections +SOURCES += $$PWD/tst_qpromiseconnections.cpp + +include(../qtpromise.pri) diff --git a/tests/auto/qtpromise/qpromiseconnections/tst_qpromiseconnections.cpp b/tests/auto/qtpromise/qpromiseconnections/tst_qpromiseconnections.cpp new file mode 100644 index 0000000..59ed57e --- /dev/null +++ b/tests/auto/qtpromise/qpromiseconnections/tst_qpromiseconnections.cpp @@ -0,0 +1,81 @@ +#include "../shared/object.h" +#include "../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_qpromiseconnections : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void connections(); + void destruction(); + void senderDestroyed(); + +}; // class tst_qpromiseconnections + +QTEST_MAIN(tst_qpromiseconnections) +#include "tst_qpromiseconnections.moc" + +void tst_qpromiseconnections::connections() +{ + Object sender; + + QPromiseConnections connections; + QCOMPARE(sender.hasConnections(), false); + QCOMPARE(connections.count(), 0); + + connections << connect(&sender, &Object::noArgSignal, [=]() {}); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(connections.count(), 1); + + connections << connect(&sender, &Object::twoArgsSignal, [=]() {}); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(connections.count(), 2); + + connections.disconnect(); + QCOMPARE(sender.hasConnections(), false); + QCOMPARE(connections.count(), 0); +} + +void tst_qpromiseconnections::destruction() +{ + Object sender; + + { + QPromiseConnections connections; + QCOMPARE(sender.hasConnections(), false); + QCOMPARE(connections.count(), 0); + + connections << connect(&sender, &Object::noArgSignal, [=]() {}); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(connections.count(), 1); + } + + QCOMPARE(sender.hasConnections(), false); +} + +void tst_qpromiseconnections::senderDestroyed() +{ + QPromiseConnections connections; + QCOMPARE(connections.count(), 0); + + { + Object sender; + QCOMPARE(sender.hasConnections(), false); + + connections << connect(&sender, &Object::noArgSignal, [=]() {}); + QCOMPARE(sender.hasConnections(), true); + QCOMPARE(connections.count(), 1); + } + + // should not throw + connections.disconnect(); + QCOMPARE(connections.count(), 0); +} diff --git a/tests/auto/qtpromise/qtpromise.pri b/tests/auto/qtpromise/qtpromise.pri index b5ad5f5..f3cc18a 100644 --- a/tests/auto/qtpromise/qtpromise.pri +++ b/tests/auto/qtpromise/qtpromise.pri @@ -21,6 +21,7 @@ coverage { } HEADERS += \ + $$PWD/shared/object.h \ $$PWD/shared/utils.h include(../../../qtpromise.pri) diff --git a/tests/auto/qtpromise/qtpromise.pro b/tests/auto/qtpromise/qtpromise.pro index 24d7c89..f48c492 100644 --- a/tests/auto/qtpromise/qtpromise.pro +++ b/tests/auto/qtpromise/qtpromise.pro @@ -5,5 +5,6 @@ SUBDIRS += \ future \ helpers \ qpromise \ + qpromiseconnections \ requirements \ thread diff --git a/tests/auto/qtpromise/shared/object.h b/tests/auto/qtpromise/shared/object.h new file mode 100644 index 0000000..334e2ea --- /dev/null +++ b/tests/auto/qtpromise/shared/object.h @@ -0,0 +1,25 @@ +#ifndef QTPROMISE_TESTS_AUTO_SHARED_SENDER_H +#define QTPROMISE_TESTS_AUTO_SHARED_SENDER_H + +// Qt +#include + +class Object : public QObject +{ + Q_OBJECT + +public: + bool hasConnections() const { return m_connections > 0; } + +Q_SIGNALS: + void noArgSignal(); + void oneArgSignal(const QString& v); + void twoArgsSignal(int v1, const QString& v0); + +protected: + int m_connections = 0; + void connectNotify(const QMetaMethod&) Q_DECL_OVERRIDE { ++m_connections; } + void disconnectNotify(const QMetaMethod&) Q_DECL_OVERRIDE { --m_connections; } +}; + +#endif // ifndef QTPROMISE_TESTS_AUTO_SHARED_SENDER_H