Utils: Introduce TaskTree and Tasking namespace

The TaskTree class is responsible for running async task tree
structure defined in a declarative way.

Change-Id: Ieaf706c7d2efdc8b431a17b2db8b28bf4b7c38e5
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Jarek Kobus
2022-10-12 14:30:24 +02:00
parent b8d68b6f65
commit c49de14c9d
14 changed files with 1304 additions and 0 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -7,6 +7,7 @@
#include "commandline.h"
#include "processenums.h"
#include "tasktree.h"
#include <QProcess>
@@ -202,4 +203,13 @@ public:
std::function<Environment(const FilePath &)> systemEnvironmentForBinary;
};
class QTCREATOR_UTILS_EXPORT QtcProcessAdapter : public Tasking::TaskAdapter<QtcProcess>
{
public:
QtcProcessAdapter();
void start() final;
};
} // namespace Utils
QTC_DECLARE_CUSTOM_TASK(Process, Utils::QtcProcessAdapter);

382
src/libs/utils/tasktree.cpp Normal file
View File

@@ -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<TaskItem> &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<TaskNode *> 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<TaskInterface> 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<TaskItem> &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<ReturnType>.
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

239
src/libs/utils/tasktree.h Normal file
View File

@@ -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 <QObject>
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<TaskInterface *(void)>;
// Called prior to task start, just after createHandler
using TaskSetupHandler = std::function<void(TaskInterface &)>;
// Called on task done / error
using TaskEndHandler = std::function<void(const TaskInterface &)>;
// Called when sub tree entered / after sub three ended with success or failure
using GroupSimpleHandler = std::function<void()>;
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<TaskItem> 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<TaskItem> &children);
private:
Type m_type = Type::Group;
ExecuteMode m_executeMode = ExecuteMode::Sequential;
WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError;
TaskHandler m_taskHandler;
GroupHandler m_groupHandler;
QList<TaskItem> m_children;
};
class QTCREATOR_UTILS_EXPORT Group : public TaskItem
{
public:
Group(const QList<TaskItem> &children) { addChildren(children); }
Group(std::initializer_list<TaskItem> 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 <typename Task>
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 <typename Adapter>
class CustomTask : public TaskItem
{
public:
using Task = typename Adapter::Type;
using SetupHandler = std::function<void(Task &)>;
using EndHandler = std::function<void(const Task &)>;
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<Adapter &>(taskInterface);
handler(*adapter.task());
};
};
static TaskEndHandler wrapEnd(EndHandler handler) {
if (!handler)
return {};
return [handler](const TaskInterface &taskInterface) {
const Adapter &adapter = static_cast<const Adapter &>(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<TaskAdapterClass>; }
#define QTC_DECLARE_CUSTOM_TEMPLATE_TASK(CustomTaskName, TaskAdapterClass)\
namespace Utils::Tasking {\
template <typename ...Args>\
using CustomTaskName = CustomTask<TaskAdapterClass<Args...>>;\
} // namespace Utils::Tasking

View File

@@ -292,6 +292,8 @@ Project {
"styledbar.h",
"stylehelper.cpp",
"stylehelper.h",
"tasktree.cpp",
"tasktree.h",
"templateengine.cpp",
"templateengine.h",
"temporarydirectory.cpp",

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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"
}

View File

@@ -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}"
)

View File

@@ -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 <app/app_version.h>
#include <QString>
#ifdef Q_OS_WIN
#include <crtdbg.h>
#include <cstdlib>
#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;
}

View File

@@ -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",
]
}

View File

@@ -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 <app/app_version.h>
#include <utils/launcherinterface.h>
#include <utils/qtcprocess.h>
#include <utils/singleton.h>
#include <utils/temporarydirectory.h>
#include <QtTest>
#include <iostream>
#include <fstream>
using namespace Utils;
enum class Handler {
Setup,
Done,
Error,
GroupSetup,
GroupDone,
GroupError
};
using Log = QList<QPair<int, Handler>>;
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<Group>("root");
QTest::addColumn<Log>("expectedLog");
QTest::addColumn<bool>("runningAfterStart");
QTest::addColumn<bool>("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<TaskItem> 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<Group>("root");
QTest::addColumn<std::shared_ptr<Log>>("storageLog");
QTest::addColumn<Log>("expectedLog");
QTest::addColumn<bool>("runningAfterStart");
QTest::addColumn<bool>("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> 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<Log>, 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"

View File

@@ -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",