Revision control
++ // Copyright (C) 2025 Jarek Kobus
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTaskTree/qttasktreeglobal.h>
++
++ #if QT_CONFIG(concurrent)
++ # include <QtTaskTree/qthreadfunctiontask.h>
++ #endif
++ #include <QtTaskTree/qbarriertask.h>
++ #include <QtTaskTree/qconditional.h>
++ #include <QtTaskTree/qtasktreerunner.h>
++
++ #include <QHash>
++ #include <QSignalSpy>
++ #include <QtTest>
++
++ using namespace QtTaskTree;
++
++ using namespace std::chrono;
++ using namespace std::chrono_literals;
++
++ using TaskObject = milliseconds;
++ using TestTask = QTimeoutTask;
++
++ constexpr milliseconds s_endlessTime = 1000000s;
++
++ namespace PrintableEnums {
++
++ Q_NAMESPACE
++
++ // TODO: Is it possible to check for synchronous invocation of subsequent events, so that
++ // we may be sure that the control didn't go back to the main event loop between 2 events?
++ // In theory: yes! We can add a signal / handler to the task tree sent before / after
++ // receiving done signal from each task!
++ // TODO: Check if the QTaskTree's main guard isn't locked when receiving done signal from each task.
++
++ enum class Handler {
++ Setup,
++ Success,
++ Error,
++ Canceled,
++ GroupSetup,
++ GroupSuccess,
++ GroupError,
++ GroupCanceled,
++ TweakSetupToSuccess,
++ TweakSetupToError,
++ TweakSetupToContinue,
++ TweakDoneToSuccess,
++ TweakDoneToError,
++ Sync,
++ BarrierAdvance,
++ Timeout,
++ Storage,
++ Iteration,
++ IteratorRangeGetter,
++ };
++ Q_ENUM_NS(Handler);
++
++ enum class ThreadResult
++ {
++ Success,
++ FailOnTaskCountCheck,
++ FailOnRunningCheck,
++ FailOnProgressCheck,
++ FailOnLogCheck,
++ FailOnDoneStatusCheck,
++ Canceled,
++ };
++ Q_ENUM_NS(ThreadResult);
++
++ enum class Execution
++ {
++ Sync,
++ Async,
++ };
++
++ } // namespace PrintableEnums
++
++ using namespace PrintableEnums;
++
++ using Log = QList<std::pair<int, Handler>>;
++
++ struct Message
++ {
++ QString name;
++ Handler handler;
++ std::optional<Execution> execution = std::nullopt;
++ };
++
++ using MessageLog = QList<Message>;
++
++ struct CustomStorage
++ {
++ CustomStorage() { s_count.fetch_add(1); }
++ ~CustomStorage() { s_count.fetch_add(-1); }
++ Log m_log;
++ static int instanceCount() { return s_count.load(); }
++ private:
++ static std::atomic_int s_count;
++ };
++
++ std::atomic_int CustomStorage::s_count = 0;
++
++ // Compile-time CTAD tests for ListIterator.
++ // Verifies that the deduction guide's forwarding-reference overload does not steal
++ // QList arguments from the const QList<T> & constructor (the underconstrained-guide hazard).
++ namespace {
++ [[maybe_unused]] void testListIteratorCTAD()
++ {
++ // --- QList constructor overload ---
++
++ // const lvalue: baseline case.
++ const QList<int> constList{1, 2, 3};
++ auto a = ListIterator(constList);
++ static_assert(std::is_same_v<decltype(a), ListIterator<int>>);
++
++ // non-const lvalue: without the enable_if constraint on the deduction guide,
++ // ListGetter&& would be a perfect match and beat const QList<T> &.
++ QList<int> mutableList{1, 2, 3};
++ auto b = ListIterator(mutableList);
++ static_assert(std::is_same_v<decltype(b), ListIterator<int>>);
++
++ // rvalue: same hazard as non-const lvalue.
++ auto c = ListIterator(QList<int>{1, 2, 3});
++ static_assert(std::is_same_v<decltype(c), ListIterator<int>>);
++
++ // Different value types.
++ auto d = ListIterator(QList<QString>{"a", "b"});
++ static_assert(std::is_same_v<decltype(d), ListIterator<QString>>);
++
++ auto e = ListIterator(QList<double>{1.0, 2.0});
++ static_assert(std::is_same_v<decltype(e), ListIterator<double>>);
++
++ // --- Deduction guide (callable) overload ---
++
++ // Lvalue lambda: T deduced from the return type's value_type.
++ auto getter = [] { return QList<int>{1, 2, 3}; };
++ auto f = ListIterator(getter);
++ static_assert(std::is_same_v<decltype(f), ListIterator<int>>);
++
++ // Rvalue (inline) lambda.
++ auto g = ListIterator([] { return QList<QString>{"a"}; });
++ static_assert(std::is_same_v<decltype(g), ListIterator<QString>>);
++
++ // std::function.
++ std::function<QList<double>()> fn = [] { return QList<double>{1.0}; };
++ auto h = ListIterator(fn);
++ static_assert(std::is_same_v<decltype(h), ListIterator<double>>);
++ }
++ } // unnamed namespace
++
++ struct TestData
++ {
++ Storage<CustomStorage> storage;
++ Group root;
++ Log expectedLog;
++ int taskCount = 0;
++ DoneWith result = DoneWith::Success;
++ std::optional<int> asyncCount = std::nullopt;
++ std::optional<MessageLog> messageLog = std::nullopt;
++ };
++
++ class tst_TaskTree : public QObject
++ {
++ Q_OBJECT
++
++ private Q_SLOTS:
++ void validConstructs(); // compile test
++ void runtimeCheck(); // checks done on runtime
++ void taskTreeRunner_data();
++ void taskTreeRunner();
++ void testTree_data();
++ void testTree();
++ #if QT_CONFIG(concurrent)
++ void testInThread_data();
++ void testInThread();
++ void testAutoDelayedSync();
++ #endif
++ void storageIO_data();
++ void storageIO();
++ void storageOperators();
++ void storageDestructor();
++ void storageInitialization_data();
++ void storageInitialization();
++ void nestedBrokenStorage();
++ void restart();
++ void destructorOfTaskEmittingDone();
++ void restartTaskTreeRunnerFromDoneHandler();
++ void validConditionalConstructs();
++ void exactHandlers();
++ void boolDoneAdapter();
++ void cleanupTestCase();
++ };
++
++ void tst_TaskTree::validConstructs()
++ {
++ const DoneResult result = DoneResult::Success;
++
++ const Group group1 {
++ parallel,
++ TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
++ TestTask([](TaskObject &) {}, [](const TaskObject &) {}),
++ TestTask([](TaskObject &) {}, [](DoneWith) {}),
++ TestTask([](TaskObject &) {}, [] {}),
++ TestTask([](TaskObject &) {}, {}),
++ TestTask([](TaskObject &) {}),
++ TestTask({}, [](const TaskObject &, DoneWith) {}),
++ TestTask({}, [](const TaskObject &) {}),
++ TestTask({}, [](DoneWith) {}),
++ TestTask({}, [] {}),
++ TestTask({}, {}),
++ TestTask({}, DoneResult::Error),
++ TestTask({}, result),
++ TestTask({})
++ };
++
++ const Group group2 {
++ group1
++ };
++
++ const Group group3 {
++ parallel,
++ Group {
++ parallel,
++ TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
++ Group {
++ parallel,
++ TestTask([](TaskObject &) {}, [](const TaskObject &) {}),
++ Group {
++ parallel,
++ TestTask([](TaskObject &) {}, [] {})
++ }
++ },
++ Group {
++ parallel,
++ TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
++ onGroupDone([] {})
++ }
++ },
++ group1,
++ onGroupDone([] {})
++ };
++
++ const auto setupHandler = [](TaskObject &) {};
++ const auto finishHandler = [](const TaskObject &) {};
++ const auto errorHandler = [](const TaskObject &) {};
++ const auto doneHandler = [](const TaskObject &, DoneWith) {};
++
++ const Group group4 {
++ parallel,
++ TestTask(),
++ TestTask(setupHandler),
++ TestTask(setupHandler, finishHandler, CallDoneFlag::OnSuccess),
++ TestTask(setupHandler, doneHandler),
++ // need to explicitly pass empty handler for done
++ TestTask(setupHandler, errorHandler, CallDoneFlag::OnError),
++ TestTask({}, finishHandler, CallDoneFlag::OnSuccess),
++ TestTask({}, errorHandler, CallDoneFlag::OnError)
++ };
++
++ const Group group5 {
++ Forever {
++ TestTask()
++ }.withTimeout(1s)
++ };
++
++ const Group group6 = Forever {
++ TestTask()
++ };
++
++ const Group group7 = Forever {
++ TestTask()
++ }.withTimeout(1s);
++
++ using GroupGetter = std::function<Group()>;
++
++ const GroupGetter getter = [] {
++ return Forever {
++ TestTask()
++ };
++ };
++
++ // When turning each of below blocks on, you should see the specific compiler error message.
++
++ // Sync handler needs to take no arguments and has to return void or bool.
++ #if 0
++ Sync([] { return 7; });
++ #endif
++ #if 0
++ Sync([](int) { });
++ #endif
++ #if 0
++ Sync([](int) { return true; });
++ #endif
++
++ // Group setup handler needs to take no arguments and has to return void or SetupResult.
++ #if 0
++ onGroupSetup([] { return 7; });
++ #endif
++ #if 0
++ onGroupSetup([](int) { });
++ #endif
++
++ // Group done handler needs to take (DoneWith) or (void) as an argument and has to
++ // return void or bool.
++ #if 0
++ onGroupDone([] { return 7; });
++ #endif
++ #if 0
++ onGroupDone([](DoneWith) { return 7; });
++ #endif
++ #if 0
++ onGroupDone([](int) { });
++ #endif
++ #if 0
++ onGroupDone([](DoneWith, int) { });
++ #endif
++
++ // Task setup handler needs to take (Task &) as an argument and has to return void or
++ // SetupResult.
++ #if 0
++ TestTask([] {});
++ #endif
++ #if 0
++ TestTask([] { return 7; });
++ #endif
++ #if 0
++ TestTask([](TaskObject &) { return 7; });
++ #endif
++ #if 0
++ TestTask([](TaskObject &, int) { return SetupResult::Continue; });
++ #endif
++
++ // Task done handler needs to take (const Task &, DoneWith), (const Task &),
++ // (DoneWith) or (void) as arguments and has to return void or bool.
++ #if 0
++ TestTask({}, [](const TaskObject &, DoneWith) { return 7; });
++ #endif
++ #if 0
++ TestTask({}, [](const TaskObject &) { return 7; });
++ #endif
++ #if 0
++ TestTask({}, [] { return 7; });
++ #endif
++ #if 0
++ TestTask({}, [](const TaskObject &, DoneWith, int) {});
++ #endif
++ #if 0
++ TestTask({}, [](const TaskObject &, int) {});
++ #endif
++ #if 0
++ TestTask({}, [](DoneWith, int) {});
++ #endif
++ #if 0
++ TestTask({}, [](int) {});
++ #endif
++ #if 0
++ TestTask({}, [](const TaskObject &, DoneWith, int) { return true; });
++ #endif
++ #if 0
++ TestTask({}, [](const TaskObject &, int) { return true; });
++ #endif
++ #if 0
++ TestTask({}, [](DoneWith, int) { return true; });
++ #endif
++ #if 0
++ TestTask({}, [](int) { return true; });
++ #endif
++ }
++
++ void tst_TaskTree::runtimeCheck()
++ {
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Can't add the same storage into one Group twice, skipping...");
++ const Storage<int> storage;
++
++ Group {
++ storage,
++ storage,
++ };
++ }
++
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Can't add the same storage into one Group twice, skipping...");
++ const Storage<int> storage1;
++ const auto storage2 = storage1;
++
++ Group {
++ storage1,
++ storage2,
++ };
++ }
++
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Group setup handler redefinition, overriding...");
++ Group {
++ onGroupSetup([] {}),
++ onGroupSetup([] {}),
++ };
++ }
++
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Group done handler redefinition, overriding...");
++ Group {
++ onGroupDone([] {}),
++ onGroupDone([] {}),
++ };
++ }
++
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Group execution mode redefinition, overriding...");
++ Group {
++ sequential,
++ sequential,
++ };
++ }
++
++ {
++ QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
++ QTest::ignoreMessage(QtWarningMsg,
++ "Group workflow policy redefinition, overriding...");
++ Group {
++ stopOnError,
++ stopOnError,
++ };
++ }
++ }
++
++ struct RunnerData
++ {
++ QList<std::pair<QString, Group>> recipes;
++ Log singleLog;
++ Log sequentialLog;
++ Log parallelLog;
++ Log mappedLog;
++ };
++
++ static Handler resultToGroupHandler(DoneWith doneWith)
++ {
++ switch (doneWith) {
++ case DoneWith::Success: return Handler::GroupSuccess;
++ case DoneWith::Error: return Handler::GroupError;
++ case DoneWith::Cancel: return Handler::GroupCanceled;
++ }
++ return Handler::GroupCanceled;
++ }
++
++ static int doneCount(const Log &log)
++ {
++ int count = 0;
++ for (const std::pair<int, Handler> &result : log) {
++ if (result.second == Handler::GroupSuccess ||
++ result.second == Handler::GroupError ||
++ result.second == Handler::GroupCanceled) {
++ ++count;
++ }
++ }
++ return count;
++ }
++
++ static Log s_globalLog = {};
++
++ void tst_TaskTree::taskTreeRunner_data()
++ {
++ QTest::addColumn<RunnerData>("runnerData");
++
++ const auto setupTask = [](int taskId, milliseconds timeout) {
++ return [taskId, timeout](TaskObject &taskObject) {
++ taskObject = timeout;
++ s_globalLog.append({taskId, Handler::Setup});
++ };
++ };
++
++ const auto setupDone = [](int taskId, DoneResult result = DoneResult::Success) {
++ return [taskId, result](DoneWith doneWith) {
++ const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
++ : result == DoneResult::Success ? Handler::Success : Handler::Error;
++ s_globalLog.append({taskId, handler});
++ return doneWith != DoneWith::Cancel && result == DoneResult::Success;
++ };
++ };
++
++ const auto createTask = [setupTask, setupDone](
++ int taskId, DoneResult result, milliseconds timeout = 0ms) {
++ return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
++ };
++
++ const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
++ return createTask(taskId, DoneResult::Success, timeout);
++ };
++
++ const auto createFailingTask = [createTask](int taskId, milliseconds timeout = 0ms) {
++ return createTask(taskId, DoneResult::Error, timeout);
++ };
++
++ const auto groupSetup = [](int taskId) {
++ return onGroupSetup([taskId] {
++ s_globalLog.append({taskId, Handler::GroupSetup});
++ });
++ };
++
++ const auto groupDone = [](int taskId, CallDone calldone = CallDoneFlag::Always) {
++ return onGroupDone([taskId](DoneWith result) {
++ s_globalLog.append({taskId, resultToGroupHandler(result)});
++ }, calldone);
++ };
++
++ {
++ const Group recipe_A {
++ groupSetup(1),
++ createSuccessTask(11),
++ createFailingTask(12),
++ groupDone(1)
++ };
++
++ const Group recipe_B {
++ groupSetup(2),
++ createSuccessTask(21),
++ groupDone(2)
++ };
++
++ const Group recipe_C {
++ groupSetup(3),
++ createFailingTask(31),
++ createSuccessTask(32),
++ groupDone(3)
++ };
++
++ const Log singleLog {
++ {1, Handler::GroupSetup},
++ {11, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {21, Handler::Setup},
++ {3, Handler::GroupSetup},
++ {31, Handler::Setup},
++ {31, Handler::Error},
++ {3, Handler::GroupError}
++ };
++
++ const Log sequentialLog {
++ {1, Handler::GroupSetup},
++ {11, Handler::Setup},
++ {11, Handler::Success},
++ {12, Handler::Setup},
++ {12, Handler::Error},
++ {1, Handler::GroupError},
++ {2, Handler::GroupSetup},
++ {21, Handler::Setup},
++ {21, Handler::Success},
++ {2, Handler::GroupSuccess},
++ {3, Handler::GroupSetup},
++ {31, Handler::Setup},
++ {31, Handler::Error},
++ {3, Handler::GroupError}
++ };
++
++ const Log parallelLog {
++ {1, Handler::GroupSetup},
++ {11, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {21, Handler::Setup},
++ {3, Handler::GroupSetup},
++ {31, Handler::Setup},
++ {11, Handler::Success},
++ {12, Handler::Setup},
++ {21, Handler::Success},
++ {2, Handler::GroupSuccess},
++ {31, Handler::Error},
++ {3, Handler::GroupError},
++ {12, Handler::Error},
++ {1, Handler::GroupError}
++ };
++
++ const Log mappedLog {
++ {1, Handler::GroupSetup},
++ {11, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {21, Handler::Setup},
++ {3, Handler::GroupSetup},
++ {31, Handler::Setup},
++ {21, Handler::Success},
++ {2, Handler::GroupSuccess},
++ {31, Handler::Error},
++ {3, Handler::GroupError}
++ };
++
++ QTest::newRow("TaskTreeRunner")
++ << RunnerData{{{"keyA", recipe_A}, {"keyB", recipe_B}, {"keyA", recipe_C}},
++ singleLog, sequentialLog, parallelLog, mappedLog};
++ }
++ }
++
++ void tst_TaskTree::taskTreeRunner()
++ {
++ QFETCH(RunnerData, runnerData);
++
++ int doneCounter = 0;
++ const auto onDone = [&doneCounter] { ++doneCounter; };
++
++ {
++ s_globalLog = {};
++ doneCounter = 0;
++ const Log expectedLog = runnerData.singleLog;
++ QSingleTaskTreeRunner taskTreeRunner;
++ for (const std::pair<QString, Group> &recipe : runnerData.recipes)
++ taskTreeRunner.start(recipe.second, {}, onDone);
++
++ QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);
++
++ QVERIFY(!taskTreeRunner.isRunning());
++ QCOMPARE(s_globalLog, expectedLog);
++ QCOMPARE(doneCounter, doneCount(expectedLog));
++ }
++
++ {
++ s_globalLog = {};
++ doneCounter = 0;
++ const Log expectedLog = runnerData.sequentialLog;
++ QSequentialTaskTreeRunner taskTreeRunner;
++ for (const std::pair<QString, Group> &recipe : runnerData.recipes)
++ taskTreeRunner.enqueue(recipe.second, {}, onDone);
++
++ QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);
++
++ QVERIFY(!taskTreeRunner.isRunning());
++ QCOMPARE(s_globalLog, expectedLog);
++ QCOMPARE(doneCounter, doneCount(expectedLog));
++ }
++
++ {
++ s_globalLog = {};
++ doneCounter = 0;
++ const Log expectedLog = runnerData.parallelLog;
++ QParallelTaskTreeRunner taskTreeRunner;
++ for (const std::pair<QString, Group> &recipe : runnerData.recipes)
++ taskTreeRunner.start(recipe.second, {}, onDone);
++
++ QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);
++
++ QVERIFY(!taskTreeRunner.isRunning());
++ QCOMPARE(s_globalLog, expectedLog);
++ QCOMPARE(doneCounter, doneCount(expectedLog));
++ }
++
++ {
++ s_globalLog = {};
++ doneCounter = 0;
++ const Log expectedLog = runnerData.mappedLog;
++ QMappedTaskTreeRunner<QString> taskTreeRunner;
++ for (const std::pair<QString, Group> &recipe : runnerData.recipes)
++ taskTreeRunner.start(recipe.first, recipe.second, {}, onDone);
++
++ QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);
++
++ QVERIFY(!taskTreeRunner.isRunning());
++ QCOMPARE(s_globalLog, expectedLog);
++ QCOMPARE(doneCounter, doneCount(expectedLog));
++ }
++
++ s_globalLog = {};
++ }
++
++ class TickAndDone final : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ void setInterval(const milliseconds &interval) { m_interval = interval; }
++ void start() {
++ QTimer::singleShot(0, this, [this] {
++ Q_EMIT tick();
++ QTimer::singleShot(m_interval, this, [this] { Q_EMIT done(DoneResult::Success); });
++ });
++ }
++
++ Q_SIGNALS:
++ void tick();
++ void done(QtTaskTree::DoneResult result);
++
++ private:
++ milliseconds m_interval;
++ };
++
++ using TickAndDoneTask = QCustomTask<TickAndDone>;
++
++ static ExecutableItem createBarrierAdvance(const Storage<CustomStorage> &storage,
++ const QStoredBarrier &barrier, int taskId)
++ {
++ return TickAndDoneTask([storage, barrier, taskId](TickAndDone &tickAndDone) {
++ tickAndDone.setInterval(1ms);
++ storage->m_log.append({taskId, Handler::Setup});
++
++ CustomStorage *currentStorage = storage.activeStorage();
++ QBarrier *activeBarrier = barrier.activeStorage();
++ QObject::connect(&tickAndDone, &TickAndDone::tick, activeBarrier,
++ [currentStorage, activeBarrier, taskId] {
++ currentStorage->m_log.append({taskId, Handler::BarrierAdvance});
++ activeBarrier->advance();
++ });
++ });
++ }
++
++ static Handler toTweakSetupHandler(SetupResult result)
++ {
++ switch (result) {
++ case SetupResult::Continue: return Handler::TweakSetupToContinue;
++ case SetupResult::StopWithSuccess: return Handler::TweakSetupToSuccess;
++ case SetupResult::StopWithError: return Handler::TweakSetupToError;
++ }
++ return Handler::TweakSetupToContinue;
++ }
++
++ static Handler toTweakDoneHandler(DoneResult result)
++ {
++ return result == DoneResult::Success ? Handler::TweakDoneToSuccess : Handler::TweakDoneToError;
++ }
++
++ static TestData storageShadowingData()
++ {
++ // This test checks if storage shadowing works OK.
++
++ const Storage<CustomStorage> storage;
++ // This helper storage collect the pointers to storages created by shadowedStorage.
++ const Storage<QHash<int, int *>> helperStorage; // One instance in this test.
++ // This storage is repeated in nested groups, the innermost storage will shadow outer ones.
++ const Storage<int> shadowedStorage; // Three instances in this test.
++
++ const auto groupSetupWithStorage = [storage, helperStorage, shadowedStorage](int taskId) {
++ return onGroupSetup([storage, helperStorage, shadowedStorage, taskId] {
++ storage->m_log.append({taskId, Handler::GroupSetup});
++ helperStorage->insert(taskId, shadowedStorage.activeStorage());
++ *shadowedStorage = taskId;
++ });
++ };
++ const auto groupDoneWithStorage = [storage, helperStorage, shadowedStorage](int taskId) {
++ return onGroupDone([storage, helperStorage, shadowedStorage, taskId](DoneWith result) {
++ storage->m_log.append({taskId, resultToGroupHandler(result)});
++ auto it = helperStorage->constFind(taskId);
++ if (it == helperStorage->constEnd()) {
++ qWarning() << "The helperStorage is missing the shadowedStorage.";
++ return;
++ } else if (*it != shadowedStorage.activeStorage()) {
++ qWarning() << "Wrong active instance of the shadowedStorage.";
++ return;
++ } else if (**it != taskId) {
++ qWarning() << "Wrong data of the active instance of the shadowedStorage.";
++ return;
++ }
++ helperStorage->erase(it);
++ storage->m_log.append({*shadowedStorage, Handler::Storage});
++ });
++ };
++
++ const Group root {
++ storage,
++ helperStorage,
++ shadowedStorage,
++ groupSetupWithStorage(1),
++ Group {
++ shadowedStorage,
++ groupSetupWithStorage(2),
++ Group {
++ shadowedStorage,
++ groupSetupWithStorage(3),
++ groupDoneWithStorage(3)
++ },
++ Group {
++ shadowedStorage,
++ groupSetupWithStorage(4),
++ groupDoneWithStorage(4)
++ },
++ groupDoneWithStorage(2)
++ },
++ groupDoneWithStorage(1)
++ };
++
++ const Log log {
++ {1, Handler::GroupSetup},
++ {2, Handler::GroupSetup},
++ {3, Handler::GroupSetup},
++ {3, Handler::GroupSuccess},
++ {3, Handler::Storage},
++ {4, Handler::GroupSetup},
++ {4, Handler::GroupSuccess},
++ {4, Handler::Storage},
++ {2, Handler::GroupSuccess},
++ {2, Handler::Storage},
++ {1, Handler::GroupSuccess},
++ {1, Handler::Storage},
++ };
++
++ return {storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ static TestData parallelData()
++ {
++ Storage<CustomStorage> storage;
++
++ const auto setupTask = [storage](int taskId, milliseconds timeout) {
++ return [storage, taskId, timeout](TaskObject &taskObject) {
++ taskObject = timeout;
++ storage->m_log.append({taskId, Handler::Setup});
++ };
++ };
++
++ const auto setupDone = [storage](int taskId, DoneResult result = DoneResult::Success) {
++ return [storage, taskId, result](DoneWith doneWith) {
++ const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
++ : result == DoneResult::Success ? Handler::Success : Handler::Error;
++ storage->m_log.append({taskId, handler});
++ return doneWith != DoneWith::Cancel && result == DoneResult::Success;
++ };
++ };
++
++ const auto createTask = [storage, setupTask, setupDone](
++ int taskId, DoneResult result, milliseconds timeout = 0ms) {
++ return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
++ };
++
++ const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
++ return createTask(taskId, DoneResult::Success, timeout);
++ };
++
++ const auto groupDone = [storage](int taskId) {
++ return onGroupDone([storage, taskId](DoneWith result) {
++ storage->m_log.append({taskId, resultToGroupHandler(result)});
++ });
++ };
++
++ const Group root {
++ storage,
++ parallel,
++ createSuccessTask(1),
++ createSuccessTask(2),
++ createSuccessTask(3),
++ createSuccessTask(4),
++ createSuccessTask(5),
++ groupDone(0)
++ };
++
++ const Log log {
++ {1, Handler::Setup}, // Setup order is determined in parallel mode
++ {2, Handler::Setup},
++ {3, Handler::Setup},
++ {4, Handler::Setup},
++ {5, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Success},
++ {3, Handler::Success},
++ {4, Handler::Success},
++ {5, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++
++ return {storage, root, log, 5, DoneWith::Success};
++ }
++
++ void tst_TaskTree::testTree_data()
++ {
++ QTest::addColumn<TestData>("testData");
++
++ Storage<CustomStorage> storage;
++
++ const auto setupTask = [storage](int taskId, milliseconds timeout) {
++ return [storage, taskId, timeout](TaskObject &taskObject) {
++ taskObject = timeout;
++ storage->m_log.append({taskId, Handler::Setup});
++ };
++ };
++
++ const auto setupTaskWithTweak = [storage](int taskId, SetupResult result) {
++ return [storage, taskId, result](TaskObject &) {
++ storage->m_log.append({taskId, Handler::Setup});
++ storage->m_log.append({taskId, toTweakSetupHandler(result)});
++ return result;
++ };
++ };
++
++ const auto setupDone = [storage](int taskId, DoneResult result = DoneResult::Success) {
++ return [storage, taskId, result](DoneWith doneWith) {
++ const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
++ : result == DoneResult::Success ? Handler::Success : Handler::Error;
++ storage->m_log.append({taskId, handler});
++ return doneWith != DoneWith::Cancel && result == DoneResult::Success;
++ };
++ };
++
++ const auto setupTimeout = [storage](int taskId) {
++ return [storage, taskId] {
++ storage->m_log.append({taskId, Handler::Timeout});
++ };
++ };
++
++ const auto createTask = [storage, setupTask, setupDone](
++ int taskId, DoneResult result, milliseconds timeout = 0ms) {
++ return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
++ };
++
++ const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
++ return createTask(taskId, DoneResult::Success, timeout);
++ };
++
++ const auto createFailingTask = [createTask](int taskId, milliseconds timeout = 0ms) {
++ return createTask(taskId, DoneResult::Error, timeout);
++ };
++
++ const auto createTaskWithSetupTweak = [storage, setupTaskWithTweak, setupDone](
++ int taskId, SetupResult desiredResult) {
++ return TestTask(setupTaskWithTweak(taskId, desiredResult), setupDone(taskId));
++ };
++
++ const auto groupSetup = [storage](int taskId) {
++ return onGroupSetup([storage, taskId] {
++ storage->m_log.append({taskId, Handler::GroupSetup});
++ });
++ };
++ const auto groupDone = [storage](int taskId, CallDone callDone = CallDoneFlag::Always) {
++ return onGroupDone([storage, taskId](DoneWith result) {
++ storage->m_log.append({taskId, resultToGroupHandler(result)});
++ }, callDone);
++ };
++ const auto groupSetupWithTweak = [storage](int taskId, SetupResult desiredResult) {
++ return onGroupSetup([storage, taskId, desiredResult] {
++ storage->m_log.append({taskId, Handler::GroupSetup});
++ storage->m_log.append({taskId, toTweakSetupHandler(desiredResult)});
++ return desiredResult;
++ });
++ };
++ const auto groupDoneWithTweak = [storage](int taskId, DoneResult result) {
++ return onGroupDone([storage, taskId, result](DoneWith doneWith) {
++ storage->m_log.append({taskId, resultToGroupHandler(doneWith)});
++ storage->m_log.append({taskId, toTweakDoneHandler(result)});
++ return result;
++ });
++ };
++ const auto createSync = [storage](int taskId) {
++ return QSyncTask([storage, taskId] { storage->m_log.append({taskId, Handler::Sync}); });
++ };
++ const auto createSyncWithTweak = [storage](int taskId, DoneResult result) {
++ return QSyncTask([storage, taskId, result] {
++ storage->m_log.append({taskId, Handler::Sync});
++ storage->m_log.append({taskId, toTweakDoneHandler(result)});
++ return result;
++ });
++ };
++
++ {
++ const Group root1 {
++ storage,
++ groupDone(0)
++ };
++ const Group root2 {
++ storage,
++ onGroupSetup([] { return SetupResult::Continue; }),
++ groupDone(0)
++ };
++ const Group root3 {
++ storage,
++ onGroupSetup([] { return SetupResult::StopWithSuccess; }),
++ groupDone(0)
++ };
++ const Group root4 {
++ storage,
++ onGroupSetup([] { return SetupResult::StopWithError; }),
++ groupDone(0)
++ };
++
++ const Log logDone {{0, Handler::GroupSuccess}};
++ const Log logError {{0, Handler::GroupError}};
++
++ QTest::newRow("Empty") << TestData{storage, root1, logDone, 0, DoneWith::Success, 0};
++ QTest::newRow("EmptyContinue") << TestData{storage, root2, logDone, 0, DoneWith::Success, 0};
++ QTest::newRow("EmptyDone") << TestData{storage, root3, logDone, 0, DoneWith::Success, 0};
++ QTest::newRow("EmptyError") << TestData{storage, root4, logError, 0, DoneWith::Error, 0};
++ }
++
++ {
++ const auto setupGroup = [=](SetupResult setupResult, WorkflowPolicy policy) {
++ return Group {
++ storage,
++ workflowPolicy(policy),
++ onGroupSetup([setupResult] { return setupResult; }),
++ groupDone(0)
++ };
++ };
++
++ const auto doneData = [storage, setupGroup](WorkflowPolicy policy) {
++ return TestData{storage, setupGroup(SetupResult::StopWithSuccess, policy),
++ Log{{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
++ };
++ const auto errorData = [storage, setupGroup](WorkflowPolicy policy) {
++ return TestData{storage, setupGroup(SetupResult::StopWithError, policy),
++ Log{{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
++ };
++
++ QTest::newRow("DoneAndStopOnError") << doneData(WorkflowPolicy::StopOnError);
++ QTest::newRow("DoneAndContinueOnError") << doneData(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("DoneAndStopOnSuccess") << doneData(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("DoneAndContinueOnSuccess") << doneData(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("DoneAndStopOnSuccessOrError") << doneData(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("DoneAndFinishAllAndSuccess") << doneData(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("DoneAndFinishAllAndError") << doneData(WorkflowPolicy::FinishAllAndError);
++
++ QTest::newRow("ErrorAndStopOnError") << errorData(WorkflowPolicy::StopOnError);
++ QTest::newRow("ErrorAndContinueOnError") << errorData(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("ErrorAndStopOnSuccess") << errorData(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("ErrorAndContinueOnSuccess") << errorData(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("ErrorAndStopOnSuccessOrError") << errorData(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("ErrorAndFinishAllAndSuccess") << errorData(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("ErrorAndFinishAllAndError") << errorData(WorkflowPolicy::FinishAllAndError);
++ }
++
++ {
++ // These tests ensure that tweaking the done result in group's done handler takes priority
++ // over the group's workflow policy. In this case the group's workflow policy is ignored.
++ const auto setupGroup = [=](DoneResult doneResult, WorkflowPolicy policy) {
++ return Group {
++ storage,
++ Group {
++ workflowPolicy(policy),
++ onGroupDone([doneResult] { return doneResult; })
++ },
++ groupDone(0)
++ };
++ };
++
++ const auto doneData = [storage, setupGroup](WorkflowPolicy policy) {
++ return TestData{storage, setupGroup(DoneResult::Success, policy),
++ Log{{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
++ };
++ const auto errorData = [storage, setupGroup](WorkflowPolicy policy) {
++ return TestData{storage, setupGroup(DoneResult::Error, policy),
++ Log{{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
++ };
++
++ QTest::newRow("GroupDoneTweakSuccessWithStopOnError")
++ << doneData(WorkflowPolicy::StopOnError);
++ QTest::newRow("GroupDoneTweakSuccessWithContinueOnError")
++ << doneData(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("GroupDoneTweakSuccessWithStopOnSuccess")
++ << doneData(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("GroupDoneTweakSuccessWithContinueOnSuccess")
++ << doneData(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("GroupDoneTweakSuccessWithStopOnSuccessOrError")
++ << doneData(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("GroupDoneTweakSuccessWithFinishAllAndSuccess")
++ << doneData(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("GroupDoneTweakSuccessWithFinishAllAndError")
++ << doneData(WorkflowPolicy::FinishAllAndError);
++
++ QTest::newRow("GroupDoneTweakErrorWithStopOnError")
++ << errorData(WorkflowPolicy::StopOnError);
++ QTest::newRow("GroupDoneTweakErrorWithContinueOnError")
++ << errorData(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("GroupDoneTweakErrorWithStopOnSuccess")
++ << errorData(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("GroupDoneTweakErrorWithContinueOnSuccess")
++ << errorData(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("GroupDoneTweakErrorWithStopOnSuccessOrError")
++ << errorData(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("GroupDoneTweakErrorWithFinishAllAndSuccess")
++ << errorData(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("GroupDoneTweakErrorWithFinishAllAndError")
++ << errorData(WorkflowPolicy::FinishAllAndError);
++ }
++
++ {
++ const Group root {
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::StopWithSuccess),
++ createTaskWithSetupTweak(2, SetupResult::StopWithSuccess)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToSuccess},
++ {2, Handler::Setup},
++ {2, Handler::TweakSetupToSuccess}
++ };
++ QTest::newRow("TweakTaskSuccess") << TestData{storage, root, log, 2, DoneWith::Success, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::StopWithError),
++ createTaskWithSetupTweak(2, SetupResult::StopWithError)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToError}
++ };
++ QTest::newRow("TweakTaskError") << TestData{storage, root, log, 2, DoneWith::Error, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::Continue),
++ createTaskWithSetupTweak(2, SetupResult::Continue),
++ createTaskWithSetupTweak(3, SetupResult::StopWithError),
++ createTaskWithSetupTweak(4, SetupResult::Continue)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToContinue},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::TweakSetupToContinue},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError}
++ };
++ QTest::newRow("TweakMixed") << TestData{storage, root, log, 4, DoneWith::Error, 2};
++ }
++
++ {
++ const Group root {
++ parallel,
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::Continue),
++ createTaskWithSetupTweak(2, SetupResult::Continue),
++ createTaskWithSetupTweak(3, SetupResult::StopWithError),
++ createTaskWithSetupTweak(4, SetupResult::Continue)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToContinue},
++ {2, Handler::Setup},
++ {2, Handler::TweakSetupToContinue},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {1, Handler::Canceled},
++ {2, Handler::Canceled}
++ };
++ QTest::newRow("TweakParallel") << TestData{storage, root, log, 4, DoneWith::Error, 0};
++ }
++
++ {
++ const Group root {
++ parallel,
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::Continue),
++ createTaskWithSetupTweak(2, SetupResult::Continue),
++ Group {
++ createTaskWithSetupTweak(3, SetupResult::StopWithError)
++ },
++ createTaskWithSetupTweak(4, SetupResult::Continue)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToContinue},
++ {2, Handler::Setup},
++ {2, Handler::TweakSetupToContinue},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {1, Handler::Canceled},
++ {2, Handler::Canceled}
++ };
++ QTest::newRow("TweakParallelGroup") << TestData{storage, root, log, 4, DoneWith::Error, 0};
++ }
++
++ {
++ const Group root {
++ parallel,
++ storage,
++ createTaskWithSetupTweak(1, SetupResult::Continue),
++ createTaskWithSetupTweak(2, SetupResult::Continue),
++ Group {
++ groupSetupWithTweak(0, SetupResult::StopWithError),
++ createTaskWithSetupTweak(3, SetupResult::Continue)
++ },
++ createTaskWithSetupTweak(4, SetupResult::Continue)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToContinue},
++ {2, Handler::Setup},
++ {2, Handler::TweakSetupToContinue},
++ {0, Handler::GroupSetup},
++ {0, Handler::TweakSetupToError},
++ {1, Handler::Canceled},
++ {2, Handler::Canceled}
++ };
++ QTest::newRow("TweakParallelGroupSetup")
++ << TestData{storage, root, log, 4, DoneWith::Error, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ Group {
++ Group {
++ Group {
++ Group {
++ Group {
++ createSuccessTask(5),
++ groupSetup(5),
++ groupDone(5)
++ },
++ groupSetup(4),
++ groupDone(4)
++ },
++ groupSetup(3),
++ groupDone(3)
++ },
++ groupSetup(2),
++ groupDone(2)
++ },
++ groupSetup(1),
++ groupDone(1)
++ },
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {2, Handler::GroupSetup},
++ {3, Handler::GroupSetup},
++ {4, Handler::GroupSetup},
++ {5, Handler::GroupSetup},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {5, Handler::GroupSuccess},
++ {4, Handler::GroupSuccess},
++ {3, Handler::GroupSuccess},
++ {2, Handler::GroupSuccess},
++ {1, Handler::GroupSuccess},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("Nested") << TestData{storage, root, log, 1, DoneWith::Success, 1};
++ }
++
++ QTest::newRow("Parallel") << parallelData();
++
++ {
++ auto setupSubTree = [storage, createSuccessTask](QTaskTree &taskTree) {
++ const Group nestedRoot {
++ storage,
++ createSuccessTask(2),
++ createSuccessTask(3),
++ createSuccessTask(4)
++ };
++ taskTree.setRecipe(nestedRoot);
++ CustomStorage *activeStorage = storage.activeStorage();
++ const auto collectSubLog = [activeStorage](const CustomStorage &subTreeStorage){
++ activeStorage->m_log += subTreeStorage.m_log;
++ };
++ taskTree.onStorageDone(storage, collectSubLog);
++ };
++ const Group root1 {
++ storage,
++ createSuccessTask(1),
++ createSuccessTask(2),
++ createSuccessTask(3),
++ createSuccessTask(4),
++ createSuccessTask(5),
++ groupDone(0)
++ };
++ const Group root2 {
++ storage,
++ Group { createSuccessTask(1) },
++ Group { createSuccessTask(2) },
++ Group { createSuccessTask(3) },
++ Group { createSuccessTask(4) },
++ Group { createSuccessTask(5) },
++ groupDone(0)
++ };
++ const Group root3 {
++ storage,
++ createSuccessTask(1),
++ QTaskTreeTask(setupSubTree),
++ createSuccessTask(5),
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("Sequential") << TestData{storage, root1, log, 5, DoneWith::Success, 5};
++ QTest::newRow("SequentialEncapsulated") << TestData{storage, root2, log, 5, DoneWith::Success, 5};
++ // We don't inspect subtrees, so taskCount is 3, not 5.
++ QTest::newRow("SequentialSubTree") << TestData{storage, root3, log, 3, DoneWith::Success, 3};
++ }
++
++ {
++ const Group root {
++ storage,
++ Group {
++ createSuccessTask(1),
++ Group {
++ createSuccessTask(2),
++ Group {
++ createSuccessTask(3),
++ Group {
++ createSuccessTask(4),
++ Group {
++ createSuccessTask(5),
++ groupDone(5)
++ },
++ groupDone(4)
++ },
++ groupDone(3)
++ },
++ groupDone(2)
++ },
++ groupDone(1)
++ },
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {5, Handler::GroupSuccess},
++ {4, Handler::GroupSuccess},
++ {3, Handler::GroupSuccess},
++ {2, Handler::GroupSuccess},
++ {1, Handler::GroupSuccess},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("SequentialNested") << TestData{storage, root, log, 5, DoneWith::Success, 5};
++ }
++
++ {
++ const Group root {
++ storage,
++ createSuccessTask(1),
++ createSuccessTask(2),
++ createFailingTask(3),
++ createSuccessTask(4),
++ createSuccessTask(5),
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Error},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("SequentialError") << TestData{storage, root, log, 5, DoneWith::Error, 3};
++ }
++
++ {
++ const auto testData = [storage, groupDone](WorkflowPolicy policy, DoneWith result) {
++ return TestData {
++ storage,
++ Group {
++ storage,
++ workflowPolicy(policy),
++ groupDone(0)
++ },
++ Log {{0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}},
++ 0,
++ result,
++ 0
++ };
++ };
++
++ QTest::newRow("EmptyStopOnError")
++ << testData(WorkflowPolicy::StopOnError, DoneWith::Success);
++ QTest::newRow("EmptyContinueOnError")
++ << testData(WorkflowPolicy::ContinueOnError, DoneWith::Success);
++ QTest::newRow("EmptyStopOnSuccess")
++ << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Error);
++ QTest::newRow("EmptyContinueOnSuccess")
++ << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Error);
++ QTest::newRow("EmptyStopOnSuccessOrError")
++ << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Error);
++ QTest::newRow("EmptyFinishAllAndSuccess")
++ << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
++ QTest::newRow("EmptyFinishAllAndError")
++ << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
++ }
++
++ {
++ const auto testData = [storage, groupDone, createSuccessTask](WorkflowPolicy policy,
++ DoneWith result) {
++ return TestData {
++ storage,
++ Group {
++ storage,
++ workflowPolicy(policy),
++ createSuccessTask(1),
++ groupDone(0)
++ },
++ Log {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}
++ },
++ 1,
++ result,
++ 1
++ };
++ };
++
++ QTest::newRow("DoneStopOnError")
++ << testData(WorkflowPolicy::StopOnError, DoneWith::Success);
++ QTest::newRow("DoneContinueOnError")
++ << testData(WorkflowPolicy::ContinueOnError, DoneWith::Success);
++ QTest::newRow("DoneStopOnSuccess")
++ << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Success);
++ QTest::newRow("DoneContinueOnSuccess")
++ << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Success);
++ QTest::newRow("DoneStopOnSuccessOrError")
++ << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Success);
++ QTest::newRow("DoneFinishAllAndSuccess")
++ << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
++ QTest::newRow("DoneFinishAllAndError")
++ << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
++ }
++
++ {
++ const auto testData = [storage, groupDone, createFailingTask](WorkflowPolicy policy,
++ DoneWith result) {
++ return TestData {
++ storage,
++ Group {
++ storage,
++ workflowPolicy(policy),
++ createFailingTask(1),
++ groupDone(0)
++ },
++ Log {
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ {0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}
++ },
++ 1,
++ result,
++ 1
++ };
++ };
++
++ QTest::newRow("ErrorStopOnError")
++ << testData(WorkflowPolicy::StopOnError, DoneWith::Error);
++ QTest::newRow("ErrorContinueOnError")
++ << testData(WorkflowPolicy::ContinueOnError, DoneWith::Error);
++ QTest::newRow("ErrorStopOnSuccess")
++ << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Error);
++ QTest::newRow("ErrorContinueOnSuccess")
++ << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Error);
++ QTest::newRow("ErrorStopOnSuccessOrError")
++ << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Error);
++ QTest::newRow("ErrorFinishAllAndSuccess")
++ << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
++ QTest::newRow("ErrorFinishAllAndError")
++ << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
++ }
++
++ {
++ // These tests check whether the proper root's group end handler is called
++ // when the group is stopped. Test it with different workflow policies.
++ // The root starts one short failing task together with one long task.
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ parallel,
++ workflowPolicy(policy),
++ createFailingTask(1, 1ms),
++ createSuccessTask(2, 1ms),
++ groupDone(0)
++ };
++ };
++
++ const Log errorErrorLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Error},
++ {2, Handler::Canceled},
++ {0, Handler::GroupError}
++ };
++
++ const Log errorDoneLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Error},
++ {2, Handler::Success},
++ {0, Handler::GroupError}
++ };
++
++ const Log doneLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Error},
++ {2, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ QTest::newRow("StopRootWithStopOnError")
++ << TestData{storage, root1, errorErrorLog, 2, DoneWith::Error, 1};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("StopRootWithContinueOnError")
++ << TestData{storage, root2, errorDoneLog, 2, DoneWith::Error, 2};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("StopRootWithStopOnSuccess")
++ << TestData{storage, root3, doneLog, 2, DoneWith::Success, 2};
++
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("StopRootWithContinueOnSuccess")
++ << TestData{storage, root4, doneLog, 2, DoneWith::Success, 2};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("StopRootWithStopOnSuccessOrError")
++ << TestData{storage, root5, errorErrorLog, 2, DoneWith::Error, 1};
++
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("StopRootWithFinishAllAndSuccess")
++ << TestData{storage, root6, doneLog, 2, DoneWith::Success, 2};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("StopRootWithFinishAllAndError")
++ << TestData{storage, root7, errorDoneLog, 2, DoneWith::Error, 2};
++ }
++
++ {
++ // These tests check whether the proper root's group end handler is called
++ // when the group is stopped. Test it with different workflow policies.
++ // The root starts in parallel: one very short successful task, one short failing task
++ // and one long task.
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ parallel,
++ workflowPolicy(policy),
++ createSuccessTask(1),
++ createFailingTask(2, 1ms),
++ createSuccessTask(3, 1ms),
++ groupDone(0)
++ };
++ };
++
++ const Log errorErrorLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Error},
++ {3, Handler::Canceled},
++ {0, Handler::GroupError}
++ };
++
++ const Log errorDoneLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Error},
++ {3, Handler::Success},
++ {0, Handler::GroupError}
++ };
++
++ const Log doneErrorLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Canceled},
++ {3, Handler::Canceled},
++ {0, Handler::GroupSuccess}
++ };
++
++ const Log doneDoneLog = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Error},
++ {3, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ QTest::newRow("StopRootAfterDoneWithStopOnError")
++ << TestData{storage, root1, errorErrorLog, 3, DoneWith::Error, 2};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("StopRootAfterDoneWithContinueOnError")
++ << TestData{storage, root2, errorDoneLog, 3, DoneWith::Error, 3};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("StopRootAfterDoneWithStopOnSuccess")
++ << TestData{storage, root3, doneErrorLog, 3, DoneWith::Success, 1};
++
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("StopRootAfterDoneWithContinueOnSuccess")
++ << TestData{storage, root4, doneDoneLog, 3, DoneWith::Success, 3};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("StopRootAfterDoneWithStopOnSuccessOrError")
++ << TestData{storage, root5, doneErrorLog, 3, DoneWith::Success, 1};
++
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("StopRootAfterDoneWithFinishAllAndSuccess")
++ << TestData{storage, root6, doneDoneLog, 3, DoneWith::Success, 3};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("StopRootAfterDoneWithFinishAllAndError")
++ << TestData{storage, root7, errorDoneLog, 3, DoneWith::Error, 3};
++ }
++
++ {
++ // These tests check whether the proper subgroup's end handler is called
++ // when the group is stopped. Test it with different workflow policies.
++ // The subgroup starts one long task.
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ parallel,
++ Group {
++ workflowPolicy(policy),
++ createSuccessTask(1, 1000ms),
++ groupDone(1)
++ },
++ createFailingTask(2, 1ms),
++ groupDone(2)
++ };
++ };
++
++ const Log log = {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {1, Handler::Canceled},
++ {1, Handler::GroupCanceled},
++ {2, Handler::GroupError}
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ QTest::newRow("StopGroupWithStopOnError")
++ << TestData{storage, root1, log, 2, DoneWith::Error, 1};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("StopGroupWithContinueOnError")
++ << TestData{storage, root2, log, 2, DoneWith::Error, 1};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("StopGroupWithStopOnSuccess")
++ << TestData{storage, root3, log, 2, DoneWith::Error, 1};
++
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("StopGroupWithContinueOnSuccess")
++ << TestData{storage, root4, log, 2, DoneWith::Error, 1};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("StopGroupWithStopOnSuccessOrError")
++ << TestData{storage, root5, log, 2, DoneWith::Error, 1};
++
++ // TODO: Behavioral change! Fix Docs!
++ // Cancellation always invokes error handler (i.e. DoneWith is Canceled)
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("StopGroupWithFinishAllAndSuccess")
++ << TestData{storage, root6, log, 2, DoneWith::Error, 1};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("StopGroupWithFinishAllAndError")
++ << TestData{storage, root7, log, 2, DoneWith::Error, 1};
++ }
++
++ {
++ // These tests check whether the proper subgroup's end handler is called
++ // when the group is stopped. Test it with different workflow policies.
++ // The sequential subgroup starts one short successful task followed by one long task.
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ parallel,
++ Group {
++ workflowPolicy(policy),
++ createSuccessTask(1),
++ createSuccessTask(2, 1000ms),
++ groupDone(1)
++ },
++ createFailingTask(3, 1ms),
++ groupDone(2)
++ };
++ };
++
++ const Log errorLog = {
++ {1, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {3, Handler::Error},
++ {2, Handler::Canceled},
++ {1, Handler::GroupCanceled},
++ {2, Handler::GroupError}
++ };
++
++ const Log doneLog = {
++ {1, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Success},
++ {1, Handler::GroupSuccess},
++ {3, Handler::Error},
++ {2, Handler::GroupError}
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ QTest::newRow("StopGroupAfterDoneWithStopOnError")
++ << TestData{storage, root1, errorLog, 3, DoneWith::Error, 2};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("StopGroupAfterDoneWithContinueOnError")
++ << TestData{storage, root2, errorLog, 3, DoneWith::Error, 2};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("StopGroupAfterDoneWithStopOnSuccess")
++ << TestData{storage, root3, doneLog, 3, DoneWith::Error, 2};
++
++ // TODO: Behavioral change!
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("StopGroupAfterDoneWithContinueOnSuccess")
++ << TestData{storage, root4, errorLog, 3, DoneWith::Error, 2};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("StopGroupAfterDoneWithStopOnSuccessOrError")
++ << TestData{storage, root5, doneLog, 3, DoneWith::Error, 2};
++
++ // TODO: Behavioral change!
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("StopGroupAfterDoneWithFinishAllAndSuccess")
++ << TestData{storage, root6, errorLog, 3, DoneWith::Error, 2};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("StopGroupAfterDoneWithFinishAllAndError")
++ << TestData{storage, root7, errorLog, 3, DoneWith::Error, 2};
++ }
++
++ {
++ // These tests check whether the proper subgroup's end handler is called
++ // when the group is stopped. Test it with different workflow policies.
++ // The sequential subgroup starts one short failing task followed by one long task.
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ parallel,
++ Group {
++ workflowPolicy(policy),
++ createFailingTask(1),
++ createSuccessTask(2, 1000ms),
++ groupDone(1)
++ },
++ createFailingTask(3, 1ms),
++ groupDone(2)
++ };
++ };
++
++ const Log shortLog = {
++ {1, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Error},
++ {1, Handler::GroupError},
++ {3, Handler::Canceled},
++ {2, Handler::GroupError}
++ };
++
++ const Log longLog = {
++ {1, Handler::Setup},
++ {3, Handler::Setup},
++ {1, Handler::Error},
++ {2, Handler::Setup},
++ {3, Handler::Error},
++ {2, Handler::Canceled},
++ {1, Handler::GroupCanceled},
++ {2, Handler::GroupError}
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ QTest::newRow("StopGroupAfterErrorWithStopOnError")
++ << TestData{storage, root1, shortLog, 3, DoneWith::Error, 1};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ QTest::newRow("StopGroupAfterErrorWithContinueOnError")
++ << TestData{storage, root2, longLog, 3, DoneWith::Error, 2};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ QTest::newRow("StopGroupAfterErrorWithStopOnSuccess")
++ << TestData{storage, root3, longLog, 3, DoneWith::Error, 2};
++
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ QTest::newRow("StopGroupAfterErrorWithContinueOnSuccess")
++ << TestData{storage, root4, longLog, 3, DoneWith::Error, 2};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ QTest::newRow("StopGroupAfterErrorWithStopOnSuccessOrError")
++ << TestData{storage, root5, shortLog, 3, DoneWith::Error, 1};
++
++ // TODO: Behavioral change!
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("StopGroupAfterErrorWithFinishAllAndSuccess")
++ << TestData{storage, root6, longLog, 3, DoneWith::Error, 2};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("StopGroupAfterErrorWithFinishAllAndError")
++ << TestData{storage, root7, longLog, 3, DoneWith::Error, 2};
++ }
++
++ {
++ const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
++ WorkflowPolicy policy) {
++ return Group {
++ storage,
++ workflowPolicy(policy),
++ createSuccessTask(1),
++ createFailingTask(2),
++ createSuccessTask(3),
++ groupDone(0)
++ };
++ };
++
++ const Group root1 = createRoot(WorkflowPolicy::StopOnError);
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("StopOnError") << TestData{storage, root1, log1, 3, DoneWith::Error, 2};
++
++ const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
++ const Log errorLog {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("ContinueOnError") << TestData{storage, root2, errorLog, 3, DoneWith::Error, 3};
++
++ const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
++ const Log log3 {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("StopOnSuccess") << TestData{storage, root3, log3, 3, DoneWith::Success, 1};
++
++ const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
++ const Log doneLog {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("ContinueOnSuccess") << TestData{storage, root4, doneLog, 3, DoneWith::Success, 3};
++
++ const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
++ const Log log5 {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("StopOnSuccessOrError") << TestData{storage, root5, log5, 3, DoneWith::Success, 1};
++
++ const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
++ QTest::newRow("FinishAllAndSuccess") << TestData{storage, root6, doneLog, 3, DoneWith::Success, 3};
++
++ const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
++ QTest::newRow("FinishAllAndError") << TestData{storage, root7, errorLog, 3, DoneWith::Error, 3};
++ }
++
++ {
++ const auto createRoot = [storage, createTask, groupDone](DoneResult firstResult,
++ DoneResult secondResult) {
++ return Group {
++ parallel,
++ stopOnSuccessOrError,
++ storage,
++ createTask(1, firstResult, 1000ms),
++ createTask(2, secondResult, 1ms),
++ groupDone(0)
++ };
++ };
++
++ const Group root1 = createRoot(DoneResult::Success, DoneResult::Success);
++ const Group root2 = createRoot(DoneResult::Success, DoneResult::Error);
++ const Group root3 = createRoot(DoneResult::Error, DoneResult::Success);
++ const Group root4 = createRoot(DoneResult::Error, DoneResult::Error);
++
++ const Log success {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {1, Handler::Canceled},
++ {0, Handler::GroupSuccess}
++ };
++ const Log failure {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {1, Handler::Canceled},
++ {0, Handler::GroupError}
++ };
++
++ QTest::newRow("StopOnSuccessOrError1")
++ << TestData{storage, root1, success, 2, DoneWith::Success, 1};
++ QTest::newRow("StopOnSuccessOrError2")
++ << TestData{storage, root2, failure, 2, DoneWith::Error, 1};
++ QTest::newRow("StopOnSuccessOrError3")
++ << TestData{storage, root3, success, 2, DoneWith::Success, 1};
++ QTest::newRow("StopOnSuccessOrError4")
++ << TestData{storage, root4, failure, 2, DoneWith::Error, 1};
++ }
++
++ {
++ // This test checks whether group setup handler's result is properly dispatched.
++ const auto createRoot = [storage, createSuccessTask, groupDone, groupSetupWithTweak](
++ SetupResult desiredResult) {
++ return Group {
++ storage,
++ Group {
++ groupSetupWithTweak(1, desiredResult),
++ createSuccessTask(1)
++ },
++ groupDone(0)
++ };
++ };
++
++ const Group root1 = createRoot(SetupResult::StopWithSuccess);
++ const Log log1 {
++ {1, Handler::GroupSetup},
++ {1, Handler::TweakSetupToSuccess},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("GroupSetupTweakToSuccess")
++ << TestData{storage, root1, log1, 1, DoneWith::Success, 0};
++
++ const Group root2 = createRoot(SetupResult::StopWithError);
++ const Log log2 {
++ {1, Handler::GroupSetup},
++ {1, Handler::TweakSetupToError},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("GroupSetupTweakToError")
++ << TestData{storage, root2, log2, 1, DoneWith::Error, 0};
++
++ const Group root3 = createRoot(SetupResult::Continue);
++ const Log log3 {
++ {1, Handler::GroupSetup},
++ {1, Handler::TweakSetupToContinue},
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("GroupSetupTweakToContinue")
++ << TestData{storage, root3, log3, 1, DoneWith::Success, 1};
++ }
++
++ {
++ // This test checks whether group done handler's result is properly dispatched.
++ const auto createRoot = [storage, createTask, groupDone, groupDoneWithTweak](
++ DoneResult firstResult, DoneResult secondResult) {
++ return Group {
++ storage,
++ Group {
++ createTask(1, firstResult),
++ groupDoneWithTweak(1, secondResult)
++ },
++ groupDone(0)
++ };
++ };
++
++ const Group root1 = createRoot(DoneResult::Success, DoneResult::Success);
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {1, Handler::GroupSuccess},
++ {1, Handler::TweakDoneToSuccess},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("GroupDoneWithSuccessTweakToSuccess")
++ << TestData{storage, root1, log1, 1, DoneWith::Success, 1};
++
++ const Group root2 = createRoot(DoneResult::Success, DoneResult::Error);
++ const Log log2 {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {1, Handler::GroupSuccess},
++ {1, Handler::TweakDoneToError},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("GroupDoneWithSuccessTweakToError")
++ << TestData{storage, root2, log2, 1, DoneWith::Error, 1};
++
++ const Group root3 = createRoot(DoneResult::Error, DoneResult::Success);
++ const Log log3 {
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ {1, Handler::GroupError},
++ {1, Handler::TweakDoneToSuccess},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("GroupDoneWithErrorTweakToSuccess")
++ << TestData{storage, root3, log3, 1, DoneWith::Success, 1};
++
++ const Group root4 = createRoot(DoneResult::Error, DoneResult::Error);
++ const Log log4 {
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ {1, Handler::GroupError},
++ {1, Handler::TweakDoneToError},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("GroupDoneWithErrorTweakToError")
++ << TestData{storage, root4, log4, 1, DoneWith::Error, 1};
++ }
++
++ {
++ // This test checks whether task setup handler's result is properly dispatched.
++ const auto createRoot = [storage, createSuccessTask, groupDone, createTaskWithSetupTweak](
++ SetupResult desiredResult) {
++ return Group {
++ storage,
++ Group {
++ createTaskWithSetupTweak(1, desiredResult),
++ createSuccessTask(2)
++ },
++ groupDone(0)
++ };
++ };
++
++ const Group root1 = createRoot(SetupResult::StopWithSuccess);
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToSuccess},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("TaskSetupTweakToSuccess")
++ << TestData{storage, root1, log1, 2, DoneWith::Success, 1};
++
++ const Group root2 = createRoot(SetupResult::StopWithError);
++ const Log log2 {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToError},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("TaskSetupTweakToError")
++ << TestData{storage, root2, log2, 2, DoneWith::Error, 0};
++
++ const Group root3 = createRoot(SetupResult::Continue);
++ const Log log3 {
++ {1, Handler::Setup},
++ {1, Handler::TweakSetupToContinue},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("TaskSetupTweakToContinue")
++ << TestData{storage, root3, log3, 2, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ createSuccessTask(1)
++ },
++ Group {
++ groupSetup(2),
++ createSuccessTask(2)
++ },
++ Group {
++ groupSetup(3),
++ createSuccessTask(3)
++ },
++ Group {
++ groupSetup(4),
++ createSuccessTask(4)
++ }
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {2, Handler::Success},
++ {4, Handler::GroupSetup},
++ {4, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Success}
++ };
++ QTest::newRow("NestedParallel") << TestData{storage, root, log, 4, DoneWith::Success, 4};
++ }
++
++ {
++ const Group root {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ createSuccessTask(1)
++ },
++ Group {
++ groupSetup(2),
++ createSuccessTask(2)
++ },
++ Group {
++ groupSetup(3),
++ createTaskWithSetupTweak(3, SetupResult::StopWithSuccess)
++ },
++ Group {
++ groupSetup(4),
++ createSuccessTask(4)
++ },
++ Group {
++ groupSetup(5),
++ createSuccessTask(5)
++ }
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToSuccess},
++ {4, Handler::GroupSetup},
++ {4, Handler::Setup},
++ {2, Handler::Success},
++ {5, Handler::GroupSetup},
++ {5, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Success}
++ };
++ QTest::newRow("NestedParallelDone") << TestData{storage, root, log, 5, DoneWith::Success, 4};
++ }
++
++ {
++ const Group root1 {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ createSuccessTask(1)
++ },
++ Group {
++ groupSetup(2),
++ createSuccessTask(2)
++ },
++ Group {
++ groupSetup(3),
++ createTaskWithSetupTweak(3, SetupResult::StopWithError)
++ },
++ Group {
++ groupSetup(4),
++ createSuccessTask(4)
++ },
++ Group {
++ groupSetup(5),
++ createSuccessTask(5)
++ }
++ };
++ const Log log1 {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {2, Handler::Canceled}
++ };
++
++ // Inside this test the task 2 should finish first, then synchonously:
++ // - task 3 should exit setup with error
++ // - task 1 should be stopped as a consequence of the error inside the group
++ // - tasks 4 and 5 should be skipped
++ const Group root2 {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ createSuccessTask(1, 1s)
++ },
++ Group {
++ groupSetup(2),
++ createSuccessTask(2)
++ },
++ Group {
++ groupSetup(3),
++ createTaskWithSetupTweak(3, SetupResult::StopWithError)
++ },
++ Group {
++ groupSetup(4),
++ createSuccessTask(4)
++ },
++ Group {
++ groupSetup(5),
++ createSuccessTask(5)
++ }
++ };
++ const Log log2 {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {1, Handler::Canceled}
++ };
++
++ // This test ensures that the task 1 doesn't invoke its done handler,
++ // being ready while sleeping in the task's 2 done handler.
++ // Inside this test the task 2 should finish first, then synchonously:
++ // - task 3 should exit setup with error
++ // - task 1 should be stopped as a consequence of error inside the group
++ // - task 4 should be skipped
++ // - the first child group of root should finish with error
++ // - task 5 should be started (because of root's continueOnError policy)
++ const Group root3 {
++ continueOnError,
++ storage,
++ Group {
++ ParallelLimit(2),
++ Group {
++ groupSetup(1),
++ createSuccessTask(1, 1s)
++ },
++ Group {
++ groupSetup(2),
++ createSuccessTask(2)
++ },
++ Group {
++ groupSetup(3),
++ createTaskWithSetupTweak(3, SetupResult::StopWithError)
++ },
++ Group {
++ groupSetup(4),
++ createSuccessTask(4)
++ }
++ },
++ Group {
++ groupSetup(5),
++ createSuccessTask(5)
++ }
++ };
++ const Log log3 {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {1, Handler::Canceled},
++ {5, Handler::GroupSetup},
++ {5, Handler::Setup},
++ {5, Handler::Success}
++ };
++ QTest::newRow("NestedParallelError1")
++ << TestData{storage, root1, log1, 5, DoneWith::Error, 1};
++ QTest::newRow("NestedParallelError2")
++ << TestData{storage, root2, log2, 5, DoneWith::Error, 1};
++ QTest::newRow("NestedParallelError3")
++ << TestData{storage, root3, log3, 5, DoneWith::Error, 2};
++ }
++
++ {
++ const Group root {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ Group {
++ parallel,
++ createSuccessTask(1)
++ }
++ },
++ Group {
++ groupSetup(2),
++ Group {
++ parallel,
++ createSuccessTask(2)
++ }
++ },
++ Group {
++ groupSetup(3),
++ Group {
++ parallel,
++ createSuccessTask(3)
++ }
++ },
++ Group {
++ groupSetup(4),
++ Group {
++ parallel,
++ createSuccessTask(4)
++ }
++ }
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {2, Handler::Success},
++ {4, Handler::GroupSetup},
++ {4, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Success}
++ };
++ QTest::newRow("DeeplyNestedParallel") << TestData{storage, root, log, 4, DoneWith::Success, 4};
++ }
++
++ {
++ const Group root {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ Group { createSuccessTask(1) }
++ },
++ Group {
++ groupSetup(2),
++ Group { createSuccessTask(2) }
++ },
++ Group {
++ groupSetup(3),
++ Group { createTaskWithSetupTweak(3, SetupResult::StopWithSuccess) }
++ },
++ Group {
++ groupSetup(4),
++ Group { createSuccessTask(4) }
++ },
++ Group {
++ groupSetup(5),
++ Group { createSuccessTask(5) }
++ }
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToSuccess},
++ {4, Handler::GroupSetup},
++ {4, Handler::Setup},
++ {2, Handler::Success},
++ {5, Handler::GroupSetup},
++ {5, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Success}
++ };
++ QTest::newRow("DeeplyNestedParallelSuccess")
++ << TestData{storage, root, log, 5, DoneWith::Success, 4};
++ }
++
++ {
++ const Group root {
++ ParallelLimit(2),
++ storage,
++ Group {
++ groupSetup(1),
++ Group { createSuccessTask(1) }
++ },
++ Group {
++ groupSetup(2),
++ Group { createSuccessTask(2) }
++ },
++ Group {
++ groupSetup(3),
++ Group { createTaskWithSetupTweak(3, SetupResult::StopWithError) }
++ },
++ Group {
++ groupSetup(4),
++ Group { createSuccessTask(4) }
++ },
++ Group {
++ groupSetup(5),
++ Group { createSuccessTask(5) }
++ }
++ };
++ const Log log {
++ {1, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {3, Handler::GroupSetup},
++ {3, Handler::Setup},
++ {3, Handler::TweakSetupToError},
++ {2, Handler::Canceled}
++ };
++ QTest::newRow("DeeplyNestedParallelError")
++ << TestData{storage, root, log, 5, DoneWith::Error, 1};
++ }
++
++ {
++ const Group root {
++ storage,
++ createSync(1),
++ createSync(2),
++ createSync(3),
++ createSync(4),
++ createSync(5)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync},
++ {4, Handler::Sync},
++ {5, Handler::Sync}
++ };
++ QTest::newRow("SyncSequential") << TestData{storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ createSyncWithTweak(1, DoneResult::Success),
++ createSyncWithTweak(2, DoneResult::Success),
++ createSyncWithTweak(3, DoneResult::Success),
++ createSyncWithTweak(4, DoneResult::Success),
++ createSyncWithTweak(5, DoneResult::Success)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {1, Handler::TweakDoneToSuccess},
++ {2, Handler::Sync},
++ {2, Handler::TweakDoneToSuccess},
++ {3, Handler::Sync},
++ {3, Handler::TweakDoneToSuccess},
++ {4, Handler::Sync},
++ {4, Handler::TweakDoneToSuccess},
++ {5, Handler::Sync},
++ {5, Handler::TweakDoneToSuccess}
++ };
++ QTest::newRow("SyncWithReturn") << TestData{storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ parallel,
++ createSync(1),
++ createSync(2),
++ createSync(3),
++ createSync(4),
++ createSync(5)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync},
++ {4, Handler::Sync},
++ {5, Handler::Sync}
++ };
++ QTest::newRow("SyncParallel") << TestData{storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ parallel,
++ createSync(1),
++ createSync(2),
++ createSyncWithTweak(3, DoneResult::Error),
++ createSync(4),
++ createSync(5)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync},
++ {3, Handler::TweakDoneToError}
++ };
++ QTest::newRow("SyncError") << TestData{storage, root, log, 0, DoneWith::Error, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ createSync(1),
++ createSuccessTask(2),
++ createSync(3),
++ createSuccessTask(4),
++ createSync(5),
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Sync},
++ {4, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Sync},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("SyncAndAsync") << TestData{storage, root, log, 2, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ createSync(1),
++ createSuccessTask(2),
++ createSyncWithTweak(3, DoneResult::Error),
++ createSuccessTask(4),
++ createSync(5),
++ groupDone(0)
++ };
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Sync},
++ {3, Handler::TweakDoneToError},
++ {0, Handler::GroupError}
++ };
++ QTest::newRow("SyncAndAsyncError") << TestData{storage, root, log, 2, DoneWith::Error, 1};
++ }
++
++ {
++ QStoredBarrier barrier;
++
++ // Test that barrier advance, triggered from inside the task described by
++ // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
++ // in the tree order, works OK in SEQUENTIAL mode.
++ const Group root1 {
++ storage,
++ barrier,
++ sequential,
++ createBarrierAdvance(storage, barrier, 1),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ // Test that barrier advance, triggered from inside the task described by
++ // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
++ // in the tree order, works OK in PARALLEL mode.
++ const Group root2 {
++ storage,
++ barrier,
++ parallel,
++ createBarrierAdvance(storage, barrier, 1),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++ const Log log2 {
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ // Test that barrier advance, triggered from inside the task described by
++ // createBarrierAdvance, placed AFTER the group containing the waitFor() element
++ // in the tree order, works OK in PARALLEL mode.
++ //
++ // Notice: This won't work in SEQUENTIAL mode, since the advancing barrier, placed after the
++ // group containing the WaitFor element, has no chance to be started in SEQUENTIAL mode,
++ // as in SEQUENTIAL mode the next task may only be started after the previous one finished.
++ // In this case, the previous task (Group element) awaits for the barrier's advance to
++ // come from the not yet started next task, causing a deadlock.
++ // The minimal requirement for this scenario to succeed is to set ParallelLimit(2) or more.
++ const Group root3 {
++ storage,
++ barrier,
++ parallel,
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ },
++ createBarrierAdvance(storage, barrier, 1)
++ };
++ const Log log3 {
++ {2, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ // Test that barrier advance, triggered from inside the task described by
++ // createBarrierAdvance, placed BEFORE the groups containing the waitFor() element
++ // in the tree order, wakes both waitFor tasks.
++ const Group root4 {
++ storage,
++ barrier,
++ parallel,
++ createBarrierAdvance(storage, barrier, 1),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(4)
++ },
++ Group {
++ groupSetup(3),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(5)
++ }
++ };
++ const Log log4 {
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {3, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {4, Handler::Setup},
++ {5, Handler::Setup},
++ {4, Handler::Success},
++ {5, Handler::Success}
++ };
++
++ // Test two separate single barriers.
++
++ QStoredBarrier barrier2;
++
++ const Group root5 {
++ storage,
++ barrier,
++ barrier2,
++ parallel,
++ createBarrierAdvance(storage, barrier, 1),
++ createBarrierAdvance(storage, barrier2, 2),
++ Group {
++ Group {
++ parallel,
++ groupSetup(1),
++ barrierAwaiterTask(barrier),
++ barrierAwaiterTask(barrier2)
++ },
++ createSuccessTask(3)
++ },
++ };
++ const Log log5 {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::BarrierAdvance},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ // Notice the different log order for each scenario.
++ QTest::newRow("BarrierSequential")
++ << TestData{storage, root1, log1, 4, DoneWith::Success, 3};
++ QTest::newRow("BarrierParallelAdvanceFirst")
++ << TestData{storage, root2, log2, 4, DoneWith::Success, 4};
++ QTest::newRow("BarrierParallelWaitForFirst")
++ << TestData{storage, root3, log3, 4, DoneWith::Success, 4};
++ QTest::newRow("BarrierParallelMultiWaitFor")
++ << TestData{storage, root4, log4, 5, DoneWith::Success, 5};
++ QTest::newRow("BarrierParallelTwoSingleBarriers")
++ << TestData{storage, root5, log5, 5, DoneWith::Success, 5};
++
++ const auto barrierKicker = [storage](int taskId) {
++ return [storage, taskId](const QStoredBarrier &barrier) {
++ return createBarrierAdvance(storage, barrier, taskId);
++ };
++ };
++
++ // The same as BarrierParallelAdvanceFirst, but in terms of When () >> Do {...}.
++ const Group rootWhenDo {
++ storage,
++ When (barrierKicker(1)) >> Do {
++ groupSetup(2),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++
++ QTest::newRow("BarrierWhenDo")
++ << TestData{storage, rootWhenDo, log2, 4, DoneWith::Success, 4};
++
++ const auto onTickSetup = [storage](TickAndDone &tickAndDone) {
++ tickAndDone.setInterval(1ms);
++ storage->m_log.append({1, Handler::Setup});
++ };
++ const Group rootWhenSignalDo {
++ storage,
++ When (TickAndDoneTask(onTickSetup), &TickAndDone::tick) >> Do {
++ groupSetup(2),
++ QSyncTask([storage] { storage->m_log.append({1, Handler::BarrierAdvance}); }),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++ QTest::newRow("BarrierWhenSignalDo")
++ << TestData{storage, rootWhenSignalDo, log2, 4, DoneWith::Success, 4};
++
++ const auto onTickSetupWithPolicy = [storage](TickAndDone &tickAndDone) {
++ tickAndDone.setInterval(s_endlessTime);
++ storage->m_log.append({1, Handler::Setup});
++ };
++ const Group rootWhenSignalDoWithPolicy {
++ storage,
++ When (TickAndDoneTask(onTickSetupWithPolicy, setupDone(1)), &TickAndDone::tick, WorkflowPolicy::StopOnSuccessOrError) >> Do {
++ groupSetup(2),
++ QSyncTask([storage] { storage->m_log.append({1, Handler::BarrierAdvance}); }),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++ const Log logWhenSignalDoWithPolicy {
++ {1, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {1, Handler::Canceled}
++ };
++ QTest::newRow("BarrierWhenSignalDoWithPolicy")
++ << TestData{storage, rootWhenSignalDoWithPolicy, logWhenSignalDoWithPolicy, 4, DoneWith::Success, 3};
++ }
++
++ {
++ const QStoredBarrier barrier{2};
++
++ // Test that multi barrier advance, triggered from inside the tasks described by
++ // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
++ // in the tree order, works OK in SEQUENTIAL mode.
++ const Group root1 {
++ storage,
++ barrier,
++ sequential,
++ createBarrierAdvance(storage, barrier, 1),
++ createBarrierAdvance(storage, barrier, 2),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(2),
++ createSuccessTask(3)
++ }
++ };
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::Setup},
++ {2, Handler::BarrierAdvance},
++ {2, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ // Test that multi barrier advance, triggered from inside the tasks described by
++ // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
++ // in the tree order, works OK in PARALLEL mode.
++ const Group root2 {
++ storage,
++ barrier,
++ parallel,
++ createBarrierAdvance(storage, barrier, 1),
++ createBarrierAdvance(storage, barrier, 2),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(3),
++ createSuccessTask(4)
++ }
++ };
++ const Log log2 {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::BarrierAdvance},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Setup},
++ {4, Handler::Success}
++ };
++
++ // Test that multi barrier advance, triggered from inside the tasks described by
++ // createBarrierAdvance, placed AFTER the group containing the waitFor() element
++ // in the tree order, works OK in PARALLEL mode.
++ //
++ // Notice: This won't work in SEQUENTIAL mode, since the advancing barriers, placed after
++ // the group containing the WaitFor element, has no chance to be started in SEQUENTIAL mode,
++ // as in SEQUENTIAL mode the next task may only be started after the previous one finished.
++ // In this case, the previous task (Group element) awaits for the barrier's advance to
++ // come from the not yet started next task, causing a deadlock.
++ // The minimal requirement for this scenario to succeed is to set ParallelLimit(2) or more.
++ const Group root3 {
++ storage,
++ barrier,
++ parallel,
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(3),
++ createSuccessTask(4)
++ },
++ createBarrierAdvance(storage, barrier, 1),
++ createBarrierAdvance(storage, barrier, 2)
++ };
++ const Log log3 {
++ {2, Handler::GroupSetup},
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::BarrierAdvance},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Setup},
++ {4, Handler::Success}
++ };
++
++ // Test that multi barrier advance, triggered from inside the task described by
++ // createBarrierAdvance, placed BEFORE the groups containing the waitFor() element
++ // in the tree order, wakes both waitFor tasks.
++ const Group root4 {
++ storage,
++ barrier,
++ parallel,
++ createBarrierAdvance(storage, barrier, 1),
++ createBarrierAdvance(storage, barrier, 2),
++ Group {
++ groupSetup(2),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(3)
++ },
++ Group {
++ groupSetup(3),
++ barrierAwaiterTask(barrier),
++ createSuccessTask(4)
++ }
++ };
++ const Log log4 {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {2, Handler::GroupSetup},
++ {3, Handler::GroupSetup},
++ {1, Handler::BarrierAdvance},
++ {2, Handler::BarrierAdvance},
++ {3, Handler::Setup},
++ {4, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Success}
++ };
++
++ // Notice the different log order for each scenario.
++ QTest::newRow("MultiBarrierSequential")
++ << TestData{storage, root1, log1, 5, DoneWith::Success, 4};
++ QTest::newRow("MultiBarrierParallelAdvanceFirst")
++ << TestData{storage, root2, log2, 5, DoneWith::Success, 5};
++ QTest::newRow("MultiBarrierParallelWaitForFirst")
++ << TestData{storage, root3, log3, 5, DoneWith::Success, 5};
++ QTest::newRow("MultiBarrierParallelMultiWaitFor")
++ << TestData{storage, root4, log4, 6, DoneWith::Success, 6};
++ }
++
++ {
++ // Test QCustomTask::withTimeout() combinations:
++ // 1. When the timeout has triggered or not.
++ // 2. With and without timeout handler.
++ const Group root1 {
++ storage,
++ TestTask(setupTask(1, 1000ms), setupDone(1))
++ .withTimeout(1ms)
++ };
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::Canceled}
++ };
++ QTest::newRow("TaskErrorWithTimeout")
++ << TestData{storage, root1, log1, 2, DoneWith::Error, 1};
++
++ const Group root2 {
++ storage,
++ TestTask(setupTask(1, 1000ms), setupDone(1))
++ .withTimeout(1ms, setupTimeout(1))
++ };
++ const Log log2 {
++ {1, Handler::Setup},
++ {1, Handler::Timeout},
++ {1, Handler::Canceled}
++ };
++ QTest::newRow("TaskErrorWithTimeoutHandler")
++ << TestData{storage, root2, log2, 2, DoneWith::Error, 1};
++
++ const Group root3 {
++ storage,
++ TestTask(setupTask(1, 1ms), setupDone(1))
++ .withTimeout(1000ms)
++ };
++ const Log doneLog {
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++ QTest::newRow("TaskDoneWithTimeout")
++ << TestData{storage, root3, doneLog, 2, DoneWith::Success, 1};
++
++ const Group root4 {
++ storage,
++ TestTask(setupTask(1, 1ms), setupDone(1))
++ .withTimeout(1000ms, setupTimeout(1))
++ };
++ QTest::newRow("TaskDoneWithTimeoutHandler")
++ << TestData{storage, root4, doneLog, 2, DoneWith::Success, 1};
++ }
++
++ {
++ // Test Group::withTimeout() combinations:
++ // 1. When the timeout has triggered or not.
++ // 2. With and without timeout handler.
++ const Group root1 {
++ storage,
++ Group {
++ createSuccessTask(1, 1000ms)
++ }.withTimeout(1ms)
++ };
++ const Log log1 {
++ {1, Handler::Setup},
++ {1, Handler::Canceled}
++ };
++ QTest::newRow("GroupErrorWithTimeout")
++ << TestData{storage, root1, log1, 2, DoneWith::Error, 1};
++
++ // Test Group::withTimeout(), passing custom handler
++ const Group root2 {
++ storage,
++ Group {
++ createSuccessTask(1, 1000ms)
++ }.withTimeout(1ms, setupTimeout(1))
++ };
++ const Log log2 {
++ {1, Handler::Setup},
++ {1, Handler::Timeout},
++ {1, Handler::Canceled}
++ };
++ QTest::newRow("GroupErrorWithTimeoutHandler")
++ << TestData{storage, root2, log2, 2, DoneWith::Error, 1};
++
++ const Group root3 {
++ storage,
++ Group {
++ createSuccessTask(1, 1ms)
++ }.withTimeout(1000ms)
++ };
++ const Log doneLog {
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++ QTest::newRow("GroupDoneWithTimeout")
++ << TestData{storage, root3, doneLog, 2, DoneWith::Success, 1};
++
++ // Test Group::withTimeout(), passing custom handler
++ const Group root4 {
++ storage,
++ Group {
++ createSuccessTask(1, 1ms)
++ }.withTimeout(1000ms, setupTimeout(1))
++ };
++ QTest::newRow("GroupDoneWithTimeoutHandler")
++ << TestData{storage, root4, doneLog, 2, DoneWith::Success, 1};
++ }
++
++ {
++ // Test if the task tree started from the setup handler may use the same storage.
++
++ const auto groupSetup = [storage] {
++ storage->m_log.append({0, Handler::GroupSetup});
++
++ const auto nestedGroupSetup = [storage] {
++ storage->m_log.append({2, Handler::GroupSetup});
++ };
++ const auto nestedGroupDone = [storage](DoneWith result) {
++ storage->m_log.append({2, resultToGroupHandler(result)});
++ };
++ QTaskTree subTree({storage,
++ onGroupSetup(nestedGroupSetup),
++ onGroupDone(nestedGroupDone)});
++ subTree.start();
++ storage->m_log.append({1, Handler::GroupSetup}); // Ensure the storage is still active.
++ };
++ const auto groupDone = [storage](DoneWith result) {
++ storage->m_log.append({0, resultToGroupHandler(result)});
++ };
++
++ const Group root {
++ storage,
++ onGroupSetup(groupSetup),
++ onGroupDone(groupDone)
++ };
++ const Log log {
++ {0, Handler::GroupSetup},
++ {1, Handler::GroupSetup},
++ {0, Handler::GroupSuccess}
++ };
++ QTest::newRow("CommonStorage") << TestData{storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ {
++ const Group root {
++ storage,
++ Group {
++ parallel,
++ createSuccessTask(1, 100ms),
++ Group {
++ createFailingTask(2, 1ms),
++ createSuccessTask(3, 1ms)
++ },
++ createSuccessTask(4, 1ms)
++ },
++ createSuccessTask(5, 1ms)
++ };
++ const Log log {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {4, Handler::Setup},
++ {2, Handler::Error},
++ {1, Handler::Canceled},
++ {4, Handler::Canceled}
++ };
++ QTest::newRow("NestedCancel") << TestData{storage, root, log, 5, DoneWith::Error, 1};
++ }
++
++ {
++ const GroupItems successItems {
++ storage,
++ createSuccessTask(1),
++ createSuccessTask(2)
++ };
++
++ const Group rootSequentialSuccess = For (RepeatIterator(2)) >> Do {
++ sequential,
++ successItems
++ };
++ const Group rootSequentialSuccess2 = For (2) >> Do {
++ sequential,
++ successItems
++ };
++ const Log logSequentialSuccess {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success}
++ };
++
++ const Group rootParallelSuccess = For (RepeatIterator(2)) >> Do {
++ parallel,
++ successItems
++ };
++ const Group rootParallelSuccess2 = For (2) >> Do {
++ parallel,
++ successItems
++ };
++ const Log logParallelSuccess {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Success},
++ {1, Handler::Success},
++ {2, Handler::Success}
++ };
++
++ const Group rootParallelLimitSuccess = For (RepeatIterator(2)) >> Do {
++ ParallelLimit(2),
++ successItems
++ };
++ const Group rootParallelLimitSuccess2 = For (2) >> Do {
++ ParallelLimit(2),
++ successItems
++ };
++ const Log logParallelLimitSuccess {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {1, Handler::Setup},
++ {2, Handler::Success},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Success}
++ };
++
++ const GroupItems errorItems {
++ storage,
++ createSuccessTask(1),
++ createFailingTask(2)
++ };
++
++ const Group rootSequentialError = For (RepeatIterator(2)) >> Do {
++ sequential,
++ errorItems
++ };
++ const Group rootSequentialError2 = For (2) >> Do {
++ sequential,
++ errorItems
++ };
++ const Log logSequentialError {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Error}
++ };
++
++ const Group rootParallelError = For (RepeatIterator(2)) >> Do {
++ parallel,
++ errorItems
++ };
++ const Group rootParallelError2 = For (2) >> Do {
++ parallel,
++ errorItems
++ };
++ const Log logParallelError {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Error},
++ {1, Handler::Canceled},
++ {2, Handler::Canceled}
++ };
++
++ const Group rootParallelLimitError = For (RepeatIterator(2)) >> Do {
++ ParallelLimit(2),
++ errorItems
++ };
++ const Group rootParallelLimitError2 = For (2) >> Do {
++ ParallelLimit(2),
++ errorItems
++ };
++ const Log logParallelLimitError {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {1, Handler::Setup},
++ {2, Handler::Error},
++ {1, Handler::Canceled}
++ };
++
++ QTest::newRow("RepeatSequentialSuccess")
++ << TestData{storage, rootSequentialSuccess, logSequentialSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("RepeatParallelSuccess")
++ << TestData{storage, rootParallelSuccess, logParallelSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("RepeatParallelLimitSuccess")
++ << TestData{storage, rootParallelLimitSuccess, logParallelLimitSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("RepeatSequentialError")
++ << TestData{storage, rootSequentialError, logSequentialError, 4, DoneWith::Error, 2};
++ QTest::newRow("RepeatParallelError")
++ << TestData{storage, rootParallelError, logParallelError, 4, DoneWith::Error, 2};
++ QTest::newRow("RepeatParallelLimitError")
++ << TestData{storage, rootParallelLimitError, logParallelLimitError, 4, DoneWith::Error, 2};
++
++ QTest::newRow("NumberSequentialSuccess")
++ << TestData{storage, rootSequentialSuccess2, logSequentialSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("NumberParallelSuccess")
++ << TestData{storage, rootParallelSuccess2, logParallelSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("NumberParallelLimitSuccess")
++ << TestData{storage, rootParallelLimitSuccess2, logParallelLimitSuccess, 4, DoneWith::Success, 4};
++ QTest::newRow("NumberSequentialError")
++ << TestData{storage, rootSequentialError2, logSequentialError, 4, DoneWith::Error, 2};
++ QTest::newRow("NumberParallelError")
++ << TestData{storage, rootParallelError2, logParallelError, 4, DoneWith::Error, 2};
++ QTest::newRow("NumberParallelLimitError")
++ << TestData{storage, rootParallelLimitError2, logParallelLimitError, 4, DoneWith::Error, 2};
++ }
++
++ {
++ const UntilIterator iterator([](int index) { return index < 3; });
++
++ const auto onSetupContinue = [storage, iterator](int taskId) {
++ return [storage, iterator, taskId](TaskObject &) {
++ storage->m_log.append({iterator.iteration() * 10 + taskId, Handler::Setup});
++ };
++ };
++
++ const auto onSetupStop = [storage, iterator](int taskId) {
++ return [storage, iterator, taskId](TaskObject &) {
++ storage->m_log.append({iterator.iteration() * 10 + taskId, Handler::Setup});
++ if (iterator.iteration() == 2)
++ return SetupResult::StopWithSuccess;
++ return SetupResult::Continue;
++ };
++ };
++
++ const auto onDone = [storage, iterator](int taskId) {
++ return [storage, iterator, taskId](DoneWith doneWith) {
++ const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
++ : doneWith == DoneWith::Success ? Handler::Success : Handler::Error;
++ storage->m_log.append({iterator.iteration() * 10 + taskId, handler});
++ };
++ };
++
++ const GroupItems items {
++ storage,
++ TestTask(onSetupContinue(1), onDone(1)),
++ TestTask(onSetupStop(2), onDone(2))
++ };
++
++ const Group rootSequential = For(iterator) >> Do {
++ sequential,
++ items
++ };
++ const Log logSequential {
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {11, Handler::Setup},
++ {11, Handler::Success},
++ {12, Handler::Setup},
++ {12, Handler::Success},
++ {21, Handler::Setup},
++ {21, Handler::Success},
++ {22, Handler::Setup}
++ };
++
++ const Group rootParallel = For(iterator) >> Do {
++ parallel,
++ items
++ };
++ const Log logParallel {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {11, Handler::Setup},
++ {12, Handler::Setup},
++ {21, Handler::Setup},
++ {22, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Success},
++ {11, Handler::Success},
++ {12, Handler::Success},
++ {21, Handler::Success}
++ };
++
++ const Group rootParallelLimit = For(iterator) >> Do {
++ ParallelLimit(2),
++ items
++ };
++ const Log logParallelLimit {
++ {1, Handler::Setup},
++ {2, Handler::Setup},
++ {1, Handler::Success},
++ {11, Handler::Setup},
++ {2, Handler::Success},
++ {12, Handler::Setup},
++ {11, Handler::Success},
++ {21, Handler::Setup},
++ {12, Handler::Success},
++ {22, Handler::Setup},
++ {21, Handler::Success}
++ };
++
++ QTest::newRow("UntilIteratorSequential")
++ << TestData{storage, rootSequential, logSequential, 2, DoneWith::Success, 5};
++ QTest::newRow("UntilIteratorParallel")
++ << TestData{storage, rootParallel, logParallel, 2, DoneWith::Success, 5};
++ QTest::newRow("UntilIteratorParallelLimit")
++ << TestData{storage, rootParallelLimit, logParallelLimit, 2, DoneWith::Success, 5};
++ }
++
++ {
++ // Check if task tree finishes with the right progress value when UntilIterator(false).
++ const Group root = For(UntilIterator([](int) { return false; })) >> Do {
++ storage,
++ createSuccessTask(1)
++ };
++ QTest::newRow("ProgressWithUntilIteratorFalse")
++ << TestData{storage, root, {}, 1, DoneWith::Success, 0};
++ }
++
++ {
++ // Check if task tree finishes with the right progress value when nested UntilIterator(false).
++ const Group root = For (UntilIterator([](int index) { return index < 2; })) >> Do {
++ storage,
++ For (UntilIterator([](int) { return false; })) >> Do {
++ createSuccessTask(1)
++ }
++ };
++ QTest::newRow("ProgressWithNestedUntilIteratorFalse")
++ << TestData{storage, root, {}, 1, DoneWith::Success, 0};
++ }
++
++ {
++ // Check if UntilIterator's condition is executed with empty loop body.
++ const UntilIterator iterator([storage](int iteration) {
++ storage->m_log.append({iteration, Handler::Iteration});
++ return iteration < 3;
++ });
++
++ const Group root = For(iterator) >> Do { storage };
++
++ const Log log {
++ {0, Handler::Iteration},
++ {1, Handler::Iteration},
++ {2, Handler::Iteration},
++ {3, Handler::Iteration} // The last iteration returns false
++ };
++
++ QTest::newRow("EmptyUntilIterator")
++ << TestData{storage, root, log, 0, DoneWith::Success, 0};
++
++ // Check if UntilIterator's condition isn't executed beetween tasks in the body.
++ const Group root2 = For(iterator) >> Do {
++ storage,
++ createSuccessTask(5),
++ createSuccessTask(6),
++ };
++
++ const Log log2 {
++ {0, Handler::Iteration},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {6, Handler::Setup},
++ {6, Handler::Success},
++ {1, Handler::Iteration},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {6, Handler::Setup},
++ {6, Handler::Success},
++ {2, Handler::Iteration},
++ {5, Handler::Setup},
++ {5, Handler::Success},
++ {6, Handler::Setup},
++ {6, Handler::Success},
++ {3, Handler::Iteration} // The last iteration returns false
++ };
++
++ QTest::newRow("NonEmptyUntilIterator")
++ << TestData{storage, root2, log2, 2, DoneWith::Success, 6};
++ }
++
++ {
++ // Check if task tree finishes with the right progress value when onGroupSetup(false).
++ const Group root {
++ storage,
++ onGroupSetup([] { return SetupResult::StopWithSuccess; }),
++ createSuccessTask(1)
++ };
++ QTest::newRow("ProgressWithGroupSetupFalse")
++ << TestData{storage, root, {}, 1, DoneWith::Success, 0};
++ }
++
++ {
++ // Check if task tree finishes with the right progress value when nested UntilIterator(false).
++ const Group root = For (UntilIterator([](int index) { return index < 2; })) >> Do {
++ storage,
++ Group {
++ onGroupSetup([] { return SetupResult::StopWithSuccess; }),
++ createSuccessTask(1)
++ }
++ };
++ QTest::newRow("ProgressWithNestedGroupSetupFalse")
++ << TestData{storage, root, {}, 1, DoneWith::Success, 0};
++ }
++
++ {
++ class DummyTaskAdapter final // bool is dummy
++ {
++ public:
++ void operator()(bool *, QTaskInterface *iface) { iface->reportDone(DoneResult::Error); }
++ };
++
++ using DummyTask = QCustomTask<bool, DummyTaskAdapter>;
++
++ // Check if progress is updated correctly on error when the 1st task finishes synchonously.
++ const Group root {
++ storage,
++ DummyTask(),
++ createSuccessTask(1)
++ };
++ QTest::newRow("ProgressOnSynchronousError")
++ << TestData{storage, root, {}, 2, DoneWith::Error, 0};
++ }
++
++ {
++ const auto recipe = [storage, createSuccessTask](bool withTask) {
++ return Group {
++ storage,
++ withTask ? createSuccessTask(1) : nullItem
++ };
++ };
++
++ const Log withTaskLog {
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++
++ QTest::newRow("RecipeWithTask")
++ << TestData{storage, recipe(true), withTaskLog, 1, DoneWith::Success, 1};
++ QTest::newRow("RecipeWithNull")
++ << TestData{storage, recipe(false), {}, 0, DoneWith::Success, 0};
++ }
++
++ {
++ // These tests confirm the expected message log.
++
++ const TestData testSuccess {
++ storage,
++ Group {
++ storage,
++ Group {
++ createSuccessTask(1, 2ms).withLog("Task 1")
++ }.withLog("Group 1")
++ },
++ Log {
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ },
++ 1,
++ DoneWith::Success,
++ 1,
++ MessageLog {
++ {"Group 1", Handler::Setup},
++ {"Task 1", Handler::Setup},
++ {"Task 1", Handler::Success, Execution::Async},
++ {"Group 1", Handler::Success, Execution::Async}
++ }
++ };
++ const TestData testError {
++ storage,
++ Group {
++ storage,
++ Group {
++ createFailingTask(1, 1ms).withLog("Task 1")
++ }.withLog("Group 1")
++ },
++ Log {
++ {1, Handler::Setup},
++ {1, Handler::Error}
++ },
++ 1,
++ DoneWith::Error,
++ 1,
++ MessageLog {
++ {"Group 1", Handler::Setup},
++ {"Task 1", Handler::Setup},
++ {"Task 1", Handler::Error, Execution::Async},
++ {"Group 1", Handler::Error, Execution::Async}
++ }
++ };
++ const TestData testCancel {
++ storage,
++ Group {
++ storage,
++ parallel,
++ Group {
++ createSuccessTask(1).withLog("Task 1"),
++ }.withLog("Group 1"),
++ createSyncWithTweak(2, DoneResult::Error).withLog("Task 2")
++ },
++ Log {
++ {1, Handler::Setup},
++ {2, Handler::Sync},
++ {2, Handler::TweakDoneToError},
++ {1, Handler::Canceled}
++ },
++ 1,
++ DoneWith::Error,
++ 0,
++ MessageLog {
++ {"Group 1", Handler::Setup},
++ {"Task 1", Handler::Setup},
++ {"Task 2", Handler::Setup},
++ {"Task 2", Handler::Error, Execution::Sync},
++ {"Task 1", Handler::Canceled, Execution::Sync},
++ {"Group 1", Handler::Canceled, Execution::Sync}
++ }
++ };
++
++ QTest::newRow("LogSuccess") << testSuccess;
++ QTest::newRow("LogError") << testError;
++ QTest::newRow("LogCancel") << testCancel;
++ }
++
++ {
++ // These tests ensure the task handlers are invoked according to the CallDone
++
++ const auto createRoot = [storage, groupDone](DoneWith doneWith, CallDone callDone) {
++ return Group {
++ storage,
++ parallel,
++ Group {
++ TestTask({}, toDoneResult(doneWith == DoneWith::Success)),
++ groupDone(1, callDone)
++ },
++ doneWith == DoneWith::Cancel ? QSyncTask([] { return false; }) : nullItem
++ };
++ };
++
++ const Log logNoDone {};
++ const Log logSuccess {{1, Handler::GroupSuccess}};
++ const Log logError {{1, Handler::GroupError}};
++ const Log logCanceled {{1, Handler::GroupCanceled}};
++
++ QTest::newRow("SuccessCallDoneNever")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::Never),
++ logNoDone, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnSuccess")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess),
++ logSuccess, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnError")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnError),
++ logNoDone, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnCancel")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnCancel),
++ logNoDone, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnSuccessOrError")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
++ logSuccess, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnSuccessOrCancel")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
++ logSuccess, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneOnErrorOrCancel")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
++ logNoDone, 1, DoneWith::Success, 1};
++ QTest::newRow("SuccessCallDoneAlways")
++ << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::Always),
++ logSuccess, 1, DoneWith::Success, 1};
++
++ QTest::newRow("ErrorCallDoneNever")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::Never),
++ logNoDone, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnSuccess")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess),
++ logNoDone, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnError")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnError),
++ logError, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnCancel")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnCancel),
++ logNoDone, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnSuccessOrError")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
++ logError, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnSuccessOrCancel")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
++ logNoDone, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneOnErrorOrCancel")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
++ logError, 1, DoneWith::Error, 1};
++ QTest::newRow("ErrorCallDoneAlways")
++ << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::Always),
++ logError, 1, DoneWith::Error, 1};
++
++ QTest::newRow("CancelCallDoneNever")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::Never),
++ logNoDone, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnSuccess")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess),
++ logNoDone, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnError")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnError),
++ logNoDone, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnCancel")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnCancel),
++ logCanceled, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnSuccessOrError")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
++ logNoDone, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnSuccessOrCancel")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
++ logCanceled, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneOnErrorOrCancel")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
++ logCanceled, 1, DoneWith::Error, 0};
++ QTest::newRow("CancelCallDoneAlways")
++ << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::Always),
++ logCanceled, 1, DoneWith::Error, 0};
++ }
++
++ {
++ // This test ensures the task done handlers are invoked in a different order
++ // than the corresponding setup handlers.
++
++ const QList<milliseconds> tasks { s_endlessTime, 0ms };
++ const ListIterator iterator(tasks);
++
++ const auto onSetup = [storage, iterator](TaskObject &taskObject) {
++ taskObject = *iterator;
++ storage->m_log.append({iterator.iteration(), Handler::Setup});
++ };
++
++ const auto onDone = [storage, iterator](DoneWith result) {
++ const Handler handler = result == DoneWith::Cancel ? Handler::Canceled : Handler::Error;
++ storage->m_log.append({iterator.iteration(), handler});
++ return DoneResult::Error;
++ };
++
++ const Group root = For (iterator) >> Do {
++ storage,
++ parallel,
++ TestTask(onSetup, onDone)
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ {0, Handler::Canceled}
++ };
++
++ QTest::newRow("ParallelDisorder") << TestData{storage, root, log, 2, DoneWith::Error, 1};
++ }
++
++ {
++ // Check if ListIterator work with QList.
++ const auto recipe = [storage](SetupResult result) {
++ const ListIterator iterator([storage] {
++ storage->m_log.append({0, Handler::IteratorRangeGetter});
++ return QList{1, 2, 3};
++ });
++
++ return Group {
++ storage,
++ For (iterator) >> Do {
++ onGroupSetup([result] { return result; }),
++ QSyncTask([storage, iterator] {
++ storage->m_log.append({*iterator, Handler::Sync});
++ })
++ }
++ };
++ };
++
++ const Log log {
++ {0, Handler::IteratorRangeGetter},
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync}
++ };
++
++ // Check if ListIterator's getter is called once when the group is entered,
++ // with storage accessible, and the returned list is used for iteration.
++ QTest::newRow("LazyListGetterExecutedOnceOnContinue")
++ << TestData{storage, recipe(SetupResult::Continue), log, 0, DoneWith::Success, 0};
++
++ // Check if ListIterator's getter is skipped when the group's setup handler
++ // returns a value other than SetupResult::Continue.
++ QTest::newRow("LazyListGetterSkippedOnStopWithSuccess")
++ << TestData{storage, recipe(SetupResult::StopWithSuccess), Log(), 0,
++ DoneWith::Success, 0};
++ QTest::newRow("LazyListGetterSkippedOnStopWithError")
++ << TestData{storage, recipe(SetupResult::StopWithError), Log(), 0,
++ DoneWith::Error, 0};
++ }
++
++ {
++ // Check if ListIterator work with std::vector.
++ const auto recipe = [storage](SetupResult result) {
++ const ListIterator iterator([storage] {
++ storage->m_log.append({0, Handler::IteratorRangeGetter});
++ return std::vector{1, 2, 3};
++ });
++
++ return Group {
++ storage,
++ For (iterator) >> Do {
++ onGroupSetup([result] { return result; }),
++ QSyncTask([storage, iterator] {
++ storage->m_log.append({*iterator, Handler::Sync});
++ })
++ }
++ };
++ };
++
++ const Log log {
++ {0, Handler::IteratorRangeGetter},
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync}
++ };
++
++ // Check if ListIterator's getter is called once when the group is entered,
++ // with storage accessible, and the returned list is used for iteration.
++ QTest::newRow("LazyVectorGetterExecutedOnceOnContinue")
++ << TestData{storage, recipe(SetupResult::Continue), log, 0, DoneWith::Success, 0};
++
++ // Check if ListIterator's getter is skipped when the group's setup handler
++ // returns a value other than SetupResult::Continue.
++ QTest::newRow("LazyVectorGetterSkippedOnStopWithSuccess")
++ << TestData{storage, recipe(SetupResult::StopWithSuccess), Log(), 0,
++ DoneWith::Success, 0};
++ QTest::newRow("LazyVectorGetterSkippedOnStopWithError")
++ << TestData{storage, recipe(SetupResult::StopWithError), Log(), 0,
++ DoneWith::Error, 0};
++ }
++
++ {
++ // Check if nested ListIterators work: the outer iterates over strings, the inner
++ // uses the callable overload to iterate over QChars of the current outer string.
++ const ListIterator outer(QStringList{"12", "34"});
++ const ListIterator inner([outer] { return *outer; });
++
++ const Group root {
++ storage,
++ For (outer) >> Do {
++ For (inner) >> Do {
++ QSyncTask([storage, inner] {
++ storage->m_log.append({(*inner).digitValue(), Handler::Sync});
++ })
++ }
++ }
++ };
++
++ const Log log {
++ {1, Handler::Sync},
++ {2, Handler::Sync},
++ {3, Handler::Sync},
++ {4, Handler::Sync},
++ };
++
++ QTest::newRow("NestedLazyList") << TestData{storage, root, log, 0, DoneWith::Success, 0};
++ }
++
++ {
++ // This test ensures the task done handler or onGroupDone accepts the DoneResult as an
++ // argument.
++
++ const Group groupSuccess {
++ storage,
++ Group {
++ onGroupDone(DoneResult::Success)
++ },
++ groupDone(0)
++ };
++ const Group groupError {
++ storage,
++ Group {
++ onGroupDone(DoneResult::Error)
++ },
++ groupDone(0)
++ };
++ const Group taskSuccess {
++ storage,
++ TestTask({}, DoneResult::Success),
++ groupDone(0)
++ };
++ const Group taskError {
++ storage,
++ TestTask({}, DoneResult::Error),
++ groupDone(0)
++ };
++
++ QTest::newRow("DoneResultGroupSuccess")
++ << TestData{storage, groupSuccess, {{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
++ QTest::newRow("DoneResultGroupError")
++ << TestData{storage, groupError, {{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
++ QTest::newRow("DoneResultTaskSuccess")
++ << TestData{storage, taskSuccess, {{0, Handler::GroupSuccess}}, 1, DoneWith::Success, 1};
++ QTest::newRow("DoneResultTaskError")
++ << TestData{storage, taskError, {{0, Handler::GroupError}}, 1, DoneWith::Error, 1};
++ }
++
++ {
++ // These tests ensure the task done handler or onGroupDone accepts the DoneResult as an
++ // argument.
++
++ const Group groupSuccess {
++ storage,
++ Group { createFailingTask(0) } || DoneResult::Success,
++ groupDone(0)
++ };
++
++ const Group groupError {
++ storage,
++ Group { createSuccessTask(0) } && DoneResult::Error,
++ groupDone(0)
++ };
++
++ const Group taskSuccess {
++ storage,
++ createFailingTask(0) || DoneResult::Success,
++ groupDone(0)
++ };
++
++ const Group taskError {
++ storage,
++ createSuccessTask(0) && DoneResult::Error,
++ groupDone(0)
++ };
++
++ const Log successLog {{0, Handler::Setup}, {0, Handler::Error}, {0, Handler::GroupSuccess}};
++ const Log errorLog {{0, Handler::Setup}, {0, Handler::Success}, {0, Handler::GroupError}};
++
++ QTest::newRow("LogicGroupSuccess")
++ << TestData{storage, groupSuccess, successLog, 1, DoneWith::Success, 1};
++ QTest::newRow("LogicGroupError")
++ << TestData{storage, groupError, errorLog, 1, DoneWith::Error, 1};
++ QTest::newRow("LogicTaskSuccess")
++ << TestData{storage, taskSuccess, successLog, 1, DoneWith::Success, 1};
++ QTest::newRow("LogicTaskError")
++ << TestData{storage, taskError, errorLog, 1, DoneWith::Error, 1};
++ }
++
++ {
++ // This test checks if ExecutableItem's negation works OK.
++
++ const Group negateSuccessTask {
++ storage,
++ !createSuccessTask(0)
++ };
++
++ const Group negateErrorTask {
++ storage,
++ !createFailingTask(0)
++ };
++
++ const Group negateSuccessGroup {
++ storage,
++ !Group {
++ createSuccessTask(0)
++ }
++ };
++
++ const Group negateErrorGroup {
++ storage,
++ !Group {
++ createFailingTask(0)
++ }
++ };
++
++ const Group doubleNegation {
++ storage,
++ !!createSuccessTask(0)
++ };
++
++ const Log successLog {{0, Handler::Setup}, {0, Handler::Success}};
++ const Log errorLog {{0, Handler::Setup}, {0, Handler::Error}};
++
++ QTest::newRow("NegateSuccessTask")
++ << TestData{storage, negateSuccessTask, successLog, 1, DoneWith::Error, 1};
++ QTest::newRow("NegateErrorTask")
++ << TestData{storage, negateErrorTask, errorLog, 1, DoneWith::Success, 1};
++ QTest::newRow("NegateSuccessGroup")
++ << TestData{storage, negateSuccessGroup, successLog, 1, DoneWith::Error, 1};
++ QTest::newRow("NegateErrorGroup")
++ << TestData{storage, negateErrorGroup, errorLog, 1, DoneWith::Success, 1};
++ QTest::newRow("DoubleNegation")
++ << TestData{storage, doubleNegation, successLog, 1, DoneWith::Success, 1};
++ }
++
++ {
++ // This test checks if ExecutableItem's AND and OR works OK.
++
++ const Group successAndSuccessTask {
++ storage,
++ createSuccessTask(0) && createSuccessTask(1)
++ };
++ const Group successAndErrorTask {
++ storage,
++ createSuccessTask(0) && createFailingTask(1)
++ };
++ const Group errorAndSuccessTask {
++ storage,
++ createFailingTask(0) && createSuccessTask(1)
++ };
++ const Group errorAndErrorTask {
++ storage,
++ createFailingTask(0) && createFailingTask(1)
++ };
++
++ const Group successOrSuccessTask {
++ storage,
++ createSuccessTask(0) || createSuccessTask(1)
++ };
++ const Group successOrErrorTask {
++ storage,
++ createSuccessTask(0) || createFailingTask(1)
++ };
++ const Group errorOrSuccessTask {
++ storage,
++ createFailingTask(0) || createSuccessTask(1)
++ };
++ const Group errorOrErrorTask {
++ storage,
++ createFailingTask(0) || createFailingTask(1)
++ };
++
++ const Log successLog {{0, Handler::Setup}, {0, Handler::Success}};
++ const Log errorLog {{0, Handler::Setup}, {0, Handler::Error}};
++
++ const Log successSuccessLog {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ };
++ const Log successErrorLog {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ };
++ const Log errorSuccessLog {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ };
++ const Log errorErrorLog {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {1, Handler::Setup},
++ {1, Handler::Error},
++ };
++
++ QTest::newRow("SuccessAndSuccessTask")
++ << TestData{storage, successAndSuccessTask, successSuccessLog, 2, DoneWith::Success, 2};
++ QTest::newRow("SuccessAndErrorTask")
++ << TestData{storage, successAndErrorTask, successErrorLog, 2, DoneWith::Error, 2};
++ QTest::newRow("ErrorAndSuccessTask")
++ << TestData{storage, errorAndSuccessTask, errorLog, 2, DoneWith::Error, 1};
++ QTest::newRow("ErrorAndErrorTask")
++ << TestData{storage, errorAndErrorTask, errorLog, 2, DoneWith::Error, 1};
++
++ QTest::newRow("SuccessOrSuccessTask")
++ << TestData{storage, successOrSuccessTask, successLog, 2, DoneWith::Success, 1};
++ QTest::newRow("SuccessOrErrorTask")
++ << TestData{storage, successOrErrorTask, successLog, 2, DoneWith::Success, 1};
++ QTest::newRow("ErrorOrSuccessTask")
++ << TestData{storage, errorOrSuccessTask, errorSuccessLog, 2, DoneWith::Success, 2};
++ QTest::newRow("ErrorOrErrorTask")
++ << TestData{storage, errorOrErrorTask, errorErrorLog, 2, DoneWith::Error, 2};
++ }
++
++ {
++ // This test ensures the nullItem in conditional expression works OK.
++
++ const auto recipe = [storage, createSuccessTask](bool condition) {
++ return Group {
++ storage,
++ condition ? createSuccessTask(0) : nullItem
++ };
++ };
++
++ const Log trueLog {{0, Handler::Setup}, {0, Handler::Success}};
++ const Log falseLog {};
++
++ QTest::newRow("NullItemTrue")
++ << TestData{storage, recipe(true), trueLog, 1, DoneWith::Success, 1};
++ QTest::newRow("NullItemFalse")
++ << TestData{storage, recipe(false), falseLog, 0, DoneWith::Success, 0};
++ }
++
++ {
++ // These tests ensure the successItem and errorItem work OK.
++
++ const auto recipe = [storage, createSuccessTask](bool success) {
++ return Group {
++ storage,
++ success ? successItem : errorItem
++ };
++ };
++
++ QTest::newRow("SuccessItem")
++ << TestData{storage, recipe(true), {}, 0, DoneWith::Success, 0};
++ QTest::newRow("ErrorItem")
++ << TestData{storage, recipe(false), {}, 0, DoneWith::Error, 0};
++ }
++
++ // This test checks if storage shadowing works OK.
++ QTest::newRow("StorageShadowing") << storageShadowingData();
++
++ // CONDITIONAL API
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createSuccessTask(1)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++
++ QTest::newRow("CondIfSuccessThenSuccess")
++ << TestData{storage, root, log, 2, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createFailingTask(1)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Error}
++ };
++
++ QTest::newRow("CondIfSuccessThenError")
++ << TestData{storage, root, log, 2, DoneWith::Error, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccess")
++ << TestData{storage, root, log, 2, DoneWith::Success, 1};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> Else {
++ createSuccessTask(2)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++
++ QTest::newRow("CondIfSuccessThenSuccessElseSuccess")
++ << TestData{storage, root, log, 3, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> Else {
++ createSuccessTask(2)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Success}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseSuccess")
++ << TestData{storage, root, log, 3, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> Else {
++ createFailingTask(2)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Error}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseError")
++ << TestData{storage, root, log, 3, DoneWith::Error, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> ElseIf (createFailingTask(2)) >> Then {
++ createSuccessTask(3)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Error}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseIfErrorThenSuccess")
++ << TestData{storage, root, log, 4, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> ElseIf (createSuccessTask(2)) >> Then {
++ createSuccessTask(3)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenSuccess")
++ << TestData{storage, root, log, 4, DoneWith::Success, 3};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> ElseIf (createSuccessTask(2)) >> Then {
++ createFailingTask(3)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Error}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenError")
++ << TestData{storage, root, log, 4, DoneWith::Error, 3};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> ElseIf (createSuccessTask(2)) >> Then {
++ createSuccessTask(3)
++ } >> Else {
++ createSuccessTask(4)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++
++ QTest::newRow("CondIfSuccessThenSuccessElseIfSuccessThenSuccessElseSuccess")
++ << TestData{storage, root, log, 5, DoneWith::Success, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createSuccessTask(1)
++ } >> ElseIf (createSuccessTask(2)) >> Then {
++ createSuccessTask(3)
++ } >> Else {
++ createSuccessTask(4)
++ }
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {3, Handler::Setup},
++ {3, Handler::Success}
++ };
++
++ QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenSuccessElseSuccess")
++ << TestData{storage, root, log, 5, DoneWith::Success, 3};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createSuccessTask(1)
++ },
++ createSuccessTask(2)
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success},
++ {2, Handler::Setup},
++ {2, Handler::Success}
++ };
++
++ QTest::newRow("CondIfSuccessThenSuccessWithContinuation")
++ << TestData{storage, root, log, 3, DoneWith::Success, 3};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createSuccessTask(0)) >> Then {
++ createFailingTask(1)
++ },
++ createSuccessTask(2)
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Error}
++ };
++
++ QTest::newRow("CondIfSuccessThenErrorWithContinuation")
++ << TestData{storage, root, log, 3, DoneWith::Error, 2};
++ }
++
++ {
++ const Group root {
++ storage,
++ If (createFailingTask(0)) >> Then {
++ createFailingTask(1)
++ },
++ createSuccessTask(2)
++ };
++
++ const Log log {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {2, Handler::Setup},
++ {2, Handler::Success}
++ };
++
++ QTest::newRow("CondIfErrorThenErrorWithContinuation")
++ << TestData{storage, root, log, 3, DoneWith::Success, 2};
++ }
++
++ {
++ // These tests ensure the successItem and errorItem work OK in if-statement.
++
++ const auto recipe = [storage, createSuccessTask, createFailingTask]
++ (bool ifCondition, bool bodyResult, bool elseResult) {
++ return Group {
++ storage,
++ If (ifCondition ? createSuccessTask(0) : createFailingTask(0)) >> Then {
++ bodyResult ? successItem : errorItem
++ } >> Else {
++ elseResult ? successItem : errorItem
++ },
++ createSuccessTask(1)
++ };
++ };
++
++ const Log trueTrueTrue {
++ {0, Handler::Setup},
++ {0, Handler::Success},
++ {1, Handler::Setup},
++ {1, Handler::Success}
++ };
++ const Log trueTrueFalse = trueTrueTrue; // Else branch skipped.
++ const Log trueFalseTrue {
++ {0, Handler::Setup},
++ {0, Handler::Success}
++ };
++ const Log trueFalseFalse = trueFalseTrue; // Else branch skipped.
++ const Log falseTrueTrue {
++ {0, Handler::Setup},
++ {0, Handler::Error},
++ {1, Handler::Setup}, // Else branch returns successItem -> continue after If.
++ {1, Handler::Success}
++ };
++ const Log falseTrueFalse {
++ {0, Handler::Setup},
++ {0, Handler::Error}
++ }; // Else branch returns errorItem -> main group skips task 2 and stops with error.
++ const Log falseFalseTrue = falseTrueTrue; // Then branch skipped.
++ const Log falseFalseFalse = falseTrueFalse; // Then branch skipped
++
++ QTest::newRow("BoolItemTrueTrueTrue")
++ << TestData{storage, recipe(true, true, true), trueTrueTrue, 2, DoneWith::Success, 2};
++ QTest::newRow("BoolItemTrueTrueFalse")
++ << TestData{storage, recipe(true, true, false), trueTrueFalse, 2, DoneWith::Success, 2};
++ QTest::newRow("BoolItemTrueFalseTrue")
++ << TestData{storage, recipe(true, false, true), trueFalseTrue, 2, DoneWith::Error, 1};
++ QTest::newRow("BoolItemTrueFalseFalse")
++ << TestData{storage, recipe(true, false, false), trueFalseFalse, 2, DoneWith::Error, 1};
++ QTest::newRow("BoolItemFalseTrueTrue")
++ << TestData{storage, recipe(false, true, true), falseTrueTrue, 2, DoneWith::Success, 2};
++ QTest::newRow("BoolItemFalseTrueFalse")
++ << TestData{storage, recipe(false, true, false), falseTrueFalse, 2, DoneWith::Error, 1};
++ QTest::newRow("BoolItemFalseFalseTrue")
++ << TestData{storage, recipe(false, false, true), falseFalseTrue, 2, DoneWith::Success, 2};
++ QTest::newRow("BoolItemFalseFalseFalse")
++ << TestData{storage, recipe(false, false, false), falseFalseFalse, 2, DoneWith::Error, 1};
++ }
++
++ {
++ // withCancel / withAccept
++
++ const Storage<std::unique_ptr<QTimer>> tickStorage;
++
++ const auto onSetup = [tickStorage](milliseconds timeout) {
++ return [tickStorage, timeout] {
++ tickStorage->reset(new QTimer);
++ tickStorage->get()->start(timeout);
++ };
++ };
++
++ const auto onTickSetup = [tickStorage] {
++ return makeObjectSignal(tickStorage->get(), &QTimer::timeout);
++ };
++
++ const Group cancelTriggeredRecipe {
++ storage,
++ tickStorage,
++ onGroupSetup(onSetup(0ms)), // 1st queued call
++ Group {
++ groupSetup(1),
++ createSuccessTask(2, s_endlessTime),
++ groupDone(1)
++ }.withCancel(onTickSetup, {
++ createSuccessTask(3), // 2nd queued call
++ createFailingTask(4), // 3rd queued call
++ createSuccessTask(5)
++ })
++ };
++
++ const Log cancelTriggeredLog {
++ {1, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Canceled},
++ {1, Handler::GroupCanceled},
++ {3, Handler::Setup},
++ {3, Handler::Success},
++ {4, Handler::Setup},
++ {4, Handler::Error}
++ };
++
++ QTest::newRow("WithCancelTriggered")
++ << TestData{storage, cancelTriggeredRecipe, cancelTriggeredLog, 5, DoneWith::Error, 3};
++
++ const Group cancelSkippedSuccessRecipe {
++ storage,
++ tickStorage,
++ onGroupSetup(onSetup(s_endlessTime)),
++ Group {
++ groupSetup(1),
++ createSuccessTask(2), // 1st queued call
++ groupDone(1)
++ }.withCancel(onTickSetup, { // This group is skipped since the previous group wasn't canceled
++ createSuccessTask(3),
++ createFailingTask(4),
++ createSuccessTask(5)
++ })
++ };
++
++ const Log cancelSkippedSuccessLog {
++ {1, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {1, Handler::GroupSuccess}
++ };
++
++ QTest::newRow("WithCancelSkippedSuccess")
++ << TestData{storage, cancelSkippedSuccessRecipe, cancelSkippedSuccessLog, 5, DoneWith::Success, 1};
++
++ const Group cancelSkippedErrorRecipe {
++ storage,
++ tickStorage,
++ onGroupSetup(onSetup(s_endlessTime)),
++ Group {
++ groupSetup(1),
++ createFailingTask(2), // 1st queued call
++ groupDone(1)
++ }.withCancel(onTickSetup, { // This group is skipped since the previous group wasn't canceled
++ createSuccessTask(3),
++ createFailingTask(4),
++ createSuccessTask(5)
++ })
++ };
++
++ const Log cancelSkippedErrorLog {
++ {1, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Error},
++ {1, Handler::GroupError}
++ };
++
++ QTest::newRow("WithCancelSkippedError")
++ << TestData{storage, cancelSkippedErrorRecipe, cancelSkippedErrorLog, 5, DoneWith::Error, 1};
++
++ const Group acceptRecipe {
++ storage,
++ tickStorage,
++ onGroupSetup(onSetup(1ms)),
++ Group {
++ groupSetup(1),
++ createSuccessTask(2, 0ms).withAccept(onTickSetup),
++ groupDone(1)
++ }
++ };
++
++ const Log acceptLog {
++ {1, Handler::GroupSetup},
++ {2, Handler::Setup},
++ {2, Handler::Success},
++ {1, Handler::GroupSuccess},
++ };
++
++ QTest::newRow("WithAccept")
++ << TestData{storage, acceptRecipe, acceptLog, 2, DoneWith::Success, 2};
++ }
++ }
++
++ static QtMessageHandler s_oldMessageHandler = nullptr;
++ static QStringList s_messages;
++
++ class LogMessageHandler
++ {
++ public:
++ LogMessageHandler()
++ {
++ s_messages.clear();
++ s_oldMessageHandler = qInstallMessageHandler(handler);
++ }
++ ~LogMessageHandler() { qInstallMessageHandler(s_oldMessageHandler); }
++
++ private:
++ static void handler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
++ {
++ QString message = msg;
++ message.remove('\r');
++ s_messages.append(message);
++ // Defer to old message handler.
++ if (s_oldMessageHandler)
++ s_oldMessageHandler(type, context, msg);
++ }
++ };
++
++ static const QList<Handler> s_logHandlers
++ = {Handler::Setup, Handler::Success, Handler::Error, Handler::Canceled};
++
++ static QString messageInfix(const Message &message)
++ {
++ if (message.handler == Handler::Setup)
++ return "started";
++
++ DoneWith doneWith;
++ switch (message.handler) {
++ case Handler::Success:
++ doneWith = DoneWith::Success;
++ break;
++ case Handler::Error:
++ doneWith = DoneWith::Error;
++ break;
++ case Handler::Canceled:
++ doneWith = DoneWith::Cancel;
++ break;
++ default:
++ return {};
++ }
++
++ const QString execution = message.execution == Execution::Async ? "asynchronously"
++ : "synchronously";
++ const QMetaEnum doneWithEnum = QMetaEnum::fromType<DoneWith>();
++ return QString("finished %1 with %2").arg(execution, doneWithEnum.valueToKey(int(doneWith)));
++ }
++
++ static bool matchesLogPattern(const QString &log, const Message &message)
++ {
++ QStringView logView(log);
++ const static QLatin1String part1("TASK TREE LOG [");
++ if (!logView.startsWith(part1))
++ return false;
++
++ logView = logView.mid(part1.size());
++ const static QLatin1String part2("HH:mm:ss.zzz"); // check only size
++
++ logView = logView.mid(part2.size());
++ const QString part3 = QString("] \"%1\" ").arg(message.name);
++ if (!logView.startsWith(part3))
++ return false;
++
++ logView = logView.mid(part3.size());
++ const QString part4(messageInfix(message));
++ if (!logView.startsWith(part4))
++ return false;
++
++ logView = logView.mid(part4.size());
++ if (message.handler == Handler::Setup)
++ return logView == QLatin1String(".");
++
++ const static QLatin1String part5(" within ");
++ if (!logView.startsWith(part5))
++ return false;
++
++ return logView.endsWith(QLatin1String("ms."));
++ }
++
++ const QMetaEnum s_handlerEnum = QMetaEnum::fromType<Handler>();
++
++ void tst_TaskTree::testTree()
++ {
++ QFETCH(TestData, testData);
++
++ LogMessageHandler handler;
++
++ QTaskTree taskTree({testData.root.withTimeout(1s)});
++ const int taskCount = taskTree.taskCount() - 1; // -1 for the timeout task above
++ QCOMPARE(taskCount, testData.taskCount);
++ Log actualLog;
++ const auto collectLog = [&actualLog](const CustomStorage &storage) {
++ actualLog = storage.m_log;
++ };
++ taskTree.onStorageDone(testData.storage, collectLog);
++ const DoneWith result = taskTree.runBlocking();
++ QCOMPARE(taskTree.isRunning(), false);
++
++ QCOMPARE(taskTree.progressValue(), taskTree.progressMaximum());
++ QCOMPARE(actualLog, testData.expectedLog);
++ QCOMPARE(CustomStorage::instanceCount(), 0);
++
++ QCOMPARE(result, testData.result);
++
++ if (testData.asyncCount)
++ QCOMPARE(taskTree.asyncCount(), *testData.asyncCount);
++
++ if (testData.messageLog) {
++ QCOMPARE(s_messages.count(), testData.messageLog->count());
++
++ for (int i = 0; i < s_messages.count(); ++i) {
++ const auto &message = testData.messageLog->at(i);
++ QVERIFY2(s_logHandlers.contains(message.handler), qPrintable(QString(
++ "The expected log message at index %1 contains invalid %2 handler.")
++ .arg(i).arg(s_handlerEnum.valueToKey(int(message.handler)))));
++ if (message.handler != Handler::Setup) {
++ QVERIFY2(message.execution, qPrintable(QString(
++ "The expected log message at index %1 of %2 type doesn't specify the required "
++ "execution mode.").arg(i).arg(s_handlerEnum.valueToKey(int(message.handler)))));
++ }
++ const QString &log = s_messages.at(i);
++ QVERIFY2(matchesLogPattern(log, message), qPrintable(QString(
++ "The log message at index %1: \"%2\" doesn't match the expected pattern: "
++ "\"%3\" %4.").arg(i).arg(log, message.name, messageInfix(message))));
++ }
++ }
++ }
++
++ #if QT_CONFIG(concurrent)
++
++ void tst_TaskTree::testInThread_data()
++ {
++ QTest::addColumn<TestData>("testData");
++ QTest::newRow("StorageShadowing") << storageShadowingData();
++ QTest::newRow("Parallel") << parallelData();
++ }
++
++ struct TestResult
++ {
++ int executeCount = 0;
++ ThreadResult threadResult = ThreadResult::Success;
++ Log actualLog = {};
++ };
++
++ static const int s_loopCount = 1000;
++ static const int s_threadCount = 12;
++
++ static void runInThread(QPromise<TestResult> &promise, const TestData &testData)
++ {
++ for (int i = 0; i < s_loopCount; ++i) {
++ if (promise.isCanceled()) {
++ promise.addResult(TestResult{i, ThreadResult::Canceled});
++ return;
++ }
++
++ QTaskTree taskTree({testData.root.withTimeout(1000ms)});
++ if (taskTree.taskCount() - 1 != testData.taskCount) { // -1 for the timeout task above
++ promise.addResult(TestResult{i, ThreadResult::FailOnTaskCountCheck});
++ return;
++ }
++ Log actualLog;
++ const auto collectLog = [&actualLog](const CustomStorage &storage) {
++ actualLog = storage.m_log;
++ };
++ taskTree.onStorageDone(testData.storage, collectLog);
++
++ const DoneWith result = taskTree.runBlocking(QFuture<void>(promise.future()));
++
++ if (taskTree.isRunning()) {
++ promise.addResult(TestResult{i, ThreadResult::FailOnRunningCheck});
++ return;
++ }
++ if (taskTree.progressValue() != taskTree.progressMaximum()) {
++ promise.addResult(TestResult{i, ThreadResult::FailOnProgressCheck});
++ return;
++ }
++ if (actualLog != testData.expectedLog) {
++ promise.addResult(TestResult{i, ThreadResult::FailOnLogCheck, actualLog});
++ return;
++ }
++ if (result != testData.result) {
++ promise.addResult(TestResult{i, ThreadResult::FailOnDoneStatusCheck});
++ return;
++ }
++ }
++ promise.addResult(TestResult{s_loopCount, ThreadResult::Success, testData.expectedLog});
++ }
++
++ void tst_TaskTree::testInThread()
++ {
++ QFETCH(TestData, testData);
++
++ const auto onSetup = [testData](QThreadFunction<TestResult> &task) {
++ task.setThreadFunctionData(&runInThread, testData);
++ };
++ const auto onDone = [testData](const QThreadFunction<TestResult> &task) {
++ QVERIFY(task.future().resultCount());
++ const TestResult result = task.result();
++ QCOMPARE(result.actualLog, testData.expectedLog);
++ QCOMPARE(result.threadResult, ThreadResult::Success);
++ QCOMPARE(result.executeCount, s_loopCount);
++ };
++
++ const Group recipe = For (RepeatIterator(s_threadCount)) >> Do {
++ parallel,
++ QThreadFunctionTask<TestResult>(onSetup, onDone)
++ };
++
++ QTaskTree taskTree(recipe);
++ const DoneWith result = taskTree.runBlocking();
++ QCOMPARE(taskTree.isRunning(), false);
++
++ QCOMPARE(CustomStorage::instanceCount(), 0);
++ QCOMPARE(result, testData.result);
++ }
++
++ static std::atomic_bool s_jobStarted = false;
++ static std::atomic_bool s_treeFinished = false;
++ static std::atomic_bool s_syncDone = false;
++
++ static void autoSyncOn(QPromise<void> &promise)
++ {
++ QThread::sleep(1ms);
++ s_jobStarted = true; // Let know the threadJob has started.
++ while (s_treeFinished == false)
++ QThread::sleep(1ms); // Ensures the tree has finished.
++ Q_ASSERT(promise.isCanceled()); // Tree has already canceled the promise.
++ QThread::sleep(1ms); // Spin for a while so that QThreadFunctionBase::syncAll() awaits for us.
++ s_syncDone = true;
++ }
++
++ static void autoSyncOff(QPromise<void> &promise)
++ {
++ Q_ASSERT(!promise.isCanceled()); // Tree is still running.
++ s_jobStarted = true; // Let know the threadJob has started.
++ while (!promise.isCanceled())
++ QThread::sleep(1ms); // Spin for a while so that ~QThreadFunction awaits for us.
++ s_syncDone = true;
++ }
++
++ void tst_TaskTree::testAutoDelayedSync()
++ {
++ const auto onAutoSyncOnSetup = [](QThreadFunction<void> &task) {
++ task.setThreadFunctionData(&autoSyncOn);
++ };
++
++ const auto isNotStarted = [](int) { return s_jobStarted == false; };
++
++ const Group awaiter = For (UntilIterator(isNotStarted)) >> Do {
++ timeoutTask(1ms, DoneResult::Success)
++ } && errorItem;
++
++ const Group recipeAutoSyncOn {
++ parallel,
++ QThreadFunctionTask<void>(onAutoSyncOnSetup),
++ awaiter
++ };
++
++ const DoneWith resultOn = QTaskTree::runBlocking(recipeAutoSyncOn);
++ QCOMPARE(resultOn, DoneWith::Error);
++ QCOMPARE(s_syncDone, false);
++ s_treeFinished = true;
++ QThreadFunctionBase::syncAll();
++ QCOMPARE(s_syncDone, true);
++
++ s_jobStarted = false;
++ s_treeFinished = false;
++ s_syncDone = false ;
++
++ const auto onAutoSyncOffSetup = [](QThreadFunction<void> &task) {
++ task.setAutoDelayedSync(false);
++ task.setThreadFunctionData(autoSyncOff);
++ };
++
++ const Group recipeAutoSyncOff {
++ parallel,
++ QThreadFunctionTask<void>(onAutoSyncOffSetup),
++ awaiter
++ };
++
++ const DoneWith resultOff = QTaskTree::runBlocking(recipeAutoSyncOff);
++ QCOMPARE(resultOff, DoneWith::Error);
++ QCOMPARE(s_syncDone, true);
++ }
++
++ #endif // QT_CONFIG(concurrent)
++
++ struct StorageIO
++ {
++ int value = 0;
++ };
++
++ static Group inputOutputRecipe(const Storage<StorageIO> &storage)
++ {
++ return Group {
++ storage,
++ onGroupSetup([storage] { ++storage->value; }),
++ onGroupDone([storage] { storage->value *= 2; })
++ };
++ }
++
++ void tst_TaskTree::storageIO_data()
++ {
++ QTest::addColumn<int>("input");
++ QTest::addColumn<int>("output");
++
++ QTest::newRow("-1 -> 0") << -1 << 0;
++ QTest::newRow("0 -> 2") << 0 << 2;
++ QTest::newRow("1 -> 4") << 1 << 4;
++ QTest::newRow("2 -> 6") << 2 << 6;
++ }
++
++ void tst_TaskTree::storageIO()
++ {
++ QFETCH(int, input);
++ QFETCH(int, output);
++
++ int actualOutput = 0;
++
++ const Storage<StorageIO> storage;
++ QTaskTree taskTree(inputOutputRecipe(storage));
++
++ const auto setInput = [input](StorageIO &storage) { storage.value = input; };
++ const auto getOutput = [&actualOutput](const StorageIO &storage) { actualOutput = storage.value; };
++
++ taskTree.onStorageSetup(storage, setInput);
++ taskTree.onStorageDone(storage, getOutput);
++ taskTree.runBlocking();
++
++ QCOMPARE(taskTree.isRunning(), false);
++ QCOMPARE(actualOutput, output);
++ }
++
++ void tst_TaskTree::storageOperators()
++ {
++ StorageBase storage1 = Storage<CustomStorage>();
++ StorageBase storage2 = Storage<CustomStorage>();
++ StorageBase storage3 = storage1;
++
++ QVERIFY(storage1 == storage3);
++ QVERIFY(storage1 != storage2);
++ QVERIFY(storage2 != storage3);
++ }
++
++ // This test checks whether a running task tree may be safely destructed.
++ // It also checks whether the destructor of a task tree deletes properly the storage created
++ // while starting the task tree. When running task tree is destructed, the storage done
++ // handler shouldn't be invoked.
++ void tst_TaskTree::storageDestructor()
++ {
++ bool setupCalled = false;
++ const auto setupHandler = [&setupCalled](CustomStorage &) {
++ setupCalled = true;
++ };
++ bool doneCalled = false;
++ const auto doneHandler = [&doneCalled](const CustomStorage &) {
++ doneCalled = true;
++ };
++ QCOMPARE(CustomStorage::instanceCount(), 0);
++ {
++ const Storage<CustomStorage> storage;
++ const auto setupSleepingTask = [](TaskObject &taskObject) {
++ taskObject = 1000ms;
++ };
++ const Group root {
++ storage,
++ TestTask(setupSleepingTask)
++ };
++
++ QTaskTree taskTree(root);
++ QCOMPARE(CustomStorage::instanceCount(), 0);
++ taskTree.onStorageSetup(storage, setupHandler);
++ taskTree.onStorageDone(storage, doneHandler);
++ taskTree.start();
++ QCOMPARE(CustomStorage::instanceCount(), 1);
++ }
++ QCOMPARE(CustomStorage::instanceCount(), 0);
++ QVERIFY(setupCalled);
++ QVERIFY(!doneCalled);
++ }
++
++ void tst_TaskTree::storageInitialization_data()
++ {
++ QTest::addColumn<Storage<int>>("storage");
++ QTest::addColumn<int>("initValue");
++
++ // This test ensures that the storage data is zero-initialized.
++ QTest::newRow("zero initialization") << Storage<int>() << 0;
++ // This test ensures that the storage c'tor with data is initialized properly.
++ QTest::newRow("data initialization") << Storage<int>(42) << 42;
++ }
++
++ void tst_TaskTree::storageInitialization()
++ {
++ QFETCH(Storage<int>, storage);
++ QFETCH(int, initValue);
++
++ std::optional<int> storageValue = std::nullopt;
++
++ const auto onSetup = [storage, &storageValue] { storageValue = *storage; };
++
++ QTaskTree taskTree({ storage, onGroupSetup(onSetup) });
++ taskTree.runBlocking();
++
++ QVERIFY(storageValue);
++ QCOMPARE(*storageValue, initValue);
++ }
++
++ // This test ensures that when a missing storage object inside the nested task tree is accessed
++ // directly from the outer task tree's handler, containing the same storage object, then we
++ // detect this misconfigured recipe, issue a warning, and return nullptr for the storage
++ // being accessed (instead of returning parent's task tree's storage instance).
++ // This test should also trigger a runtime assert that we are accessing the nullptr storage.
++ void tst_TaskTree::nestedBrokenStorage()
++ {
++ int *outerStorage = nullptr;
++ int *innerStorage1 = nullptr;
++ int *innerStorage2 = nullptr;
++ const Storage<int> storage;
++
++ const auto onOuterSync = [storage, &outerStorage, &innerStorage1, &innerStorage2] {
++ outerStorage = &*storage;
++
++ const auto onInnerSync1 = [storage, &innerStorage1] {
++ innerStorage1 = &*storage; // Triggers the runtime assert on purpose.
++ };
++ const auto onInnerSync2 = [storage, &innerStorage2] {
++ innerStorage2 = &*storage; // Storage is accessible in currently running tree - all OK.
++ };
++
++ const Group innerRecipe {
++ Group {
++ // Broken subrecipe, the storage wasn't placed inside the recipe.
++ // storage,
++ QSyncTask(onInnerSync1)
++ },
++ Group {
++ storage, // Subrecipe OK, another instance for the nested storage will be created.
++ QSyncTask(onInnerSync2)
++ }
++ };
++
++ QTaskTree::runBlocking(innerRecipe);
++ };
++
++ const Group outerRecipe {
++ storage,
++ QSyncTask(onOuterSync)
++ };
++
++ QTaskTree::runBlocking(outerRecipe);
++ QVERIFY(outerStorage != nullptr);
++ QCOMPARE(innerStorage1, nullptr);
++ QVERIFY(innerStorage2 != nullptr);
++ QVERIFY(innerStorage2 != outerStorage);
++ }
++
++ void tst_TaskTree::restart()
++ {
++ QTaskTree taskTree({TestTask([](TaskObject &taskObject) { taskObject = 1000ms; })});
++ taskTree.start();
++ QVERIFY(taskTree.isRunning());
++ taskTree.cancel();
++ QVERIFY(!taskTree.isRunning());
++ taskTree.start();
++ QVERIFY(taskTree.isRunning());
++ }
++
++ class BrokenTaskAdapter final
++ {
++ public:
++ // QTCREATORBUG-30204
++ ~BrokenTaskAdapter()
++ {
++ if (m_iface)
++ m_iface->reportDone(DoneResult::Success);
++ }
++ void operator()(int *, QTaskInterface *iface) { m_iface = iface; }
++ private:
++ QTaskInterface *m_iface = nullptr;
++ };
++
++ using BrokenTask = QCustomTask<int, BrokenTaskAdapter>;
++
++ void tst_TaskTree::destructorOfTaskEmittingDone()
++ {
++ QTaskTree taskTree({BrokenTask()});
++ taskTree.start();
++ }
++
++ void tst_TaskTree::restartTaskTreeRunnerFromDoneHandler()
++ {
++ QSingleTaskTreeRunner runner;
++ QStringList log;
++ QStringList expectedLog{"1", "2"};
++
++ const auto onFirstDone = [&runner, &log] {
++ log.append("1");
++ runner.start({TestTask()}, {}, [&log] { log.append("2"); });
++ };
++ runner.start({TestTask()}, {}, onFirstDone);
++ QTRY_VERIFY(!runner.isRunning());
++ QCOMPARE(log, expectedLog);
++ }
++
++ void tst_TaskTree::validConditionalConstructs()
++ {
++ const TestTask condition;
++ const TestTask continuation;
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ Group {
++ parallel,
++ TestTask(),
++ If (condition) >> Then {
++ continuation
++ },
++ If (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> ElseIf (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ Group {
++ If (condition && condition) >> Then {
++ continuation
++ } >> ElseIf (!condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ // The following constucts are invalid and won't compile:
++ #if 0
++
++ // Lack of "Then" body.
++ Group {
++ If (condition)
++ };
++
++ // Can't start with "ElseIf".
++ Group {
++ ElseIf (condition)
++ };
++
++ // Can't start with "Else".
++ Group {
++ Else { continuation }
++ };
++
++ // Can't start with "Then".
++ Group {
++ Then { continuation }
++ };
++
++ // "Then" can't be followed by "If".
++ // Replace ">>" with "," after the first "Then" to construct 2 independent if conditions.
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> If (condition) >> Then {
++ continuation
++ }
++ };
++
++ // "Else" can't be followed by anything, it must be the final statement.
++ Group {
++ If (condition) >> Then {
++ continuation
++ } >> Else {
++ continuation
++ } >> Else {
++ continuation
++ }
++ };
++
++ #endif
++ }
++
++ void tst_TaskTree::exactHandlers()
++ {
++ bool taskSetupCalled = false;
++ bool taskDoneCalled = false;
++ bool treeSetupCalled = false;
++ bool treeDoneCalled = false;
++
++ const TestTask::TaskSetupHandler onTaskSetup = [&taskSetupCalled](TaskObject &) {
++ taskSetupCalled = true;
++ return SetupResult::Continue;
++ };
++
++ const TestTask::TaskDoneHandler onTaskDone = [&taskDoneCalled](const TaskObject &, DoneWith) {
++ taskDoneCalled = true;
++ return DoneResult::Success;
++ };
++
++ const TreeSetupHandler onTreeSetup = [&treeSetupCalled](QTaskTree &) {
++ treeSetupCalled = true;
++ };
++
++ const TreeDoneHandler onTreeDone
++ = [&treeDoneCalled](const QTaskTree &, DoneWith) {
++ treeDoneCalled = true;
++ };
++
++ QSingleTaskTreeRunner taskTreeRunner;
++ taskTreeRunner.start({TestTask(onTaskSetup, onTaskDone)}, onTreeSetup, onTreeDone);
++
++ QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);
++
++ QVERIFY(taskSetupCalled);
++ QVERIFY(taskDoneCalled);
++ QVERIFY(treeSetupCalled);
++ QVERIFY(treeDoneCalled);
++ }
++
++ class BoolDone : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ void start() { Q_EMIT done(true); }
++
++ Q_SIGNALS:
++ void done(bool);
++ };
++
++ using BoolDoneTask = QCustomTask<BoolDone>;
++
++ void tst_TaskTree::boolDoneAdapter()
++ {
++ std::optional<bool> result = std::nullopt;
++ const auto onDone = [&result](DoneWith doneWith) {
++ result = doneWith == DoneWith::Success ? true : false;
++ };
++ QSingleTaskTreeRunner taskTreeRunner;
++ taskTreeRunner.start({BoolDoneTask()}, {}, onDone);
++ QVERIFY(!taskTreeRunner.isRunning());
++ QVERIFY(result);
++ QVERIFY(*result);
++ }
++
++ void tst_TaskTree::cleanupTestCase()
++ {
++ #if QT_CONFIG(concurrent)
++ QThreadFunctionBase::syncAll();
++ #endif
++ }
++
++ QTEST_GUILESS_MAIN(tst_TaskTree)
++
++ #include "tst_qttasktree.moc"