/* * Copyright 2014 Daniel Vrátil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "../src/async.h" #include #include #include #include #include #include class AsyncTest : public QObject { Q_OBJECT public: AsyncTest() {} ~AsyncTest() {} private Q_SLOTS: void testSyncPromises(); void testAsyncPromises(); void testAsyncPromises2(); void testNestedAsync(); void testStartValue(); void testAsyncThen(); void testSyncThen(); void testJoinedThen(); void testVoidThen(); void testAsyncEach(); void testSyncEach(); void testJoinedEach(); void testVoidEachThen(); void testAsyncVoidEachThen(); void testAsyncReduce(); void testSyncReduce(); void testJoinedReduce(); void testVoidReduce(); void testProgressReporting(); void testErrorHandler(); void testErrorPropagation(); void testErrorHandlerAsync(); void testErrorPropagationAsync(); void testNestedErrorPropagation(); void testChainingRunningJob(); void testChainingFinishedJob(); void testLifetimeWithoutHandle(); void testLifetimeWithHandle(); void benchmarkSyncThenExecutor(); private: template class AsyncSimulator { public: AsyncSimulator(KAsync::Future &future, const T &result) : mFuture(future) , mResult(result) { QObject::connect(&mTimer, &QTimer::timeout, [this]() { mFuture.setValue(mResult); mFuture.setFinished(); }); QObject::connect(&mTimer, &QTimer::timeout, [this]() { delete this; }); mTimer.setSingleShot(true); mTimer.start(200); } AsyncSimulator(KAsync::Future &future, std::function&)> callback) : mFuture(future) , mCallback(callback) { QObject::connect(&mTimer, &QTimer::timeout, [this]() { mCallback(mFuture); }); QObject::connect(&mTimer, &QTimer::timeout, [this]() { delete this; }); mTimer.setSingleShot(true); mTimer.start(200); } private: KAsync::Future mFuture; std::function&)> mCallback; T mResult; QTimer mTimer; }; }; template<> class AsyncTest::AsyncSimulator { public: AsyncSimulator(KAsync::Future &future) : mFuture(future) { QObject::connect(&mTimer, &QTimer::timeout, [this]() { mFuture.setFinished(); }); QObject::connect(&mTimer, &QTimer::timeout, [this]() { delete this; }); mTimer.setSingleShot(true); mTimer.start(200); } private: KAsync::Future mFuture; QTimer mTimer; }; void AsyncTest::testSyncPromises() { auto baseJob = KAsync::start( [](KAsync::Future &f) { f.setValue(42); f.setFinished(); }) .then( [](int v, KAsync::Future &f) { f.setValue(QLatin1String("Result is ") + QString::number(v)); f.setFinished(); }); auto job = baseJob.then( [](const QString &v, KAsync::Future &f) { f.setValue(v.toUpper()); f.setFinished(); }); KAsync::Future future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), QString::fromLatin1("RESULT IS 42")); } void AsyncTest::testAsyncPromises() { auto job = KAsync::start( [](KAsync::Future &future) { new AsyncSimulator(future, 42); }); KAsync::Future future = job.exec(); future.waitForFinished(); QCOMPARE(future.value(), 42); } void AsyncTest::testAsyncPromises2() { bool done = false; auto job = KAsync::start( [](KAsync::Future &future) { new AsyncSimulator(future, 42); } ).then([&done](int result, KAsync::Future &future) { done = true; future.setValue(result); future.setFinished(); }); auto future = job.exec(); QTRY_VERIFY(done); QCOMPARE(future.value(), 42); } void AsyncTest::testNestedAsync() { bool done = false; auto job = KAsync::start( [](KAsync::Future &future) { auto innerJob = KAsync::start([](KAsync::Future &innerFuture) { new AsyncSimulator(innerFuture, 42); }).then([&future](KAsync::Future &innerThenFuture) { future.setFinished(); innerThenFuture.setFinished(); }); innerJob.exec().waitForFinished(); } ).then([&done](int result, KAsync::Future &future) { done = true; future.setValue(result); future.setFinished(); }); job.exec(); QTRY_VERIFY(done); } void AsyncTest::testStartValue() { auto job = KAsync::start( [](int in, KAsync::Future &future) { future.setValue(in); future.setFinished(); }); auto future = job.exec(42); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 42); } void AsyncTest::testAsyncThen() { auto job = KAsync::start( [](KAsync::Future &future) { new AsyncSimulator(future, 42); }); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 42); } void AsyncTest::testSyncThen() { auto job = KAsync::start( []() -> int { return 42; }) .then( [](int in) -> int { return in * 2; }); auto future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 84); } void AsyncTest::testJoinedThen() { auto job1 = KAsync::start( [](int in, KAsync::Future &future) { new AsyncSimulator(future, in * 2); }); auto job2 = KAsync::start( [](KAsync::Future &future) { new AsyncSimulator(future, 42); }) .then(job1); auto future = job2.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 84); } void AsyncTest::testVoidThen() { int check = 0; auto job = KAsync::start( [&check](KAsync::Future &future) { new AsyncSimulator(future); ++check; }) .then( [&check](KAsync::Future &future) { new AsyncSimulator(future); ++check; }) .then( [&check]() { ++check; }); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(check, 3); } void AsyncTest::testAsyncEach() { auto job = KAsync::start>( [](KAsync::Future> &future) { new AsyncSimulator>(future, { 1, 2, 3, 4 }); }) .each, int>( [](const int &v, KAsync::Future> &future) { new AsyncSimulator>(future, { v + 1 }); }); auto future = job.exec(); future.waitForFinished(); const QList expected({ 2, 3, 4, 5 }); QVERIFY(future.isFinished()); QCOMPARE(future.value(), expected); } void AsyncTest::testSyncEach() { auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }) .each, int>( [](const int &v) -> QList { return { v + 1 }; }); KAsync::Future> future = job.exec(); const QList expected({ 2, 3, 4, 5 }); QVERIFY(future.isFinished()); QCOMPARE(future.value(), expected); } void AsyncTest::testJoinedEach() { auto job1 = KAsync::start, int>( [](int v, KAsync::Future> &future) { new AsyncSimulator>(future, { v * 2 }); }); auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }) .each(job1); auto future = job.exec(); future.waitForFinished(); const QList expected({ 2, 4, 6, 8 }); QVERIFY(future.isFinished()); QCOMPARE(future.value(), expected); } void AsyncTest::testVoidEachThen() { QList check; auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }).each( [&check](const int &v) { check << v; }).then([](){}); auto future = job.exec(); const QList expected({ 1, 2, 3, 4 }); QVERIFY(future.isFinished()); QCOMPARE(check, expected); } void AsyncTest::testAsyncVoidEachThen() { bool completedJob = false; QList check; auto job = KAsync::start>( [](KAsync::Future > &future) { new AsyncSimulator>(future, { 1, 2, 3, 4 }); }).each( [&check](const int &v, KAsync::Future &future) { check << v; new AsyncSimulator(future); }).then([&completedJob](KAsync::Future &future) { completedJob = true; future.setFinished(); }); auto future = job.exec(); future.waitForFinished(); const QList expected({ 1, 2, 3, 4 }); QVERIFY(future.isFinished()); QVERIFY(completedJob); QCOMPARE(check, expected); } void AsyncTest::testAsyncReduce() { auto job = KAsync::start>( [](KAsync::Future> &future) { new AsyncSimulator>(future, { 1, 2, 3, 4 }); }) .reduce>( [](const QList &list, KAsync::Future &future) { new AsyncSimulator(future, [list](KAsync::Future &future) { int sum = 0; for (int i : list) sum += i; future.setValue(sum); future.setFinished(); } ); }); KAsync::Future future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 10); } void AsyncTest::testSyncReduce() { auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }) .reduce>( [](const QList &list) -> int { int sum = 0; for (int i : list) sum += i; return sum; }); KAsync::Future future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 10); } void AsyncTest::testJoinedReduce() { auto job1 = KAsync::start>( [](const QList &list, KAsync::Future &future) { int sum = 0; for (int i : list) sum += i; new AsyncSimulator(future, sum); }); auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }) .reduce(job1); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.value(), 10); } void AsyncTest::testVoidReduce() { // This must not compile (reduce with void result makes no sense) #ifdef TEST_BUILD_FAIL auto job = KAsync::start>( []() -> QList { return { 1, 2, 3, 4 }; }) .reduce>( [](const QList &list) -> int { return; }); auto future = job.exec(); QVERIFY(future.isFinished()); #endif } void AsyncTest::testProgressReporting() { static int progress; progress = 0; auto job = KAsync::start( [](KAsync::Future &f) { QTimer *timer = new QTimer(); connect(timer, &QTimer::timeout, [&f, timer]() { f.setProgress(++progress); if (progress == 100) { timer->stop(); timer->deleteLater(); f.setFinished(); } }); timer->start(1); }); int progressCheck = 0; KAsync::FutureWatcher watcher; connect(&watcher, &KAsync::FutureWatcher::futureProgress, [&progressCheck](qreal progress) { progressCheck++; // FIXME: Don't use Q_ASSERT in unit tests Q_ASSERT((int) progress == progressCheck); }); watcher.setFuture(job.exec()); watcher.future().waitForFinished(); QVERIFY(watcher.future().isFinished()); QCOMPARE(progressCheck, 100); } void AsyncTest::testErrorHandler() { { auto job = KAsync::start( [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); }); auto future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); } { int error = 0; auto job = KAsync::start( [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); }, [&error](int errorCode, const QString &errorMessage) { error += errorCode; } ); auto future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(error, 1); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); } } void AsyncTest::testErrorPropagation() { int error = 0; bool called = false; auto job = KAsync::start( [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); }) .then( [&called](int v, KAsync::Future &f) { called = true; f.setFinished(); }, [&error](int errorCode, const QString &errorMessage) { error += errorCode; } ); auto future = job.exec(); QVERIFY(future.isFinished()); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); QCOMPARE(called, false); QCOMPARE(error, 1); } void AsyncTest::testErrorHandlerAsync() { { auto job = KAsync::start( [](KAsync::Future &f) { new AsyncSimulator(f, [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); } ); } ); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); } { int error = 0; auto job = KAsync::start( [](KAsync::Future &f) { new AsyncSimulator(f, [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); } ); }, [&error](int errorCode, const QString &errorMessage) { error += errorCode; } ); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(error, 1); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); } } void AsyncTest::testErrorPropagationAsync() { int error = 0; bool called = false; auto job = KAsync::start( [](KAsync::Future &f) { new AsyncSimulator(f, [](KAsync::Future &f) { f.setError(1, QLatin1String("error")); } ); }) .then( [&called](int v, KAsync::Future &f) { called = true; f.setFinished(); }, [&error](int errorCode, const QString &errorMessage) { error += errorCode; } ); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); QCOMPARE(called, false); QCOMPARE(error, 1); } void AsyncTest::testNestedErrorPropagation() { int error = 0; auto job = KAsync::start([](){}) .then(KAsync::error(1, QLatin1String("error"))) //Nested job that throws error .then([](KAsync::Future &future) { //We should never get here Q_ASSERT(false); }, [&error](int errorCode, const QString &errorMessage) { error += errorCode; } ); auto future = job.exec(); future.waitForFinished(); QVERIFY(future.isFinished()); QCOMPARE(future.errorCode(), 1); QCOMPARE(future.errorMessage(), QString::fromLatin1("error")); QCOMPARE(error, 1); } void AsyncTest::testChainingRunningJob() { int check = 0; auto job = KAsync::start( [&check](KAsync::Future &future) { QTimer *timer = new QTimer(); QObject::connect(timer, &QTimer::timeout, [&future, &check]() { ++check; future.setValue(42); future.setFinished(); }); QObject::connect(timer, &QTimer::timeout, timer, &QObject::deleteLater); timer->setSingleShot(true); timer->start(500); }); auto future1 = job.exec(); QTest::qWait(200); auto job2 = job.then( [&check](int in) -> int { ++check; return in * 2; }); auto future2 = job2.exec(); QVERIFY(!future1.isFinished()); future2.waitForFinished(); QEXPECT_FAIL("", "Chaining new job to a running job no longer executes the new job. " "This is a trade-off for being able to re-execute single job multiple times.", Abort); QCOMPARE(check, 2); QVERIFY(future1.isFinished()); QVERIFY(future2.isFinished()); QCOMPARE(future1.value(), 42); QCOMPARE(future2.value(), 84); } void AsyncTest::testChainingFinishedJob() { int check = 0; auto job = KAsync::start( [&check]() -> int { ++check; return 42; }); auto future1 = job.exec(); QVERIFY(future1.isFinished()); auto job2 = job.then( [&check](int in) -> int { ++check; return in * 2; }); auto future2 = job2.exec(); QVERIFY(future2.isFinished()); QEXPECT_FAIL("", "Resuming finished job by chaining a new job and calling exec() is no longer suppported. " "This is a trade-off for being able to re-execute single job multiple times.", Abort); QCOMPARE(check, 2); QCOMPARE(future1.value(), 42); QCOMPARE(future2.value(), 84); } /* * We want to be able to execute jobs without keeping a handle explicitly alive. * If the future handle inside the continuation would keep the executor alive, that would probably already work. */ void AsyncTest::testLifetimeWithoutHandle() { bool done = false; { auto job = KAsync::start([&done](KAsync::Future &future) { QTimer *timer = new QTimer(); QObject::connect(timer, &QTimer::timeout, [&future, &done]() { done = true; future.setFinished(); }); QObject::connect(timer, &QTimer::timeout, timer, &QObject::deleteLater); timer->setSingleShot(true); timer->start(500); }); job.exec(); } QTRY_VERIFY(done); } /* * The future handle should keep the executor alive, and the future reference should probably not become invalid inside the continuation, * until the job is done (alternatively a copy of the future inside the continuation should work as well). */ void AsyncTest::testLifetimeWithHandle() { KAsync::Future future; { auto job = KAsync::start([](KAsync::Future &future) { QTimer *timer = new QTimer(); QObject::connect(timer, &QTimer::timeout, [&future]() { future.setFinished(); }); QObject::connect(timer, &QTimer::timeout, timer, &QObject::deleteLater); timer->setSingleShot(true); timer->start(500); }); future = job.exec(); } QTRY_VERIFY(future.isFinished()); } void AsyncTest::benchmarkSyncThenExecutor() { auto job = KAsync::start( []() -> int { return 0; }); QBENCHMARK { job.exec(); } } QTEST_MAIN(AsyncTest); #include "asynctest.moc"