diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index a4135401651..362a0b0eebe 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -158,6 +158,7 @@ add_qtc_library(Utils stringutils.cpp stringutils.h styledbar.cpp styledbar.h stylehelper.cpp stylehelper.h + tasktree.cpp tasktree.h templateengine.cpp templateengine.h temporarydirectory.cpp temporarydirectory.h temporaryfile.cpp temporaryfile.h diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index 285ba25eb42..2f629d3f24f 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -2091,6 +2091,18 @@ void QtcProcessPrivate::storeEventLoopDebugInfo(const QVariant &value) setProperty(QTC_PROCESS_BLOCKING_TYPE, value); } +QtcProcessAdapter::QtcProcessAdapter() +{ + connect(task(), &QtcProcess::done, this, [this] { + emit done(task()->result() == ProcessResult::FinishedWithSuccess); + }); +} + +void QtcProcessAdapter::start() +{ + task()->start(); +} + } // namespace Utils #include "qtcprocess.moc" diff --git a/src/libs/utils/qtcprocess.h b/src/libs/utils/qtcprocess.h index 966288efa4e..bb642b6fcaf 100644 --- a/src/libs/utils/qtcprocess.h +++ b/src/libs/utils/qtcprocess.h @@ -7,6 +7,7 @@ #include "commandline.h" #include "processenums.h" +#include "tasktree.h" #include @@ -202,4 +203,13 @@ public: std::function systemEnvironmentForBinary; }; +class QTCREATOR_UTILS_EXPORT QtcProcessAdapter : public Tasking::TaskAdapter +{ +public: + QtcProcessAdapter(); + void start() final; +}; + } // namespace Utils + +QTC_DECLARE_CUSTOM_TASK(Process, Utils::QtcProcessAdapter); diff --git a/src/libs/utils/tasktree.cpp b/src/libs/utils/tasktree.cpp new file mode 100644 index 00000000000..a7051cf78e4 --- /dev/null +++ b/src/libs/utils/tasktree.cpp @@ -0,0 +1,382 @@ +// 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 "tasktree.h" + +#include "guard.h" +#include "qtcassert.h" + +namespace Utils { +namespace Tasking { + +ExecuteInSequence sequential; +ExecuteInParallel parallel; +WorkflowPolicy stopOnError(TaskItem::WorkflowPolicy::StopOnError); +WorkflowPolicy continueOnError(TaskItem::WorkflowPolicy::ContinueOnError); +WorkflowPolicy stopOnDone(TaskItem::WorkflowPolicy::StopOnDone); +WorkflowPolicy continueOnDone(TaskItem::WorkflowPolicy::ContinueOnDone); +WorkflowPolicy optional(TaskItem::WorkflowPolicy::Optional); + +void TaskItem::addChildren(const QList &children) +{ + QTC_ASSERT(m_type == Type::Group, qWarning("Only Task may have children, skipping..."); return); + for (const TaskItem &child : children) { + switch (child.m_type) { + case Type::Group: + m_children.append(child); + break; + case Type::Mode: + QTC_ASSERT(m_type == Type::Group, + qWarning("Mode may only be a child of Group, skipping..."); return); + m_executeMode = child.m_executeMode; // TODO: Assert on redefinition? + break; + case Type::Policy: + QTC_ASSERT(m_type == Type::Group, + qWarning("Workflow Policy may only be a child of Group, skipping..."); return); + m_workflowPolicy = child.m_workflowPolicy; // TODO: Assert on redefinition? + break; + case Type::TaskHandler: + QTC_ASSERT(child.m_taskHandler.m_createHandler, + qWarning("Task Create Handler can't be null, skipping..."); return); + QTC_ASSERT(child.m_taskHandler.m_setupHandler, + qWarning("Task Setup Handler can't be null, skipping..."); return); + m_children.append(child); + break; + case Type::GroupHandler: + QTC_ASSERT(m_type == Type::Group, qWarning("Group Handler may only be a " + "child of Group, skipping..."); break); + QTC_ASSERT(!child.m_groupHandler.m_simpleSetupHandler + || !m_groupHandler.m_simpleSetupHandler, + qWarning("Group Setup Handler redefinition, overriding...")); + QTC_ASSERT(!child.m_groupHandler.m_simpleDoneHandler + || !m_groupHandler.m_simpleDoneHandler, + qWarning("Group Done Handler redefinition, overriding...")); + QTC_ASSERT(!child.m_groupHandler.m_simpleErrorHandler + || !m_groupHandler.m_simpleErrorHandler, + qWarning("Group Error Handler redefinition, overriding...")); + if (child.m_groupHandler.m_simpleSetupHandler) + m_groupHandler.m_simpleSetupHandler = child.m_groupHandler.m_simpleSetupHandler; + if (child.m_groupHandler.m_simpleDoneHandler) + m_groupHandler.m_simpleDoneHandler = child.m_groupHandler.m_simpleDoneHandler; + if (child.m_groupHandler.m_simpleErrorHandler) + m_groupHandler.m_simpleErrorHandler = child.m_groupHandler.m_simpleErrorHandler; + break; + } + } +} + +} // namespace Tasking + +using namespace Tasking; + +class TaskTreePrivate; +class TaskNode; + +class TaskContainer +{ +public: + TaskContainer(TaskTreePrivate *taskTreePrivate, TaskContainer *parentContainer, + const TaskItem &task); + ~TaskContainer(); + void start(); + void stop(); + bool isRunning() const; + void childDone(bool success); + void invokeSubTreeHandler(bool success); + void resetSuccessBit(); + void updateSuccessBit(bool success); + + TaskTreePrivate *m_taskTreePrivate = nullptr; + TaskContainer *m_parentContainer = nullptr; + const TaskItem::ExecuteMode m_executeMode = TaskItem::ExecuteMode::Parallel; + TaskItem::WorkflowPolicy m_workflowPolicy = TaskItem::WorkflowPolicy::StopOnError; + const TaskItem::GroupHandler m_groupHandler; + QList m_children; + int m_currentIndex = -1; + bool m_successBit = true; +}; + +class TaskNode : public QObject +{ +public: + TaskNode(TaskTreePrivate *taskTreePrivate, TaskContainer *parentContainer, + const TaskItem &task) + : m_taskHandler(task.taskHandler()) + , m_container(taskTreePrivate, parentContainer, task) + { + } + + bool start(); + void stop(); + bool isRunning(); + +private: + const TaskItem::TaskHandler m_taskHandler; + TaskContainer m_container; + std::unique_ptr m_task; +}; + +class TaskTreePrivate +{ +public: + TaskTreePrivate(TaskTree *taskTree, const Group &root) + : q(taskTree) + , m_root(this, nullptr, root) {} + + void emitDone() { + GuardLocker locker(m_guard); + emit q->done(); + } + void emitError() { + GuardLocker locker(m_guard); + emit q->errorOccurred(); + } + + TaskTree *q = nullptr; + TaskNode m_root; + Guard m_guard; +}; + +TaskContainer::TaskContainer(TaskTreePrivate *taskTreePrivate, TaskContainer *parentContainer, + const TaskItem &task) + : m_taskTreePrivate(taskTreePrivate) + , m_parentContainer(parentContainer) + , m_executeMode(task.executeMode()) + , m_workflowPolicy(task.workflowPolicy()) + , m_groupHandler(task.groupHandler()) +{ + const QList &children = task.children(); + for (const TaskItem &child : children) + m_children.append(new TaskNode(m_taskTreePrivate, this, child)); +} + +TaskContainer::~TaskContainer() +{ + qDeleteAll(m_children); +} + +void TaskContainer::start() +{ + if (m_groupHandler.m_simpleSetupHandler) { + GuardLocker locker(m_taskTreePrivate->m_guard); + m_groupHandler.m_simpleSetupHandler(); + } + + if (m_children.isEmpty()) { + invokeSubTreeHandler(true); + return; + } + + m_currentIndex = 0; + resetSuccessBit(); + + if (m_executeMode == TaskItem::ExecuteMode::Sequential) { + m_children.at(m_currentIndex)->start(); + return; + } + + // Parallel case + for (TaskNode *child : std::as_const(m_children)) { + if (!child->start()) + return; + } +} + +void TaskContainer::stop() +{ + m_currentIndex = -1; + for (TaskNode *child : std::as_const(m_children)) + child->stop(); +} + +bool TaskContainer::isRunning() const +{ + return m_currentIndex >= 0; +} + +void TaskContainer::childDone(bool success) +{ + if ((m_workflowPolicy == TaskItem::WorkflowPolicy::StopOnDone && success) + || (m_workflowPolicy == TaskItem::WorkflowPolicy::StopOnError && !success)) { + stop(); + invokeSubTreeHandler(success); + return; + } + + ++m_currentIndex; + updateSuccessBit(success); + + if (m_currentIndex == m_children.size()) { + invokeSubTreeHandler(m_successBit); + return; + } + + if (m_executeMode == TaskItem::ExecuteMode::Sequential) + m_children.at(m_currentIndex)->start(); +} + +void TaskContainer::invokeSubTreeHandler(bool success) +{ + m_currentIndex = -1; + m_successBit = success; + if (success && m_groupHandler.m_simpleDoneHandler) { + GuardLocker locker(m_taskTreePrivate->m_guard); + m_groupHandler.m_simpleDoneHandler(); + } else if (!success && m_groupHandler.m_simpleErrorHandler) { + GuardLocker locker(m_taskTreePrivate->m_guard); + m_groupHandler.m_simpleErrorHandler(); + } + if (m_parentContainer) { + m_parentContainer->childDone(success); + return; + } + if (success) + m_taskTreePrivate->emitDone(); + else + m_taskTreePrivate->emitError(); +} + +void TaskContainer::resetSuccessBit() +{ + if (m_children.isEmpty()) + m_successBit = true; + + if (m_workflowPolicy == TaskItem::WorkflowPolicy::StopOnDone + || m_workflowPolicy == TaskItem::WorkflowPolicy::ContinueOnDone) { + m_successBit = false; + } else { + m_successBit = true; + } +} + +void TaskContainer::updateSuccessBit(bool success) +{ + if (m_workflowPolicy == TaskItem::WorkflowPolicy::Optional) + return; + if (m_workflowPolicy == TaskItem::WorkflowPolicy::StopOnDone + || m_workflowPolicy == TaskItem::WorkflowPolicy::ContinueOnDone) { + m_successBit = m_successBit || success; + } else { + m_successBit = m_successBit && success; + } +} + + +bool TaskNode::start() +{ + if (!m_taskHandler.m_createHandler || !m_taskHandler.m_setupHandler) { + m_container.start(); + return true; + } + m_task.reset(m_taskHandler.m_createHandler()); + { + GuardLocker locker(m_container.m_taskTreePrivate->m_guard); + m_taskHandler.m_setupHandler(*m_task.get()); + } + connect(m_task.get(), &TaskInterface::done, this, [this](bool success) { + if (success && m_taskHandler.m_doneHandler) { + GuardLocker locker(m_container.m_taskTreePrivate->m_guard); + m_taskHandler.m_doneHandler(*m_task.get()); + } else if (!success && m_taskHandler.m_errorHandler) { + GuardLocker locker(m_container.m_taskTreePrivate->m_guard); + m_taskHandler.m_errorHandler(*m_task.get()); + } + + m_task.release()->deleteLater(); + + QTC_CHECK(m_container.m_parentContainer); + m_container.m_parentContainer->childDone(success); + }); + + m_task->start(); + return m_task.get(); // In case of failed to start, done handler already released process +} + +void TaskNode::stop() +{ + m_task.reset(); + m_container.stop(); +} + +bool TaskNode::isRunning() +{ + return m_task || m_container.isRunning(); +} + +/*! + \class Utils::TaskTree + + \brief The TaskTree class is responsible for running async task tree structure defined in a + declarative way. + + The Tasking namespace (similar to Layouting) is designer for building declarative task + tree structure. The examples of tasks that can be used inside TaskTree are e.g. QtcProcess, + FileTransfer, AsyncTask<>. It's extensible, so any possible asynchronous task may be + integrated and used inside TaskTree. TaskTree enables to form sophisticated mixtures of + parallel or sequential flow of tasks in tree form. + + The TaskTree consist of Group root element. The Group can have nested Group elements. + The Group may also contain any number of tasks, e.g. Process, FileTransfer, + AsyncTask. + + Each Group can contain various other elements describing the processing flow. + + The execute mode elements of a Group specify how direct children of a Group will be executed. + The "sequential" element of a Group means all tasks in a group will be executed in chain, + so after the previous task finished, the next will be started. This is the default Group + behavior. The "parallel" element of a Group means that all tasks in a Group will be started + simultaneously. When having nested Groups hierarchy, we may mix execute modes freely + and each Group will be executed according to its own execute mode. + The "sequential" mode may be very useful in cases when result data from one task is need to + be passed as an input data to the other task - sequential mode guarantees that the next + task will be started only after the previous task has already finished. + + There are many possible "workflow" behaviors for the Group. E.g. "stopOnError", + the default Group workflow behavior, means that whenever any direct child of a Group + finished with error, we immediately stop processing other tasks in this group + (in parallel case) by canceling them and immediately finish the Group with error. + + The user of TaskTree specifies how to setup his tasks (by providing TaskSetupHandlers) + and how to collect output data from the finished tasks (by providing TaskEndHandlers). + The user don't need to create tasks manually - TaskTree will create them when it's needed + and destroy when they are not used anymore. + + Whenever a Group elemenent is being started, the Group's OnGroupSetup handler is being called. + Just after the handler finishes, all Group's children are executed (either in parallel or + in sequence). When all Group's children finished, one of Group's OnGroupDone or OnGroupError + is being executed, depending on results of children execution and Group's workflow policy. +*/ + +TaskTree::TaskTree(const Group &root) + : d(new TaskTreePrivate(this, root)) +{ +} + +TaskTree::~TaskTree() +{ + QTC_ASSERT(!d->m_guard.isLocked(), qWarning("Deleting TaskTree instance directly from " + "one of its handlers will lead to crash!")); + delete d; +} + +void TaskTree::start() +{ + QTC_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); + QTC_ASSERT(!d->m_guard.isLocked(), qWarning("The start() is called from one of the" + "TaskTree handlers, ingoring..."); return); + d->m_root.start(); +} + +void TaskTree::stop() +{ + QTC_ASSERT(!d->m_guard.isLocked(), qWarning("The stop() is called from one of the" + "TaskTree handlers, ingoring..."); return); + d->m_root.stop(); +} + +bool TaskTree::isRunning() const +{ + return d->m_root.isRunning(); +} + +} // namespace Utils diff --git a/src/libs/utils/tasktree.h b/src/libs/utils/tasktree.h new file mode 100644 index 00000000000..5ead17ec2da --- /dev/null +++ b/src/libs/utils/tasktree.h @@ -0,0 +1,239 @@ +// 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 + +#pragma once + +#include "utils_global.h" + +#include + +namespace Utils { +namespace Tasking { + +class QTCREATOR_UTILS_EXPORT TaskInterface : public QObject +{ + Q_OBJECT + +public: + TaskInterface() = default; + virtual void start() = 0; + +signals: + void done(bool success); +}; + +class QTCREATOR_UTILS_EXPORT TaskItem +{ +public: + // Internal, provided by QTC_DECLARE_CUSTOM_TASK + using TaskCreateHandler = std::function; + // Called prior to task start, just after createHandler + using TaskSetupHandler = std::function; + // Called on task done / error + using TaskEndHandler = std::function; + // Called when sub tree entered / after sub three ended with success or failure + using GroupSimpleHandler = std::function; + + struct TaskHandler { + TaskCreateHandler m_createHandler; + TaskSetupHandler m_setupHandler; + TaskEndHandler m_doneHandler; + TaskEndHandler m_errorHandler; + }; + + struct GroupHandler { + GroupSimpleHandler m_simpleSetupHandler; + GroupSimpleHandler m_simpleDoneHandler; + GroupSimpleHandler m_simpleErrorHandler; + }; + + enum class ExecuteMode { + Parallel, // default + Sequential + }; + + // 4 policies: + // 1. When all children finished with done -> report done, otherwise: + // a) Report error on first error and stop executing other children (including their subtree) + // b) On first error - wait for all children to be finished and report error afterwards + // 2. When all children finished with error -> report error, otherwise: + // a) Report done on first done and stop executing other children (including their subtree) + // b) On first done - wait for all children to be finished and report done afterwards + + enum class WorkflowPolicy { + StopOnError, // 1a - Will report error on any child error, otherwise done (if all children were done) + ContinueOnError, // 1b - the same. When no children it reports done. + StopOnDone, // 2a - Will report done on any child done, otherwise error (if all children were error) + ContinueOnDone, // 2b - the same. When no children it reports done. (?) + Optional // Returns always done after all children finished + }; + + ExecuteMode executeMode() const { return m_executeMode; } + WorkflowPolicy workflowPolicy() const { return m_workflowPolicy; } + TaskHandler taskHandler() const { return m_taskHandler; } + GroupHandler groupHandler() const { return m_groupHandler; } + QList children() const { return m_children; } + +protected: + enum class Type { + Group, + Mode, + Policy, + TaskHandler, + GroupHandler + // TODO: Add Cond type (with CondHandler and True and False branches)? + }; + + TaskItem() = default; + TaskItem(ExecuteMode mode) + : m_type(Type::Mode) + , m_executeMode(mode) {} + TaskItem(WorkflowPolicy policy) + : m_type(Type::Policy) + , m_workflowPolicy(policy) {} + TaskItem(const TaskHandler &handler) + : m_type(Type::TaskHandler) + , m_taskHandler(handler) {} + TaskItem(const GroupHandler &handler) + : m_type(Type::GroupHandler) + , m_groupHandler(handler) {} + void addChildren(const QList &children); + +private: + Type m_type = Type::Group; + ExecuteMode m_executeMode = ExecuteMode::Sequential; + WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError; + TaskHandler m_taskHandler; + GroupHandler m_groupHandler; + QList m_children; +}; + +class QTCREATOR_UTILS_EXPORT Group : public TaskItem +{ +public: + Group(const QList &children) { addChildren(children); } + Group(std::initializer_list children) { addChildren(children); } +}; + +class QTCREATOR_UTILS_EXPORT ExecuteInSequence : public TaskItem +{ +public: + ExecuteInSequence() : TaskItem(ExecuteMode::Sequential) {} +}; + +class QTCREATOR_UTILS_EXPORT ExecuteInParallel : public TaskItem +{ +public: + ExecuteInParallel() : TaskItem(ExecuteMode::Parallel) {} +}; + +class QTCREATOR_UTILS_EXPORT WorkflowPolicy : public TaskItem +{ +public: + WorkflowPolicy(TaskItem::WorkflowPolicy policy) : TaskItem(policy) {} +}; + +class QTCREATOR_UTILS_EXPORT OnGroupSetup : public TaskItem +{ +public: + OnGroupSetup(const GroupSimpleHandler &handler) : TaskItem({{handler}, {}, {}}) {} +}; + +class QTCREATOR_UTILS_EXPORT OnGroupDone : public TaskItem +{ +public: + OnGroupDone(const GroupSimpleHandler &handler) : TaskItem({{}, handler, {}}) {} +}; + +class QTCREATOR_UTILS_EXPORT OnGroupError : public TaskItem +{ +public: + OnGroupError(const GroupSimpleHandler &handler) : TaskItem({{}, {}, handler}) {} +}; + +QTCREATOR_UTILS_EXPORT extern ExecuteInSequence sequential; +QTCREATOR_UTILS_EXPORT extern ExecuteInParallel parallel; +QTCREATOR_UTILS_EXPORT extern WorkflowPolicy stopOnError; +QTCREATOR_UTILS_EXPORT extern WorkflowPolicy continueOnError; +QTCREATOR_UTILS_EXPORT extern WorkflowPolicy stopOnDone; +QTCREATOR_UTILS_EXPORT extern WorkflowPolicy continueOnDone; +QTCREATOR_UTILS_EXPORT extern WorkflowPolicy optional; + +template +class TaskAdapter : public TaskInterface +{ +public: + using Type = Task; + TaskAdapter() = default; + Task *task() { return &m_task; } + const Task *task() const { return &m_task; } +private: + Task m_task; +}; + +template +class CustomTask : public TaskItem +{ +public: + using Task = typename Adapter::Type; + using SetupHandler = std::function; + using EndHandler = std::function; + static Adapter *createAdapter() { return new Adapter; } + CustomTask(const SetupHandler &setup, const EndHandler &done = {}, const EndHandler &error = {}) + : TaskItem({&createAdapter, wrapSetup(setup), + wrapEnd(done), wrapEnd(error)}) {} + +private: + static TaskSetupHandler wrapSetup(SetupHandler handler) { + if (!handler) + return {}; + return [handler](TaskInterface &taskInterface) { + Adapter &adapter = static_cast(taskInterface); + handler(*adapter.task()); + }; + }; + static TaskEndHandler wrapEnd(EndHandler handler) { + if (!handler) + return {}; + return [handler](const TaskInterface &taskInterface) { + const Adapter &adapter = static_cast(taskInterface); + handler(*adapter.task()); + }; + }; +}; + +} // namespace Tasking + +class TaskTreePrivate; + +class QTCREATOR_UTILS_EXPORT TaskTree : public QObject +{ + Q_OBJECT + +public: + TaskTree(const Tasking::Group &root); + ~TaskTree(); + + void start(); + void stop(); + bool isRunning() const; + +signals: + void done(); + void errorOccurred(); + +private: + TaskTreePrivate *d; +}; + +} // namespace Utils + +#define QTC_DECLARE_CUSTOM_TASK(CustomTaskName, TaskAdapterClass)\ +namespace Utils::Tasking { using CustomTaskName = CustomTask; } + +#define QTC_DECLARE_CUSTOM_TEMPLATE_TASK(CustomTaskName, TaskAdapterClass)\ +namespace Utils::Tasking {\ +template \ +using CustomTaskName = CustomTask>;\ +} // namespace Utils::Tasking + diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index 4cc71591c35..79d95617277 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -292,6 +292,8 @@ Project { "styledbar.h", "stylehelper.cpp", "stylehelper.h", + "tasktree.cpp", + "tasktree.h", "templateengine.cpp", "templateengine.h", "temporarydirectory.cpp", diff --git a/tests/auto/utils/CMakeLists.txt b/tests/auto/utils/CMakeLists.txt index b8be57696bb..2c30269d86d 100644 --- a/tests/auto/utils/CMakeLists.txt +++ b/tests/auto/utils/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(qtcprocess) add_subdirectory(settings) add_subdirectory(stringutils) add_subdirectory(templateengine) +add_subdirectory(tasktree) add_subdirectory(treemodel) add_subdirectory(multicursor) add_subdirectory(deviceshell) diff --git a/tests/auto/utils/tasktree/CMakeLists.txt b/tests/auto/utils/tasktree/CMakeLists.txt new file mode 100644 index 00000000000..87c31f18866 --- /dev/null +++ b/tests/auto/utils/tasktree/CMakeLists.txt @@ -0,0 +1,11 @@ +add_subdirectory(testapp) + +file(RELATIVE_PATH RELATIVE_TEST_PATH "${PROJECT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") +file(RELATIVE_PATH TEST_RELATIVE_LIBEXEC_PATH "/${RELATIVE_TEST_PATH}" "/${IDE_LIBEXEC_PATH}") + +add_qtc_test(tst_utils_tasktree + DEFINES "TEST_RELATIVE_LIBEXEC_PATH=\"${TEST_RELATIVE_LIBEXEC_PATH}\"" + "TESTAPP_PATH=\"${CMAKE_CURRENT_BINARY_DIR}/testapp\"" + DEPENDS Utils app_version + SOURCES tst_tasktree.cpp +) diff --git a/tests/auto/utils/tasktree/tasktree.qbs b/tests/auto/utils/tasktree/tasktree.qbs new file mode 100644 index 00000000000..ed138d9f393 --- /dev/null +++ b/tests/auto/utils/tasktree/tasktree.qbs @@ -0,0 +1,27 @@ +import qbs.FileInfo + +Project { + QtcAutotest { + name: "TaskTree autotest" + + Depends { name: "Utils" } + Depends { name: "app_version_header" } + + files: [ + "tst_tasktree.cpp", + ] + cpp.defines: { + var defines = base; + if (qbs.targetOS === "windows") + defines.push("_CRT_SECURE_NO_WARNINGS"); + var absLibExecPath = FileInfo.joinPaths(qbs.installRoot, qbs.installPrefix, + qtc.ide_libexec_path); + var relLibExecPath = FileInfo.relativePath(destinationDirectory, absLibExecPath); + defines.push('TEST_RELATIVE_LIBEXEC_PATH="' + relLibExecPath + '"'); + defines.push('TESTAPP_PATH="' + + FileInfo.joinPaths(destinationDirectory, "testapp") + '"'); + return defines; + } + } + references: "testapp/testapp.qbs" +} diff --git a/tests/auto/utils/tasktree/testapp/CMakeLists.txt b/tests/auto/utils/tasktree/testapp/CMakeLists.txt new file mode 100644 index 00000000000..712164e5663 --- /dev/null +++ b/tests/auto/utils/tasktree/testapp/CMakeLists.txt @@ -0,0 +1,12 @@ +add_qtc_executable(testapp + DEPENDS Utils + SOURCES main.cpp + SKIP_INSTALL + INTERNAL_ONLY +) + +set_target_properties(testapp PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" +) diff --git a/tests/auto/utils/tasktree/testapp/main.cpp b/tests/auto/utils/tasktree/testapp/main.cpp new file mode 100644 index 00000000000..dc308cca292 --- /dev/null +++ b/tests/auto/utils/tasktree/testapp/main.cpp @@ -0,0 +1,45 @@ +// 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 + +#ifdef Q_OS_WIN +#include +#include +#endif + +const char CRASH_OPTION[] = "-crash"; +const char RETURN_OPTION[] = "-return"; + +int main(int argc, char **argv) +{ +#ifdef Q_OS_WIN + // avoid crash reporter dialog + _set_error_mode(_OUT_TO_STDERR); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG); +#endif + + if (argc > 1) { + const auto arg = QString::fromLocal8Bit(argv[1]); + if (arg == CRASH_OPTION) { + qFatal("The application has crashed purposefully!"); + return 1; + } + if (arg == RETURN_OPTION) { + if (argc > 2) { + const auto retString = QString::fromLocal8Bit(argv[2]); + bool ok = false; + const int retVal = retString.toInt(&ok); + if (ok) + return retVal; + // not an int return value + return 1; + } + // lacking return value + return 1; + } + } + // not recognized option + return 1; +} diff --git a/tests/auto/utils/tasktree/testapp/testapp.qbs b/tests/auto/utils/tasktree/testapp/testapp.qbs new file mode 100644 index 00000000000..34ca870c869 --- /dev/null +++ b/tests/auto/utils/tasktree/testapp/testapp.qbs @@ -0,0 +1,19 @@ +import qbs.FileInfo + +QtApplication { + name: "testapp" + Depends { name: "qtc" } + Depends { name: "Utils" } + + consoleApplication: true + cpp.cxxLanguageVersion: "c++17" + cpp.rpaths: project.buildDirectory + '/' + qtc.ide_library_path + + install: false + destinationDirectory: project.buildDirectory + '/' + + FileInfo.relativePath(project.ide_source_tree, sourceDirectory) + + files: [ + "main.cpp", + ] +} diff --git a/tests/auto/utils/tasktree/tst_tasktree.cpp b/tests/auto/utils/tasktree/tst_tasktree.cpp new file mode 100644 index 00000000000..53854ece041 --- /dev/null +++ b/tests/auto/utils/tasktree/tst_tasktree.cpp @@ -0,0 +1,542 @@ +// 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"); + + 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}}; + + 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}}; + + 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 + + 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}}; + + 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}}; + + 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}}; + + const QList simpleSequence { + 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 auto constructSimpleSequence = [=](const WorkflowPolicy &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}}; + + 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}}; + + const Group stopOnDoneRoot = constructSimpleSequence(stopOnDone); + const Log stopOnDoneLog{{1, Handler::Setup}, + {1, Handler::Done}, + {-1, Handler::GroupDone}}; + + 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}}; + + 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("Empty") << emptyRoot << emptyLog << false << true; + QTest::newRow("Nested") << nestedRoot << nestedLog << true << true; + QTest::newRow("Parallel") << parallelRoot << parallelLog << true << true; + QTest::newRow("Sequential") << sequentialRoot << sequentialLog << true << true; + QTest::newRow("SequentialEncapsulated") << sequentialEncapsulatedRoot << sequentialLog << true << true; + QTest::newRow("SequentialNested") << sequentialNestedRoot << sequentialNestedLog << true << true; + QTest::newRow("SequentialError") << sequentialErrorRoot << sequentialErrorLog << true << false; + QTest::newRow("StopOnError") << stopOnErrorRoot << stopOnErrorLog << true << false; + QTest::newRow("ContinueOnError") << continueOnErrorRoot << continueOnErrorLog << true << false; + QTest::newRow("StopOnDone") << stopOnDoneRoot << stopOnDoneLog << true << true; + QTest::newRow("ContinueOnDone") << continueOnDoneRoot << continueOnDoneLog << true << true; + QTest::newRow("Optional") << optionalRoot << optionalLog << true << true; +} + +void tst_TaskTree::processTree() +{ + m_log = {}; + using namespace Tasking; + + QFETCH(Group, root); + 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(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" diff --git a/tests/auto/utils/utils.qbs b/tests/auto/utils/utils.qbs index 363d0203971..e4cf4fdec04 100644 --- a/tests/auto/utils/utils.qbs +++ b/tests/auto/utils/utils.qbs @@ -12,6 +12,7 @@ Project { "qtcprocess/qtcprocess.qbs", "settings/settings.qbs", "stringutils/stringutils.qbs", + "tasktree/tasktree.qbs", "templateengine/templateengine.qbs", "treemodel/treemodel.qbs", "multicursor/multicursor.qbs",