aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCristian Adam <[email protected]>2025-08-21 21:00:42 +0200
committerCristian Adam <[email protected]>2025-09-12 07:27:42 +0000
commit2b22cb280617069ae2c59e8d26392e59a148ee4d (patch)
tree070f1c77bafcb4099d71dad55d47d190e9fce0e8
parent468fb0f8f769918b7e783afcaf918e5c16ab7f58 (diff)
CMakePM: Add CMake test presets support
The documentation is at https://cmake.org/cmake/help/v3.25/manual/cmake- presets.7.html#test-preset Change-Id: Ida335a94a347732aa8814dbb62b385cbec2ef4d3 Reviewed-by: Christian Stenger <[email protected]>
-rw-r--r--src/plugins/cmakeprojectmanager/CMakeLists.txt9
-rw-r--r--src/plugins/cmakeprojectmanager/cmakeproject.cpp30
-rw-r--r--src/plugins/cmakeprojectmanager/cmakeproject.h1
-rw-r--r--src/plugins/cmakeprojectmanager/presetsmacros.cpp36
-rw-r--r--src/plugins/cmakeprojectmanager/presetsparser.cpp269
-rw-r--r--src/plugins/cmakeprojectmanager/presetsparser.h91
-rw-r--r--src/plugins/cmakeprojectmanager/tests/tst_cmake_test_presets.cpp414
7 files changed, 850 insertions, 0 deletions
diff --git a/src/plugins/cmakeprojectmanager/CMakeLists.txt b/src/plugins/cmakeprojectmanager/CMakeLists.txt
index 62060f0dd28..8c2fcafaf3f 100644
--- a/src/plugins/cmakeprojectmanager/CMakeLists.txt
+++ b/src/plugins/cmakeprojectmanager/CMakeLists.txt
@@ -58,6 +58,15 @@ add_qtc_plugin(CMakeProjectManager
vitaut-rstparser
)
+add_qtc_test(tst_cmake_test_presets
+ CONDITION WITH_TESTS
+ DEPENDS Utils ProjectExplorer CMakeProjectManager
+ SOURCES
+ tests/tst_cmake_test_presets.cpp
+ presetsparser.cpp
+ presetsmacros.cpp
+)
+
file(GLOB_RECURSE test_cases RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} testcases/*)
qtc_add_resources(CMakeProjectManager "testcases"
CONDITION WITH_TESTS
diff --git a/src/plugins/cmakeprojectmanager/cmakeproject.cpp b/src/plugins/cmakeprojectmanager/cmakeproject.cpp
index 591b022c7e2..d8ddd5dfea8 100644
--- a/src/plugins/cmakeprojectmanager/cmakeproject.cpp
+++ b/src/plugins/cmakeprojectmanager/cmakeproject.cpp
@@ -220,6 +220,7 @@ Internal::PresetsData CMakeProject::combinePresets(Internal::PresetsData &cmakeP
QHash<QString, PresetsDetails::ConfigurePreset> configurePresetsHash;
QHash<QString, PresetsDetails::BuildPreset> buildPresetsHash;
+ QHash<QString, PresetsDetails::TestPreset> testPresetsHash;
result.configurePresets = combinePresetsInternal(configurePresetsHash,
cmakePresetsData.configurePresets,
@@ -229,6 +230,8 @@ Internal::PresetsData CMakeProject::combinePresets(Internal::PresetsData &cmakeP
cmakePresetsData.buildPresets,
cmakeUserPresetsData.buildPresets,
"build");
+ result.testPresets = combinePresetsInternal(
+ testPresetsHash, cmakePresetsData.testPresets, cmakeUserPresetsData.testPresets, "test");
return result;
}
@@ -257,6 +260,31 @@ void CMakeProject::setupBuildPresets(Internal::PresetsData &presetsData)
}
}
+void CMakeProject::setupTestPresets(Internal::PresetsData &presetsData)
+{
+ for (auto &testPreset : presetsData.testPresets) {
+ if (testPreset.inheritConfigureEnvironment) {
+ if (!testPreset.configurePreset && !testPreset.hidden) {
+ TaskHub::addTask<BuildSystemTask>(
+ Task::TaskType::DisruptingError,
+ Tr::tr("Test preset %1 is missing a corresponding configure preset.")
+ .arg(testPreset.name));
+ presetsData.hasValidPresets = false;
+ }
+
+ const QString &configurePresetName = testPreset.configurePreset.value_or(QString());
+ testPreset.environment
+ = Utils::findOrDefault(presetsData.configurePresets,
+ [configurePresetName](
+ const PresetsDetails::ConfigurePreset &configurePreset) {
+ return configurePresetName == configurePreset.name;
+ })
+ .environment;
+ }
+ }
+}
+
+
QString CMakeProject::projectDisplayName(const Utils::FilePath &projectFilePath)
{
const QString fallbackDisplayName = projectFilePath.absolutePath().fileName();
@@ -343,6 +371,7 @@ void CMakeProject::readPresets()
presetData.configurePresets = includeData.configurePresets
+ presetData.configurePresets;
presetData.buildPresets = includeData.buildPresets + presetData.buildPresets;
+ presetData.testPresets = includeData.testPresets + presetData.testPresets;
presetData.hasValidPresets = includeData.hasValidPresets && presetData.hasValidPresets;
includeStack << includePath;
@@ -370,6 +399,7 @@ void CMakeProject::readPresets()
m_presetsData = combinePresets(cmakePresetsData, cmakeUserPresetsData);
setupBuildPresets(m_presetsData);
+ setupTestPresets(m_presetsData);
if (!m_presetsData.hasValidPresets) {
m_presetsData = {};
diff --git a/src/plugins/cmakeprojectmanager/cmakeproject.h b/src/plugins/cmakeprojectmanager/cmakeproject.h
index 862d5b9a56a..61b4e4c43b7 100644
--- a/src/plugins/cmakeprojectmanager/cmakeproject.h
+++ b/src/plugins/cmakeprojectmanager/cmakeproject.h
@@ -49,6 +49,7 @@ private:
Internal::PresetsData combinePresets(Internal::PresetsData &cmakePresetsData,
Internal::PresetsData &cmakeUserPresetsData);
void setupBuildPresets(Internal::PresetsData &presetsData);
+ void setupTestPresets(Internal::PresetsData &presetsData);
mutable Internal::CMakeProjectImporter *m_projectImporter = nullptr;
mutable QList<ProjectExplorer::Kit*> m_oldPresetKits;
diff --git a/src/plugins/cmakeprojectmanager/presetsmacros.cpp b/src/plugins/cmakeprojectmanager/presetsmacros.cpp
index d0495398060..3643e2b0085 100644
--- a/src/plugins/cmakeprojectmanager/presetsmacros.cpp
+++ b/src/plugins/cmakeprojectmanager/presetsmacros.cpp
@@ -68,6 +68,24 @@ static void expandAllButEnv(const PresetsDetails::BuildPreset &preset,
Utils::OsSpecificAspects::pathListSeparator(sourceDirectory.osType()));
}
+static void expandAllButEnv(const PresetsDetails::TestPreset &preset,
+ const Utils::FilePath &sourceDirectory,
+ QString &value)
+{
+ value.replace("${dollar}", "$");
+
+ value.replace("${sourceDir}", sourceDirectory.path());
+ value.replace("${fileDir}", preset.fileDir.path());
+ value.replace("${sourceParentDir}", sourceDirectory.parentDir().path());
+ value.replace("${sourceDirName}", sourceDirectory.fileName());
+
+ value.replace("${presetName}", preset.name);
+ value.replace("${hostSystemName}", getHostSystemName(sourceDirectory.osType()));
+ value.replace("${pathListSep}",
+ Utils::OsSpecificAspects::pathListSeparator(sourceDirectory.osType()));
+}
+
+
static QString expandMacroEnv(const QString &macroPrefix,
const QString &value,
const std::function<QString(const QString &)> &op)
@@ -414,4 +432,22 @@ template void expand<PresetsDetails::BuildPreset>(const PresetsDetails::BuildPre
template bool evaluatePresetCondition<PresetsDetails::BuildPreset>(
const PresetsDetails::BuildPreset &preset, const Utils::FilePath &sourceDirectory);
+
+// Expand for PresetsDetails::TestPreset
+template void expand<PresetsDetails::TestPreset>(const PresetsDetails::TestPreset &preset,
+ Utils::Environment &env,
+ const Utils::FilePath &sourceDirectory);
+
+template void expand<PresetsDetails::TestPreset>(const PresetsDetails::TestPreset &preset,
+ Utils::EnvironmentItems &envItems,
+ const Utils::FilePath &sourceDirectory);
+
+template void expand<PresetsDetails::TestPreset>(const PresetsDetails::TestPreset &preset,
+ const Utils::Environment &env,
+ const Utils::FilePath &sourceDirectory,
+ QString &value);
+
+template bool evaluatePresetCondition<PresetsDetails::TestPreset>(
+ const PresetsDetails::TestPreset &preset, const Utils::FilePath &sourceDirectory);
+
} // namespace CMakeProjectManager::Internal::CMakePresets::Macros
diff --git a/src/plugins/cmakeprojectmanager/presetsparser.cpp b/src/plugins/cmakeprojectmanager/presetsparser.cpp
index 7e4a6b5d5a6..87392e50d03 100644
--- a/src/plugins/cmakeprojectmanager/presetsparser.cpp
+++ b/src/plugins/cmakeprojectmanager/presetsparser.cpp
@@ -467,6 +467,233 @@ static bool parseBuildPresets(const QJsonValue &jsonValue,
return true;
}
+static std::optional<PresetsDetails::Output> parseOutput(const QJsonValue &jsonValue)
+{
+ if (jsonValue.isUndefined())
+ return std::nullopt;
+
+ PresetsDetails::Output output;
+
+ QJsonObject object = jsonValue.toObject();
+ if (object.contains("shortProgress"))
+ output.shortProgress = object.value("shortProgress").toBool();
+ if (object.contains("verbosity"))
+ output.verbosity = object.value("verbosity").toString();
+ if (object.contains("debug"))
+ output.debug = object.value("debug").toBool();
+ if (object.contains("outputOnFailure"))
+ output.outputOnFailure = object.value("outputOnFailure").toBool();
+ if (object.contains("quiet"))
+ output.quiet = object.value("quiet").toBool();
+ if (object.contains("oputputLogFile"))
+ output.outputLogFile = Utils::FilePath::fromUserInput(
+ object.value("oputputLogFile").toString());
+ if (object.contains("outputJUnitFile"))
+ output.outputJUnitFile = Utils::FilePath::fromUserInput(
+ object.value("outputJUnitFile").toString());
+ if (object.contains("labelSummary"))
+ output.labelSummary = object.value("labelSummary").toBool();
+ if (object.contains("subprojectSummary"))
+ output.subprojectSummary = object.value("subprojectSummary").toBool();
+ if (object.contains("maxPassedTestOutputSize"))
+ output.maxPassedTestOutputSize = object.value("maxPassedTestOutputSize").toInt();
+ if (object.contains("maxFailedTestOutputSize"))
+ output.maxFailedTestOutputSize = object.value("maxFailedTestOutputSize").toInt();
+ if (object.contains("testOutputTruncation"))
+ output.testOutputTruncation = object.value("testOutputTruncation").toString();
+ if (object.contains("maxTestNameWidth"))
+ output.maxTestNameWidth = object.value("maxTestNameWidth").toInt();
+
+ return output;
+}
+
+static std::optional<PresetsDetails::Filter> parseFilter(const QJsonValue &jsonValue)
+{
+ if (jsonValue.isUndefined())
+ return std::nullopt;
+
+ PresetsDetails::Filter filter;
+
+ QJsonObject object = jsonValue.toObject();
+ if (object.contains("include")) {
+ QJsonObject includeObj = object.value("include").toObject();
+ if (!includeObj.isEmpty()) {
+ filter.include = PresetsDetails::Filter::Include();
+ if (includeObj.contains("name"))
+ filter.include->name = includeObj.value("name").toString();
+ if (includeObj.contains("label"))
+ filter.include->label = includeObj.value("label").toString();
+ if (includeObj.contains("useUnion"))
+ filter.include->useUnion = includeObj.value("useUnion").toBool();
+
+ if (includeObj.contains("index")) {
+ QJsonObject indexObj = includeObj.value("index").toObject();
+ if (!indexObj.isEmpty()) {
+ filter.include->index = PresetsDetails::Filter::Include::Index();
+ if (indexObj.contains("start"))
+ filter.include->index->start = indexObj.value("start").toInt();
+ if (indexObj.contains("end"))
+ filter.include->index->end = indexObj.value("end").toInt();
+ if (indexObj.contains("stride"))
+ filter.include->index->stride = indexObj.value("stride").toInt();
+ if (indexObj.contains("specificTests")) {
+ QJsonValue specificTestsValue = indexObj.value("specificTests");
+ if (specificTestsValue.isArray()) {
+ filter.include->index->specificTests = QList<int>();
+ const QJsonArray specificTestsArray = specificTestsValue.toArray();
+ for (const auto &arrayVal : specificTestsArray)
+ filter.include->index->specificTests.value() << arrayVal.toInt();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (object.contains("exclude")) {
+ QJsonObject excludeObj = object.value("exclude").toObject();
+ if (!excludeObj.isEmpty()) {
+ filter.exclude = PresetsDetails::Filter::Exclude();
+ if (excludeObj.contains("name"))
+ filter.exclude->name = excludeObj.value("name").toString();
+ if (excludeObj.contains("label"))
+ filter.exclude->label = excludeObj.value("label").toString();
+
+ if (excludeObj.contains("fixtures")) {
+ QJsonObject fixturesObj = excludeObj.value("fixtures").toObject();
+ if (!fixturesObj.isEmpty()) {
+ filter.exclude->fixtures = PresetsDetails::Filter::Exclude::Fixtures();
+ if (fixturesObj.contains("any"))
+ filter.exclude->fixtures->any = fixturesObj.value("any").toString();
+ if (fixturesObj.contains("setup"))
+ filter.exclude->fixtures->setup = fixturesObj.value("setup").toString();
+ if (fixturesObj.contains("cleanup"))
+ filter.exclude->fixtures->cleanup = fixturesObj.value("cleanup").toString();
+ }
+ }
+ }
+ }
+
+ return filter;
+}
+
+static std::optional<PresetsDetails::Execution> parseExecution(const QJsonValue &jsonValue)
+{
+ if (jsonValue.isUndefined())
+ return std::nullopt;
+ PresetsDetails::Execution execution;
+
+ QJsonObject object = jsonValue.toObject();
+ if (object.contains("stopOnFailure"))
+ execution.stopOnFailure = object.value("stopOnFailure").toBool();
+ if (object.contains("enableFailover"))
+ execution.enableFailover = object.value("enableFailover").toBool();
+ if (object.contains("jobs"))
+ execution.jobs = object.value("jobs").toInt();
+ if (object.contains("resourceSpecFile"))
+ execution.resourceSpecFile = Utils::FilePath::fromUserInput(
+ object.value("resourceSpecFile").toString());
+ if (object.contains("testLoad"))
+ execution.testLoad = object.value("testLoad").toInt();
+ if (object.contains("showOnly"))
+ execution.showOnly = object.value("showOnly").toString();
+ if (object.contains("repeat")) {
+ QJsonObject repeatObj = object.value("repeat").toObject();
+ if (!repeatObj.isEmpty()) {
+ execution.repeat = PresetsDetails::Execution::Repeat();
+ execution.repeat->mode = repeatObj.value("mode").toString();
+ execution.repeat->count = repeatObj.value("count").toInt();
+ }
+ }
+ if (object.contains("interactiveDebugging"))
+ execution.interactiveDebugging = object.value("interactiveDebugging").toBool();
+ if (object.contains("scheduleRandom"))
+ execution.scheduleRandom = object.value("scheduleRandom").toBool();
+ if (object.contains("timeout"))
+ execution.timeout = object.value("timeout").toInt();
+ if (object.contains("noTestsAction"))
+ execution.noTestsAction = object.value("noTestsAction").toString();
+
+ return execution;
+}
+
+static bool parseTestPresets(const QJsonValue &jsonValue,
+ QList<PresetsDetails::TestPreset> &testPresets,
+ const FilePath &fileDir)
+{
+ // The whole section is optional
+ if (jsonValue.isUndefined())
+ return true;
+
+ if (!jsonValue.isArray())
+ return false;
+
+ const QJsonArray testPresetsArray = jsonValue.toArray();
+ for (const auto &presetJson : testPresetsArray) {
+ if (!presetJson.isObject())
+ continue;
+
+ QJsonObject object = presetJson.toObject();
+ PresetsDetails::TestPreset preset;
+
+ preset.name = object.value("name").toString();
+ preset.hidden = object.value("hidden").toBool();
+ preset.fileDir = fileDir;
+
+ QJsonValue inherits = object.value("inherits");
+ if (!inherits.isUndefined()) {
+ preset.inherits = QStringList();
+ if (inherits.isArray()) {
+ const QJsonArray inheritsArray = inherits.toArray();
+ for (const auto &inheritsValue : inheritsArray)
+ preset.inherits.value() << inheritsValue.toString();
+ } else {
+ QString inheritsValue = inherits.toString();
+ if (!inheritsValue.isEmpty())
+ preset.inherits.value() << inheritsValue;
+ }
+ }
+ if (object.contains("condition"))
+ preset.condition = parseCondition(object.value("condition"));
+ if (object.contains("vendor"))
+ parseVendor(object.value("vendor"), preset.vendor);
+ if (object.contains("displayName"))
+ preset.displayName = object.value("displayName").toString();
+ if (object.contains("description"))
+ preset.description = object.value("description").toString();
+ const QJsonObject environmentObj = object.value("environment").toObject();
+ for (const QString &envKey : environmentObj.keys()) {
+ if (!preset.environment)
+ preset.environment = Utils::Environment();
+ QJsonValue envValue = environmentObj.value(envKey);
+ preset.environment.value().set(envKey, envValue.toString());
+ }
+ if (object.contains("configurePreset"))
+ preset.configurePreset = object.value("configurePreset").toString();
+ if (object.contains("inheritConfigureEnvironment"))
+ preset.inheritConfigureEnvironment = object.value("inheritConfigureEnvironment").toBool();
+ if (object.contains("configuration"))
+ preset.configuration = object.value("configuration").toString();
+ if (object.contains("overwriteConfigurationFile")) {
+ preset.overwriteConfigurationFile = QStringList();
+ if (object.value("overwriteConfigurationFile").isArray()) {
+ const QJsonArray overwriteArray = object.value("overwriteConfigurationFile").toArray();
+ for (const auto &overwriteValue : overwriteArray)
+ preset.overwriteConfigurationFile.value() << overwriteValue.toString();
+ }
+ }
+ if (object.contains("output"))
+ preset.output = parseOutput(object.value("output"));
+ if (object.contains("filter"))
+ preset.filter = parseFilter(object.value("filter"));
+ if (object.contains("execution"))
+ preset.execution = parseExecution(object.value("execution"));
+ testPresets.emplace_back(preset);
+ }
+ return true;
+}
+
+
const PresetsData &PresetsParser::presetsData() const
{
return m_presetsData;
@@ -536,6 +763,16 @@ bool PresetsParser::parse(const FilePath &jsonFile, QString &errorMessage, int &
}
// optional
+ if (!parseTestPresets(root.value("testPresets"),
+ m_presetsData.testPresets,
+ jsonFile.parentDir())) {
+ errorMessage = ::CMakeProjectManager::Tr::tr(
+ "Invalid \"testPresets\" section in file \"%1\".")
+ .arg(jsonFile.fileName());
+ return false;
+ }
+
+ // optional
if (!parseVendor(root.value("vendor"), m_presetsData.vendor)) {
errorMessage = ::CMakeProjectManager::Tr::tr("Invalid \"vendor\" section in file \"%1\".")
.arg(jsonFile.fileName());
@@ -710,4 +947,36 @@ bool PresetsDetails::Condition::evaluate() const
return false;
}
+void PresetsDetails::TestPreset::inheritFrom(const TestPreset &other)
+{
+ if (!condition && other.condition && !other.condition->isNull())
+ condition = other.condition;
+
+ if (!vendor && other.vendor)
+ vendor = other.vendor;
+ else if (vendor && other.vendor)
+ vendor = merge(*other.vendor, *vendor);
+
+ if (!environment && other.environment)
+ environment = other.environment;
+ else if (environment && other.environment)
+ environment = environment->appliedToEnvironment(*other.environment);
+
+ if (!configurePreset && other.configurePreset)
+ configurePreset = other.configurePreset;
+ if (!inheritConfigureEnvironment && other.inheritConfigureEnvironment)
+ inheritConfigureEnvironment = other.inheritConfigureEnvironment;
+
+ if (!configuration && other.configuration)
+ configuration = other.configuration;
+ if (!overwriteConfigurationFile && other.overwriteConfigurationFile)
+ overwriteConfigurationFile = other.overwriteConfigurationFile;
+ if (!output && other.output)
+ output = other.output;
+ if (!filter && other.filter)
+ filter = other.filter;
+ if (!execution && other.execution)
+ execution = other.execution;
+}
+
} // CMakeProjectManager::Internal
diff --git a/src/plugins/cmakeprojectmanager/presetsparser.h b/src/plugins/cmakeprojectmanager/presetsparser.h
index a0e743f833a..381dfb3964d 100644
--- a/src/plugins/cmakeprojectmanager/presetsparser.h
+++ b/src/plugins/cmakeprojectmanager/presetsparser.h
@@ -85,6 +85,74 @@ public:
std::optional<ConditionPtr> condition;
};
+struct Output
+{
+ std::optional<bool> shortProgress;
+ std::optional<QString> verbosity;
+ std::optional<bool> debug;
+ std::optional<bool> outputOnFailure;
+ std::optional<bool> quiet;
+ std::optional<Utils::FilePath> outputLogFile;
+ std::optional<Utils::FilePath> outputJUnitFile;
+ std::optional<bool> labelSummary;
+ std::optional<bool> subprojectSummary;
+ std::optional<int> maxPassedTestOutputSize;
+ std::optional<int> maxFailedTestOutputSize;
+ std::optional<QString> testOutputTruncation;
+ std::optional<int> maxTestNameWidth;
+};
+
+struct Filter
+{
+ struct Include
+ {
+ std::optional<QString> name;
+ std::optional<QString> label;
+ std::optional<bool> useUnion;
+ struct Index {
+ std::optional<int> start;
+ std::optional<int> end;
+ std::optional<int> stride;
+ std::optional<QList<int>> specificTests;
+ };
+ std::optional<Index> index;
+ };
+ struct Exclude
+ {
+ std::optional<QString> name;
+ std::optional<QString> label;
+ struct Fixtures
+ {
+ std::optional<QString> any;
+ std::optional<QString> setup;
+ std::optional<QString> cleanup;
+ };
+ std::optional<Fixtures> fixtures;
+ };
+
+ std::optional<Include> include;
+ std::optional<Exclude> exclude;
+};
+
+struct Execution {
+ std::optional<bool> stopOnFailure;
+ std::optional<bool> enableFailover;
+ std::optional<int> jobs;
+ std::optional<Utils::FilePath> resourceSpecFile;
+ std::optional<int> testLoad;
+ std::optional<QString> showOnly;
+ struct Repeat
+ {
+ QString mode;
+ int count;
+ };
+ std::optional<Repeat> repeat;
+ std::optional<bool> interactiveDebugging;
+ std::optional<bool> scheduleRandom;
+ std::optional<int> timeout;
+ std::optional<QString> noTestsAction;
+};
+
class ConfigurePreset {
public:
void inheritFrom(const ConfigurePreset &other);
@@ -134,6 +202,28 @@ public:
std::optional<QStringList> nativeToolOptions;
};
+class TestPreset {
+public:
+ void inheritFrom(const TestPreset &other);
+
+ QString name;
+ bool hidden = false;
+ Utils::FilePath fileDir;
+ std::optional<QStringList> inherits;
+ std::optional<Condition> condition;
+ std::optional<QVariantMap> vendor;
+ std::optional<QString> displayName;
+ std::optional<QString> description;
+ std::optional<Utils::Environment> environment;
+ std::optional<QString> configurePreset;
+ bool inheritConfigureEnvironment = true;
+ std::optional<QString> configuration;
+ std::optional<QStringList> overwriteConfigurationFile;
+ std::optional<Output> output;
+ std::optional<Filter> filter;
+ std::optional<Execution> execution;
+};
+
} // namespace PresetsDetails
class PresetsData
@@ -148,6 +238,7 @@ public:
Utils::FilePath fileDir;
QList<PresetsDetails::ConfigurePreset> configurePresets;
QList<PresetsDetails::BuildPreset> buildPresets;
+ QList<PresetsDetails::TestPreset> testPresets;
};
class PresetsParser
diff --git a/src/plugins/cmakeprojectmanager/tests/tst_cmake_test_presets.cpp b/src/plugins/cmakeprojectmanager/tests/tst_cmake_test_presets.cpp
new file mode 100644
index 00000000000..1bcff91582d
--- /dev/null
+++ b/src/plugins/cmakeprojectmanager/tests/tst_cmake_test_presets.cpp
@@ -0,0 +1,414 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QDir>
+#include <QFile>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QtTest>
+
+#include <utils/filepath.h>
+#include <utils/hostosinfo.h>
+
+#include "../presetsmacros.h"
+#include "../presetsparser.h"
+
+using namespace CMakeProjectManager;
+using namespace CMakeProjectManager::Internal;
+using namespace CMakeProjectManager::Internal::CMakePresets::Macros;
+
+using namespace CMakePresets::Macros;
+using namespace Utils;
+
+namespace CMakeProjectManager::Internal {
+std::optional<PresetsDetails::Condition> parseCondition(const QJsonValue &jsonValue);
+}
+
+class TestPresetsTests : public QObject
+{
+ Q_OBJECT
+
+private slots:
+
+ void testParseTestPresetsMinimal()
+ {
+ // Create a JSON containing only the minimal fields
+ QJsonObject root;
+ QJsonArray arr;
+ QJsonObject obj;
+ obj["name"] = "minimal";
+ arr.append(obj);
+ root["version"] = 5;
+ root["testPresets"] = arr;
+
+ QFile file(QDir::tempPath() + "/minimal.json");
+ QVERIFY(file.open(QIODevice::WriteOnly));
+ file.write(QJsonDocument(root).toJson());
+ file.close();
+
+ PresetsParser parser;
+ QString error;
+ int errorLine{0};
+ bool ok = parser.parse(Utils::FilePath::fromUserInput(file.fileName()), error, errorLine);
+ QVERIFY(ok);
+ QVERIFY(error.isEmpty());
+
+ const auto &data = parser.presetsData();
+ QCOMPARE(data.testPresets.size(), 1);
+ const auto &tp = data.testPresets.at(0);
+ QCOMPARE(tp.name, QString("minimal"));
+ QCOMPARE(tp.hidden, false);
+ QCOMPARE(tp.fileDir.fileName(), QFileInfo(file.fileName()).dir().dirName());
+ QVERIFY(!tp.inherits);
+ QVERIFY(!tp.condition);
+ QVERIFY(!tp.vendor);
+ QVERIFY(!tp.displayName);
+ QVERIFY(!tp.description);
+ QVERIFY(!tp.environment);
+ QVERIFY(!tp.configurePreset);
+ QCOMPARE(tp.inheritConfigureEnvironment, true);
+ QVERIFY(!tp.configuration);
+ QVERIFY(!tp.overwriteConfigurationFile);
+ QVERIFY(!tp.output);
+ QVERIFY(!tp.filter);
+ QVERIFY(!tp.execution);
+ }
+
+ void testParseTestPresetsFull()
+ {
+ const QJsonObject root = QJsonDocument::fromJson(R"(
+ {
+ "version": 5,
+ "testPresets": [
+ {
+ "name": "full",
+ "hidden": true,
+ "inherits": ["base1", "base2"],
+ "condition": {
+ "type": "matches",
+ "string": "Darwin"
+ },
+ "vendor": {
+ "qt.io/QtCreator/1.0": {
+ "debugger": "/path/to/dbg"
+ }
+ },
+ "displayName": "Full Test",
+ "description": "A test preset with all fields",
+ "environment": {
+ "PATH": "/custom/bin"
+ },
+ "configurePreset": "config1",
+ "inheritConfigureEnvironment": false,
+ "configuration": "Debug",
+ "overwriteConfigurationFile": ["CMakeLists.txt"],
+ "output": {
+ "shortProgress": true,
+ "verbosity": "high",
+ "debug": false,
+ "outputOnFailure": true,
+ "quiet": false,
+ "oputputLogFile": "log.txt",
+ "outputJUnitFile": "results.xml",
+ "labelSummary": true,
+ "subprojectSummary": false,
+ "maxPassedTestOutputSize": 1024,
+ "maxFailedTestOutputSize": 2048,
+ "testOutputTruncation": "truncate",
+ "maxTestNameWidth": 80
+ },
+ "filter": {
+ "include": {
+ "name": "TestA",
+ "label": "unit",
+ "useUnion": true,
+ "index": {
+ "start": 1,
+ "end": 10,
+ "stride": 2,
+ "specificTests": [1, 3, 5]
+ }
+ },
+ "exclude": {
+ "name": "TestB",
+ "label": "integration",
+ "fixtures": {
+ "any": "BaseFixture",
+ "setup": "SetupFixture",
+ "cleanup": "CleanupFixture"
+ }
+ }
+ },
+ "execution": {
+ "stopOnFailure": true,
+ "enableFailover": false,
+ "jobs": 4,
+ "resourceSpecFile": "resources.json",
+ "testLoad": 2,
+ "showOnly": "TestA",
+ "repeat": {
+ "mode": "fixed",
+ "count": 3
+ },
+ "interactiveDebugging": true,
+ "scheduleRandom": false,
+ "timeout": 120,
+ "noTestsAction": "skip"
+ }
+ }
+ ]
+ }
+ )").object();
+
+
+ QFile file(QDir::tempPath() + "/full.json");
+ QVERIFY(file.open(QIODevice::WriteOnly));
+ file.write(QJsonDocument(root).toJson());
+ file.close();
+
+ PresetsParser parser;
+ QString error;
+ int errorLine;
+ bool ok = parser.parse(Utils::FilePath::fromUserInput(file.fileName()), error, errorLine);
+ QVERIFY(ok);
+ QVERIFY(error.isEmpty());
+
+ const auto &data = parser.presetsData();
+ QCOMPARE(data.testPresets.size(), 1);
+ const auto &tp = data.testPresets.at(0);
+ QCOMPARE(tp.name, QString("full"));
+ QCOMPARE(tp.hidden, true);
+ QCOMPARE(tp.inherits, QStringList() << "base1" << "base2");
+ QVERIFY(tp.condition);
+ QCOMPARE(tp.condition->isMatches(), true);
+ QCOMPARE(tp.condition->string, "Darwin");
+ QVERIFY(tp.vendor);
+ QCOMPARE(tp.vendor->value("debugger").toString(), "/path/to/dbg");
+ QCOMPARE(tp.displayName, QString("Full Test"));
+ QCOMPARE(tp.description, QString("A test preset with all fields"));
+ QVERIFY(tp.environment);
+ QCOMPARE(tp.environment->value("PATH"), QString("/custom/bin"));
+ QCOMPARE(tp.configurePreset, QString("config1"));
+ QCOMPARE(tp.inheritConfigureEnvironment, false);
+ QCOMPARE(tp.configuration, QString("Debug"));
+ QCOMPARE(tp.overwriteConfigurationFile, QStringList() << "CMakeLists.txt");
+ QVERIFY(tp.output);
+ QCOMPARE(tp.output->shortProgress, true);
+ QCOMPARE(tp.output->verbosity, QString("high"));
+ QCOMPARE(tp.output->debug, false);
+ QCOMPARE(tp.output->outputOnFailure, true);
+ QCOMPARE(tp.output->quiet, false);
+ QCOMPARE(tp.output->outputLogFile, Utils::FilePath::fromUserInput("log.txt"));
+ QCOMPARE(tp.output->outputJUnitFile, Utils::FilePath::fromUserInput("results.xml"));
+ QCOMPARE(tp.output->labelSummary, true);
+ QCOMPARE(tp.output->subprojectSummary, false);
+ QCOMPARE(tp.output->maxPassedTestOutputSize, 1024);
+ QCOMPARE(tp.output->maxFailedTestOutputSize, 2048);
+ QCOMPARE(tp.output->testOutputTruncation, QString("truncate"));
+ QCOMPARE(tp.output->maxTestNameWidth, 80);
+
+ QVERIFY(tp.filter);
+ const auto &inc = tp.filter->include.value();
+ QCOMPARE(inc.name, QString("TestA"));
+ QCOMPARE(inc.label, QString("unit"));
+ QCOMPARE(inc.useUnion, true);
+ const auto &idx = inc.index.value();
+ QCOMPARE(idx.start, 1);
+ QCOMPARE(idx.end, 10);
+ QCOMPARE(idx.stride, 2);
+ QCOMPARE(idx.specificTests, QList<int>() << 1 << 3 << 5);
+
+ const auto &exc = tp.filter->exclude.value();
+ QCOMPARE(exc.name, QString("TestB"));
+ QCOMPARE(exc.label, QString("integration"));
+ const auto &fx = exc.fixtures.value();
+ QCOMPARE(fx.any, QString("BaseFixture"));
+ QCOMPARE(fx.setup, QString("SetupFixture"));
+ QCOMPARE(fx.cleanup, QString("CleanupFixture"));
+
+ QVERIFY(tp.execution);
+ const auto &exe = tp.execution.value();
+ QCOMPARE(exe.stopOnFailure, true);
+ QCOMPARE(exe.enableFailover, false);
+ QCOMPARE(exe.jobs, 4);
+ QCOMPARE(exe.resourceSpecFile, Utils::FilePath::fromUserInput("resources.json"));
+ QCOMPARE(exe.testLoad, 2);
+ QCOMPARE(exe.showOnly, QString("TestA"));
+ QCOMPARE(exe.repeat->mode, QString("fixed"));
+ QCOMPARE(exe.repeat->count, 3);
+ QCOMPARE(exe.interactiveDebugging, true);
+ QCOMPARE(exe.scheduleRandom, false);
+ QCOMPARE(exe.timeout, 120);
+ QCOMPARE(exe.noTestsAction, QString("skip"));
+ }
+
+ void testParseTestPresetsInvalidArray_data()
+ {
+ QTest::addColumn<QString>("json");
+ QTest::addColumn<QString>("errorContains");
+
+ QTest::newRow("not array") << R"({"version":5, "testPresets":"not an array"})"
+ << "Invalid \"testPresets\" section in file \"invalid.json\".";
+ QTest::newRow("array of non objects") << R"({""version":5, testPresets":[1,2,3]})"
+ << "missing name separator";
+ }
+
+ void testParseTestPresetsInvalidArray()
+ {
+ QFETCH(QString, json);
+ QFETCH(QString, errorContains);
+
+ QFile file(QDir::tempPath() + "/invalid.json");
+ QVERIFY(file.open(QIODevice::WriteOnly));
+ file.write(json.toUtf8());
+ file.close();
+
+ PresetsParser parser;
+ QString error;
+ int errorLine;
+ bool ok = parser.parse(Utils::FilePath::fromUserInput(file.fileName()), error, errorLine);
+ QVERIFY(!ok);
+ QVERIFY(error.contains(errorContains));
+ }
+
+ void testTestPresetInheritFrom()
+ {
+ PresetsDetails::TestPreset parent;
+ parent.name = "parent";
+ parent.hidden = true;
+ parent.inherits = QStringList() << "grandparent";
+ parent.condition = CMakeProjectManager::Internal::parseCondition(
+ QJsonObject{{"matches", "Darwin"}});
+ parent.vendor = QVariantMap{
+ {"qt.io/QtCreator/1.0", QVariantMap{{"debugger", "/path/to/dbg"}}}};
+ parent.displayName = "Parent Display";
+ parent.description = "Parent description";
+ parent.environment = Utils::Environment::systemEnvironment();
+ parent.environment->set("PARENT_VAR", "value");
+ parent.configurePreset = "configP";
+ parent.inheritConfigureEnvironment = true;
+ parent.configuration = "Release";
+ parent.overwriteConfigurationFile = QStringList() << "CMakeLists.txt";
+ parent.output = PresetsDetails::Output{
+ true,
+ "high",
+ false,
+ true,
+ false,
+ Utils::FilePath::fromUserInput("log.txt"),
+ Utils::FilePath::fromUserInput("out.xml"),
+ true,
+ false,
+ 100,
+ 200,
+ "trunc",
+ 80};
+ parent.filter = PresetsDetails::Filter{
+ PresetsDetails::Filter::Include{
+ QString("TestA"),
+ QString("unit"),
+ true,
+ PresetsDetails::Filter::Include::Index{0, 10, 1, QList<int>{1, 2}}},
+ PresetsDetails::Filter::Exclude{
+ QString("TestB"),
+ QString("unit"),
+ PresetsDetails::Filter::Exclude::Fixtures{
+ "BaseFixture", "SetupFixture", "CleanupFixture"}}};
+ parent.execution = PresetsDetails::Execution{
+ true,
+ false,
+ 4,
+ Utils::FilePath::fromUserInput("spec.json"),
+ 2,
+ "TestA",
+ PresetsDetails::Execution::Repeat{"fixed", 3},
+ true,
+ false,
+ 120,
+ "skip"};
+
+ PresetsDetails::TestPreset child;
+ child.name = "child";
+ // child should inherit all, except: name, hidden, inherits, description and displayName
+
+ child.inheritFrom(parent);
+
+ QCOMPARE(child.name, QString("child")); // name does not inherit
+ QCOMPARE(child.hidden, false); // unchanged
+ QCOMPARE(child.inherits, std::nullopt);
+ QVERIFY(child.condition);
+ QVERIFY(child.vendor);
+ QVERIFY(!child.displayName); // not inherited
+ QVERIFY(!child.description); // not inherited
+ QVERIFY(child.environment);
+ QVERIFY(child.configurePreset);
+ QCOMPARE(child.inheritConfigureEnvironment, true);
+ QVERIFY(child.configuration);
+ QVERIFY(child.overwriteConfigurationFile);
+ QVERIFY(child.output);
+ QVERIFY(child.filter);
+ QVERIFY(child.execution);
+ }
+
+ void testTestPresetInheritFromPartial()
+ {
+ PresetsDetails::TestPreset parent;
+ parent.environment = Utils::Environment();
+ parent.environment->set("VAR1", "A");
+
+ PresetsDetails::TestPreset child;
+ child.environment = Utils::Environment();
+ child.environment->set("VAR2", "B");
+
+ child.inheritFrom(parent);
+
+ // Environment should be merged: VAR2 remains B, VAR1 added
+ QVERIFY(child.environment);
+ QCOMPARE(child.environment->value("VAR1"), QString("A"));
+ QCOMPARE(child.environment->value("VAR2"), QString("B"));
+ }
+
+ void testExpandMacrosTestPreset()
+ {
+ PresetsDetails::TestPreset preset;
+ preset.name = "preset1";
+ preset.fileDir = Utils::FilePath::fromUserInput("/tmp/project");
+ preset.environment = Utils::Environment();
+ preset.environment->set("VAR", "VALUE");
+
+ Utils::Environment env = preset.environment.value();
+
+ // Test simple variable expansion
+ QString val1 = "$env{VAR}";
+ expand(preset, env, FilePath::fromString("/tmp/project"), val1);
+ QCOMPARE(val1, QString("VALUE"));
+
+ // Test sourceDir macro
+ QString val2 = "${sourceDir}";
+ expand(preset, env, FilePath::fromString("/tmp/project"), val2);
+ QCOMPARE(val2, QString("/tmp/project"));
+
+ // Test presetName macro
+ QString val3 = "Hello ${presetName}";
+ expand(preset, env, FilePath::fromString("/tmp/project"), val3);
+ QCOMPARE(val3, QString("Hello preset1"));
+
+ // Test hostSystemName macro
+ QString val4 = "${hostSystemName}";
+ expand(preset, env, FilePath::fromString("/tmp/project"), val4);
+ // hostSystemName should be a non-empty string
+ QVERIFY(!val4.isEmpty());
+
+ // Test pathListSep macro
+ QString val5 = "a${pathListSep}b";
+ expand(preset, env, FilePath::fromString("/tmp/project"), val5);
+ if (HostOsInfo::isWindowsHost())
+ QCOMPARE(val5, QString("a;b"));
+ else
+ QCOMPARE(val5, QString("a:b"));
+ }
+};
+
+QTEST_GUILESS_MAIN(TestPresetsTests)
+#include "tst_cmake_test_presets.moc"