/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qbs. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "tst_api.h" #include "../shared.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VERIFY_NO_ERROR(errorInfo) \ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString())) #define WAIT_FOR_NEW_TIMESTAMP() waitForNewTimestamp(m_workingDataDir) class LogSink: public qbs::ILogSink { public: QString output; void doPrintWarning(const qbs::ErrorInfo &error) { qDebug("%s", qPrintable(error.toString())); warnings << error; } void doPrintMessage(qbs::LoggerLevel, const QString &message, const QString &) { output += message; } QList warnings; }; class BuildDescriptionReceiver : public QObject { Q_OBJECT public: QString descriptions; QStringList descriptionLines; void handleDescription(const QString &, const QString &description) { descriptions += description; descriptionLines << description; } }; class ProcessResultReceiver : public QObject { Q_OBJECT public: QString output; std::vector results; void handleProcessResult(const qbs::ProcessResult &result) { results.push_back(result); output += result.stdErr().join(QLatin1Char('\n')); output += result.stdOut().join(QLatin1Char('\n')); } }; class TaskReceiver : public QObject { Q_OBJECT public: QString taskDescriptions; void handleTaskStart(const QString &task) { taskDescriptions += task; } }; static void removeBuildDir(const qbs::SetupProjectParameters ¶ms) { QString message; const QString dir = params.buildRoot() + '/' + params.configurationName(); if (!qbs::Internal::removeDirectoryWithContents(dir, &message)) qFatal("Could not remove build dir: %s", qPrintable(message)); } static bool waitForFinished(qbs::AbstractJob *job, int timeout = 0) { if (job->state() == qbs::AbstractJob::StateFinished) return true; QEventLoop loop; QObject::connect(job, &qbs::AbstractJob::finished, &loop, &QEventLoop::quit); if (timeout > 0) { QTimer timer; QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); timer.setSingleShot(true); timer.start(timeout); loop.exec(); return timer.isActive(); // Timer ended the loop <=> job did not finish. } loop.exec(); return true; } TestApi::TestApi() : m_logSink(new LogSink) , m_sourceDataDir(QDir::cleanPath(SRCDIR "/testdata")) , m_workingDataDir(testWorkDir(QStringLiteral("api"))) { } TestApi::~TestApi() { delete m_logSink; } void TestApi::initTestCase() { QString errorMessage; qbs::Internal::removeDirectoryWithContents(m_workingDataDir, &errorMessage); QVERIFY2(qbs::Internal::copyFileRecursion(m_sourceDataDir, m_workingDataDir, false, true, &errorMessage), qPrintable(errorMessage)); } void TestApi::init() { m_logSink->warnings.clear(); } void TestApi::addQObjectMacroToCppFile() { BuildDescriptionReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("add-qobject-macro-to-cpp-file", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(!receiver.descriptions.contains("moc"), qPrintable(receiver.descriptions)); receiver.descriptions.clear(); WAIT_FOR_NEW_TIMESTAMP(); QFile cppFile("object.cpp"); QVERIFY2(cppFile.open(QIODevice::ReadWrite), qPrintable(cppFile.errorString())); QByteArray contents = cppFile.readAll(); contents.replace("// ", ""); cppFile.resize(0); cppFile.write(contents); cppFile.close(); errorInfo = doBuildProject("add-qobject-macro-to-cpp-file", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(receiver.descriptions.contains("moc"), qPrintable(receiver.descriptions)); } static bool isAboutUndefinedSymbols(const QString &_message) { const QString message = _message.toLower(); return message.contains("undefined") || message.contains("unresolved"); } void TestApi::addedFilePersistent() { // On the initial run, linking will fail. const QString relProjectFilePath = "added-file-persistent"; ProcessResultReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject(relProjectFilePath, 0, &receiver); QVERIFY(errorInfo.hasError()); QVERIFY2(isAboutUndefinedSymbols(receiver.output), qPrintable((receiver.output))); receiver.output.clear(); // Add a file. qbs must schedule it for rule application on the next build. WAIT_FOR_NEW_TIMESTAMP(); const qbs::SetupProjectParameters params = defaultSetupParameters(relProjectFilePath); QFile projectFile(params.projectFilePath()); QVERIFY2(projectFile.open(QIODevice::ReadWrite), qPrintable(projectFile.errorString())); const QByteArray originalContent = projectFile.readAll(); QByteArray addedFileContent = originalContent; addedFileContent.replace("/* 'file.cpp' */", "'file.cpp'"); projectFile.resize(0); projectFile.write(addedFileContent); projectFile.flush(); QScopedPointer setupJob(qbs::Project().setupProject(params, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); setupJob.reset(0); // Remove the file again. qbs must unschedule the rule application again. // Consequently, the linking step must fail as in the initial run. WAIT_FOR_NEW_TIMESTAMP(); projectFile.resize(0); projectFile.write(originalContent); projectFile.flush(); errorInfo = doBuildProject(relProjectFilePath, 0, &receiver); QVERIFY(errorInfo.hasError()); QVERIFY2(isAboutUndefinedSymbols(receiver.output), qPrintable((receiver.output))); // Add the file again. qbs must schedule it for rule application on the next build. WAIT_FOR_NEW_TIMESTAMP(); projectFile.resize(0); projectFile.write(addedFileContent); projectFile.close(); setupJob.reset(qbs::Project().setupProject(params, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); setupJob.reset(0); // qbs must remember that a file was scheduled for rule application. The build must then // succeed, as now all necessary symbols are linked in. errorInfo = doBuildProject(relProjectFilePath); VERIFY_NO_ERROR(errorInfo); } void TestApi::baseProperties() { const qbs::ErrorInfo errorInfo = doBuildProject("base-properties/prj.qbs"); VERIFY_NO_ERROR(errorInfo); } void TestApi::buildErrorCodeLocation() { const qbs::ErrorInfo errorInfo = doBuildProject("build-error-code-location/build-error-code-location.qbs"); QVERIFY(errorInfo.hasError()); const qbs::ErrorItem errorItem = errorInfo.items().first(); QCOMPARE(errorItem.description(), QString("Rule.outputArtifacts must return an array of objects.")); const qbs::CodeLocation errorLoc = errorItem.codeLocation(); QCOMPARE(QFileInfo(errorLoc.filePath()).fileName(), QString("build-error-code-location.qbs")); QCOMPARE(errorLoc.line(), 9); QCOMPARE(errorLoc.column(), 26); } void TestApi::buildGraphLocking() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("buildgraph-locking"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); const qbs::Project project = setupJob->project(); Q_UNUSED(project); // Case 1: Setting up a competing project from scratch. setupJob.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY2(setupJob->error().toString().contains("lock"), qPrintable(setupJob->error().toString())); // Case 2: Setting up a non-competing project and then making it competing. qbs::SetupProjectParameters setupParams2 = setupParams; setupParams2.setBuildRoot(setupParams.buildRoot() + "/2"); setupJob.reset(qbs::Project().setupProject(setupParams2, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); const QString buildDirName = relativeBuildDir(setupParams2.configurationName()); const QString lockFile = setupParams2.buildRoot() + '/' + buildDirName + '/' + buildDirName + ".bg.lock"; QVERIFY2(QFileInfo(lockFile).isFile(), qPrintable(lockFile)); qbs::Project project2 = setupJob->project(); QVERIFY(project2.isValid()); setupJob.reset(project2.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY2(setupJob->error().toString().contains("lock"), qPrintable(setupJob->error().toString())); QVERIFY2(QFileInfo(lockFile).isFile(), qPrintable(lockFile)); // Case 3: Changing the build directory of an existing project to something non-competing. qbs::SetupProjectParameters setupParams3 = setupParams2; setupParams3.setBuildRoot(setupParams.buildRoot() + "/3"); setupJob.reset(qbs::Project().setupProject(setupParams3, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); project2 = qbs::Project(); QVERIFY2(!QFileInfo(lockFile).exists(), qPrintable(lockFile)); const QString newLockFile = setupParams3.buildRoot() + '/' + buildDirName + '/' + buildDirName + ".bg.lock"; QVERIFY2(QFileInfo(newLockFile).isFile(), qPrintable(newLockFile)); qbs::Project project3 = setupJob->project(); QVERIFY(project3.isValid()); // Case 4: Changing the build directory again, but cancelling the job. setupJob.reset(project3.setupProject(setupParams2, m_logSink, 0)); QThread::sleep(1); setupJob->cancel(); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY2(!QFileInfo(lockFile).exists(), qPrintable(lockFile)); QVERIFY2(QFileInfo(newLockFile).isFile(), qPrintable(newLockFile)); setupJob.reset(nullptr); project3 = qbs::Project(); QVERIFY2(!QFileInfo(newLockFile).exists(), qPrintable(newLockFile)); } void TestApi::buildProject() { QFETCH(QString, projectSubDir); QFETCH(QString, productFileName); const QString projectFilePath = projectSubDir + QLatin1Char('/') + projectSubDir + QLatin1String(".qbs"); qbs::SetupProjectParameters params = defaultSetupParameters(projectFilePath); removeBuildDir(params); qbs::ErrorInfo errorInfo = doBuildProject(projectFilePath); VERIFY_NO_ERROR(errorInfo); QVERIFY(regularFileExists(relativeBuildGraphFilePath())); if (!productFileName.isEmpty()) { QVERIFY2(regularFileExists(productFileName), qPrintable(productFileName)); QVERIFY2(QFile::remove(productFileName), qPrintable(productFileName)); } WAIT_FOR_NEW_TIMESTAMP(); qbs::BuildOptions options; options.setForceTimestampCheck(true); errorInfo = doBuildProject(projectFilePath, 0, 0, 0, options); VERIFY_NO_ERROR(errorInfo); if (!productFileName.isEmpty()) QVERIFY2(regularFileExists(productFileName), qPrintable(productFileName)); QVERIFY(regularFileExists(relativeBuildGraphFilePath())); } void TestApi::buildProject_data() { QTest::addColumn("projectSubDir"); QTest::addColumn("productFileName"); QTest::newRow("BPs in Sources") << QString("build-properties-source") << relativeExecutableFilePath("HelloWorld"); QTest::newRow("code generator") << QString("codegen") << relativeExecutableFilePath("codegen"); QTest::newRow("link static libs") << QString("link-static-lib") << relativeExecutableFilePath("HelloWorld"); QTest::newRow("link staticlib dynamiclib") << QString("link-staticlib-dynamiclib") << relativeExecutableFilePath("app"); QTest::newRow("precompiled header new") << QString("precompiled-header-new") << relativeExecutableFilePath("MyApp"); QTest::newRow("precompiled header dynamic") << QString("precompiled-header-dynamic") << relativeExecutableFilePath("MyApp"); QTest::newRow("lots of dots") << QString("lots-of-dots") << relativeExecutableFilePath("lots.of.dots"); QTest::newRow("Qt5 plugin") << QString("qt5-plugin") << relativeProductBuildDir("echoplugin") + '/' + qbs::Internal::HostOsInfo::dynamicLibraryName("echoplugin"); QTest::newRow("Q_OBJECT in source") << QString("moc-cpp") << relativeExecutableFilePath("moc_cpp"); QTest::newRow("Q_OBJECT in header") << QString("moc-hpp") << relativeExecutableFilePath("moc_hpp"); QTest::newRow("Q_OBJECT in header, moc_XXX.cpp included") << QString("moc-hpp-included") << relativeExecutableFilePath("moc_hpp_included"); QTest::newRow("app and lib with same source file") << QString("lib-same-source") << relativeExecutableFilePath("HelloWorldApp"); QTest::newRow("source files with the same base name but different extensions") << QString("same-base-name") << relativeExecutableFilePath("basename"); QTest::newRow("static library dependencies") << QString("static-lib-deps") << relativeExecutableFilePath("staticLibDeps"); QTest::newRow("simple probes") << QString("simple-probe") << relativeExecutableFilePath("MyApp"); QTest::newRow("application without sources") << QString("app-without-sources") << relativeExecutableFilePath("appWithoutSources"); QTest::newRow("productNameWithDots") << QString("productNameWithDots") << relativeExecutableFilePath("myapp"); QTest::newRow("only default properties") << QString("two-default-property-values") << relativeProductBuildDir("two-default-property-values") + "/set"; QTest::newRow("Export item with Group") << QString("export-item-with-group") << relativeExecutableFilePath("app"); QTest::newRow("QBS-728") << QString("QBS-728") << QString(); } void TestApi::buildProjectDryRun() { QFETCH(QString, projectSubDir); QFETCH(QString, productFileName); const QString projectFilePath = projectSubDir + QLatin1Char('/') + projectSubDir + QLatin1String(".qbs"); qbs::SetupProjectParameters params = defaultSetupParameters(projectFilePath); removeBuildDir(params); qbs::BuildOptions options; options.setDryRun(true); const qbs::ErrorInfo errorInfo = doBuildProject(projectFilePath, 0, 0, 0, options); VERIFY_NO_ERROR(errorInfo); QVERIFY2(!QFileInfo::exists(relativeBuildDir()), qPrintable(QDir(relativeBuildDir()) .entryList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System).join(", "))); } void TestApi::buildProjectDryRun_data() { return buildProject_data(); } void TestApi::buildSingleFile() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("build-single-file"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); qbs::BuildOptions options; options.setFilesToConsider(QStringList(setupParams.buildRoot() + "/compiled.cpp")); options.setActiveFileTags(QStringList("obj")); m_logSink->setLogLevel(qbs::LoggerMaxLevel); QScopedPointer buildJob(project.buildAllProducts(options)); BuildDescriptionReceiver receiver; connect(buildJob.data(), &qbs::BuildJob::reportCommandDescription, &receiver, &BuildDescriptionReceiver::handleDescription); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QCOMPARE(receiver.descriptions.count("compiling"), 2); QCOMPARE(receiver.descriptions.count("precompiling"), 1); QVERIFY2(receiver.descriptions.contains("generating generated.h"), qPrintable(receiver.descriptions)); QVERIFY2(receiver.descriptions.contains("compiling compiled.cpp"), qPrintable(receiver.descriptions)); } void TestApi::canonicalToolchainList() { // All the known toolchain lists should be equal QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode", "clang", "llvm", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang", "llvm", "gcc"})), QStringList({"clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm", "gcc"})), QStringList({"llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"mingw", "gcc"})), QStringList({"mingw", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc"})), QStringList({"gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"msvc"})), QStringList({"msvc"})); // Single names should canonicalize to the known lists QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang"})), QStringList({"clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm"})), QStringList({"llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"mingw"})), QStringList({"mingw", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc"})), QStringList({"gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"msvc"})), QStringList({"msvc"})); // Missing some in the middle QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode", "llvm", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode", "clang", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang", "llvm"})), QStringList({"clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang", "gcc"})), QStringList({"clang", "llvm", "gcc"})); // Sorted wrong, missing some in the middle QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "llvm", "clang", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang", "gcc", "llvm", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm", "clang", "xcode", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "llvm", "clang"})), QStringList({"clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "clang", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "llvm"})), QStringList({"llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "mingw"})), QStringList({"mingw", "gcc"})); // Duplicates QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "llvm", "clang", "xcode", "xcode", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"clang", "gcc", "llvm", "clang", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm", "clang", "clang", "xcode", "xcode", "gcc"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm", "clang", "gcc", "llvm", "clang"})), QStringList({"clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"xcode", "gcc", "clang", "gcc", "clang", "xcode"})), QStringList({"xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"llvm", "gcc", "llvm", "llvm"})), QStringList({"llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain(QStringList({"gcc", "gcc", "gcc", "mingw"})), QStringList({"mingw", "gcc"})); // Custom insanity QCOMPARE(qbs::canonicalToolchain( QStringList({"crazy", "gcc", "llvm", "clang", "xcode", "insane"})), QStringList({"crazy", "insane", "xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain( QStringList({"crazy", "gcc", "llvm", "clang", "xcode", "insane", "crazy"})), QStringList({"crazy", "insane", "xcode", "clang", "llvm", "gcc"})); QCOMPARE(qbs::canonicalToolchain( QStringList({"crazy", "insane", "gcc", "trade", "llvm", "clang", "xcode", "insane", "mark", "crazy"})), QStringList({"crazy", "insane", "trade", "mark", "xcode", "clang", "llvm", "gcc"})); } void TestApi::checkOutputs() { QFETCH(bool, check); qbs::SetupProjectParameters params = defaultSetupParameters("/check-outputs"); qbs::BuildOptions options; options.setForceOutputCheck(check); removeBuildDir(params); qbs::ErrorInfo errorInfo = doBuildProject("/check-outputs", 0, 0, 0, options); if (check) QVERIFY(errorInfo.hasError()); else VERIFY_NO_ERROR(errorInfo); } void TestApi::checkOutputs_data() { QTest::addColumn("check"); QTest::newRow("checked outputs") << true; QTest::newRow("unchecked outputs") << false; } qbs::GroupData findGroup(const qbs::ProductData &product, const QString &name) { foreach (const qbs::GroupData &g, product.groups()) { if (g.name() == name) return g; } return qbs::GroupData(); } #ifdef QBS_ENABLE_PROJECT_FILE_UPDATES static qbs::Project::ProductSelection defaultProducts() { return qbs::Project::ProductSelectionDefaultOnly; } static void printProjectData(const qbs::ProjectData &project) { foreach (const qbs::ProductData &p, project.products()) { qDebug(" Product '%s' at %s", qPrintable(p.name()), qPrintable(p.location().toString())); foreach (const qbs::GroupData &g, p.groups()) { qDebug(" Group '%s' at %s", qPrintable(g.name()), qPrintable(g.location().toString())); qDebug(" Files: %s", qPrintable(g.allFilePaths().join(QLatin1String(", ")))); } } } void TestApi::changeContent() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("project-editing"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); qbs::Project project = job->project(); qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); qbs::ProductData product = projectData.allProducts().first(); QVERIFY(product.groups().count() >= 8); // Error handling: Invalid product. qbs::ErrorInfo errorInfo = project.addGroup(qbs::ProductData(), "blubb"); QVERIFY(errorInfo.hasError()); QVERIFY(errorInfo.toString().contains("invalid")); // Error handling: Empty group name. errorInfo = project.addGroup(product, QString()); QVERIFY(errorInfo.hasError()); QVERIFY(errorInfo.toString().contains("empty")); errorInfo = project.addGroup(product, "New Group 1"); VERIFY_NO_ERROR(errorInfo); errorInfo = project.addGroup(product, "New Group 2"); VERIFY_NO_ERROR(errorInfo); // Error handling: Group already inserted. errorInfo = project.addGroup(product, "New Group 1"); QVERIFY(errorInfo.hasError()); QVERIFY(errorInfo.toString().contains("already")); // Error handling: Add list of files with double entries. errorInfo = project.addFiles(product, qbs::GroupData(), QStringList() << "file.cpp" << "file.cpp"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("more than once"), qPrintable(errorInfo.toString())); // Add files to empty array literal. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); QVERIFY(product.groups().count() >= 10); qbs::GroupData group = findGroup(product, "New Group 1"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "file.h" << "file.cpp"); VERIFY_NO_ERROR(errorInfo); // Error handling: Add the same file again. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); QVERIFY(product.groups().count() >= 10); group = findGroup(product, "New Group 1"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "file.cpp"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("already"), qPrintable(errorInfo.toString())); // Remove one of the newly added files again. errorInfo = project.removeFiles(product, group, QStringList("file.h")); VERIFY_NO_ERROR(errorInfo); // Error handling: Try to remove the same file again. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); QVERIFY(product.groups().count() >= 10); group = findGroup(product, "New Group 1"); QVERIFY(group.isValid()); errorInfo = project.removeFiles(product, group, QStringList() << "file.h"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("not known"), qPrintable(errorInfo.toString())); // Error handling: Try to remove a file from a complex list. group = findGroup(product, "Existing Group 2"); QVERIFY(group.isValid()); errorInfo = project.removeFiles(product, group, QStringList() << "existingfile2.txt"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("complex"), qPrintable(errorInfo.toString())); // Remove file from product's 'files' binding. errorInfo = project.removeFiles(product, qbs::GroupData(), QStringList("main.cpp")); VERIFY_NO_ERROR(errorInfo); // Add file to non-empty array literal. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 1"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "newfile1.txt"); VERIFY_NO_ERROR(errorInfo); // Add files to list represented as a single string. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); errorInfo = project.addFiles(product, qbs::GroupData(), QStringList() << "newfile2.txt"); VERIFY_NO_ERROR(errorInfo); // Add files to list represented as an identifier. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 2"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "newfile3.txt"); VERIFY_NO_ERROR(errorInfo); // Add files to list represented as a block of code (not yet implemented). projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 3"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "newfile4.txt"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("complex"), qPrintable(errorInfo.toString())); // Add file to group with directory prefix. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 4"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "file.txt"); VERIFY_NO_ERROR(errorInfo); // Error handling: Add file to group with non-directory prefix. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 5"); QVERIFY(group.isValid()); errorInfo = project.addFiles(product, group, QStringList() << "newfile1.txt"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("prefix"), qPrintable(errorInfo.toString())); // Remove group. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Existing Group 5"); QVERIFY(group.isValid()); errorInfo = project.removeGroup(product, group); VERIFY_NO_ERROR(errorInfo); projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); QVERIFY(projectData.products().first().groups().count() >= 9); // Error handling: Try to remove the same group again. errorInfo = project.removeGroup(product, group); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("does not exist"), qPrintable(errorInfo.toString())); // Add a file to a group where the file name is already matched by a wildcard. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Group with wildcards"); QVERIFY(group.isValid()); QFile newFile("koerper.klaus"); QVERIFY2(newFile.open(QIODevice::WriteOnly), qPrintable(newFile.errorString())); newFile.close(); errorInfo = project.addFiles(product, group, QStringList() << newFile.fileName()); VERIFY_NO_ERROR(errorInfo); projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Group with wildcards"); QVERIFY(group.isValid()); QCOMPARE(group.sourceArtifactsFromWildcards().count(), 1); QCOMPARE(group.sourceArtifactsFromWildcards().first().filePath(), QFileInfo(newFile).absoluteFilePath()); // Error checking: Try to remove a file that originates from a wildcard pattern. projectData = project.projectData(); QVERIFY(projectData.products().count() == 1); product = projectData.products().first(); group = findGroup(product, "Other group with wildcards"); QVERIFY(group.isValid()); errorInfo = project.removeFiles(product, group, QStringList() << "test.wildcard"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("pattern"), qPrintable(errorInfo.toString())); // Check whether building will take the added and removed cpp files into account. // This must not be moved below the re-resolving test!!! qbs::BuildOptions buildOptions; buildOptions.setDryRun(true); BuildDescriptionReceiver rcvr; QScopedPointer buildJob(project.buildAllProducts(buildOptions, defaultProducts(), this)); connect(buildJob.data(), &qbs::BuildJob::reportCommandDescription, &rcvr, &BuildDescriptionReceiver::handleDescription); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QVERIFY(rcvr.descriptions.contains("compiling file.cpp")); QVERIFY(!rcvr.descriptions.contains("compiling main.cpp")); // Now check whether the data updates were done correctly. projectData = project.projectData(); job.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); project = job->project(); qbs::ProjectData newProjectData = project.projectData(); // Can't use Project::operator== here, as the target artifacts will differ due to the build // not having run yet. bool projectDataMatches = newProjectData.products().count() == 1 && projectData.products().count() == 1 && newProjectData.products().first().groups() == projectData.products().first().groups(); if (!projectDataMatches) { qDebug("This is the assumed project:"); printProjectData(projectData); qDebug("This is the actual project:"); printProjectData(newProjectData); } QVERIFY(projectDataMatches); // Will fail if e.g. code locations don't match. // Now try building again and check if the newly resolved product behaves the same way. buildJob.reset(project.buildAllProducts(buildOptions, defaultProducts(), this)); connect(buildJob.data(), &qbs::BuildJob::reportCommandDescription, &rcvr, &BuildDescriptionReceiver::handleDescription); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QVERIFY(rcvr.descriptions.contains("compiling file.cpp")); QVERIFY(!rcvr.descriptions.contains("compiling main.cpp")); // Now, after the build, the project data must be entirely identical. QVERIFY(projectData == project.projectData()); // Error handling: Try to change the project during a build. buildJob.reset(project.buildAllProducts(buildOptions, defaultProducts(), this)); errorInfo = project.addGroup(newProjectData.products().first(), "blubb"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("in process"), qPrintable(errorInfo.toString())); waitForFinished(buildJob.data()); errorInfo = project.addGroup(newProjectData.products().first(), "blubb"); VERIFY_NO_ERROR(errorInfo); project = qbs::Project(); job.reset(0); buildJob.reset(0); removeBuildDir(setupParams); // Add a file to the top level of a product that does not have a "files" binding yet. setupParams.setProjectFilePath(QDir::cleanPath(m_workingDataDir + "/project-editing/project-with-no-files.qbs")); job.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); project = job->project(); projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); product = projectData.allProducts().first(); errorInfo = project.addFiles(product, qbs::GroupData(), QStringList("main.cpp")); VERIFY_NO_ERROR(errorInfo); projectData = project.projectData(); rcvr.descriptions.clear(); buildJob.reset(project.buildAllProducts(buildOptions, defaultProducts(), this)); connect(buildJob.data(), &qbs::BuildJob::reportCommandDescription, &rcvr, &BuildDescriptionReceiver::handleDescription); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QVERIFY(rcvr.descriptions.contains("compiling main.cpp")); job.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); // Can't use Project::operator== here, as the target artifacts will differ due to the build // not having run yet. newProjectData = job->project().projectData(); projectDataMatches = newProjectData.products().count() == 1 && projectData.products().count() == 1 && newProjectData.products().first().groups() == projectData.products().first().groups(); if (!projectDataMatches) { printProjectData(projectData); qDebug("\n====\n"); printProjectData(newProjectData); } QVERIFY(projectDataMatches); } #endif // QBS_ENABLE_PROJECT_FILE_UPDATES void TestApi::commandExtraction() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("/command-extraction"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); qbs::ProductData productData = projectData.allProducts().first(); qbs::ErrorInfo errorInfo; const QString projectDirPath = QDir::cleanPath(QFileInfo(setupParams.projectFilePath()).path()); const QString sourceFilePath = projectDirPath + "/main.cpp"; // Before the first build, no rules exist. qbs::RuleCommandList commands = project.ruleCommands(productData, sourceFilePath, "obj", &errorInfo); QCOMPARE(commands.count(), 0); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("No rule"), qPrintable(errorInfo.toString())); qbs::BuildOptions options; options.setDryRun(true); QScopedPointer buildJob(project.buildAllProducts(options)); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); productData = projectData.allProducts().first(); errorInfo = qbs::ErrorInfo(); // After the build, the compile command must be found. commands = project.ruleCommands(productData, sourceFilePath, "obj", &errorInfo); QCOMPARE(commands.count(), 1); QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString())); const qbs::RuleCommand command = commands.first(); QCOMPARE(command.type(), qbs::RuleCommand::ProcessCommandType); QVERIFY(!command.executable().isEmpty()); QVERIFY(!command.arguments().isEmpty()); } void TestApi::changeDependentLib() { qbs::ErrorInfo errorInfo = doBuildProject("change-dependent-lib"); VERIFY_NO_ERROR(errorInfo); WAIT_FOR_NEW_TIMESTAMP(); const QString qbsFileName("change-dependent-lib.qbs"); QFile qbsFile(qbsFileName); QVERIFY(qbsFile.open(QIODevice::ReadWrite)); const QByteArray content1 = qbsFile.readAll(); QByteArray content2 = content1; content2.replace("cpp.defines: [\"XXXX\"]", "cpp.defines: [\"ABCD\"]"); QVERIFY(content1 != content2); qbsFile.seek(0); qbsFile.write(content2); qbsFile.close(); errorInfo = doBuildProject("change-dependent-lib"); VERIFY_NO_ERROR(errorInfo); } void TestApi::enableAndDisableProduct() { BuildDescriptionReceiver bdr; qbs::ErrorInfo errorInfo = doBuildProject("enable-and-disable-product", &bdr); VERIFY_NO_ERROR(errorInfo); QVERIFY(!bdr.descriptions.contains("compiling")); WAIT_FOR_NEW_TIMESTAMP(); QFile projectFile("enable-and-disable-product.qbs"); QVERIFY(projectFile.open(QIODevice::ReadWrite)); QByteArray content = projectFile.readAll(); content.replace("undefined", "'hidden'"); projectFile.resize(0); projectFile.write(content); projectFile.close(); bdr.descriptions.clear(); errorInfo = doBuildProject("enable-and-disable-product", &bdr); VERIFY_NO_ERROR(errorInfo); QVERIFY(bdr.descriptions.contains("linking")); WAIT_FOR_NEW_TIMESTAMP(); touch("main.cpp"); QVERIFY(projectFile.open(QIODevice::ReadWrite)); content = projectFile.readAll(); content.replace("'hidden'", "undefined"); projectFile.resize(0); projectFile.write(content); projectFile.close(); bdr.descriptions.clear(); errorInfo = doBuildProject("enable-and-disable-product", &bdr); VERIFY_NO_ERROR(errorInfo); QVERIFY(!bdr.descriptions.contains("compiling")); } void TestApi::errorInSetupRunEnvironment() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("error-in-setup-run-environment"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); const qbs::Project project = job->project(); QVERIFY(project.isValid()); QCOMPARE(project.projectData().products().count(), 1); const qbs::ProductData product = project.projectData().products().first(); bool exceptionCaught = false; try { qbs::Settings settings((QString())); qbs::RunEnvironment runEnv = project.getRunEnvironment(product, qbs::InstallOptions(), QProcessEnvironment(), &settings); qbs::ErrorInfo error; const QProcessEnvironment env = runEnv.runEnvironment(&error); QVERIFY(error.hasError()); QVERIFY(error.toString().contains("trallala")); } catch (const qbs::ErrorInfo &) { exceptionCaught = true; } QVERIFY(!exceptionCaught); } static qbs::ErrorInfo forceRuleEvaluation(const qbs::Project project) { qbs::BuildOptions buildOptions; buildOptions.setDryRun(true); QScopedPointer buildJob(project.buildAllProducts(buildOptions)); waitForFinished(buildJob.data()); return buildJob->error(); } void TestApi::disabledInstallGroup() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("disabled_install_group"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); const qbs::Project project = job->project(); const qbs::ErrorInfo errorInfo = forceRuleEvaluation(project); VERIFY_NO_ERROR(errorInfo); qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); qbs::ProductData product = projectData.allProducts().first(); const QList targets = product.targetArtifacts(); QCOMPARE(targets.count(), 1); QVERIFY(targets.first().isGenerated()); QVERIFY(targets.first().isExecutable()); QVERIFY(targets.first().isTargetArtifact()); QCOMPARE(projectData.installableArtifacts().count(), 0); QCOMPARE(product.targetExecutable(), targets.first().filePath()); } void TestApi::disabledProduct() { const qbs::ErrorInfo errorInfo = doBuildProject("disabled-product"); VERIFY_NO_ERROR(errorInfo); } void TestApi::disabledProject() { const qbs::ErrorInfo errorInfo = doBuildProject("disabled-project"); VERIFY_NO_ERROR(errorInfo); } void TestApi::duplicateProductNames() { QFETCH(QString, projectFileName); const qbs::ErrorInfo errorInfo = doBuildProject("duplicate-product-names/" + projectFileName); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("Duplicate product name"), qPrintable(errorInfo.toString())); } void TestApi::duplicateProductNames_data() { QTest::addColumn("projectFileName"); QTest::newRow("Names explicitly set") << QString("explicit.qbs"); QTest::newRow("Unnamed products in same file") << QString("implicit.qbs"); QTest::newRow("Unnamed products in files of the same name") << QString("implicit-indirect.qbs"); } void TestApi::emptyFileTagList() { const qbs::ErrorInfo errorInfo = doBuildProject("empty-filetag-list"); VERIFY_NO_ERROR(errorInfo); } void TestApi::emptySubmodulesList() { const qbs::ErrorInfo errorInfo = doBuildProject("empty-submodules-list"); VERIFY_NO_ERROR(errorInfo); } void TestApi::explicitlyDependsOn() { BuildDescriptionReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("explicitly-depends-on", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.descriptions.contains("Creating output artifact")); receiver.descriptions.clear(); errorInfo = doBuildProject("explicitly-depends-on", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(!receiver.descriptions.contains("Creating output artifact")); WAIT_FOR_NEW_TIMESTAMP(); touch("dependency.txt"); errorInfo = doBuildProject("explicitly-depends-on", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.descriptions.contains("Creating output artifact")); } void TestApi::exportSimple() { const qbs::ErrorInfo errorInfo = doBuildProject("export-simple"); VERIFY_NO_ERROR(errorInfo); } void TestApi::exportWithRecursiveDepends() { const qbs::ErrorInfo errorInfo = doBuildProject("export-with-recursive-depends"); VERIFY_NO_ERROR(errorInfo); } void TestApi::fileTagger() { BuildDescriptionReceiver receiver; const qbs::ErrorInfo errorInfo = doBuildProject("file-tagger/moc_cpp.qbs", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(receiver.descriptions.contains("moc bla.cpp"), qPrintable(receiver.descriptions)); } void TestApi::fileTagsFilterOverride() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("filetagsfilter_override"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); qbs::Project project = job->project(); const qbs::ErrorInfo errorInfo = forceRuleEvaluation(project); VERIFY_NO_ERROR(errorInfo); qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); const qbs::ProductData product = projectData.allProducts().first(); QList installableFiles = product.installableArtifacts(); QCOMPARE(installableFiles.count(), 1); QVERIFY(installableFiles.first().installData().installFilePath().contains("habicht")); } void TestApi::generatedFilesList() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("generated-files-list"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); QVERIFY(waitForFinished(setupJob.data())); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); qbs::BuildOptions options; options.setExecuteRulesOnly(true); const QScopedPointer buildJob(project.buildAllProducts(options)); QVERIFY(waitForFinished(buildJob.data())); VERIFY_NO_ERROR(buildJob->error()); const qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.products().count(), 1); const qbs::ProductData product = projectData.products().first(); QString uiFilePath; QVERIFY(product.generatedArtifacts().count() >= 6); foreach (const qbs::ArtifactData &a, product.generatedArtifacts()) { QVERIFY(a.isGenerated()); QFileInfo fi(a.filePath()); using qbs::Internal::HostOsInfo; const QStringList possibleFileNames = QStringList() << "main.cpp.o" << "main.cpp.obj" << "mainwindow.cpp.o" << "mainwindow.cpp.obj" << "moc_mainwindow.cpp" << "moc_mainwindow.cpp.o" << "moc_mainwindow.cpp.obj" << "ui_mainwindow.h" << HostOsInfo::appendExecutableSuffix("generated-files-list"); QVERIFY2(possibleFileNames.contains(fi.fileName()) || fi.fileName().endsWith(".plist"), qPrintable(fi.fileName())); } foreach (const qbs::GroupData &group, product.groups()) { foreach (const qbs::ArtifactData &a, group.sourceArtifacts()) { QVERIFY(!a.isGenerated()); QVERIFY(!a.isTargetArtifact()); if (a.fileTags().contains(QLatin1String("ui"))) { uiFilePath = a.filePath(); break; } } if (!uiFilePath.isEmpty()) break; } QVERIFY(!uiFilePath.isEmpty()); const QStringList directParents = project.generatedFiles(product, uiFilePath, false); QCOMPARE(directParents.count(), 1); const QFileInfo uiHeaderFileInfo(directParents.first()); QCOMPARE(uiHeaderFileInfo.fileName(), QLatin1String("ui_mainwindow.h")); QVERIFY(!uiHeaderFileInfo.exists()); const QStringList allParents = project.generatedFiles(product, uiFilePath, true); QCOMPARE(allParents.count(), 3); } void TestApi::infiniteLoopBuilding() { QFETCH(QString, projectDirName); qbs::SetupProjectParameters setupParams = defaultSetupParameters(projectDirName + "/infinite-loop.qbs"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); const QScopedPointer buildJob(project.buildAllProducts(qbs::BuildOptions())); QTimer::singleShot(1000, buildJob.data(), &qbs::AbstractJob::cancel); QVERIFY(waitForFinished(buildJob.data(), 600000)); QVERIFY(buildJob->error().hasError()); } void TestApi::infiniteLoopBuilding_data() { QTest::addColumn("projectDirName"); QTest::newRow("JS Command") << QString("infinite-loop-js"); QTest::newRow("Process Command") << QString("infinite-loop-process"); } void TestApi::infiniteLoopResolving() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("infinite-loop-resolving"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); QTimer::singleShot(1000, setupJob.data(), &qbs::AbstractJob::cancel); QVERIFY(waitForFinished(setupJob.data(), 600000)); QVERIFY2(setupJob->error().toString().toLower().contains("cancel"), qPrintable(setupJob->error().toString())); } void TestApi::inheritQbsSearchPaths() { const QString projectFilePath = "inherit-qbs-search-paths/prj.qbs"; qbs::ErrorInfo errorInfo = doBuildProject(projectFilePath); VERIFY_NO_ERROR(errorInfo); WAIT_FOR_NEW_TIMESTAMP(); QFile projectFile(m_workingDataDir + '/' + projectFilePath); QVERIFY(projectFile.open(QIODevice::ReadWrite)); QByteArray content = projectFile.readAll(); content.replace("qbsSearchPaths: \"subdir\"", "//qbsSearchPaths: \"subdir\""); projectFile.resize(0); projectFile.write(content); projectFile.close(); errorInfo = doBuildProject(projectFilePath); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("Dependency 'bli' not found"), qPrintable(errorInfo.toString())); QVariantMap overriddenValues; overriddenValues.insert("project.qbsSearchPaths", QStringList() << m_workingDataDir + "/inherit-qbs-search-paths/subdir"); errorInfo = doBuildProject(projectFilePath, 0, 0, 0, qbs::BuildOptions(), overriddenValues); VERIFY_NO_ERROR(errorInfo); } template T findElem(const QList &list, Pred p) { const auto it = std::find_if(list.constBegin(), list.constEnd(), p); return it == list.constEnd() ? T() : *it; } void TestApi::installableFiles() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("installed-artifact"); QVariantMap overriddenValues; overriddenValues.insert(QLatin1String("qbs.installRoot"), QLatin1String("/tmp")); setupParams.setOverriddenValues(overriddenValues); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); qbs::Project project = job->project(); const qbs::ErrorInfo errorInfo = forceRuleEvaluation(project); VERIFY_NO_ERROR(errorInfo); qbs::ProjectData projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 2); qbs::ProductData product = findElem(projectData.allProducts(), [](const qbs::ProductData &p) { return p.name() == QLatin1String("installedApp"); }); QVERIFY(product.isValid()); QList installableFiles = product.installableArtifacts(); QCOMPARE(installableFiles.count(), 2); foreach (const qbs::ArtifactData &f,installableFiles) { if (!f.filePath().endsWith("main.cpp")) { QVERIFY(f.isExecutable()); QString expectedTargetFilePath = qbs::Internal::HostOsInfo ::appendExecutableSuffix(QLatin1String("/tmp/usr/bin/installedApp")); QCOMPARE(f.installData().localInstallFilePath(), expectedTargetFilePath); QCOMPARE(product.targetExecutable(), expectedTargetFilePath); break; } } setupParams = defaultSetupParameters("recursive-wildcards"); setupParams.setOverriddenValues(overriddenValues); job.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); project = job->project(); projectData = project.projectData(); QCOMPARE(projectData.allProducts().count(), 1); product = projectData.allProducts().first(); installableFiles = product.installableArtifacts(); QCOMPARE(installableFiles.count(), 2); foreach (const qbs::ArtifactData &f, installableFiles) QVERIFY(!f.isExecutable()); QCOMPARE(installableFiles.first().installData().localInstallFilePath(), QLatin1String("/tmp/dir/file1.txt")); QCOMPARE(installableFiles.last().installData().localInstallFilePath(), QLatin1String("/tmp/dir/file2.txt")); } void TestApi::isRunnable() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("is-runnable"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); qbs::Project project = job->project(); const QList products = project.projectData().products(); QCOMPARE(products.count(), 2); foreach (const qbs::ProductData &p, products) { QVERIFY2(p.name() == "app" || p.name() == "lib", qPrintable(p.name())); if (p.name() == "app") QVERIFY(p.isRunnable()); else QVERIFY(!p.isRunnable()); } } void TestApi::linkDynamicLibs() { const qbs::ErrorInfo errorInfo = doBuildProject("link-dynamiclibs"); VERIFY_NO_ERROR(errorInfo); } void TestApi::linkDynamicAndStaticLibs() { BuildDescriptionReceiver bdr; qbs::BuildOptions options; options.setEchoMode(qbs::CommandEchoModeCommandLine); const qbs::ErrorInfo errorInfo = doBuildProject("link-dynamiclibs-staticlibs", &bdr, nullptr, nullptr, options); VERIFY_NO_ERROR(errorInfo); // The dependent static libs should not appear in the link command for the executable. qbs::Settings settings((QString())); const qbs::Profile buildProfile(profileName(), &settings); if (buildProfile.value("qbs.toolchain").toStringList().contains("gcc")) { QRegularExpression appLinkCmdRex(" -o [^ ]*/HelloWorld" QBS_HOST_EXE_SUFFIX " "); QString appLinkCmd; for (const QString &line : qAsConst(bdr.descriptionLines)) { if (line.contains(appLinkCmdRex)) { appLinkCmd = line; break; } } QVERIFY(!appLinkCmd.isEmpty()); QVERIFY(!appLinkCmd.contains("static1")); QVERIFY(!appLinkCmd.contains("static2")); } } void TestApi::linkStaticAndDynamicLibs() { BuildDescriptionReceiver bdr; qbs::BuildOptions options; options.setEchoMode(qbs::CommandEchoModeCommandLine); const qbs::ErrorInfo errorInfo = doBuildProject("link-staticlibs-dynamiclibs", &bdr, nullptr, nullptr, options); VERIFY_NO_ERROR(errorInfo); // The dependencies libdynamic1.so and libstatic2.a must not appear in the link command for the // executable. The -rpath-link line for libdynamic1.so must be there. qbs::Settings settings((QString())); const qbs::Profile buildProfile(profileName(), &settings); if (buildProfile.value("qbs.toolchain").toStringList().contains("gcc")) { QRegularExpression appLinkCmdRex(" -o [^ ]*/HelloWorld" QBS_HOST_EXE_SUFFIX " "); QString appLinkCmd; for (const QString &line : qAsConst(bdr.descriptionLines)) { if (line.contains(appLinkCmdRex)) { appLinkCmd = line; break; } } QVERIFY(!appLinkCmd.isEmpty()); const auto targetOs = buildProfile.value("qbs.targetOS").toStringList(); if (!targetOs.contains("darwin") && !targetOs.contains("windows")) { QRegularExpression rpathLinkRex(QString("-rpath-link=\\S*/") + relativeProductBuildDir("dynamic2")); QVERIFY(appLinkCmd.contains(rpathLinkRex)); } QVERIFY(!appLinkCmd.contains("libstatic2.a")); QVERIFY(!appLinkCmd.contains("libdynamic2.so")); } } void TestApi::listBuildSystemFiles() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("subprojects/toplevelproject.qbs"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); const auto buildSystemFiles = qbs::Internal::Set::fromStdSet( job->project().buildSystemFiles()); QVERIFY(buildSystemFiles.contains(setupParams.projectFilePath())); QVERIFY(buildSystemFiles.contains(setupParams.buildRoot() + "/subproject2/subproject2.qbs")); QVERIFY(buildSystemFiles.contains(setupParams.buildRoot() + "/subproject2/subproject3/subproject3.qbs")); } void TestApi::missingSourceFile() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("missing-source-file/missing-source-file.qbs"); setupParams.setProductErrorMode(qbs::ErrorHandlingMode::Relaxed); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); qbs::ProjectData project = job->project().projectData(); QCOMPARE(project.allProducts().count(), 1); qbs::ProductData product = project.allProducts().first(); QCOMPARE(product.groups().count(), 1); qbs::GroupData group = product.groups().first(); QCOMPARE(group.allSourceArtifacts().count(), 2); QFile::rename("file2.txt.missing", "file2.txt"); job.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); project = job->project().projectData(); QCOMPARE(project.allProducts().count(), 1); product = project.allProducts().first(); QCOMPARE(product.groups().count(), 1); group = product.groups().first(); QCOMPARE(group.allSourceArtifacts().count(), 3); } void TestApi::mocCppIncluded() { // Initial build. qbs::ErrorInfo errorInfo = doBuildProject("moc-hpp-included"); VERIFY_NO_ERROR(errorInfo); // Touch header and try again. WAIT_FOR_NEW_TIMESTAMP(); QFile headerFile("object.h"); QVERIFY2(headerFile.open(QIODevice::WriteOnly | QIODevice::Append), qPrintable(headerFile.errorString())); headerFile.write("\n"); headerFile.close(); errorInfo = doBuildProject("moc-hpp-included"); VERIFY_NO_ERROR(errorInfo); // Touch cpp file and try again. WAIT_FOR_NEW_TIMESTAMP(); QFile cppFile("object.cpp"); QVERIFY2(cppFile.open(QIODevice::WriteOnly | QIODevice::Append), qPrintable(cppFile.errorString())); cppFile.write("\n"); cppFile.close(); errorInfo = doBuildProject("moc-hpp-included"); VERIFY_NO_ERROR(errorInfo); } void TestApi::multiArch() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("multi-arch"); qbs::Settings settings((QString())); qbs::Internal::TemporaryProfile tph("host", &settings); qbs::Profile hostProfile = tph.p; hostProfile.setValue("qbs.architecture", "host-arch"); qbs::Internal::TemporaryProfile tpt("target", &settings); qbs::Profile targetProfile = tpt.p; targetProfile.setValue("qbs.architecture", "target-arch"); QVariantMap overriddenValues; overriddenValues.insert("project.hostProfile", hostProfile.name()); overriddenValues.insert("project.targetProfile", targetProfile.name()); setupParams.setOverriddenValues(overriddenValues); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); QCOMPARE(project.profile(), profileName()); const QList &products = project.projectData().products(); QCOMPARE(products.count(), 3); QList hostProducts; QList targetProducts; foreach (const qbs::ProductData &p, products) { QVERIFY2(p.profile() == hostProfile.name() || p.profile() == targetProfile.name(), qPrintable(p.profile())); if (p.profile() == hostProfile.name()) hostProducts << p; else targetProducts << p; } QCOMPARE(hostProducts.count(), 2); QCOMPARE(targetProducts.count(), 1); QCOMPARE(targetProducts.first().name(), QLatin1String("p1")); QStringList hostProductNames = QStringList() << hostProducts.first().name() << hostProducts.last().name(); QCOMPARE(hostProductNames.count("p1"), 1); QCOMPARE(hostProductNames.count("p2"), 1); QScopedPointer buildJob(project.buildAllProducts(qbs::BuildOptions())); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); const QString outputBaseDir = setupParams.buildRoot() + '/'; QFile p1HostArtifact(outputBaseDir + relativeProductBuildDir("p1", "host") + "/host+target.output"); QVERIFY2(p1HostArtifact.exists(), qPrintable(p1HostArtifact.fileName())); QVERIFY2(p1HostArtifact.open(QIODevice::ReadOnly), qPrintable(p1HostArtifact.errorString())); QCOMPARE(p1HostArtifact.readAll().constData(), "host-arch"); QFile p1TargetArtifact(outputBaseDir + relativeProductBuildDir("p1", "target") + "/host+target.output"); QVERIFY2(p1TargetArtifact.exists(), qPrintable(p1TargetArtifact.fileName())); QVERIFY2(p1TargetArtifact.open(QIODevice::ReadOnly), qPrintable(p1TargetArtifact.errorString())); QCOMPARE(p1TargetArtifact.readAll().constData(), "target-arch"); QFile p2Artifact(outputBaseDir + relativeProductBuildDir("p2", "host") + "/host-tool.output"); QVERIFY2(p2Artifact.exists(), qPrintable(p2Artifact.fileName())); QVERIFY2(p2Artifact.open(QIODevice::ReadOnly), qPrintable(p2Artifact.errorString())); QCOMPARE(p2Artifact.readAll().constData(), "host-arch"); const QString installRoot = outputBaseDir + relativeBuildDir() + '/' + qbs::InstallOptions::defaultInstallRoot(); QScopedPointer installJob(project.installAllProducts(qbs::InstallOptions())); waitForFinished(installJob.data()); QVERIFY2(!installJob->error().hasError(), qPrintable(installJob->error().toString())); QFile p1HostArtifactInstalled(installRoot + "/host/host+target.output"); QVERIFY2(p1HostArtifactInstalled.exists(), qPrintable(p1HostArtifactInstalled.fileName())); QFile p1TargetArtifactInstalled(installRoot + "/target/host+target.output"); QVERIFY2(p1TargetArtifactInstalled.exists(), qPrintable(p1TargetArtifactInstalled.fileName())); QFile p2ArtifactInstalled(installRoot + "/host/host-tool.output"); QVERIFY2(p2ArtifactInstalled.exists(), qPrintable(p2ArtifactInstalled.fileName())); // Error check: Try to build for the same profile twice. overriddenValues.insert("project.targetProfile", hostProfile.name()); setupParams.setOverriddenValues(overriddenValues); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY2(setupJob->error().toString().contains(hostProfile.name()) && setupJob->error().toString().contains("not allowed"), qPrintable(setupJob->error().toString())); // Error check: Try to build for the same profile twice, this time attaching // the properties via the product name. overriddenValues.clear(); overriddenValues.insert("products.p1.profiles", targetProfile.name() + ',' + targetProfile.name()); setupParams.setOverriddenValues(overriddenValues); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY2(setupJob->error().toString().contains(targetProfile.name()) && setupJob->error().toString().contains("not allowed"), qPrintable(setupJob->error().toString())); } void TestApi::newOutputArtifactInDependency() { BuildDescriptionReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("new-output-artifact-in-dependency", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.descriptions.contains("linking app")); const QByteArray linkingLibString = QByteArray("linking ") + qbs::Internal::HostOsInfo::dynamicLibraryName("lib").toLatin1(); QVERIFY(!receiver.descriptions.contains(linkingLibString)); receiver.descriptions.clear(); WAIT_FOR_NEW_TIMESTAMP(); QFile projectFile("new-output-artifact-in-dependency.qbs"); QVERIFY2(projectFile.open(QIODevice::ReadWrite), qPrintable(projectFile.errorString())); QByteArray contents = projectFile.readAll(); contents.replace("//Depends", "Depends"); projectFile.resize(0); projectFile.write(contents); projectFile.close(); errorInfo = doBuildProject("new-output-artifact-in-dependency", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.descriptions.contains("linking app")); QVERIFY(receiver.descriptions.contains(linkingLibString)); } void TestApi::newPatternMatch() { TaskReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("new-pattern-match", 0, 0, &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(receiver.taskDescriptions.contains("Resolving"), qPrintable(m_logSink->output)); receiver.taskDescriptions.clear(); errorInfo = doBuildProject("new-pattern-match", 0, 0, &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(!receiver.taskDescriptions.contains("Resolving")); WAIT_FOR_NEW_TIMESTAMP(); QFile f("test.txt"); QVERIFY2(f.open(QIODevice::WriteOnly), qPrintable(f.errorString())); f.close(); errorInfo = doBuildProject("new-pattern-match", 0, 0, &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.taskDescriptions.contains("Resolving")); receiver.taskDescriptions.clear(); errorInfo = doBuildProject("new-pattern-match", 0, 0, &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(!receiver.taskDescriptions.contains("Resolving")); WAIT_FOR_NEW_TIMESTAMP(); f.remove(); errorInfo = doBuildProject("new-pattern-match", 0, 0, &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY(receiver.taskDescriptions.contains("Resolving")); } void TestApi::nonexistingProjectPropertyFromProduct() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("nonexistingprojectproperties"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QEXPECT_FAIL("", "QBS-432", Abort); QVERIFY(job->error().hasError()); QVERIFY2(job->error().toString().contains(QLatin1String("blubb")), qPrintable(job->error().toString())); } void TestApi::nonexistingProjectPropertyFromCommandLine() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("nonexistingprojectproperties"); removeBuildDir(setupParams); QVariantMap projectProperties; projectProperties.insert(QLatin1String("project.blubb"), QLatin1String("true")); setupParams.setOverriddenValues(projectProperties); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY(job->error().hasError()); QVERIFY2(job->error().toString().contains(QLatin1String("blubb")), qPrintable(job->error().toString())); } void TestApi::objC() { const qbs::ErrorInfo errorInfo = doBuildProject("objc"); VERIFY_NO_ERROR(errorInfo); } void TestApi::projectDataAfterProductInvalidation() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("project-data-after-" "product-invalidation/project-data-after-product-invalidation.qbs"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); QVERIFY(project.isValid()); QCOMPARE(project.projectData().products().count(), 1); QVERIFY(project.projectData().products().first().generatedArtifacts().isEmpty()); QScopedPointer buildJob(project.buildAllProducts(qbs::BuildOptions())); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QCOMPARE(project.projectData().products().count(), 1); const qbs::ProductData productAfterBulding = project.projectData().products().first(); QVERIFY(!productAfterBulding.generatedArtifacts().isEmpty()); QFile projectFile(setupParams.projectFilePath()); WAIT_FOR_NEW_TIMESTAMP(); QVERIFY2(projectFile.open(QIODevice::ReadWrite), qPrintable(projectFile.errorString())); QByteArray content = projectFile.readAll(); QVERIFY(!content.isEmpty()); content.replace("\"file.cpp", "// \"file.cpp"); projectFile.resize(0); projectFile.write(content); projectFile.flush(); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); QVERIFY(!project.isValid()); project = setupJob->project(); QVERIFY(project.isValid()); QCOMPARE(project.projectData().products().count(), 1); QVERIFY(project.projectData().products().first().generatedArtifacts() == productAfterBulding.generatedArtifacts()); buildJob.reset(project.buildAllProducts(qbs::BuildOptions())); waitForFinished(buildJob.data()); QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString())); QCOMPARE(project.projectData().products().count(), 1); QVERIFY(project.projectData().products().first().generatedArtifacts() != productAfterBulding.generatedArtifacts()); } void TestApi::processResult() { // On Windows, even closed files seem to sometimes block the removal of their parent directories // for a while. if (qbs::Internal::HostOsInfo::isWindowsHost()) QTest::qWait(500); removeBuildDir(defaultSetupParameters("process-result")); QFETCH(int, expectedExitCode); QFETCH(bool, redirectStdout); QFETCH(bool, redirectStderr); QVariantMap overridden; overridden.insert("products.app-caller.argument", expectedExitCode); overridden.insert("products.app-caller.redirectStdout", redirectStdout); overridden.insert("products.app-caller.redirectStderr", redirectStderr); ProcessResultReceiver resultReceiver; const qbs::ErrorInfo errorInfo = doBuildProject("process-result", nullptr, &resultReceiver, nullptr, qbs::BuildOptions(), overridden); QCOMPARE(expectedExitCode != 0, errorInfo.hasError()); QVERIFY(resultReceiver.results.size() > 1); const qbs::ProcessResult &result = resultReceiver.results.back(); QVERIFY2(result.executableFilePath().contains("app"), qPrintable(result.executableFilePath())); QCOMPARE(expectedExitCode, result.exitCode()); QCOMPARE(expectedExitCode == 0, result.success()); QCOMPARE(result.error(), QProcess::UnknownError); struct CheckParams { CheckParams(bool r, const QString &f, const QByteArray &c, const QStringList &co) : redirect(r), fileName(f), expectedContent(c), consoleOutput(co) {} bool redirect; QString fileName; QByteArray expectedContent; const QStringList consoleOutput; }; const std::vector checkParams({ CheckParams(redirectStdout, "stdout.txt", "stdout", result.stdOut()), CheckParams(redirectStderr, "stderr.txt", "stderr", result.stdErr()) }); foreach (const CheckParams &p, checkParams) { QFile f(relativeProductBuildDir("app-caller") + '/' + p.fileName); QCOMPARE(f.exists(), p.redirect); if (p.redirect) { QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(f.errorString())); QCOMPARE(f.readAll(), p.expectedContent); QCOMPARE(p.consoleOutput, QStringList()); } else { QCOMPARE(p.consoleOutput.join("").toLocal8Bit(), p.expectedContent); } } } void TestApi::processResult_data() { QTest::addColumn("expectedExitCode"); QTest::addColumn("redirectStdout"); QTest::addColumn("redirectStderr"); QTest::newRow("success, no redirection") << 0 << false << false; QTest::newRow("success, stdout redirection") << 0 << true << false; QTest::newRow("failure, stderr redirection") << 1 << false << true; } void TestApi::projectInvalidation() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("project-invalidation"); QVERIFY(QFile::copy("project.no-error.qbs", "project-invalidation.qbs")); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); QVERIFY(project.isValid()); WAIT_FOR_NEW_TIMESTAMP(); copyFileAndUpdateTimestamp("project.early-error.qbs", "project-invalidation.qbs"); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY(project.isValid()); // Error in Loader, old project still valid. WAIT_FOR_NEW_TIMESTAMP(); copyFileAndUpdateTimestamp("project.late-error.qbs", "project-invalidation.qbs"); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY(setupJob->error().hasError()); QVERIFY(!project.isValid()); // Error in build data re-resolving, old project not valid anymore. } void TestApi::projectLocking() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("project-locking"); QScopedPointer setupJob(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); qbs::Project project = setupJob->project(); setupJob.reset(project.setupProject(setupParams, m_logSink, 0)); QScopedPointer setupJob2(project.setupProject(setupParams, m_logSink, 0)); waitForFinished(setupJob2.data()); QVERIFY(setupJob2->error().hasError()); QVERIFY2(setupJob2->error().toString() .contains("Cannot start a job while another one is in progress."), qPrintable(setupJob2->error().toString())); waitForFinished(setupJob.data()); QVERIFY2(!setupJob->error().hasError(), qPrintable(setupJob->error().toString())); } void TestApi::projectPropertiesByName() { const QString projectFile = "project-properties-by-name/project-properties-by-name.qbs"; qbs::ErrorInfo errorInfo = doBuildProject(projectFile); QVERIFY(errorInfo.hasError()); QVariantMap overridden; overridden.insert("project.theDefines", QStringList() << "SUB1" << "SUB2"); errorInfo = doBuildProject(projectFile, 0, 0, 0, qbs::BuildOptions(), overridden); QVERIFY(errorInfo.hasError()); overridden.clear(); overridden.insert("projects.subproject1.theDefines", QStringList() << "SUB1"); errorInfo = doBuildProject(projectFile, 0, 0, 0, qbs::BuildOptions(), overridden); QVERIFY(errorInfo.hasError()); overridden.insert("projects.subproject2.theDefines", QStringList() << "SUB2"); errorInfo = doBuildProject(projectFile, 0, 0, 0, qbs::BuildOptions(), overridden); VERIFY_NO_ERROR(errorInfo); } void TestApi::projectWithPropertiesItem() { const qbs::ErrorInfo errorInfo = doBuildProject("project-with-properties-item"); VERIFY_NO_ERROR(errorInfo); } void TestApi::propertiesBlocks() { const qbs::ErrorInfo errorInfo = doBuildProject("properties-blocks"); VERIFY_NO_ERROR(errorInfo); } void TestApi::rc() { BuildDescriptionReceiver bdr; ProcessResultReceiver prr; const qbs::ErrorInfo errorInfo = doBuildProject("rc", &bdr, &prr); if (errorInfo.hasError()) qDebug() << prr.output; VERIFY_NO_ERROR(errorInfo); const bool rcFileWasCompiled = bdr.descriptions.contains("compiling test.rc"); QCOMPARE(rcFileWasCompiled, qbs::Internal::HostOsInfo::isWindowsHost()); } void TestApi::referencedFileErrors() { QFETCH(bool, relaxedMode); qbs::SetupProjectParameters params = defaultSetupParameters("referenced-file-errors"); params.setDryRun(true); params.setProductErrorMode(relaxedMode ? qbs::ErrorHandlingMode::Relaxed : qbs::ErrorHandlingMode::Strict); QScopedPointer job(qbs::Project().setupProject(params, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(job->error().hasError() != relaxedMode, qPrintable(job->error().toString())); const qbs::Project project = job->project(); QCOMPARE(project.isValid(), relaxedMode); if (!relaxedMode) return; const QList products = project.projectData().allProducts(); QCOMPARE(products.count(), 5); foreach (const qbs::ProductData &p, products) QCOMPARE(p.isEnabled(), p.name() != "p5"); } void TestApi::referencedFileErrors_data() { QTest::addColumn("relaxedMode"); QTest::newRow("strict mode") << false; QTest::newRow("relaxed mode") << true; } qbs::SetupProjectParameters TestApi::defaultSetupParameters(const QString &projectFileOrDir) const { QFileInfo fi(m_workingDataDir + QLatin1Char('/') + projectFileOrDir); QString projectDirPath; QString projectFilePath; if (fi.isDir()) { projectDirPath = fi.absoluteFilePath(); projectFilePath = projectDirPath + QLatin1Char('/') + projectFileOrDir + QStringLiteral(".qbs"); } else { projectDirPath = fi.absolutePath(); projectFilePath = fi.absoluteFilePath(); } qbs::SetupProjectParameters setupParams; setupParams.setProjectFilePath(projectFilePath); setupParams.setPropertyCheckingMode(qbs::ErrorHandlingMode::Strict); QDir::setCurrent(projectDirPath); setupParams.setBuildRoot(projectDirPath); qbs::Settings settings((QString())); const qbs::Preferences prefs(&settings, profileName()); setupParams.setSearchPaths(prefs.searchPaths(QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/" QBS_RELATIVE_SEARCH_PATH)))); setupParams.setPluginPaths(prefs.pluginPaths(QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/" QBS_RELATIVE_PLUGINS_PATH)))); setupParams.setLibexecPath(QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/" QBS_RELATIVE_LIBEXEC_PATH))); setupParams.setTopLevelProfile(profileName()); setupParams.setConfigurationName(QStringLiteral("default")); return setupParams; } void TestApi::references() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("references/invalid1.qbs"); const QString projectDir = QDir::cleanPath(m_workingDataDir + "/references"); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY(job->error().hasError()); QString errorString = job->error().toString(); QVERIFY2(errorString.contains("does not contain"), qPrintable(errorString)); setupParams.setProjectFilePath(projectDir + QLatin1String("/invalid2.qbs")); job.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY(job->error().hasError()); errorString = job->error().toString(); QVERIFY2(errorString.contains("contains more than one"), qPrintable(errorString)); setupParams.setProjectFilePath(projectDir + QLatin1String("/valid.qbs")); job.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); const qbs::ProjectData topLevelProject = job->project().projectData(); QCOMPARE(topLevelProject.subProjects().count(), 1); const QString subProjectFileName = QFileInfo(topLevelProject.subProjects().first().location().filePath()).fileName(); QCOMPARE(subProjectFileName, QString("p.qbs")); } void TestApi::relaxedModeRecovery() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("relaxed-mode-recovery"); setupParams.setProductErrorMode(qbs::ErrorHandlingMode::Relaxed); setupParams.setPropertyCheckingMode(qbs::ErrorHandlingMode::Relaxed); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); if (m_logSink->warnings.count() != 4) { foreach (const qbs::ErrorInfo &error, m_logSink->warnings) qDebug() << error.toString(); } QCOMPARE(m_logSink->warnings.count(), 4); foreach (const qbs::ErrorInfo &error, m_logSink->warnings) { QVERIFY2(!error.toString().contains("ASSERT") && (error.toString().contains("Dependency 'blubb' not found") || error.toString().contains("Product 'p1' had errors and was disabled") || error.toString().contains("Product 'p2' had errors and was disabled")), qPrintable(error.toString())); } } void TestApi::renameProduct() { // Initial run. qbs::ErrorInfo errorInfo = doBuildProject("rename-product/rename.qbs"); VERIFY_NO_ERROR(errorInfo); // Rename lib and adapt Depends item. WAIT_FOR_NEW_TIMESTAMP(); QFile f("rename.qbs"); QVERIFY(f.open(QIODevice::ReadWrite)); QByteArray contents = f.readAll(); contents.replace("TheLib", "thelib"); f.resize(0); f.write(contents); f.close(); errorInfo = doBuildProject("rename-product/rename.qbs"); VERIFY_NO_ERROR(errorInfo); // Rename lib and don't adapt Depends item. WAIT_FOR_NEW_TIMESTAMP(); QVERIFY(f.open(QIODevice::ReadWrite)); contents = f.readAll(); const int libNameIndex = contents.lastIndexOf("thelib"); QVERIFY(libNameIndex != -1); contents.replace(libNameIndex, 6, "TheLib"); f.resize(0); f.write(contents); f.close(); errorInfo = doBuildProject("rename-product/rename.qbs"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("Dependency 'thelib' not found"), qPrintable(errorInfo.toString())); } void TestApi::renameTargetArtifact() { // Initial run. BuildDescriptionReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("rename-target-artifact/rename.qbs", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(receiver.descriptions.contains("compiling"), qPrintable(receiver.descriptions)); QCOMPARE(receiver.descriptions.count("linking"), 2); receiver.descriptions.clear(); // Rename library file name. WAIT_FOR_NEW_TIMESTAMP(); QFile f("rename.qbs"); QVERIFY(f.open(QIODevice::ReadWrite)); QByteArray contents = f.readAll(); contents.replace("the_lib", "TheLib"); f.resize(0); f.write(contents); f.close(); errorInfo = doBuildProject("rename-target-artifact/rename.qbs", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(!receiver.descriptions.contains("compiling"), qPrintable(receiver.descriptions)); QCOMPARE(receiver.descriptions.count("linking"), 2); } void TestApi::removeFileDependency() { qbs::ErrorInfo errorInfo = doBuildProject("remove-file-dependency/removeFileDependency.qbs"); VERIFY_NO_ERROR(errorInfo); QFile::remove("someheader.h"); ProcessResultReceiver receiver; errorInfo = doBuildProject("remove-file-dependency/removeFileDependency.qbs", 0, &receiver); QVERIFY(errorInfo.hasError()); QVERIFY2(receiver.output.contains("someheader.h"), qPrintable(receiver.output)); } void TestApi::resolveProject() { QFETCH(QString, projectSubDir); QFETCH(QString, productFileName); const qbs::SetupProjectParameters params = defaultSetupParameters(projectSubDir); removeBuildDir(params); const QScopedPointer setupJob(qbs::Project().setupProject(params, m_logSink, 0)); waitForFinished(setupJob.data()); VERIFY_NO_ERROR(setupJob->error()); QVERIFY2(!QFile::exists(productFileName), qPrintable(productFileName)); QVERIFY(regularFileExists(relativeBuildGraphFilePath())); } void TestApi::resolveProject_data() { return buildProject_data(); } void TestApi::resolveProjectDryRun() { QFETCH(QString, projectSubDir); QFETCH(QString, productFileName); qbs::SetupProjectParameters params = defaultSetupParameters(projectSubDir); params.setDryRun(true); removeBuildDir(params); const QScopedPointer setupJob(qbs::Project().setupProject(params, m_logSink, 0)); waitForFinished(setupJob.data()); VERIFY_NO_ERROR(setupJob->error()); QVERIFY2(!QFile::exists(productFileName), qPrintable(productFileName)); QVERIFY(!regularFileExists(relativeBuildGraphFilePath())); } void TestApi::resolveProjectDryRun_data() { return resolveProject_data(); } void TestApi::restoredWarnings() { qbs::SetupProjectParameters setupParams = defaultSetupParameters("restored-warnings"); setupParams.setPropertyCheckingMode(qbs::ErrorHandlingMode::Relaxed); setupParams.setProductErrorMode(qbs::ErrorHandlingMode::Relaxed); // Initial resolving: Errors are new. QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); job.reset(nullptr); QCOMPARE(m_logSink->warnings.toSet().count(), 2); foreach (const qbs::ErrorInfo &e, m_logSink->warnings) { const QString msg = e.toString(); QVERIFY2(msg.contains("Superfluous version") || msg.contains("Property 'blubb' is not declared"), qPrintable(msg)); } m_logSink->warnings.clear(); // Re-resolving with no changes: Errors come from the stored build graph. job.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); job.reset(nullptr); QCOMPARE(m_logSink->warnings.toSet().count(), 2); m_logSink->warnings.clear(); // Re-resolving with changes: Errors come from the re-resolving, stored ones must be suppressed. QVariantMap overridenValues; overridenValues.insert("products.theProduct.moreFiles", true); setupParams.setOverriddenValues(overridenValues); job.reset(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); job.reset(nullptr); QCOMPARE(m_logSink->warnings.toSet().count(), 3); // One more for the additional group foreach (const qbs::ErrorInfo &e, m_logSink->warnings) { const QString msg = e.toString(); QVERIFY2(msg.contains("Superfluous version") || msg.contains("Property 'blubb' is not declared") || msg.contains("blubb.cpp' does not exist"), qPrintable(msg)); } m_logSink->warnings.clear(); } void TestApi::ruleConflict() { const qbs::ErrorInfo errorInfo = doBuildProject("rule-conflict"); QVERIFY(errorInfo.hasError()); const QString errorString = errorInfo.toString(); QVERIFY2(errorString.contains("conflict") && errorString.contains("pch1.h") && errorString.contains("pch2.h"), qPrintable(errorString)); } void TestApi::softDependency() { const qbs::ErrorInfo errorInfo = doBuildProject("soft-dependency"); VERIFY_NO_ERROR(errorInfo); } void TestApi::sourceFileInBuildDir() { VERIFY_NO_ERROR(doBuildProject("source-file-in-build-dir")); qbs::SetupProjectParameters setupParams = defaultSetupParameters("source-file-in-build-dir"); const QString generatedFile = relativeProductBuildDir("theProduct") + "/generated.cpp"; QVERIFY2(regularFileExists(generatedFile), qPrintable(generatedFile)); QScopedPointer job(qbs::Project().setupProject(setupParams, m_logSink, 0)); waitForFinished(job.data()); QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString())); const qbs::ProjectData projectData = job->project().projectData(); QCOMPARE(projectData.allProducts().count(), 1); const qbs::ProductData product = projectData.allProducts().first(); QCOMPARE(product.profile(), profileName()); const qbs::GroupData group = findGroup(product, "the group"); QVERIFY(group.isValid()); QCOMPARE(group.allFilePaths().count(), 1); } void TestApi::subProjects() { const qbs::SetupProjectParameters params = defaultSetupParameters("subprojects/toplevelproject.qbs"); removeBuildDir(params); // Check all three types of subproject creation, plus property overrides. qbs::ErrorInfo errorInfo = doBuildProject("subprojects/toplevelproject.qbs"); VERIFY_NO_ERROR(errorInfo); // Disabling both the project with the dependency and the one with the dependent // should not cause an error. WAIT_FOR_NEW_TIMESTAMP(); QFile f(params.projectFilePath()); QVERIFY(f.open(QIODevice::ReadWrite)); QByteArray contents = f.readAll(); contents.replace("condition: true", "condition: false"); f.resize(0); f.write(contents); f.close(); f.setFileName(params.buildRoot() + "/subproject2/subproject2.qbs"); QVERIFY(f.open(QIODevice::ReadWrite)); contents = f.readAll(); contents.replace("condition: qbs.targetOS.length > 0", "condition: false"); f.resize(0); f.write(contents); f.close(); errorInfo = doBuildProject("subprojects/toplevelproject.qbs"); VERIFY_NO_ERROR(errorInfo); // Disabling the project with the dependency only is an error. // This tests also whether changes in sub-projects are detected. WAIT_FOR_NEW_TIMESTAMP(); f.setFileName(params.projectFilePath()); QVERIFY(f.open(QIODevice::ReadWrite)); contents = f.readAll(); contents.replace("condition: false", "condition: true"); f.resize(0); f.write(contents); f.close(); errorInfo = doBuildProject("subprojects/toplevelproject.qbs"); QVERIFY(errorInfo.hasError()); QVERIFY2(errorInfo.toString().contains("Dependency 'testLib' not found"), qPrintable(errorInfo.toString())); } void TestApi::trackAddQObjectHeader() { const qbs::SetupProjectParameters params = defaultSetupParameters("missing-qobject-header/missingheader.qbs"); QFile qbsFile(params.projectFilePath()); QVERIFY(qbsFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); qbsFile.write("import qbs.base 1.0\nCppApplication {\n Depends { name: 'Qt.core' }\n" " files: ['main.cpp', 'myobject.cpp']\n}"); qbsFile.close(); ProcessResultReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("missing-qobject-header/missingheader.qbs", 0, &receiver); QVERIFY(errorInfo.hasError()); QVERIFY2(isAboutUndefinedSymbols(receiver.output), qPrintable(receiver.output)); WAIT_FOR_NEW_TIMESTAMP(); QVERIFY(qbsFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); qbsFile.write("import qbs.base 1.0\nCppApplication {\n Depends { name: 'Qt.core' }\n" " files: ['main.cpp', 'myobject.cpp','myobject.h']\n}"); qbsFile.close(); errorInfo = doBuildProject("missing-qobject-header/missingheader.qbs"); VERIFY_NO_ERROR(errorInfo); } void TestApi::trackRemoveQObjectHeader() { const qbs::SetupProjectParameters params = defaultSetupParameters("missing-qobject-header/missingheader.qbs"); removeBuildDir(params); QFile qbsFile(params.projectFilePath()); QVERIFY(qbsFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); qbsFile.write("import qbs.base 1.0\nCppApplication {\n Depends { name: 'Qt.core' }\n" " files: ['main.cpp', 'myobject.cpp','myobject.h']\n}"); qbsFile.close(); qbs::ErrorInfo errorInfo = doBuildProject("missing-qobject-header/missingheader.qbs"); VERIFY_NO_ERROR(errorInfo); WAIT_FOR_NEW_TIMESTAMP(); QVERIFY(qbsFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); qbsFile.write("import qbs.base 1.0\nCppApplication {\n Depends { name: 'Qt.core' }\n" " files: ['main.cpp', 'myobject.cpp']\n}"); qbsFile.close(); ProcessResultReceiver receiver; errorInfo = doBuildProject("missing-qobject-header/missingheader.qbs", 0, &receiver); QVERIFY(errorInfo.hasError()); QVERIFY2(isAboutUndefinedSymbols(receiver.output), qPrintable(receiver.output)); } void TestApi::transformers() { const qbs::ErrorInfo errorInfo = doBuildProject("transformers/transformers.qbs"); VERIFY_NO_ERROR(errorInfo); } void TestApi::typeChange() { BuildDescriptionReceiver receiver; qbs::ErrorInfo errorInfo = doBuildProject("type-change", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(!receiver.descriptions.contains("compiling"), qPrintable(receiver.descriptions)); WAIT_FOR_NEW_TIMESTAMP(); QFile projectFile("type-change.qbs"); QVERIFY2(projectFile.open(QIODevice::ReadWrite), qPrintable(projectFile.errorString())); QByteArray content = projectFile.readAll(); content.replace("//", ""); projectFile.resize(0); projectFile.write(content); projectFile.close(); errorInfo = doBuildProject("type-change", &receiver); VERIFY_NO_ERROR(errorInfo); QVERIFY2(receiver.descriptions.contains("compiling"), qPrintable(receiver.descriptions)); } void TestApi::uic() { const qbs::ErrorInfo errorInfo = doBuildProject("uic"); VERIFY_NO_ERROR(errorInfo); } qbs::ErrorInfo TestApi::doBuildProject( const QString &projectFilePath, BuildDescriptionReceiver *buildDescriptionReceiver, ProcessResultReceiver *procResultReceiver, TaskReceiver *taskReceiver, const qbs::BuildOptions &options, const QVariantMap overriddenValues) { qbs::SetupProjectParameters params = defaultSetupParameters(projectFilePath); params.setOverriddenValues(overriddenValues); params.setDryRun(options.dryRun()); const QScopedPointer setupJob(qbs::Project().setupProject(params, m_logSink, 0)); if (taskReceiver) { connect(setupJob.data(), &qbs::AbstractJob::taskStarted, taskReceiver, &TaskReceiver::handleTaskStart); } waitForFinished(setupJob.data()); if (setupJob->error().hasError()) return setupJob->error(); const QScopedPointer buildJob(setupJob->project().buildAllProducts(options)); if (buildDescriptionReceiver) { connect(buildJob.data(), &qbs::BuildJob::reportCommandDescription, buildDescriptionReceiver, &BuildDescriptionReceiver::handleDescription); } if (procResultReceiver) { connect(buildJob.data(), &qbs::BuildJob::reportProcessResult, procResultReceiver, &ProcessResultReceiver::handleProcessResult); } waitForFinished(buildJob.data()); return buildJob->error(); } QTEST_MAIN(TestApi) #include "tst_api.moc"