diff options
author | Cristian Adam <[email protected]> | 2025-08-21 21:00:42 +0200 |
---|---|---|
committer | Cristian Adam <[email protected]> | 2025-09-12 07:27:42 +0000 |
commit | 2b22cb280617069ae2c59e8d26392e59a148ee4d (patch) | |
tree | 070f1c77bafcb4099d71dad55d47d190e9fce0e8 | |
parent | 468fb0f8f769918b7e783afcaf918e5c16ab7f58 (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.txt | 9 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/cmakeproject.cpp | 30 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/cmakeproject.h | 1 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/presetsmacros.cpp | 36 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/presetsparser.cpp | 269 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/presetsparser.h | 91 | ||||
-rw-r--r-- | src/plugins/cmakeprojectmanager/tests/tst_cmake_test_presets.cpp | 414 |
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 ¯oPrefix, 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" |