blob: 40ba3941cba4258ca1a427ddc3769f487277af05 [file] [edit]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "CMakeGUITest.h"
#include <QApplication>
#include <QEventLoop>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QTimer>
#include <QtGlobal>
#include <QtTest>
#include "CMakeSetupDialog.h"
#include "CatchShow.h"
#include "FirstConfigure.h"
#include "QCMake.h"
using WindowSetupHelper = std::function<void(CMakeSetupDialog*)>;
Q_DECLARE_METATYPE(WindowSetupHelper)
namespace {
void loopSleep(int msecs = 500)
{
QEventLoop loop;
QTimer::singleShot(msecs, &loop, &QEventLoop::quit);
loop.exec();
}
}
CMakeGUITest::CMakeGUITest(CMakeSetupDialog* window, QObject* parent)
: QObject(parent)
, m_window(window)
{
}
void CMakeGUITest::tryConfigure(int expectedResult, int timeout)
{
auto* cmake = this->m_window->findChild<QCMakeThread*>()->cmakeInstance();
bool done = false;
CatchShow catchConfigure;
catchConfigure.setCallback<FirstConfigure>([&done](FirstConfigure* dialog) {
if (done) {
return;
}
done = true;
dialog->findChild<StartCompilerSetup*>()->setCurrentGenerator(
CMAKE_GENERATOR);
dialog->accept();
});
CatchShow catchMessages;
catchMessages.setCallback<QMessageBox>([](QMessageBox* box) {
if (box->text().contains("Build directory does not exist")) {
box->accept();
}
if (box->text().contains("Error in configuration process")) {
box->accept();
}
});
QSignalSpy configureDoneSpy(cmake, &QCMake::configureDone);
QVERIFY(configureDoneSpy.isValid());
QMetaObject::invokeMethod(
this->m_window, [this]() { this->m_window->ConfigureButton->click(); },
Qt::QueuedConnection);
QVERIFY(configureDoneSpy.wait(timeout));
QList<QVariant> configureDoneSignalArguments = configureDoneSpy.takeFirst();
QCOMPARE(configureDoneSignalArguments.at(0).toInt(), expectedResult);
}
void CMakeGUITest::tryGenerate(int expectedResult, int timeout)
{
auto* cmake = this->m_window->findChild<QCMakeThread*>()->cmakeInstance();
CatchShow catchMessages;
catchMessages.setCallback<QMessageBox>([](QMessageBox* box) {
if (box->text().contains("Error in generation process")) {
box->accept();
}
});
QSignalSpy generateDoneSpy(cmake, &QCMake::generateDone);
QVERIFY(generateDoneSpy.isValid());
QMetaObject::invokeMethod(
this->m_window, [this]() { this->m_window->GenerateButton->click(); },
Qt::QueuedConnection);
QVERIFY(generateDoneSpy.wait(timeout));
QList<QVariant> generateDoneSignalArguments = generateDoneSpy.takeFirst();
QCOMPARE(generateDoneSignalArguments.at(0).toInt(), expectedResult);
}
void CMakeGUITest::sourceBinaryArgs()
{
QFETCH(QString, sourceDir);
QFETCH(QString, binaryDir);
// Wait a bit for everything to update
loopSleep();
QCOMPARE(this->m_window->SourceDirectory->text(), sourceDir);
QCOMPARE(this->m_window->BinaryDirectory->currentText(), binaryDir);
}
void CMakeGUITest::sourceBinaryArgs_data()
{
QTest::addColumn<QString>("sourceDir");
QTest::addColumn<QString>("binaryDir");
QTest::newRow("sourceAndBinaryDir")
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-sourceAndBinaryDir/src"
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-sourceAndBinaryDir/build";
QTest::newRow("sourceAndBinaryDirRelative") << CMakeGUITest_BINARY_DIR
"/sourceBinaryArgs-sourceAndBinaryDirRelative/src"
<< CMakeGUITest_BINARY_DIR
"/sourceBinaryArgs-sourceAndBinaryDirRelative/build";
QTest::newRow("sourceDir")
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-sourceDir/src"
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-sourceDir";
QTest::newRow("binaryDir")
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-binaryDir/src"
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-binaryDir/build";
QTest::newRow("noExist") << ""
<< "";
QTest::newRow("noExistConfig")
<< ""
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-noExistConfig/oldbuild";
QTest::newRow("noExistConfigExists")
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-noExistConfigExists/src"
<< CMakeGUITest_BINARY_DIR "/sourceBinaryArgs-noExistConfigExists/build";
}
void CMakeGUITest::simpleConfigure()
{
QFETCH(QString, sourceDir);
QFETCH(QString, binaryDir);
QFETCH(int, expectedResult);
this->m_window->SourceDirectory->setText(sourceDir);
this->m_window->BinaryDirectory->setCurrentText(binaryDir);
// Wait a bit for everything to update
loopSleep();
this->tryConfigure(expectedResult, 1000);
}
void CMakeGUITest::simpleConfigure_data()
{
QTest::addColumn<QString>("sourceDir");
QTest::addColumn<QString>("binaryDir");
QTest::addColumn<int>("expectedResult");
QTest::newRow("success") << CMakeGUITest_BINARY_DIR
"/simpleConfigure-success/src"
<< CMakeGUITest_BINARY_DIR
"/simpleConfigure-success/build"
<< 0;
QTest::newRow("fail") << CMakeGUITest_BINARY_DIR "/simpleConfigure-fail/src"
<< CMakeGUITest_BINARY_DIR
"/simpleConfigure-fail/build"
<< -1;
}
void CMakeGUITest::instrumentation()
{
this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
"/instrumentation/src");
this->m_window->BinaryDirectory->setCurrentText(CMakeGUITest_BINARY_DIR
"/instrumentation/build");
// Wait a bit for everything to update
loopSleep();
this->tryConfigure();
this->tryConfigure();
this->tryGenerate();
}
void CMakeGUITest::environment()
{
auto* cmake = this->m_window->findChild<QCMakeThread*>()->cmakeInstance();
this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
"/environment/src");
this->m_window->BinaryDirectory->setCurrentText(CMakeGUITest_BINARY_DIR
"/environment/build");
// We are already testing EnvironmentDialog, so just trust that it's
// connected correctly and modify the environment directly.
auto env = cmake->environment();
env.insert("ADDED_VARIABLE", "Added variable");
env.insert("CHANGED_VARIABLE", "Changed variable");
env.remove("REMOVED_VARIABLE");
cmake->setEnvironment(env);
// Wait a bit for everything to update
loopSleep();
this->tryConfigure();
auto penv = QProcessEnvironment::systemEnvironment();
QVERIFY(!penv.contains("ADDED_VARIABLE"));
QCOMPARE(penv.value("KEPT_VARIABLE"), "Kept variable");
QCOMPARE(penv.value("CHANGED_VARIABLE"), "This variable will be changed");
QCOMPARE(penv.value("REMOVED_VARIABLE"), "Removed variable");
}
void CMakeGUITest::presetArg()
{
QFETCH(WindowSetupHelper, setupFunction);
QFETCH(QString, presetName);
QFETCH(QString, sourceDir);
QFETCH(QString, binaryDir);
QFETCH(QCMakePropertyList, properties);
if (setupFunction) {
setupFunction(this->m_window);
}
// Wait a bit for everything to update
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), presetName);
QCOMPARE(this->m_window->SourceDirectory->text(), sourceDir);
QCOMPARE(this->m_window->BinaryDirectory->currentText(), binaryDir);
auto actualProperties =
this->m_window->CacheValues->cacheModel()->properties();
QCOMPARE(actualProperties.size(), properties.size());
for (int i = 0; i < actualProperties.size(); ++i) {
// operator==() only compares Key, we need to compare Value and Type too
QCOMPARE(actualProperties[i].Key, properties[i].Key);
QCOMPARE(actualProperties[i].Value, properties[i].Value);
QCOMPARE(actualProperties[i].Type, properties[i].Type);
}
}
namespace {
QCMakePropertyList makePresetProperties(QString const& name)
{
return QCMakePropertyList{
QCMakeProperty{
/*Key=*/"FALSE_VARIABLE",
/*Value=*/false,
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::BOOL,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"FILEPATH_VARIABLE",
/*Value=*/
QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR "/%1/src/CMakeLists.txt")
.arg(name),
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::FILEPATH,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"ON_VARIABLE",
/*Value=*/true,
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::BOOL,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"PATH_VARIABLE",
/*Value=*/
QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR "/%1/src").arg(name),
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::PATH,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"SOURCE_DIR_NAME_VARIABLE",
/*Value=*/"src",
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::STRING,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"STRING_VARIABLE",
/*Value=*/"String value",
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::STRING,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"UNINITIALIZED_VARIABLE",
/*Value=*/"Uninitialized value",
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::STRING,
/*Advanced=*/false,
},
};
}
}
void CMakeGUITest::presetArg_data()
{
QTest::addColumn<WindowSetupHelper>("setupFunction");
QTest::addColumn<QString>("presetName");
QTest::addColumn<QString>("sourceDir");
QTest::addColumn<QString>("binaryDir");
QTest::addColumn<QCMakePropertyList>("properties");
QTest::newRow("preset") << WindowSetupHelper{} << "ninja"
<< CMakeGUITest_BINARY_DIR "/presetArg-preset/src"
<< CMakeGUITest_BINARY_DIR
"/presetArg-preset/src/build"
<< makePresetProperties("presetArg-preset");
QTest::newRow("presetBinary")
<< WindowSetupHelper{} << "ninja"
<< CMakeGUITest_BINARY_DIR "/presetArg-presetBinary/src"
<< CMakeGUITest_BINARY_DIR "/presetArg-presetBinary/build"
<< makePresetProperties("presetArg-presetBinary");
QTest::newRow("presetBinaryChange")
<< WindowSetupHelper{ [](CMakeSetupDialog* window) {
loopSleep();
window->Preset->setPresetName("ninja2");
} }
<< "ninja2" << CMakeGUITest_BINARY_DIR "/presetArg-presetBinaryChange/src"
<< CMakeGUITest_BINARY_DIR "/presetArg-presetBinaryChange/src/build"
<< makePresetProperties("presetArg-presetBinaryChange");
QTest::newRow("noPresetBinaryChange")
<< WindowSetupHelper{ [](CMakeSetupDialog* window) {
loopSleep();
window->Preset->setPresetName("ninja");
} }
<< "ninja" << CMakeGUITest_BINARY_DIR "/presetArg-noPresetBinaryChange/src"
<< CMakeGUITest_BINARY_DIR "/presetArg-noPresetBinaryChange/src/build"
<< makePresetProperties("presetArg-noPresetBinaryChange");
QTest::newRow("presetConfigExists")
<< WindowSetupHelper{} << "ninja"
<< CMakeGUITest_BINARY_DIR "/presetArg-presetConfigExists/src"
<< CMakeGUITest_BINARY_DIR "/presetArg-presetConfigExists/src/build"
<< makePresetProperties("presetArg-presetConfigExists");
QCMakePropertyList installDirProperties{
QCMakeProperty{
/*Key=*/"CMAKE_INSTALL_PREFIX",
/*Value=*/
QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR
"/presetArg-installDir/src/install"),
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::PATH,
/*Advanced=*/false,
},
QCMakeProperty{
/*Key=*/"CMAKE_TOOLCHAIN_FILE",
/*Value=*/
QString::fromLocal8Bit(CMakeGUITest_BINARY_DIR
"/presetArg-installDir/src/toolchain.cmake"),
/*Strings=*/{},
/*Help=*/"",
/*Type=*/QCMakeProperty::FILEPATH,
/*Advanced=*/false,
},
};
QTest::newRow("installDir")
<< WindowSetupHelper{} << "installDir"
<< CMakeGUITest_BINARY_DIR "/presetArg-installDir/src"
<< CMakeGUITest_BINARY_DIR "/presetArg-installDir/src/build"
<< installDirProperties;
QTest::newRow("noExist") << WindowSetupHelper{} << QString{}
<< CMakeGUITest_BINARY_DIR "/presetArg-noExist/src"
<< "" << QCMakePropertyList{};
}
namespace {
void writePresets(QString const& buildDir, QStringList const& names)
{
QJsonArray presets{
QJsonObject{
{ "name", "base" },
{ "generator", "Ninja" },
{ "binaryDir",
QString::fromLocal8Bit("${sourceDir}/%1/${presetName}")
.arg(buildDir) },
{ "hidden", true },
},
};
for (auto const& name : names) {
presets.append(QJsonObject{
{ "name", name },
{ "inherits", QJsonArray{ "base" } },
});
}
QJsonDocument doc{ QJsonObject{
{ "version", 1 },
{ "configurePresets", presets },
} };
QFile presetsFile(CMakeGUITest_BINARY_DIR
"/changingPresets/src/CMakePresets.json");
bool open = presetsFile.open(QIODevice::WriteOnly);
Q_ASSERT(open);
presetsFile.write(doc.toJson());
}
}
void CMakeGUITest::changingPresets()
{
QDir::root().mkpath(CMakeGUITest_BINARY_DIR "/changingPresets/src");
this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
"/changingPresets/src");
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), QString{});
QCOMPARE(this->m_window->Preset->presets().size(), 0);
QCOMPARE(this->m_window->BinaryDirectory->currentText(), "");
QCOMPARE(this->m_window->Preset->isEnabled(), false);
writePresets("build1", { "preset" });
this->m_window->ReloadPresetsButton->click();
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), QString{});
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(), "");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
this->m_window->Preset->setPresetName("preset");
this->m_window->ReloadPresetsButton->click();
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), "preset");
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
writePresets("build2", { "preset2", "preset" });
this->m_window->ReloadPresetsButton->click();
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), "preset");
QCOMPARE(this->m_window->Preset->presets().size(), 2);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
writePresets("build3", { "preset2" });
this->m_window->ReloadPresetsButton->click();
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), QString{});
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src/build1/preset");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
this->m_window->Preset->setPresetName("preset2");
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), "preset2");
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src/build3/preset2");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
QDir::root().mkpath(CMakeGUITest_BINARY_DIR "/changingPresets/src2");
QFile::copy(CMakeGUITest_BINARY_DIR "/changingPresets/src/CMakePresets.json",
CMakeGUITest_BINARY_DIR
"/changingPresets/src2/CMakePresets.json");
this->m_window->SourceDirectory->setText(CMakeGUITest_BINARY_DIR
"/changingPresets/src2");
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), QString{});
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src/build3/preset2");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
this->m_window->Preset->setPresetName("preset2");
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), "preset2");
QCOMPARE(this->m_window->Preset->presets().size(), 1);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src2/build3/preset2");
QCOMPARE(this->m_window->Preset->isEnabled(), true);
QFile(CMakeGUITest_BINARY_DIR "/changingPresets/src2/CMakePresets.json")
.remove();
this->m_window->ReloadPresetsButton->click();
loopSleep();
QCOMPARE(this->m_window->Preset->presetName(), QString{});
QCOMPARE(this->m_window->Preset->presets().size(), 0);
QCOMPARE(this->m_window->BinaryDirectory->currentText(),
CMakeGUITest_BINARY_DIR "/changingPresets/src2/build3/preset2");
QCOMPARE(this->m_window->Preset->isEnabled(), false);
}
namespace {
QString modelValue(QCMakeCacheModel* model, QString const& key)
{
for (auto const& prop : model->properties()) {
if (prop.Key == key) {
return prop.Value.toString();
}
}
return QString{};
}
QString readCacheValue(QString const& buildDir, QString const& key)
{
QFile cache(buildDir + "/CMakeCache.txt");
if (!cache.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString{};
}
QString const prefix = key + ":";
while (!cache.atEnd()) {
QString const line = QString::fromLocal8Bit(cache.readLine()).trimmed();
if (line.startsWith(prefix)) {
int const eq = line.indexOf('=');
if (eq >= 0) {
return line.mid(eq + 1);
}
}
}
return QString{};
}
}
void CMakeGUITest::presetApplyOrdering()
{
QFETCH(QString, scenario);
QFETCH(QString, buildDir);
QFETCH(QString, expectedValue);
auto* cmake = this->m_window->findChild<QCMakeThread*>()->cmakeInstance();
// Let the initial cache load from the preconfigured build settle.
loopSleep();
QSignalSpy configureDoneSpy(cmake, &QCMake::configureDone);
QVERIFY(configureDoneSpy.isValid());
// Select and configure in one main-thread run, with no yield between, to
// hit the race.
this->m_window->Preset->setPresetName("raceA");
if (scenario == "replace" || scenario == "replaceCycle") {
this->m_window->Preset->setPresetName("raceB");
}
if (scenario == "replaceCycle") {
this->m_window->Preset->setPresetName("raceA");
}
// Dependent actions must be off the instant the request is submitted.
QVERIFY(!this->m_window->ConfigureButton->isEnabled());
QVERIFY(!this->m_window->GenerateButton->isEnabled());
// Activating while a preset applies must not configure with the stale model.
this->m_window->ConfigureButton->click();
QCOMPARE(configureDoneSpy.count(), 0);
// Gate lifts only once the accepted completion has installed the model.
QTRY_VERIFY(this->m_window->ConfigureButton->isEnabled());
// The click during the pending window did nothing.
QCOMPARE(configureDoneSpy.count(), 0);
// Model holds the winning preset, not the preconfigured value.
QCOMPARE(modelValue(this->m_window->CacheValues->cacheModel(), "RACE_VALUE"),
expectedValue);
// A real configure writes that value through to the cache.
this->tryConfigure();
QCOMPARE(readCacheValue(buildDir, "RACE_VALUE"), expectedValue);
}
void CMakeGUITest::presetApplyOrdering_data()
{
QTest::addColumn<QString>("scenario");
QTest::addColumn<QString>("buildDir");
QTest::addColumn<QString>("expectedValue");
QTest::newRow("apply") << "apply"
<< CMakeGUITest_BINARY_DIR
"/presetApplyOrdering-apply/build"
<< "presetA";
QTest::newRow("replace") << "replace"
<< CMakeGUITest_BINARY_DIR
"/presetApplyOrdering-replace/build"
<< "presetB";
QTest::newRow("replaceCycle")
<< "replaceCycle"
<< CMakeGUITest_BINARY_DIR "/presetApplyOrdering-replaceCycle/build"
<< "presetA";
}
void CMakeGUITest::presetApplyStartup()
{
QFETCH(QString, requestedPreset);
QFETCH(bool, available);
// The deferred gate must have the actions off at startup, before presets
// load. Deterministic: pending was set on the main thread before the event
// loop processed any worker result.
QVERIFY(!this->m_window->ConfigureButton->isEnabled());
QVERIFY(!this->m_window->GenerateButton->isEnabled());
// Gate lifts once presets resolve, applied or not.
QTRY_VERIFY(this->m_window->ConfigureButton->isEnabled());
QVERIFY(this->m_window->GenerateButton->isEnabled());
if (available) {
QCOMPARE(this->m_window->Preset->presetName(), requestedPreset);
QCOMPARE(
modelValue(this->m_window->CacheValues->cacheModel(), "RACE_VALUE"),
QString("presetA"));
} else {
QVERIFY(this->m_window->Preset->presetName() != requestedPreset);
}
}
void CMakeGUITest::presetApplyStartup_data()
{
QTest::addColumn<QString>("requestedPreset");
QTest::addColumn<bool>("available");
QTest::newRow("available") << "raceA" << true;
QTest::newRow("unavailable") << "doesNotExist" << false;
}
void SetupDefaultQSettings()
{
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope,
QString::fromLocal8Bit(qgetenv("CMake_GUI_CONFIG_DIR")));
}
int CMakeGUIExec(CMakeSetupDialog* window)
{
auto nameArray = qgetenv("CMake_GUI_TEST_NAME");
auto name = QString::fromLocal8Bit(nameArray);
if (name.isEmpty()) {
return QApplication::exec();
}
QStringList args{ "CMakeGUITest", name };
CMakeGUITest obj(window);
return QTest::qExec(&obj, args);
}