// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include using namespace Utils; enum class Handler { Setup, Done, Error, GroupSetup, GroupDone, GroupError }; using Log = QList>; class tst_TaskTree : public QObject { Q_OBJECT private slots: void initTestCase(); void validConstructs(); // compile test void processTree_data(); void processTree(); void storage_data(); void storage(); void cleanupTestCase(); private: Log m_log; FilePath m_testAppPath; }; void tst_TaskTree::initTestCase() { Utils::TemporaryDirectory::setMasterTemporaryDirectory(QDir::tempPath() + "/" + Core::Constants::IDE_CASED_ID + "-XXXXXX"); const QString libExecPath(qApp->applicationDirPath() + '/' + QLatin1String(TEST_RELATIVE_LIBEXEC_PATH)); LauncherInterface::setPathToLauncher(libExecPath); m_testAppPath = FilePath::fromString(QLatin1String(TESTAPP_PATH) + QLatin1String("/testapp")).withExecutableSuffix(); } void tst_TaskTree::cleanupTestCase() { Utils::Singleton::deleteAll(); } void tst_TaskTree::validConstructs() { using namespace Tasking; const Group process { parallel, Process([](QtcProcess &) {}, [](const QtcProcess &) {}), Process([](QtcProcess &) {}, [](const QtcProcess &) {}), Process([](QtcProcess &) {}, [](const QtcProcess &) {}) }; const Group group1 { process }; const Group group2 { parallel, Group { parallel, Process([](QtcProcess &) {}, [](const QtcProcess &) {}), Group { parallel, Process([](QtcProcess &) {}, [](const QtcProcess &) {}), Group { parallel, Process([](QtcProcess &) {}, [](const QtcProcess &) {}) } }, Group { parallel, Process([](QtcProcess &) {}, [](const QtcProcess &) {}), OnGroupDone([] {}), } }, process, OnGroupDone([] {}), OnGroupError([] {}) }; } static const char s_processIdProperty[] = "__processId"; void tst_TaskTree::processTree_data() { using namespace Tasking; using namespace std::placeholders; QTest::addColumn("root"); QTest::addColumn("expectedLog"); QTest::addColumn("runningAfterStart"); QTest::addColumn("success"); QTest::addColumn("taskCount"); const auto setupProcessHelper = [this](QtcProcess &process, const QStringList &args, int processId) { process.setCommand(CommandLine(m_testAppPath, args)); process.setProperty(s_processIdProperty, processId); m_log.append({processId, Handler::Setup}); }; const auto setupProcess = [setupProcessHelper](QtcProcess &process, int processId) { setupProcessHelper(process, {"-return", "0"}, processId); }; // const auto setupErrorProcess = [setupProcessHelper](QtcProcess &process, int processId) { // setupProcessHelper(process, {"-return", "1"}, processId); // }; const auto setupCrashProcess = [setupProcessHelper](QtcProcess &process, int processId) { setupProcessHelper(process, {"-crash"}, processId); }; const auto readResultAnonymous = [this](const QtcProcess &) { m_log.append({-1, Handler::Done}); }; const auto readResult = [this](const QtcProcess &process) { const int processId = process.property(s_processIdProperty).toInt(); m_log.append({processId, Handler::Done}); }; const auto readError = [this](const QtcProcess &process) { const int processId = process.property(s_processIdProperty).toInt(); m_log.append({processId, Handler::Error}); }; const auto groupSetup = [this](int processId) { m_log.append({processId, Handler::GroupSetup}); }; const auto groupDone = [this](int processId) { m_log.append({processId, Handler::GroupDone}); }; const auto rootDone = [this] { m_log.append({-1, Handler::GroupDone}); }; const auto rootError = [this] { m_log.append({-1, Handler::GroupError}); }; const Group emptyRoot { OnGroupDone(rootDone) }; const Log emptyLog{{-1, Handler::GroupDone}}; QTest::newRow("Empty") << emptyRoot << emptyLog << false << true << 0; const Group nestedRoot { Group { Group { Group { Group { Group { Process(std::bind(setupProcess, _1, 5), readResult), OnGroupSetup(std::bind(groupSetup, 5)), OnGroupDone(std::bind(groupDone, 5)) }, OnGroupSetup(std::bind(groupSetup, 4)), OnGroupDone(std::bind(groupDone, 4)) }, OnGroupSetup(std::bind(groupSetup, 3)), OnGroupDone(std::bind(groupDone, 3)) }, OnGroupSetup(std::bind(groupSetup, 2)), OnGroupDone(std::bind(groupDone, 2)) }, OnGroupSetup(std::bind(groupSetup, 1)), OnGroupDone(std::bind(groupDone, 1)) }, OnGroupDone(rootDone) }; const Log nestedLog{{1, Handler::GroupSetup}, {2, Handler::GroupSetup}, {3, Handler::GroupSetup}, {4, Handler::GroupSetup}, {5, Handler::GroupSetup}, {5, Handler::Setup}, {5, Handler::Done}, {5, Handler::GroupDone}, {4, Handler::GroupDone}, {3, Handler::GroupDone}, {2, Handler::GroupDone}, {1, Handler::GroupDone}, {-1, Handler::GroupDone}}; QTest::newRow("Nested") << nestedRoot << nestedLog << true << true << 1; const Group parallelRoot { parallel, Process(std::bind(setupProcess, _1, 1), readResultAnonymous), Process(std::bind(setupProcess, _1, 2), readResultAnonymous), Process(std::bind(setupProcess, _1, 3), readResultAnonymous), Process(std::bind(setupProcess, _1, 4), readResultAnonymous), Process(std::bind(setupProcess, _1, 5), readResultAnonymous), OnGroupDone(rootDone) }; const Log parallelLog{{1, Handler::Setup}, // Setup order is determined in parallel mode {2, Handler::Setup}, {3, Handler::Setup}, {4, Handler::Setup}, {5, Handler::Setup}, {-1, Handler::Done}, // Done order isn't determined in parallel mode {-1, Handler::Done}, {-1, Handler::Done}, {-1, Handler::Done}, {-1, Handler::Done}, {-1, Handler::GroupDone}}; // Done handlers may come in different order QTest::newRow("Parallel") << parallelRoot << parallelLog << true << true << 5; const Group sequentialRoot { Process(std::bind(setupProcess, _1, 1), readResult), Process(std::bind(setupProcess, _1, 2), readResult), Process(std::bind(setupProcess, _1, 3), readResult), Process(std::bind(setupProcess, _1, 4), readResult), Process(std::bind(setupProcess, _1, 5), readResult), OnGroupDone(rootDone) }; const Group sequentialEncapsulatedRoot { Group { Process(std::bind(setupProcess, _1, 1), readResult) }, Group { Process(std::bind(setupProcess, _1, 2), readResult) }, Group { Process(std::bind(setupProcess, _1, 3), readResult) }, Group { Process(std::bind(setupProcess, _1, 4), readResult) }, Group { Process(std::bind(setupProcess, _1, 5), readResult) }, OnGroupDone(rootDone) }; const Log sequentialLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Done}, {3, Handler::Setup}, {3, Handler::Done}, {4, Handler::Setup}, {4, Handler::Done}, {5, Handler::Setup}, {5, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("Sequential") << sequentialRoot << sequentialLog << true << true << 5; QTest::newRow("SequentialEncapsulated") << sequentialEncapsulatedRoot << sequentialLog << true << true << 5; const Group sequentialNestedRoot { Group { Process(std::bind(setupProcess, _1, 1), readResult), Group { Process(std::bind(setupProcess, _1, 2), readResult), Group { Process(std::bind(setupProcess, _1, 3), readResult), Group { Process(std::bind(setupProcess, _1, 4), readResult), Group { Process(std::bind(setupProcess, _1, 5), readResult), OnGroupDone(std::bind(groupDone, 5)) }, OnGroupDone(std::bind(groupDone, 4)) }, OnGroupDone(std::bind(groupDone, 3)) }, OnGroupDone(std::bind(groupDone, 2)) }, OnGroupDone(std::bind(groupDone, 1)) }, OnGroupDone(rootDone) }; const Log sequentialNestedLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Done}, {3, Handler::Setup}, {3, Handler::Done}, {4, Handler::Setup}, {4, Handler::Done}, {5, Handler::Setup}, {5, Handler::Done}, {5, Handler::GroupDone}, {4, Handler::GroupDone}, {3, Handler::GroupDone}, {2, Handler::GroupDone}, {1, Handler::GroupDone}, {-1, Handler::GroupDone}}; QTest::newRow("SequentialNested") << sequentialNestedRoot << sequentialNestedLog << true << true << 5; const Group sequentialErrorRoot { Process(std::bind(setupProcess, _1, 1), readResult), Process(std::bind(setupProcess, _1, 2), readResult), Process(std::bind(setupCrashProcess, _1, 3), readResult, readError), Process(std::bind(setupProcess, _1, 4), readResult), Process(std::bind(setupProcess, _1, 5), readResult), OnGroupDone(rootDone), OnGroupError(rootError) }; const Log sequentialErrorLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Done}, {3, Handler::Setup}, {3, Handler::Error}, {-1, Handler::GroupError}}; QTest::newRow("SequentialError") << sequentialErrorRoot << sequentialErrorLog << true << false << 5; const auto constructSimpleSequence = [=](const Workflow &policy) { return Group { policy, Process(std::bind(setupProcess, _1, 1), readResult), Process(std::bind(setupCrashProcess, _1, 2), readResult, readError), Process(std::bind(setupProcess, _1, 3), readResult), OnGroupDone(rootDone), OnGroupError(rootError) }; }; const Group stopOnErrorRoot = constructSimpleSequence(stopOnError); const Log stopOnErrorLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Error}, {-1, Handler::GroupError}}; QTest::newRow("StopOnError") << stopOnErrorRoot << stopOnErrorLog << true << false << 3; const Group continueOnErrorRoot = constructSimpleSequence(continueOnError); const Log continueOnErrorLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Error}, {3, Handler::Setup}, {3, Handler::Done}, {-1, Handler::GroupError}}; QTest::newRow("ContinueOnError") << continueOnErrorRoot << continueOnErrorLog << true << false << 3; const Group stopOnDoneRoot = constructSimpleSequence(stopOnDone); const Log stopOnDoneLog{{1, Handler::Setup}, {1, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("StopOnDone") << stopOnDoneRoot << stopOnDoneLog << true << true << 3; const Group continueOnDoneRoot = constructSimpleSequence(continueOnDone); const Log continueOnDoneLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Error}, {3, Handler::Setup}, {3, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("ContinueOnDone") << continueOnDoneRoot << continueOnDoneLog << true << true << 3; const Group optionalRoot { optional, Process(std::bind(setupCrashProcess, _1, 1), readResult, readError), Process(std::bind(setupCrashProcess, _1, 2), readResult, readError), OnGroupDone(rootDone), OnGroupError(rootError) }; const Log optionalLog{{1, Handler::Setup}, {1, Handler::Error}, {2, Handler::Setup}, {2, Handler::Error}, {-1, Handler::GroupDone}}; QTest::newRow("Optional") << optionalRoot << optionalLog << true << true << 2; const auto stopWithDoneSetup = [] { return GroupConfig{GroupAction::StopWithDone}; }; const auto stopWithErrorSetup = [] { return GroupConfig{GroupAction::StopWithError}; }; const auto continueAllSetup = [] { return GroupConfig{GroupAction::ContinueAll}; }; const auto continueSelSetup = [] { return GroupConfig{GroupAction::ContinueSelected, {0, 2}}; }; const auto constructDynamicSetup = [=](const DynamicSetup &dynamicSetup) { return Group { Group { Process(std::bind(setupProcess, _1, 1), readResult) }, Group { dynamicSetup, Process(std::bind(setupProcess, _1, 2), readResult), Process(std::bind(setupProcess, _1, 3), readResult), Process(std::bind(setupProcess, _1, 4), readResult) }, OnGroupDone(rootDone), OnGroupError(rootError) }; }; const Group dynamicSetupDoneRoot = constructDynamicSetup({stopWithDoneSetup}); const Log dynamicSetupDoneLog{{1, Handler::Setup}, {1, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("DynamicSetupDone") << dynamicSetupDoneRoot << dynamicSetupDoneLog << true << true << 4; const Group dynamicSetupErrorRoot = constructDynamicSetup({stopWithErrorSetup}); const Log dynamicSetupErrorLog{{1, Handler::Setup}, {1, Handler::Done}, {-1, Handler::GroupError}}; QTest::newRow("DynamicSetupError") << dynamicSetupErrorRoot << dynamicSetupErrorLog << true << false << 4; const Group dynamicSetupAllRoot = constructDynamicSetup({continueAllSetup}); const Log dynamicSetupAllLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Done}, {3, Handler::Setup}, {3, Handler::Done}, {4, Handler::Setup}, {4, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("DynamicSetupAll") << dynamicSetupAllRoot << dynamicSetupAllLog << true << true << 4; const Group dynamicSetupSelRoot = constructDynamicSetup({continueSelSetup}); const Log dynamicSetupSelLog{{1, Handler::Setup}, {1, Handler::Done}, {2, Handler::Setup}, {2, Handler::Done}, {4, Handler::Setup}, {4, Handler::Done}, {-1, Handler::GroupDone}}; QTest::newRow("DynamicSetupSelected") << dynamicSetupSelRoot << dynamicSetupSelLog << true << true << 4; } void tst_TaskTree::processTree() { m_log = {}; using namespace Tasking; QFETCH(Group, root); QFETCH(Log, expectedLog); QFETCH(bool, runningAfterStart); QFETCH(bool, success); QFETCH(int, taskCount); QEventLoop eventLoop; TaskTree processTree(root); QCOMPARE(processTree.taskCount(), taskCount); int doneCount = 0; int errorCount = 0; connect(&processTree, &TaskTree::done, this, [&doneCount, &eventLoop] { ++doneCount; eventLoop.quit(); }); connect(&processTree, &TaskTree::errorOccurred, this, [&errorCount, &eventLoop] { ++errorCount; eventLoop.quit(); }); processTree.start(); QCOMPARE(processTree.isRunning(), runningAfterStart); QTimer timer; connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit); timer.setInterval(1000); timer.setSingleShot(true); timer.start(); eventLoop.exec(); QVERIFY(!processTree.isRunning()); QCOMPARE(processTree.progressValue(), taskCount); QCOMPARE(m_log, expectedLog); const int expectedDoneCount = success ? 1 : 0; const int expectedErrorCount = success ? 0 : 1; QCOMPARE(doneCount, expectedDoneCount); QCOMPARE(errorCount, expectedErrorCount); } void tst_TaskTree::storage_data() { using namespace Tasking; using namespace std::placeholders; QTest::addColumn("root"); QTest::addColumn>("storageLog"); QTest::addColumn("expectedLog"); QTest::addColumn("runningAfterStart"); QTest::addColumn("success"); // TODO: Much better approach would be that the TaskTree, when started, creates a // storage dynamically (then Group should be a template with storage type as a // template parameter) and a pointer (or reference) to the storage is being passed // into handlers (? how ?). std::shared_ptr log(new Log); const auto setupProcessHelper = [this, log](QtcProcess &process, const QStringList &args, int processId) { process.setCommand(CommandLine(m_testAppPath, args)); process.setProperty(s_processIdProperty, processId); log->append({processId, Handler::Setup}); }; const auto setupProcess = [setupProcessHelper](QtcProcess &process, int processId) { setupProcessHelper(process, {"-return", "0"}, processId); }; const auto readResult = [log](const QtcProcess &process) { const int processId = process.property(s_processIdProperty).toInt(); log->append({processId, Handler::Done}); }; const auto groupSetup = [log](int processId) { log->append({processId, Handler::GroupSetup}); }; const auto groupDone = [log](int processId) { log->append({processId, Handler::GroupDone}); }; const auto rootDone = [log] { log->append({-1, Handler::GroupDone}); }; const Group nestedRoot { Group { Group { Group { Group { Group { Process(std::bind(setupProcess, _1, 5), readResult), OnGroupSetup(std::bind(groupSetup, 5)), OnGroupDone(std::bind(groupDone, 5)) }, OnGroupSetup(std::bind(groupSetup, 4)), OnGroupDone(std::bind(groupDone, 4)) }, OnGroupSetup(std::bind(groupSetup, 3)), OnGroupDone(std::bind(groupDone, 3)) }, OnGroupSetup(std::bind(groupSetup, 2)), OnGroupDone(std::bind(groupDone, 2)) }, OnGroupSetup(std::bind(groupSetup, 1)), OnGroupDone(std::bind(groupDone, 1)) }, OnGroupDone(rootDone) }; const Log nestedLog{{1, Handler::GroupSetup}, {2, Handler::GroupSetup}, {3, Handler::GroupSetup}, {4, Handler::GroupSetup}, {5, Handler::GroupSetup}, {5, Handler::Setup}, {5, Handler::Done}, {5, Handler::GroupDone}, {4, Handler::GroupDone}, {3, Handler::GroupDone}, {2, Handler::GroupDone}, {1, Handler::GroupDone}, {-1, Handler::GroupDone}}; QTest::newRow("Nested") << nestedRoot << log << nestedLog << true << true; } void tst_TaskTree::storage() { using namespace Tasking; QFETCH(Group, root); QFETCH(std::shared_ptr, storageLog); QFETCH(Log, expectedLog); QFETCH(bool, runningAfterStart); QFETCH(bool, success); QEventLoop eventLoop; TaskTree processTree(root); int doneCount = 0; int errorCount = 0; connect(&processTree, &TaskTree::done, this, [&doneCount, &eventLoop] { ++doneCount; eventLoop.quit(); }); connect(&processTree, &TaskTree::errorOccurred, this, [&errorCount, &eventLoop] { ++errorCount; eventLoop.quit(); }); processTree.start(); QCOMPARE(processTree.isRunning(), runningAfterStart); QTimer timer; connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit); timer.setInterval(1000); timer.setSingleShot(true); timer.start(); eventLoop.exec(); QVERIFY(!processTree.isRunning()); QCOMPARE(*storageLog, expectedLog); const int expectedDoneCount = success ? 1 : 0; const int expectedErrorCount = success ? 0 : 1; QCOMPARE(doneCount, expectedDoneCount); QCOMPARE(errorCount, expectedErrorCount); } QTEST_GUILESS_MAIN(tst_TaskTree) #include "tst_tasktree.moc"