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`).
This commit is contained in:
Simon Brunel 2017-05-25 18:00:17 +02:00
parent 596855f579
commit 4919a68959
4 changed files with 386 additions and 6 deletions

View File

@ -1,9 +1,24 @@
#ifndef _QTPROMISE_QPROMISEFUTURE_P_H #ifndef _QTPROMISE_QPROMISEFUTURE_P_H
#define _QTPROMISE_QPROMISEFUTURE_P_H #define _QTPROMISE_QPROMISEFUTURE_P_H
// Qt
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QFuture> #include <QFuture>
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 { namespace QtPromisePrivate {
template <typename T> template <typename T>
@ -24,8 +39,18 @@ struct PromiseFulfill<QFuture<T> >
Watcher* watcher = new Watcher(); Watcher* watcher = new Watcher();
QObject::connect(watcher, &Watcher::finished, [=]() mutable { QObject::connect(watcher, &Watcher::finished, [=]() mutable {
try { try {
T res = watcher->result(); if (watcher->isCanceled()) {
PromiseFulfill<T>::call(res, resolve, reject); // 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<T>::call(res, resolve, reject);
}
} catch(...) { } catch(...) {
reject(std::current_exception()); reject(std::current_exception());
} }
@ -50,9 +75,13 @@ struct PromiseFulfill<QFuture<void> >
Watcher* watcher = new Watcher(); Watcher* watcher = new Watcher();
QObject::connect(watcher, &Watcher::finished, [=]() mutable { QObject::connect(watcher, &Watcher::finished, [=]() mutable {
try { try {
// let's rethrown possibe exception if (watcher->isCanceled()) {
watcher->waitForFinished(); // let's rethrown potential exception
resolve(); watcher->waitForFinished();
reject(QtPromise::QPromiseCanceledException());
} else {
resolve();
}
} catch(...) { } catch(...) {
reject(std::current_exception()); reject(std::current_exception());
} }
@ -64,6 +93,6 @@ struct PromiseFulfill<QFuture<void> >
} }
}; };
} // namespace QtPromise } // namespace QtPromisePrivate
#endif // _QTPROMISE_QPROMISEFUTURE_P_H #endif // _QTPROMISE_QPROMISEFUTURE_P_H

View File

@ -1,5 +1,6 @@
TEMPLATE = subdirs TEMPLATE = subdirs
SUBDIRS += \ SUBDIRS += \
future \
helpers \ helpers \
qpromise \ qpromise \
requirements requirements

View File

@ -0,0 +1,4 @@
TARGET = tst_future
SOURCES += $$PWD/tst_future.cpp
include(../tests.pri)

View File

@ -0,0 +1,346 @@
// QtPromise
#include <QtPromise>
// Qt
#include <QtConcurrent>
#include <QtTest>
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<decltype(p), QPromise<int> >::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<decltype(p), QPromise<void> >::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<decltype(p), QPromise<int> >::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<decltype(p), QPromise<void> >::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<decltype(p), QPromise<int> >::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<decltype(p), QPromise<void> >::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<int>()); // 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<void>()); // 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<QString>::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<void>::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<decltype(output), QPromise<int> >::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<decltype(output), QPromise<int> >::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"));
}