commit 6a642446dfde1030127b7c1c1e87f559c6262f67 Author: Simon Brunel Date: Sun May 14 19:03:01 2017 +0200 Initial implementation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7a4d4d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/*.user diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e1f86a6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +**The MIT License (MIT)** + +Copyright (c) 2017 Simon Brunel + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..486f58d --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Promises/A+ + +# QtPromise +[Promises/A+](https://promisesaplus.com/) implementation for [Qt](https://www.qt.io/). + +## License +QtPromise is available under the [MIT license](LICENSE.md). diff --git a/package/features/qtpromise.prf b/package/features/qtpromise.prf new file mode 100644 index 0000000..db70101 --- /dev/null +++ b/package/features/qtpromise.prf @@ -0,0 +1 @@ +include(../../qtpromise.pri) diff --git a/qtpromise.pri b/qtpromise.pri new file mode 100644 index 0000000..6247160 --- /dev/null +++ b/qtpromise.pri @@ -0,0 +1,2 @@ +INCLUDEPATH += $$PWD/src +DEPENDPATH += $$PWD/src diff --git a/qtpromise.pro b/qtpromise.pro new file mode 100644 index 0000000..f594992 --- /dev/null +++ b/qtpromise.pro @@ -0,0 +1,10 @@ +TEMPLATE = subdirs +SUBDIRS = \ + src \ + tests + +tests.depends = src + +OTHER_FILES = \ + package/features/*.prf \ + qtpromise.pri diff --git a/src/qtpromise/qpromise.h b/src/qtpromise/qpromise.h new file mode 100644 index 0000000..fb80f15 --- /dev/null +++ b/src/qtpromise/qpromise.h @@ -0,0 +1,107 @@ +#ifndef _QTPROMISE_QPROMISE_H +#define _QTPROMISE_QPROMISE_H + +// QPromise +#include "qpromise_p.h" +#include "qpromiseglobal.h" + +// Qt +#include +#include + +namespace QtPromise { + +using namespace QtPromisePrivate; + +template +class QPromiseBase +{ +public: + using Type = T; + + QPromiseBase() : m_d(new QPromiseData()) { } + virtual ~QPromiseBase() { } + + bool isFulfilled() const { return m_d->resolved && !m_d->rejected; } + bool isRejected() const { return m_d->resolved && m_d->rejected; } + bool isPending() const { return !m_d->resolved; } + + template + inline auto then(TFulfilled fulfilled, TRejected rejected = nullptr) + -> typename QPromiseHandler::Promise; + + template + inline auto fail(TRejected rejected) + -> typename QPromiseHandler::Promise; + + template + inline QPromise reject(const TError& error); + + inline QPromise wait() const; + +protected: + QExplicitlySharedDataPointer > m_d; + + virtual void notify(const typename QPromiseData::HandlerList& handlers) const = 0; + + inline void dispatch(); +}; + +template +class QPromise: public QPromiseBase +{ +public: + QPromise() : QPromiseBase() {} + + template + inline QPromise finally(THandler handler); + + inline QPromise fulfill(const T& value); + +public: // STATIC + inline static QPromise > all(const QVector >& promises); + +protected: + inline void notify(const typename QPromiseData::HandlerList& handlers) const Q_DECL_OVERRIDE; + +private: + friend class QPromiseBase; + + QPromise(const QPromiseBase& p) : QPromiseBase(p) { } + +}; + +template <> +class QPromise: public QPromiseBase +{ +public: + QPromise(): QPromiseBase() {} + + template + inline QPromise finally(THandler handler); + + inline QPromise fulfill(); + +public: // STATIC + inline static QPromise all(const QVector >& promises); + +protected: + inline void notify(const typename QPromiseData::HandlerList& handlers) const Q_DECL_OVERRIDE; + +private: + friend class QPromiseBase; + + QPromise(const QPromiseBase& p) : QPromiseBase(p) { } +}; + +using QVariantPromise = QtPromise::QPromise; + +} // namespace QtPromise + +Q_DECLARE_TYPEINFO(QtPromise::QVariantPromise, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(QtPromise::QVariantPromise) + +#include "qpromise.inl" +#include "qpromise_qfuture.inl" + +#endif // ifndef _QTPROMISE_QPROMISE_H diff --git a/src/qtpromise/qpromise.inl b/src/qtpromise/qpromise.inl new file mode 100644 index 0000000..3736c7b --- /dev/null +++ b/src/qtpromise/qpromise.inl @@ -0,0 +1,228 @@ +// Qt +#include +#include + +namespace QtPromise { + +template +template +inline auto QPromiseBase::then(TFulfilled fulfilled, TRejected rejected) + -> typename QPromiseHandler::Promise +{ + typename QPromiseHandler::Promise next; + + m_d->handlers << QPromiseHandler::create(next, fulfilled); + m_d->catchers << QPromiseCatcher::create(next, rejected); + + if (m_d->resolved) { + dispatch(); + } + + return next; +} + +template +template +inline auto QPromiseBase::fail(TRejected rejected) + -> typename QPromiseHandler::Promise +{ + return then(nullptr, rejected); +} + +template +template +inline QPromise QPromiseBase::reject(const TError& error) +{ + if (!m_d->resolved) { + m_d->error = QtPromisePrivate::to_exception_ptr(error); + m_d->rejected = true; + m_d->resolved = true; + dispatch(); + } + + return *this; +} + +template +inline QPromise QPromiseBase::wait() const +{ + // @TODO wait timeout + global timeout + while (!m_d->resolved) { + QCoreApplication::processEvents(QEventLoop::AllEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + return *this; +} + +template +inline void QPromiseBase::dispatch() +{ + Q_ASSERT(m_d->resolved); + + typename QPromiseData::HandlerList handlers; + typename QPromiseData::CatcherList catchers; + + handlers.swap(m_d->handlers); + catchers.swap(m_d->catchers); + + if (m_d->rejected) { + const std::exception_ptr error = m_d->error; + qtpromise_defer([catchers, error]() { + for (auto catcher: catchers) { + catcher(error); + } + }); + } else { + notify(handlers); + } +} + +template +template +inline QPromise QPromise::finally(THandler handler) +{ + return this->then([=](const T& res) { + return QPromise().fulfill().then(handler).then([=](){ + return res; + }); + }, [=]() { + const auto exception = std::current_exception(); + return QPromise().fulfill().then(handler).then([=](){ + std::rethrow_exception(exception); + return T(); + }); + }); +} + +template +inline QPromise QPromise::fulfill(const T& value) +{ + if (!this->m_d->resolved) { + this->m_d->rejected = false; + this->m_d->resolved = true; + this->m_d->value = value; + this->dispatch(); + } + + return *this; +} + +template +inline QPromise > QPromise::all(const QVector >& promises) +{ + QPromise > next; + + const int count = promises.size(); + if (count == 0) { + return next.fulfill({}); + } + + QSharedPointer remaining(new int(count)); + QSharedPointer > results(new QVector(count)); + + for (int i=0; i(promises[i]).then([=](const T& res) mutable { + if (next.isPending()) { + (*results)[i] = res; + if (--(*remaining) == 0) { + next.fulfill(*results); + } + } + }, [=]() mutable { + if (next.isPending()) { + next.reject(std::current_exception()); + } + }); + } + + return next; +} + +template +inline void QPromise::notify(const typename QPromiseData::HandlerList& handlers) const +{ + const T value = this->m_d->value; + qtpromise_defer([handlers, value]() { + for (auto handler: handlers) { + handler(value); + } + }); +} + +template +inline QPromise QPromise::finally(THandler handler) +{ + return this->then([=]() { + return QPromise().fulfill().then(handler).then([](){}); + }, [=]() { + const auto exception = std::current_exception(); + return QPromise().fulfill().then(handler).then([=](){ + std::rethrow_exception(exception); + }); + }); +} + +inline QPromise QPromise::fulfill() +{ + if (!m_d->resolved) { + m_d->rejected = false; + m_d->resolved = true; + dispatch(); + } + + return *this; +} + +inline QPromise QPromise::all(const QVector >& promises) +{ + QPromise next; + + QSharedPointer remaining(new int(promises.size())); + + for (const auto& promise: promises) { + QPromise(promise).then([=]() mutable { + if (next.isPending()) { + if (--(*remaining) == 0) { + next.fulfill(); + } + } + }, [=]() mutable { + if (next.isPending()) { + next.reject(std::current_exception()); + } + }); + } + + return next; +} + +inline void QPromise::notify(const typename QPromiseData::HandlerList& handlers) const +{ + qtpromise_defer([handlers]() { + for (const auto& handler: handlers) { + handler(); + } + }); +} + +// Helpers + +template +QPromise qPromise(const T& value) +{ + return QPromise().fulfill(value); +} + +template +QPromise > qPromiseAll(const QVector >& promises) +{ + return QPromise::all(promises); +} + +QPromise qPromiseAll(const QVector >& promises) +{ + return QPromise::all(promises); +} + +} // namespace QtPromise diff --git a/src/qtpromise/qpromise_p.h b/src/qtpromise/qpromise_p.h new file mode 100644 index 0000000..d8e49b7 --- /dev/null +++ b/src/qtpromise/qpromise_p.h @@ -0,0 +1,318 @@ +#ifndef _QTPROMISE_QPROMISE_P_H +#define _QTPROMISE_QPROMISE_P_H + +// QPromise +#include "qpromiseglobal.h" + +// Qt +#include +#include +#include +#include + +// STL +#include + +namespace QtPromise { +template +class QPromise; +} + +namespace QtPromisePrivate { + +using namespace QtPromise; + +template +inline void qtpromise_defer(F f) +{ + QTimer::singleShot(0, f); +} + +template +struct QPromiseDeduce +{ + using Type = QPromise >; +}; + +template +struct QPromiseDeduce > + : public QPromiseDeduce +{ }; + +template ::Type> +struct QPromiseFulfill +{ + static void call(TPromise next, const T& value) + { + next.fulfill(value); + } +}; + +template +struct QPromiseFulfill, QPromise > +{ + static void call(QPromise next, QPromise promise) + { + promise.then( + [=](const T& value) mutable { + next.fulfill(value); + }, + [=]() mutable { + next.reject(std::current_exception()); + }); + } +}; + +template <> +struct QPromiseFulfill, QPromise > +{ + template + static void call(TPromise next, TPromise promise) + { + promise.then( + [=]() mutable { + next.fulfill(); + }, + [=]() mutable { + next.reject(std::current_exception()); + }); + } +}; + +template +struct QPromiseDispatch +{ + using Promise = typename QPromiseDeduce::Type; + using ResType = Unqualified; + + template + static void call(TPromise next, THandler handler, const T& value) + { + ResType res; + try { + res = handler(value); + } catch (...) { + next.reject(std::current_exception()); + return; + } + QPromiseFulfill::call(next, res); + } +}; + +template +struct QPromiseDispatch +{ + using Promise = QPromise; + + template + static void call(TPromise next, THandler handler, const T& value) + { + try { + handler(value); + } catch (...) { + next.reject(std::current_exception()); + return; + } + next.fulfill(); + } +}; + +template +struct QPromiseDispatch +{ + using Promise = typename QPromiseDeduce::Type; + using ResType = Unqualified; + + template + static void call(TPromise next, THandler handler) + { + ResType res; + try { + res = handler(); + } catch (...) { + next.reject(std::current_exception()); + return; + } + QPromiseFulfill::call(next, res); + } +}; + +template <> +struct QPromiseDispatch +{ + using Promise = QPromise; + + template + static void call(TPromise next, THandler handler) + { + try { + handler(); + } catch (...) { + next.reject(std::current_exception()); + return; + } + next.fulfill(); + } +}; + +template ::first> +struct QPromiseHandler +{ + using ResType = typename std::result_of::type; + using Promise = typename QPromiseDispatch::Promise; + + static std::function create(const Promise& next, THandler handler) + { + return [=](const T& value) mutable { + QPromiseDispatch::call(next, handler, value); + }; + } +}; + +template +struct QPromiseHandler +{ + using ResType = typename std::result_of::type; + using Promise = typename QPromiseDispatch::Promise; + + static std::function create(const Promise& next, THandler handler) + { + return [=](const T&) mutable { + QPromiseDispatch::call(next, handler); + }; + } +}; + +template +struct QPromiseHandler +{ + using ResType = typename std::result_of::type; + using Promise = typename QPromiseDispatch::Promise; + + static std::function create(const Promise& next, THandler handler) + { + return [=]() { + QPromiseDispatch::call(next, handler); + }; + } +}; + +template +struct QPromiseHandler +{ + using Promise = QPromise; + + static std::function create(const Promise& next, std::nullptr_t) + { + return [next](const T& value) mutable { + // 2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, + // promise2 must be fulfilled with the same value as promise1. + QPromiseFulfill::call(next, value); + }; + } +}; + +template <> +struct QPromiseHandler +{ + using Promise = QPromise; + + template + static std::function create(const TPromise& next, std::nullptr_t) + { + return [=]() mutable { + // 2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, + // promise2 must be fulfilled with the same value as promise1. + TPromise(next).fulfill(); + }; + } +}; + +template ::first> +struct QPromiseCatcher +{ + using Type = std::function; + using ResType = typename std::result_of::type; + + template + static Type create(const TPromise& next, THandler handler) + { + return [=](const std::exception_ptr& eptr) mutable { + try { + std::rethrow_exception(eptr); + } catch (const TArg& error) { + QPromiseDispatch::call(next, handler, error); + } catch(...) { + TPromise(next).reject(std::current_exception()); + } + }; + } +}; + +template +struct QPromiseCatcher +{ + using Type = std::function; + using ResType = typename std::result_of::type; + + template + static Type create(const TPromise& next, THandler handler) + { + return [=](const std::exception_ptr& eptr) mutable { + try { + std::rethrow_exception(eptr); + } catch (...) { + QPromiseDispatch::call(next, handler); + } + }; + } +}; + +template +struct QPromiseCatcher +{ + using Type = std::function; + + template + static Type create(const TPromise& next, std::nullptr_t) + { + return [=](const std::exception_ptr& eptr) mutable { + // 2.2.7.4. If onRejected is not a function and promise1 is rejected, + // promise2 must be rejected with the same reason as promise1 + TPromise(next).reject(eptr); + }; + } +}; + +template +struct QPromiseDataBase: public QSharedData +{ + using ErrorType = std::exception_ptr; + using CatcherList = QVector >; + + bool resolved; + bool rejected; + ErrorType error; + CatcherList catchers; +}; + +template +struct QPromiseData: QPromiseDataBase +{ + using HandlerList = QVector >; + + HandlerList handlers; + T value; +}; + +template <> +struct QPromiseData: QPromiseDataBase +{ + using HandlerList = QVector >; + + HandlerList handlers; +}; + +} // namespace QtPromise + +#endif // ifndef _QTPROMISE_QPROMISE_H diff --git a/src/qtpromise/qpromise_qfuture.inl b/src/qtpromise/qpromise_qfuture.inl new file mode 100644 index 0000000..cc29194 --- /dev/null +++ b/src/qtpromise/qpromise_qfuture.inl @@ -0,0 +1,79 @@ +#include +#include + +namespace QtPromisePrivate { + +template +struct QPromiseDeduce > + : public QPromiseDeduce +{ }; + +template +struct QPromiseFulfill, QPromise > +{ + static void call(QPromise next, const QFuture& future) + { + using Watcher = QFutureWatcher; + + Watcher* watcher = new Watcher(); + QObject::connect( + watcher, &Watcher::finished, + [next, watcher]() mutable { + T res; + try { + res = watcher->result(); + } catch(...) { + next.reject(std::current_exception()); + } + + watcher->deleteLater(); + if (next.isPending()) { + QPromiseFulfill::call(next, res); + } + }); + + watcher->setFuture(future); + } +}; + +template <> +struct QPromiseFulfill, QPromise > +{ + static void call(QPromise next, const QFuture& future) + { + using Watcher = QFutureWatcher; + + Watcher* watcher = new Watcher(); + QObject::connect( + watcher, &Watcher::finished, + [next, watcher]() mutable { + try { + // let's rethrown possibe exception + watcher->waitForFinished(); + } catch(...) { + next.reject(std::current_exception()); + } + + watcher->deleteLater(); + if (next.isPending()) { + next.fulfill(); + } + }); + + watcher->setFuture(future); + } +}; + +} // namespace QtPromisePrivate + +namespace QtPromise { + +template +QPromise qPromise(const QFuture& future) +{ + QPromise next; + QPromiseFulfill >::call(next, future); + return next; +} + +} // namespace QtPromise diff --git a/src/qtpromise/qpromiseglobal.h b/src/qtpromise/qpromiseglobal.h new file mode 100644 index 0000000..bfd1e46 --- /dev/null +++ b/src/qtpromise/qpromiseglobal.h @@ -0,0 +1,124 @@ +#ifndef _QTPROMISE_QPROMISEGLOBAL_H +#define _QTPROMISE_QPROMISEGLOBAL_H + +// QtCore +#include + +// STL +#include + +namespace QtPromisePrivate +{ +// https://rmf.io/cxx11/even-more-traits#unqualified_types +template +using Unqualified = typename std::remove_cv::type>::type; + +/*! + * \struct ArgsOf + * http://stackoverflow.com/a/7943765 + * http://stackoverflow.com/a/27885283 + */ +template +struct ArgsTraits +{ + using types = std::tuple; + using first = typename std::tuple_element<0, types>::type; +}; + +template <> +struct ArgsTraits<> +{ + using types = std::tuple<>; + using first = void; +}; + +template +struct ArgsOf : public ArgsOf +{ }; + +template <> +struct ArgsOf : public ArgsTraits<> +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf : public ArgsTraits +{ }; + +template +struct ArgsOf > : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +template +struct ArgsOf : public ArgsOf +{ }; + +/*! + * \fn to_exception_ptr + */ +template +std::exception_ptr to_exception_ptr(const T& value) +{ + try { + throw value; + } catch(...) { + return std::current_exception(); + } +} + +template <> +std::exception_ptr to_exception_ptr(const std::exception_ptr& value) +{ + return value; +} + +} // namespace QtPromisePrivate + +#endif // ifndef _QTPROMISE_QPROMISEGLOBAL_H diff --git a/src/qtpromise/qtpromise.pri b/src/qtpromise/qtpromise.pri new file mode 100644 index 0000000..ed263e4 --- /dev/null +++ b/src/qtpromise/qtpromise.pri @@ -0,0 +1,6 @@ +HEADERS += \ + $$PWD/qpromise.h \ + $$PWD/qpromise.inl \ + $$PWD/qpromise_p.h \ + $$PWD/qpromise_qfuture.inl \ + $$PWD/qpromiseglobal.h diff --git a/src/qtpromise/qtpromise.pro b/src/qtpromise/qtpromise.pro new file mode 100644 index 0000000..71afe76 --- /dev/null +++ b/src/qtpromise/qtpromise.pro @@ -0,0 +1,5 @@ +TEMPLATE = lib +CONFIG += c++11 qt thread warn_on +DEFINES += QT_DEPRECATED_WARNINGS + +include(qtpromise.pri) diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..720a606 --- /dev/null +++ b/src/src.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS = qtpromise diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro new file mode 100644 index 0000000..a95fbee --- /dev/null +++ b/tests/auto/auto.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs +SUBDIRS += \ + qpromise \ + requirements diff --git a/tests/auto/qpromise/qpromise.pro b/tests/auto/qpromise/qpromise.pro new file mode 100644 index 0000000..92ed1c4 --- /dev/null +++ b/tests/auto/qpromise/qpromise.pro @@ -0,0 +1,4 @@ +TARGET = tst_qpromise +SOURCES += $$PWD/tst_qpromise.cpp + +include(../tests.pri) diff --git a/tests/auto/qpromise/tst_qpromise.cpp b/tests/auto/qpromise/tst_qpromise.cpp new file mode 100644 index 0000000..9efbeee --- /dev/null +++ b/tests/auto/qpromise/tst_qpromise.cpp @@ -0,0 +1,199 @@ +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +static const int ASYNC_DELAY = 256; + +class tst_qpromise: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void finallyReturns(); + void finallyThrows(); + void finallyDelayedFulfilled(); + void finallyDelayedRejected(); + +}; // class tst_qpromise + +QTEST_MAIN(tst_qpromise) +#include "tst_qpromise.moc" + +void tst_qpromise::finallyReturns() +{ + { + QPromise p; + QVector values; + auto next = p.finally([&values]() { + values << 8; + return 16; + }); + + p.fulfill(42); + next.then([&values](int r) { + values << r; + }).wait(); + + QVERIFY(p.isFulfilled()); + QVERIFY(next.isFulfilled()); + QCOMPARE(values, QVector({8, 42})); + } + { + QPromise p; + QVector values; + auto next = p.finally([&values]() { + values << 8; + return 16; + }); + + p.reject(QString("foo")); + next.then([&values](int r) { + values << r; + }).wait(); + + QVERIFY(p.isRejected()); + QVERIFY(next.isRejected()); + QCOMPARE(values, QVector({8})); + } +} + +void tst_qpromise::finallyThrows() +{ + { + QPromise p; + QString error; + auto next = p.finally([]() { + throw QString("bar"); + }); + + p.fulfill(42); + next.fail([&error](const QString& err) { + error = err; + return 0; + }).wait(); + + QVERIFY(p.isFulfilled()); + QVERIFY(next.isRejected()); + QCOMPARE(error, QString("bar")); + } + { + QPromise p; + QString error; + auto next = p.finally([]() { + throw QString("bar"); + }); + + p.reject(QString("foo")); + next.fail([&error](const QString& err) { + error = err; + return 0; + }).wait(); + + QVERIFY(p.isRejected()); + QVERIFY(next.isRejected()); + QCOMPARE(error, QString("bar")); + } +} + +void tst_qpromise::finallyDelayedFulfilled() +{ + { + QPromise p0; + QVector values; + auto next = p0.finally([&values]() { + QPromise p1; + QTimer::singleShot(ASYNC_DELAY, [p1, &values]() mutable { + values << 64; + p1.fulfill(16); + }); + + values << 8; + return p1; + }); + + p0.fulfill(42); + next.then([&values](int r) { + values << r; + }).wait(); + + QVERIFY(p0.isFulfilled()); + QVERIFY(next.isFulfilled()); + QCOMPARE(values, QVector({8, 64, 42})); + } + { + QPromise p0; + QVector values; + auto next = p0.finally([&values]() { + QPromise p1; + QTimer::singleShot(ASYNC_DELAY, [p1, &values]() mutable { + values << 64; + p1.fulfill(16); + }); + + values << 8; + return p1; + }); + + p0.reject(QString("foo")); + next.then([&values](int r) { + values << r; + }).wait(); + + QVERIFY(p0.isRejected()); + QVERIFY(next.isRejected()); + QCOMPARE(values, QVector({8, 64})); + } +} + +void tst_qpromise::finallyDelayedRejected() +{ + { + QPromise p0; + QString error; + auto next = p0.finally([]() { + QPromise p1; + QTimer::singleShot(ASYNC_DELAY, [p1]() mutable { + p1.reject(QString("bar")); + }); + + return p1; + }); + + p0.fulfill(42); + next.fail([&error](const QString& err) { + error = err; + return 0; + }).wait(); + + QVERIFY(p0.isFulfilled()); + QVERIFY(next.isRejected()); + QCOMPARE(error, QString("bar")); + } + { + QPromise p0; + QString error; + auto next = p0.finally([]() { + QPromise p1; + QTimer::singleShot(ASYNC_DELAY, [p1]() mutable { + p1.reject(QString("bar")); + }); + + return p1; + }); + + p0.reject(QString("foo")); + next.fail([&error](const QString& err) { + error = err; + return 0; + }).wait(); + + QVERIFY(p0.isRejected()); + QVERIFY(next.isRejected()); + QCOMPARE(error, QString("bar")); + } +} diff --git a/tests/auto/requirements/requirements.pro b/tests/auto/requirements/requirements.pro new file mode 100644 index 0000000..2fab75b --- /dev/null +++ b/tests/auto/requirements/requirements.pro @@ -0,0 +1,4 @@ +TARGET = tst_requirements +SOURCES += $$PWD/tst_requirements.cpp + +include(../tests.pri) diff --git a/tests/auto/requirements/tst_requirements.cpp b/tests/auto/requirements/tst_requirements.cpp new file mode 100644 index 0000000..8832b5e --- /dev/null +++ b/tests/auto/requirements/tst_requirements.cpp @@ -0,0 +1,315 @@ +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +// https://promisesaplus.com/#requirements +class tst_requirements: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + // 2.1. Promise States + void statePending(); // 2.1.1. + void stateFulfilled(); // 2.1.2. + void stateRejected(); // 2.1.3. + + // 2.2. The then Method + void thenArguments(); // 2.2.1. + void thenOnFulfilled(); // 2.2.2. + void thenOnRejected(); // 2.2.3. + void thenAsynchronous(); // 2.2.4. + // 2.2.5. NOT APPLICABLE + void thenMultipleCalls(); // 2.2.6. + void thenHandlers(); // 2.2.7. + + // 2.3. The Promise Resolution Procedure + +}; // class tst_requirements + +QTEST_MAIN(tst_requirements) +#include "tst_requirements.moc" + +void tst_requirements::statePending() +{ + // 2.1.1. When pending, a promise: + { + QPromise<> p; + QVERIFY(p.isPending()); + QVERIFY(!p.isFulfilled()); + QVERIFY(!p.isRejected()); + } + + // 2.1.1.1. may transition to either the fulfilled state + { + QPromise<> p; + p.fulfill(); + QVERIFY(!p.isPending()); + QVERIFY(p.isFulfilled()); + } + + // 2.1.1.1. ... or the rejected state + { + QPromise<> p; + p.reject("foo"); + QVERIFY(!p.isPending()); + QVERIFY(p.isRejected()); + } +} + +void tst_requirements::stateFulfilled() +{ + QVector values; + auto log_value = [&values](int res) { values << res; }; + + QPromise p; + QVERIFY(p.isPending()); + + // 2.1.2. When fulfilled, a promise: + p.fulfill(42).then(log_value).wait(); + QVERIFY(p.isFulfilled()); + QVERIFY(!p.isRejected()); + + // 2.1.2.1. must not transition to any other state. + p.reject("foo").then(log_value).wait(); + QVERIFY(p.isFulfilled()); + QVERIFY(!p.isRejected()); + + // 2.1.2.2. must have a value, which must not change. + p.fulfill(51).then(log_value).wait(); + QVERIFY(p.isFulfilled()); + QVERIFY(!p.isRejected()); + + QCOMPARE(values, QVector({42, 42, 42})); +} + +void tst_requirements::stateRejected() +{ + QStringList errors; + auto log_error = [&errors](const QString& err) { errors << err; return -1; }; + + QPromise p; + QVERIFY(p.isPending()); + + // 2.1.3 When rejected, a promise: + p.reject(QString("foo")).then(nullptr, log_error).wait(); + QVERIFY(!p.isFulfilled()); + QVERIFY(p.isRejected()); + + // 2.1.3.1. must not transition to any other state. + p.fulfill(51).then(nullptr, log_error).wait(); + QVERIFY(!p.isFulfilled()); + QVERIFY(p.isRejected()); + + // 2.1.3.2. must have a reason, which must not change. + p.reject(QString("bar")).then(nullptr, log_error).wait(); + QVERIFY(!p.isFulfilled()); + QVERIFY(p.isRejected()); + + QCOMPARE(errors, QStringList({"foo", "foo", "foo"})); +} + +void tst_requirements::thenArguments() +{ + // Both onFulfilled and onRejected are given + { + int value = 0; + QPromise p; + p.fulfill(42).then([&value](int res) { value = res; }, [](const QString&){}).wait(); + QVERIFY(p.isFulfilled()); + QCOMPARE(value, 42); + } + { + QString error; + QPromise p; + p.reject(QString("foo")).then([](int) {}, [&error](const QString& err){ error = err; }).wait(); + QVERIFY(p.isRejected()); + QCOMPARE(error, QString("foo")); + } + + // 2.2.1. onFulfilled is an optional arguments: + { + QString error; + QPromise p; + p.reject(QString("foo")).then(nullptr, [&error](const QString& err){ error = err; return 42; }).wait(); + QVERIFY(p.isRejected()); + QCOMPARE(error, QString("foo")); + } + + // 2.2.1. onRejected is an optional arguments: + { + int value = 0; + QPromise p; + p.fulfill(42).then([&value](int res) { value = res; }).wait(); + QVERIFY(p.isFulfilled()); + QCOMPARE(value, 42); + } + + // 2.2.1.1. If onFulfilled is not a function, it must be ignored. + // 2.2.1.2. If onRejected is not a function, it must be ignored. + // -> compilation fails on type check. +} + +void tst_requirements::thenOnFulfilled() +{ + // 2.2.2. If onFulfilled is a function: + + QPromise p0; + QVector values; + auto p1 = p0.then([&values](int res) { values << res; }); // @TODO .wait(10) + + // 2.2.2.2. it must not be called before promise is fulfilled. + QVERIFY(p0.isPending()); + QVERIFY(p1.isPending()); + QVERIFY(values.isEmpty()); + + // 2.2.2.1. it must be called after promise is fulfilled, + // with promise’s value as its first argument. + p0.fulfill(42); + p1.wait(); + QVERIFY(p0.isFulfilled()); + QVERIFY(p1.isFulfilled()); + QCOMPARE(values, QVector({42})); + + // 2.2.2.3. it must not be called more than once + p0.fulfill(12); + p1.wait(); + QCOMPARE(values, QVector({42})); +} + +void tst_requirements::thenOnRejected() +{ + // 2.2.3. If onRejected is a function: + + QPromise<> p0; + QStringList errors; + auto p1 = p0.then(nullptr, [&errors](const QString& err) { errors << err; }); // @TODO .wait(10) + + // 2.2.3.2. it must not be called before promise is rejected. + QVERIFY(p0.isPending()); + QVERIFY(p1.isPending()); + QVERIFY(errors.isEmpty()); + + // 2.2.3.1. it must be called after promise is rejected, + // with promise’s reason as its first argument. + p0.reject(QString("foo")); + p1.wait(); + QVERIFY(p0.isRejected()); + QVERIFY(p1.isFulfilled()); + QCOMPARE(errors, QStringList({"foo"})); + + // 2.2.2.3. it must not be called more than once. + p0.reject(12); + p1.wait(); + QCOMPARE(errors, QStringList({"foo"})); +} + +void tst_requirements::thenAsynchronous() +{ + // 2.2.4. onFulfilled or onRejected must not be called until the execution context + // stack contains only platform code (ie. executed asynchronously, after the event + // loop turn in which then is called, and with a fresh stack). + + int value = 0; + QPromise p0; + p0.fulfill(42); + QVERIFY(p0.isFulfilled()); + + auto p1 = p0.then([&value](int res){ value = res; }); + QVERIFY(p1.isPending()); + QCOMPARE(value, 0); + + p1.wait(); + QVERIFY(p1.isFulfilled()); + QCOMPARE(value, 42); +} + +void tst_requirements::thenMultipleCalls() +{ + // 2.2.6. then may be called multiple times on the same promise: + + // 2.2.6.1. If/when promise is fulfilled, all respective onFulfilled callbacks + // must execute in the order of their originating calls to then: + { + QPromise p; + QVector values; + auto all = qPromiseAll(QVector >{ + p.then([&values](int r) { values << r + 1; }), + p.then([&values](int r) { values << r + 2; }), + p.then([&values](int r) { values << r + 3; }) + }); + + p.fulfill(42); + all.wait(); + QCOMPARE(values, QVector({43, 44, 45})); + } + + // 2.2.6.2. If/when promise is rejected, all respective onRejected callbacks + // must execute in the order of their originating calls to then: + { + QPromise p; + QVector values; + auto all = qPromiseAll(QVector >{ + p.then(nullptr, [&values](int r) { values << r + 1; return r + 1; }), + p.then(nullptr, [&values](int r) { values << r + 2; return r + 2; }), + p.then(nullptr, [&values](int r) { values << r + 3; return r + 3; }) + }); + + p.reject(8); + all.wait(); + QCOMPARE(values, QVector({9, 10, 11})); + } +} + +void tst_requirements::thenHandlers() +{ + // 2.2.7. then must return a promise: p2 = p1.then(onFulfilled, onRejected); + { + QPromise p1; + auto handler = [](){ return 42; }; + Q_STATIC_ASSERT((std::is_same >::value)); + Q_STATIC_ASSERT((std::is_same >::value)); + Q_STATIC_ASSERT((std::is_same >::value)); + } + + // 2.2.7.1. If either onFulfilled or onRejected returns a value x, run the + // Promise Resolution Procedure [[Resolve]](p2, x) -> See 2.3. + + // 2.2.7.2. If either onFulfilled or onRejected throws an exception e, + // p2 must be rejected with e as the reason. + { + QString reason; + QPromise p1; + auto p2 = p1.fulfill(42).then([](){ throw QString("foo"); }); + p2.then(nullptr, [&reason](const QString& e) { reason = e; }).wait(); + + QVERIFY(p2.isRejected()); + QCOMPARE(reason, QString("foo")); + } + { + QString reason; + QPromise p1; + auto p2 = p1.reject(QString("foo")).then(nullptr, [](){ throw QString("bar"); return 42; }); + p2.then(nullptr, [&reason](const QString& e) { reason = e; return 0; }).wait(); + + QVERIFY(p2.isRejected()); + QCOMPARE(reason, QString("bar")); + } + + // 2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, + // p2 must be fulfilled with the same value as promise1 + { + QString value; + QPromise p1; + auto p2 = p1.fulfill("42").then(nullptr, [](){ return QString(); }); + Q_STATIC_ASSERT((std::is_same >::value)); + p2.then([&value](const QString& e) { value = e; }).wait(); + + QVERIFY(p2.isFulfilled()); + QCOMPARE(value, QString("42")); + } +} diff --git a/tests/auto/tests.pri b/tests/auto/tests.pri new file mode 100644 index 0000000..343679f --- /dev/null +++ b/tests/auto/tests.pri @@ -0,0 +1,6 @@ +TEMPLATE = app +CONFIG += testcase +QT += testlib concurrent +DEFINES += QT_DEPRECATED_WARNINGS + +include(../../qtpromise.pri) diff --git a/tests/tests.pro b/tests/tests.pro new file mode 100644 index 0000000..157ef34 --- /dev/null +++ b/tests/tests.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS += auto