From 4919a68959d5c280ef8749eb4b2497d540f276bb Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 25 May 2017 18:00:17 +0200 Subject: [PATCH] Enhance QFuture integration and add unit tests QFuture canceled with `QFuture::cancel()` now rejects attached promises with `QPromiseCanceledException`. In case the future is canceled because an exception (e) has been thrown, the promise is rejected with the same (e) exception (or `QUnhandledException` if not a subclass of `QException`). --- src/qtpromise/qpromisefuture.h | 41 +++- tests/auto/auto.pro | 1 + tests/auto/future/future.pro | 4 + tests/auto/future/tst_future.cpp | 346 +++++++++++++++++++++++++++++++ 4 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 tests/auto/future/future.pro create mode 100644 tests/auto/future/tst_future.cpp diff --git a/src/qtpromise/qpromisefuture.h b/src/qtpromise/qpromisefuture.h index 0ca4d94..ff97599 100644 --- a/src/qtpromise/qpromisefuture.h +++ b/src/qtpromise/qpromisefuture.h @@ -1,9 +1,24 @@ #ifndef _QTPROMISE_QPROMISEFUTURE_P_H #define _QTPROMISE_QPROMISEFUTURE_P_H +// Qt #include #include +namespace QtPromise { + +class QPromiseCanceledException: public QException +{ +public: + void raise() const Q_DECL_OVERRIDE { throw *this; } + QPromiseCanceledException* clone() const Q_DECL_OVERRIDE + { + return new QPromiseCanceledException(*this); + } +}; + +} // namespace QtPromise + namespace QtPromisePrivate { template @@ -24,8 +39,18 @@ struct PromiseFulfill > Watcher* watcher = new Watcher(); QObject::connect(watcher, &Watcher::finished, [=]() mutable { try { - T res = watcher->result(); - PromiseFulfill::call(res, resolve, reject); + if (watcher->isCanceled()) { + // A QFuture is canceled if cancel() has been explicitly called OR if an + // exception has been thrown from the associated thread. Trying to call + // result() in the first case causes a "read access violation", so let's + // rethrown potential exceptions using waitForFinished() and thus detect + // if the future has been canceled by the user or an exception. + watcher->waitForFinished(); + reject(QtPromise::QPromiseCanceledException()); + } else { + T res = watcher->result(); + PromiseFulfill::call(res, resolve, reject); + } } catch(...) { reject(std::current_exception()); } @@ -50,9 +75,13 @@ struct PromiseFulfill > Watcher* watcher = new Watcher(); QObject::connect(watcher, &Watcher::finished, [=]() mutable { try { - // let's rethrown possibe exception - watcher->waitForFinished(); - resolve(); + if (watcher->isCanceled()) { + // let's rethrown potential exception + watcher->waitForFinished(); + reject(QtPromise::QPromiseCanceledException()); + } else { + resolve(); + } } catch(...) { reject(std::current_exception()); } @@ -64,6 +93,6 @@ struct PromiseFulfill > } }; -} // namespace QtPromise +} // namespace QtPromisePrivate #endif // _QTPROMISE_QPROMISEFUTURE_P_H diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index ac52c3a..3b0b440 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,5 +1,6 @@ TEMPLATE = subdirs SUBDIRS += \ + future \ helpers \ qpromise \ requirements diff --git a/tests/auto/future/future.pro b/tests/auto/future/future.pro new file mode 100644 index 0000000..afb02c8 --- /dev/null +++ b/tests/auto/future/future.pro @@ -0,0 +1,4 @@ +TARGET = tst_future +SOURCES += $$PWD/tst_future.cpp + +include(../tests.pri) diff --git a/tests/auto/future/tst_future.cpp b/tests/auto/future/tst_future.cpp new file mode 100644 index 0000000..be93309 --- /dev/null +++ b/tests/auto/future/tst_future.cpp @@ -0,0 +1,346 @@ +// QtPromise +#include + +// Qt +#include +#include + +using namespace QtPromise; + +class tst_future: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void fulfilled(); + void fulfilled_void(); + void rejected(); + void rejected_void(); + void unhandled(); + void unhandled_void(); + void canceled(); + void canceled_void(); + void canceledFromThread(); + void then(); + void then_void(); + void fail(); + void fail_void(); + void finally(); + void finallyRejected(); + +}; // class tst_future + +class MyException: public QException +{ +public: + MyException(const QString& error) + : m_error(error) + { } + + const QString& error() const { return m_error; } + + void raise() const { throw *this; } + MyException* clone() const { return new MyException(*this); } + +private: + QString m_error; +}; + +QTEST_MAIN(tst_future) +#include "tst_future.moc" + +void tst_future::fulfilled() +{ + int result = -1; + auto p = qPromise(QtConcurrent::run([]() { + return 42; + })); + + Q_STATIC_ASSERT((std::is_same >::value)); + QCOMPARE(p.isPending(), true); + + p.then([&](int res) { + result = res; + }).wait(); + + QCOMPARE(p.isFulfilled(), true); + QCOMPARE(result, 42); +} + +void tst_future::fulfilled_void() +{ + int result = -1; + auto p = qPromise(QtConcurrent::run([]() { })); + + Q_STATIC_ASSERT((std::is_same >::value)); + QCOMPARE(p.isPending(), true); + + p.then([&]() { + result = 42; + }).wait(); + + QCOMPARE(p.isFulfilled(), true); + QCOMPARE(result, 42); +} + +void tst_future::rejected() +{ + QString error; + auto p = qPromise(QtConcurrent::run([]() { + throw MyException("foo"); + return 42; + })); + + Q_STATIC_ASSERT((std::is_same >::value)); + QCOMPARE(p.isPending(), true); + + p.fail([&](const MyException& e) { + error = e.error(); + return -1; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("foo")); +} + +void tst_future::rejected_void() +{ + QString error; + auto p = qPromise(QtConcurrent::run([]() { + throw MyException("foo"); + })); + + Q_STATIC_ASSERT((std::is_same >::value)); + + QCOMPARE(p.isPending(), true); + + p.fail([&](const MyException& e) { + error = e.error(); + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("foo")); +} + +void tst_future::unhandled() +{ + QString error; + auto p = qPromise(QtConcurrent::run([]() { + throw QString("foo"); + return 42; + })); + + Q_STATIC_ASSERT((std::is_same >::value)); + + QCOMPARE(p.isPending(), true); + + p.fail([&](const QString& err) { + error += err; + return -1; + }).fail([&](const QUnhandledException&) { + error += "bar"; + return -1; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("bar")); +} + +void tst_future::unhandled_void() +{ + QString error; + auto p = qPromise(QtConcurrent::run([]() { + throw QString("foo"); + })); + + Q_STATIC_ASSERT((std::is_same >::value)); + QCOMPARE(p.isPending(), true); + + p.fail([&](const QString& err) { + error += err; + }).fail([&](const QUnhandledException&) { + error += "bar"; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("bar")); +} + +void tst_future::canceled() +{ + QString error; + auto p = qPromise(QFuture()); // Constructs an empty, canceled future. + + QCOMPARE(p.isPending(), true); + + p.fail([&](const QPromiseCanceledException&) { + error = "canceled"; + return -1; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("canceled")); +} + +void tst_future::canceled_void() +{ + QString error; + auto p = qPromise(QFuture()); // Constructs an empty, canceled future. + + QCOMPARE(p.isPending(), true); + + p.fail([&](const QPromiseCanceledException&) { + error = "canceled"; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("canceled")); +} + +void tst_future::canceledFromThread() +{ + QString error; + auto p = qPromise(QtConcurrent::run([]() { + throw QPromiseCanceledException(); + })); + + QCOMPARE(p.isPending(), true); + + p.fail([&](const QPromiseCanceledException&) { + error = "bar"; + }).wait(); + + QCOMPARE(p.isRejected(), true); + QCOMPARE(error, QString("bar")); +} + +void tst_future::then() +{ + QString result; + auto input = qPromise(42); + auto output = input.then([](int res) { + return QtConcurrent::run([=]() { + return QString("foo%1").arg(res); + }); + }); + + QCOMPARE(input.isFulfilled(), true); + QCOMPARE(output.isPending(), true); + + output.then([&](const QString& res) { + result = res; + }).wait(); + + QCOMPARE(output.isFulfilled(), true); + QCOMPARE(result, QString("foo42")); +} + +void tst_future::then_void() +{ + QString result; + auto input = qPromise(); + auto output = input.then([&]() { + return QtConcurrent::run([&]() { + result = "foo"; + }); + }); + + QCOMPARE(input.isFulfilled(), true); + QCOMPARE(output.isPending(), true); + + output.then([&]() { + result += "bar"; + }).wait(); + + QCOMPARE(input.isFulfilled(), true); + QCOMPARE(result, QString("foobar")); +} + +void tst_future::fail() +{ + QString result; + auto input = QPromise::reject(MyException("bar")); + auto output = input.fail([](const MyException& e) { + return QtConcurrent::run([=]() { + return QString("foo") + e.error(); + }); + }); + + QCOMPARE(input.isRejected(), true); + QCOMPARE(output.isPending(), true); + + output.then([&](const QString& res) { + result = res; + }).wait(); + + QCOMPARE(output.isFulfilled(), true); + QCOMPARE(result, QString("foobar")); +} + +void tst_future::fail_void() +{ + QString result; + auto input = QPromise::reject(MyException("bar")); + auto output = input.fail([&](const MyException& e) { + return QtConcurrent::run([&]() { + result = e.error(); + }); + }); + + QCOMPARE(input.isRejected(), true); + QCOMPARE(output.isPending(), true); + + output.then([&]() { + result = result.prepend("foo"); + }).wait(); + + QCOMPARE(output.isFulfilled(), true); + QCOMPARE(result, QString("foobar")); +} + +void tst_future::finally() +{ + auto input = qPromise(42); + auto output = input.finally([]() { + return QtConcurrent::run([]() { + return QString("foo"); + }); + }); + + Q_STATIC_ASSERT((std::is_same >::value)); + + QCOMPARE(input.isFulfilled(), true); + QCOMPARE(output.isPending(), true); + + int value = -1; + output.then([&](int res) { + value = res; + }).wait(); + + QCOMPARE(output.isFulfilled(), true); + QCOMPARE(value, 42); +} + +void tst_future::finallyRejected() +{ + auto input = qPromise(42); + auto output = input.finally([]() { + return QtConcurrent::run([]() { + throw MyException("foo"); + }); + }); + + Q_STATIC_ASSERT((std::is_same >::value)); + + QCOMPARE(input.isFulfilled(), true); + QCOMPARE(output.isPending(), true); + + QString error; + output.fail([&](const MyException& e) { + error = e.error(); + return -1; + }).wait(); + + QCOMPARE(output.isRejected(), true); + QCOMPARE(error, QString("foo")); +}