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