1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Copyright (C) 2019 Jochen Ulrich <jochenulrich@t-online.de>
5 ** Contact: https://www.qt.io/licensing/
6 **
7 ** This file is part of Qbs.
8 **
9 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
10 ** Commercial License Usage
11 ** Licensees holding valid commercial Qt licenses may use this file in
12 ** accordance with the commercial license agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and The Qt Company. For licensing terms
15 ** and conditions see https://www.qt.io/terms-conditions. For further
16 ** information use the contact form at https://www.qt.io/contact-us.
17 **
18 ** GNU General Public License Usage
19 ** Alternatively, this file may be used under the terms of the GNU
20 ** General Public License version 3 as published by the Free Software
21 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
22 ** included in the packaging of this file. Please review the following
23 ** information to ensure the GNU General Public License requirements will
24 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25 **
26 ** $QT_END_LICENSE$
27 **
28 ****************************************************************************/
29 
30 #include "tst_blackbox.h"
31 
32 #include "../shared.h"
33 
34 #include <api/languageinfo.h>
35 #include <tools/hostosinfo.h>
36 #include <tools/installoptions.h>
37 #include <tools/preferences.h>
38 #include <tools/profile.h>
39 #include <tools/qttools.h>
40 #include <tools/settings.h>
41 #include <tools/shellutils.h>
42 #include <tools/stlutils.h>
43 #include <tools/version.h>
44 
45 #include <QtCore/qdebug.h>
46 #include <QtCore/qelapsedtimer.h>
47 #include <QtCore/qjsonarray.h>
48 #include <QtCore/qjsondocument.h>
49 #include <QtCore/qjsonobject.h>
50 #include <QtCore/qjsonvalue.h>
51 #include <QtCore/qlocale.h>
52 #include <QtCore/qregularexpression.h>
53 #include <QtCore/qsettings.h>
54 #include <QtCore/qtemporarydir.h>
55 #include <QtCore/qtemporaryfile.h>
56 
57 #include <algorithm>
58 #include <functional>
59 #include <regex>
60 #include <utility>
61 
62 #define WAIT_FOR_NEW_TIMESTAMP() waitForNewTimestamp(testDataDir)
63 
64 using qbs::Internal::HostOsInfo;
65 using qbs::Profile;
66 
67 class MacosTarHealer {
68 public:
MacosTarHealer()69     MacosTarHealer() {
70         if (HostOsInfo::hostOs() == HostOsInfo::HostOsMacos) {
71             // work around absurd tar behavior on macOS
72             qputenv("COPY_EXTENDED_ATTRIBUTES_DISABLE", "true");
73             qputenv("COPYFILE_DISABLE", "true");
74         }
75     }
76 
~MacosTarHealer()77     ~MacosTarHealer() {
78         if (HostOsInfo::hostOs() == HostOsInfo::HostOsMacos) {
79             qunsetenv("COPY_EXTENDED_ATTRIBUTES_DISABLE");
80             qunsetenv("COPYFILE_DISABLE");
81         }
82     }
83 };
84 
findCli(int * status)85 QMap<QString, QString> TestBlackbox::findCli(int *status)
86 {
87     QTemporaryDir temp;
88     QDir::setCurrent(testDataDir + "/find");
89     QbsRunParameters params = QStringList() << "-f" << "find-cli.qbs";
90     params.buildDirectory = temp.path();
91     const int res = runQbs(params);
92     if (status)
93         *status = res;
94     QFile file(temp.path() + "/" + relativeProductBuildDir("find-cli") + "/cli.json");
95     if (!file.open(QIODevice::ReadOnly))
96         return {};
97     const auto tools = QJsonDocument::fromJson(file.readAll()).toVariant().toMap();
98     return {{"path", QDir::fromNativeSeparators(tools["path"].toString())}};
99 }
100 
findNodejs(int * status)101 QMap<QString, QString> TestBlackbox::findNodejs(int *status)
102 {
103     QTemporaryDir temp;
104     QDir::setCurrent(testDataDir + "/find");
105     QbsRunParameters params = QStringList() << "-f" << "find-nodejs.qbs";
106     params.buildDirectory = temp.path();
107     const int res = runQbs(params);
108     if (status)
109         *status = res;
110     QFile file(temp.path() + "/" + relativeProductBuildDir("find-nodejs") + "/nodejs.json");
111     if (!file.open(QIODevice::ReadOnly))
112         return {};
113     const auto tools = QJsonDocument::fromJson(file.readAll()).toVariant().toMap();
114     return {{"node", QDir::fromNativeSeparators(tools["node"].toString())}};
115 }
116 
findTypeScript(int * status)117 QMap<QString, QString> TestBlackbox::findTypeScript(int *status)
118 {
119     QTemporaryDir temp;
120     QDir::setCurrent(testDataDir + "/find");
121     QbsRunParameters params = QStringList() << "-f" << "find-typescript.qbs";
122     params.buildDirectory = temp.path();
123     const int res = runQbs(params);
124     if (status)
125         *status = res;
126     QFile file(temp.path() + "/" + relativeProductBuildDir("find-typescript") + "/typescript.json");
127     if (!file.open(QIODevice::ReadOnly))
128         return {};
129     const auto tools = QJsonDocument::fromJson(file.readAll()).toVariant().toMap();
130     return {{"tsc", QDir::fromNativeSeparators(tools["tsc"].toString())}};
131 }
132 
findArchiver(const QString & fileName,int * status)133 QString TestBlackbox::findArchiver(const QString &fileName, int *status)
134 {
135     if (fileName == "jar")
136         return findJdkTools(status).value(fileName);
137 
138     QString binary = findExecutable(QStringList(fileName));
139     if (binary.isEmpty()) {
140         const SettingsPtr s = settings();
141         Profile p(profileName(), s.get());
142         binary = findExecutable(p.value("archiver.command").toStringList());
143     }
144     return binary;
145 }
146 
lexYaccExist()147 bool TestBlackbox::lexYaccExist()
148 {
149     return !findExecutable(QStringList("lex")).isEmpty()
150             && !findExecutable(QStringList("yacc")).isEmpty();
151 }
152 
bisonVersion()153 qbs::Version TestBlackbox::bisonVersion()
154 {
155     const auto yaccBinary = findExecutable(QStringList("yacc"));
156     QProcess process;
157     process.start(yaccBinary, QStringList() << "--version");
158     if (!process.waitForStarted())
159         return qbs::Version();
160     if (!process.waitForFinished())
161         return qbs::Version();
162     const auto processStdOut = process.readAllStandardOutput();
163     if (processStdOut.isEmpty())
164         return qbs::Version();
165     const auto line = processStdOut.split('\n')[0];
166     const auto words = line.split(' ');
167     if (words.empty())
168         return qbs::Version();
169     return qbs::Version::fromString(words.last());
170 }
171 
sevenZip()172 void TestBlackbox::sevenZip()
173 {
174     QDir::setCurrent(testDataDir + "/archiver");
175     QString binary = findArchiver("7z");
176     if (binary.isEmpty())
177         QSKIP("7zip not found");
178     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "modules.archiver.type:7zip")), 0);
179     const QString outputFile = relativeProductBuildDir("archivable") + "/archivable.7z";
180     QVERIFY2(regularFileExists(outputFile), qPrintable(outputFile));
181     QProcess listContents;
182     listContents.start(binary, QStringList() << "l" << outputFile);
183     QVERIFY2(listContents.waitForStarted(), qPrintable(listContents.errorString()));
184     QVERIFY2(listContents.waitForFinished(), qPrintable(listContents.errorString()));
185     QVERIFY2(listContents.exitCode() == 0, listContents.readAllStandardError().constData());
186     const QByteArray output = listContents.readAllStandardOutput();
187     QVERIFY2(output.contains("2 files"), output.constData());
188     QVERIFY2(output.contains("test.txt"), output.constData());
189     QVERIFY2(output.contains("archivable.qbs"), output.constData());
190 }
191 
sourceArtifactInInputsFromDependencies()192 void TestBlackbox::sourceArtifactInInputsFromDependencies()
193 {
194     QDir::setCurrent(testDataDir + "/source-artifact-in-inputs-from-dependencies");
195     QCOMPARE(runQbs(), 0);
196     QFile outFile(relativeProductBuildDir("p") + "/output.txt");
197     QVERIFY2(outFile.exists(), qPrintable(outFile.fileName()));
198     QVERIFY2(outFile.open(QIODevice::ReadOnly), qPrintable(outFile.errorString()));
199     const QByteArrayList output = outFile.readAll().trimmed().split('\n');
200     QCOMPARE(output.size(), 2);
201     bool header1Found = false;
202     bool header2Found = false;
203     for (const QByteArray &line : output) {
204         const QByteArray &path = line.trimmed();
205         if (path == "include1/header.h")
206             header1Found = true;
207         else if (path == "include2/header.h")
208             header2Found = true;
209     }
210     QVERIFY(header1Found);
211     QVERIFY(header2Found);
212 }
213 
staticLibWithoutSources()214 void TestBlackbox::staticLibWithoutSources()
215 {
216     QDir::setCurrent(testDataDir + "/static-lib-without-sources");
217     QCOMPARE(runQbs(), 0);
218 }
219 
suspiciousCalls()220 void TestBlackbox::suspiciousCalls()
221 {
222     const QString projectDir = testDataDir + "/suspicious-calls";
223     QDir::setCurrent(projectDir);
224     rmDirR(relativeBuildDir());
225     QFETCH(QString, projectFile);
226     QbsRunParameters params(QStringList() << "-f" << projectFile);
227     QCOMPARE(runQbs(params), 0);
228     QFETCH(QByteArray, expectedWarning);
229     QVERIFY2(m_qbsStderr.contains(expectedWarning), m_qbsStderr.constData());
230 }
231 
suspiciousCalls_data()232 void TestBlackbox::suspiciousCalls_data()
233 {
234     QTest::addColumn<QString>("projectFile");
235     QTest::addColumn<QByteArray>("expectedWarning");
236     QTest::newRow("File.copy() in Probe") << "copy-probe.qbs" << QByteArray();
237     QTest::newRow("File.copy() during evaluation") << "copy-eval.qbs" << QByteArray("File.copy()");
238     QTest::newRow("File.copy() in prepare script")
239             << "copy-prepare.qbs" << QByteArray("File.copy()");
240     QTest::newRow("File.copy() in command") << "copy-command.qbs" << QByteArray();
241     QTest::newRow("File.directoryEntries() in Probe") << "direntries-probe.qbs" << QByteArray();
242     QTest::newRow("File.directoryEntries() during evaluation")
243             << "direntries-eval.qbs" << QByteArray("File.directoryEntries()");
244     QTest::newRow("File.directoryEntries() in prepare script")
245             << "direntries-prepare.qbs" << QByteArray();
246     QTest::newRow("File.directoryEntries() in command") << "direntries-command.qbs" << QByteArray();
247 }
248 
systemIncludePaths()249 void TestBlackbox::systemIncludePaths()
250 {
251     const QString projectDir = testDataDir + "/system-include-paths";
252     QDir::setCurrent(projectDir);
253     QCOMPARE(runQbs(), 0);
254 }
255 
distributionIncludePaths()256 void TestBlackbox::distributionIncludePaths()
257 {
258     const QString projectDir = testDataDir + "/distribution-include-paths";
259     QDir::setCurrent(projectDir);
260     QCOMPARE(runQbs(), 0);
261 }
262 
tar()263 void TestBlackbox::tar()
264 {
265     if (HostOsInfo::hostOs() == HostOsInfo::HostOsWindows)
266         QSKIP("Beware of the msys tar");
267     MacosTarHealer tarHealer;
268     QDir::setCurrent(testDataDir + "/archiver");
269     rmDirR(relativeBuildDir());
270     QString binary = findArchiver("tar");
271     if (binary.isEmpty())
272         QSKIP("tar not found");
273     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "modules.archiver.type:tar")), 0);
274     const QString outputFile = relativeProductBuildDir("archivable") + "/archivable.tar.gz";
275     QVERIFY2(regularFileExists(outputFile), qPrintable(outputFile));
276     QProcess listContents;
277     listContents.start(binary, QStringList() << "tf" << outputFile);
278     QVERIFY2(listContents.waitForStarted(), qPrintable(listContents.errorString()));
279     QVERIFY2(listContents.waitForFinished(), qPrintable(listContents.errorString()));
280     QVERIFY2(listContents.exitCode() == 0, listContents.readAllStandardError().constData());
281     QFile listFile("list.txt");
282     QVERIFY2(listFile.open(QIODevice::ReadOnly), qPrintable(listFile.errorString()));
283     QCOMPARE(listContents.readAllStandardOutput(), listFile.readAll());
284 }
285 
textTemplate()286 void TestBlackbox::textTemplate()
287 {
288     QVERIFY(QDir::setCurrent(testDataDir + "/texttemplate"));
289     rmDirR(relativeBuildDir());
290     QCOMPARE(runQbs(), 0);
291     QString outputFilePath = relativeProductBuildDir("one") + "/output.txt";
292     QString expectedOutputFilePath = QFINDTESTDATA("expected/output.txt");
293     TEXT_FILE_COMPARE(outputFilePath, expectedOutputFilePath);
294     outputFilePath = relativeProductBuildDir("one") + "/lalala.txt";
295     expectedOutputFilePath = QFINDTESTDATA("expected/lalala.txt");
296     TEXT_FILE_COMPARE(outputFilePath, expectedOutputFilePath);
297 }
298 
sortedFileList(const QByteArray & ba)299 static QStringList sortedFileList(const QByteArray &ba)
300 {
301     auto list = QString::fromUtf8(ba).split(QRegularExpression("[\r\n]"), QBS_SKIP_EMPTY_PARTS);
302     std::sort(list.begin(), list.end());
303     return list;
304 }
305 
zip()306 void TestBlackbox::zip()
307 {
308     QFETCH(QString, binaryName);
309     int status = 0;
310     const QString binary = findArchiver(binaryName, &status);
311     QCOMPARE(status, 0);
312     if (binary.isEmpty())
313         QSKIP("zip tool not found");
314 
315     QDir::setCurrent(testDataDir + "/archiver");
316     rmDirR(relativeBuildDir());
317     QbsRunParameters params(QStringList()
318                             << "modules.archiver.type:zip" << "modules.archiver.command:" + binary);
319     QCOMPARE(runQbs(params), 0);
320     const QString outputFile = relativeProductBuildDir("archivable") + "/archivable.zip";
321     QVERIFY2(regularFileExists(outputFile), qPrintable(outputFile));
322     QProcess listContents;
323     if (binaryName == "zip") {
324         // zipinfo is part of Info-Zip
325         listContents.start("zipinfo", QStringList() << "-1" << outputFile);
326     } else {
327         listContents.start(binary, QStringList() << "tf" << outputFile);
328     }
329     QVERIFY2(listContents.waitForStarted(), qPrintable(listContents.errorString()));
330     QVERIFY2(listContents.waitForFinished(), qPrintable(listContents.errorString()));
331     QVERIFY2(listContents.exitCode() == 0, listContents.readAllStandardError().constData());
332     QFile listFile("list.txt");
333     QVERIFY2(listFile.open(QIODevice::ReadOnly), qPrintable(listFile.errorString()));
334     QCOMPARE(sortedFileList(listContents.readAllStandardOutput()),
335              sortedFileList(listFile.readAll()));
336 
337     // Make sure the module is still loaded when the java/jar fallback is not available
338     params.command = "resolve";
339     params.arguments << "modules.java.jdkPath:/blubb";
340     QCOMPARE(runQbs(params), 0);
341     QCOMPARE(runQbs(), 0);
342 }
343 
zip_data()344 void TestBlackbox::zip_data()
345 {
346     QTest::addColumn<QString>("binaryName");
347     QTest::newRow("zip") << "zip";
348     QTest::newRow("jar") << "jar";
349 }
350 
zipInvalid()351 void TestBlackbox::zipInvalid()
352 {
353     QDir::setCurrent(testDataDir + "/archiver");
354     rmDirR(relativeBuildDir());
355     QbsRunParameters params(QStringList() << "modules.archiver.type:zip"
356                             << "modules.archiver.command:/bin/something");
357     params.expectFailure = true;
358     QVERIFY(runQbs(params) != 0);
359     QVERIFY2(m_qbsStderr.contains("unrecognized archive tool: 'something'"), m_qbsStderr.constData());
360 }
361 
TestBlackbox()362 TestBlackbox::TestBlackbox() : TestBlackboxBase (SRCDIR "/testdata", "blackbox")
363 {
364 }
365 
allowedValues()366 void TestBlackbox::allowedValues()
367 {
368     QFETCH(QString, property);
369     QFETCH(QString, value);
370     QFETCH(QString, invalidValue);
371 
372     QDir::setCurrent(testDataDir + "/allowed-values");
373     rmDirR(relativeBuildDir());
374 
375     QbsRunParameters params;
376     if (!property.isEmpty() && !value.isEmpty()) {
377         params.arguments << QStringLiteral("%1:%2").arg(property, value);
378     }
379 
380     params.expectFailure = !invalidValue.isEmpty();
381     QCOMPARE(runQbs(params) == 0, !params.expectFailure);
382     if (params.expectFailure) {
383         const auto errorString =
384                 QStringLiteral("Value '%1' is not allowed for property").arg(invalidValue);
385         QVERIFY2(m_qbsStderr.contains(errorString.toUtf8()), m_qbsStderr.constData());
386     }
387 }
388 
allowedValues_data()389 void TestBlackbox::allowedValues_data()
390 {
391     QTest::addColumn<QString>("property");
392     QTest::addColumn<QString>("value");
393     QTest::addColumn<QString>("invalidValue");
394 
395     QTest::newRow("default") << QString() << QString() << QString();
396 
397     QTest::newRow("allowed (product, CLI)") << "products.p.prop" << "foo" << QString();
398     QTest::newRow("not allowed (product, CLI)") << "products.p.prop" << "bar" << "bar";
399     QTest::newRow("allowed (product, JS)") << "products.p.prop2" << "foo" << QString();
400     QTest::newRow("not allowed (product, JS)") << "products.p.prop2" << "bar" << "bar";
401 
402     QTest::newRow("allowed single (module, CLI)") << "modules.a.prop" << "foo" << QString();
403     QTest::newRow("not allowed single (module, CLI)") << "modules.a.prop" << "baz" << "baz";
404     QTest::newRow("allowed mult (module, CLI)") << "modules.a.prop" << "foo,bar" << QString();
405     QTest::newRow("not allowed mult (module, CLI)") << "modules.a.prop" << "foo,baz" << "baz";
406 
407     QTest::newRow("allowed single (module, JS)") << "modules.a.prop2" << "foo" << QString();
408     QTest::newRow("not allowed single (module, JS)") << "modules.a.prop2" << "baz" << "baz";
409     QTest::newRow("allowed mult (module, JS)") << "modules.a.prop2" << "foo,bar" << QString();
410     QTest::newRow("not allowed mult (module, JS)") << "modules.a.prop2" << "foo,baz" << "baz";
411 
412     // undefined should always be allowed
413     QTest::newRow("undefined (product)") << "products.p.prop" << "undefined" << QString();
414     QTest::newRow("undefined (module)") << "modules.a.prop" << "undefined" << QString();
415 }
416 
addFileTagToGeneratedArtifact()417 void TestBlackbox::addFileTagToGeneratedArtifact()
418 {
419     QDir::setCurrent(testDataDir + "/add-filetag-to-generated-artifact");
420     QCOMPARE(runQbs(QStringList("project.enableTagging:true")), 0);
421     QVERIFY2(m_qbsStdout.contains("compressing my_app"), m_qbsStdout.constData());
422     const QString compressedAppFilePath
423             = relativeProductBuildDir("my_compressed_app") + '/'
424             + qbs::Internal::HostOsInfo::appendExecutableSuffix("compressed-my_app");
425     QVERIFY(regularFileExists(compressedAppFilePath));
426     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("project.enableTagging:false"))), 0);
427     QCOMPARE(runQbs(), 0);
428     QVERIFY(!regularFileExists(compressedAppFilePath));
429 }
430 
alwaysRun()431 void TestBlackbox::alwaysRun()
432 {
433     QFETCH(QString, projectFile);
434 
435     QDir::setCurrent(testDataDir + "/always-run");
436     rmDirR(relativeBuildDir());
437     QbsRunParameters params("build", QStringList() << "-f" << projectFile);
438     if (projectFile.contains("transformer")) {
439         params.expectFailure = true;
440         QVERIFY(runQbs(params) != 0);
441         QVERIFY2(m_qbsStderr.contains("removed"), m_qbsStderr.constData());
442         return;
443     }
444     QCOMPARE(runQbs(params), 0);
445     QVERIFY(m_qbsStdout.contains("yo"));
446     QCOMPARE(runQbs(params), 0);
447     QVERIFY(!m_qbsStdout.contains("yo"));
448     WAIT_FOR_NEW_TIMESTAMP();
449     REPLACE_IN_FILE(projectFile, "alwaysRun: false", "alwaysRun: true");
450 
451     QCOMPARE(runQbs(params), 0);
452     QVERIFY(m_qbsStdout.contains("yo"));
453     QCOMPARE(runQbs(params), 0);
454     QVERIFY(m_qbsStdout.contains("yo"));
455 }
456 
alwaysRun_data()457 void TestBlackbox::alwaysRun_data()
458 {
459     QTest::addColumn<QString>("projectFile");
460     QTest::newRow("Transformer") << "transformer.qbs";
461     QTest::newRow("Rule") << "rule.qbs";
462 }
463 
artifactsMapChangeTracking()464 void TestBlackbox::artifactsMapChangeTracking()
465 {
466     QDir::setCurrent(testDataDir + "/artifacts-map-change-tracking");
467     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
468     QVERIFY2(m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
469     QVERIFY2(m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
470     QVERIFY2(m_qbsStdout.contains("linking"), m_qbsStdout.constData());
471     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
472     QVERIFY2(m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
473     QVERIFY2(!m_qbsStdout.contains("test.txt"), m_qbsStdout.constData());
474     QVERIFY2(m_qbsStdout.contains("main.cpp"), m_qbsStdout.constData());
475     QVERIFY2(m_qbsStdout.contains("test.cpp"), m_qbsStdout.constData());
476     QVERIFY2(m_qbsStdout.contains("TheBinary"), m_qbsStdout.constData());
477     QVERIFY2(m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
478     QVERIFY2(m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
479 
480     // Change name of target binary. Command must be re-run, because the file name of an
481     // artifact changed.
482     WAIT_FOR_NEW_TIMESTAMP();
483     const QString projectFile("artifacts-map-change-tracking.qbs");
484     REPLACE_IN_FILE(projectFile, "TheBinary", "TheNewBinary");
485     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
486 
487     QVERIFY2(!m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
488     QVERIFY2(!m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
489     QVERIFY2(m_qbsStdout.contains("linking"), m_qbsStdout.constData());
490     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
491     QVERIFY2(m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
492     QVERIFY2(!m_qbsStdout.contains("test.txt"), m_qbsStdout.constData());
493     QVERIFY2(m_qbsStdout.contains("main.cpp"), m_qbsStdout.constData());
494     QVERIFY2(m_qbsStdout.contains("test.cpp"), m_qbsStdout.constData());
495     QVERIFY2(m_qbsStdout.contains("TheNewBinary"), m_qbsStdout.constData());
496     QVERIFY2(!m_qbsStdout.contains("TheBinary"), m_qbsStdout.constData());
497     QVERIFY2(!m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
498     QVERIFY2(!m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
499 
500     // Add file tag to generated artifact. Command must be re-run, because it enumerates the keys
501     // of the artifacts map.
502     WAIT_FOR_NEW_TIMESTAMP();
503     REPLACE_IN_FILE(projectFile, "fileTags: 'cpp'", "fileTags: ['cpp', 'blubb']");
504     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
505     QVERIFY2(m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
506     QVERIFY2(!m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
507     QVERIFY2(!m_qbsStdout.contains("linking"), m_qbsStdout.constData());
508     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
509     QVERIFY2(m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
510     QVERIFY2(!m_qbsStdout.contains("test.txt"), m_qbsStdout.constData());
511     QVERIFY2(m_qbsStdout.contains("main.cpp"), m_qbsStdout.constData());
512     QVERIFY2(m_qbsStdout.contains("test.cpp"), m_qbsStdout.constData());
513     QVERIFY2(m_qbsStdout.contains("TheNewBinary"), m_qbsStdout.constData());
514     QVERIFY2(!m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
515     QVERIFY2(!m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
516 
517     // Add redundant file tag to generated artifact. Command must not be re-run, because
518     // the artifacts map has not changed.
519     WAIT_FOR_NEW_TIMESTAMP();
520     REPLACE_IN_FILE(projectFile, "fileTags: ['cpp', 'blubb']",
521                     "fileTags: ['cpp', 'blubb', 'blubb']");
522     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
523     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
524     QVERIFY2(!m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
525     QVERIFY2(!m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
526     QVERIFY2(!m_qbsStdout.contains("linking"), m_qbsStdout.constData());
527     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
528     QVERIFY2(!m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
529     QVERIFY2(!m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
530     QVERIFY2(!m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
531 
532     // Rebuild the app. Command must not be re-run, because the artifacts map has not changed.
533     WAIT_FOR_NEW_TIMESTAMP();
534     touch("main.cpp");
535     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
536     QVERIFY2(!m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
537     QVERIFY2(!m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
538     QVERIFY2(m_qbsStdout.contains("linking"), m_qbsStdout.constData());
539     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
540     QVERIFY2(!m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
541     QVERIFY2(!m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
542     QVERIFY2(!m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
543 
544     // Add source file to app. Command must be re-run, because the artifacts map has changed.
545     WAIT_FOR_NEW_TIMESTAMP();
546     REPLACE_IN_FILE(projectFile, "/* 'test.txt' */", "'test.txt'");
547     QCOMPARE(runQbs(QStringList{"-p", "TheApp"}), 0);
548     QEXPECT_FAIL("", "change tracking could become even more fine-grained", Continue);
549     QVERIFY2(!m_qbsStdout.contains("running rule for test.cpp"), m_qbsStdout.constData());
550     QVERIFY2(!m_qbsStdout.contains("creating test.cpp"), m_qbsStdout.constData());
551     QVERIFY2(!m_qbsStdout.contains("linking"), m_qbsStdout.constData());
552     QCOMPARE(runQbs(QStringList{"-p", "meta,p"}), 0);
553     QVERIFY2(m_qbsStdout.contains("printing artifacts"), m_qbsStdout.constData());
554     QVERIFY2(m_qbsStdout.contains("test.txt"), m_qbsStdout.constData());
555     QVERIFY2(m_qbsStdout.contains("main.cpp"), m_qbsStdout.constData());
556     QVERIFY2(m_qbsStdout.contains("test.cpp"), m_qbsStdout.constData());
557     QVERIFY2(m_qbsStdout.contains("TheNewBinary"), m_qbsStdout.constData());
558     QVERIFY2(!m_qbsStdout.contains("dummy1"), m_qbsStdout.constData());
559     QVERIFY2(!m_qbsStdout.contains("dummy2"), m_qbsStdout.constData());
560 }
561 
artifactsMapInvalidation()562 void TestBlackbox::artifactsMapInvalidation()
563 {
564     const QString projectDir = testDataDir + "/artifacts-map-invalidation";
565     QDir::setCurrent(projectDir);
566     QCOMPARE(runQbs(), 0);
567     TEXT_FILE_COMPARE(relativeProductBuildDir("p") + "/myfile.out", "file.in");
568 }
569 
artifactsMapRaceCondition()570 void TestBlackbox::artifactsMapRaceCondition()
571 {
572     QDir::setCurrent(testDataDir + "/artifacts-map-race-condition");
573     QCOMPARE(runQbs(), 0);
574 }
575 
artifactScanning()576 void TestBlackbox::artifactScanning()
577 {
578     const QString projectDir = testDataDir + "/artifact-scanning";
579     QDir::setCurrent(projectDir);
580     QbsRunParameters params(QStringList("-vv"));
581 
582     QCOMPARE(runQbs(params), 0);
583     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 1);
584     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 1);
585     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 1);
586     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 1);
587     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 1);
588     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 1);
589     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 1);
590     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
591 
592     WAIT_FOR_NEW_TIMESTAMP();
593     touch("p1.cpp");
594     QCOMPARE(runQbs(params), 0);
595     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 1);
596     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
597     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
598     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
599     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
600     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
601     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
602     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
603 
604     WAIT_FOR_NEW_TIMESTAMP();
605     touch("p2.cpp");
606     QCOMPARE(runQbs(params), 0);
607     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
608     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 1);
609     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
610     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
611     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
612     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
613     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
614     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
615 
616     WAIT_FOR_NEW_TIMESTAMP();
617     touch("p3.cpp");
618     QCOMPARE(runQbs(params), 0);
619     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
620     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
621     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 1);
622     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
623     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
624     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
625     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
626     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
627 
628     WAIT_FOR_NEW_TIMESTAMP();
629     touch("shared.h");
630     QCOMPARE(runQbs(params), 0);
631     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
632     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
633     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
634     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 1);
635     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
636     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
637     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
638     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
639 
640     WAIT_FOR_NEW_TIMESTAMP();
641     touch("external.h");
642     QCOMPARE(runQbs(params), 0);
643     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
644     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
645     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
646     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
647     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 1);
648     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
649     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
650     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
651 
652     WAIT_FOR_NEW_TIMESTAMP();
653     touch("subdir/external2.h");
654     QCOMPARE(runQbs(params), 0);
655     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
656     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
657     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
658     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
659     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
660     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 1);
661     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
662     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
663 
664     WAIT_FOR_NEW_TIMESTAMP();
665     touch("external-indirect.h");
666     QCOMPARE(runQbs(params), 0);
667     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 0);
668     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
669     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
670     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
671     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
672     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
673     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 1);
674     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 0);
675 
676     WAIT_FOR_NEW_TIMESTAMP();
677     touch("p1.cpp");
678     params.command = "resolve";
679     params.arguments << "modules.cpp.treatSystemHeadersAsDependencies:true";
680     QCOMPARE(runQbs(params), 0);
681     params.command = "build";
682     QCOMPARE(runQbs(params), 0);
683     QCOMPARE(m_qbsStderr.count("scanning \"p1.cpp\""), 1);
684     QCOMPARE(m_qbsStderr.count("scanning \"p2.cpp\""), 0);
685     QCOMPARE(m_qbsStderr.count("scanning \"p3.cpp\""), 0);
686     QCOMPARE(m_qbsStderr.count("scanning \"shared.h\""), 0);
687     QCOMPARE(m_qbsStderr.count("scanning \"external.h\""), 0);
688     QCOMPARE(m_qbsStderr.count("scanning \"external2.h\""), 0);
689     QCOMPARE(m_qbsStderr.count("scanning \"external-indirect.h\""), 0);
690     QCOMPARE(m_qbsStderr.count("scanning \"iostream\""), 1);
691 }
692 
buildDirectories()693 void TestBlackbox::buildDirectories()
694 {
695     const QString projectDir
696             = QDir::cleanPath(testDataDir + QLatin1String("/build-directories"));
697     const QString projectBuildDir = projectDir + '/' + relativeBuildDir();
698     QDir::setCurrent(projectDir);
699     QCOMPARE(runQbs(), 0);
700     const QStringList outputLines
701             = QString::fromLocal8Bit(m_qbsStdout.trimmed()).split('\n', QBS_SKIP_EMPTY_PARTS);
702     QVERIFY2(outputLines.contains(projectDir + '/' + relativeProductBuildDir("p1")),
703              m_qbsStdout.constData());
704     QVERIFY2(outputLines.contains(projectDir + '/' + relativeProductBuildDir("p2")),
705              m_qbsStdout.constData());
706     QVERIFY2(outputLines.contains(projectBuildDir), m_qbsStdout.constData());
707     QVERIFY2(outputLines.contains(projectDir), m_qbsStdout.constData());
708 }
709 
buildEnvChange()710 void TestBlackbox::buildEnvChange()
711 {
712     QDir::setCurrent(testDataDir + "/buildenv-change");
713     QbsRunParameters params;
714     params.expectFailure = true;
715     params.arguments << "-k";
716     QVERIFY(runQbs(params) != 0);
717     const bool isMsvc = m_qbsStdout.contains("msvc");
718     QVERIFY2(m_qbsStdout.contains("compiling file.c"), m_qbsStdout.constData());
719     QString includePath = QDir::currentPath() + "/subdir";
720     params.environment.insert("CPLUS_INCLUDE_PATH", includePath);
721     params.environment.insert("CL", "/I" + includePath);
722     QVERIFY(runQbs(params) != 0);
723     params.command = "resolve";
724     params.expectFailure = false;
725     params.arguments.clear();
726     QCOMPARE(runQbs(params), 0);
727     QCOMPARE(runQbs(), 0);
728     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
729     QCOMPARE(m_qbsStdout.contains("compiling file.c"), isMsvc);
730     includePath = QDir::currentPath() + "/subdir2";
731     params.environment.insert("CPLUS_INCLUDE_PATH", includePath);
732     params.environment.insert("CL", "/I" + includePath);
733     QCOMPARE(runQbs(params), 0);
734     QCOMPARE(runQbs(), 0);
735     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
736     QCOMPARE(m_qbsStdout.contains("compiling file.c"), isMsvc);
737     params.environment = QProcessEnvironment::systemEnvironment();
738     QCOMPARE(runQbs(params), 0);
739     params.command = "build";
740     params.expectFailure = true;
741     QVERIFY(runQbs(params) != 0);
742 }
743 
buildGraphVersions()744 void TestBlackbox::buildGraphVersions()
745 {
746     QDir::setCurrent(testDataDir + "/build-graph-versions");
747     QCOMPARE(runQbs(), 0);
748     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
749     QFile bgFile(relativeBuildGraphFilePath());
750     QVERIFY2(bgFile.open(QIODevice::ReadWrite), qPrintable(bgFile.errorString()));
751     bgFile.write("blubb");
752     bgFile.close();
753 
754     // The first attempt at simple rebuilding as well as subsequent ones must fail.
755     QbsRunParameters params;
756     params.expectFailure = true;
757     QVERIFY(runQbs(params) != 0);
758     QVERIFY2(m_qbsStderr.contains("Cannot use stored build graph"), m_qbsStderr.constData());
759     QVERIFY2(m_qbsStderr.contains("Use the 'resolve' command"), m_qbsStderr.constData());
760     QVERIFY(runQbs(params) != 0);
761     QVERIFY2(m_qbsStderr.contains("Cannot use stored build graph"), m_qbsStderr.constData());
762     QVERIFY2(m_qbsStderr.contains("Use the 'resolve' command"), m_qbsStderr.constData());
763 
764     // On re-resolving, the error turns into a warning and a new build graph is created.
765     QCOMPARE(runQbs(QbsRunParameters("resolve")), 0);
766     QVERIFY2(m_qbsStderr.contains("Cannot use stored build graph"), m_qbsStderr.constData());
767     QVERIFY2(!m_qbsStderr.contains("Use the 'resolve' command"), m_qbsStderr.constData());
768 
769     QCOMPARE(runQbs(), 0);
770     QVERIFY2(!m_qbsStderr.contains("Cannot use stored build graph"), m_qbsStderr.constData());
771     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
772 }
773 
buildVariantDefaults_data()774 void TestBlackbox::buildVariantDefaults_data()
775 {
776     QTest::addColumn<QString>("buildVariant");
777     QTest::newRow("default") << QString();
778     QTest::newRow("debug") << QStringLiteral("debug");
779     QTest::newRow("release") << QStringLiteral("release");
780     QTest::newRow("profiling") << QStringLiteral("profiling");
781 }
782 
buildVariantDefaults()783 void TestBlackbox::buildVariantDefaults()
784 {
785     QFETCH(QString, buildVariant);
786     QDir::setCurrent(testDataDir + "/build-variant-defaults");
787     QbsRunParameters params{QStringLiteral("resolve")};
788     if (!buildVariant.isEmpty())
789         params.arguments << ("modules.qbs.buildVariant:" + buildVariant);
790     QCOMPARE(runQbs(params), 0);
791 }
792 
capnproto()793 void TestBlackbox::capnproto()
794 {
795     QFETCH(QString, projectFile);
796     QDir::setCurrent(testDataDir + "/capnproto");
797     rmDirR(relativeBuildDir());
798 
799     QbsRunParameters params{QStringLiteral("resolve"), {QStringLiteral("-f"), projectFile}};
800     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
801         QSKIP("Cannot run binaries in cross-compiled build");
802     if (m_qbsStdout.contains("capnproto is not present"))
803         QSKIP("capnproto is not present");
804 
805     params.command.clear();
806     QCOMPARE(runQbs(params), 0);
807 }
808 
capnproto_data()809 void TestBlackbox::capnproto_data()
810 {
811     QTest::addColumn<QString>("projectFile");
812 
813     QTest::newRow("cpp") << QStringLiteral("capnproto_cpp.qbs");
814     QTest::newRow("cpp-pkgconfig") << QStringLiteral("capnproto_cpp_pkgconfig.qbs");
815     QTest::newRow("greeter cpp (grpc)") << QStringLiteral("greeter_cpp.qbs");
816     QTest::newRow("relative import") << QStringLiteral("capnproto_relative_import.qbs");
817     QTest::newRow("absolute import") << QStringLiteral("capnproto_absolute_import.qbs");
818 }
819 
changedFiles_data()820 void TestBlackbox::changedFiles_data()
821 {
822     QTest::addColumn<bool>("useChangedFilesForInitialBuild");
823     QTest::newRow("initial build with changed files") << true;
824     QTest::newRow("initial build without changed files") << false;
825 }
826 
changedFiles()827 void TestBlackbox::changedFiles()
828 {
829     QFETCH(bool, useChangedFilesForInitialBuild);
830 
831     QDir::setCurrent(testDataDir + "/changed-files");
832     rmDirR(relativeBuildDir());
833     const QString changedFile = QDir::cleanPath(QDir::currentPath() + "/file1.cpp");
834     QbsRunParameters params1;
835     if (useChangedFilesForInitialBuild)
836         params1 = QbsRunParameters(QStringList("--changed-files") << changedFile);
837 
838     // Initial run: Build all files, even though only one of them was marked as changed
839     //              (if --changed-files was used).
840     QCOMPARE(runQbs(params1), 0);
841     QCOMPARE(m_qbsStdout.count("compiling"), 3);
842     QCOMPARE(m_qbsStdout.count("creating"), 3);
843 
844     WAIT_FOR_NEW_TIMESTAMP();
845     touch(QDir::currentPath() + "/main.cpp");
846 
847     // Now only the file marked as changed must be compiled, even though it hasn't really
848     // changed and another one has.
849     QbsRunParameters params2(QStringList("--changed-files") << changedFile);
850     QCOMPARE(runQbs(params2), 0);
851     QCOMPARE(m_qbsStdout.count("compiling"), 1);
852     QCOMPARE(m_qbsStdout.count("creating"), 1);
853     QVERIFY2(m_qbsStdout.contains("file1.cpp"), m_qbsStdout.constData());
854 }
855 
changedInputsFromDependencies()856 void TestBlackbox::changedInputsFromDependencies()
857 {
858     QDir::setCurrent(testDataDir + "/changed-inputs-from-dependencies");
859     QCOMPARE(runQbs(), 0);
860     QVERIFY2(m_qbsStdout.contains("final prepare script"), m_qbsStdout.constData());
861     QCOMPARE(runQbs(), 0);
862     QVERIFY2(!m_qbsStdout.contains("final prepare script"), m_qbsStdout.constData());
863     WAIT_FOR_NEW_TIMESTAMP();
864     touch("input.txt");
865     QCOMPARE(runQbs(), 0);
866     QVERIFY2(m_qbsStdout.contains("final prepare script"), m_qbsStdout.constData());
867 }
868 
changedRuleInputs()869 void TestBlackbox::changedRuleInputs()
870 {
871     QDir::setCurrent(testDataDir + "/changed-rule-inputs");
872 
873     // Initial build.
874     QCOMPARE(runQbs(), 0);
875     QVERIFY2(m_qbsStdout.contains("generating p1-dummy"), m_qbsStdout.constData());
876     QVERIFY2(m_qbsStdout.contains("generating p2-dummy"), m_qbsStdout.constData());
877 
878     // Re-build: p1 is always regenerated, and p2 has a dependency on it.
879     QCOMPARE(runQbs(), 0);
880     QVERIFY2(m_qbsStdout.contains("generating p1-dummy"), m_qbsStdout.constData());
881     QVERIFY2(m_qbsStdout.contains("generating p2-dummy"), m_qbsStdout.constData());
882 
883     // Remove the dependency. p2 gets re-generated one last time, because its set of
884     // inputs changed.
885     WAIT_FOR_NEW_TIMESTAMP();
886     REPLACE_IN_FILE("changed-rule-inputs.qbs", "inputsFromDependencies: \"p1\"",
887                     "inputsFromDependencies: \"p3\"");
888     QCOMPARE(runQbs(), 0);
889     QVERIFY2(m_qbsStdout.contains("generating p1-dummy"), m_qbsStdout.constData());
890     QVERIFY2(m_qbsStdout.contains("generating p2-dummy"), m_qbsStdout.constData());
891 
892     // Now the artifacts are no longer connected, and p2 must not get rebuilt anymore.
893     QCOMPARE(runQbs(), 0);
894     QVERIFY2(m_qbsStdout.contains("generating p1-dummy"), m_qbsStdout.constData());
895     QVERIFY2(!m_qbsStdout.contains("generating p2-dummy"), m_qbsStdout.constData());
896 }
897 
changeInDisabledProduct()898 void TestBlackbox::changeInDisabledProduct()
899 {
900     QDir::setCurrent(testDataDir + "/change-in-disabled-product");
901     QCOMPARE(runQbs(), 0);
902     WAIT_FOR_NEW_TIMESTAMP();
903     REPLACE_IN_FILE("change-in-disabled-product.qbs", "// 'test2.txt'", "'test2.txt'");
904     QCOMPARE(runQbs(), 0);
905 }
906 
changeInImportedFile()907 void TestBlackbox::changeInImportedFile()
908 {
909     QDir::setCurrent(testDataDir + "/change-in-imported-file");
910     QCOMPARE(runQbs(), 0);
911     QVERIFY2(m_qbsStdout.contains("old output"), m_qbsStdout.constData());
912 
913     WAIT_FOR_NEW_TIMESTAMP();
914     REPLACE_IN_FILE("prepare.js", "old output", "new output");
915     QCOMPARE(runQbs(), 0);
916     QVERIFY2(m_qbsStdout.contains("new output"), m_qbsStdout.constData());
917 
918     WAIT_FOR_NEW_TIMESTAMP();
919     touch("prepare.js");
920     QCOMPARE(runQbs(), 0);
921     QVERIFY2(!m_qbsStdout.contains("output"), m_qbsStdout.constData());
922 }
923 
changeTrackingAndMultiplexing()924 void TestBlackbox::changeTrackingAndMultiplexing()
925 {
926     QDir::setCurrent(testDataDir + "/change-tracking-and-multiplexing");
927     QCOMPARE(runQbs(QStringList("modules.cpp.staticLibraryPrefix:prefix1")), 0);
928     QCOMPARE(m_qbsStdout.count("compiling lib.cpp"), 2);
929     QCOMPARE(m_qbsStdout.count("creating prefix1l"), 2);
930     QCOMPARE(runQbs(QbsRunParameters("resolve",
931                                      QStringList("modules.cpp.staticLibraryPrefix:prefix2"))), 0);
932     QCOMPARE(runQbs(), 0);
933     QCOMPARE(m_qbsStdout.count("compiling lib.cpp"), 0);
934     QCOMPARE(m_qbsStdout.count("creating prefix2l"), 2);
935 }
936 
findByName(const QJsonArray & objects,const QString & name)937 static QJsonObject findByName(const QJsonArray &objects, const QString &name)
938 {
939     for (const QJsonValue &v : objects) {
940         if (!v.isObject())
941             continue;
942         QJsonObject obj = v.toObject();
943         const QString objName = obj.value(QStringLiteral("name")).toString();
944         if (objName == name)
945             return obj;
946     }
947     return {};
948 }
949 
readDepsOutput(const QString & depsFilePath,QJsonDocument & jsonDocument)950 static void readDepsOutput(const QString &depsFilePath, QJsonDocument &jsonDocument)
951 {
952     jsonDocument = QJsonDocument();
953     QFile depsFile(depsFilePath);
954     QVERIFY2(depsFile.open(QFile::ReadOnly), qPrintable(depsFile.errorString()));
955     QJsonParseError jsonerror;
956     jsonDocument = QJsonDocument::fromJson(depsFile.readAll(), &jsonerror);
957     if (jsonerror.error != QJsonParseError::NoError) {
958         qDebug() << jsonerror.errorString();
959         QFAIL("JSON parsing failed.");
960     }
961 }
962 
dependenciesProperty()963 void TestBlackbox::dependenciesProperty()
964 {
965     QDir::setCurrent(testDataDir + QLatin1String("/dependenciesProperty"));
966     QCOMPARE(runQbs(), 0);
967     const QString depsFile(relativeProductBuildDir("product1") + "/product1.deps");
968     QJsonDocument jsondoc;
969     readDepsOutput(depsFile, jsondoc);
970     QVERIFY(jsondoc.isArray());
971     QJsonArray dependencies = jsondoc.array();
972     QCOMPARE(dependencies.size(), 2);
973     QJsonObject product2 = findByName(dependencies, QStringLiteral("product2"));
974     QJsonArray product2_type = product2.value(QStringLiteral("type")).toArray();
975     QCOMPARE(product2_type.size(), 1);
976     QCOMPARE(product2_type.first().toString(), QLatin1String("application"));
977     QCOMPARE(product2.value(QLatin1String("narf")).toString(), QLatin1String("zort"));
978     QJsonArray product2_cppArtifacts
979             = product2.value("artifacts").toObject().value("cpp").toArray();
980     QCOMPARE(product2_cppArtifacts.size(), 1);
981     QJsonArray product2_deps = product2.value(QStringLiteral("dependencies")).toArray();
982     QVERIFY(!product2_deps.empty());
983     QJsonObject product2_qbs = findByName(product2_deps, QStringLiteral("qbs"));
984     QVERIFY(!product2_qbs.empty());
985     QJsonObject product2_cpp = findByName(product2_deps, QStringLiteral("cpp"));
986     QJsonArray product2_cpp_defines = product2_cpp.value(QLatin1String("defines")).toArray();
987     QCOMPARE(product2_cpp_defines.size(), 1);
988     QCOMPARE(product2_cpp_defines.first().toString(), QLatin1String("SMURF"));
989     QJsonArray cpp_dependencies = product2_cpp.value("dependencies").toArray();
990     QVERIFY(!cpp_dependencies.isEmpty());
991     int qbsCount = 0;
992     for (const auto dep : cpp_dependencies) {
993         if (dep.toObject().value("name").toString() == "qbs")
994             ++qbsCount;
995     }
996     QCOMPARE(qbsCount, 1);
997 
998     // Add new dependency, check that command is re-run.
999     const QString projectFile("dependenciesProperty.qbs");
1000     WAIT_FOR_NEW_TIMESTAMP();
1001     REPLACE_IN_FILE(projectFile, "// Depends { name: 'newDependency' }",
1002                     "Depends { name: 'newDependency' }");
1003     QCOMPARE(runQbs(), 0);
1004     QVERIFY2(m_qbsStdout.contains("generate product1.deps"), m_qbsStdout.constData());
1005     readDepsOutput(depsFile, jsondoc);
1006     dependencies = jsondoc.array();
1007     QCOMPARE(dependencies.size(), 3);
1008 
1009     // Add new Depends item that does not actually introduce a new dependency, check
1010     // that command is not re-run.
1011     WAIT_FOR_NEW_TIMESTAMP();
1012     REPLACE_IN_FILE(projectFile, "// Depends { name: 'product2' }", "Depends { name: 'product2' }");
1013     QCOMPARE(runQbs(), 0);
1014     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
1015     QVERIFY2(!m_qbsStdout.contains("generate product1.deps"), m_qbsStdout.constData());
1016     readDepsOutput(depsFile, jsondoc);
1017     dependencies = jsondoc.array();
1018     QCOMPARE(dependencies.size(), 3);
1019 
1020     // Change property of dependency, check that command is re-run.
1021     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{"products.product2.narf:zortofsky"})),
1022              0);
1023     QCOMPARE(runQbs(), 0);
1024     QVERIFY2(!m_qbsStdout.contains("compiling product2.cpp"), m_qbsStdout.constData());
1025     QVERIFY2(m_qbsStdout.contains("generate product1.deps"), m_qbsStdout.constData());
1026     readDepsOutput(depsFile, jsondoc);
1027     dependencies = jsondoc.array();
1028     QCOMPARE(dependencies.size(), 3);
1029     product2 = findByName(dependencies, QStringLiteral("product2"));
1030     QCOMPARE(product2.value(QLatin1String("narf")).toString(), QLatin1String("zortofsky"));
1031 
1032     // Change module property of dependency, check that command is re-run.
1033     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{"products.product2.narf:zortofsky",
1034                                      "products.product2.cpp.defines:DIGEDAG"})), 0);
1035     QCOMPARE(runQbs(), 0);
1036     QVERIFY2(m_qbsStdout.contains("compiling product2.cpp"), m_qbsStdout.constData());
1037     QVERIFY2(m_qbsStdout.contains("generate product1.deps"), m_qbsStdout.constData());
1038     readDepsOutput(depsFile, jsondoc);
1039     dependencies = jsondoc.array();
1040     QCOMPARE(dependencies.size(), 3);
1041     product2 = findByName(dependencies, QStringLiteral("product2"));
1042     product2_deps = product2.value(QStringLiteral("dependencies")).toArray();
1043     product2_cpp = findByName(product2_deps, QStringLiteral("cpp"));
1044     product2_cpp_defines = product2_cpp.value(QStringLiteral("defines")).toArray();
1045     QCOMPARE(product2_cpp_defines.size(), 1);
1046     QCOMPARE(product2_cpp_defines.first().toString(), QLatin1String("DIGEDAG"));
1047 }
1048 
dependencyScanningLoop()1049 void TestBlackbox::dependencyScanningLoop()
1050 {
1051     QDir::setCurrent(testDataDir + "/dependency-scanning-loop");
1052     QCOMPARE(runQbs(), 0);
1053     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
1054 }
1055 
deprecatedProperty()1056 void TestBlackbox::deprecatedProperty()
1057 {
1058     QDir::setCurrent(testDataDir + "/deprecated-property");
1059     QbsRunParameters params(QStringList("-q"));
1060     params.expectFailure = true;
1061     QVERIFY(runQbs(params) != 0);
1062     m_qbsStderr = QDir::fromNativeSeparators(QString::fromLocal8Bit(m_qbsStderr)).toLocal8Bit();
1063     QVERIFY2(m_qbsStderr.contains("deprecated-property.qbs:6:24 The property 'oldProp' is "
1064             "deprecated and will be removed in Qbs 99.9.0."), m_qbsStderr.constData());
1065     QVERIFY2(m_qbsStderr.contains("deprecated-property.qbs:7:28 The property 'veryOldProp' can no "
1066             "longer be used. It was removed in Qbs 1.3.0."), m_qbsStderr.constData());
1067     QVERIFY2(m_qbsStderr.contains("Property 'forgottenProp' was scheduled for removal in version "
1068                                   "1.8.0, but is still present."), m_qbsStderr.constData());
1069     QVERIFY2(m_qbsStderr.contains("themodule/m.qbs:22:5 Removal version for 'forgottenProp' "
1070                                   "specified here."), m_qbsStderr.constData());
1071     QVERIFY2(m_qbsStderr.count("Use newProp instead.") == 2, m_qbsStderr.constData());
1072     QVERIFY2(m_qbsStderr.count("is deprecated") == 1, m_qbsStderr.constData());
1073     QVERIFY2(m_qbsStderr.count("was removed") == 1, m_qbsStderr.constData());
1074 }
1075 
disappearedProfile()1076 void TestBlackbox::disappearedProfile()
1077 {
1078     QDir::setCurrent(testDataDir + "/disappeared-profile");
1079     QbsRunParameters resolveParams;
1080 
1081     // First, we need to fail, because we don't tell qbs where the module is.
1082     resolveParams.expectFailure = true;
1083     QVERIFY(runQbs(resolveParams) != 0);
1084 
1085     // Now we set up a profile with all the necessary information, and qbs succeeds.
1086     qbs::Settings settings(QDir::currentPath() + "/settings-dir");
1087     qbs::Profile profile("p", &settings);
1088     profile.setValue("m.p1", "p1 from profile");
1089     profile.setValue("m.p2", "p2 from profile");
1090     profile.setValue("preferences.qbsSearchPaths",
1091                      QStringList({QDir::currentPath() + "/modules-dir"}));
1092     settings.sync();
1093     resolveParams.command = "resolve";
1094     resolveParams.expectFailure = false;
1095     resolveParams.settingsDir = settings.baseDirectory();
1096     resolveParams.profile = profile.name();
1097     QCOMPARE(runQbs(resolveParams), 0);
1098 
1099     // Now we change a property in the profile, but because we don't use the "resolve" command,
1100     // the old profile contents stored in the build graph are used.
1101     profile.setValue("m.p2", "p2 new from profile");
1102     settings.sync();
1103     QbsRunParameters buildParams;
1104     buildParams.profile.clear();
1105     QCOMPARE(runQbs(buildParams), 0);
1106     QVERIFY2(m_qbsStdout.contains("Creating dummy1.txt with p1 from profile"),
1107              m_qbsStdout.constData());
1108     QVERIFY2(m_qbsStdout.contains("Creating dummy2.txt with p2 from profile"),
1109              m_qbsStdout.constData());
1110 
1111     // Now we do use the "resolve" command, so the new property value is taken into account.
1112     QCOMPARE(runQbs(resolveParams), 0);
1113     QCOMPARE(runQbs(buildParams), 0);
1114     QVERIFY2(!m_qbsStdout.contains("Creating dummy1.txt"), m_qbsStdout.constData());
1115     QVERIFY2(m_qbsStdout.contains("Creating dummy2.txt with p2 new from profile"),
1116              m_qbsStdout.constData());
1117 
1118     // Now we change the profile again without a "resolve" command. However, this time we
1119     // force re-resolving indirectly by changing a project file. The updated property value
1120     // must still not be taken into account.
1121     profile.setValue("m.p1", "p1 new from profile");
1122     settings.sync();
1123     WAIT_FOR_NEW_TIMESTAMP();
1124     REPLACE_IN_FILE("modules-dir/modules/m/m.qbs", "property string p1",
1125                     "property string p1: 'p1 from module'");
1126     QCOMPARE(runQbs(buildParams), 0);
1127     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
1128     QVERIFY2(!m_qbsStdout.contains("Creating dummy1.txt"), m_qbsStdout.constData());
1129     QVERIFY2(!m_qbsStdout.contains("Creating dummy2.txt"), m_qbsStdout.constData());
1130 
1131     // Now we run the "resolve" command without giving the necessary settings path to find
1132     // the profile.
1133     resolveParams.expectFailure = true;
1134     resolveParams.settingsDir.clear();
1135     resolveParams.profile.clear();
1136     QVERIFY(runQbs(resolveParams) != 0);
1137     QVERIFY2(m_qbsStderr.contains("profile"), m_qbsStderr.constData());
1138 }
1139 
discardUnusedData()1140 void TestBlackbox::discardUnusedData()
1141 {
1142     QDir::setCurrent(testDataDir + "/discard-unused-data");
1143     rmDirR(relativeBuildDir());
1144     QFETCH(QString, discardString);
1145     QFETCH(bool, symbolPresent);
1146     QbsRunParameters params;
1147     if (!discardString.isEmpty())
1148         params.arguments << ("modules.cpp.discardUnusedData:" + discardString);
1149     QCOMPARE(runQbs(params), 0);
1150     QVERIFY2(m_qbsStdout.contains("is Darwin"), m_qbsStdout.constData());
1151     const bool isDarwin = m_qbsStdout.contains("is Darwin: true");
1152     const QString output = QString::fromLocal8Bit(m_qbsStdout);
1153     const QRegularExpression pattern(QRegularExpression::anchoredPattern(".*---(.*)---.*"),
1154                                      QRegularExpression::DotMatchesEverythingOption);
1155     const QRegularExpressionMatch match = pattern.match(output);
1156     QVERIFY2(match.hasMatch(), qPrintable(output));
1157     QCOMPARE(match.lastCapturedIndex(), 1);
1158     const QString nmPath = match.captured(1);
1159     if (!QFile::exists(nmPath))
1160         QSKIP("Cannot check for symbol presence: No nm found.");
1161     QProcess nm;
1162     nm.start(nmPath, QStringList(QDir::currentPath() + '/' + relativeExecutableFilePath("app")));
1163     QVERIFY(nm.waitForStarted());
1164     QVERIFY(nm.waitForFinished());
1165     const QByteArray nmOutput = nm.readAllStandardOutput();
1166     QVERIFY2(nm.exitCode() == 0, nm.readAllStandardError().constData());
1167     if (!symbolPresent && !isDarwin)
1168         QSKIP("Unused symbol detection only supported on Darwin");
1169     QVERIFY2(nmOutput.contains("unusedFunc") == symbolPresent, nmOutput.constData());
1170 }
1171 
discardUnusedData_data()1172 void TestBlackbox::discardUnusedData_data()
1173 {
1174     QTest::addColumn<QString>("discardString");
1175     QTest::addColumn<bool>("symbolPresent");
1176 
1177     QTest::newRow("discard") << QString("true") << false;
1178     QTest::newRow("don't discard") << QString("false") << true;
1179     QTest::newRow("default") << QString() << true;
1180 }
1181 
driverLinkerFlags()1182 void TestBlackbox::driverLinkerFlags()
1183 {
1184     QDir::setCurrent(testDataDir + QLatin1String("/driver-linker-flags"));
1185     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("-n"))), 0);
1186     if (!m_qbsStdout.contains("toolchain is GCC-like"))
1187         QSKIP("Test applies on GCC-like toolchains only");
1188     QFETCH(QString, linkerMode);
1189     QFETCH(bool, expectDriverOption);
1190     const QString linkerModeArg = "modules.cpp.linkerMode:" + linkerMode;
1191     QCOMPARE(runQbs(QStringList({"-n", "--command-echo-mode", "command-line", linkerModeArg})), 0);
1192     const QByteArray driverArg = "-nostartfiles";
1193     const QByteArrayList output = m_qbsStdout.split('\n');
1194     QByteArray compileLine;
1195     QByteArray linkLine;
1196     for (const QByteArray &line : output) {
1197         if (line.contains(" -c "))
1198             compileLine = line;
1199         else if (line.contains("main.cpp.o"))
1200             linkLine = line;
1201     }
1202     QVERIFY(!compileLine.isEmpty());
1203     QVERIFY(!linkLine.isEmpty());
1204     QVERIFY2(!compileLine.contains(driverArg), compileLine.constData());
1205     QVERIFY2(linkLine.contains(driverArg) == expectDriverOption, linkLine.constData());
1206 }
1207 
driverLinkerFlags_data()1208 void TestBlackbox::driverLinkerFlags_data()
1209 {
1210     QTest::addColumn<QString>("linkerMode");
1211     QTest::addColumn<bool>("expectDriverOption");
1212 
1213     QTest::newRow("link using compiler driver") << "automatic" << true;
1214     QTest::newRow("link using linker") << "manual" << false;
1215 }
1216 
dynamicLibraryInModule()1217 void TestBlackbox::dynamicLibraryInModule()
1218 {
1219     QDir::setCurrent(testDataDir + "/dynamic-library-in-module");
1220     const QString installRootSpec = QString("qbs.installRoot:") + QDir::currentPath();
1221     QbsRunParameters libParams(QStringList({"-f", "thelibs.qbs", installRootSpec}));
1222     libParams.buildDirectory = "libbuild";
1223     QCOMPARE(runQbs(libParams), 0);
1224     QbsRunParameters appParams("build", QStringList({"-f", "theapp.qbs", installRootSpec}));
1225     appParams.buildDirectory = "appbuild";
1226     QCOMPARE(runQbs(appParams), 0);
1227     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
1228         QSKIP("Cannot run binaries in cross-compiled build");
1229     appParams.command = "run";
1230     QCOMPARE(runQbs(appParams), 0);
1231     QVERIFY2(m_qbsStdout.contains("Hello from thelib"), m_qbsStdout.constData());
1232     QVERIFY2(m_qbsStdout.contains("Hello from theotherlib"), m_qbsStdout.constData());
1233     QVERIFY2(!m_qbsStdout.contains("thirdlib"), m_qbsStdout.constData());
1234     QVERIFY(!QFileInfo::exists(appParams.buildDirectory + '/'
1235                                + qbs::InstallOptions::defaultInstallRoot()));
1236 }
1237 
symlinkRemoval()1238 void TestBlackbox::symlinkRemoval()
1239 {
1240     if (HostOsInfo::isWindowsHost())
1241         QSKIP("No symlink support on Windows.");
1242     QDir::setCurrent(testDataDir + "/symlink-removal");
1243     QVERIFY(QDir::current().mkdir("dir1"));
1244     QVERIFY(QDir::current().mkdir("dir2"));
1245     QVERIFY(QFile::link("dir2", "dir1/broken-link"));
1246     QVERIFY(QFile::link(QFileInfo("dir2").absoluteFilePath(), "dir1/valid-link-to-dir"));
1247     QVERIFY(QFile::link(QFileInfo("symlink-removal.qbs").absoluteFilePath(),
1248                         "dir1/valid-link-to-file"));
1249     QCOMPARE(runQbs(), 0);
1250     QVERIFY(!QFile::exists("dir1"));
1251     QVERIFY(QFile::exists("dir2"));
1252     QVERIFY(QFile::exists("symlink-removal.qbs"));
1253 }
1254 
usingsAsSoleInputsNonMultiplexed()1255 void TestBlackbox::usingsAsSoleInputsNonMultiplexed()
1256 {
1257     QDir::setCurrent(testDataDir + QLatin1String("/usings-as-sole-inputs-non-multiplexed"));
1258     QCOMPARE(runQbs(), 0);
1259     const QString p3BuildDir = relativeProductBuildDir("p3");
1260     QVERIFY(regularFileExists(p3BuildDir + "/custom1.out.plus"));
1261     QVERIFY(regularFileExists(p3BuildDir + "/custom2.out.plus"));
1262 }
1263 
variantSuffix()1264 void TestBlackbox::variantSuffix()
1265 {
1266     QDir::setCurrent(testDataDir + "/variant-suffix");
1267     QFETCH(bool, multiplex);
1268     QFETCH(bool, expectFailure);
1269     QFETCH(QString, variantSuffix);
1270     QFETCH(QString, buildVariant);
1271     QFETCH(QVariantMap, fileNames);
1272     QbsRunParameters params;
1273     params.command = "resolve";
1274     params.arguments << "--force-probe-execution";
1275     if (multiplex)
1276         params.arguments << "products.l.multiplex:true";
1277     else
1278         params.arguments << ("modules.qbs.buildVariant:" + buildVariant);
1279     if (!variantSuffix.isEmpty())
1280         params.arguments << ("modules.cpp.variantSuffix:" + variantSuffix);
1281     QCOMPARE(runQbs(params), 0);
1282     const QString fileNameMapKey = m_qbsStdout.contains("is Windows: true")
1283             ? "windows" : m_qbsStdout.contains("is Apple: true") ? "apple" : "unix";
1284     if (variantSuffix.isEmpty() && multiplex && fileNameMapKey == "unix")
1285         expectFailure = true;
1286     params.command = "build";
1287     params.expectFailure = expectFailure;
1288     params.arguments = QStringList("--clean-install-root");
1289     QCOMPARE(runQbs(params) == 0, !expectFailure);
1290     if (expectFailure)
1291         return;
1292     const QStringList fileNameList = fileNames.value(fileNameMapKey).toStringList();
1293     for (const QString &fileName : fileNameList) {
1294         QFile libFile("default/install-root/lib/" + fileName);
1295         QVERIFY2(libFile.exists(), qPrintable(libFile.fileName()));
1296     }
1297 }
1298 
variantSuffix_data()1299 void TestBlackbox::variantSuffix_data()
1300 {
1301     QTest::addColumn<bool>("multiplex");
1302     QTest::addColumn<bool>("expectFailure");
1303     QTest::addColumn<QString>("variantSuffix");
1304     QTest::addColumn<QString>("buildVariant");
1305     QTest::addColumn<QVariantMap>("fileNames");
1306 
1307     QTest::newRow("default suffix, debug") << false << false << QString() << QString("debug")
1308             << QVariantMap({std::make_pair(QString("windows"), QStringList("libl.ext")),
1309                             std::make_pair(QString("apple"), QStringList("libl.ext")),
1310                             std::make_pair(QString("unix"), QStringList("libl.ext"))});
1311     QTest::newRow("default suffix, release") << false << false << QString() << QString("release")
1312             << QVariantMap({std::make_pair(QString("windows"), QStringList("libl.ext")),
1313                             std::make_pair(QString("apple"), QStringList("libl.ext")),
1314                             std::make_pair(QString("unix"), QStringList("libl.ext"))});
1315     QTest::newRow("custom suffix, debug") << false << false << QString("blubb") << QString("debug")
1316             << QVariantMap({std::make_pair(QString("windows"), QStringList("liblblubb.ext")),
1317                             std::make_pair(QString("apple"), QStringList("liblblubb.ext")),
1318                             std::make_pair(QString("unix"), QStringList("liblblubb.ext"))});
1319     QTest::newRow("custom suffix, release") << false << false << QString("blubb")
1320             << QString("release")
1321             << QVariantMap({std::make_pair(QString("windows"), QStringList("liblblubb.ext")),
1322                             std::make_pair(QString("apple"), QStringList("liblblubb.ext")),
1323                             std::make_pair(QString("unix"), QStringList("liblblubb.ext"))});
1324     QTest::newRow("default suffix, multiplex") << true << false << QString() << QString()
1325             << QVariantMap({std::make_pair(QString("windows"),
1326                             QStringList({"libl.ext", "libld.ext"})),
1327                             std::make_pair(QString("apple"),
1328                             QStringList({"libl.ext", "libl_debug.ext"})),
1329                             std::make_pair(QString("unix"), QStringList())});
1330     QTest::newRow("custom suffix, multiplex") << true << true << QString("blubb") << QString()
1331             << QVariantMap({std::make_pair(QString("windows"), QStringList()),
1332                             std::make_pair(QString("apple"), QStringList()),
1333                             std::make_pair(QString("unix"), QStringList())});
1334 }
1335 
waitForProcessSuccess(QProcess & p)1336 static bool waitForProcessSuccess(QProcess &p)
1337 {
1338     if (!p.waitForStarted() || !p.waitForFinished()) {
1339         qDebug() << p.errorString();
1340         return false;
1341     }
1342     if (p.exitCode() != 0) {
1343         qDebug() << p.readAllStandardError();
1344         return false;
1345     }
1346     return true;
1347 }
1348 
vcsGit()1349 void TestBlackbox::vcsGit()
1350 {
1351     const QString gitFilePath = findExecutable(QStringList("git"));
1352     if (gitFilePath.isEmpty())
1353         QSKIP("git not found");
1354 
1355     // Set up repo.
1356     QTemporaryDir repoDir;
1357     QVERIFY(repoDir.isValid());
1358     ccp(testDataDir + "/vcs", repoDir.path());
1359     QDir::setCurrent(repoDir.path());
1360 
1361     QProcess git;
1362     git.start(gitFilePath, QStringList("init"));
1363     QVERIFY(waitForProcessSuccess(git));
1364     git.start(gitFilePath, QStringList({"config", "user.name", "My Name"}));
1365     QVERIFY(waitForProcessSuccess(git));
1366     git.start(gitFilePath, QStringList({"config", "user.email", "me@example.com"}));
1367     QVERIFY(waitForProcessSuccess(git));
1368 
1369     const auto getRepoStateFromApp = [this] {
1370         const int startIndex = m_qbsStdout.indexOf("__");
1371         if (startIndex == -1)
1372             return QByteArray();
1373         const int endIndex = m_qbsStdout.indexOf("__", startIndex + 2);
1374         if (endIndex == -1)
1375             return QByteArray();
1376         return m_qbsStdout.mid(startIndex + 2, endIndex - startIndex - 2);
1377     };
1378 
1379     QCOMPARE(runQbs({"resolve", {"-f", repoDir.path()}}), 0);
1380     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
1381         QSKIP("Cannot run binaries in cross-compiled build");
1382     // Run without git metadata.
1383     QbsRunParameters params("run", QStringList{"-f", repoDir.path()});
1384     params.workingDir = repoDir.path() + "/..";
1385     params.buildDirectory = repoDir.path();
1386     QCOMPARE(runQbs(params), 0);
1387     QVERIFY2(m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1388     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1389     QByteArray newRepoState = getRepoStateFromApp();
1390     QCOMPARE(newRepoState, QByteArray("none"));
1391     QByteArray oldRepoState = newRepoState;
1392 
1393     // Initial commit
1394     git.start(gitFilePath, QStringList({"add", "main.cpp"}));
1395     QVERIFY(waitForProcessSuccess(git));
1396     git.start(gitFilePath, QStringList({"commit", "-m", "initial commit"}));
1397     QVERIFY(waitForProcessSuccess(git));
1398 
1399     // Run with git metadata.
1400     WAIT_FOR_NEW_TIMESTAMP();
1401     QCOMPARE(runQbs(params), 0);
1402     QVERIFY2(m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1403     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1404     newRepoState = getRepoStateFromApp();
1405     QVERIFY(oldRepoState != newRepoState);
1406     oldRepoState = newRepoState;
1407 
1408     // Run with no changes.
1409     QCOMPARE(runQbs(params), 0);
1410     QVERIFY2(!m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1411     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1412     newRepoState = getRepoStateFromApp();
1413     QCOMPARE(oldRepoState, newRepoState);
1414 
1415     // Run with changed source file.
1416     WAIT_FOR_NEW_TIMESTAMP();
1417     touch("main.cpp");
1418     QCOMPARE(runQbs(params), 0);
1419     QVERIFY2(!m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1420     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1421     newRepoState = getRepoStateFromApp();
1422     QCOMPARE(oldRepoState, newRepoState);
1423 
1424     // Add new file to repo.
1425     WAIT_FOR_NEW_TIMESTAMP();
1426     touch("blubb.txt");
1427     git.start(gitFilePath, QStringList({"add", "blubb.txt"}));
1428     QVERIFY(waitForProcessSuccess(git));
1429     git.start(gitFilePath, QStringList({"commit", "-m", "blubb!"}));
1430     QVERIFY(waitForProcessSuccess(git));
1431     QCOMPARE(runQbs(params), 0);
1432     QVERIFY2(m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1433     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1434     newRepoState = getRepoStateFromApp();
1435     QVERIFY(oldRepoState != newRepoState);
1436 }
1437 
vcsSubversion()1438 void TestBlackbox::vcsSubversion()
1439 {
1440     const QString svnadminFilePath = findExecutable(QStringList("svnadmin"));
1441     if (svnadminFilePath.isEmpty())
1442         QSKIP("svnadmin not found");
1443     const QString svnFilePath = findExecutable(QStringList("svn"));
1444     if (svnFilePath.isEmpty())
1445         QSKIP("svn not found");
1446 
1447     if (HostOsInfo::isWindowsHost() && qEnvironmentVariableIsSet("GITHUB_ACTIONS"))
1448         QSKIP("Skip this test when running on GitHub");
1449 
1450     // Set up repo.
1451     QTemporaryDir repoDir;
1452     QVERIFY(repoDir.isValid());
1453     QProcess proc;
1454     proc.setWorkingDirectory(repoDir.path());
1455     proc.start(svnadminFilePath, QStringList({"create", "vcstest"}));
1456     QVERIFY(waitForProcessSuccess(proc));
1457     const QString projectUrl = "file://" + repoDir.path() + "/vcstest/trunk";
1458     proc.start(svnFilePath, QStringList({"import", testDataDir + "/vcs", projectUrl, "-m",
1459                                          "initial import"}));
1460     QVERIFY(waitForProcessSuccess(proc));
1461     QTemporaryDir checkoutDir;
1462     QVERIFY(checkoutDir.isValid());
1463     proc.setWorkingDirectory(checkoutDir.path());
1464     proc.start(svnFilePath, QStringList({"co", projectUrl, "."}));
1465     QVERIFY(waitForProcessSuccess(proc));
1466 
1467     // Initial runs
1468     QDir::setCurrent(checkoutDir.path());
1469     QbsRunParameters failParams;
1470     failParams.command = "run";
1471     failParams.expectFailure = true;
1472     const int retval = runQbs(failParams);
1473     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
1474         QSKIP("Cannot run binaries in cross-compiled build");
1475     if (m_qbsStderr.contains("svn too old"))
1476         QSKIP("svn too old");
1477     QCOMPARE(retval, 0);
1478     QVERIFY2(m_qbsStdout.contains("__1__"), m_qbsStdout.constData());
1479     QCOMPARE(runQbs(QbsRunParameters("run")), 0);
1480     QVERIFY2(!m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1481     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1482     QVERIFY2(m_qbsStdout.contains("__1__"), m_qbsStdout.constData());
1483 
1484     // Run with changed source file.
1485     WAIT_FOR_NEW_TIMESTAMP();
1486     touch("main.cpp");
1487     QCOMPARE(runQbs(QbsRunParameters("run")), 0);
1488     QVERIFY2(!m_qbsStdout.contains("generating my-repo-state.h"), m_qbsStderr.constData());
1489     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStderr.constData());
1490     QVERIFY2(m_qbsStdout.contains("__1__"), m_qbsStdout.constData());
1491 
1492     // Add new file to repo.
1493     WAIT_FOR_NEW_TIMESTAMP();
1494     touch("blubb.txt");
1495     proc.start(svnFilePath, QStringList({"add", "blubb.txt"}));
1496     QVERIFY(waitForProcessSuccess(proc));
1497     proc.start(svnFilePath, QStringList({"commit", "-m", "blubb!"}));
1498     QVERIFY(waitForProcessSuccess(proc));
1499     QCOMPARE(runQbs(QbsRunParameters("run")), 0);
1500     QVERIFY2(m_qbsStdout.contains("__2__"), m_qbsStdout.constData());
1501 }
1502 
versionCheck()1503 void TestBlackbox::versionCheck()
1504 {
1505     QDir::setCurrent(testDataDir + "/versioncheck");
1506     QFETCH(QString, requestedMinVersion);
1507     QFETCH(QString, requestedMaxVersion);
1508     QFETCH(QString, actualVersion);
1509     QFETCH(QString, errorMessage);
1510     QbsRunParameters params;
1511     params.expectFailure = !errorMessage.isEmpty();
1512     params.arguments << "-n"
1513                      << ("products.versioncheck.requestedMinVersion:'" + requestedMinVersion + "'")
1514                      << ("products.versioncheck.requestedMaxVersion:'" + requestedMaxVersion + "'")
1515                      << ("modules.lower.version:'" + actualVersion + "'");
1516     QCOMPARE(runQbs(params) == 0, errorMessage.isEmpty());
1517     if (params.expectFailure)
1518         QVERIFY2(QString(m_qbsStderr).contains(errorMessage), m_qbsStderr.constData());
1519 }
1520 
versionCheck_data()1521 void TestBlackbox::versionCheck_data()
1522 {
1523     QTest::addColumn<QString>("requestedMinVersion");
1524     QTest::addColumn<QString>("requestedMaxVersion");
1525     QTest::addColumn<QString>("actualVersion");
1526     QTest::addColumn<QString>("errorMessage");
1527 
1528     QTest::newRow("ok1") << "1.0" << "1.1" << "1.0" << QString();
1529     QTest::newRow("ok2") << "1.0" << "2.0.1" << "2.0" << QString();
1530     QTest::newRow("ok3") << "1.0" << "2.5" << "1.5" << QString();
1531     QTest::newRow("ok3") << "1.0" << "2.0" << "1.99" << QString();
1532     QTest::newRow("bad1") << "2.0" << "2.1" << "1.5" << "needs to be at least";
1533     QTest::newRow("bad2") << "2.0" << "3.0" << "1.5" << "needs to be at least";
1534     QTest::newRow("bad3") << "2.0" << "3.0" << "3.5" << "needs to be lower than";
1535     QTest::newRow("bad4") << "2.0" << "3.0" << "3.0" << "needs to be lower than";
1536 
1537     // "bad" because the "higer" module has stronger requirements.
1538     QTest::newRow("bad5") << "0.1" << "0.9" << "0.5" << "Impossible version constraint";
1539 }
1540 
versionScript()1541 void TestBlackbox::versionScript()
1542 {
1543     const SettingsPtr s = settings();
1544     Profile buildProfile(profileName(), s.get());
1545     QStringList toolchain = profileToolchain(buildProfile);
1546     if (!toolchain.contains("gcc") || targetOs() != HostOsInfo::HostOsLinux)
1547         QSKIP("version script test only applies to Linux");
1548     QDir::setCurrent(testDataDir + "/versionscript");
1549     QCOMPARE(runQbs(QbsRunParameters(QStringList("-q")
1550                                      << ("qbs.installRoot:" + QDir::currentPath()))), 0);
1551     const QString output = QString::fromLocal8Bit(m_qbsStderr);
1552     const QRegularExpression pattern(QRegularExpression::anchoredPattern(".*---(.*)---.*"),
1553                                      QRegularExpression::DotMatchesEverythingOption);
1554     const QRegularExpressionMatch match = pattern.match(output);
1555     QVERIFY2(match.hasMatch(), qPrintable(output));
1556     QCOMPARE(pattern.captureCount(), 1);
1557     const QString nmPath = match.captured(1);
1558     if (!QFile::exists(nmPath))
1559         QSKIP("Cannot check for symbol presence: No nm found.");
1560     QProcess nm;
1561     nm.start(nmPath, QStringList(QDir::currentPath() + "/libversionscript.so"));
1562     QVERIFY(nm.waitForStarted());
1563     QVERIFY(nm.waitForFinished());
1564     const QByteArray allSymbols = nm.readAllStandardOutput();
1565     QCOMPARE(nm.exitCode(), 0);
1566     QVERIFY2(allSymbols.contains("dummyLocal"), allSymbols.constData());
1567     QVERIFY2(allSymbols.contains("dummyGlobal"), allSymbols.constData());
1568     nm.start(nmPath, QStringList() << "-g" << QDir::currentPath() + "/libversionscript.so");
1569     QVERIFY(nm.waitForStarted());
1570     QVERIFY(nm.waitForFinished());
1571     const QByteArray globalSymbols = nm.readAllStandardOutput();
1572     QCOMPARE(nm.exitCode(), 0);
1573     QVERIFY2(!globalSymbols.contains("dummyLocal"), allSymbols.constData());
1574     QVERIFY2(globalSymbols.contains("dummyGlobal"), allSymbols.constData());
1575 }
1576 
wholeArchive()1577 void TestBlackbox::wholeArchive()
1578 {
1579     QDir::setCurrent(testDataDir + "/whole-archive");
1580     QFETCH(QString, wholeArchiveString);
1581     QFETCH(bool, ruleInvalidationExpected);
1582     QFETCH(bool, dllLinkingExpected);
1583     const QbsRunParameters resolveParams("resolve",
1584             QStringList("products.dynamiclib.linkWholeArchive:" + wholeArchiveString));
1585     QCOMPARE(runQbs(QbsRunParameters(resolveParams)), 0);
1586     const bool linkerSupportsWholeArchive = m_qbsStdout.contains("can link whole archives");
1587     const bool linkerDoesNotSupportWholeArchive
1588             = m_qbsStdout.contains("cannot link whole archives");
1589     QVERIFY(linkerSupportsWholeArchive != linkerDoesNotSupportWholeArchive);
1590 
1591     QCOMPARE(runQbs(QbsRunParameters(QStringList({ "-vvp", "dynamiclib" }))), 0);
1592     const bool wholeArchive = !wholeArchiveString.isEmpty();
1593     const bool outdatedVisualStudio = wholeArchive && linkerDoesNotSupportWholeArchive;
1594     const QByteArray invalidationOutput
1595             = "Value for property 'staticlib 1:cpp.linkWholeArchive' has changed.";
1596     if (!outdatedVisualStudio)
1597         QCOMPARE(m_qbsStderr.contains(invalidationOutput), ruleInvalidationExpected);
1598     QCOMPARE(m_qbsStdout.contains("linking"), dllLinkingExpected && !outdatedVisualStudio);
1599     QbsRunParameters buildParams(QStringList("-p"));
1600     buildParams.expectFailure = !wholeArchive || outdatedVisualStudio;
1601     buildParams.arguments << "app1";
1602     QCOMPARE(runQbs(QbsRunParameters(buildParams)) == 0, wholeArchive && !outdatedVisualStudio);
1603     buildParams.arguments.last() = "app2";
1604     QCOMPARE(runQbs(QbsRunParameters(buildParams)) == 0, wholeArchive && !outdatedVisualStudio);
1605     buildParams.arguments.last() = "app4";
1606     QCOMPARE(runQbs(QbsRunParameters(buildParams)) == 0, wholeArchive && !outdatedVisualStudio);
1607     buildParams.arguments.last() = "app3";
1608     buildParams.expectFailure = true;
1609     QVERIFY(runQbs(QbsRunParameters(buildParams)) != 0);
1610 }
1611 
wholeArchive_data()1612 void TestBlackbox::wholeArchive_data()
1613 {
1614     QTest::addColumn<QString>("wholeArchiveString");
1615     QTest::addColumn<bool>("ruleInvalidationExpected");
1616     QTest::addColumn<bool>("dllLinkingExpected");
1617     QTest::newRow("link normally") << QString() << false << true;
1618     QTest::newRow("link whole archive") << "true" << true << true;
1619     QTest::newRow("link whole archive again") << "notfalse" << false << false;
1620 }
1621 
symlinkExists(const QString & linkFilePath)1622 static bool symlinkExists(const QString &linkFilePath)
1623 {
1624     return QFileInfo(linkFilePath).isSymLink();
1625 }
1626 
clean()1627 void TestBlackbox::clean()
1628 {
1629     const QString appObjectFilePath = relativeProductBuildDir("app") + '/' + inputDirHash(".")
1630             + objectFileName("/main.cpp", profileName());
1631     const QString appExeFilePath = relativeExecutableFilePath("app");
1632     const QString depObjectFilePath = relativeProductBuildDir("dep") + '/' + inputDirHash(".")
1633             + objectFileName("/dep.cpp", profileName());
1634     const QString depLibBase = relativeProductBuildDir("dep")
1635             + '/' + QBS_HOST_DYNAMICLIB_PREFIX + "dep";
1636     QString depLibFilePath;
1637     QStringList symlinks;
1638     if (qbs::Internal::HostOsInfo::isMacosHost()) {
1639         depLibFilePath = depLibBase + ".1.1.0" + QBS_HOST_DYNAMICLIB_SUFFIX;
1640         symlinks << depLibBase + ".1.1" + QBS_HOST_DYNAMICLIB_SUFFIX
1641                  << depLibBase + ".1"  + QBS_HOST_DYNAMICLIB_SUFFIX
1642                  << depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX;
1643     } else if (qbs::Internal::HostOsInfo::isAnyUnixHost()) {
1644         depLibFilePath = depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX + ".1.1.0";
1645         symlinks << depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX + ".1.1"
1646                  << depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX + ".1"
1647                  << depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX;
1648     } else {
1649         depLibFilePath = depLibBase + QBS_HOST_DYNAMICLIB_SUFFIX;
1650     }
1651 
1652     QDir::setCurrent(testDataDir + "/clean");
1653 
1654     // Can't clean without a build graph.
1655     QbsRunParameters failParams("clean");
1656     failParams.expectFailure = true;
1657     QVERIFY(runQbs(failParams) != 0);
1658 
1659     // Default behavior: Remove all.
1660     QCOMPARE(runQbs(), 0);
1661     QVERIFY(regularFileExists(appObjectFilePath));
1662     QVERIFY(regularFileExists(appExeFilePath));
1663     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("clean"))), 0);
1664     QVERIFY(!QFile(appObjectFilePath).exists());
1665     QVERIFY(!QFile(appExeFilePath).exists());
1666     QVERIFY(!QFile(depObjectFilePath).exists());
1667     QVERIFY(!QFile(depLibFilePath).exists());
1668     for (const QString &symLink : qAsConst(symlinks))
1669         QVERIFY2(!symlinkExists(symLink), qPrintable(symLink));
1670 
1671     // Remove all, with a forced re-resolve in between.
1672     // This checks that rescuable artifacts are also removed.
1673     QCOMPARE(runQbs(QbsRunParameters("resolve",
1674                                      QStringList() << "modules.cpp.optimization:none")), 0);
1675     QCOMPARE(runQbs(), 0);
1676     QVERIFY(regularFileExists(appObjectFilePath));
1677     QVERIFY(regularFileExists(appExeFilePath));
1678     QCOMPARE(runQbs(QbsRunParameters("resolve",
1679                                      QStringList() << "modules.cpp.optimization:fast")), 0);
1680     QVERIFY(regularFileExists(appObjectFilePath));
1681     QVERIFY(regularFileExists(appExeFilePath));
1682     QCOMPARE(runQbs(QbsRunParameters("clean")), 0);
1683     QVERIFY(!QFile(appObjectFilePath).exists());
1684     QVERIFY(!QFile(appExeFilePath).exists());
1685     QVERIFY(!QFile(depObjectFilePath).exists());
1686     QVERIFY(!QFile(depLibFilePath).exists());
1687     for (const QString &symLink : qAsConst(symlinks))
1688         QVERIFY2(!symlinkExists(symLink), qPrintable(symLink));
1689 
1690     // Dry run.
1691     QCOMPARE(runQbs(), 0);
1692     QVERIFY(regularFileExists(appObjectFilePath));
1693     QVERIFY(regularFileExists(appExeFilePath));
1694     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("clean"), QStringList("-n"))), 0);
1695     QVERIFY(regularFileExists(appObjectFilePath));
1696     QVERIFY(regularFileExists(appExeFilePath));
1697     QVERIFY(regularFileExists(depObjectFilePath));
1698     QVERIFY(regularFileExists(depLibFilePath));
1699     for (const QString &symLink : qAsConst(symlinks))
1700         QVERIFY2(symlinkExists(symLink), qPrintable(symLink));
1701 
1702     // Product-wise, dependency only.
1703     QCOMPARE(runQbs(), 0);
1704     QVERIFY(regularFileExists(appObjectFilePath));
1705     QVERIFY(regularFileExists(appExeFilePath));
1706     QVERIFY(regularFileExists(depObjectFilePath));
1707     QVERIFY(regularFileExists(depLibFilePath));
1708     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("clean"), QStringList("-p") << "dep")), 0);
1709     QVERIFY(regularFileExists(appObjectFilePath));
1710     QVERIFY(regularFileExists(appExeFilePath));
1711     QVERIFY(!QFile(depObjectFilePath).exists());
1712     QVERIFY(!QFile(depLibFilePath).exists());
1713     for (const QString &symLink : qAsConst(symlinks))
1714         QVERIFY2(!symlinkExists(symLink), qPrintable(symLink));
1715 
1716     // Product-wise, dependent product only.
1717     QCOMPARE(runQbs(), 0);
1718     QVERIFY(regularFileExists(appObjectFilePath));
1719     QVERIFY(regularFileExists(appExeFilePath));
1720     QVERIFY(regularFileExists(depObjectFilePath));
1721     QVERIFY(regularFileExists(depLibFilePath));
1722     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("clean"), QStringList("-p") << "app")), 0);
1723     QVERIFY(!QFile(appObjectFilePath).exists());
1724     QVERIFY(!QFile(appExeFilePath).exists());
1725     QVERIFY(regularFileExists(depObjectFilePath));
1726     QVERIFY(regularFileExists(depLibFilePath));
1727     for (const QString &symLink : qAsConst(symlinks))
1728         QVERIFY2(symlinkExists(symLink), qPrintable(symLink));
1729 }
1730 
concurrentExecutor()1731 void TestBlackbox::concurrentExecutor()
1732 {
1733     QDir::setCurrent(testDataDir + "/concurrent-executor");
1734     QCOMPARE(runQbs(QStringList() << "-j" << "2"), 0);
1735     QVERIFY2(!m_qbsStderr.contains("ASSERT"), m_qbsStderr.constData());
1736 }
1737 
conditionalExport()1738 void TestBlackbox::conditionalExport()
1739 {
1740     QDir::setCurrent(testDataDir + "/conditional-export");
1741     QbsRunParameters params;
1742     params.expectFailure = true;
1743     QVERIFY(runQbs(params) != 0);
1744     QVERIFY2(m_qbsStderr.contains("missing define"), m_qbsStderr.constData());
1745 
1746     params.expectFailure = false;
1747     params.arguments << "project.enableExport:true";
1748     params.command = "resolve";
1749     QCOMPARE(runQbs(params), 0);
1750     params.command = "build";
1751     QCOMPARE(runQbs(params), 0);
1752 }
1753 
conditionalFileTagger()1754 void TestBlackbox::conditionalFileTagger()
1755 {
1756     QDir::setCurrent(testDataDir + "/conditional-filetagger");
1757     QbsRunParameters params(QStringList("products.theApp.enableTagger:false"));
1758     QCOMPARE(runQbs(params), 0);
1759     QVERIFY(!m_qbsStdout.contains("compiling"));
1760     params.arguments = QStringList("products.theApp.enableTagger:true");
1761     params.command = "resolve";
1762     QCOMPARE(runQbs(params), 0);
1763     params.command = "build";
1764     QCOMPARE(runQbs(params), 0);
1765     QVERIFY(m_qbsStdout.contains("compiling"));
1766 }
1767 
configure()1768 void TestBlackbox::configure()
1769 {
1770     QDir::setCurrent(testDataDir + "/configure");
1771     QCOMPARE(runQbs({"resolve"}), 0);
1772     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
1773         QSKIP("Cannot run binaries in cross-compiled build");
1774     QbsRunParameters params;
1775     params.command = "run";
1776     QCOMPARE(runQbs(params), 0);
1777     QVERIFY2(m_qbsStdout.contains("Configured at"), m_qbsStdout.constData());
1778 }
1779 
conflictingArtifacts()1780 void TestBlackbox::conflictingArtifacts()
1781 {
1782     QDir::setCurrent(testDataDir + "/conflicting-artifacts");
1783     QbsRunParameters params(QStringList() << "-n");
1784     params.expectFailure = true;
1785     QVERIFY(runQbs(params) != 0);
1786     QVERIFY2(m_qbsStderr.contains("Conflicting artifacts"), m_qbsStderr.constData());
1787 }
1788 
cxxLanguageVersion()1789 void TestBlackbox::cxxLanguageVersion()
1790 {
1791     QDir::setCurrent(testDataDir + "/cxx-language-version");
1792     rmDirR(relativeBuildDir());
1793     QFETCH(QString, version);
1794     QFETCH(QVariantMap, requiredFlags);
1795     QFETCH(QVariantMap, forbiddenFlags);
1796     QbsRunParameters resolveParams;
1797     resolveParams.command = "resolve";
1798     resolveParams.arguments << "--force-probe-execution";
1799     resolveParams.arguments << "modules.cpp.useLanguageVersionFallback:true";
1800     if (!version.isEmpty())
1801         resolveParams.arguments << ("modules.cpp.cxxLanguageVersion:" + version);
1802     QCOMPARE(runQbs(resolveParams), 0);
1803     QString mapKey;
1804     if (version == "c++17" && m_qbsStdout.contains("is even newer MSVC: true"))
1805         mapKey = "msvc-brandnew";
1806     else if (m_qbsStdout.contains("is newer MSVC: true"))
1807         mapKey = "msvc-new";
1808     else if (m_qbsStdout.contains("is older MSVC: true"))
1809         mapKey = "msvc_old";
1810     else if (m_qbsStdout.contains("is GCC: true"))
1811         mapKey = "gcc";
1812     QVERIFY2(!mapKey.isEmpty(), m_qbsStdout.constData());
1813     QbsRunParameters buildParams;
1814     buildParams.expectFailure = mapKey == "gcc" && (version == "c++17" || version == "c++21");
1815     buildParams.arguments = QStringList({"--command-echo-mode", "command-line"});
1816     const int retVal = runQbs(buildParams);
1817     if (!buildParams.expectFailure)
1818         QCOMPARE(retVal, 0);
1819     const QString requiredFlag = requiredFlags.value(mapKey).toString();
1820     if (!requiredFlag.isEmpty())
1821         QVERIFY2(m_qbsStdout.contains(requiredFlag.toLocal8Bit()), m_qbsStdout.constData());
1822     const QString forbiddenFlag = forbiddenFlags.value(mapKey).toString();
1823     if (!forbiddenFlag.isEmpty())
1824         QVERIFY2(!m_qbsStdout.contains(forbiddenFlag.toLocal8Bit()), m_qbsStdout.constData());
1825 }
1826 
cxxLanguageVersion_data()1827 void TestBlackbox::cxxLanguageVersion_data()
1828 {
1829     QTest::addColumn<QString>("version");
1830     QTest::addColumn<QVariantMap>("requiredFlags");
1831     QTest::addColumn<QVariantMap>("forbiddenFlags");
1832 
1833     QTest::newRow("C++98")
1834             << QString("c++98")
1835             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=c++98"))})
1836             << QVariantMap({std::make_pair(QString("msvc-old"), QString("/std:")),
1837                             std::make_pair(QString("msvc-new"), QString("/std:"))});
1838     QTest::newRow("C++11")
1839             << QString("c++11")
1840             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=c++0x"))})
1841             << QVariantMap({std::make_pair(QString("msvc-old"), QString("/std:")),
1842                             std::make_pair(QString("msvc-new"), QString("/std:"))});
1843     QTest::newRow("C++14")
1844             << QString("c++14")
1845             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=c++1y")),
1846                             std::make_pair(QString("msvc-new"), QString("/std:c++14"))
1847                            })
1848             << QVariantMap({std::make_pair(QString("msvc-old"), QString("/std:"))});
1849     QTest::newRow("C++17")
1850             << QString("c++17")
1851             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=c++1z")),
1852                             std::make_pair(QString("msvc-new"), QString("/std:c++latest")),
1853                             std::make_pair(QString("msvc-brandnew"), QString("/std:c++17"))
1854                            })
1855             << QVariantMap({std::make_pair(QString("msvc-old"), QString("/std:"))});
1856     QTest::newRow("C++21")
1857             << QString("c++21")
1858             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=c++21")),
1859                             std::make_pair(QString("msvc-new"), QString("/std:c++latest"))
1860                            })
1861             << QVariantMap({std::make_pair(QString("msvc-old"), QString("/std:"))});
1862     QTest::newRow("default")
1863             << QString()
1864             << QVariantMap()
1865             << QVariantMap({std::make_pair(QString("gcc"), QString("-std=")),
1866                             std::make_pair(QString("msvc-old"), QString("/std:")),
1867                             std::make_pair(QString("msvc-new"), QString("/std:"))});
1868 }
1869 
conanfileProbe_data()1870 void TestBlackbox::conanfileProbe_data()
1871 {
1872     QTest::addColumn<bool>("forceFailure");
1873 
1874     QTest::newRow("success") << false;
1875     QTest::newRow("failure") << true;
1876 }
1877 
conanfileProbe()1878 void TestBlackbox::conanfileProbe()
1879 {
1880     QFETCH(bool, forceFailure);
1881 
1882     QString executable = findExecutable({"conan"});
1883     if (executable.isEmpty())
1884         QSKIP("conan is not installed or not available in PATH.");
1885 
1886     // We first build a dummy package testlib and use that as dependency
1887     // in the testapp package.
1888     QDir::setCurrent(testDataDir + "/conanfile-probe/testlib");
1889     QStringList arguments { "create", "-o", "opt=True", "-s", "os=AIX", ".",
1890                             "testlib/1.2.3@qbs/testing" };
1891     QProcess conan;
1892     conan.start(executable, arguments);
1893     QVERIFY(waitForProcessSuccess(conan));
1894 
1895     QDir::setCurrent(testDataDir + "/conanfile-probe/testapp");
1896     QCOMPARE(runQbs(QbsRunParameters("resolve",
1897                                      {"--force-probe-execution",
1898                                       QStringLiteral("projects.conanfile-probe-project.forceFailure:") +
1899                                       (forceFailure ? "true" : "false")})), forceFailure ? 1 : 0);
1900 
1901     QFile file(relativeBuildDir() + "/results.json");
1902     QVERIFY(file.open(QIODevice::ReadOnly));
1903     QVariantMap actualResults = QJsonDocument::fromJson(file.readAll()).toVariant().toMap();
1904     const auto generatedFilesPath = actualResults.take("generatedFilesPath").toString();
1905     // We want to make sure that generatedFilesPath is under the project directory,
1906     // but we don't care about the actual name.
1907     QVERIFY(directoryExists(relativeBuildDir() + "/genconan/"
1908                             + QFileInfo(generatedFilesPath).baseName()));
1909 
1910     const QVariantMap expectedResults = {
1911         { "json", "TESTLIB_ENV_VAL" },
1912         { "dependencies", QVariantList{"testlib1", "testlib2"} },
1913     };
1914     QCOMPARE(actualResults, expectedResults);
1915 }
1916 
cpuFeatures()1917 void TestBlackbox::cpuFeatures()
1918 {
1919     QDir::setCurrent(testDataDir + "/cpu-features");
1920     QCOMPARE(runQbs(QbsRunParameters("resolve")), 0);
1921     const bool isX86 = m_qbsStdout.contains("is x86: true");
1922     const bool isX64 = m_qbsStdout.contains("is x64: true");
1923     if (!isX86 && !isX64) {
1924         QVERIFY2(m_qbsStdout.contains("is x86: false") && m_qbsStdout.contains("is x64: false"),
1925                  m_qbsStdout.constData());
1926         QSKIP("Not an x86 host");
1927     }
1928     const bool isGcc = m_qbsStdout.contains("is gcc: true");
1929     const bool isMsvc = m_qbsStdout.contains("is msvc: true");
1930     if (!isGcc && !isMsvc) {
1931         QVERIFY2(m_qbsStdout.contains("is gcc: false") && m_qbsStdout.contains("is msvc: false"),
1932                  m_qbsStdout.constData());
1933         QSKIP("Neither GCC nor MSVC");
1934     }
1935     QbsRunParameters params(QStringList{"--command-echo-mode", "command-line"});
1936     params.expectFailure = true;
1937     runQbs(params);
1938     if (isGcc) {
1939         QVERIFY2(m_qbsStdout.contains("-msse2") && m_qbsStdout.contains("-mavx")
1940                  && m_qbsStdout.contains("-mno-avx512f"), m_qbsStdout.constData());
1941     } else {
1942         QVERIFY2(m_qbsStdout.contains("/arch:AVX"), m_qbsStdout.constData());
1943         QVERIFY2(!m_qbsStdout.contains("/arch:AVX2"), m_qbsStdout.constData());
1944         QVERIFY2(m_qbsStdout.contains("/arch:SSE2") == isX86, m_qbsStdout.constData());
1945     }
1946 }
1947 
renameDependency()1948 void TestBlackbox::renameDependency()
1949 {
1950     QDir::setCurrent(testDataDir + "/renameDependency");
1951     if (QFile::exists("work"))
1952         rmDirR("work");
1953     QDir().mkdir("work");
1954     ccp("before", "work");
1955     QDir::setCurrent(testDataDir + "/renameDependency/work");
1956     QCOMPARE(runQbs(), 0);
1957 
1958     WAIT_FOR_NEW_TIMESTAMP();
1959     QFile::remove("lib.h");
1960     QFile::remove("lib.cpp");
1961     ccp("../after", ".");
1962     QbsRunParameters params;
1963     params.expectFailure = true;
1964     QVERIFY(runQbs(params) != 0);
1965     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
1966 }
1967 
separateDebugInfo()1968 void TestBlackbox::separateDebugInfo()
1969 {
1970     QDir::setCurrent(testDataDir + "/separate-debug-info");
1971     QCOMPARE(runQbs(QbsRunParameters(QStringList("qbs.debugInformation:true"))), 0);
1972     const bool isWindows = m_qbsStdout.contains("is windows: yes");
1973     const bool isNotWindows = m_qbsStdout.contains("is windows: no");
1974     QVERIFY(isWindows != isNotWindows);
1975     const bool isMacos = m_qbsStdout.contains("is macos: yes");
1976     const bool isNotMacos = m_qbsStdout.contains("is macos: no");
1977     QVERIFY(isMacos != isNotMacos);
1978     const bool isDarwin = m_qbsStdout.contains("is darwin: yes");
1979     const bool isNotDarwin = m_qbsStdout.contains("is darwin: no");
1980     QVERIFY(isDarwin != isNotDarwin);
1981 
1982     const SettingsPtr s = settings();
1983     Profile buildProfile(profileName(), s.get());
1984     QStringList toolchain = profileToolchain(buildProfile);
1985     if (isDarwin) {
1986         QVERIFY(directoryExists(relativeProductBuildDir("app1") + "/app1.app.dSYM"));
1987         QVERIFY(regularFileExists(relativeProductBuildDir("app1")
1988             + "/app1.app.dSYM/Contents/Info.plist"));
1989         QVERIFY(regularFileExists(relativeProductBuildDir("app1")
1990             + "/app1.app.dSYM/Contents/Resources/DWARF/app1"));
1991         QCOMPARE(QDir(relativeProductBuildDir("app1")
1992             + "/app1.app.dSYM/Contents/Resources/DWARF")
1993                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
1994         QVERIFY(!QFile::exists(relativeProductBuildDir("app2") + "/app2.app.dSYM"));
1995         QVERIFY(!QFile::exists(relativeProductBuildDir("app3") + "/app3.app.dSYM"));
1996         if (isMacos) {
1997             QVERIFY(regularFileExists(relativeProductBuildDir("app3")
1998                 + "/app3.app/Contents/MacOS/app3.dwarf"));
1999         } else {
2000             QVERIFY(regularFileExists(relativeProductBuildDir("app3")
2001                 + "/app3.app/app3.dwarf"));
2002         }
2003         QVERIFY(directoryExists(relativeProductBuildDir("app4") + "/app4.dSYM"));
2004         QVERIFY(regularFileExists(relativeProductBuildDir("app4")
2005             + "/app4.dSYM/Contents/Info.plist"));
2006         QVERIFY(regularFileExists(relativeProductBuildDir("app4")
2007             + "/app4.dSYM/Contents/Resources/DWARF/app4"));
2008         QCOMPARE(QDir(relativeProductBuildDir("app4")
2009             + "/app4.dSYM/Contents/Resources/DWARF")
2010                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
2011         QVERIFY(regularFileExists(relativeProductBuildDir("app5") + "/app5.dwarf"));
2012         QVERIFY(directoryExists(relativeProductBuildDir("foo1") + "/foo1.framework.dSYM"));
2013         QVERIFY(regularFileExists(relativeProductBuildDir("foo1")
2014             + "/foo1.framework.dSYM/Contents/Info.plist"));
2015         QVERIFY(regularFileExists(relativeProductBuildDir("foo1")
2016             + "/foo1.framework.dSYM/Contents/Resources/DWARF/foo1"));
2017         QCOMPARE(QDir(relativeProductBuildDir("foo1")
2018             + "/foo1.framework.dSYM/Contents/Resources/DWARF")
2019                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
2020         QVERIFY(!QFile::exists(relativeProductBuildDir("foo2") + "/foo2.framework.dSYM"));
2021         QVERIFY(!QFile::exists(relativeProductBuildDir("foo3") + "/foo3.framework.dSYM"));
2022         if (isMacos) {
2023             QVERIFY(regularFileExists(relativeProductBuildDir("foo3")
2024                 + "/foo3.framework/Versions/A/foo3.dwarf"));
2025         } else {
2026             QVERIFY(regularFileExists(relativeProductBuildDir("foo3")
2027                 + "/foo3.framework/foo3.dwarf"));
2028         }
2029         QVERIFY(directoryExists(relativeProductBuildDir("foo4") + "/libfoo4.dylib.dSYM"));
2030         QVERIFY(regularFileExists(relativeProductBuildDir("foo4")
2031             + "/libfoo4.dylib.dSYM/Contents/Info.plist"));
2032         QVERIFY(regularFileExists(relativeProductBuildDir("foo4")
2033             + "/libfoo4.dylib.dSYM/Contents/Resources/DWARF/libfoo4.dylib"));
2034         QCOMPARE(QDir(relativeProductBuildDir("foo4")
2035             + "/libfoo4.dylib.dSYM/Contents/Resources/DWARF")
2036                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
2037         QVERIFY(regularFileExists(relativeProductBuildDir("foo5") + "/libfoo5.dylib.dwarf"));
2038         QVERIFY(directoryExists(relativeProductBuildDir("bar1") + "/bar1.bundle.dSYM"));
2039         QVERIFY(regularFileExists(relativeProductBuildDir("bar1")
2040             + "/bar1.bundle.dSYM/Contents/Info.plist"));
2041         QVERIFY(regularFileExists(relativeProductBuildDir("bar1")
2042             + "/bar1.bundle.dSYM/Contents/Resources/DWARF/bar1"));
2043         QCOMPARE(QDir(relativeProductBuildDir("bar1")
2044             + "/bar1.bundle.dSYM/Contents/Resources/DWARF")
2045                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
2046         QVERIFY(!QFile::exists(relativeProductBuildDir("bar2") + "/bar2.bundle.dSYM"));
2047         QVERIFY(!QFile::exists(relativeProductBuildDir("bar3") + "/bar3.bundle.dSYM"));
2048         if (isMacos) {
2049             QVERIFY(regularFileExists(relativeProductBuildDir("bar3")
2050                 + "/bar3.bundle/Contents/MacOS/bar3.dwarf"));
2051         } else {
2052             QVERIFY(regularFileExists(relativeProductBuildDir("bar3")
2053                 + "/bar3.bundle/bar3.dwarf"));
2054         }
2055         QVERIFY(directoryExists(relativeProductBuildDir("bar4") + "/bar4.bundle.dSYM"));
2056         QVERIFY(regularFileExists(relativeProductBuildDir("bar4")
2057             + "/bar4.bundle.dSYM/Contents/Info.plist"));
2058         QVERIFY(regularFileExists(relativeProductBuildDir("bar4")
2059             + "/bar4.bundle.dSYM/Contents/Resources/DWARF/bar4.bundle"));
2060         QCOMPARE(QDir(relativeProductBuildDir("bar4")
2061             + "/bar4.bundle.dSYM/Contents/Resources/DWARF")
2062                 .entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).size(), 1);
2063         QVERIFY(regularFileExists(relativeProductBuildDir("bar5") + "/bar5.bundle.dwarf"));
2064     } else if (toolchain.contains("gcc")) {
2065         const QString exeSuffix = isWindows ? ".exe" : "";
2066         const QString dllPrefix = isWindows ? "" : "lib";
2067         const QString dllSuffix = isWindows ? ".dll" : ".so";
2068         QVERIFY(QFile::exists(relativeProductBuildDir("app1") + "/app1" + exeSuffix + ".debug"));
2069         QVERIFY(!QFile::exists(relativeProductBuildDir("app2") + "/app2" + exeSuffix + ".debug"));
2070         QVERIFY(QFile::exists(relativeProductBuildDir("foo1")
2071                               + '/' + dllPrefix + "foo1" + dllSuffix + ".debug"));
2072         QVERIFY(!QFile::exists(relativeProductBuildDir("foo2")
2073                                + '/' + "foo2" + dllSuffix + ".debug"));
2074         QVERIFY(QFile::exists(relativeProductBuildDir("bar1")
2075                               + '/' + dllPrefix +  "bar1" + dllSuffix + ".debug"));
2076         QVERIFY(!QFile::exists(relativeProductBuildDir("bar2")
2077                                + '/' + dllPrefix + "bar2" + dllSuffix + ".debug"));
2078     } else if (toolchain.contains("msvc")) {
2079         QVERIFY(QFile::exists(relativeProductBuildDir("app1") + "/app1.pdb"));
2080         QVERIFY(QFile::exists(relativeProductBuildDir("foo1") + "/foo1.pdb"));
2081         QVERIFY(QFile::exists(relativeProductBuildDir("bar1") + "/bar1.pdb"));
2082         // MSVC's linker even creates a pdb file if /Z7 is passed to the compiler.
2083     } else {
2084         QSKIP("Unsupported toolchain. Skipping.");
2085     }
2086 }
2087 
trackAddFile()2088 void TestBlackbox::trackAddFile()
2089 {
2090     QList<QByteArray> output;
2091     QDir::setCurrent(testDataDir + "/trackAddFile");
2092     if (QFile::exists("work"))
2093         rmDirR("work");
2094     QDir().mkdir("work");
2095     ccp("before", "work");
2096     QDir::setCurrent(testDataDir + "/trackAddFile/work");
2097     QCOMPARE(runQbs({"resolve"}), 0);
2098     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2099         QSKIP("Cannot run binaries in cross-compiled build");
2100     const QbsRunParameters runParams("run", QStringList{"-qp", "someapp"});
2101     QCOMPARE(runQbs(runParams), 0);
2102 
2103     output = m_qbsStdout.split('\n');
2104     QCOMPARE(output.takeFirst().trimmed().constData(), "Hello World!");
2105     QCOMPARE(output.takeFirst().trimmed().constData(), "NARF!");
2106     QString unchangedObjectFile = relativeBuildDir()
2107             + objectFileName("/someapp/narf.cpp", profileName());
2108     QDateTime unchangedObjectFileTime1 = QFileInfo(unchangedObjectFile).lastModified();
2109 
2110     WAIT_FOR_NEW_TIMESTAMP();
2111     ccp("../after", ".");
2112     touch("trackAddFile.qbs");
2113     touch("main.cpp");
2114     QCOMPARE(runQbs(runParams), 0);
2115 
2116     output = m_qbsStdout.split('\n');
2117     QCOMPARE(output.takeFirst().trimmed().constData(), "Hello World!");
2118     QCOMPARE(output.takeFirst().trimmed().constData(), "NARF!");
2119     QCOMPARE(output.takeFirst().trimmed().constData(), "ZORT!");
2120 
2121     // the object file of the untouched source should not have changed
2122     QDateTime unchangedObjectFileTime2 = QFileInfo(unchangedObjectFile).lastModified();
2123     QCOMPARE(unchangedObjectFileTime1, unchangedObjectFileTime2);
2124 }
2125 
trackExternalProductChanges()2126 void TestBlackbox::trackExternalProductChanges()
2127 {
2128     QDir::setCurrent(testDataDir + "/trackExternalProductChanges");
2129     QCOMPARE(runQbs(), 0);
2130     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
2131     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2132     QVERIFY(!m_qbsStdout.contains("compiling jsFileChange.cpp"));
2133     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2134 
2135     QbsRunParameters params;
2136     params.environment.insert("QBS_TEST_PULL_IN_FILE_VIA_ENV", "1");
2137     QCOMPARE(runQbs(params), 0);
2138     QVERIFY(!m_qbsStdout.contains("compiling main.cpp"));
2139     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2140     QVERIFY(!m_qbsStdout.contains("compiling jsFileChange.cpp"));
2141     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2142     params.command = "resolve";
2143     QCOMPARE(runQbs(params), 0);
2144     params.command = "build";
2145     QCOMPARE(runQbs(params), 0);
2146     QVERIFY(!m_qbsStdout.contains("compiling main.cpp"));
2147     QVERIFY(m_qbsStdout.contains("compiling environmentChange.cpp"));
2148     QVERIFY2(!m_qbsStdout.contains("compiling jsFileChange.cpp"), m_qbsStdout.constData());
2149     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2150 
2151     rmDirR(relativeBuildDir());
2152     QCOMPARE(runQbs(), 0);
2153     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
2154     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2155     QVERIFY(!m_qbsStdout.contains("compiling jsFileChange.cpp"));
2156     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2157 
2158     WAIT_FOR_NEW_TIMESTAMP();
2159     REPLACE_IN_FILE("fileList.js", "return []", "return ['jsFileChange.cpp']");
2160     QCOMPARE(runQbs(), 0);
2161     QVERIFY(!m_qbsStdout.contains("compiling main.cpp"));
2162     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2163     QVERIFY(m_qbsStdout.contains("compiling jsFileChange.cpp"));
2164     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2165 
2166     rmDirR(relativeBuildDir());
2167     REPLACE_IN_FILE("fileList.js", "['jsFileChange.cpp']", "[]");
2168     QCOMPARE(runQbs(), 0);
2169     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
2170     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2171     QVERIFY(!m_qbsStdout.contains("compiling jsFileChange.cpp"));
2172     QVERIFY(!m_qbsStdout.contains("compiling fileExists.cpp"));
2173 
2174     QFile cppFile("fileExists.cpp");
2175     QVERIFY(cppFile.open(QIODevice::WriteOnly));
2176     cppFile.write("void fileExists() { }\n");
2177     cppFile.close();
2178     QCOMPARE(runQbs(), 0);
2179     QVERIFY(!m_qbsStdout.contains("compiling main.cpp"));
2180     QVERIFY(!m_qbsStdout.contains("compiling environmentChange.cpp"));
2181     QVERIFY(!m_qbsStdout.contains("compiling jsFileChange.cpp"));
2182     QVERIFY(m_qbsStdout.contains("compiling fileExists.cpp"));
2183 
2184     rmDirR(relativeBuildDir());
2185     const SettingsPtr s = settings();
2186     const Profile profile(profileName(), s.get());
2187     const QStringList toolchainTypes = profileToolchain(profile);
2188     if (!toolchainTypes.contains("gcc"))
2189         QSKIP("Need GCC-like compiler to run this test");
2190     params.environment = QbsRunParameters::defaultEnvironment();
2191     params.environment.insert("INCLUDE_PATH_TEST", "1");
2192     params.expectFailure = true;
2193     QVERIFY(runQbs(params) != 0);
2194     QVERIFY2(m_qbsStderr.contains("hiddenheaderqbs.h"), m_qbsStderr.constData());
2195     params.command = "resolve";
2196     params.environment.insert("CPLUS_INCLUDE_PATH",
2197                               QDir::toNativeSeparators(QDir::currentPath() + "/hidden"));
2198     params.expectFailure = false;
2199     QCOMPARE(runQbs(params), 0);
2200     params.command = "build";
2201     QCOMPARE(runQbs(params), 0);
2202 }
2203 
trackGroupConditionChange()2204 void TestBlackbox::trackGroupConditionChange()
2205 {
2206     QbsRunParameters params;
2207     params.expectFailure = true;
2208     QDir::setCurrent(testDataDir + "/group-condition-change");
2209     QVERIFY(runQbs(params) != 0);
2210     QVERIFY(m_qbsStderr.contains("jibbetnich"));
2211 
2212     params.command = "resolve";
2213     params.arguments = QStringList("project.kaputt:false");
2214     params.expectFailure = false;
2215     QCOMPARE(runQbs(params), 0);
2216     QCOMPARE(runQbs(), 0);
2217 }
2218 
trackRemoveFile()2219 void TestBlackbox::trackRemoveFile()
2220 {
2221     QList<QByteArray> output;
2222     QDir::setCurrent(testDataDir + "/trackAddFile");
2223     if (QFile::exists("work"))
2224         rmDirR("work");
2225     QDir().mkdir("work");
2226     ccp("before", "work");
2227     ccp("after", "work");
2228     QDir::setCurrent(testDataDir + "/trackAddFile/work");
2229     QCOMPARE(runQbs({"resolve"}), 0);
2230     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2231         QSKIP("Cannot run binaries in cross-compiled build");
2232     const QbsRunParameters runParams("run", QStringList{"-qp", "someapp"});
2233     QCOMPARE(runQbs(runParams), 0);
2234     output = m_qbsStdout.split('\n');
2235     QCOMPARE(output.takeFirst().trimmed().constData(), "Hello World!");
2236     QCOMPARE(output.takeFirst().trimmed().constData(), "NARF!");
2237     QCOMPARE(output.takeFirst().trimmed().constData(), "ZORT!");
2238     QString unchangedObjectFile = relativeBuildDir()
2239             + objectFileName("/someapp/narf.cpp", profileName());
2240     QDateTime unchangedObjectFileTime1 = QFileInfo(unchangedObjectFile).lastModified();
2241 
2242     WAIT_FOR_NEW_TIMESTAMP();
2243     QFile::remove("trackAddFile.qbs");
2244     QFile::remove("main.cpp");
2245     QFile::copy("../before/trackAddFile.qbs", "trackAddFile.qbs");
2246     QFile::copy("../before/main.cpp", "main.cpp");
2247     QVERIFY(QFile::remove("zort.h"));
2248     QVERIFY(QFile::remove("zort.cpp"));
2249     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("resolve"))), 0);
2250 
2251     touch("main.cpp");
2252     touch("trackAddFile.qbs");
2253     QCOMPARE(runQbs(runParams), 0);
2254     output = m_qbsStdout.split('\n');
2255     QCOMPARE(output.takeFirst().trimmed().constData(), "Hello World!");
2256     QCOMPARE(output.takeFirst().trimmed().constData(), "NARF!");
2257 
2258     // the object file of the untouched source should not have changed
2259     QDateTime unchangedObjectFileTime2 = QFileInfo(unchangedObjectFile).lastModified();
2260     QCOMPARE(unchangedObjectFileTime1, unchangedObjectFileTime2);
2261 
2262     // the object file for the removed cpp file should have vanished too
2263     QVERIFY(!regularFileExists(relativeBuildDir()
2264                                + objectFileName("/someapp/zort.cpp", profileName())));
2265 }
2266 
trackAddFileTag()2267 void TestBlackbox::trackAddFileTag()
2268 {
2269     QList<QByteArray> output;
2270     QDir::setCurrent(testDataDir + "/trackFileTags");
2271     if (QFile::exists("work"))
2272         rmDirR("work");
2273     QDir().mkdir("work");
2274     ccp("before", "work");
2275     QDir::setCurrent(testDataDir + "/trackFileTags/work");
2276     QCOMPARE(runQbs({"resolve"}), 0);
2277     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2278         QSKIP("Cannot run binaries in cross-compiled build");
2279     const QbsRunParameters runParams("run", QStringList{"-qp", "someapp"});
2280     QCOMPARE(runQbs(runParams), 0);
2281     output = m_qbsStdout.split('\n');
2282     QCOMPARE(output.takeFirst().trimmed().constData(), "there's no foo here");
2283 
2284     WAIT_FOR_NEW_TIMESTAMP();
2285     ccp("../after", ".");
2286     touch("main.cpp");
2287     touch("trackFileTags.qbs");
2288     waitForFileUnlock();
2289     QCOMPARE(runQbs(runParams), 0);
2290     output = m_qbsStdout.split('\n');
2291     QCOMPARE(output.takeFirst().trimmed().constData(), "there's 15 foo here");
2292 }
2293 
trackRemoveFileTag()2294 void TestBlackbox::trackRemoveFileTag()
2295 {
2296     QList<QByteArray> output;
2297     QDir::setCurrent(testDataDir + "/trackFileTags");
2298     if (QFile::exists("work"))
2299         rmDirR("work");
2300     QDir().mkdir("work");
2301     ccp("after", "work");
2302     QDir::setCurrent(testDataDir + "/trackFileTags/work");
2303     QCOMPARE(runQbs({"resolve"}), 0);
2304     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2305         QSKIP("Cannot run binaries in cross-compiled build");
2306     const QbsRunParameters runParams("run", QStringList{"-qp", "someapp"});
2307     QCOMPARE(runQbs(runParams), 0);
2308 
2309     // check if the artifacts are here that will become stale in the 2nd step
2310     QVERIFY(regularFileExists(relativeProductBuildDir("someapp") + '/' + inputDirHash(".")
2311                               + objectFileName("/main_foo.cpp", profileName())));
2312     QVERIFY(regularFileExists(relativeProductBuildDir("someapp") + "/main_foo.cpp"));
2313     QVERIFY(regularFileExists(relativeProductBuildDir("someapp") + "/main.foo"));
2314     output = m_qbsStdout.split('\n');
2315     QCOMPARE(output.takeFirst().trimmed().constData(), "there's 15 foo here");
2316 
2317     WAIT_FOR_NEW_TIMESTAMP();
2318     ccp("../before", ".");
2319     touch("main.cpp");
2320     touch("trackFileTags.qbs");
2321     QCOMPARE(runQbs(runParams), 0);
2322     output = m_qbsStdout.split('\n');
2323     QCOMPARE(output.takeFirst().trimmed().constData(), "there's no foo here");
2324 
2325     // check if stale artifacts have been removed
2326     QCOMPARE(regularFileExists(relativeProductBuildDir("someapp") + '/' + inputDirHash(".")
2327                                + objectFileName("/main_foo.cpp", profileName())), false);
2328     QCOMPARE(regularFileExists(relativeProductBuildDir("someapp") + "/main_foo.cpp"), false);
2329     QCOMPARE(regularFileExists(relativeProductBuildDir("someapp") + "/main.foo"), false);
2330 }
2331 
trackAddProduct()2332 void TestBlackbox::trackAddProduct()
2333 {
2334     QDir::setCurrent(testDataDir + "/trackProducts");
2335     if (QFile::exists("work"))
2336         rmDirR("work");
2337     QDir().mkdir("work");
2338     ccp("before", "work");
2339     QDir::setCurrent(testDataDir + "/trackProducts/work");
2340     QbsRunParameters params(QStringList() << "-f" << "trackProducts.qbs");
2341     QCOMPARE(runQbs(params), 0);
2342     QVERIFY(m_qbsStdout.contains("compiling foo.cpp"));
2343     QVERIFY(m_qbsStdout.contains("compiling bar.cpp"));
2344     QVERIFY(m_qbsStdout.contains("linking product1"));
2345     QVERIFY(m_qbsStdout.contains("linking product2"));
2346 
2347     WAIT_FOR_NEW_TIMESTAMP();
2348     ccp("../after", ".");
2349     touch("trackProducts.qbs");
2350     QCOMPARE(runQbs(params), 0);
2351     QVERIFY(m_qbsStdout.contains("compiling zoo.cpp"));
2352     QVERIFY(m_qbsStdout.contains("linking product3"));
2353     QVERIFY(!m_qbsStdout.contains("compiling foo.cpp"));
2354     QVERIFY(!m_qbsStdout.contains("compiling bar.cpp"));
2355     QVERIFY(!m_qbsStdout.contains("linking product1"));
2356     QVERIFY(!m_qbsStdout.contains("linking product2"));
2357 }
2358 
trackRemoveProduct()2359 void TestBlackbox::trackRemoveProduct()
2360 {
2361     QDir::setCurrent(testDataDir + "/trackProducts");
2362     if (QFile::exists("work"))
2363         rmDirR("work");
2364     QDir().mkdir("work");
2365     ccp("before", "work");
2366     ccp("after", "work");
2367     QDir::setCurrent(testDataDir + "/trackProducts/work");
2368     QbsRunParameters params(QStringList() << "-f" << "trackProducts.qbs");
2369     QCOMPARE(runQbs(params), 0);
2370     QVERIFY(m_qbsStdout.contains("compiling foo.cpp"));
2371     QVERIFY(m_qbsStdout.contains("compiling bar.cpp"));
2372     QVERIFY(m_qbsStdout.contains("compiling zoo.cpp"));
2373     QVERIFY(m_qbsStdout.contains("linking product1"));
2374     QVERIFY(m_qbsStdout.contains("linking product2"));
2375     QVERIFY(m_qbsStdout.contains("linking product3"));
2376 
2377     WAIT_FOR_NEW_TIMESTAMP();
2378     QFile::remove("zoo.cpp");
2379     QFile::remove("product3.qbs");
2380     copyFileAndUpdateTimestamp("../before/trackProducts.qbs", "trackProducts.qbs");
2381     QCOMPARE(runQbs(params), 0);
2382     QVERIFY(!m_qbsStdout.contains("compiling foo.cpp"));
2383     QVERIFY(!m_qbsStdout.contains("compiling bar.cpp"));
2384     QVERIFY(!m_qbsStdout.contains("compiling zoo.cpp"));
2385     QVERIFY(!m_qbsStdout.contains("linking product1"));
2386     QVERIFY(!m_qbsStdout.contains("linking product2"));
2387     QVERIFY(!m_qbsStdout.contains("linking product3"));
2388 }
2389 
wildcardRenaming()2390 void TestBlackbox::wildcardRenaming()
2391 {
2392     QDir::setCurrent(testDataDir + "/wildcard_renaming");
2393     QCOMPARE(runQbs(QbsRunParameters("install")), 0);
2394     QVERIFY(QFileInfo(defaultInstallRoot + "/pioniere.txt").exists());
2395     WAIT_FOR_NEW_TIMESTAMP();
2396     QFile::rename(QDir::currentPath() + "/pioniere.txt", QDir::currentPath() + "/fdj.txt");
2397     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("install"),
2398                                      QStringList("--clean-install-root"))), 0);
2399     QVERIFY(!QFileInfo(defaultInstallRoot + "/pioniere.txt").exists());
2400     QVERIFY(QFileInfo(defaultInstallRoot + "/fdj.txt").exists());
2401 }
2402 
recursiveRenaming()2403 void TestBlackbox::recursiveRenaming()
2404 {
2405     QDir::setCurrent(testDataDir + "/recursive_renaming");
2406     QCOMPARE(runQbs(QbsRunParameters("install")), 0);
2407     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/wasser.txt").exists());
2408     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/subdir/blubb.txt").exists());
2409     WAIT_FOR_NEW_TIMESTAMP();
2410     QVERIFY(QFile::rename(QDir::currentPath() + "/dir/wasser.txt", QDir::currentPath() + "/dir/wein.txt"));
2411     QCOMPARE(runQbs(QbsRunParameters(QStringLiteral("install"),
2412                                      QStringList("--clean-install-root"))), 0);
2413     QVERIFY(!QFileInfo(defaultInstallRoot + "/dir/wasser.txt").exists());
2414     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/wein.txt").exists());
2415     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/subdir/blubb.txt").exists());
2416 }
2417 
recursiveWildcards()2418 void TestBlackbox::recursiveWildcards()
2419 {
2420     QDir::setCurrent(testDataDir + "/recursive_wildcards");
2421     QCOMPARE(runQbs(QbsRunParameters("install")), 0);
2422     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file1.txt").exists());
2423     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file2.txt").exists());
2424     QFile outputFile(defaultInstallRoot + "/output.txt");
2425     QVERIFY2(outputFile.open(QIODevice::ReadOnly), qPrintable(outputFile.errorString()));
2426     QCOMPARE(outputFile.readAll(), QByteArray("file1.txtfile2.txt"));
2427     outputFile.close();
2428     WAIT_FOR_NEW_TIMESTAMP();
2429     QFile newFile("dir/subdir/file3.txt");
2430     QVERIFY2(newFile.open(QIODevice::WriteOnly), qPrintable(newFile.errorString()));
2431     newFile.close();
2432     QCOMPARE(runQbs(QbsRunParameters("install")), 0);
2433     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file3.txt").exists());
2434     QVERIFY2(outputFile.open(QIODevice::ReadOnly), qPrintable(outputFile.errorString()));
2435     QCOMPARE(outputFile.readAll(), QByteArray("file1.txtfile2.txtfile3.txt"));
2436     outputFile.close();
2437     WAIT_FOR_NEW_TIMESTAMP();
2438     QVERIFY2(newFile.remove(), qPrintable(newFile.errorString()));
2439     QVERIFY2(!newFile.exists(), qPrintable(newFile.fileName()));
2440     QCOMPARE(runQbs(QbsRunParameters("install", QStringList{ "--clean-install-root"})), 0);
2441     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file1.txt").exists());
2442     QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file2.txt").exists());
2443     QVERIFY(!QFileInfo(defaultInstallRoot + "/dir/file3.txt").exists());
2444     QVERIFY2(outputFile.open(QIODevice::ReadOnly), qPrintable(outputFile.errorString()));
2445     QCOMPARE(outputFile.readAll(), QByteArray("file1.txtfile2.txt"));
2446 }
2447 
referenceErrorInExport()2448 void TestBlackbox::referenceErrorInExport()
2449 {
2450     QDir::setCurrent(testDataDir + "/referenceErrorInExport");
2451     QbsRunParameters params;
2452     params.expectFailure = true;
2453     QVERIFY(runQbs(params) != 0);
2454     QVERIFY(m_qbsStderr.contains(
2455         "referenceErrorInExport.qbs:15:12 ReferenceError: Can't find variable: includePaths"));
2456 }
2457 
removeDuplicateLibraries_data()2458 void TestBlackbox::removeDuplicateLibraries_data()
2459 {
2460     QTest::addColumn<bool>("removeDuplicates");
2461     QTest::newRow("remove duplicates") << true;
2462     QTest::newRow("don't remove duplicates") << false;
2463 }
2464 
removeDuplicateLibraries()2465 void TestBlackbox::removeDuplicateLibraries()
2466 {
2467     QDir::setCurrent(testDataDir + "/remove-duplicate-libs");
2468     QFETCH(bool, removeDuplicates);
2469     const QbsRunParameters resolveParams("resolve", {"-f", "remove-duplicate-libs.qbs",
2470             "project.removeDuplicates:" + QString(removeDuplicates? "true" : "false")});
2471     QCOMPARE(runQbs(resolveParams), 0);
2472     const bool isBfd = m_qbsStdout.contains("is bfd linker: true");
2473     const bool isNotBfd = m_qbsStdout.contains("is bfd linker: false");
2474     QVERIFY2(isBfd != isNotBfd, m_qbsStdout.constData());
2475     QbsRunParameters buildParams("build");
2476     buildParams.expectFailure = removeDuplicates && isBfd;
2477     QCOMPARE(runQbs(buildParams) == 0, !buildParams.expectFailure);
2478 }
2479 
reproducibleBuild()2480 void TestBlackbox::reproducibleBuild()
2481 {
2482     const SettingsPtr s = settings();
2483     const Profile profile(profileName(), s.get());
2484     const QStringList toolchains = profileToolchain(profile);
2485     if (!toolchains.contains("gcc"))
2486         QSKIP("reproducible builds only supported for gcc");
2487     if (toolchains.contains("clang"))
2488         QSKIP("reproducible builds are not supported for clang");
2489 
2490     QFETCH(bool, reproducible);
2491 
2492     QDir::setCurrent(testDataDir + "/reproducible-build");
2493     QbsRunParameters params;
2494     params.arguments << QString("modules.cpp.enableReproducibleBuilds:")
2495                         + (reproducible ? "true" : "false");
2496     rmDirR(relativeBuildDir());
2497     QCOMPARE(runQbs(params), 0);
2498     QFile object(relativeProductBuildDir("the product") + '/' + inputDirHash(".") + '/'
2499                  + objectFileName("file1.cpp", profileName()));
2500     QVERIFY2(object.open(QIODevice::ReadOnly), qPrintable(object.fileName()));
2501     const QByteArray oldContents = object.readAll();
2502     object.close();
2503     QCOMPARE(runQbs(QbsRunParameters("clean")), 0);
2504     QVERIFY(!object.exists());
2505     QCOMPARE(runQbs(params), 0);
2506     if (reproducible) {
2507         QVERIFY(object.open(QIODevice::ReadOnly));
2508         const QByteArray newContents = object.readAll();
2509         QVERIFY(oldContents == newContents);
2510         object.close();
2511     }
2512     QCOMPARE(runQbs(QbsRunParameters("clean")), 0);
2513 }
2514 
reproducibleBuild_data()2515 void TestBlackbox::reproducibleBuild_data()
2516 {
2517     QTest::addColumn<bool>("reproducible");
2518     QTest::newRow("non-reproducible build") << false;
2519     QTest::newRow("reproducible build") << true;
2520 }
2521 
responseFiles()2522 void TestBlackbox::responseFiles()
2523 {
2524     QDir::setCurrent(testDataDir + "/response-files");
2525     QCOMPARE(runQbs({"resolve"}), 0);
2526     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2527         QSKIP("Cannot run binaries in cross-compiled build");
2528     QbsRunParameters params;
2529     params.command = "install";
2530     params.arguments << "--install-root" << "installed";
2531     QCOMPARE(runQbs(params), 0);
2532     QFile file("installed/response-file-content.txt");
2533     QVERIFY(file.open(QIODevice::ReadOnly));
2534     const QList<QByteArray> expected = QList<QByteArray>()
2535             << "foo" << qbs::Internal::shellQuote(QStringLiteral("with space")).toUtf8()
2536             << "bar" << "";
2537     QList<QByteArray> lines = file.readAll().split('\n');
2538     for (auto &line : lines)
2539         line = line.trimmed();
2540     QCOMPARE(lines, expected);
2541 }
2542 
retaggedOutputArtifact()2543 void TestBlackbox::retaggedOutputArtifact()
2544 {
2545     QDir::setCurrent(testDataDir + "/retagged-output-artifact");
2546     QbsRunParameters resolveParams("resolve");
2547     resolveParams.arguments = QStringList("products.p.useTag1:true");
2548     QCOMPARE(runQbs(resolveParams), 0);
2549     QCOMPARE(runQbs(), 0);
2550     const QString a2 = relativeProductBuildDir("p") + "/a2.txt";
2551     const QString a3 = relativeProductBuildDir("p") + "/a3.txt";
2552     QVERIFY2(QFile::exists(a2), qPrintable(a2));
2553     QVERIFY2(!QFile::exists(a3), qPrintable(a3));
2554     resolveParams.arguments = QStringList("products.p.useTag1:false");
2555     QCOMPARE(runQbs(resolveParams), 0);
2556     QCOMPARE(runQbs(), 0);
2557     QVERIFY2(!QFile::exists(a2), qPrintable(a2));
2558     QVERIFY2(QFile::exists(a3), qPrintable(a3));
2559     resolveParams.arguments = QStringList("products.p.useTag1:true");
2560     QCOMPARE(runQbs(resolveParams), 0);
2561     QCOMPARE(runQbs(), 0);
2562     QVERIFY2(QFile::exists(a2), qPrintable(a2));
2563     QVERIFY2(!QFile::exists(a3), qPrintable(a3));
2564 }
2565 
ruleConditions()2566 void TestBlackbox::ruleConditions()
2567 {
2568     QDir::setCurrent(testDataDir + "/ruleConditions");
2569     QCOMPARE(runQbs(), 0);
2570     QVERIFY(QFileInfo(relativeExecutableFilePath("zorted")).exists());
2571     QVERIFY(QFileInfo(relativeExecutableFilePath("unzorted")).exists());
2572     QVERIFY(QFileInfo(relativeProductBuildDir("zorted") + "/zorted.foo.narf.zort").exists());
2573     QVERIFY(!QFileInfo(relativeProductBuildDir("unzorted") + "/unzorted.foo.narf.zort").exists());
2574 }
2575 
ruleConnectionWithExcludedInputs()2576 void TestBlackbox::ruleConnectionWithExcludedInputs()
2577 {
2578     QDir::setCurrent(testDataDir + "/rule-connection-with-excluded-inputs");
2579     QCOMPARE(runQbs(), 0);
2580     QVERIFY2(m_qbsStdout.contains("inputs.x: 2") && m_qbsStdout.contains("inputs.y: 0"),
2581              m_qbsStdout.constData());
2582 }
2583 
ruleCycle()2584 void TestBlackbox::ruleCycle()
2585 {
2586     QDir::setCurrent(testDataDir + "/ruleCycle");
2587     QbsRunParameters params;
2588     params.expectFailure = true;
2589     QVERIFY(runQbs(params) != 0);
2590     QVERIFY(m_qbsStderr.contains("Cycle detected in rule dependencies"));
2591 }
2592 
ruleWithNoInputs()2593 void TestBlackbox::ruleWithNoInputs()
2594 {
2595     QDir::setCurrent(testDataDir + "/rule-with-no-inputs");
2596     QVERIFY2(runQbs() == 0, m_qbsStderr.constData());
2597     QVERIFY2(m_qbsStdout.contains("running the rule"), m_qbsStdout.constData());
2598     QVERIFY2(m_qbsStdout.contains("creating output"), m_qbsStdout.constData());
2599     QVERIFY2(runQbs() == 0, m_qbsStderr.constData());
2600     QVERIFY2(!m_qbsStdout.contains("running the rule"), m_qbsStdout.constData());
2601     QVERIFY2(!m_qbsStdout.contains("creating output"), m_qbsStdout.constData());
2602     QbsRunParameters params("resolve", QStringList() << "products.theProduct.version:1");
2603     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2604     params.command = "build";
2605     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2606     QVERIFY2(!m_qbsStdout.contains("running the rule"), m_qbsStdout.constData());
2607     QVERIFY2(!m_qbsStdout.contains("creating output"), m_qbsStdout.constData());
2608     params.command = "resolve";
2609     params.arguments = QStringList() << "products.theProduct.version:2";
2610     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2611     params.command = "build";
2612     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2613     QVERIFY2(!m_qbsStdout.contains("running the rule"), m_qbsStdout.constData());
2614     QVERIFY2(m_qbsStdout.contains("creating output"), m_qbsStdout.constData());
2615     params.command = "resolve";
2616     params.arguments = QStringList() << "products.theProduct.version:2"
2617                                      << "products.theProduct.dummy:true";
2618     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2619     params.command = "build";
2620     QVERIFY2(runQbs(params) == 0, m_qbsStderr.constData());
2621     QVERIFY2(m_qbsStdout.contains("running the rule"), m_qbsStdout.constData());
2622     QVERIFY2(!m_qbsStdout.contains("creating output"), m_qbsStdout.constData());
2623 }
2624 
ruleWithNonRequiredInputs()2625 void TestBlackbox::ruleWithNonRequiredInputs()
2626 {
2627     QDir::setCurrent(testDataDir + "/rule-with-non-required-inputs");
2628     QbsRunParameters params("build", {"products.p.enableTagger:false"});
2629     QCOMPARE(runQbs(params), 0);
2630     QFile outFile(relativeProductBuildDir("p") + "/output.txt");
2631     QVERIFY2(outFile.open(QIODevice::ReadOnly), qPrintable(outFile.errorString()));
2632     QByteArray output = outFile.readAll();
2633     QCOMPARE(output, QByteArray("()"));
2634     outFile.close();
2635     params.command = "resolve";
2636     params.arguments = QStringList({"products.p.enableTagger:true"});
2637     QCOMPARE(runQbs(params), 0);
2638     QCOMPARE(runQbs(), 0);
2639     QVERIFY2(outFile.open(QIODevice::ReadOnly), qPrintable(outFile.errorString()));
2640     output = outFile.readAll();
2641     QCOMPARE(output, QByteArray("(a.inp,b.inp,c.inp,)"));
2642     QCOMPARE(runQbs(), 0);
2643     QVERIFY2(!m_qbsStdout.contains("Generating"), m_qbsStdout.constData());
2644     WAIT_FOR_NEW_TIMESTAMP();
2645     touch("a.inp");
2646     QCOMPARE(runQbs(), 0);
2647     QVERIFY2(m_qbsStdout.contains("Generating"), m_qbsStdout.constData());
2648 }
2649 
sanitizer_data()2650 void TestBlackbox::sanitizer_data()
2651 {
2652     QTest::addColumn<QString>("sanitizer");
2653     QTest::newRow("none") << QString();
2654     QTest::newRow("address") << QStringLiteral("address");
2655     QTest::newRow("undefined") << QStringLiteral("undefined");
2656     QTest::newRow("thread") << QStringLiteral("thread");
2657 }
2658 
sanitizer()2659 void TestBlackbox::sanitizer()
2660 {
2661     QFETCH(QString, sanitizer);
2662     QDir::setCurrent(testDataDir + "/sanitizer");
2663     rmDirR(relativeBuildDir());
2664     QbsRunParameters params("build", {"--command-echo-mode", "command-line"});
2665     if (!sanitizer.isEmpty()) {
2666         params.arguments.append(
2667                 {QStringLiteral("products.sanitizer.sanitizer:\"") + sanitizer + "\""});
2668     }
2669     QCOMPARE(runQbs(params), 0);
2670     if (m_qbsStdout.contains(QByteArrayLiteral("Compiler does not support sanitizer")))
2671         QSKIP("Compiler does not support the specified sanitizer");
2672     if (!sanitizer.isEmpty()) {
2673         QVERIFY2(m_qbsStdout.contains(QByteArrayLiteral("-fsanitize=") + sanitizer.toLatin1()),
2674                  qPrintable(m_qbsStdout));
2675     } else {
2676         QVERIFY2(!m_qbsStdout.contains(QByteArrayLiteral("-fsanitize=")), qPrintable(m_qbsStdout));
2677     }
2678 }
2679 
scannerItem()2680 void TestBlackbox::scannerItem()
2681 {
2682     QDir::setCurrent(testDataDir + "/scanner-item");
2683     QCOMPARE(runQbs(), 0);
2684     QVERIFY2(m_qbsStdout.contains("handling file1.in"), m_qbsStdout.constData());
2685     QVERIFY2(m_qbsStdout.contains("handling file2.in"), m_qbsStdout.constData());
2686     WAIT_FOR_NEW_TIMESTAMP();
2687     touch("subdir1/file.inc");
2688     QCOMPARE(runQbs(), 0);
2689     QVERIFY2(m_qbsStdout.contains("handling file1.in"), m_qbsStdout.constData());
2690     QVERIFY2(!m_qbsStdout.contains("handling file2.in"), m_qbsStdout.constData());
2691     WAIT_FOR_NEW_TIMESTAMP();
2692     touch("subdir2/file.inc");
2693     QCOMPARE(runQbs(), 0);
2694     QVERIFY2(!m_qbsStdout.contains("handling file1.in"), m_qbsStdout.constData());
2695     QVERIFY2(m_qbsStdout.contains("handling file2.in"), m_qbsStdout.constData());
2696 }
2697 
scanResultInOtherProduct()2698 void TestBlackbox::scanResultInOtherProduct()
2699 {
2700     QDir::setCurrent(testDataDir + "/scan-result-in-other-product");
2701     QCOMPARE(runQbs(QStringList("-vv")), 0);
2702     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2703     QVERIFY2(m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2704     QVERIFY2(!m_qbsStderr.contains("The file dependency might get lost during change tracking"),
2705              m_qbsStderr.constData());
2706     WAIT_FOR_NEW_TIMESTAMP();
2707     REPLACE_IN_FILE("other/other.qbs", "blubb", "blubb2");
2708     QCOMPARE(runQbs(), 0);
2709     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2710     QVERIFY2(m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2711     WAIT_FOR_NEW_TIMESTAMP();
2712     touch("lib/lib.h");
2713     QCOMPARE(runQbs(), 0);
2714     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2715     QVERIFY2(!m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2716 }
2717 
scanResultInNonDependency()2718 void TestBlackbox::scanResultInNonDependency()
2719 {
2720     QDir::setCurrent(testDataDir + "/scan-result-in-non-dependency");
2721     QCOMPARE(runQbs(QStringList("-vv")), 0);
2722     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2723     QVERIFY2(m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2724     QVERIFY2(m_qbsStderr.contains("The file dependency might get lost during change tracking"),
2725              m_qbsStderr.constData());
2726     WAIT_FOR_NEW_TIMESTAMP();
2727     REPLACE_IN_FILE("other/other.qbs", "blubb", "blubb2");
2728     QCOMPARE(runQbs(), 0);
2729     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2730     QVERIFY2(m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2731     WAIT_FOR_NEW_TIMESTAMP();
2732     touch("lib/lib.h");
2733     QCOMPARE(runQbs(), 0);
2734     QEXPECT_FAIL("", "QBS-1532", Continue);
2735     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
2736     QVERIFY2(!m_qbsStdout.contains("generating text file"), m_qbsStdout.constData());
2737 }
2738 
setupBuildEnvironment()2739 void TestBlackbox::setupBuildEnvironment()
2740 {
2741     QDir::setCurrent(testDataDir + "/setup-build-environment");
2742     QCOMPARE(runQbs(), 0);
2743     QFile f(relativeProductBuildDir("first_product") + QLatin1String("/m.output"));
2744     QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(f.errorString()));
2745     QCOMPARE(f.readAll().trimmed(), QByteArray("1"));
2746     f.close();
2747     f.setFileName(relativeProductBuildDir("second_product") + QLatin1String("/m.output"));
2748     QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(f.errorString()));
2749     QCOMPARE(f.readAll().trimmed(), QByteArray());
2750 }
2751 
setupRunEnvironment()2752 void TestBlackbox::setupRunEnvironment()
2753 {
2754     QDir::setCurrent(testDataDir + "/setup-run-environment");
2755     QCOMPARE(runQbs(QbsRunParameters("resolve")), 0);
2756     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
2757         QSKIP("Cannot run binaries in cross-compiled build");
2758     QbsRunParameters failParams("run", QStringList({"--setup-run-env-config",
2759                                                     "ignore-lib-dependencies"}));
2760     failParams.expectFailure = true;
2761     failParams.expectCrash = m_qbsStdout.contains("is windows");
2762     QVERIFY(runQbs(QbsRunParameters(failParams)) != 0);
2763     QVERIFY2(failParams.expectCrash || m_qbsStderr.contains("lib"), m_qbsStderr.constData());
2764     QCOMPARE(runQbs(QbsRunParameters("run")), 0);
2765     QbsRunParameters dryRunParams("run", QStringList("--dry-run"));
2766     dryRunParams.buildDirectory = "dryrun";
2767     QCOMPARE(runQbs(dryRunParams), 0);
2768     const QString appFilePath = QDir::currentPath() + "/dryrun/"
2769             + relativeExecutableFilePath("app");
2770     QVERIFY2(m_qbsStdout.contains("Would start target")
2771              && m_qbsStdout.contains(QDir::toNativeSeparators(appFilePath).toLocal8Bit()),
2772              m_qbsStdout.constData());
2773 }
2774 
smartRelinking()2775 void TestBlackbox::smartRelinking()
2776 {
2777     QDir::setCurrent(testDataDir + "/smart-relinking");
2778     rmDirR(relativeBuildDir());
2779     QFETCH(bool, strictChecking);
2780     QbsRunParameters params(QStringList() << (QString("modules.cpp.exportedSymbolsCheckMode:%1")
2781             .arg(strictChecking ? "strict" : "ignore-undefined")));
2782     QCOMPARE(runQbs(params), 0);
2783     if (m_qbsStdout.contains("project disabled"))
2784         QSKIP("Test does not apply on this target");
2785     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2786     QVERIFY2(m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2787 
2788     // Irrelevant change.
2789     WAIT_FOR_NEW_TIMESTAMP();
2790     touch("lib.cpp");
2791     QCOMPARE(runQbs(params), 0);
2792     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2793     QVERIFY2(!m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2794 
2795     // Add new private symbol.
2796     WAIT_FOR_NEW_TIMESTAMP();
2797     params.command = "resolve";
2798     params.arguments << "products.lib.defines:PRIV2";
2799     QCOMPARE(runQbs(params), 0);
2800     QCOMPARE(runQbs(), 0);
2801     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2802     QVERIFY2(!m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2803 
2804     // Remove private symbol.
2805     WAIT_FOR_NEW_TIMESTAMP();
2806     params.arguments.removeLast();
2807     QCOMPARE(runQbs(params), 0);
2808     QCOMPARE(runQbs(), 0);
2809     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2810     QVERIFY2(!m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2811 
2812     // Add new public symbol.
2813     WAIT_FOR_NEW_TIMESTAMP();
2814     params.arguments << "products.lib.defines:PUB2";
2815     QCOMPARE(runQbs(params), 0);
2816     QCOMPARE(runQbs(), 0);
2817     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2818     QVERIFY2(m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2819 
2820     // Remove public symbol.
2821     WAIT_FOR_NEW_TIMESTAMP();
2822     params.arguments.removeLast();
2823     QCOMPARE(runQbs(params), 0);
2824     QCOMPARE(runQbs(), 0);
2825     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2826     QVERIFY2(m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2827 
2828     // Add new undefined symbol.
2829     WAIT_FOR_NEW_TIMESTAMP();
2830     params.arguments << "products.lib.defines:PRINTF";
2831     QCOMPARE(runQbs(params), 0);
2832     QCOMPARE(runQbs(), 0);
2833     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2834     QVERIFY2(strictChecking == m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2835 
2836     // Remove undefined symbol.
2837     WAIT_FOR_NEW_TIMESTAMP();
2838     params.arguments.removeLast();
2839     QCOMPARE(runQbs(params), 0);
2840     QCOMPARE(runQbs(), 0);
2841     QVERIFY2(m_qbsStdout.contains("linking lib"), m_qbsStdout.constData());
2842     QVERIFY2(strictChecking == m_qbsStdout.contains("linking app"), m_qbsStdout.constData());
2843 }
2844 
smartRelinking_data()2845 void TestBlackbox::smartRelinking_data()
2846 {
2847     QTest::addColumn<bool>("strictChecking");
2848     QTest::newRow("strict checking") << true;
2849     QTest::newRow("ignore undefined") << false;
2850 }
2851 
2852 
soName(const QString & readElfPath,const QString & libFilePath)2853 static QString soName(const QString &readElfPath, const QString &libFilePath)
2854 {
2855     QProcess readElf;
2856     auto env = QProcessEnvironment::systemEnvironment();
2857     env.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); // force readelf to use US encoding
2858     readElf.setProcessEnvironment(env);
2859     readElf.start(readElfPath, QStringList() << "-a" << libFilePath);
2860     if (!readElf.waitForStarted() || !readElf.waitForFinished() || readElf.exitCode() != 0) {
2861         qDebug() << readElf.errorString() << readElf.readAllStandardError();
2862         return {};
2863     }
2864     const QByteArray output = readElf.readAllStandardOutput();
2865     const QByteArray magicString = "Library soname: [";
2866     const int magicStringIndex = output.indexOf(magicString);
2867     if (magicStringIndex == -1)
2868         return {};
2869     const int endIndex = output.indexOf(']', magicStringIndex);
2870     if (endIndex == -1)
2871         return {};
2872     const int nameIndex = magicStringIndex + magicString.size();
2873     const QByteArray theName = output.mid(nameIndex, endIndex - nameIndex);
2874     return QString::fromLatin1(theName);
2875 }
2876 
soVersion()2877 void TestBlackbox::soVersion()
2878 {
2879     const QString readElfPath = findExecutable(QStringList("readelf"));
2880     if (readElfPath.isEmpty() || readElfPath.endsWith("exe"))
2881         QSKIP("soversion test not applicable on this system");
2882     QDir::setCurrent(testDataDir + "/soversion");
2883 
2884     QFETCH(QString, soVersion);
2885     QFETCH(bool, useVersion);
2886     QFETCH(QString, expectedSoName);
2887 
2888     QbsRunParameters params;
2889     params.arguments << ("products.mylib.useVersion:" + QString((useVersion ? "true" : "false")));
2890     if (!soVersion.isNull())
2891         params.arguments << ("modules.cpp.soVersion:" + soVersion);
2892     const QString libFilePath = relativeProductBuildDir("mylib") + "/libmylib.so"
2893             + (useVersion ? ".1.2.3" : QString());
2894     rmDirR(relativeBuildDir());
2895     QCOMPARE(runQbs(params), 0);
2896     QVERIFY2(regularFileExists(libFilePath), qPrintable(libFilePath));
2897     QCOMPARE(soName(readElfPath, libFilePath), expectedSoName);
2898 }
2899 
soVersion_data()2900 void TestBlackbox::soVersion_data()
2901 {
2902     QTest::addColumn<QString>("soVersion");
2903     QTest::addColumn<bool>("useVersion");
2904     QTest::addColumn<QString>("expectedSoName");
2905 
2906     QTest::newRow("default") << QString() << true << QString("libmylib.so.1");
2907     QTest::newRow("explicit soVersion") << QString("1.2") << true << QString("libmylib.so.1.2");
2908     QTest::newRow("empty soVersion") << QString("") << true << QString("libmylib.so.1.2.3");
2909     QTest::newRow("no version, explicit soVersion") << QString("5") << false
2910                                                     << QString("libmylib.so.5");
2911     QTest::newRow("no version, default soVersion") << QString() << false << QString("libmylib.so");
2912     QTest::newRow("no version, empty soVersion") << QString("") << false << QString("libmylib.so");
2913 }
2914 
sourceArtifactChanges()2915 void TestBlackbox::sourceArtifactChanges()
2916 {
2917     QDir::setCurrent(testDataDir + "/source-artifact-changes");
2918     bool useCustomFileTags = false;
2919     bool overrideFileTags = true;
2920     bool filesAreTargets = false;
2921     bool useCxx11 = false;
2922     const QString appFilePath = QDir::currentPath() + '/' + relativeExecutableFilePath("app");
2923     static const auto b2s = [](bool b) { return QString(b ? "true" : "false"); };
2924     const auto resolveParams = [&useCustomFileTags, &overrideFileTags, &filesAreTargets, &useCxx11] {
2925         return QbsRunParameters("resolve", QStringList{
2926             "modules.module_with_files.overrideTags:" + b2s(overrideFileTags),
2927             "modules.module_with_files.filesAreTargets:" + b2s(filesAreTargets),
2928             "modules.module_with_files.fileTags:" + QString(useCustomFileTags ? "custom" : "cpp"),
2929             "modules.cpp.cxxLanguageVersion:" + QString(useCxx11 ? "c++11" : "c++98")
2930         });
2931     };
2932 #define VERIFY_COMPILATION(expected) \
2933     do { \
2934         QVERIFY2(m_qbsStdout.contains("compiling main.cpp") == expected, m_qbsStdout.constData()); \
2935         QVERIFY2(QFile::exists(appFilePath) == expected, qPrintable(appFilePath)); \
2936         if (expected) \
2937             QVERIFY2(m_qbsStdout.contains("cpp artifacts: 1"), m_qbsStdout.constData()); \
2938         else \
2939             QVERIFY2(m_qbsStdout.contains("cpp artifacts: 0"), m_qbsStdout.constData()); \
2940     } while (false)
2941 
2942     // Initial build.
2943     QCOMPARE(runQbs(resolveParams()), 0);
2944     QVERIFY2(m_qbsStdout.contains("is gcc: "), m_qbsStdout.constData());
2945     const bool isGcc = m_qbsStdout.contains("is gcc: true");
2946     QCOMPARE(runQbs(), 0);
2947     VERIFY_COMPILATION(true);
2948 
2949     // Overwrite the file tags. Now the source file is no longer tagged "cpp" and nothing
2950     // should get built.
2951     WAIT_FOR_NEW_TIMESTAMP();
2952     touch("modules/module_with_files/main.cpp");
2953     useCustomFileTags = true;
2954     QCOMPARE(runQbs(resolveParams()), 0);
2955     QCOMPARE(runQbs(), 0);
2956     VERIFY_COMPILATION(false);
2957 
2958     // Now the custom file tag exists in addition to "cpp", and the app should get built again.
2959     overrideFileTags = false;
2960     QCOMPARE(runQbs(resolveParams()), 0);
2961     QCOMPARE(runQbs(), 0);
2962     VERIFY_COMPILATION(true);
2963 
2964     // Mark the cpp file as a module target. Now it will no longer be considered an input
2965     // by the compiler rule, and nothing should get built.
2966     WAIT_FOR_NEW_TIMESTAMP();
2967     touch("modules/module_with_files/main.cpp");
2968     filesAreTargets = true;
2969     QCOMPARE(runQbs(resolveParams()), 0);
2970     QCOMPARE(runQbs(), 0);
2971     VERIFY_COMPILATION(false);
2972 
2973     // Now just revert the last change.
2974     filesAreTargets = false;
2975     QCOMPARE(runQbs(resolveParams()), 0);
2976     QCOMPARE(runQbs(), 0);
2977     VERIFY_COMPILATION(true);
2978 
2979     // Change a relevant cpp property. A rebuild is expected.
2980     useCxx11 = true;
2981     QCOMPARE(runQbs(resolveParams()), 0);
2982     QCOMPARE(runQbs(QStringList({"--command-echo-mode", "command-line"})), 0);
2983     if (isGcc) {
2984         QVERIFY2(m_qbsStdout.contains("-std=c++11") || m_qbsStdout.contains("-std=c++0x"),
2985                  m_qbsStdout.constData());
2986     }
2987 
2988 #undef VERIFY_COMPILATION
2989 }
2990 
overrideProjectProperties()2991 void TestBlackbox::overrideProjectProperties()
2992 {
2993     QDir::setCurrent(testDataDir + "/overrideProjectProperties");
2994     QCOMPARE(runQbs(QbsRunParameters(QStringList()
2995                                      << QStringLiteral("-f")
2996                                      << QStringLiteral("overrideProjectProperties.qbs")
2997                                      << QStringLiteral("project.nameSuffix:ForYou")
2998                                      << QStringLiteral("project.someBool:false")
2999                                      << QStringLiteral("project.someInt:156")
3000                                      << QStringLiteral("project.someStringList:one")
3001                                      << QStringLiteral("products.MyAppForYou.mainFile:main.cpp"))),
3002              0);
3003     QVERIFY(regularFileExists(relativeExecutableFilePath("MyAppForYou")));
3004     QVERIFY(QFile::remove(relativeBuildGraphFilePath()));
3005     QbsRunParameters params;
3006     params.arguments << QStringLiteral("-f") << QStringLiteral("project_using_helper_lib.qbs");
3007     params.expectFailure = true;
3008     QVERIFY(runQbs(params) != 0);
3009 
3010     rmDirR(relativeBuildDir());
3011     params.arguments = QStringList() << QStringLiteral("-f")
3012             << QStringLiteral("project_using_helper_lib.qbs")
3013             << QStringLiteral("project.linkSuccessfully:true");
3014     params.expectFailure = false;
3015     QCOMPARE(runQbs(params), 0);
3016 }
3017 
pathProbe_data()3018 void TestBlackbox::pathProbe_data()
3019 {
3020     QTest::addColumn<QString>("projectFile");
3021     QTest::addColumn<bool>("successExpected");
3022     QTest::newRow("non-existent") << QString("non-existent.qbs") << false;
3023     QTest::newRow("non-existent-selector.qbs") << QString("non-existent-selector.qbs") << false;
3024     QTest::newRow("single-file") << QString("single-file.qbs") << true;
3025     QTest::newRow("single-file-selector") << QString("single-file-selector.qbs") << true;
3026     QTest::newRow("single-file-selector-array") << QString("single-file-selector-array.qbs") << true;
3027     QTest::newRow("single-file-mult-variants") << QString("single-file-mult-variants.qbs") << true;
3028     QTest::newRow("mult-files") << QString("mult-files.qbs") << true;
3029     QTest::newRow("mult-files-mult-variants") << QString("mult-files-mult-variants.qbs") << true;
3030     QTest::newRow("single-file-suffixes") << QString("single-file-suffixes.qbs") << true;
3031     QTest::newRow("mult-files-suffixes") << QString("mult-files-suffixes.qbs") << true;
3032     QTest::newRow("mult-files-common-suffixes") << QString("mult-files-common-suffixes.qbs") << true;
3033     QTest::newRow("mult-files-mult-suffixes") << QString("mult-files-mult-suffixes.qbs") << true;
3034     QTest::newRow("name-filter") << QString("name-filter.qbs") << true;
3035     QTest::newRow("candidate-filter") << QString("candidate-filter.qbs") << true;
3036     QTest::newRow("environment-paths") << QString("environment-paths.qbs") << true;
3037 }
3038 
pathProbe()3039 void TestBlackbox::pathProbe()
3040 {
3041     QDir::setCurrent(testDataDir + "/path-probe");
3042     QFETCH(QString, projectFile);
3043     QFETCH(bool, successExpected);
3044     rmDirR(relativeBuildDir());
3045 
3046     QbsRunParameters buildParams("build", QStringList{"-f", projectFile});
3047     buildParams.expectFailure = !successExpected;
3048     buildParams.environment.insert("SEARCH_PATH", "usr/bin");
3049     QCOMPARE(runQbs(buildParams) == 0, successExpected);
3050     if (!successExpected)
3051         QVERIFY2(m_qbsStderr.contains("Probe failed to find files"), m_qbsStderr);
3052 }
3053 
pchChangeTracking()3054 void TestBlackbox::pchChangeTracking()
3055 {
3056     QDir::setCurrent(testDataDir + "/pch-change-tracking");
3057     QCOMPARE(runQbs(), 0);
3058     QVERIFY(m_qbsStdout.contains("precompiling pch.h (cpp)"));
3059     WAIT_FOR_NEW_TIMESTAMP();
3060     touch("header1.h");
3061     QCOMPARE(runQbs(), 0);
3062     QVERIFY(m_qbsStdout.contains("precompiling pch.h (cpp)"));
3063     QVERIFY(m_qbsStdout.contains("compiling header2.cpp"));
3064     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
3065     WAIT_FOR_NEW_TIMESTAMP();
3066     touch("header2.h");
3067     QCOMPARE(runQbs(), 0);
3068     QVERIFY2(!m_qbsStdout.contains("precompiling pch.h (cpp)"), m_qbsStdout.constData());
3069 }
3070 
perGroupDefineInExportItem()3071 void TestBlackbox::perGroupDefineInExportItem()
3072 {
3073     QDir::setCurrent(testDataDir + "/per-group-define-in-export-item");
3074     QCOMPARE(runQbs(), 0);
3075 }
3076 
pkgConfigProbe()3077 void TestBlackbox::pkgConfigProbe()
3078 {
3079     const QString exe = findExecutable(QStringList() << "pkg-config");
3080     if (exe.isEmpty())
3081         QSKIP("This test requires the pkg-config tool");
3082 
3083     QDir::setCurrent(testDataDir + "/pkg-config-probe");
3084 
3085     QFETCH(QString, packageBaseName);
3086     QFETCH(QStringList, found);
3087     QFETCH(QStringList, libs);
3088     QFETCH(QStringList, cflags);
3089     QFETCH(QStringList, version);
3090 
3091     rmDirR(relativeBuildDir());
3092     QbsRunParameters params(QStringList() << ("project.packageBaseName:" + packageBaseName));
3093     QCOMPARE(runQbs(params), 0);
3094     const QString stdOut = m_qbsStdout;
3095     QVERIFY2(stdOut.contains("theProduct1 found: " + found.at(0)), m_qbsStdout.constData());
3096     QVERIFY2(stdOut.contains("theProduct2 found: " + found.at(1)), m_qbsStdout.constData());
3097     QVERIFY2(stdOut.contains("theProduct1 libs: " + libs.at(0)), m_qbsStdout.constData());
3098     QVERIFY2(stdOut.contains("theProduct2 libs: " + libs.at(1)), m_qbsStdout.constData());
3099     QVERIFY2(stdOut.contains("theProduct1 cflags: " + cflags.at(0)), m_qbsStdout.constData());
3100     QVERIFY2(stdOut.contains("theProduct2 cflags: " + cflags.at(1)), m_qbsStdout.constData());
3101     QVERIFY2(stdOut.contains("theProduct1 version: " + version.at(0)), m_qbsStdout.constData());
3102     QVERIFY2(stdOut.contains("theProduct2 version: " + version.at(1)), m_qbsStdout.constData());
3103 }
3104 
pkgConfigProbe_data()3105 void TestBlackbox::pkgConfigProbe_data()
3106 {
3107     QTest::addColumn<QString>("packageBaseName");
3108     QTest::addColumn<QStringList>("found");
3109     QTest::addColumn<QStringList>("libs");
3110     QTest::addColumn<QStringList>("cflags");
3111     QTest::addColumn<QStringList>("version");
3112 
3113     QTest::newRow("existing package")
3114             << "dummy" << (QStringList() << "true" << "true")
3115             << (QStringList() << "[\"-Ldummydir1\",\"-ldummy1\"]"
3116                 << "[\"-Ldummydir2\",\"-ldummy2\"]")
3117             << (QStringList() << "[]" << "[]") << (QStringList() << "0.0.1" << "0.0.2");
3118 
3119     // Note: The array values should be "undefined", but we lose that information when
3120     //       converting to QVariants in the ProjectResolver.
3121     QTest::newRow("non-existing package")
3122             << "blubb" << (QStringList() << "false" << "false") << (QStringList() << "[]" << "[]")
3123             << (QStringList() << "[]" << "[]") << (QStringList() << "undefined" << "undefined");
3124 }
3125 
pkgConfigProbeSysroot()3126 void TestBlackbox::pkgConfigProbeSysroot()
3127 {
3128     const QString exe = findExecutable(QStringList() << "pkg-config");
3129     if (exe.isEmpty())
3130         QSKIP("This test requires the pkg-config tool");
3131 
3132     QDir::setCurrent(testDataDir + "/pkg-config-probe-sysroot");
3133     QCOMPARE(runQbs(QStringList("-v")), 0);
3134     QCOMPARE(m_qbsStderr.count("PkgConfigProbe: found packages"), 2);
3135     const QString outputTemplate = "theProduct%1 libs: [\"-L%2/usr/dummy\",\"-ldummy1\"]";
3136     QVERIFY2(m_qbsStdout.contains(outputTemplate
3137                                   .arg("1", QDir::currentPath() + "/sysroot1").toLocal8Bit()),
3138              m_qbsStdout.constData());
3139     QVERIFY2(m_qbsStdout.contains(outputTemplate
3140                                   .arg("2", QDir::currentPath() + "/sysroot2").toLocal8Bit()),
3141              m_qbsStdout.constData());
3142     QVERIFY2(m_qbsStdout.contains(outputTemplate
3143                                   .arg("3", QDir::currentPath() + "/sysroot1").toLocal8Bit()),
3144              m_qbsStdout.constData());
3145 }
3146 
pluginDependency()3147 void TestBlackbox::pluginDependency()
3148 {
3149     QDir::setCurrent(testDataDir + "/plugin-dependency");
3150 
3151     // Build the plugins and the helper2 lib.
3152     QCOMPARE(runQbs(QStringList{"--products", "plugin1,plugin2,plugin3,plugin4,helper2"}), 0);
3153     QVERIFY(m_qbsStdout.contains("plugin1"));
3154     QVERIFY(m_qbsStdout.contains("plugin2"));
3155     QVERIFY(m_qbsStdout.contains("plugin3"));
3156     QVERIFY(m_qbsStdout.contains("plugin4"));
3157     QVERIFY(m_qbsStdout.contains("helper2"));
3158     QVERIFY(!m_qbsStderr.contains("SOFT ASSERT"));
3159 
3160     // Build the app. Plugins 1 and 2 must not be linked. Plugin 3 must be linked.
3161     QCOMPARE(runQbs(QStringList{"--command-echo-mode", "command-line"}), 0);
3162     QByteArray output = m_qbsStdout + '\n' + m_qbsStderr;
3163     QVERIFY(!output.contains("plugin1"));
3164     QVERIFY(!output.contains("plugin2"));
3165     QVERIFY(!output.contains("helper2"));
3166 
3167     // Test change tracking for parameter in Parameters item.
3168     WAIT_FOR_NEW_TIMESTAMP();
3169     REPLACE_IN_FILE("plugin-dependency.qbs", "false // marker 1", "true");
3170     QCOMPARE(runQbs(QStringList{"-p", "plugin2"}), 0);
3171     QVERIFY2(!m_qbsStdout.contains("linking"), m_qbsStdout.constData());
3172     QCOMPARE(runQbs(QStringList{"--command-echo-mode", "command-line"}), 0);
3173     output = m_qbsStdout + '\n' + m_qbsStderr;
3174     QVERIFY2(!output.contains("plugin1"), output.constData());
3175     QVERIFY2(!output.contains("helper2"), output.constData());
3176     QVERIFY2(output.contains("plugin2"), output.constData());
3177 
3178     // Test change tracking for parameter in Depends item.
3179     WAIT_FOR_NEW_TIMESTAMP();
3180     REPLACE_IN_FILE("plugin-dependency.qbs", "false /* marker 2 */", "true");
3181     QCOMPARE(runQbs(QStringList{"-p", "helper1", "--command-echo-mode", "command-line"}), 0);
3182     output = m_qbsStdout + '\n' + m_qbsStderr;
3183     QVERIFY2(output.contains("helper2"), output.constData());
3184 
3185     // Check that the build dependency still works.
3186     QCOMPARE(runQbs(QStringLiteral("clean")), 0);
3187     QCOMPARE(runQbs(QStringList{"--products", "myapp", "--command-echo-mode", "command-line"}), 0);
3188     QVERIFY(m_qbsStdout.contains("plugin1"));
3189     QVERIFY(m_qbsStdout.contains("plugin2"));
3190     QVERIFY(m_qbsStdout.contains("plugin3"));
3191     QVERIFY(m_qbsStdout.contains("plugin4"));
3192 }
3193 
precompiledAndPrefixHeaders()3194 void TestBlackbox::precompiledAndPrefixHeaders()
3195 {
3196     QDir::setCurrent(testDataDir + "/precompiled-and-prefix-headers");
3197     QCOMPARE(runQbs(), 0);
3198 }
3199 
precompiledHeaderAndRedefine()3200 void TestBlackbox::precompiledHeaderAndRedefine()
3201 {
3202     QDir::setCurrent(testDataDir + "/precompiled-headers-and-redefine");
3203     QCOMPARE(runQbs(), 0);
3204 }
3205 
preventFloatingPointValues()3206 void TestBlackbox::preventFloatingPointValues()
3207 {
3208     QDir::setCurrent(testDataDir + "/prevent-floating-point-values");
3209     QCOMPARE(runQbs(QStringList("products.p.version:1.50")), 0);
3210     QVERIFY2(m_qbsStdout.contains("version: 1.50"), m_qbsStdout.constData());
3211 }
3212 
probeChangeTracking()3213 void TestBlackbox::probeChangeTracking()
3214 {
3215     QDir::setCurrent(testDataDir + "/probe-change-tracking");
3216 
3217     // Product probe disabled, other probes enabled.
3218     QbsRunParameters params;
3219     params.command = "resolve";
3220     params.arguments = QStringList("products.theProduct.runProbe:false");
3221     QCOMPARE(runQbs(params), 0);
3222     QVERIFY(m_qbsStdout.contains("running tlpProbe"));
3223     QVERIFY(m_qbsStdout.contains("running subProbe"));
3224     QVERIFY(!m_qbsStdout.contains("running productProbe"));
3225 
3226     // Product probe newly enabled.
3227     params.arguments = QStringList("products.theProduct.runProbe:true");
3228     QCOMPARE(runQbs(params), 0);
3229     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3230     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3231     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3232 
3233     // Re-resolving with unchanged probe.
3234     WAIT_FOR_NEW_TIMESTAMP();
3235     touch("probe-change-tracking.qbs");
3236     QCOMPARE(runQbs(params), 0);
3237     QVERIFY(m_qbsStdout.contains("Resolving"));
3238     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3239     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3240     QVERIFY(!m_qbsStdout.contains("running productProbe"));
3241 
3242     // Re-resolving with changed configure scripts.
3243     WAIT_FOR_NEW_TIMESTAMP();
3244     REPLACE_IN_FILE("probe-change-tracking.qbs", "console.info", " console.info");
3245     QCOMPARE(runQbs(params), 0);
3246     QVERIFY(m_qbsStdout.contains("Resolving"));
3247     QVERIFY(m_qbsStdout.contains("running tlpProbe"));
3248     QVERIFY(m_qbsStdout.contains("running subProbe"));
3249     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3250 
3251     // Re-resolving with added property.
3252     WAIT_FOR_NEW_TIMESTAMP();
3253     REPLACE_IN_FILE("probe-change-tracking.qbs", "condition: product.runProbe",
3254                     "condition: product.runProbe\nproperty string something: 'x'");
3255     QCOMPARE(runQbs(params), 0);
3256     QVERIFY(m_qbsStdout.contains("Resolving"));
3257     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3258     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3259     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3260 
3261     // Re-resolving with changed property.
3262     WAIT_FOR_NEW_TIMESTAMP();
3263     REPLACE_IN_FILE("probe-change-tracking.qbs", "'x'", "'y'");
3264     QCOMPARE(runQbs(params), 0);
3265     QVERIFY(m_qbsStdout.contains("Resolving"));
3266     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3267     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3268     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3269 
3270     // Re-resolving with removed property.
3271     WAIT_FOR_NEW_TIMESTAMP();
3272     REPLACE_IN_FILE("probe-change-tracking.qbs", "property string something: 'y'", "");
3273     QCOMPARE(runQbs(params), 0);
3274     QVERIFY(m_qbsStdout.contains("Resolving"));
3275     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3276     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3277     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3278 
3279     // Re-resolving with unchanged probe again.
3280     WAIT_FOR_NEW_TIMESTAMP();
3281     touch("probe-change-tracking.qbs");
3282     QCOMPARE(runQbs(params), 0);
3283     QVERIFY(m_qbsStdout.contains("Resolving"));
3284     QVERIFY(!m_qbsStdout.contains("running tlpProbe"));
3285     QVERIFY(!m_qbsStdout.contains("running subProbe"));
3286     QVERIFY(!m_qbsStdout.contains("running productProbe"));
3287 
3288     // Enforcing re-running via command-line option.
3289     params.arguments.prepend("--force-probe-execution");
3290     QCOMPARE(runQbs(params), 0);
3291     QVERIFY(m_qbsStdout.contains("Resolving"));
3292     QVERIFY(m_qbsStdout.contains("running tlpProbe"));
3293     QVERIFY(m_qbsStdout.contains("running subProbe"));
3294     QVERIFY(m_qbsStdout.contains("running productProbe: 12"));
3295 }
3296 
probeProperties()3297 void TestBlackbox::probeProperties()
3298 {
3299     QDir::setCurrent(testDataDir + "/probeProperties");
3300     const QByteArray dir = QDir::cleanPath(testDataDir).toLocal8Bit() + "/probeProperties";
3301     QCOMPARE(runQbs(), 0);
3302     QVERIFY2(m_qbsStdout.contains("probe1.fileName=bin/tool"), m_qbsStdout.constData());
3303     QVERIFY2(m_qbsStdout.contains("probe1.path=" + dir), m_qbsStdout.constData());
3304     QVERIFY2(m_qbsStdout.contains("probe1.filePath=" + dir + "/bin/tool"), m_qbsStdout.constData());
3305     QVERIFY2(m_qbsStdout.contains("probe2.fileName=tool"), m_qbsStdout.constData());
3306     QVERIFY2(m_qbsStdout.contains("probe2.path=" + dir + "/bin"), m_qbsStdout.constData());
3307     QVERIFY2(m_qbsStdout.contains("probe2.filePath=" + dir + "/bin/tool"), m_qbsStdout.constData());
3308     QVERIFY2(m_qbsStdout.contains("probe3.fileName=tool"), m_qbsStdout.constData());
3309     QVERIFY2(m_qbsStdout.contains("probe3.path=" + dir + "/bin"), m_qbsStdout.constData());
3310     QVERIFY2(m_qbsStdout.contains("probe3.filePath=" + dir + "/bin/tool"), m_qbsStdout.constData());
3311 }
3312 
probesAndShadowProducts()3313 void TestBlackbox::probesAndShadowProducts()
3314 {
3315     QDir::setCurrent(testDataDir + "/probes-and-shadow-products");
3316     QCOMPARE(runQbs(QStringList("--log-time")), 0);
3317     QVERIFY2(m_qbsStdout.contains("2 probes encountered, 1 configure scripts executed"),
3318              m_qbsStdout.constData());
3319     WAIT_FOR_NEW_TIMESTAMP();
3320     touch("probes-and-shadow-products.qbs");
3321     QCOMPARE(runQbs(QStringList("--log-time")), 0);
3322     QVERIFY2(m_qbsStdout.contains("2 probes encountered, 0 configure scripts executed"),
3323              m_qbsStdout.constData());
3324 }
3325 
probeInExportedModule()3326 void TestBlackbox::probeInExportedModule()
3327 {
3328     QDir::setCurrent(testDataDir + "/probe-in-exported-module");
3329     QCOMPARE(runQbs(QbsRunParameters(QStringList() << QStringLiteral("-f")
3330                                      << QStringLiteral("probe-in-exported-module.qbs"))), 0);
3331     QVERIFY2(m_qbsStdout.contains("found: true"), m_qbsStdout.constData());
3332     QVERIFY2(m_qbsStdout.contains("prop: yes"), m_qbsStdout.constData());
3333     QVERIFY2(m_qbsStdout.contains("listProp: myother,my"), m_qbsStdout.constData());
3334 }
3335 
probesAndArrayProperties()3336 void TestBlackbox::probesAndArrayProperties()
3337 {
3338     QDir::setCurrent(testDataDir + "/probes-and-array-properties");
3339     QCOMPARE(runQbs(), 0);
3340     QVERIFY2(m_qbsStdout.contains("prop: [\"probe\"]"), m_qbsStdout.constData());
3341     WAIT_FOR_NEW_TIMESTAMP();
3342     REPLACE_IN_FILE("probes-and-array-properties.qbs", "//", "");
3343     QCOMPARE(runQbs(), 0);
3344     QVERIFY2(m_qbsStdout.contains("prop: [\"product\",\"probe\"]"), m_qbsStdout.constData());
3345 }
3346 
productProperties()3347 void TestBlackbox::productProperties()
3348 {
3349     QDir::setCurrent(testDataDir + "/productproperties");
3350     QCOMPARE(runQbs(QbsRunParameters(QStringList() << QStringLiteral("-f")
3351                                      << QStringLiteral("productproperties.qbs"))), 0);
3352     QVERIFY(regularFileExists(relativeExecutableFilePath("blubb_user")));
3353 }
3354 
propertyAssignmentOnNonPresentModule()3355 void TestBlackbox::propertyAssignmentOnNonPresentModule()
3356 {
3357     QDir::setCurrent(testDataDir + "/property-assignment-on-non-present-module");
3358     QCOMPARE(runQbs(), 0);
3359     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
3360 }
3361 
propertyAssignmentInFailedModule()3362 void TestBlackbox::propertyAssignmentInFailedModule()
3363 {
3364     QDir::setCurrent(testDataDir + "/property-assignment-in-failed-module");
3365     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("modules.m.doFail:false"))), 0);
3366     QbsRunParameters failParams;
3367     failParams.expectFailure = true;
3368     QVERIFY(runQbs(failParams) != 0);
3369     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("modules.m.doFail:true"))), 0);
3370     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
3371     QEXPECT_FAIL(nullptr, "circular dependency between module merging and validation", Continue);
3372     QCOMPARE(runQbs(failParams), 0);
3373 }
3374 
propertyChanges()3375 void TestBlackbox::propertyChanges()
3376 {
3377     QDir::setCurrent(testDataDir + "/propertyChanges");
3378     const QString projectFile("propertyChanges.qbs");
3379     QbsRunParameters params(QStringList({"-f", "propertyChanges.qbs", "qbs.enableDebugCode:true"}));
3380 
3381     // Initial build.
3382     QCOMPARE(runQbs(params), 0);
3383     QVERIFY(m_qbsStdout.contains("compiling source1.cpp"));
3384     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3385     QVERIFY(m_qbsStdout.contains("compiling source3.cpp"));
3386     QVERIFY(m_qbsStdout.contains("compiling lib.cpp"));
3387     QVERIFY(m_qbsStdout.contains("linking product 1.debug"));
3388     QVERIFY(m_qbsStdout.contains("generated.txt"));
3389     QVERIFY(m_qbsStdout.contains("Making output from input"));
3390     QVERIFY(m_qbsStdout.contains("default value"));
3391     QVERIFY(m_qbsStdout.contains("Making output from other output"));
3392     QFile generatedFile(relativeProductBuildDir("generated text file") + "/generated.txt");
3393     QVERIFY(generatedFile.open(QIODevice::ReadOnly));
3394     QCOMPARE(generatedFile.readAll(), QByteArray("prefix 1contents 1suffix 1"));
3395     generatedFile.close();
3396 
3397     // Incremental build with no changes.
3398     QCOMPARE(runQbs(params), 0);
3399     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3400     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3401     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3402     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp.cpp"));
3403     QVERIFY(!m_qbsStdout.contains("linking"));
3404     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3405     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3406     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3407 
3408     // Incremental build with no changes, but updated project file timestamp.
3409     WAIT_FOR_NEW_TIMESTAMP();
3410     touch(projectFile);
3411     QCOMPARE(runQbs(params), 0);
3412     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3413     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3414     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3415     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3416     QVERIFY(!m_qbsStdout.contains("linking"));
3417     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3418     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3419     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3420 
3421     // Incremental build, input property changed for first product
3422     WAIT_FOR_NEW_TIMESTAMP();
3423     REPLACE_IN_FILE(projectFile, "blubb1", "blubb01");
3424     QCOMPARE(runQbs(params), 0);
3425     QVERIFY(m_qbsStdout.contains("compiling source1.cpp"));
3426     QVERIFY(m_qbsStdout.contains("linking product 1.debug"));
3427     QVERIFY(!m_qbsStdout.contains("linking product 2"));
3428     QVERIFY(!m_qbsStdout.contains("linking product 3"));
3429     QVERIFY(!m_qbsStdout.contains("linking library"));
3430     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3431     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3432     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3433 
3434     // Incremental build, input property changed via project for second product.
3435     WAIT_FOR_NEW_TIMESTAMP();
3436     REPLACE_IN_FILE(projectFile, "blubb2", "blubb02");
3437     QCOMPARE(runQbs(params), 0);
3438     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3439     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3440     QVERIFY(!m_qbsStdout.contains("linking product 3"));
3441     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3442     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3443     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3444 
3445     // Incremental build, input property changed via command line for second product.
3446     params.command = "resolve";
3447     params.arguments << "project.projectDefines:blubb002";
3448     QCOMPARE(runQbs(params), 0);
3449     QCOMPARE(runQbs(), 0);
3450     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3451     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3452     QVERIFY(!m_qbsStdout.contains("linking product 3"));
3453     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3454     params.arguments.removeLast();
3455     QCOMPARE(runQbs(params), 0);
3456     QCOMPARE(runQbs(), 0);
3457     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3458     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3459     QVERIFY(!m_qbsStdout.contains("linking product 3"));
3460     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3461     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3462     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3463 
3464     // Incremental build, input property changed via environment for third product.
3465     params.environment.insert("QBS_BLACKBOX_DEFINE", "newvalue");
3466     QCOMPARE(runQbs(params), 0);
3467     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3468     QVERIFY(!m_qbsStdout.contains("linking product 2"));
3469     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3470     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3471     params.environment.remove("QBS_BLACKBOX_DEFINE");
3472     QCOMPARE(runQbs(params), 0);
3473     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3474     QVERIFY(!m_qbsStdout.contains("linking product 2"));
3475     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3476     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3477     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3478     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3479     params.environment.insert("QBS_BLACKBOX_DEFINE", "newvalue");
3480     QCOMPARE(runQbs(params), 0);
3481     QCOMPARE(runQbs(), 0);
3482     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3483     QVERIFY(!m_qbsStdout.contains("linking product 2"));
3484     QVERIFY(m_qbsStdout.contains("compiling source3.cpp"));
3485     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3486     params.environment.remove("QBS_BLACKBOX_DEFINE");
3487     QCOMPARE(runQbs(params), 0);
3488     QCOMPARE(runQbs(), 0);
3489     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3490     QVERIFY(!m_qbsStdout.contains("linking product 2"));
3491     QVERIFY(m_qbsStdout.contains("compiling source3.cpp"));
3492     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3493     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3494     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3495 
3496     // Incremental build, module property changed via command line.
3497     params.arguments << "qbs.enableDebugCode:false";
3498     QCOMPARE(runQbs(params), 0);
3499     QCOMPARE(runQbs(), 0);
3500     QVERIFY(m_qbsStdout.contains("compiling source1.cpp"));
3501     QVERIFY(m_qbsStdout.contains("linking product 1.release"));
3502     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3503     QVERIFY(m_qbsStdout.contains("compiling source3.cpp"));
3504     QVERIFY(m_qbsStdout.contains("compiling lib.cpp"));
3505     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3506     params.arguments.removeLast();
3507     QCOMPARE(runQbs(params), 0);
3508     QCOMPARE(runQbs(), 0);
3509     QVERIFY(m_qbsStdout.contains("compiling source1.cpp"));
3510     QVERIFY(m_qbsStdout.contains("linking product 1.debug"));
3511     QVERIFY(m_qbsStdout.contains("compiling source2.cpp"));
3512     QVERIFY(m_qbsStdout.contains("compiling source3.cpp"));
3513     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3514     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3515     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3516 
3517     // Incremental build, non-essential dependency removed.
3518     WAIT_FOR_NEW_TIMESTAMP();
3519     REPLACE_IN_FILE(projectFile, "Depends { name: 'library' }", "// Depends { name: 'library' }");
3520     params.command = "build";
3521     QCOMPARE(runQbs(params), 0);
3522     QVERIFY(!m_qbsStdout.contains("linking product 1"));
3523     QVERIFY(m_qbsStdout.contains("linking product 2"));
3524     QVERIFY(!m_qbsStdout.contains("linking product 3"));
3525     QVERIFY(!m_qbsStdout.contains("linking library"));
3526     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3527     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3528     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3529 
3530     // Incremental build, prepare script of a transformer changed.
3531     WAIT_FOR_NEW_TIMESTAMP();
3532     REPLACE_IN_FILE(projectFile, "contents 1", "contents 2");
3533     QCOMPARE(runQbs(params), 0);
3534     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3535     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3536     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3537     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3538     QVERIFY(m_qbsStdout.contains("generated.txt"));
3539     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3540     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3541     QVERIFY(generatedFile.open(QIODevice::ReadOnly));
3542     QCOMPARE(generatedFile.readAll(), QByteArray("prefix 1contents 2suffix 1"));
3543     generatedFile.close();
3544 
3545     // Incremental build, product property used in JavaScript command changed.
3546     WAIT_FOR_NEW_TIMESTAMP();
3547     REPLACE_IN_FILE(projectFile, "prefix 1", "prefix 2");
3548     QCOMPARE(runQbs(params), 0);
3549     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3550     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3551     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3552     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3553     QVERIFY(m_qbsStdout.contains("generated.txt"));
3554     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3555     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3556     QVERIFY(generatedFile.open(QIODevice::ReadOnly));
3557     QCOMPARE(generatedFile.readAll(), QByteArray("prefix 2contents 2suffix 1"));
3558     generatedFile.close();
3559 
3560     // Incremental build, project property used in JavaScript command changed.
3561     WAIT_FOR_NEW_TIMESTAMP();
3562     REPLACE_IN_FILE(projectFile, "suffix 1", "suffix 2");
3563     QCOMPARE(runQbs(params), 0);
3564     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3565     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3566     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3567     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3568     QVERIFY(m_qbsStdout.contains("generated.txt"));
3569     QVERIFY(!m_qbsStdout.contains("Making output from input"));
3570     QVERIFY(!m_qbsStdout.contains("Making output from other output"));
3571     QVERIFY(generatedFile.open(QIODevice::ReadOnly));
3572     QCOMPARE(generatedFile.readAll(), QByteArray("prefix 2contents 2suffix 2"));
3573     generatedFile.close();
3574 
3575     // Incremental build, module property used in JavaScript command changed.
3576     WAIT_FOR_NEW_TIMESTAMP();
3577     REPLACE_IN_FILE(projectFile, "default value", "new value");
3578     QCOMPARE(runQbs(params), 0);
3579     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3580     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3581     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3582     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3583     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3584     QVERIFY(m_qbsStdout.contains("Making output from input"));
3585     QVERIFY(m_qbsStdout.contains("Making output from other output"));
3586     QVERIFY(m_qbsStdout.contains("new value"));
3587 
3588     // Incremental build, prepare script of a rule in a module changed.
3589     WAIT_FOR_NEW_TIMESTAMP();
3590     REPLACE_IN_FILE("modules/TestModule/module.qbs", "// console.info('Change in source code')",
3591                     "console.info('Change in source code')");
3592     QCOMPARE(runQbs(params), 0);
3593     QVERIFY(!m_qbsStdout.contains("compiling source1.cpp"));
3594     QVERIFY(!m_qbsStdout.contains("compiling source2.cpp"));
3595     QVERIFY(!m_qbsStdout.contains("compiling source3.cpp"));
3596     QVERIFY(!m_qbsStdout.contains("compiling lib.cpp"));
3597     QVERIFY(!m_qbsStdout.contains("generated.txt"));
3598     QVERIFY(m_qbsStdout.contains("Making output from input"));
3599     QVERIFY(m_qbsStdout.contains("Making output from other output"));
3600 }
3601 
propertyEvaluationContext()3602 void TestBlackbox::propertyEvaluationContext()
3603 {
3604     const QString testDir = testDataDir + "/property-evaluation-context";
3605     QDir::setCurrent(testDir);
3606     QCOMPARE(runQbs(), 0);
3607     QCOMPARE(m_qbsStdout.count("base.productInBase evaluated in: myapp"), 1);
3608     QCOMPARE(m_qbsStdout.count("base.productInTop evaluated in: myapp"), 1);
3609     QCOMPARE(m_qbsStdout.count("top.productInExport evaluated in: mylib"), 1);
3610     QCOMPARE(m_qbsStdout.count("top.productInTop evaluated in: myapp"), 1);
3611 }
3612 
qtBug51237()3613 void TestBlackbox::qtBug51237()
3614 {
3615     const SettingsPtr s = settings();
3616     qbs::Internal::TemporaryProfile profile("qbs_autotests_qtBug51237", s.get());
3617     profile.p.setValue("mymodule.theProperty", QStringList());
3618     s->sync();
3619 
3620     QDir::setCurrent(testDataDir + "/QTBUG-51237");
3621     QbsRunParameters params;
3622     params.profile = profile.p.name();
3623     QCOMPARE(runQbs(params), 0);
3624 }
3625 
dynamicMultiplexRule()3626 void TestBlackbox::dynamicMultiplexRule()
3627 {
3628     const QString testDir = testDataDir + "/dynamicMultiplexRule";
3629     QDir::setCurrent(testDir);
3630     QCOMPARE(runQbs(), 0);
3631     const QString outputFilePath = relativeProductBuildDir("dynamicMultiplexRule") + "/stuff-from-3-inputs";
3632     QVERIFY(regularFileExists(outputFilePath));
3633     WAIT_FOR_NEW_TIMESTAMP();
3634     touch("two.txt");
3635     QCOMPARE(runQbs(), 0);
3636     QVERIFY(regularFileExists(outputFilePath));
3637 }
3638 
dynamicProject()3639 void TestBlackbox::dynamicProject()
3640 {
3641     const QString testDir = testDataDir + "/dynamic-project";
3642     QDir::setCurrent(testDir);
3643     QCOMPARE(runQbs(), 0);
3644     QCOMPARE(m_qbsStdout.count("compiling main.cpp"), 2);
3645 }
3646 
dynamicRuleOutputs()3647 void TestBlackbox::dynamicRuleOutputs()
3648 {
3649     const QString testDir = testDataDir + "/dynamicRuleOutputs";
3650     QDir::setCurrent(testDir);
3651     if (QFile::exists("work"))
3652         rmDirR("work");
3653     QDir().mkdir("work");
3654     ccp("before", "work");
3655     QDir::setCurrent(testDir + "/work");
3656     QCOMPARE(runQbs(), 0);
3657 
3658     const QString appFile = relativeExecutableFilePath("genlexer");
3659     const QString headerFile1 = relativeProductBuildDir("genlexer") + "/GeneratedFiles/numberscanner.h";
3660     const QString sourceFile1 = relativeProductBuildDir("genlexer") + "/GeneratedFiles/numberscanner.c";
3661     const QString sourceFile2 = relativeProductBuildDir("genlexer") + "/GeneratedFiles/lex.yy.c";
3662 
3663     // Check build #1: source and header file name are specified in numbers.l
3664     QVERIFY(regularFileExists(appFile));
3665     QVERIFY(regularFileExists(headerFile1));
3666     QVERIFY(regularFileExists(sourceFile1));
3667     QVERIFY(!QFile::exists(sourceFile2));
3668 
3669     QDateTime appFileTimeStamp1 = QFileInfo(appFile).lastModified();
3670     WAIT_FOR_NEW_TIMESTAMP();
3671     copyFileAndUpdateTimestamp("../after/numbers.l", "numbers.l");
3672     QCOMPARE(runQbs(), 0);
3673 
3674     // Check build #2: no file names are specified in numbers.l
3675     //                 flex will default to lex.yy.c without header file.
3676     QDateTime appFileTimeStamp2 = QFileInfo(appFile).lastModified();
3677     QVERIFY(appFileTimeStamp1 < appFileTimeStamp2);
3678     QVERIFY(!QFile::exists(headerFile1));
3679     QVERIFY(!QFile::exists(sourceFile1));
3680     QVERIFY(regularFileExists(sourceFile2));
3681 
3682     WAIT_FOR_NEW_TIMESTAMP();
3683     copyFileAndUpdateTimestamp("../before/numbers.l", "numbers.l");
3684     QCOMPARE(runQbs(), 0);
3685 
3686     // Check build #3: source and header file name are specified in numbers.l
3687     QDateTime appFileTimeStamp3 = QFileInfo(appFile).lastModified();
3688     QVERIFY(appFileTimeStamp2 < appFileTimeStamp3);
3689     QVERIFY(regularFileExists(appFile));
3690     QVERIFY(regularFileExists(headerFile1));
3691     QVERIFY(regularFileExists(sourceFile1));
3692     QVERIFY(!QFile::exists(sourceFile2));
3693 }
3694 
emptyProfile()3695 void TestBlackbox::emptyProfile()
3696 {
3697     QDir::setCurrent(testDataDir + "/empty-profile");
3698 
3699     const SettingsPtr s = settings();
3700     const Profile buildProfile(profileName(), s.get());
3701     bool isMsvc = false;
3702     auto toolchainType = buildProfile.value(QStringLiteral("qbs.toolchainType")).toString();
3703     QbsRunParameters params;
3704     params.profile = "none";
3705 
3706     if (toolchainType.isEmpty()) {
3707         const auto toolchain = buildProfile.value(QStringLiteral("qbs.toolchain")).toStringList();
3708         if (!toolchain.isEmpty())
3709             toolchainType = toolchain.first();
3710     }
3711     if (!toolchainType.isEmpty()) {
3712         params.arguments = QStringList{QStringLiteral("qbs.toolchainType:") + toolchainType};
3713         isMsvc = toolchainType == "msvc" || toolchainType == "clang-cl";
3714     }
3715 
3716     if (!isMsvc) {
3717         const auto tcPath =
3718                 QDir::toNativeSeparators(
3719                         buildProfile.value(QStringLiteral("cpp.toolchainInstallPath")).toString());
3720         auto paths = params.environment.value(QStringLiteral("PATH"))
3721                 .split(HostOsInfo::pathListSeparator(), QBS_SKIP_EMPTY_PARTS);
3722         if (!tcPath.isEmpty() && !paths.contains(tcPath)) {
3723             paths.prepend(tcPath);
3724             params.environment.insert(
3725                     QStringLiteral("PATH"), paths.join(HostOsInfo::pathListSeparator()));
3726         }
3727     }
3728     QCOMPARE(runQbs(params), 0);
3729 }
3730 
erroneousFiles_data()3731 void TestBlackbox::erroneousFiles_data()
3732 {
3733     QTest::addColumn<QString>("errorMessage");
3734     QTest::newRow("nonexistentWorkingDir")
3735             << "The working directory '.does.not.exist' for process '.*ls.*' is invalid.";
3736     QTest::newRow("outputArtifacts-missing-filePath")
3737             << "Error in Rule\\.outputArtifacts\\[0\\]\n\r?"
3738                "Property filePath must be a non-empty string\\.";
3739     QTest::newRow("outputArtifacts-missing-fileTags")
3740             << "Error in Rule\\.outputArtifacts\\[0\\]\n\r?"
3741                "Property fileTags for artifact 'outputArtifacts-missing-fileTags\\.txt' "
3742                "must be a non-empty string list\\.";
3743     QTest::newRow("texttemplate-unknown-placeholder")
3744             << "Placeholder 'what' is not defined in textemplate.dict for 'boom.txt.in'";
3745     QTest::newRow("tag-mismatch")
3746             << "tag-mismatch.qbs:8:18.*Artifact '.*dummy1' has undeclared file tags "
3747                "\\[\"y\",\"z\"\\].";
3748 }
3749 
erroneousFiles()3750 void TestBlackbox::erroneousFiles()
3751 {
3752     QFETCH(QString, errorMessage);
3753     QDir::setCurrent(testDataDir + "/erroneous/" + QTest::currentDataTag());
3754     QbsRunParameters params;
3755     params.expectFailure = true;
3756     QVERIFY(runQbs(params) != 0);
3757     QString err = QString::fromLocal8Bit(m_qbsStderr);
3758     if (!err.contains(QRegularExpression(errorMessage))) {
3759         qDebug().noquote() << "Output:  " << err;
3760         qDebug().noquote() << "Expected: " << errorMessage;
3761         QFAIL("Unexpected error message.");
3762     }
3763 }
3764 
errorInfo()3765 void TestBlackbox::errorInfo()
3766 {
3767     QDir::setCurrent(testDataDir + "/error-info");
3768     QCOMPARE(runQbs(), 0);
3769 
3770     QbsRunParameters resolveParams;
3771     QbsRunParameters buildParams;
3772     buildParams.expectFailure = true;
3773 
3774     resolveParams.command = "resolve";
3775     resolveParams.arguments = QStringList() << "project.fail1:true";
3776     QCOMPARE(runQbs(resolveParams), 0);
3777     buildParams.arguments = resolveParams.arguments;
3778     QVERIFY(runQbs(buildParams) != 0);
3779     QVERIFY2(m_qbsStderr.contains("error-info.qbs:24"), m_qbsStderr);
3780 
3781     resolveParams.arguments = QStringList() << "project.fail2:true";
3782     QCOMPARE(runQbs(resolveParams), 0);
3783     buildParams.arguments = resolveParams.arguments;
3784     QVERIFY(runQbs(buildParams) != 0);
3785     QVERIFY2(m_qbsStderr.contains("error-info.qbs:36"), m_qbsStderr);
3786 
3787     resolveParams.arguments = QStringList() << "project.fail3:true";
3788     QCOMPARE(runQbs(resolveParams), 0);
3789     buildParams.arguments = resolveParams.arguments;
3790     QVERIFY(runQbs(buildParams) != 0);
3791     QVERIFY2(m_qbsStderr.contains("error-info.qbs:51"), m_qbsStderr);
3792 
3793     resolveParams.arguments = QStringList() << "project.fail4:true";
3794     QCOMPARE(runQbs(resolveParams), 0);
3795     buildParams.arguments = resolveParams.arguments;
3796     QVERIFY(runQbs(buildParams) != 0);
3797     QVERIFY2(m_qbsStderr.contains("error-info.qbs:66"), m_qbsStderr);
3798 
3799     resolveParams.arguments = QStringList() << "project.fail5:true";
3800     QCOMPARE(runQbs(resolveParams), 0);
3801     buildParams.arguments = resolveParams.arguments;
3802     QVERIFY(runQbs(buildParams) != 0);
3803     QVERIFY2(m_qbsStderr.contains("helper.js:4"), m_qbsStderr);
3804 
3805     resolveParams.arguments = QStringList() << "project.fail6:true";
3806     QCOMPARE(runQbs(resolveParams), 0);
3807     buildParams.arguments = resolveParams.arguments;
3808     QVERIFY(runQbs(buildParams) != 0);
3809     QVERIFY2(m_qbsStderr.contains("helper.js:8"), m_qbsStderr);
3810 
3811     resolveParams.arguments = QStringList() << "project.fail7:true";
3812     QCOMPARE(runQbs(resolveParams), 0);
3813     buildParams.arguments = resolveParams.arguments;
3814     QVERIFY(runQbs(buildParams) != 0);
3815     QVERIFY2(m_qbsStderr.contains("JavaScriptCommand.sourceCode"), m_qbsStderr);
3816     QVERIFY2(m_qbsStderr.contains("error-info.qbs:57"), m_qbsStderr);
3817 }
3818 
escapedLinkerFlags()3819 void TestBlackbox::escapedLinkerFlags()
3820 {
3821     const SettingsPtr s = settings();
3822     const Profile buildProfile(profileName(), s.get());
3823     const QStringList toolchain = profileToolchain(buildProfile);
3824     if (!toolchain.contains("gcc"))
3825         QSKIP("escaped linker flags test only applies with gcc and GNU ld");
3826     if (targetOs() == HostOsInfo::HostOsMacos)
3827         QSKIP("Does not apply on macOS");
3828     QDir::setCurrent(testDataDir + "/escaped-linker-flags");
3829     QbsRunParameters params(QStringList("products.app.escapeLinkerFlags:false"));
3830     QCOMPARE(runQbs(params), 0);
3831     params.command = "resolve";
3832     params.arguments = QStringList() << "products.app.escapeLinkerFlags:true";
3833     QCOMPARE(runQbs(params), 0);
3834     params.command = "build";
3835     params.expectFailure = true;
3836     QVERIFY(runQbs(params) != 0);
3837     QVERIFY2(m_qbsStderr.contains("Encountered escaped linker flag"), m_qbsStderr.constData());
3838 }
3839 
exportedDependencyInDisabledProduct()3840 void TestBlackbox::exportedDependencyInDisabledProduct()
3841 {
3842     QDir::setCurrent(testDataDir + "/exported-dependency-in-disabled-product");
3843     QFETCH(QString, depCondition);
3844     QFETCH(bool, compileExpected);
3845     rmDirR(relativeBuildDir());
3846     const QString propertyArg = "products.dep.conditionString:" + depCondition;
3847     QCOMPARE(runQbs(QStringList(propertyArg)), 0);
3848     QCOMPARE(m_qbsStdout.contains("compiling"), compileExpected);
3849 }
3850 
exportedDependencyInDisabledProduct_data()3851 void TestBlackbox::exportedDependencyInDisabledProduct_data()
3852 {
3853     QTest::addColumn<QString>("depCondition");
3854     QTest::addColumn<bool>("compileExpected");
3855     QTest::newRow("dependency enabled") << "true" << true;
3856     QTest::newRow("dependency directly disabled") << "false" << false;
3857     QTest::newRow("dependency disabled via non-present module") << "nosuchmodule.present" << false;
3858     QTest::newRow("dependency disabled via failed module") << "broken.present" << false;
3859 }
3860 
exportedPropertyInDisabledProduct()3861 void TestBlackbox::exportedPropertyInDisabledProduct()
3862 {
3863     QDir::setCurrent(testDataDir + "/exported-property-in-disabled-product");
3864     QFETCH(QString, depCondition);
3865     QFETCH(bool, successExpected);
3866     const QString propertyArg = "products.dep.conditionString:" + depCondition;
3867     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList(propertyArg))), 0);
3868     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
3869     QbsRunParameters buildParams;
3870     buildParams.expectFailure = !successExpected;
3871     QCOMPARE(runQbs(buildParams) == 0, successExpected);
3872 }
3873 
exportedPropertyInDisabledProduct_data()3874 void TestBlackbox::exportedPropertyInDisabledProduct_data()
3875 {
3876     QTest::addColumn<QString>("depCondition");
3877     QTest::addColumn<bool>("successExpected");
3878     QTest::newRow("dependency enabled") << "true" << false;
3879     QTest::newRow("dependency directly disabled") << "false" << true;
3880     QTest::newRow("dependency disabled via non-present module") << "nosuchmodule.present" << true;
3881     QTest::newRow("dependency disabled via failed module") << "broken.present" << true;
3882 }
3883 
systemRunPaths()3884 void TestBlackbox::systemRunPaths()
3885 {
3886     const SettingsPtr s = settings();
3887     const Profile buildProfile(profileName(), s.get());
3888     switch (targetOs()) {
3889     case HostOsInfo::HostOsLinux:
3890     case HostOsInfo::HostOsMacos:
3891     case HostOsInfo::HostOsOtherUnix:
3892         break;
3893     default:
3894         QSKIP("only applies on Unix");
3895     }
3896 
3897     const QString lddFilePath = findExecutable(QStringList() << "ldd");
3898     if (lddFilePath.isEmpty())
3899         QSKIP("ldd not found");
3900     QDir::setCurrent(testDataDir + "/system-run-paths");
3901     QFETCH(bool, setRunPaths);
3902     rmDirR(relativeBuildDir());
3903     QbsRunParameters params;
3904     params.arguments << QString("project.setRunPaths:") + (setRunPaths ? "true" : "false");
3905     QCOMPARE(runQbs(params), 0);
3906     QProcess ldd;
3907     ldd.start(lddFilePath, QStringList() << relativeExecutableFilePath("app"));
3908     QVERIFY2(ldd.waitForStarted(), qPrintable(ldd.errorString()));
3909     QVERIFY2(ldd.waitForFinished(), qPrintable(ldd.errorString()));
3910     QVERIFY2(ldd.exitCode() == 0, ldd.readAllStandardError().constData());
3911     const QByteArray output = ldd.readAllStandardOutput();
3912     const QList<QByteArray> outputLines = output.split('\n');
3913     QByteArray libLine;
3914     for (const auto &line : outputLines) {
3915         if (line.contains("theLib")) {
3916             libLine = line;
3917             break;
3918         }
3919     }
3920     QVERIFY2(!libLine.isEmpty(), output.constData());
3921     QVERIFY2(setRunPaths == libLine.contains("not found"), libLine.constData());
3922 }
3923 
systemRunPaths_data()3924 void TestBlackbox::systemRunPaths_data()
3925 {
3926     QTest::addColumn<bool>("setRunPaths");
3927     QTest::newRow("do not set system run paths") << false;
3928     QTest::newRow("do set system run paths") << true;
3929 }
3930 
exportRule()3931 void TestBlackbox::exportRule()
3932 {
3933     QDir::setCurrent(testDataDir + "/export-rule");
3934     QbsRunParameters params(QStringList{"modules.blubber.enableTagger:false"});
3935     params.expectFailure = true;
3936     QVERIFY(runQbs(params) != 0);
3937     params.command = "resolve";
3938     params.arguments = QStringList{"modules.blubber.enableTagger:true"};
3939     params.expectFailure = false;
3940     QCOMPARE(runQbs(params), 0);
3941     QCOMPARE(runQbs(), 0);
3942     QVERIFY2(m_qbsStdout.contains("Creating C++ source file"), m_qbsStdout.constData());
3943     QVERIFY2(m_qbsStdout.contains("compiling myapp.cpp"), m_qbsStdout.constData());
3944 }
3945 
exportToOutsideSearchPath()3946 void TestBlackbox::exportToOutsideSearchPath()
3947 {
3948     QDir::setCurrent(testDataDir + "/export-to-outside-searchpath");
3949     QbsRunParameters params;
3950     params.expectFailure = true;
3951     QVERIFY(runQbs(params) != 0);
3952     QVERIFY2(m_qbsStderr.contains("Dependency 'aModule' not found for product 'theProduct'."),
3953              m_qbsStderr.constData());
3954 }
3955 
exportsPkgconfig()3956 void TestBlackbox::exportsPkgconfig()
3957 {
3958     QDir::setCurrent(testDataDir + "/exports-pkgconfig");
3959     QCOMPARE(runQbs(), 0);
3960     QVERIFY2(m_qbsStdout.contains("Creating TheFirstLib.pc"), m_qbsStdout.constData());
3961     QVERIFY2(m_qbsStdout.contains("Creating TheSecondLib.pc"), m_qbsStdout.constData());
3962     QFile sourcePcFile(HostOsInfo::isWindowsHost() ? "TheFirstLib_windows.pc" : "TheFirstLib.pc");
3963     QString generatedPcFilePath = relativeProductBuildDir("TheFirstLib") + "/TheFirstLib.pc";
3964     QFile generatedPcFile(generatedPcFilePath);
3965     QVERIFY2(sourcePcFile.open(QIODevice::ReadOnly), qPrintable(sourcePcFile.errorString()));
3966     QVERIFY2(generatedPcFile.open(QIODevice::ReadOnly), qPrintable(generatedPcFile.errorString()));
3967     QCOMPARE(generatedPcFile.readAll().replace("\r", ""), sourcePcFile.readAll().replace("\r", ""));
3968     sourcePcFile.close();
3969     generatedPcFile.close();
3970     TEXT_FILE_COMPARE(relativeProductBuildDir("TheSecondLib") + "/TheSecondLib.pc",
3971                       "TheSecondLib.pc");
3972     WAIT_FOR_NEW_TIMESTAMP();
3973     touch("firstlib.cpp");
3974     QCOMPARE(runQbs(), 0);
3975     QVERIFY2(m_qbsStdout.contains("linking"), m_qbsStdout.constData());
3976     QVERIFY2(!m_qbsStdout.contains("Creating TheFirstLib.pc"), m_qbsStdout.constData());
3977     QVERIFY2(!m_qbsStdout.contains("Creating TheSecondLib.pc"), m_qbsStdout.constData());
3978 }
3979 
exportsQbs()3980 void TestBlackbox::exportsQbs()
3981 {
3982     QDir::setCurrent(testDataDir + "/exports-qbs");
3983 
3984     QCOMPARE(runQbs({"resolve", {"-f", "exports-qbs.qbs"}}), 0);
3985     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
3986         QSKIP("Cannot run binaries in cross-compiled build");
3987     // First we build exportable products and use them (as products) inside
3988     // the original project.
3989     QCOMPARE(runQbs(QStringList{"-f", "exports-qbs.qbs", "--command-echo-mode", "command-line"}),
3990              0);
3991     QVERIFY2(m_qbsStdout.contains("somelocaldir"), m_qbsStdout.constData());
3992 
3993     // Now we build an external product against the modules that were just installed.
3994     // We try debug and release mode; one module exists for each of them.
3995     QbsRunParameters paramsExternalBuild(QStringList{"-f", "consumer.qbs",
3996                                                      "--command-echo-mode", "command-line",
3997                                                      "modules.qbs.buildVariant:debug",});
3998     paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-debug";
3999     QCOMPARE(runQbs(paramsExternalBuild), 0);
4000     QVERIFY2(!m_qbsStdout.contains("somelocaldir"), m_qbsStdout.constData());
4001 
4002     paramsExternalBuild.arguments = QStringList{"-f", "consumer.qbs",
4003             "modules.qbs.buildVariant:release"};
4004     paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-release";
4005     QCOMPARE(runQbs(paramsExternalBuild), 0);
4006 
4007     // Trying to build with an unsupported build variant must fail.
4008     paramsExternalBuild.arguments = QStringList{"-f", "consumer.qbs",
4009             "modules.qbs.buildVariant:profiling"};
4010     paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-profile";
4011     paramsExternalBuild.expectFailure = true;
4012     QVERIFY(runQbs(paramsExternalBuild) != 0);
4013     QVERIFY2(m_qbsStderr.contains("MyLib could not be loaded"), m_qbsStderr.constData());
4014 
4015     // Removing the condition from the generated module leaves us with two conflicting
4016     // candidates.
4017     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{ "-f", "exports-qbs.qbs",
4018             "modules.Exporter.qbs.additionalContent:''"})), 0);
4019     QCOMPARE(runQbs(), 0);
4020     QVERIFY(runQbs(paramsExternalBuild) != 0);
4021     QVERIFY2(m_qbsStderr.contains("There is more than one equally prioritized candidate "
4022                                   "for module 'MyLib'."), m_qbsStderr.constData());
4023 
4024     // Change tracking for accesses to product.exports (negative).
4025     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{"-f", "exports-qbs.qbs"})), 0);
4026     QCOMPARE(runQbs(), 0);
4027     QVERIFY2(m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
4028     WAIT_FOR_NEW_TIMESTAMP();
4029     touch("exports-qbs.qbs");
4030     QCOMPARE(runQbs(QStringList({"-p", "MyTool"})), 0);
4031     QVERIFY2(!m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
4032 
4033     // Rebuilding the target binary should not cause recreating the module file.
4034     WAIT_FOR_NEW_TIMESTAMP();
4035     touch("mylib.cpp");
4036     QCOMPARE(runQbs(), 0);
4037     QVERIFY2(m_qbsStdout.count("linking") >= 2, m_qbsStdout.constData());
4038     QVERIFY2(!m_qbsStdout.contains("Creating MyLib"), m_qbsStdout.constData());
4039     QVERIFY2(!m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
4040 
4041     // Changing a setting that influences the name of a target artifact should cause
4042     // recreating the module file.
4043     const QbsRunParameters resolveParams("resolve", QStringList{"-f", "exports-qbs.qbs",
4044             "modules.cpp.dynamicLibrarySuffix:.blubb"});
4045     QCOMPARE(runQbs(resolveParams), 0);
4046     QCOMPARE(runQbs(), 0);
4047     QVERIFY2(m_qbsStdout.count("linking") >= 2, m_qbsStdout.constData());
4048     QVERIFY2(m_qbsStdout.count("Creating MyLib") == 2, m_qbsStdout.constData());
4049     QVERIFY2(!m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
4050 
4051     // Change tracking for accesses to product.exports (positive).
4052     WAIT_FOR_NEW_TIMESTAMP();
4053     REPLACE_IN_FILE("tool.qbs", "exportingProduct.toolTags", "[]");
4054     QCOMPARE(runQbs(QStringList({"-p", "MyTool"})), 0);
4055     QVERIFY2(m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
4056 }
4057 
externalLibs()4058 void TestBlackbox::externalLibs()
4059 {
4060     QDir::setCurrent(testDataDir + "/external-libs");
4061     QCOMPARE(runQbs(), 0);
4062 }
4063 
fileDependencies()4064 void TestBlackbox::fileDependencies()
4065 {
4066     QDir::setCurrent(testDataDir + "/fileDependencies");
4067     rmDirR(relativeBuildDir());
4068     QCOMPARE(runQbs(), 0);
4069     QVERIFY(m_qbsStdout.contains("compiling narf.cpp"));
4070     QVERIFY(m_qbsStdout.contains("compiling zort.cpp"));
4071     const QString productFileName = relativeExecutableFilePath("myapp");
4072     QVERIFY2(regularFileExists(productFileName), qPrintable(productFileName));
4073 
4074     // Incremental build without changes.
4075     QCOMPARE(runQbs(), 0);
4076     QVERIFY(!m_qbsStdout.contains("compiling"));
4077     QVERIFY(!m_qbsStdout.contains("linking"));
4078 
4079     // Incremental build with changed file dependency.
4080     WAIT_FOR_NEW_TIMESTAMP();
4081     touch("awesomelib/awesome.h");
4082     QCOMPARE(runQbs(), 0);
4083     QVERIFY(m_qbsStdout.contains("compiling narf.cpp"));
4084     QVERIFY(!m_qbsStdout.contains("compiling zort.cpp"));
4085 
4086     // Incremental build with changed 2nd level file dependency.
4087     WAIT_FOR_NEW_TIMESTAMP();
4088     touch("awesomelib/magnificent.h");
4089     QCOMPARE(runQbs(), 0);
4090     QVERIFY(m_qbsStdout.contains("compiling narf.cpp"));
4091     QVERIFY(!m_qbsStdout.contains("compiling zort.cpp"));
4092 
4093     // Change the product in between to force the list of dependencies to get rescued.
4094     REPLACE_IN_FILE("fileDependencies.qbs", "//", "");
4095     QCOMPARE(runQbs(), 0);
4096     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
4097     QVERIFY(!m_qbsStdout.contains("compiling narf.cpp"));
4098     QVERIFY(!m_qbsStdout.contains("compiling zort.cpp"));
4099     WAIT_FOR_NEW_TIMESTAMP();
4100     touch("awesomelib/magnificent.h");
4101     QCOMPARE(runQbs(), 0);
4102     QVERIFY2(m_qbsStdout.contains("compiling narf.cpp [myapp]"), m_qbsStdout.constData());
4103     QVERIFY(!m_qbsStdout.contains("compiling zort.cpp"));
4104 }
4105 
fileTagsFilterMerging()4106 void TestBlackbox::fileTagsFilterMerging()
4107 {
4108     QDir::setCurrent(testDataDir + "/filetagsfilter-merging");
4109     QCOMPARE(runQbs(QStringList{"-f", "filetagsfilter-merging.qbs"}), 0);
4110     const QString installedApp = defaultInstallRoot + "/myapp/binDir/"
4111             + QFileInfo(relativeExecutableFilePath("myapp")).fileName();
4112     QVERIFY2(QFile::exists(installedApp), qPrintable(installedApp));
4113     const QString otherOutput = relativeProductBuildDir("myapp") + "/myapp.txt";
4114     QVERIFY2(QFile::exists(otherOutput), qPrintable(otherOutput));
4115 }
4116 
freedesktop()4117 void TestBlackbox::freedesktop()
4118 {
4119     if (!HostOsInfo::isAnyUnixHost())
4120         QSKIP("only applies on Unix");
4121     if (HostOsInfo::isMacosHost())
4122         QSKIP("Does not apply on macOS");
4123     QDir::setCurrent(testDataDir + "/freedesktop");
4124     QCOMPARE(runQbs(), 0);
4125 
4126     // Check desktop file
4127     QString desktopFilePath =
4128         defaultInstallRoot + "/usr/local/share/applications/myapp.desktop";
4129     QVERIFY(QFile::exists(desktopFilePath));
4130     QFile desktopFile(desktopFilePath);
4131     QVERIFY2(desktopFile.open(QIODevice::ReadOnly), qPrintable(desktopFile.errorString()));
4132     QByteArrayList lines = desktopFile.readAll().split('\n');
4133     // Automatically filled line:
4134     QVERIFY(lines.contains("Exec=main"));
4135     // Name specified in `freedesktop.name` property
4136     QVERIFY(lines.contains("Name=My App"));
4137     // Overridden line:
4138     QVERIFY(lines.contains("Icon=myapp.png"));
4139     // Untouched line:
4140     QVERIFY(lines.contains("Terminal=false"));
4141 
4142     // Check AppStream file
4143     QVERIFY(QFile::exists(defaultInstallRoot +
4144                           "/usr/local/share/metainfo/myapp.appdata.xml"));
4145 
4146     // Check icon file
4147     QVERIFY(QFile::exists(defaultInstallRoot +
4148                           "/usr/local/share/icons/hicolor/scalable/apps/myapp.png"));
4149 }
4150 
installedTransformerOutput()4151 void TestBlackbox::installedTransformerOutput()
4152 {
4153     QDir::setCurrent(testDataDir + "/installed-transformer-output");
4154     QCOMPARE(runQbs(), 0);
4155     const QString installedFilePath = defaultInstallRoot + "/textfiles/HelloWorld.txt";
4156     QVERIFY2(QFile::exists(installedFilePath), qPrintable(installedFilePath));
4157 }
4158 
installLocations_data()4159 void TestBlackbox::installLocations_data()
4160 {
4161     QTest::addColumn<QString>("binDir");
4162     QTest::addColumn<QString>("dllDir");
4163     QTest::addColumn<QString>("libDir");
4164     QTest::addColumn<QString>("pluginDir");
4165     QTest::addColumn<QString>("dsymDir");
4166     QTest::newRow("explicit values")
4167             << QString("bindir")
4168             << QString("dlldir")
4169             << QString("libdir")
4170             << QString("pluginDir")
4171             << QString("dsymDir");
4172     QTest::newRow("default values")
4173             << QString() << QString() << QString() << QString() << QString();
4174 }
4175 
installLocations()4176 void TestBlackbox::installLocations()
4177 {
4178     QDir::setCurrent(testDataDir + "/install-locations");
4179     QFETCH(QString, binDir);
4180     QFETCH(QString, dllDir);
4181     QFETCH(QString, libDir);
4182     QFETCH(QString, pluginDir);
4183     QFETCH(QString, dsymDir);
4184     QbsRunParameters params("resolve");
4185     if (!binDir.isEmpty())
4186         params.arguments.push_back("products.theapp.installDir:" + binDir);
4187     if (!dllDir.isEmpty())
4188         params.arguments.push_back("products.thelib.installDir:" + dllDir);
4189     if (!libDir.isEmpty())
4190         params.arguments.push_back("products.thelib.importLibInstallDir:" + libDir);
4191     if (!pluginDir.isEmpty())
4192         params.arguments.push_back("products.theplugin.installDir:" + pluginDir);
4193     if (!dsymDir.isEmpty()) {
4194         params.arguments.push_back("products.theapp.debugInformationInstallDir:" + dsymDir);
4195         params.arguments.push_back("products.thelib.debugInformationInstallDir:" + dsymDir);
4196         params.arguments.push_back("products.theplugin.debugInformationInstallDir:" + dsymDir);
4197     }
4198     QCOMPARE(runQbs(params), 0);
4199     const bool isWindows = m_qbsStdout.contains("is windows");
4200     const bool isDarwin = m_qbsStdout.contains("is darwin");
4201     const bool isMac = m_qbsStdout.contains("is mac");
4202     const bool isUnix = m_qbsStdout.contains("is unix");
4203     const bool isMingw = m_qbsStdout.contains("is mingw");
4204     QVERIFY(isWindows || isDarwin || isUnix);
4205     QCOMPARE(runQbs(QbsRunParameters(QStringList("--clean-install-root"))), 0);
4206 
4207     struct BinaryInfo
4208     {
4209         QString fileName;
4210         QString installDir;
4211         QString subDir;
4212 
4213         QString absolutePath(const QString &prefix) const
4214         {
4215             return QDir::cleanPath(prefix + '/' + installDir + '/' + subDir + '/' + fileName);
4216         }
4217     };
4218 
4219     const BinaryInfo dll = {
4220         isWindows ? "thelib.dll" : isDarwin ? "thelib" : "libthelib.so",
4221         dllDir.isEmpty()
4222             ? (isDarwin ? "/Library/Frameworks" : (isWindows ? "/bin" : "/lib"))
4223             : dllDir,
4224         isDarwin ? "thelib.framework" : ""
4225     };
4226     const BinaryInfo dllDsym = {
4227         isWindows
4228             ? (!isMingw ? "thelib.pdb" : "thelib.dll.debug")
4229             : isDarwin ? "thelib.framework.dSYM" : "libthelib.so.debug",
4230         dsymDir.isEmpty() ? dll.installDir : dsymDir,
4231         {}
4232     };
4233     const BinaryInfo plugin = {
4234         isWindows ? "theplugin.dll" : isDarwin ? "theplugin" : "libtheplugin.so",
4235         pluginDir.isEmpty() ? dll.installDir : pluginDir,
4236         isDarwin ? (isMac ? "theplugin.bundle/Contents/MacOS" : "theplugin.bundle") : ""
4237     };
4238     const BinaryInfo pluginDsym = {
4239         isWindows
4240             ? (!isMingw ? "theplugin.pdb" : "theplugin.dll.debug")
4241             : isDarwin ? "theplugin.bundle.dSYM" : "libtheplugin.so.debug",
4242         dsymDir.isEmpty() ? plugin.installDir : dsymDir,
4243         {}
4244     };
4245     const BinaryInfo app = {
4246         isWindows ? "theapp.exe" : "theapp",
4247         binDir.isEmpty() ? (isDarwin ? "/Applications" : "/bin") : binDir,
4248         isDarwin ? (isMac ? "theapp.app/Contents/MacOS" : "theapp.app") : ""
4249     };
4250     const BinaryInfo appDsym = {
4251         isWindows
4252             ? (!isMingw ? "theapp.pdb" : "theapp.exe.debug")
4253             : isDarwin ? "theapp.app.dSYM" : "theapp.debug",
4254         dsymDir.isEmpty() ? app.installDir : dsymDir,
4255         {}
4256     };
4257 
4258     const QString installRoot = QDir::currentPath() + "/default/install-root";
4259     const QString installPrefix = isWindows ? QString() : "/usr/local";
4260     const QString fullInstallPrefix = installRoot + '/' + installPrefix + '/';
4261     const QString appFilePath = app.absolutePath(fullInstallPrefix);
4262     QVERIFY2(QFile::exists(appFilePath), qPrintable(appFilePath));
4263     const QString dllFilePath = dll.absolutePath(fullInstallPrefix);
4264     QVERIFY2(QFile::exists(dllFilePath), qPrintable(dllFilePath));
4265     if (isWindows) {
4266         const BinaryInfo lib = {
4267             "thelib.lib",
4268             libDir.isEmpty() ? "/lib" : libDir,
4269             ""
4270         };
4271         const QString libFilePath = lib.absolutePath(fullInstallPrefix);
4272         QVERIFY2(QFile::exists(libFilePath), qPrintable(libFilePath));
4273     }
4274     const QString pluginFilePath = plugin.absolutePath(fullInstallPrefix);
4275     QVERIFY2(QFile::exists(pluginFilePath), qPrintable(pluginFilePath));
4276 
4277     const QString appDsymFilePath = appDsym.absolutePath(fullInstallPrefix);
4278     QVERIFY2(QFileInfo(appDsymFilePath).exists(), qPrintable(appDsymFilePath));
4279     const QString dllDsymFilePath = dllDsym.absolutePath(fullInstallPrefix);
4280     QVERIFY2(QFileInfo(dllDsymFilePath).exists(), qPrintable(dllDsymFilePath));
4281     const QString pluginDsymFilePath = pluginDsym.absolutePath(fullInstallPrefix);
4282     QVERIFY2(QFile::exists(pluginDsymFilePath), qPrintable(pluginDsymFilePath));
4283 }
4284 
inputsFromDependencies()4285 void TestBlackbox::inputsFromDependencies()
4286 {
4287     QDir::setCurrent(testDataDir + "/inputs-from-dependencies");
4288     QCOMPARE(runQbs(), 0);
4289     const QList<QByteArray> output = m_qbsStdout.trimmed().split('\n');
4290     QVERIFY2(output.contains((QDir::currentPath() + "/file1.txt").toUtf8()),
4291              m_qbsStdout.constData());
4292     QVERIFY2(output.contains((QDir::currentPath() + "/file2.txt").toUtf8()),
4293              m_qbsStdout.constData());
4294     QVERIFY2(output.contains((QDir::currentPath() + "/file3.txt").toUtf8()),
4295              m_qbsStdout.constData());
4296     QVERIFY2(!output.contains((QDir::currentPath() + "/file4.txt").toUtf8()),
4297              m_qbsStdout.constData());
4298 }
4299 
installPackage()4300 void TestBlackbox::installPackage()
4301 {
4302     if (HostOsInfo::hostOs() == HostOsInfo::HostOsWindows)
4303         QSKIP("Beware of the msys tar");
4304     QString binary = findArchiver("tar");
4305     if (binary.isEmpty())
4306         QSKIP("tar not found");
4307     MacosTarHealer tarHealer;
4308     QDir::setCurrent(testDataDir + "/installpackage");
4309     QCOMPARE(runQbs(), 0);
4310     const QString tarFilePath = relativeProductBuildDir("tar-package") + "/tar-package.tar.gz";
4311     QVERIFY2(regularFileExists(tarFilePath), qPrintable(tarFilePath));
4312     QProcess tarList;
4313     tarList.start(binary, QStringList() << "tf" << tarFilePath);
4314     QVERIFY2(tarList.waitForStarted(), qPrintable(tarList.errorString()));
4315     QVERIFY2(tarList.waitForFinished(), qPrintable(tarList.errorString()));
4316     const QList<QByteArray> outputLines = tarList.readAllStandardOutput().split('\n');
4317     QList<QByteArray> cleanOutputLines;
4318     for (const QByteArray &line : outputLines) {
4319         const QByteArray trimmedLine = line.trimmed();
4320         if (!trimmedLine.isEmpty())
4321             cleanOutputLines.push_back(trimmedLine);
4322     }
4323     QCOMPARE(cleanOutputLines.size(), 3);
4324     for (const QByteArray &line : qAsConst(cleanOutputLines)) {
4325         QVERIFY2(line.contains("public_tool") || line.contains("mylib") || line.contains("lib.h"),
4326                  line.constData());
4327     }
4328 }
4329 
installRootFromProjectFile()4330 void TestBlackbox::installRootFromProjectFile()
4331 {
4332     QDir::setCurrent(testDataDir + "/install-root-from-project-file");
4333     const QString installRoot = QDir::currentPath() + '/' + relativeBuildDir()
4334             + "/my-install-root/";
4335     QCOMPARE(runQbs(QbsRunParameters(QStringList("products.p.installRoot:" + installRoot))), 0);
4336     const QString installedFile = installRoot + "/install-prefix/install-dir/file.txt";
4337     QVERIFY2(QFile::exists(installedFile), qPrintable(installedFile));
4338 }
4339 
installable()4340 void TestBlackbox::installable()
4341 {
4342     QDir::setCurrent(testDataDir + "/installable");
4343     QCOMPARE(runQbs(), 0);
4344     QFile installList(relativeProductBuildDir("install-list") + "/installed-files.txt");
4345     QVERIFY2(installList.open(QIODevice::ReadOnly), qPrintable(installList.errorString()));
4346     QCOMPARE(installList.readAll().count('\n'), 2);
4347 }
4348 
installableAsAuxiliaryInput()4349 void TestBlackbox::installableAsAuxiliaryInput()
4350 {
4351     QDir::setCurrent(testDataDir + "/installable-as-auxiliary-input");
4352     QCOMPARE(runQbs({"resolve"}), 0);
4353     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
4354         QSKIP("Cannot run binaries in cross-compiled build");
4355     QCOMPARE(runQbs(QbsRunParameters("run")), 0);
4356     QVERIFY2(m_qbsStdout.contains("f-impl"), m_qbsStdout.constData());
4357 }
4358 
installTree()4359 void TestBlackbox::installTree()
4360 {
4361     QDir::setCurrent(testDataDir + "/install-tree");
4362     QbsRunParameters params;
4363     params.command = "install";
4364     QCOMPARE(runQbs(params), 0);
4365     const QString installRoot = relativeBuildDir() + "/install-root/";
4366     QVERIFY(QFile::exists(installRoot + "content/foo.txt"));
4367     QVERIFY(QFile::exists(installRoot + "content/subdir1/bar.txt"));
4368     QVERIFY(QFile::exists(installRoot + "content/subdir2/baz.txt"));
4369 }
4370 
invalidCommandProperty_data()4371 void TestBlackbox::invalidCommandProperty_data()
4372 {
4373     QTest::addColumn<QString>("errorType");
4374 
4375     QTest::newRow("assigning QObject") << QString("qobject");
4376     QTest::newRow("assigning input artifact") << QString("input");
4377     QTest::newRow("assigning other artifact") << QString("artifact");
4378 }
4379 
invalidCommandProperty()4380 void TestBlackbox::invalidCommandProperty()
4381 {
4382     QDir::setCurrent(testDataDir + "/invalid-command-property");
4383     QFETCH(QString, errorType);
4384     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("products.p.errorType:" + errorType))),
4385              0);
4386     QbsRunParameters params;
4387     params.expectFailure = true;
4388     QVERIFY(runQbs(params) != 0);
4389     QVERIFY2(m_qbsStderr.contains("unsuitable"), m_qbsStderr.constData());
4390 }
4391 
invalidLibraryNames()4392 void TestBlackbox::invalidLibraryNames()
4393 {
4394     QDir::setCurrent(testDataDir + "/invalid-library-names");
4395     QFETCH(QString, index);
4396     QFETCH(bool, success);
4397     QFETCH(QStringList, diagnostics);
4398     QbsRunParameters params(QStringList("project.valueIndex:" + index));
4399     params.expectFailure = !success;
4400     QCOMPARE(runQbs(params) == 0, success);
4401     for (const QString &diag : qAsConst(diagnostics))
4402         QVERIFY2(m_qbsStderr.contains(diag.toLocal8Bit()), m_qbsStderr.constData());
4403 }
4404 
invalidLibraryNames_data()4405 void TestBlackbox::invalidLibraryNames_data()
4406 {
4407     QTest::addColumn<QString>("index");
4408     QTest::addColumn<bool>("success");
4409     QTest::addColumn<QStringList>("diagnostics");
4410 
4411     QTest::newRow("null") << "0" << false << QStringList("is null");
4412     QTest::newRow("undefined") << "1" << false << QStringList("is undefined");
4413     QTest::newRow("number") << "2" << false << QStringList("does not have string type");
4414     QTest::newRow("array") << "3" << false << QStringList("does not have string type");
4415     QTest::newRow("empty string") << "4" << true << (QStringList()
4416                                   << "WARNING: Removing empty string from value of property "
4417                                      "'cpp.dynamicLibraries' in product 'invalid-library-names'."
4418                                   << "WARNING: Removing empty string from value of property "
4419                                      "'cpp.staticLibraries' in product 'invalid-library-names'.");
4420 }
4421 
invalidExtensionInstantiation()4422 void TestBlackbox::invalidExtensionInstantiation()
4423 {
4424     rmDirR(relativeBuildDir());
4425     QDir::setCurrent(testDataDir + "/invalid-extension-instantiation");
4426     QbsRunParameters params;
4427     params.expectFailure = true;
4428     params.arguments << (QString("products.theProduct.extension:") + QTest::currentDataTag());
4429     QVERIFY(runQbs(params) != 0);
4430     QVERIFY2(m_qbsStderr.contains("invalid-extension-instantiation.qbs:17")
4431              && m_qbsStderr.contains('\'' + QByteArray(QTest::currentDataTag())
4432                                      + "' cannot be instantiated"),
4433              m_qbsStderr.constData());
4434 }
4435 
invalidExtensionInstantiation_data()4436 void TestBlackbox::invalidExtensionInstantiation_data()
4437 {
4438     QTest::addColumn<bool>("dummy");
4439 
4440     QTest::newRow("Environment");
4441     QTest::newRow("File");
4442     QTest::newRow("FileInfo");
4443     QTest::newRow("Utilities");
4444 }
4445 
invalidInstallDir()4446 void TestBlackbox::invalidInstallDir()
4447 {
4448     QDir::setCurrent(testDataDir + "/invalid-install-dir");
4449     QbsRunParameters params;
4450     params.expectFailure = true;
4451     QVERIFY(runQbs(params) != 0);
4452     QVERIFY2(m_qbsStderr.contains("outside of install root"), m_qbsStderr.constData());
4453 }
4454 
cli()4455 void TestBlackbox::cli()
4456 {
4457     int status;
4458     findCli(&status);
4459     QCOMPARE(status, 0);
4460 
4461     const SettingsPtr s = settings();
4462     Profile p("qbs_autotests-cli", s.get());
4463     const QStringList toolchain = profileToolchain(p);
4464     if (!p.exists() || !(toolchain.contains("dotnet") || toolchain.contains("mono")))
4465         QSKIP("No suitable Common Language Infrastructure test profile");
4466 
4467     QDir::setCurrent(testDataDir + "/cli");
4468     QbsRunParameters params(QStringList() << "-f" << "dotnettest.qbs");
4469     params.profile = p.name();
4470 
4471     status = runQbs(params);
4472     if (p.value("cli.toolchainInstallPath").toString().isEmpty()
4473             && status != 0 && m_qbsStderr.contains("toolchainInstallPath"))
4474         QSKIP("cli.toolchainInstallPath not set and automatic detection failed");
4475 
4476     QCOMPARE(status, 0);
4477     rmDirR(relativeBuildDir());
4478 
4479     QbsRunParameters params2(QStringList() << "-f" << "fshello.qbs");
4480     params2.profile = p.name();
4481     QCOMPARE(runQbs(params2), 0);
4482     rmDirR(relativeBuildDir());
4483 }
4484 
combinedSources()4485 void TestBlackbox::combinedSources()
4486 {
4487     QDir::setCurrent(testDataDir + "/combined-sources");
4488     QbsRunParameters params(QStringList("modules.cpp.combineCxxSources:false"));
4489     QCOMPARE(runQbs(params), 0);
4490     QVERIFY(m_qbsStdout.contains("compiling main.cpp"));
4491     QVERIFY(m_qbsStdout.contains("compiling combinable.cpp"));
4492     QVERIFY(m_qbsStdout.contains("compiling uncombinable.cpp"));
4493     QVERIFY(!m_qbsStdout.contains("compiling amalgamated_theapp.cpp"));
4494     params.arguments = QStringList("modules.cpp.combineCxxSources:true");
4495     params.command = "resolve";
4496     QCOMPARE(runQbs(params), 0);
4497     WAIT_FOR_NEW_TIMESTAMP();
4498     touch("combinable.cpp");
4499     touch("main.cpp");
4500     touch("uncombinable.cpp");
4501     params.command = "build";
4502     QCOMPARE(runQbs(params), 0);
4503     QVERIFY(!m_qbsStdout.contains("compiling main.cpp"));
4504     QVERIFY(!m_qbsStdout.contains("compiling combinable.cpp"));
4505     QVERIFY(m_qbsStdout.contains("compiling uncombinable.cpp"));
4506     QVERIFY(m_qbsStdout.contains("compiling amalgamated_theapp.cpp"));
4507 }
4508 
commandFile()4509 void TestBlackbox::commandFile()
4510 {
4511     QDir::setCurrent(testDataDir + "/command-file");
4512     QbsRunParameters params(QStringList() << "-p" << "theLib");
4513     QCOMPARE(runQbs(params), 0);
4514     params.arguments = QStringList() << "-p" << "theApp";
4515     QCOMPARE(runQbs(params), 0);
4516 }
4517 
compilerDefinesByLanguage()4518 void TestBlackbox::compilerDefinesByLanguage()
4519 {
4520     QDir::setCurrent(testDataDir + "/compilerDefinesByLanguage");
4521     QbsRunParameters params(QStringList { "-f", "compilerDefinesByLanguage.qbs" });
4522     QCOMPARE(runQbs(params), 0);
4523 }
4524 
jsExtensionsFile()4525 void TestBlackbox::jsExtensionsFile()
4526 {
4527     QDir::setCurrent(testDataDir + "/jsextensions-file");
4528     QFile fileToMove("tomove.txt");
4529     QVERIFY2(fileToMove.open(QIODevice::WriteOnly), qPrintable(fileToMove.errorString()));
4530     fileToMove.close();
4531     fileToMove.setPermissions(fileToMove.permissions() & ~(QFile::ReadUser | QFile::ReadOwner
4532                                                            | QFile::ReadGroup | QFile::ReadOther));
4533     QbsRunParameters params(QStringList() << "-f" << "file.qbs");
4534     QCOMPARE(runQbs(params), 0);
4535     QVERIFY(!QFileInfo("original.txt").exists());
4536     QFile copy("copy.txt");
4537     QVERIFY(copy.exists());
4538     QVERIFY(copy.open(QIODevice::ReadOnly));
4539     const QList<QByteArray> lines = copy.readAll().trimmed().split('\n');
4540     QCOMPARE(lines.size(), 2);
4541     QCOMPARE(lines.at(0).trimmed().constData(), "false");
4542     QCOMPARE(lines.at(1).trimmed().constData(), "true");
4543 }
4544 
jsExtensionsFileInfo()4545 void TestBlackbox::jsExtensionsFileInfo()
4546 {
4547     QDir::setCurrent(testDataDir + "/jsextensions-fileinfo");
4548     QbsRunParameters params(QStringList() << "-f" << "fileinfo.qbs");
4549     QCOMPARE(runQbs(params), 0);
4550     QFile output("output.txt");
4551     QVERIFY(output.exists());
4552     QVERIFY(output.open(QIODevice::ReadOnly));
4553     const QList<QByteArray> lines = output.readAll().trimmed().split('\n');
4554     QCOMPARE(lines.size(), 26);
4555     int i = 0;
4556     QCOMPARE(lines.at(i++).trimmed().constData(), "blubb");
4557     QCOMPARE(lines.at(i++).trimmed().constData(), qUtf8Printable(
4558                  QFileInfo(QDir::currentPath()).canonicalFilePath()));
4559     QCOMPARE(lines.at(i++).trimmed().constData(), "/usr/bin");
4560     QCOMPARE(lines.at(i++).trimmed().constData(), "blubb.tar");
4561     QCOMPARE(lines.at(i++).trimmed().constData(), "blubb.tar.gz");
4562     QCOMPARE(lines.at(i++).trimmed().constData(), "/tmp/blubb.tar.gz");
4563     QCOMPARE(lines.at(i++).trimmed().constData(), "c:/tmp/blubb.tar.gz");
4564     QCOMPARE(lines.at(i++).trimmed().constData(), "true");
4565     QCOMPARE(lines.at(i++).trimmed().constData(), HostOsInfo::isWindowsHost() ? "true" : "false");
4566     QCOMPARE(lines.at(i++).trimmed().constData(), "false");
4567     QCOMPARE(lines.at(i++).trimmed().constData(), "true");
4568     QCOMPARE(lines.at(i++).trimmed().constData(), "false");
4569     QCOMPARE(lines.at(i++).trimmed().constData(), "false");
4570     QCOMPARE(lines.at(i++).trimmed().constData(), "/tmp/blubb.tar.gz");
4571     QCOMPARE(lines.at(i++).trimmed().constData(), "/tmp/blubb.tar.gz");
4572     QCOMPARE(lines.at(i++).trimmed().constData(), "/tmp");
4573     QCOMPARE(lines.at(i++).trimmed().constData(), "/tmp");
4574     QCOMPARE(lines.at(i++).trimmed().constData(), "/");
4575     QCOMPARE(lines.at(i++).trimmed().constData(), HostOsInfo::isWindowsHost() ? "d:/" : "d:");
4576     QCOMPARE(lines.at(i++).trimmed().constData(), "d:");
4577     QCOMPARE(lines.at(i++).trimmed().constData(), "d:/");
4578     QCOMPARE(lines.at(i++).trimmed().constData(), "blubb.tar.gz");
4579     QCOMPARE(lines.at(i++).trimmed().constData(), "tmp/blubb.tar.gz");
4580     QCOMPARE(lines.at(i++).trimmed().constData(), "../blubb.tar.gz");
4581     QCOMPARE(lines.at(i++).trimmed().constData(), "\\tmp\\blubb.tar.gz");
4582     QCOMPARE(lines.at(i++).trimmed().constData(), "c:\\tmp\\blubb.tar.gz");
4583 }
4584 
jsExtensionsProcess()4585 void TestBlackbox::jsExtensionsProcess()
4586 {
4587     QDir::setCurrent(testDataDir + "/jsextensions-process");
4588     QCOMPARE(runQbs({"resolve", {"-f", "process.qbs"}}), 0);
4589     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
4590         QSKIP("Cannot run binaries in cross-compiled build");
4591     QbsRunParameters params(QStringList() << "-f" << "process.qbs");
4592     QCOMPARE(runQbs(params), 0);
4593     QFile output("output.txt");
4594     QVERIFY(output.exists());
4595     QVERIFY(output.open(QIODevice::ReadOnly));
4596     const QList<QByteArray> lines = output.readAll().trimmed().split('\n');
4597     QCOMPARE(lines.size(), 9);
4598     QCOMPARE(lines.at(0).trimmed().constData(), "0");
4599     QVERIFY(lines.at(1).startsWith("qbs "));
4600     QCOMPARE(lines.at(2).trimmed().constData(), "true");
4601     QCOMPARE(lines.at(3).trimmed().constData(), "true");
4602     QCOMPARE(lines.at(4).trimmed().constData(), "0");
4603     QVERIFY(lines.at(5).startsWith("qbs "));
4604     QCOMPARE(lines.at(6).trimmed().constData(), "false");
4605     QCOMPARE(lines.at(7).trimmed().constData(), "should be");
4606     QCOMPARE(lines.at(8).trimmed().constData(), "123");
4607 }
4608 
jsExtensionsPropertyList()4609 void TestBlackbox::jsExtensionsPropertyList()
4610 {
4611     if (!HostOsInfo::isMacosHost())
4612         QSKIP("temporarily only applies on macOS");
4613 
4614     QDir::setCurrent(testDataDir + "/jsextensions-propertylist");
4615     QbsRunParameters params(QStringList() << "-nf" << "propertylist.qbs");
4616     QCOMPARE(runQbs(params), 0);
4617     QFile file1("test.json");
4618     QVERIFY(file1.exists());
4619     QVERIFY(file1.open(QIODevice::ReadOnly));
4620     QFile file2("test.xml");
4621     QVERIFY(file2.exists());
4622     QVERIFY(file2.open(QIODevice::ReadOnly));
4623     QFile file3("test2.json");
4624     QVERIFY(file3.exists());
4625     QVERIFY(file3.open(QIODevice::ReadOnly));
4626     QByteArray file1Contents = file1.readAll();
4627     QCOMPARE(file3.readAll(), file1Contents);
4628     //QCOMPARE(file1Contents, file2.readAll()); // keys don't have guaranteed order
4629     QJsonParseError err1, err2;
4630     QCOMPARE(QJsonDocument::fromJson(file1Contents, &err1),
4631              QJsonDocument::fromJson(file2.readAll(), &err2));
4632     QVERIFY(err1.error == QJsonParseError::NoError && err2.error == QJsonParseError::NoError);
4633     QFile file4("test.openstep.plist");
4634     QVERIFY(file4.exists());
4635     QFile file5("test3.json");
4636     QVERIFY(file5.exists());
4637     QVERIFY(file5.open(QIODevice::ReadOnly));
4638     QVERIFY(file1Contents != file5.readAll());
4639 }
4640 
jsExtensionsTemporaryDir()4641 void TestBlackbox::jsExtensionsTemporaryDir()
4642 {
4643     QDir::setCurrent(testDataDir + "/jsextensions-temporarydir");
4644     QbsRunParameters params;
4645     QCOMPARE(runQbs(params), 0);
4646 }
4647 
jsExtensionsTextFile()4648 void TestBlackbox::jsExtensionsTextFile()
4649 {
4650     QDir::setCurrent(testDataDir + "/jsextensions-textfile");
4651     QbsRunParameters params(QStringList() << "-f" << "textfile.qbs");
4652     QCOMPARE(runQbs(params), 0);
4653     QFile file1("file1.txt");
4654     QVERIFY(file1.exists());
4655     QVERIFY(file1.open(QIODevice::ReadOnly));
4656     QCOMPARE(file1.size(), qint64(0));
4657     QFile file2("file2.txt");
4658     QVERIFY(file2.exists());
4659     QVERIFY(file2.open(QIODevice::ReadOnly));
4660     const QList<QByteArray> lines = file2.readAll().trimmed().split('\n');
4661     QCOMPARE(lines.size(), 6);
4662     QCOMPARE(lines.at(0).trimmed().constData(), "false");
4663     QCOMPARE(lines.at(1).trimmed().constData(), "First line.");
4664     QCOMPARE(lines.at(2).trimmed().constData(), "Second line.");
4665     QCOMPARE(lines.at(3).trimmed().constData(), "Third line.");
4666     QCOMPARE(lines.at(4).trimmed().constData(), qPrintable(QDir::currentPath() + "/file1.txt"));
4667     QCOMPARE(lines.at(5).trimmed().constData(), "true");
4668 }
4669 
jsExtensionsBinaryFile()4670 void TestBlackbox::jsExtensionsBinaryFile()
4671 {
4672     QDir::setCurrent(testDataDir + "/jsextensions-binaryfile");
4673     QbsRunParameters params(QStringList() << "-f" << "binaryfile.qbs");
4674     QCOMPARE(runQbs(params), 0);
4675     QFile source("source.dat");
4676     QVERIFY(source.exists());
4677     QVERIFY(source.open(QIODevice::ReadOnly));
4678     QCOMPARE(source.size(), qint64(0));
4679     QFile destination("destination.dat");
4680     QVERIFY(destination.exists());
4681     QVERIFY(destination.open(QIODevice::ReadOnly));
4682     const QByteArray data = destination.readAll();
4683     QCOMPARE(data.size(), 8);
4684     QCOMPARE(data.at(0), char(0x00));
4685     QCOMPARE(data.at(1), char(0x01));
4686     QCOMPARE(data.at(2), char(0x02));
4687     QCOMPARE(data.at(3), char(0x03));
4688     QCOMPARE(data.at(4), char(0x04));
4689     QCOMPARE(data.at(5), char(0x05));
4690     QCOMPARE(data.at(6), char(0x06));
4691     QCOMPARE(data.at(7), char(0xFF));
4692 }
4693 
lastModuleCandidateBroken()4694 void TestBlackbox::lastModuleCandidateBroken()
4695 {
4696     QDir::setCurrent(testDataDir + "/last-module-candidate-broken");
4697     QbsRunParameters params;
4698     params.expectFailure = true;
4699     QVERIFY(runQbs(params) != 0);
4700     QVERIFY2(m_qbsStderr.contains("Module Foo could not be loaded"), m_qbsStderr);
4701 }
4702 
ld()4703 void TestBlackbox::ld()
4704 {
4705     QDir::setCurrent(testDataDir + "/ld");
4706     QCOMPARE(runQbs(), 0);
4707 }
4708 
symbolLinkMode()4709 void TestBlackbox::symbolLinkMode()
4710 {
4711     if (!HostOsInfo::isAnyUnixHost())
4712         QSKIP("only applies on Unix");
4713 
4714     QDir::setCurrent(testDataDir + "/symbolLinkMode");
4715 
4716     QCOMPARE(runQbs({"resolve"}), 0);
4717     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
4718         QSKIP("Cannot run binaries in cross-compiled build");
4719     QbsRunParameters params;
4720     params.command = "run";
4721     const QStringList commonArgs{"-p", "driver", "--setup-run-env-config",
4722                                  "ignore-lib-dependencies", "qbs.installPrefix:''"};
4723 
4724     rmDirR(relativeBuildDir());
4725     params.arguments = QStringList() << commonArgs << "project.shouldInstallLibrary:true";
4726     QCOMPARE(runQbs(params), 0);
4727     QVERIFY2(m_qbsStdout.contains("somefunction existed and it returned 42"),
4728              m_qbsStdout.constData());
4729 
4730     rmDirR(relativeBuildDir());
4731     params.arguments = QStringList() << commonArgs << "project.shouldInstallLibrary:false";
4732     QCOMPARE(runQbs(params), 0);
4733     QVERIFY2(m_qbsStdout.contains("somefunction did not exist"), m_qbsStdout.constData());
4734 
4735     rmDirR(relativeBuildDir());
4736     params.arguments = QStringList() << commonArgs << "project.lazy:false";
4737     QCOMPARE(runQbs(params), 0);
4738     QVERIFY2(m_qbsStdout.contains("Lib was loaded!\nmeow\n"), m_qbsStdout.constData());
4739 
4740     if (HostOsInfo::isMacosHost()) {
4741         rmDirR(relativeBuildDir());
4742         params.arguments = QStringList() << commonArgs << "project.lazy:true";
4743         QCOMPARE(runQbs(params), 0);
4744         QVERIFY2(m_qbsStdout.contains("meow\n") && m_qbsStdout.contains("Lib was loaded!\n"),
4745                  m_qbsStdout.constData());
4746     }
4747 }
4748 
linkerMode()4749 void TestBlackbox::linkerMode()
4750 {
4751     if (!HostOsInfo::isAnyUnixHost())
4752         QSKIP("only applies on Unix");
4753 
4754     QDir::setCurrent(testDataDir + "/linkerMode");
4755     QCOMPARE(runQbs(QbsRunParameters(QStringList("qbs.installPrefix:''"))), 0);
4756 
4757     auto testCondition = [&](const QString &lang,
4758             const std::function<bool(const QByteArray &)> &condition) {
4759         if ((lang == "Objective-C" || lang == "Objective-C++")
4760                 && HostOsInfo::hostOs() != HostOsInfo::HostOsMacos)
4761             return;
4762         const QString binary = defaultInstallRoot + "/LinkedProduct-" + lang;
4763         QProcess deptool;
4764         if (HostOsInfo::hostOs() == HostOsInfo::HostOsMacos)
4765             deptool.start("otool", QStringList() << "-L" << binary);
4766         else
4767             deptool.start("readelf", QStringList() << "-a" << binary);
4768         QVERIFY(deptool.waitForStarted());
4769         QVERIFY(deptool.waitForFinished());
4770         QByteArray deptoolOutput = deptool.readAllStandardOutput();
4771         if (HostOsInfo::hostOs() != HostOsInfo::HostOsMacos) {
4772             QList<QByteArray> lines = deptoolOutput.split('\n');
4773             int sz = lines.size();
4774             for (int i = 0; i < sz; ++i) {
4775                 if (!lines.at(i).contains("NEEDED")) {
4776                     lines.removeAt(i--);
4777                     sz--;
4778                 }
4779             }
4780 
4781             deptoolOutput = lines.join('\n');
4782         }
4783         QCOMPARE(deptool.exitCode(), 0);
4784         QVERIFY2(condition(deptoolOutput), deptoolOutput.constData());
4785     };
4786 
4787     const QStringList nocpplangs = QStringList() << "Assembly" << "C" << "Objective-C";
4788     for (const QString &lang : nocpplangs)
4789         testCondition(lang, [](const QByteArray &lddOutput) { return !lddOutput.contains("c++"); });
4790 
4791     const QStringList cpplangs = QStringList() << "C++" << "Objective-C++";
4792     for (const QString &lang : cpplangs)
4793         testCondition(lang, [](const QByteArray &lddOutput) { return lddOutput.contains("c++"); });
4794 
4795     const QStringList objclangs = QStringList() << "Objective-C" << "Objective-C++";
4796     for (const QString &lang : objclangs)
4797         testCondition(lang, [](const QByteArray &lddOutput) { return lddOutput.contains("objc"); });
4798 }
4799 
linkerVariant_data()4800 void TestBlackbox::linkerVariant_data()
4801 {
4802     QTest::addColumn<QString>("theType");
4803     QTest::newRow("default") << QString();
4804     QTest::newRow("bfd") << QString("bfd");
4805     QTest::newRow("gold") << QString("gold");
4806 }
4807 
linkerVariant()4808 void TestBlackbox::linkerVariant()
4809 {
4810     QDir::setCurrent(testDataDir + "/linker-variant");
4811     QFETCH(QString, theType);
4812     QStringList resolveArgs("--force-probe-execution");
4813     if (!theType.isEmpty())
4814         resolveArgs << ("products.p.linkerVariant:" + theType);
4815     QCOMPARE(runQbs(QbsRunParameters("resolve", resolveArgs)), 0);
4816     const bool isGcc = m_qbsStdout.contains("is GCC: true");
4817     const bool isNotGcc = m_qbsStdout.contains("is GCC: false");
4818     QVERIFY2(isGcc != isNotGcc, m_qbsStdout.constData());
4819     QbsRunParameters buildParams("build", QStringList{"--command-echo-mode", "command-line"});
4820     buildParams.expectFailure = true;
4821     runQbs(buildParams);
4822     if (isGcc && !theType.isEmpty())
4823         QCOMPARE(m_qbsStdout.count("-fuse-ld=" + theType.toLocal8Bit()), 1);
4824     else
4825         QVERIFY2(!m_qbsStdout.contains("-fuse-ld"), m_qbsStdout.constData());
4826 }
4827 
lexyacc()4828 void TestBlackbox::lexyacc()
4829 {
4830     if (!lexYaccExist())
4831         QSKIP("lex or yacc not present");
4832     QDir::setCurrent(testDataDir + "/lexyacc/one-grammar");
4833     QCOMPARE(runQbs({"resolve"}), 0);
4834     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
4835         QSKIP("Cannot run binaries in cross-compiled build");
4836     QCOMPARE(runQbs(), 0);
4837     const QString parserBinary = relativeExecutableFilePath("one-grammar");
4838     QProcess p;
4839     const QByteArray magicString = "add to PATH: ";
4840     const int magicStringIndex = m_qbsStdout.indexOf(magicString);
4841     if (magicStringIndex != -1) {
4842         const int newLineIndex = m_qbsStdout.indexOf('\n', magicStringIndex);
4843         QVERIFY(newLineIndex != -1);
4844         const int dirIndex = magicStringIndex + magicString.length();
4845         const QString dir = QString::fromLocal8Bit(m_qbsStdout.mid(dirIndex,
4846                                                                    newLineIndex - dirIndex));
4847         QProcessEnvironment env;
4848         env.insert("PATH", dir);
4849         p.setProcessEnvironment(env);
4850     }
4851     p.start(parserBinary, QStringList());
4852     QVERIFY2(p.waitForStarted(), qPrintable(p.errorString()));
4853     p.write("a && b || c && !d");
4854     p.closeWriteChannel();
4855     QVERIFY2(p.waitForFinished(), qPrintable(p.errorString()));
4856     QVERIFY2(p.exitCode() == 0, p.readAllStandardError().constData());
4857     const QByteArray parserOutput = p.readAllStandardOutput();
4858     QVERIFY2(parserOutput.contains("OR AND a b AND c NOT d"), parserOutput.constData());
4859 
4860     QDir::setCurrent(testDataDir + "/lexyacc/two-grammars");
4861     QbsRunParameters params;
4862     params.expectFailure = true;
4863     QVERIFY(runQbs(params) != 0);
4864 
4865     params.expectFailure = false;
4866     params.command = "resolve";
4867     params.arguments << (QStringList() << "modules.lex_yacc.uniqueSymbolPrefix:true");
4868     QCOMPARE(runQbs(params), 0);
4869     QCOMPARE(runQbs(), 0);
4870     QVERIFY2(!m_qbsStderr.contains("whatever"), m_qbsStderr.constData());
4871     params.arguments << "modules.lex_yacc.enableCompilerWarnings:true";
4872     QCOMPARE(runQbs(params), 0);
4873     QCOMPARE(runQbs(), 0);
4874     const QByteArray outputToCheck = m_qbsStdout + m_qbsStderr;
4875     QVERIFY2(outputToCheck.contains("whatever"), outputToCheck.constData());
4876 }
4877 
lexyaccOutputs()4878 void TestBlackbox::lexyaccOutputs()
4879 {
4880     if (!lexYaccExist())
4881         QSKIP("lex or yacc not present");
4882 
4883     QFETCH(QString, lexOutputFilePath);
4884     QFETCH(QString, yaccOutputFilePath);
4885     QbsRunParameters params;
4886     if (!lexOutputFilePath.isEmpty())
4887         params.arguments << "modules.lex_yacc.lexOutputFilePath:" + lexOutputFilePath;
4888     if (!yaccOutputFilePath.isEmpty())
4889         params.arguments << "modules.lex_yacc.yaccOutputFilePath:" + yaccOutputFilePath;
4890 
4891 #define VERIFY_COMPILATION(file) \
4892     if (!file.isEmpty()) { \
4893         QByteArray expected = "compiling " + file.toUtf8(); \
4894         if (!m_qbsStdout.contains(expected)) { \
4895             qDebug() << "Expected output:" << expected; \
4896             qDebug() << "Actual output:" << m_qbsStdout; \
4897             QFAIL("Expected stdout content missing."); \
4898         } \
4899     }
4900 
4901     const auto version = bisonVersion();
4902     if (version >= qbs::Version(2, 6)) {
4903         // prefix only supported starting from bison 2.6
4904         QVERIFY(QDir::setCurrent(testDataDir + "/lexyacc/lex_prefix"));
4905         rmDirR(relativeBuildDir());
4906         QCOMPARE(runQbs(params), 0);
4907         VERIFY_COMPILATION(yaccOutputFilePath);
4908     }
4909 
4910     QVERIFY(QDir::setCurrent(testDataDir + "/lexyacc/lex_outfile"));
4911     rmDirR(relativeBuildDir());
4912     QCOMPARE(runQbs(params), 0);
4913     VERIFY_COMPILATION(yaccOutputFilePath);
4914 
4915     if (version >= qbs::Version(2, 4)) {
4916         // output syntax was changed in bison 2.4
4917         QVERIFY(QDir::setCurrent(testDataDir + "/lexyacc/yacc_output"));
4918         rmDirR(relativeBuildDir());
4919         QCOMPARE(runQbs(params), 0);
4920         VERIFY_COMPILATION(lexOutputFilePath);
4921     }
4922 
4923 #undef VERIFY_COMPILATION
4924 }
4925 
lexyaccOutputs_data()4926 void TestBlackbox::lexyaccOutputs_data()
4927 {
4928     QTest::addColumn<QString>("lexOutputFilePath");
4929     QTest::addColumn<QString>("yaccOutputFilePath");
4930     QTest::newRow("none") << QString() << QString();
4931     QTest::newRow("lexOutputFilePath")
4932             << QString{"lex_luthor.cpp"} << QString();
4933     QTest::newRow("yaccOutputFilePath")
4934             << QString() << QString{"shaven_yak.cpp"};
4935 }
4936 
linkerLibraryDuplicates()4937 void TestBlackbox::linkerLibraryDuplicates()
4938 {
4939     const SettingsPtr s = settings();
4940     Profile buildProfile(profileName(), s.get());
4941     QStringList toolchain = profileToolchain(buildProfile);
4942     if (!toolchain.contains("gcc"))
4943         QSKIP("linkerLibraryDuplicates test only applies to GCC toolchain");
4944 
4945     QDir::setCurrent(testDataDir + "/linker-library-duplicates");
4946     rmDirR(relativeBuildDir());
4947 
4948     QFETCH(QString, removeDuplicateLibraries);
4949     QStringList runParams;
4950     if (!removeDuplicateLibraries.isEmpty()) {
4951         runParams.append(removeDuplicateLibraries);
4952     }
4953 
4954     QCOMPARE(runQbs(QbsRunParameters("resolve", runParams)), 0);
4955     QCOMPARE(runQbs(QStringList { "--command-echo-mode", "command-line" }), 0);
4956     const QByteArrayList output = m_qbsStdout.split('\n');
4957     QByteArray linkLine;
4958     for (const QByteArray &line : output) {
4959         if (line.contains("main.cpp.o"))
4960             linkLine = line;
4961     }
4962     QVERIFY(!linkLine.isEmpty());
4963 
4964     /* Now check the the libraries appear just once. In order to avoid dealing
4965      * with the different file extensions used in different platforms, we check
4966      * only for the library base name. But we must also take into account that
4967      * the build directories of each library will contain the library base name,
4968      * so we now exclude them. */
4969     QByteArrayList elementsWithoutPath;
4970     for (const QByteArray &element: linkLine.split(' ')) {
4971         if (element.indexOf('/') < 0)
4972             elementsWithoutPath.append(element);
4973     }
4974     QByteArray pathLessLinkLine = elementsWithoutPath.join(' ');
4975 
4976     typedef QMap<QByteArray,int> ObjectCount;
4977     QFETCH(ObjectCount, expectedObjectCount);
4978     for (auto i = expectedObjectCount.begin();
4979          i != expectedObjectCount.end();
4980          i++) {
4981         QCOMPARE(pathLessLinkLine.count(i.key()), i.value());
4982     }
4983 }
4984 
linkerLibraryDuplicates_data()4985 void TestBlackbox::linkerLibraryDuplicates_data()
4986 {
4987     typedef QMap<QByteArray,int> ObjectCount;
4988 
4989     QTest::addColumn<QString>("removeDuplicateLibraries");
4990     QTest::addColumn<ObjectCount>("expectedObjectCount");
4991 
4992     QTest::newRow("default") <<
4993         QString() <<
4994         ObjectCount {
4995             { "lib1", 1 },
4996             { "lib2", 1 },
4997             { "lib3", 1 },
4998         };
4999 
5000     QTest::newRow("enabled") <<
5001         "modules.cpp.removeDuplicateLibraries:true" <<
5002         ObjectCount {
5003             { "lib1", 1 },
5004             { "lib2", 1 },
5005             { "lib3", 1 },
5006         };
5007 
5008     QTest::newRow("disabled") <<
5009         "modules.cpp.removeDuplicateLibraries:false" <<
5010         ObjectCount {
5011             { "lib1", 3 },
5012             { "lib2", 2 },
5013             { "lib3", 1 },
5014         };
5015 }
5016 
linkerScripts()5017 void TestBlackbox::linkerScripts()
5018 {
5019     const SettingsPtr s = settings();
5020     Profile buildProfile(profileName(), s.get());
5021     QStringList toolchain = profileToolchain(buildProfile);
5022     if (!toolchain.contains("gcc") || targetOs() != HostOsInfo::HostOsLinux)
5023         QSKIP("linker script test only applies to Linux ");
5024 
5025     QbsRunParameters runParams(QStringList()
5026 //                               << "--log-level" << "debug"
5027                                << ("qbs.installRoot:" + QDir::currentPath()));
5028     const QString sourceDir = QDir::cleanPath(testDataDir + "/linkerscripts");
5029     runParams.buildDirectory = sourceDir + "/build";
5030     runParams.workingDir = sourceDir;
5031 
5032     QCOMPARE(runQbs(runParams), 0);
5033     const QString output = QString::fromLocal8Bit(m_qbsStderr);
5034     const QRegularExpression pattern(QRegularExpression::anchoredPattern(".*---(.*)---.*"),
5035                                      QRegularExpression::DotMatchesEverythingOption);
5036     const QRegularExpressionMatch match = pattern.match(output);
5037     QVERIFY2(match.hasMatch(), qPrintable(output));
5038     QCOMPARE(pattern.captureCount(), 1);
5039     const QString nmPath = match.captured(1);
5040     if (!QFile::exists(nmPath))
5041         QSKIP("Cannot check for symbol presence: No nm found.");
5042 
5043     const auto verifySymbols = [nmPath](const QByteArrayList& symbols) -> bool {
5044         QProcess nm;
5045         nm.start(nmPath, QStringList(QDir::currentPath() + "/liblinkerscripts.so"));
5046         if (!nm.waitForStarted()) {
5047             qDebug() << "Wait for process started failed.";
5048             return false;
5049         }
5050         if (!nm.waitForFinished()) {
5051             qDebug() << "Wait for process finished failed.";
5052             return false;
5053         }
5054         if (nm.exitCode() != 0) {
5055             qDebug() << "nm returned exit code " << nm.exitCode();
5056             return false;
5057         }
5058         const QByteArray nmOutput = nm.readAllStandardOutput();
5059         for (const QByteArray& symbol : symbols) {
5060             if (!nmOutput.contains(symbol)) {
5061                 qDebug() << "Expected symbol" << symbol
5062                          << "not found in" << nmOutput.constData();
5063                 return false;
5064             }
5065         }
5066         return true;
5067     };
5068 
5069     QVERIFY(verifySymbols({"TEST_SYMBOL1",
5070                            "TEST_SYMBOL2",
5071                            "TEST_SYMBOL_FROM_INCLUDE",
5072                            "TEST_SYMBOL_FROM_DIRECTORY",
5073                            "TEST_SYMBOL_FROM_RECURSIVE"}));
5074     WAIT_FOR_NEW_TIMESTAMP();
5075     REPLACE_IN_FILE(sourceDir + "/linkerscript_to_include",
5076                     "TEST_SYMBOL_FROM_INCLUDE = 1;",
5077                     "TEST_SYMBOL_FROM_INCLUDE_MODIFIED = 1;\n");
5078     QCOMPARE(runQbs(runParams), 0);
5079     QVERIFY2(m_qbsStdout.contains("linking liblinkerscripts.so"),
5080              "No linking after modifying included file");
5081     QVERIFY(verifySymbols({"TEST_SYMBOL1",
5082                            "TEST_SYMBOL2",
5083                            "TEST_SYMBOL_FROM_INCLUDE_MODIFIED",
5084                            "TEST_SYMBOL_FROM_DIRECTORY",
5085                            "TEST_SYMBOL_FROM_RECURSIVE"}));
5086     WAIT_FOR_NEW_TIMESTAMP();
5087     REPLACE_IN_FILE(sourceDir + "/scripts/linkerscript_in_directory",
5088                     "TEST_SYMBOL_FROM_DIRECTORY = 1;\n",
5089                     "TEST_SYMBOL_FROM_DIRECTORY_MODIFIED = 1;\n");
5090     QCOMPARE(runQbs(runParams), 0);
5091     QVERIFY2(m_qbsStdout.contains("linking liblinkerscripts.so"),
5092              "No linking after modifying file in directory");
5093     QVERIFY(verifySymbols({"TEST_SYMBOL1",
5094                            "TEST_SYMBOL2",
5095                            "TEST_SYMBOL_FROM_INCLUDE_MODIFIED",
5096                            "TEST_SYMBOL_FROM_DIRECTORY_MODIFIED",
5097                            "TEST_SYMBOL_FROM_RECURSIVE"}));
5098     WAIT_FOR_NEW_TIMESTAMP();
5099     REPLACE_IN_FILE(sourceDir + "/linkerscript_recursive",
5100                     "TEST_SYMBOL_FROM_RECURSIVE = 1;\n",
5101                     "TEST_SYMBOL_FROM_RECURSIVE_MODIFIED = 1;\n");
5102     QCOMPARE(runQbs(runParams), 0);
5103     QVERIFY2(m_qbsStdout.contains("linking liblinkerscripts.so"),
5104              "No linking after modifying recursive file");
5105     QVERIFY(verifySymbols({"TEST_SYMBOL1",
5106                            "TEST_SYMBOL2",
5107                            "TEST_SYMBOL_FROM_INCLUDE_MODIFIED",
5108                            "TEST_SYMBOL_FROM_DIRECTORY_MODIFIED",
5109                            "TEST_SYMBOL_FROM_RECURSIVE_MODIFIED"}));
5110 }
5111 
linkerModuleDefinition()5112 void TestBlackbox::linkerModuleDefinition()
5113 {
5114     QDir::setCurrent(testDataDir + "/linker-module-definition");
5115     QCOMPARE(runQbs({"build"}), 0);
5116     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
5117         QSKIP("Cannot run binaries in cross-compiled build");
5118     QCOMPARE(runQbs({"run"}), 0);
5119     const auto verifyOutput = [this](const QByteArrayList &symbols) {
5120         for (const QByteArray &symbol : symbols) {
5121             if (!m_qbsStdout.contains(symbol)) {
5122                 qDebug() << "Expected symbol" << symbol
5123                          << "not found in" << m_qbsStdout;
5124                 return false;
5125             }
5126         }
5127         return true;
5128     };
5129     QVERIFY(verifyOutput({"foo", "bar"}));
5130 }
5131 
listProducts()5132 void TestBlackbox::listProducts()
5133 {
5134     QDir::setCurrent(testDataDir + "/list-products");
5135     QCOMPARE(runQbs(QbsRunParameters("list-products")), 0);
5136     m_qbsStdout.replace("\r\n", "\n");
5137     QVERIFY2(m_qbsStdout.contains(
5138                  "a\n"
5139                  "b {\"architecture\":\"mips\",\"buildVariant\":\"debug\"}\n"
5140                  "b {\"architecture\":\"mips\",\"buildVariant\":\"release\"}\n"
5141                  "b {\"architecture\":\"vax\",\"buildVariant\":\"debug\"}\n"
5142                  "b {\"architecture\":\"vax\",\"buildVariant\":\"release\"}\n"
5143                  "c\n"), m_qbsStdout.constData());
5144 }
5145 
listPropertiesWithOuter()5146 void TestBlackbox::listPropertiesWithOuter()
5147 {
5148     QDir::setCurrent(testDataDir + "/list-properties-with-outer");
5149     QCOMPARE(runQbs(), 0);
5150     QVERIFY2(m_qbsStdout.contains("listProp: [\"product\",\"higher\",\"group\"]"),
5151              m_qbsStdout.constData());
5152 }
5153 
listPropertyOrder()5154 void TestBlackbox::listPropertyOrder()
5155 {
5156     QDir::setCurrent(testDataDir + "/list-property-order");
5157     const QbsRunParameters params(QStringList() << "-q");
5158     QCOMPARE(runQbs(params), 0);
5159     const QByteArray firstOutput = m_qbsStderr;
5160     QVERIFY(firstOutput.contains("listProp = [\"product\",\"higher3\",\"higher2\",\"higher1\",\"lower\"]"));
5161     for (int i = 0; i < 25; ++i) {
5162         rmDirR(relativeBuildDir());
5163         QCOMPARE(runQbs(params), 0);
5164         if (m_qbsStderr != firstOutput)
5165             break;
5166     }
5167     QCOMPARE(m_qbsStderr.constData(), firstOutput.constData());
5168 }
5169 
require()5170 void TestBlackbox::require()
5171 {
5172     QDir::setCurrent(testDataDir + "/require");
5173     QCOMPARE(runQbs(), 0);
5174 }
5175 
requireDeprecated()5176 void TestBlackbox::requireDeprecated()
5177 {
5178     QDir::setCurrent(testDataDir + "/require-deprecated");
5179     QCOMPARE(runQbs(), 0);
5180     QVERIFY2(m_qbsStderr.contains("loadExtension() function is deprecated"),
5181              m_qbsStderr.constData());
5182     QVERIFY2(m_qbsStderr.contains("loadFile() function is deprecated"),
5183              m_qbsStderr.constData());
5184 }
5185 
rescueTransformerData()5186 void TestBlackbox::rescueTransformerData()
5187 {
5188     QDir::setCurrent(testDataDir + "/rescue-transformer-data");
5189     QCOMPARE(runQbs(), 0);
5190     QVERIFY2(m_qbsStdout.contains("compiling main.cpp") && m_qbsStdout.contains("m.p: undefined"),
5191              m_qbsStdout.constData());
5192     WAIT_FOR_NEW_TIMESTAMP();
5193     touch("main.cpp");
5194     QCOMPARE(runQbs(), 0);
5195     QVERIFY2(m_qbsStdout.contains("compiling main.cpp") && !m_qbsStdout.contains("m.p: "),
5196              m_qbsStdout.constData());
5197     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("modules.m.p:true"))), 0);
5198     QCOMPARE(runQbs(), 0);
5199     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp") && m_qbsStdout.contains("m.p: true"),
5200              m_qbsStdout.constData());
5201 }
5202 
multipleChanges()5203 void TestBlackbox::multipleChanges()
5204 {
5205     QDir::setCurrent(testDataDir + "/multiple-changes");
5206     QCOMPARE(runQbs(), 0);
5207     QFile newFile("test.blubb");
5208     QVERIFY(newFile.open(QIODevice::WriteOnly));
5209     newFile.close();
5210     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList() << "project.prop:true")), 0);
5211     QCOMPARE(runQbs(), 0);
5212     QVERIFY(m_qbsStdout.contains("prop: true"));
5213 }
5214 
multipleConfigurations()5215 void TestBlackbox::multipleConfigurations()
5216 {
5217     QDir::setCurrent(testDataDir + "/multiple-configurations");
5218     QbsRunParameters params(QStringList{"config:x", "config:y", "config:z"});
5219     params.profile.clear();
5220     struct DefaultProfileSwitcher
5221     {
5222         DefaultProfileSwitcher()
5223         {
5224             const SettingsPtr s = settings();
5225             oldDefaultProfile = s->defaultProfile();
5226             s->setValue("defaultProfile", profileName());
5227             s->sync();
5228         }
5229         ~DefaultProfileSwitcher()
5230         {
5231             const SettingsPtr s = settings();
5232             s->setValue("defaultProfile", oldDefaultProfile);
5233             s->sync();
5234         }
5235         QVariant oldDefaultProfile;
5236     };
5237     DefaultProfileSwitcher dps;
5238     QCOMPARE(runQbs(params), 0);
5239     QCOMPARE(m_qbsStdout.count("compiling lib.cpp"), 3);
5240     QCOMPARE(m_qbsStdout.count("compiling file.cpp"), 3);
5241     QCOMPARE(m_qbsStdout.count("compiling main.cpp"), 3);
5242 }
5243 
multiplexedTool()5244 void TestBlackbox::multiplexedTool()
5245 {
5246     QDir::setCurrent(testDataDir + "/multiplexed-tool");
5247     QCOMPARE(runQbs(QStringLiteral("resolve")), 0);
5248     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
5249         QSKIP("Cannot run binaries in cross-compiled build");
5250     QCOMPARE(runQbs(), 0);
5251     QCOMPARE(m_qbsStdout.count("creating tool.out"), 4);
5252 }
5253 
nestedGroups()5254 void TestBlackbox::nestedGroups()
5255 {
5256     QDir::setCurrent(testDataDir + "/nested-groups");
5257     QCOMPARE(runQbs(), 0);
5258     QVERIFY(regularFileExists(relativeExecutableFilePath("nested-groups")));
5259 }
5260 
nestedProperties()5261 void TestBlackbox::nestedProperties()
5262 {
5263     QDir::setCurrent(testDataDir + "/nested-properties");
5264     QCOMPARE(runQbs(), 0);
5265     QVERIFY2(m_qbsStdout.contains("value in higherlevel"), m_qbsStdout.constData());
5266 }
5267 
newOutputArtifact()5268 void TestBlackbox::newOutputArtifact()
5269 {
5270     QDir::setCurrent(testDataDir + "/new-output-artifact");
5271     QCOMPARE(runQbs(), 0);
5272     QVERIFY(regularFileExists(relativeBuildDir() + "/install-root/output_98.out"));
5273     const QString the100thArtifact = relativeBuildDir() + "/install-root/output_99.out";
5274     QVERIFY(!regularFileExists(the100thArtifact));
5275     QbsRunParameters params("resolve", QStringList() << "products.theProduct.artifactCount:100");
5276     QCOMPARE(runQbs(params), 0);
5277     QCOMPARE(runQbs(), 0);
5278     QVERIFY(regularFileExists(the100thArtifact));
5279 }
5280 
noExportedSymbols_data()5281 void TestBlackbox::noExportedSymbols_data()
5282 {
5283     QTest::addColumn<bool>("link");
5284     QTest::addColumn<bool>("dryRun");
5285     QTest::newRow("link") << true << false;
5286     QTest::newRow("link (dry run)") << true << true;
5287     QTest::newRow("do not link") << false << false;
5288 }
5289 
noExportedSymbols()5290 void TestBlackbox::noExportedSymbols()
5291 {
5292     QDir::setCurrent(testDataDir + "/no-exported-symbols");
5293     QFETCH(bool, link);
5294     QFETCH(bool, dryRun);
5295     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{"--force-probe-execution",
5296             QString("products.the_app.link:") + (link ? "true" : "false")})), 0);
5297     const bool isMsvc = m_qbsStdout.contains("compiler is MSVC");
5298     const bool isNotMsvc = m_qbsStdout.contains("compiler is not MSVC");
5299     QVERIFY2(isMsvc || isNotMsvc, m_qbsStdout.constData());
5300     if (isNotMsvc)
5301         QSKIP("Test applies with MSVC only");
5302     QbsRunParameters buildParams;
5303     if (dryRun)
5304         buildParams.arguments << "--dry-run";
5305     buildParams.expectFailure = link && !dryRun;
5306     QCOMPARE(runQbs(buildParams) == 0, !buildParams.expectFailure);
5307     QVERIFY2(m_qbsStderr.contains("This typically happens when a DLL does not export "
5308                                   "any symbols.") == buildParams.expectFailure,
5309              m_qbsStderr.constData());
5310 }
5311 
noProfile()5312 void TestBlackbox::noProfile()
5313 {
5314     QDir::setCurrent(testDataDir + "/no-profile");
5315     QbsRunParameters params;
5316     params.profile = "none";
5317     QCOMPARE(runQbs(params), 0);
5318     QVERIFY2(m_qbsStdout.contains("profile: none"), m_qbsStdout.constData());
5319 }
5320 
noSuchProfile()5321 void TestBlackbox::noSuchProfile()
5322 {
5323     QDir::setCurrent(testDataDir + "/no-such-profile");
5324     QbsRunParameters params(QStringList("products.theProduct.p:1"));
5325     params.profile = "jibbetnich";
5326     params.expectFailure = true;
5327     QVERIFY(runQbs(params) != 0);
5328     QVERIFY2(m_qbsStderr.contains("Profile 'jibbetnich' does not exist"), m_qbsStderr.constData());
5329 }
5330 
nonBrokenFilesInBrokenProduct()5331 void TestBlackbox::nonBrokenFilesInBrokenProduct()
5332 {
5333     QDir::setCurrent(testDataDir + "/non-broken-files-in-broken-product");
5334     QbsRunParameters params(QStringList() << "-k");
5335     params.expectFailure = true;
5336     QVERIFY(runQbs(params) != 0);
5337     QVERIFY(m_qbsStdout.contains("fine.cpp"));
5338     QVERIFY(runQbs(params) != 0);
5339     QVERIFY(!m_qbsStdout.contains("fine.cpp")); // The non-broken file must not be recompiled.
5340 }
5341 
nonDefaultProduct()5342 void TestBlackbox::nonDefaultProduct()
5343 {
5344     QDir::setCurrent(testDataDir + "/non-default-product");
5345     const QString defaultAppExe = relativeExecutableFilePath("default app");
5346     const QString nonDefaultAppExe = relativeExecutableFilePath("non-default app");
5347 
5348     QCOMPARE(runQbs(), 0);
5349     QVERIFY2(QFile::exists(defaultAppExe), qPrintable(defaultAppExe));
5350     QVERIFY2(!QFile::exists(nonDefaultAppExe), qPrintable(nonDefaultAppExe));
5351 
5352     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "--all-products")), 0);
5353     QVERIFY2(QFile::exists(nonDefaultAppExe), qPrintable(nonDefaultAppExe));
5354 }
5355 
notAlwaysUpdated()5356 void TestBlackbox::notAlwaysUpdated()
5357 {
5358     QDir::setCurrent(testDataDir + "/not-always-updated");
5359     QCOMPARE(runQbs(), 0);
5360     QCOMPARE(runQbs(), 0);
5361 }
5362 
switchProfileContents(qbs::Profile & p,qbs::Settings * s,bool on)5363 static void switchProfileContents(qbs::Profile &p, qbs::Settings *s, bool on)
5364 {
5365     const QString scalarKey = "leaf.scalarProp";
5366     const QString listKey = "leaf.listProp";
5367     if (on) {
5368         p.setValue(scalarKey, "profile");
5369         p.setValue(listKey, QStringList() << "profile");
5370     } else {
5371         p.remove(scalarKey);
5372         p.remove(listKey);
5373     }
5374     s->sync();
5375 }
5376 
switchFileContents(QFile & f,bool on)5377 static void switchFileContents(QFile &f, bool on)
5378 {
5379     f.seek(0);
5380     QByteArray contents = f.readAll();
5381     f.resize(0);
5382     if (on)
5383         contents.replace("// leaf.", "leaf.");
5384     else
5385         contents.replace("leaf.", "// leaf.");
5386     f.write(contents);
5387     f.flush();
5388 }
5389 
propertyPrecedence()5390 void TestBlackbox::propertyPrecedence()
5391 {
5392     QDir::setCurrent(testDataDir + "/property-precedence");
5393     const SettingsPtr s = settings();
5394     qbs::Internal::TemporaryProfile profile("qbs_autotests_propPrecedence", s.get());
5395     profile.p.setValue("qbs.architecture", "x86"); // Profiles must not be empty...
5396     s->sync();
5397     const QStringList args = QStringList() << "-f" << "property-precedence.qbs";
5398     QbsRunParameters params(args);
5399     params.profile = profile.p.name();
5400     QbsRunParameters resolveParams = params;
5401     resolveParams.command = "resolve";
5402 
5403     // Case 1: [cmdline=0,prod=0,export=0,nonleaf=0,profile=0]
5404     QCOMPARE(runQbs(params), 0);
5405     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5406 
5407     QVERIFY2(m_qbsStdout.contains("scalar prop: leaf\n")
5408              && m_qbsStdout.contains("list prop: [\"leaf\"]\n"),
5409              m_qbsStdout.constData());
5410     params.arguments.clear();
5411 
5412     // Case 2: [cmdline=0,prod=0,export=0,nonleaf=0,profile=1]
5413     switchProfileContents(profile.p, s.get(), true);
5414     QCOMPARE(runQbs(resolveParams), 0);
5415     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5416 
5417     QCOMPARE(runQbs(params), 0);
5418     QVERIFY2(m_qbsStdout.contains("scalar prop: profile\n")
5419              && m_qbsStdout.contains("list prop: [\"profile\"]\n"),
5420              m_qbsStdout.constData());
5421 
5422     // Case 3: [cmdline=0,prod=0,export=0,nonleaf=1,profile=0]
5423     QFile nonleafFile("modules/nonleaf/nonleaf.qbs");
5424     QVERIFY2(nonleafFile.open(QIODevice::ReadWrite), qPrintable(nonleafFile.errorString()));
5425     switchProfileContents(profile.p, s.get(), false);
5426     switchFileContents(nonleafFile, true);
5427     QCOMPARE(runQbs(resolveParams), 0);
5428     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5429     QCOMPARE(runQbs(params), 0);
5430     QVERIFY2(m_qbsStdout.contains("scalar prop: nonleaf\n")
5431              && m_qbsStdout.contains("list prop: [\"nonleaf\",\"leaf\"]\n"),
5432              m_qbsStdout.constData());
5433 
5434     // Case 4: [cmdline=0,prod=0,export=0,nonleaf=1,profile=1]
5435     switchProfileContents(profile.p, s.get(), true);
5436     QCOMPARE(runQbs(resolveParams), 0);
5437     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5438     QCOMPARE(runQbs(params), 0);
5439     QVERIFY2(m_qbsStdout.contains("scalar prop: nonleaf\n")
5440              && m_qbsStdout.contains("list prop: [\"nonleaf\",\"profile\"]\n"),
5441              m_qbsStdout.constData());
5442 
5443     // Case 5: [cmdline=0,prod=0,export=1,nonleaf=0,profile=0]
5444     QFile depFile("dep.qbs");
5445     QVERIFY2(depFile.open(QIODevice::ReadWrite), qPrintable(depFile.errorString()));
5446     switchProfileContents(profile.p, s.get(), false);
5447     switchFileContents(nonleafFile, false);
5448     switchFileContents(depFile, true);
5449     QCOMPARE(runQbs(resolveParams), 0);
5450     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5451     QCOMPARE(runQbs(params), 0);
5452     QVERIFY2(m_qbsStdout.contains("scalar prop: export\n")
5453              && m_qbsStdout.contains("list prop: [\"export\",\"leaf\"]\n"),
5454              m_qbsStdout.constData());
5455 
5456     // Case 6: [cmdline=0,prod=0,export=1,nonleaf=0,profile=1]
5457     switchProfileContents(profile.p, s.get(), true);
5458     QCOMPARE(runQbs(resolveParams), 0);
5459     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5460     QCOMPARE(runQbs(params), 0);
5461     QVERIFY2(m_qbsStdout.contains("scalar prop: export\n")
5462              && m_qbsStdout.contains("list prop: [\"export\",\"profile\"]\n"),
5463              m_qbsStdout.constData());
5464 
5465 
5466     // Case 7: [cmdline=0,prod=0,export=1,nonleaf=1,profile=0]
5467     switchProfileContents(profile.p, s.get(), false);
5468     switchFileContents(nonleafFile, true);
5469     QCOMPARE(runQbs(resolveParams), 0);
5470     QVERIFY2(m_qbsStderr.contains("WARNING: Conflicting scalar values at")
5471              && m_qbsStderr.contains("nonleaf.qbs:4:22")
5472              && m_qbsStderr.contains("dep.qbs:6:26"),
5473              m_qbsStderr.constData());
5474     QCOMPARE(runQbs(params), 0);
5475     QVERIFY2(m_qbsStdout.contains("scalar prop: export\n")
5476              && m_qbsStdout.contains("list prop: [\"export\",\"nonleaf\",\"leaf\"]\n"),
5477              m_qbsStdout.constData());
5478 
5479     // Case 8: [cmdline=0,prod=0,export=1,nonleaf=1,profile=1]
5480     switchProfileContents(profile.p, s.get(), true);
5481     QCOMPARE(runQbs(resolveParams), 0);
5482     QVERIFY2(m_qbsStderr.contains("WARNING: Conflicting scalar values at")
5483              && m_qbsStderr.contains("nonleaf.qbs:4:22")
5484              && m_qbsStderr.contains("dep.qbs:6:26"),
5485              m_qbsStderr.constData());
5486     QCOMPARE(runQbs(params), 0);
5487     QVERIFY2(m_qbsStdout.contains("scalar prop: export\n")
5488              && m_qbsStdout.contains("list prop: [\"export\",\"nonleaf\",\"profile\"]\n"),
5489              m_qbsStdout.constData());
5490 
5491     // Case 9: [cmdline=0,prod=1,export=0,nonleaf=0,profile=0]
5492     QFile productFile("property-precedence.qbs");
5493     QVERIFY2(productFile.open(QIODevice::ReadWrite), qPrintable(productFile.errorString()));
5494     switchProfileContents(profile.p, s.get(), false);
5495     switchFileContents(nonleafFile, false);
5496     switchFileContents(depFile, false);
5497     switchFileContents(productFile, true);
5498     QCOMPARE(runQbs(resolveParams), 0);
5499     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5500     QCOMPARE(runQbs(params), 0);
5501     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5502              && m_qbsStdout.contains("list prop: [\"product\",\"leaf\"]\n"),
5503              m_qbsStdout.constData());
5504 
5505     // Case 10: [cmdline=0,prod=1,export=0,nonleaf=0,profile=1]
5506     switchProfileContents(profile.p, s.get(), true);
5507     QCOMPARE(runQbs(resolveParams), 0);
5508     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5509     QCOMPARE(runQbs(params), 0);
5510     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5511              && m_qbsStdout.contains("list prop: [\"product\",\"profile\"]\n"),
5512              m_qbsStdout.constData());
5513 
5514     // Case 11: [cmdline=0,prod=1,export=0,nonleaf=1,profile=0]
5515     switchProfileContents(profile.p, s.get(), false);
5516     switchFileContents(nonleafFile, true);
5517     QCOMPARE(runQbs(resolveParams), 0);
5518     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5519     QCOMPARE(runQbs(params), 0);
5520     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5521              && m_qbsStdout.contains("list prop: [\"product\",\"nonleaf\",\"leaf\"]\n"),
5522              m_qbsStdout.constData());
5523 
5524     // Case 12: [cmdline=0,prod=1,export=0,nonleaf=1,profile=1]
5525     switchProfileContents(profile.p, s.get(), true);
5526     QCOMPARE(runQbs(resolveParams), 0);
5527     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5528     QCOMPARE(runQbs(params), 0);
5529     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5530              && m_qbsStdout.contains("list prop: [\"product\",\"nonleaf\",\"profile\"]\n"),
5531              m_qbsStdout.constData());
5532 
5533     // Case 13: [cmdline=0,prod=1,export=1,nonleaf=0,profile=0]
5534     switchProfileContents(profile.p, s.get(), false);
5535     switchFileContents(nonleafFile, false);
5536     switchFileContents(depFile, true);
5537     QCOMPARE(runQbs(resolveParams), 0);
5538     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5539     QCOMPARE(runQbs(params), 0);
5540     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5541              && m_qbsStdout.contains("list prop: [\"product\",\"export\",\"leaf\"]\n"),
5542              m_qbsStdout.constData());
5543 
5544     // Case 14: [cmdline=0,prod=1,export=1,nonleaf=0,profile=1]
5545     switchProfileContents(profile.p, s.get(), true);
5546     QCOMPARE(runQbs(resolveParams), 0);
5547     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5548     QCOMPARE(runQbs(params), 0);
5549     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5550              && m_qbsStdout.contains("list prop: [\"product\",\"export\",\"profile\"]\n"),
5551              m_qbsStdout.constData());
5552 
5553     // Case 15: [cmdline=0,prod=1,export=1,nonleaf=1,profile=0]
5554     switchProfileContents(profile.p, s.get(), false);
5555     switchFileContents(nonleafFile, true);
5556     QCOMPARE(runQbs(resolveParams), 0);
5557     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5558     QCOMPARE(runQbs(params), 0);
5559     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5560              && m_qbsStdout.contains("list prop: [\"product\",\"export\",\"nonleaf\",\"leaf\"]\n"),
5561              m_qbsStdout.constData());
5562 
5563     // Case 16: [cmdline=0,prod=1,export=1,nonleaf=1,profile=1]
5564     switchProfileContents(profile.p, s.get(), true);
5565     QCOMPARE(runQbs(resolveParams), 0);
5566     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5567     QCOMPARE(runQbs(params), 0);
5568     QVERIFY2(m_qbsStdout.contains("scalar prop: product\n")
5569              && m_qbsStdout.contains("list prop: [\"product\",\"export\",\"nonleaf\",\"profile\"]\n"),
5570              m_qbsStdout.constData());
5571 
5572     // Command line properties wipe everything, including lists.
5573     // Case 17: [cmdline=1,prod=0,export=0,nonleaf=0,profile=0]
5574     switchProfileContents(profile.p, s.get(), false);
5575     switchFileContents(nonleafFile, false);
5576     switchFileContents(depFile, false);
5577     switchFileContents(productFile, false);
5578     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5579     QCOMPARE(runQbs(resolveParams), 0);
5580     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5581     QCOMPARE(runQbs(params), 0);
5582     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5583              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5584              m_qbsStdout.constData());
5585 
5586     // Case 18: [cmdline=1,prod=0,export=0,nonleaf=0,profile=1]
5587     switchProfileContents(profile.p, s.get(), true);
5588     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5589     QCOMPARE(runQbs(resolveParams), 0);
5590     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5591     QCOMPARE(runQbs(params), 0);
5592     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5593              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5594              m_qbsStdout.constData());
5595 
5596     // Case 19: [cmdline=1,prod=0,export=0,nonleaf=1,profile=0]
5597     switchProfileContents(profile.p, s.get(), false);
5598     switchFileContents(nonleafFile, true);
5599     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5600     QCOMPARE(runQbs(resolveParams), 0);
5601     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5602     QCOMPARE(runQbs(params), 0);
5603     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5604              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5605              m_qbsStdout.constData());
5606 
5607     // Case 20: [cmdline=1,prod=0,export=0,nonleaf=1,profile=1]
5608     switchProfileContents(profile.p, s.get(), true);
5609     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5610     QCOMPARE(runQbs(resolveParams), 0);
5611     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5612     QCOMPARE(runQbs(params), 0);
5613     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5614              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5615              m_qbsStdout.constData());
5616 
5617     // Case 21: [cmdline=1,prod=0,export=1,nonleaf=0,profile=0]
5618     switchProfileContents(profile.p, s.get(), false);
5619     switchFileContents(nonleafFile, false);
5620     switchFileContents(depFile, true);
5621     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5622     QCOMPARE(runQbs(resolveParams), 0);
5623     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5624     QCOMPARE(runQbs(params), 0);
5625     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5626              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5627              m_qbsStdout.constData());
5628 
5629     // Case 22: [cmdline=1,prod=0,export=1,nonleaf=0,profile=1]
5630     switchProfileContents(profile.p, s.get(), true);
5631     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5632     QCOMPARE(runQbs(resolveParams), 0);
5633     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5634     QCOMPARE(runQbs(params), 0);
5635     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5636              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5637              m_qbsStdout.constData());
5638 
5639     // Case 23: [cmdline=1,prod=0,export=1,nonleaf=1,profile=0]
5640     switchProfileContents(profile.p, s.get(), false);
5641     switchFileContents(nonleafFile, true);
5642     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5643     QCOMPARE(runQbs(resolveParams), 0);
5644     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5645     QCOMPARE(runQbs(params), 0);
5646     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5647              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5648              m_qbsStdout.constData());
5649 
5650     // Case 24: [cmdline=1,prod=0,export=1,nonleaf=1,profile=1]
5651     switchProfileContents(profile.p, s.get(), true);
5652     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5653     QCOMPARE(runQbs(resolveParams), 0);
5654     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5655     QCOMPARE(runQbs(params), 0);
5656     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5657              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5658              m_qbsStdout.constData());
5659 
5660     // Case 25: [cmdline=1,prod=1,export=0,nonleaf=0,profile=0]
5661     switchProfileContents(profile.p, s.get(), false);
5662     switchFileContents(nonleafFile, false);
5663     switchFileContents(depFile, false);
5664     switchFileContents(productFile, true);
5665     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5666     QCOMPARE(runQbs(resolveParams), 0);
5667     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5668     QCOMPARE(runQbs(params), 0);
5669     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5670              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5671              m_qbsStdout.constData());
5672 
5673     // Case 26: [cmdline=1,prod=1,export=0,nonleaf=0,profile=1]
5674     switchProfileContents(profile.p, s.get(), true);
5675     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5676     QCOMPARE(runQbs(resolveParams), 0);
5677     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5678     QCOMPARE(runQbs(params), 0);
5679     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5680              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5681              m_qbsStdout.constData());
5682 
5683     // Case 27: [cmdline=1,prod=1,export=0,nonleaf=1,profile=0]
5684     switchProfileContents(profile.p, s.get(), false);
5685     switchFileContents(nonleafFile, true);
5686     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5687     QCOMPARE(runQbs(resolveParams), 0);
5688     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5689     QCOMPARE(runQbs(params), 0);
5690     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5691              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5692              m_qbsStdout.constData());
5693 
5694     // Case 28: [cmdline=1,prod=1,export=0,nonleaf=1,profile=1]
5695     switchProfileContents(profile.p, s.get(), true);
5696     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5697     QCOMPARE(runQbs(resolveParams), 0);
5698     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5699     QCOMPARE(runQbs(params), 0);
5700     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5701              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5702              m_qbsStdout.constData());
5703 
5704     // Case 29: [cmdline=1,prod=1,export=1,nonleaf=0,profile=0]
5705     switchProfileContents(profile.p, s.get(), false);
5706     switchFileContents(nonleafFile, false);
5707     switchFileContents(depFile, true);
5708     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5709     QCOMPARE(runQbs(resolveParams), 0);
5710     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5711     QCOMPARE(runQbs(params), 0);
5712     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5713              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5714              m_qbsStdout.constData());
5715 
5716     // Case 30: [cmdline=1,prod=1,export=1,nonleaf=0,profile=1]
5717     switchProfileContents(profile.p, s.get(), true);
5718     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5719     QCOMPARE(runQbs(resolveParams), 0);
5720     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5721     QCOMPARE(runQbs(params), 0);
5722     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5723              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5724              m_qbsStdout.constData());
5725 
5726     // Case 31: [cmdline=1,prod=1,export=1,nonleaf=1,profile=0]
5727     switchProfileContents(profile.p, s.get(), false);
5728     switchFileContents(nonleafFile, true);
5729     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5730     QCOMPARE(runQbs(resolveParams), 0);
5731     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5732     QCOMPARE(runQbs(params), 0);
5733     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5734              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5735              m_qbsStdout.constData());
5736 
5737     // Case 32: [cmdline=1,prod=1,export=1,nonleaf=1,profile=1]
5738     switchProfileContents(profile.p, s.get(), true);
5739     resolveParams.arguments << "modules.leaf.scalarProp:cmdline" << "modules.leaf.listProp:cmdline";
5740     QCOMPARE(runQbs(resolveParams), 0);
5741     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5742     QCOMPARE(runQbs(params), 0);
5743     QVERIFY2(m_qbsStdout.contains("scalar prop: cmdline\n")
5744              && m_qbsStdout.contains("list prop: [\"cmdline\"]\n"),
5745              m_qbsStdout.constData());
5746 }
5747 
productDependenciesByType()5748 void TestBlackbox::productDependenciesByType()
5749 {
5750     QDir::setCurrent(testDataDir + "/product-dependencies-by-type");
5751     QCOMPARE(runQbs(), 0);
5752     QFile appListFile(relativeProductBuildDir("app list") + "/app-list.txt");
5753     QVERIFY2(appListFile.open(QIODevice::ReadOnly), qPrintable(appListFile.fileName()));
5754     const QList<QByteArray> appList = appListFile.readAll().trimmed().split('\n');
5755     QCOMPARE(appList.size(), 6);
5756     QStringList apps = QStringList()
5757             << QDir::currentPath() + '/' + relativeExecutableFilePath("app1")
5758             << QDir::currentPath() + '/' + relativeExecutableFilePath("app2")
5759             << QDir::currentPath() + '/' + relativeExecutableFilePath("app3")
5760             << QDir::currentPath() + '/' + relativeExecutableFilePath("app4")
5761             << QDir::currentPath() + '/' + relativeProductBuildDir("other-product") + "/output.txt"
5762             << QDir::currentPath() + '/' + relativeProductBuildDir("yet-another-product")
5763                + "/output.txt";
5764     for (const QByteArray &line : appList) {
5765         const QString cleanLine = QString::fromLocal8Bit(line.trimmed());
5766         QVERIFY2(apps.removeOne(cleanLine), qPrintable(cleanLine));
5767     }
5768     QVERIFY(apps.empty());
5769 }
5770 
productInExportedModule()5771 void TestBlackbox::productInExportedModule()
5772 {
5773     QDir::setCurrent(testDataDir + "/product-in-exported-module");
5774     QCOMPARE(runQbs(), 0);
5775     QEXPECT_FAIL(nullptr, "QBS-1576", Abort);
5776     QVERIFY2(!m_qbsStdout.contains("product: dep"), m_qbsStdout.constData());
5777 }
5778 
properQuoting()5779 void TestBlackbox::properQuoting()
5780 {
5781     QDir::setCurrent(testDataDir + "/proper quoting");
5782     QCOMPARE(runQbs(), 0);
5783     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
5784         QSKIP("Cannot run binaries in cross-compiled build");
5785     QbsRunParameters params(QStringLiteral("run"), QStringList() << "-q" << "-p" << "Hello World");
5786     params.expectFailure = true; // Because the exit code is non-zero.
5787     QCOMPARE(runQbs(params), 156);
5788     const char * const expectedOutput = "whitespaceless\ncontains space\ncontains\ttab\n"
5789             "backslash\\\nHello World! The magic number is 156.";
5790     QCOMPARE(unifiedLineEndings(m_qbsStdout).constData(), expectedOutput);
5791 }
5792 
propertiesInExportItems()5793 void TestBlackbox::propertiesInExportItems()
5794 {
5795     QDir::setCurrent(testDataDir + "/properties-in-export-items");
5796     QCOMPARE(runQbs(), 0);
5797     QVERIFY(regularFileExists(relativeExecutableFilePath("p1")));
5798     QVERIFY(regularFileExists(relativeExecutableFilePath("p2")));
5799     QVERIFY2(m_qbsStderr.isEmpty(), m_qbsStderr.constData());
5800 }
5801 
protobuf_data()5802 void TestBlackbox::protobuf_data()
5803 {
5804     QTest::addColumn<QString>("projectFile");
5805     QTest::addColumn<QStringList>("properties");
5806     QTest::addColumn<bool>("hasModules");
5807     QTest::addColumn<bool>("successExpected");
5808     QTest::newRow("cpp") << QString("addressbook_cpp.qbs") << QStringList() << false << true;
5809     QTest::newRow("cpp-pkgconfig")
5810         << QString("addressbook_cpp.qbs")
5811         << QStringList("project.qbsModuleProviders:qbspkgconfig")
5812         << true
5813         << true;
5814     QTest::newRow("objc") << QString("addressbook_objc.qbs") << QStringList() << false << true;
5815     QTest::newRow("nanopb") << QString("addressbook_nanopb.qbs") << QStringList() << false << true;
5816     QTest::newRow("import") << QString("import.qbs") << QStringList() << false << true;
5817     QTest::newRow("missing import dir") << QString("needs-import-dir.qbs")
5818                                         << QStringList() << false << false;
5819     QTest::newRow("provided import dir")
5820             << QString("needs-import-dir.qbs")
5821             << QStringList("products.app.theImportDir:subdir") << false << true;
5822     QTest::newRow("create proto library")
5823             << QString("create-proto-library.qbs") << QStringList() << false << true;
5824 }
5825 
protobuf()5826 void TestBlackbox::protobuf()
5827 {
5828     QDir::setCurrent(testDataDir + "/protobuf");
5829     QFETCH(QString, projectFile);
5830     QFETCH(QStringList, properties);
5831     QFETCH(bool, hasModules);
5832     QFETCH(bool, successExpected);
5833     rmDirR(relativeBuildDir());
5834     QbsRunParameters resolveParams("resolve", QStringList{"-f", projectFile} << properties);
5835     QCOMPARE(runQbs(resolveParams), 0);
5836     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
5837         QSKIP("Cannot run binaries in cross-compiled build");
5838     const bool withProtobuf = m_qbsStdout.contains("has protobuf: true");
5839     const bool withoutProtobuf = m_qbsStdout.contains("has protobuf: false");
5840     QVERIFY2(withProtobuf || withoutProtobuf, m_qbsStdout.constData());
5841     if (withoutProtobuf)
5842         QSKIP("protobuf module not present");
5843     const bool hasMods = m_qbsStdout.contains("has modules: true");
5844     const bool dontHaveMods = m_qbsStdout.contains("has modules: false");
5845     QVERIFY2(hasMods == !dontHaveMods, m_qbsStdout.constData());
5846     QCOMPARE(hasMods, hasModules);
5847     QbsRunParameters runParams("run");
5848     runParams.expectFailure = !successExpected;
5849     QCOMPARE(runQbs(runParams) == 0, successExpected);
5850 }
5851 
protobufLibraryInstall()5852 void TestBlackbox::protobufLibraryInstall()
5853 {
5854     QDir::setCurrent(testDataDir + "/protobuf-library-install");
5855     rmDirR(relativeBuildDir());
5856     QbsRunParameters resolveParams("resolve", QStringList{"qbs.installPrefix:/usr/local"});
5857     QCOMPARE(runQbs(resolveParams), 0);
5858     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
5859         QSKIP("Cannot run binaries in cross-compiled build");
5860     const bool withProtobuf = m_qbsStdout.contains("has protobuf: true");
5861     const bool withoutProtobuf = m_qbsStdout.contains("has protobuf: false");
5862     QVERIFY2(withProtobuf || withoutProtobuf, m_qbsStdout.constData());
5863     if (withoutProtobuf)
5864         QSKIP("protobuf module not present");
5865     QbsRunParameters buildParams("build");
5866     buildParams.expectFailure = false;
5867     QCOMPARE(runQbs(buildParams), 0);
5868 
5869     const QString installRootInclude = relativeBuildDir() + "/install-root/usr/local/include";
5870     QVERIFY(QFileInfo::exists(installRootInclude + "/hello.pb.h") &&
5871             QFileInfo::exists(installRootInclude + "/hello/world.pb.h"));
5872 }
5873 
5874 // Tests whether it is possible to set providers properties in a Product or from command-line
providersProperties()5875 void TestBlackbox::providersProperties()
5876 {
5877     QDir::setCurrent(testDataDir + "/providers-properties");
5878 
5879     QbsRunParameters params("build");
5880     params.arguments = QStringList("moduleProviders.provider_b.someProp: \"first,second\"");
5881     QCOMPARE(runQbs(params), 0);
5882     QVERIFY2(m_qbsStdout.contains("p.qbsmetatestmodule.listProp: [\"someValue\"]"), m_qbsStdout);
5883     QVERIFY2(m_qbsStdout.contains(
5884             "p.qbsothermodule.listProp: [\"first\",\"second\"]"), m_qbsStdout);
5885 }
5886 
pseudoMultiplexing()5887 void TestBlackbox::pseudoMultiplexing()
5888 {
5889     // This is "pseudo-multiplexing" on all platforms that initialize qbs.architectures
5890     // to an array with one element. See QBS-1243.
5891     QDir::setCurrent(testDataDir + "/pseudo-multiplexing");
5892     QCOMPARE(runQbs(), 0);
5893 }
5894 
qbsConfig()5895 void TestBlackbox::qbsConfig()
5896 {
5897     QbsRunParameters params("config");
5898 #ifdef QBS_ENABLE_UNIT_TESTS
5899     QTemporaryDir tempSystemSettingsDir;
5900     params.environment.insert("QBS_AUTOTEST_SYSTEM_SETTINGS_DIR", tempSystemSettingsDir.path());
5901     QTemporaryDir tempUserSettingsDir;
5902     QVERIFY(tempSystemSettingsDir.isValid());
5903     QVERIFY(tempUserSettingsDir.isValid());
5904     const QStringList settingsDirArgs = QStringList{"--settings-dir", tempUserSettingsDir.path()};
5905 
5906     // Set values.
5907     params.arguments = settingsDirArgs + QStringList{"--system", "key.subkey.scalar", "s"};
5908     QCOMPARE(runQbs(params), 0);
5909     params.arguments = settingsDirArgs + QStringList{"--system", "key.subkey.list", "['sl']"};
5910     QCOMPARE(runQbs(params), 0);
5911     params.arguments = settingsDirArgs + QStringList{"--user", "key.subkey.scalar", "u"};
5912     QCOMPARE(runQbs(params), 0);
5913     params.arguments = settingsDirArgs + QStringList{"key.subkey.list", "[\"u1\",\"u2\"]"};
5914     QCOMPARE(runQbs(params), 0);
5915 
5916     // Check outputs.
5917     const auto valueExtractor = [this] {
5918         const QByteArray trimmed = m_qbsStdout.trimmed();
5919         return trimmed.mid(trimmed.lastIndexOf(':') + 2);
5920     };
5921     params.arguments = settingsDirArgs + QStringList{"--list", "key.subkey.scalar"};
5922     QCOMPARE(runQbs(params), 0);
5923     QCOMPARE(valueExtractor(), QByteArray("\"u\""));
5924     params.arguments = settingsDirArgs + QStringList{"--list", "--user", "key.subkey.scalar"};
5925     QCOMPARE(runQbs(params), 0);
5926     QCOMPARE(valueExtractor(), QByteArray("\"u\""));
5927     params.arguments = settingsDirArgs + QStringList{"--list", "--system", "key.subkey.scalar"};
5928     QCOMPARE(runQbs(params), 0);
5929     QCOMPARE(valueExtractor(), QByteArray("\"s\""));
5930     params.arguments = settingsDirArgs + QStringList{"--list", "key.subkey.list"};
5931     QCOMPARE(runQbs(params), 0);
5932     QCOMPARE(valueExtractor(), QByteArray("[\"u1\", \"u2\", \"sl\"]"));
5933     params.arguments = settingsDirArgs + QStringList{"--list", "--user", "key.subkey.list"};
5934     QCOMPARE(runQbs(params), 0);
5935     QCOMPARE(valueExtractor(), QByteArray("[\"u1\", \"u2\"]"));
5936     params.arguments = settingsDirArgs + QStringList{"--list", "--system", "key.subkey.list"};
5937     QCOMPARE(runQbs(params), 0);
5938     QCOMPARE(valueExtractor(), QByteArray("[\"sl\"]"));
5939 
5940     // Remove some values and re-check.
5941     params.arguments = settingsDirArgs + QStringList{"--unset", "key.subkey.scalar"};
5942     QCOMPARE(runQbs(params), 0);
5943     params.arguments = settingsDirArgs + QStringList{"--system", "--unset", "key.subkey.list"};
5944     QCOMPARE(runQbs(params), 0);
5945     params.arguments = settingsDirArgs + QStringList{"--list", "key.subkey.scalar"};
5946     QCOMPARE(runQbs(params), 0);
5947     QCOMPARE(valueExtractor(), QByteArray("\"s\""));
5948     params.arguments = settingsDirArgs + QStringList{"--list", "key.subkey.list"};
5949     QCOMPARE(runQbs(params), 0);
5950     QCOMPARE(valueExtractor(), QByteArray("[\"u1\", \"u2\"]"));
5951 
5952     // Check preferences.ignoreSystemSearchPaths
5953     params.arguments = settingsDirArgs
5954             + QStringList{"--system", "preferences.qbsSearchPaths", "['/usr/lib/qbs']"};
5955     QCOMPARE(runQbs(params), 0);
5956     params.arguments = settingsDirArgs
5957             + QStringList{"preferences.qbsSearchPaths", "['/home/user/qbs']"};
5958     QCOMPARE(runQbs(params), 0);
5959     qbs::Settings settings(tempUserSettingsDir.path(), tempSystemSettingsDir.path());
5960     const qbs::Preferences prefs(&settings, "SomeProfile");
5961     QVERIFY2(prefs.searchPaths().contains("/usr/lib/qbs")
5962              && prefs.searchPaths().contains("/home/user/qbs"),
5963              qPrintable(prefs.searchPaths().join(',')));
5964     settings.setValue("profiles.SomeProfile.preferences.ignoreSystemSearchPaths", true);
5965     QVERIFY2(!prefs.searchPaths().contains("/usr/lib/qbs")
5966              && prefs.searchPaths().contains("/home/user/qbs"),
5967              qPrintable(prefs.searchPaths().join(',')));
5968 
5969 #else
5970     qDebug() << "ability to redirect the system settings dir not compiled in, skipping"
5971                 "most qbs-config tests";
5972 #endif // QBS_ENABLE_UNIT_TESTS
5973 
5974     bool canWriteToSystemSettings;
5975     QString testSettingsFilePath;
5976     {
5977         QSettings testSettings(
5978                 qbs::Settings::defaultSystemSettingsBaseDir() + "/dummyOrg" + "/dummyApp.conf",
5979                 QSettings::IniFormat);
5980         testSettings.setValue("dummyKey", "dummyValue");
5981         testSettings.sync();
5982         canWriteToSystemSettings = testSettings.status() == QSettings::NoError;
5983         testSettingsFilePath = testSettings.fileName();
5984     }
5985     if (canWriteToSystemSettings)
5986         QVERIFY(QFile::remove(testSettingsFilePath));
5987 
5988     // Check that trying to write to actual system settings causes access failure.
5989     params.expectFailure = !canWriteToSystemSettings;
5990     params.environment.clear();
5991     params.arguments = QStringList{"--system", "key.subkey.scalar", "s"};
5992     QCOMPARE(runQbs(params) == 0, canWriteToSystemSettings);
5993     if (!canWriteToSystemSettings) {
5994         QVERIFY2(m_qbsStderr.contains("You do not have permission to write to that location."),
5995                  m_qbsStderr.constData());
5996     }
5997 }
5998 
qbsConfigAddProfile()5999 void TestBlackbox::qbsConfigAddProfile()
6000 {
6001     QbsRunParameters params("config");
6002     QTemporaryDir settingsDir1;
6003     QTemporaryDir settingsDir2;
6004     QVERIFY(settingsDir1.isValid());
6005     QVERIFY(settingsDir2.isValid());
6006     const QStringList settingsDir1Args = QStringList{"--settings-dir", settingsDir1.path()};
6007     const QStringList settingsDir2Args = QStringList{"--settings-dir", settingsDir2.path()};
6008 
6009     QFETCH(QStringList, args);
6010     QFETCH(QString, errorMsg);
6011 
6012     // Step 1: Run --add-profile.
6013     params.arguments = settingsDir1Args;
6014     params.arguments << "--add-profile";
6015     params.arguments << args;
6016     params.expectFailure = !errorMsg.isEmpty();
6017     QCOMPARE(runQbs(params) == 0, !params.expectFailure);
6018     if (params.expectFailure) {
6019         QVERIFY(QString::fromLocal8Bit(m_qbsStderr).contains(errorMsg));
6020         return;
6021     }
6022     params.expectFailure = false;
6023     params.arguments = settingsDir1Args;
6024     params.arguments << "--list";
6025     QCOMPARE(runQbs(params), 0);
6026     const QByteArray output1 = m_qbsStdout;
6027 
6028     // Step 2: Set properties manually.
6029     for (int i = 1; i < args.size(); i += 2) {
6030         params.arguments = settingsDir2Args;
6031         params.arguments << ("profiles." + args.first() + '.' + args.at(i)) << args.at(i + 1);
6032         QCOMPARE(runQbs(params), 0);
6033     }
6034     params.arguments = settingsDir2Args;
6035     params.arguments << "--list";
6036     QCOMPARE(runQbs(params), 0);
6037     const QByteArray output2 = m_qbsStdout;
6038 
6039     // Step3: Compare results.
6040     QCOMPARE(output1, output2);
6041 }
6042 
qbsConfigAddProfile_data()6043 void TestBlackbox::qbsConfigAddProfile_data()
6044 {
6045     QTest::addColumn<QStringList>("args");
6046     QTest::addColumn<QString>("errorMsg");
6047     QTest::newRow("no arguments") << QStringList() << QString("Profile name missing");
6048     QTest::newRow("empty name") << QStringList{"", "p", "v"}
6049                                 << QString("Profile name must not be empty");
6050     QTest::newRow("no properties") << QStringList("p")
6051                                    << QString("Profile properties must be provided");
6052     QTest::newRow("one property") << QStringList{"p", "p", "v"} << QString();
6053     QTest::newRow("two properties") << QStringList{"p", "p1", "v1", "p2", "v2"} << QString();
6054     QTest::newRow("missing value") << QStringList{"p", "p"}
6055                                    << QString("Profile properties must be key/value pairs");
6056     QTest::newRow("missing values") << QStringList{"p", "p1", "v1", "p2"}
6057                                     << QString("Profile properties must be key/value pairs");
6058 }
6059 
6060 // Tests whether it is possible to set qbsModuleProviders in Product and Project items
6061 // and that the order of providers results in correct priority
qbsModuleProviders()6062 void TestBlackbox::qbsModuleProviders()
6063 {
6064     QFETCH(QStringList, arguments);
6065     QFETCH(QString, firstProp);
6066     QFETCH(QString, secondProp);
6067 
6068     QDir::setCurrent(testDataDir + "/qbs-module-providers");
6069 
6070     QbsRunParameters params("resolve");
6071     params.arguments = arguments;
6072     QCOMPARE(runQbs(params), 0);
6073     QVERIFY2(m_qbsStdout.contains(("p1.qbsmetatestmodule.prop: " + firstProp).toUtf8()),
6074              m_qbsStdout);
6075     QVERIFY2(m_qbsStdout.contains(("p1.qbsothermodule.prop: " + secondProp).toUtf8()),
6076              m_qbsStdout);
6077     QVERIFY2(m_qbsStdout.contains(("p2.qbsmetatestmodule.prop: " + firstProp).toUtf8()),
6078              m_qbsStdout);
6079     QVERIFY2(m_qbsStdout.contains(("p2.qbsothermodule.prop: " + secondProp).toUtf8()),
6080              m_qbsStdout);
6081 }
6082 
qbsModuleProviders_data()6083 void TestBlackbox::qbsModuleProviders_data()
6084 {
6085     QTest::addColumn<QStringList>("arguments");
6086     QTest::addColumn<QString>("firstProp");
6087     QTest::addColumn<QString>("secondProp");
6088 
6089     QTest::newRow("default") << QStringList() << "from_provider_a" << "undefined";
6090     QTest::newRow("override")
6091             << QStringList("projects.project.qbsModuleProviders:provider_b")
6092             << "from_provider_b"
6093             << "from_provider_b";
6094     QTest::newRow("override list a")
6095             << QStringList("projects.project.qbsModuleProviders:provider_a,provider_b")
6096             << "from_provider_a"
6097             << "from_provider_b";
6098     QTest::newRow("override list b")
6099             << QStringList("projects.project.qbsModuleProviders:provider_b,provider_a")
6100             << "from_provider_b"
6101             << "from_provider_b";
6102 }
6103 
6104 // Tests possible use-cases how to override providers from command-line
qbsModuleProvidersCliOverride()6105 void TestBlackbox::qbsModuleProvidersCliOverride()
6106 {
6107     QFETCH(QStringList, arguments);
6108     QFETCH(QString, propertyValue);
6109 
6110     QDir::setCurrent(testDataDir + "/qbs-module-providers-cli-override");
6111 
6112     QbsRunParameters params("resolve");
6113     params.arguments = arguments;
6114     QCOMPARE(runQbs(params), 0);
6115     QVERIFY2(m_qbsStdout.contains(("qbsmetatestmodule.prop: " + propertyValue).toUtf8()),
6116              m_qbsStdout);
6117 }
6118 
qbsModuleProvidersCliOverride_data()6119 void TestBlackbox::qbsModuleProvidersCliOverride_data()
6120 {
6121     QTest::addColumn<QStringList>("arguments");
6122     QTest::addColumn<QString>("propertyValue");
6123 
6124     QTest::newRow("default") << QStringList() << "undefined";
6125     QTest::newRow("project-wide")
6126             << QStringList("project.qbsModuleProviders:provider_a")
6127             << "from_provider_a";
6128     QTest::newRow("concrete project")
6129             << QStringList("projects.innerProject.qbsModuleProviders:provider_a")
6130             << "from_provider_a";
6131     QTest::newRow("concrete product")
6132             << QStringList("products.product.qbsModuleProviders:provider_a")
6133             << "from_provider_a";
6134     QTest::newRow("concrete project override project-wide")
6135             << QStringList({
6136                     "project.qbsModuleProviders:provider_a",
6137                     "projects.innerProject.qbsModuleProviders:provider_b"})
6138             << "from_provider_b";
6139     QTest::newRow("concrete product override project-wide")
6140             << QStringList({
6141                     "project.qbsModuleProviders:provider_a",
6142                     "products.product.qbsModuleProviders:provider_b"})
6143             << "from_provider_b";
6144 }
6145 
6146 // Tests whether scoped providers can be used as named, i.e. new provider machinery
6147 // is compatible with the old one
qbsModuleProvidersCompatibility()6148 void TestBlackbox::qbsModuleProvidersCompatibility()
6149 {
6150     QFETCH(QStringList, arguments);
6151     QFETCH(QString, propertyValue);
6152 
6153     QDir::setCurrent(testDataDir + "/qbs-module-providers-compatibility");
6154 
6155     QbsRunParameters params("resolve");
6156     params.arguments = arguments;
6157     QCOMPARE(runQbs(params), 0);
6158     QVERIFY2(m_qbsStdout.contains(("qbsmetatestmodule.prop: " + propertyValue).toUtf8()),
6159              m_qbsStdout);
6160 }
6161 
qbsModuleProvidersCompatibility_data()6162 void TestBlackbox::qbsModuleProvidersCompatibility_data()
6163 {
6164     QTest::addColumn<QStringList>("arguments");
6165     QTest::addColumn<QString>("propertyValue");
6166 
6167     QTest::newRow("default") << QStringList() << "from_scoped_provider";
6168     QTest::newRow("scoped by name") << QStringList("project.qbsModuleProviders:qbsmetatestmodule") << "from_scoped_provider";
6169     QTest::newRow("named") << QStringList("project.qbsModuleProviders:named_provider") << "from_named_provider";
6170 }
6171 
qbspkgconfigModuleProvider()6172 void TestBlackbox::qbspkgconfigModuleProvider()
6173 {
6174     QDir::setCurrent(testDataDir + "/qbspkgconfig-module-provider/libs");
6175 
6176     const auto commonParams = QbsRunParameters(QStringLiteral("install"), {
6177             QStringLiteral("qbs.installPrefix:/usr/local"),
6178             QStringLiteral("--install-root"),
6179             QStringLiteral("install-root")
6180     });
6181     auto dynamicParams = commonParams;
6182     dynamicParams.arguments << "config:library" << "projects.libs.isBundle:false";
6183     QCOMPARE(runQbs(dynamicParams), 0);
6184 
6185     QDir::setCurrent(testDataDir + "/qbspkgconfig-module-provider");
6186 
6187     QbsRunParameters params;
6188     params.arguments
6189             << "moduleProviders.qbspkgconfig.libDirs:libdir"
6190             << "moduleProviders.qbspkgconfig.sysroot:" + QDir::currentPath() + "/libs/install-root";
6191     QCOMPARE(runQbs(params), 0);
6192 }
6193 
getNextSessionPacket(QProcess & session,QByteArray & data)6194 static QJsonObject getNextSessionPacket(QProcess &session, QByteArray &data)
6195 {
6196     int totalSize = -1;
6197     QElapsedTimer timer;
6198     timer.start();
6199     QByteArray msg;
6200     while (totalSize == -1 || msg.size() < totalSize) {
6201         if (data.isEmpty())
6202             session.waitForReadyRead(1000);
6203         if (timer.elapsed() >= testTimeoutInMsecs())
6204             return QJsonObject();
6205         data += session.readAllStandardOutput();
6206         if (totalSize == -1) {
6207             static const QByteArray magicString = "qbsmsg:";
6208             const int magicStringOffset = data.indexOf(magicString);
6209             if (magicStringOffset == -1)
6210                 continue;
6211             const int sizeOffset = magicStringOffset + magicString.length();
6212             const int newlineOffset = data.indexOf('\n', sizeOffset);
6213             if (newlineOffset == -1)
6214                 continue;
6215             const QByteArray sizeString = data.mid(sizeOffset, newlineOffset - sizeOffset);
6216             bool isNumber;
6217             const int size = sizeString.toInt(&isNumber);
6218             if (!isNumber || size <= 0)
6219                 return QJsonObject();
6220             data = data.mid(newlineOffset + 1);
6221             totalSize = size;
6222         }
6223         const int bytesToTake = std::min(totalSize - msg.size(), data.size());
6224         msg += data.left(bytesToTake);
6225         data = data.mid(bytesToTake);
6226     }
6227     return QJsonDocument::fromJson(QByteArray::fromBase64(msg)).object();
6228 }
6229 
qbsSession()6230 void TestBlackbox::qbsSession()
6231 {
6232     QDir::setCurrent(testDataDir + "/qbs-session");
6233     QProcess sessionProc;
6234     sessionProc.start(qbsExecutableFilePath, QStringList("session"));
6235 
6236     // Uncomment for debugging.
6237     /*
6238     connect(&sessionProc, &QProcess::readyReadStandardError, [&sessionProc] {
6239         qDebug() << "stderr:" << sessionProc.readAllStandardError();
6240     });
6241     */
6242 
6243     QVERIFY(sessionProc.waitForStarted());
6244 
6245     const auto sendPacket = [&sessionProc](const QJsonObject &message) {
6246         const QByteArray data = QJsonDocument(message).toJson().toBase64();
6247         sessionProc.write("qbsmsg:");
6248         sessionProc.write(QByteArray::number(data.length()));
6249         sessionProc.write("\n");
6250         sessionProc.write(data);
6251     };
6252 
6253     static const auto envToJson = [](const QProcessEnvironment &env) {
6254         QJsonObject envObj;
6255         const QStringList keys = env.keys();
6256         for (const QString &key : keys)
6257             envObj.insert(key, env.value(key));
6258         return envObj;
6259     };
6260 
6261     static const auto envFromJson = [](const QJsonValue &v) {
6262         const QJsonObject obj = v.toObject();
6263         QProcessEnvironment env;
6264         for (auto it = obj.begin(); it != obj.end(); ++it)
6265             env.insert(it.key(), it.value().toString());
6266         return env;
6267     };
6268 
6269     QByteArray incomingData;
6270 
6271     // Wait for and verify hello packet.
6272     QJsonObject receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6273     QCOMPARE(receivedMessage.value("type"), "hello");
6274     QCOMPARE(receivedMessage.value("api-level").toInt(), 2);
6275     QCOMPARE(receivedMessage.value("api-compat-level").toInt(), 2);
6276 
6277     // Resolve & verify structure
6278     QJsonObject resolveMessage;
6279     resolveMessage.insert("type", "resolve-project");
6280     resolveMessage.insert("top-level-profile", profileName());
6281     resolveMessage.insert("configuration-name", "my-config");
6282     resolveMessage.insert("project-file-path", QDir::currentPath() + "/qbs-session.qbs");
6283     resolveMessage.insert("build-root", QDir::currentPath());
6284     resolveMessage.insert("settings-directory", settings()->baseDirectory());
6285     QJsonObject overriddenValues;
6286     overriddenValues.insert("products.theLib.cpp.cxxLanguageVersion", "c++17");
6287     resolveMessage.insert("overridden-properties", overriddenValues);
6288     resolveMessage.insert("environment", envToJson(QbsRunParameters::defaultEnvironment()));
6289     resolveMessage.insert("data-mode", "only-if-changed");
6290     resolveMessage.insert("log-time", true);
6291     resolveMessage.insert("module-properties",
6292                           QJsonArray::fromStringList({"cpp.cxxLanguageVersion"}));
6293     sendPacket(resolveMessage);
6294     bool receivedLogData = false;
6295     bool receivedStartedSignal = false;
6296     bool receivedProgressData = false;
6297     bool receivedReply = false;
6298     while (!receivedReply) {
6299         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6300         QVERIFY(!receivedMessage.isEmpty());
6301         const QString msgType = receivedMessage.value("type").toString();
6302         if (msgType == "project-resolved") {
6303             receivedReply = true;
6304             const QJsonObject error = receivedMessage.value("error").toObject();
6305             if (!error.isEmpty())
6306                 qDebug() << error;
6307             QVERIFY(error.isEmpty());
6308             const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6309             QCOMPARE(projectData.value("name").toString(), "qbs-session");
6310             const QJsonArray products = projectData.value("products").toArray();
6311             QCOMPARE(products.size(), 2);
6312             for (const QJsonValue &v : products) {
6313                 const QJsonObject product = v.toObject();
6314                 const QString productName = product.value("name").toString();
6315                 QVERIFY(!productName.isEmpty());
6316                 QVERIFY2(product.value("is-enabled").toBool(), qPrintable(productName));
6317                 bool theLib = false;
6318                 bool theApp = false;
6319                 if (productName == "theLib")
6320                     theLib = true;
6321                 else if (productName == "theApp")
6322                     theApp = true;
6323                 QVERIFY2(theLib || theApp, qPrintable(productName));
6324                 const QJsonArray groups = product.value("groups").toArray();
6325                 if (theLib)
6326                     QVERIFY(groups.size() >= 3);
6327                 else
6328                     QVERIFY(!groups.isEmpty());
6329                 for (const QJsonValue &v : groups) {
6330                     const QJsonObject group = v.toObject();
6331                     const QJsonArray sourceArtifacts
6332                             = group.value("source-artifacts").toArray();
6333                     const auto findArtifact = [&sourceArtifacts](const QString &fileName) {
6334                         for (const QJsonValue &v : sourceArtifacts) {
6335                             const QJsonObject artifact = v.toObject();
6336                             if (QFileInfo(artifact.value("file-path").toString()).fileName()
6337                                     == fileName) {
6338                                 return artifact;
6339                             }
6340                         }
6341                         return QJsonObject();
6342                     };
6343                     const QString groupName = group.value("name").toString();
6344                     const auto getCxxLanguageVersion = [&group, &product] {
6345                         QJsonObject moduleProperties = group.value("module-properties").toObject();
6346                         if (moduleProperties.isEmpty())
6347                             moduleProperties = product.value("module-properties").toObject();
6348                         return moduleProperties.toVariantMap().value("cpp.cxxLanguageVersion")
6349                                 .toStringList();
6350                     };
6351                     if (groupName == "sources") {
6352                         const QJsonObject artifact = findArtifact("lib.cpp");
6353                         QVERIFY2(!artifact.isEmpty(), "lib.cpp");
6354                         QCOMPARE(getCxxLanguageVersion(), {"c++17"});
6355                     } else if (groupName == "headers") {
6356                         const QJsonObject artifact = findArtifact("lib.h");
6357                         QVERIFY2(!artifact.isEmpty(), "lib.h");
6358                     } else if (groupName == "theApp") {
6359                         const QJsonObject artifact = findArtifact("main.cpp");
6360                         QVERIFY2(!artifact.isEmpty(), "main.cpp");
6361                         QCOMPARE(getCxxLanguageVersion(), {"c++14"});
6362                     }
6363                 }
6364             }
6365             break;
6366         } else if (msgType == "log-data") {
6367             if (receivedMessage.value("message").toString().contains("activity"))
6368                 receivedLogData = true;
6369         } else if (msgType == "task-started") {
6370             receivedStartedSignal = true;
6371         } else if (msgType == "task-progress") {
6372             receivedProgressData = true;
6373         } else if (msgType != "new-max-progress") {
6374             QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType)));
6375         }
6376     }
6377     QVERIFY(receivedReply);
6378     QVERIFY(receivedLogData);
6379     QVERIFY(receivedStartedSignal);
6380     QVERIFY(receivedProgressData);
6381 
6382     // First build: No install, log time, default command description.
6383     QJsonObject buildRequest;
6384     buildRequest.insert("type", "build-project");
6385     buildRequest.insert("log-time", true);
6386     buildRequest.insert("install", false);
6387     buildRequest.insert("data-mode", "only-if-changed");
6388     sendPacket(buildRequest);
6389     receivedReply = false;
6390     receivedLogData = false;
6391     receivedStartedSignal = false;
6392     receivedProgressData = false;
6393     bool receivedCommandDescription = false;
6394     bool receivedProcessResult = false;
6395     while (!receivedReply) {
6396         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6397         QVERIFY(!receivedMessage.isEmpty());
6398         const QString msgType = receivedMessage.value("type").toString();
6399         if (msgType == "project-built") {
6400             receivedReply = true;
6401             const QJsonObject error = receivedMessage.value("error").toObject();
6402             if (!error.isEmpty())
6403                 qDebug() << error;
6404             QVERIFY(error.isEmpty());
6405             const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6406             QCOMPARE(projectData.value("name").toString(), "qbs-session");
6407         } else if (msgType == "log-data") {
6408             if (receivedMessage.value("message").toString().contains("activity"))
6409                 receivedLogData = true;
6410         } else if (msgType == "task-started") {
6411             receivedStartedSignal = true;
6412         } else if (msgType == "task-progress") {
6413             receivedProgressData = true;
6414         } else if (msgType == "command-description") {
6415             if (receivedMessage.value("message").toString().contains("compiling main.cpp"))
6416                 receivedCommandDescription = true;
6417         } else if (msgType == "process-result") {
6418             QCOMPARE(receivedMessage.value("exit-code").toInt(), 0);
6419             receivedProcessResult = true;
6420         } else if (msgType != "new-max-progress") {
6421             QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType)));
6422         }
6423     }
6424     QVERIFY(receivedReply);
6425     QVERIFY(receivedLogData);
6426     QVERIFY(receivedStartedSignal);
6427     QVERIFY(receivedProgressData);
6428     QVERIFY(receivedCommandDescription);
6429     QVERIFY(receivedProcessResult);
6430     const QString &exeFilePath = QDir::currentPath() + '/'
6431             + relativeExecutableFilePath("theApp", "my-config");
6432     QVERIFY2(regularFileExists(exeFilePath), qPrintable(exeFilePath));
6433     const QString defaultInstallRoot = QDir::currentPath() + '/'
6434             + relativeBuildDir("my-config") + "/install-root";
6435     QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot));
6436 
6437     // Clean.
6438     QJsonObject cleanRequest;
6439     cleanRequest.insert("type", "clean-project");
6440     cleanRequest.insert("settings-dir", settings()->baseDirectory());
6441     cleanRequest.insert("log-time", true);
6442     sendPacket(cleanRequest);
6443     receivedReply = false;
6444     receivedLogData = false;
6445     receivedStartedSignal = false;
6446     receivedProgressData = false;
6447     while (!receivedReply) {
6448         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6449         QVERIFY(!receivedMessage.isEmpty());
6450         const QString msgType = receivedMessage.value("type").toString();
6451         if (msgType == "project-cleaned") {
6452             receivedReply = true;
6453             const QJsonObject error = receivedMessage.value("error").toObject();
6454             if (!error.isEmpty())
6455                 qDebug() << error;
6456             QVERIFY(error.isEmpty());
6457         } else if (msgType == "log-data") {
6458             if (receivedMessage.value("message").toString().contains("activity"))
6459                 receivedLogData = true;
6460         } else if (msgType == "task-started") {
6461             receivedStartedSignal = true;
6462         } else if (msgType == "task-progress") {
6463             receivedProgressData = true;
6464         } else if (msgType != "new-max-progress") {
6465             QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType)));
6466         }
6467     }
6468     QVERIFY(receivedReply);
6469     QVERIFY(receivedLogData);
6470     QVERIFY(receivedStartedSignal);
6471     QVERIFY(receivedProgressData);
6472     QVERIFY2(!regularFileExists(exeFilePath), qPrintable(exeFilePath));
6473 
6474     // Second build: Do not log the time, show command lines.
6475     buildRequest.insert("log-time", false);
6476     buildRequest.insert("command-echo-mode", "command-line");
6477     sendPacket(buildRequest);
6478     receivedReply = false;
6479     receivedLogData = false;
6480     receivedStartedSignal = false;
6481     receivedProgressData = false;
6482     receivedCommandDescription = false;
6483     receivedProcessResult = false;
6484     while (!receivedReply) {
6485         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6486         QVERIFY(!receivedMessage.isEmpty());
6487         const QString msgType = receivedMessage.value("type").toString();
6488         if (msgType == "project-built") {
6489             receivedReply = true;
6490             const QJsonObject error = receivedMessage.value("error").toObject();
6491             if (!error.isEmpty())
6492                 qDebug() << error;
6493             QVERIFY(error.isEmpty());
6494             const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6495             QVERIFY(projectData.isEmpty());
6496         } else if (msgType == "log-data") {
6497             if (receivedMessage.value("message").toString().contains("activity"))
6498                 receivedLogData = true;
6499         } else if (msgType == "task-started") {
6500             receivedStartedSignal = true;
6501         } else if (msgType == "task-progress") {
6502             receivedProgressData = true;
6503         } else if (msgType == "command-description") {
6504             if (QDir::fromNativeSeparators(receivedMessage.value("message").toString())
6505                     .contains("/main.cpp")) {
6506                 receivedCommandDescription = true;
6507             }
6508         } else if (msgType == "process-result") {
6509             QCOMPARE(receivedMessage.value("exit-code").toInt(), 0);
6510             receivedProcessResult = true;
6511         } else if (msgType != "new-max-progress") {
6512             QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType)));
6513         }
6514     }
6515     QVERIFY(receivedReply);
6516     QVERIFY(!receivedLogData);
6517     QVERIFY(receivedStartedSignal);
6518     QVERIFY(receivedProgressData);
6519     QVERIFY(receivedCommandDescription);
6520     QVERIFY(receivedProcessResult);
6521     QVERIFY2(regularFileExists(exeFilePath), qPrintable(exeFilePath));
6522     QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot));
6523 
6524     // Install.
6525     QJsonObject installRequest;
6526     installRequest.insert("type", "install-project");
6527     installRequest.insert("log-time", true);
6528     const QString customInstallRoot = QDir::currentPath() + "/my-install-root";
6529     QVERIFY2(!QFile::exists(customInstallRoot), qPrintable(customInstallRoot));
6530     installRequest.insert("install-root", customInstallRoot);
6531     sendPacket(installRequest);
6532     receivedReply = false;
6533     receivedLogData = false;
6534     receivedStartedSignal = false;
6535     receivedProgressData = false;
6536     while (!receivedReply) {
6537         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6538         QVERIFY(!receivedMessage.isEmpty());
6539         const QString msgType = receivedMessage.value("type").toString();
6540         if (msgType == "install-done") {
6541             receivedReply = true;
6542             const QJsonObject error = receivedMessage.value("error").toObject();
6543             if (!error.isEmpty())
6544                 qDebug() << error;
6545             QVERIFY(error.isEmpty());
6546         } else if (msgType == "log-data") {
6547             if (receivedMessage.value("message").toString().contains("activity"))
6548                 receivedLogData = true;
6549         } else if (msgType == "task-started") {
6550             receivedStartedSignal = true;
6551         } else if (msgType == "task-progress") {
6552             receivedProgressData = true;
6553         } else if (msgType != "new-max-progress") {
6554             QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType)));
6555         }
6556     }
6557     QVERIFY(receivedReply);
6558     QVERIFY(receivedLogData);
6559     QVERIFY(receivedStartedSignal);
6560     QVERIFY(receivedProgressData);
6561     QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot));
6562     QVERIFY2(directoryExists(customInstallRoot), qPrintable(customInstallRoot));
6563 
6564     // Retrieve modified environment.
6565     QJsonObject getRunEnvRequest;
6566     getRunEnvRequest.insert("type", "get-run-environment");
6567     getRunEnvRequest.insert("product", "theApp");
6568     const QProcessEnvironment inEnv = QProcessEnvironment::systemEnvironment();
6569     QVERIFY(!inEnv.contains("MY_MODULE"));
6570     getRunEnvRequest.insert("base-environment", envToJson(inEnv));
6571     sendPacket(getRunEnvRequest);
6572     receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6573     QCOMPARE(receivedMessage.value("type").toString(), QString("run-environment"));
6574     QJsonObject error = receivedMessage.value("error").toObject();
6575     if (!error.isEmpty())
6576         qDebug() << error;
6577     QVERIFY(error.isEmpty());
6578     const QProcessEnvironment outEnv = envFromJson(receivedMessage.value("full-environment"));
6579     QVERIFY(outEnv.keys().size() > inEnv.keys().size());
6580     QCOMPARE(outEnv.value("MY_MODULE"), QString("1"));
6581 
6582     // Add two files to library and re-build.
6583     QJsonObject addFilesRequest;
6584     addFilesRequest.insert("type", "add-files");
6585     addFilesRequest.insert("product", "theLib");
6586     addFilesRequest.insert("group", "sources");
6587     addFilesRequest.insert("files",
6588                            QJsonArray::fromStringList({QDir::currentPath() + "/file1.cpp",
6589                                                        QDir::currentPath() + "/file2.cpp"}));
6590     sendPacket(addFilesRequest);
6591     receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6592     QCOMPARE(receivedMessage.value("type").toString(), QString("files-added"));
6593     error = receivedMessage.value("error").toObject();
6594     if (!error.isEmpty()) {
6595         for (const auto item: error[QStringLiteral("items")].toArray()) {
6596             const auto description = QStringLiteral("Project file updates are not enabled");
6597             if (item.toObject()[QStringLiteral("description")].toString().contains(description))
6598                 QSKIP("File updates are disabled");
6599         }
6600         qDebug() << error;
6601     }
6602     QVERIFY(error.isEmpty());
6603 
6604     receivedReply = false;
6605     sendPacket(resolveMessage);
6606     while (!receivedReply) {
6607         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6608         QVERIFY(!receivedMessage.isEmpty());
6609         const QString msgType = receivedMessage.value("type").toString();
6610         if (msgType == "project-resolved") {
6611             receivedReply = true;
6612             const QJsonObject error = receivedMessage.value("error").toObject();
6613             if (!error.isEmpty())
6614                 qDebug() << error;
6615             QVERIFY(error.isEmpty());
6616             const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6617             QJsonArray products = projectData.value("products").toArray();
6618             bool file1 = false;
6619             bool file2 = false;
6620             for (const QJsonValue v : products) {
6621                 const QJsonObject product = v.toObject();
6622                 const QString productName = product.value("full-display-name").toString();
6623                 const QJsonArray groups = product.value("groups").toArray();
6624                 for (const QJsonValue &v : groups) {
6625                     const QJsonObject group = v.toObject();
6626                     const QString groupName = group.value("name").toString();
6627                     const QJsonArray sourceArtifacts = group.value("source-artifacts").toArray();
6628                     for (const QJsonValue &v : sourceArtifacts) {
6629                         const QString filePath = v.toObject().value("file-path").toString();
6630                         if (filePath.endsWith("file1.cpp")) {
6631                             QCOMPARE(productName, QString("theLib"));
6632                             QCOMPARE(groupName, QString("sources"));
6633                             file1 = true;
6634                         } else if (filePath.endsWith("file2.cpp")) {
6635                             QCOMPARE(productName, QString("theLib"));
6636                             QCOMPARE(groupName, QString("sources"));
6637                             file2 = true;
6638                         }
6639                     }
6640                 }
6641             }
6642             QVERIFY(file1);
6643             QVERIFY(file2);
6644         }
6645     }
6646     QVERIFY(receivedReply);
6647 
6648     receivedReply = false;
6649     receivedProcessResult = false;
6650     bool compiledFile1 = false;
6651     bool compiledFile2 = false;
6652     bool compiledMain = false;
6653     bool compiledLib = false;
6654     buildRequest.remove("command-echo-mode");
6655     sendPacket(buildRequest);
6656     while (!receivedReply) {
6657         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6658         QVERIFY(!receivedMessage.isEmpty());
6659         const QString msgType = receivedMessage.value("type").toString();
6660         if (msgType == "project-built") {
6661             receivedReply = true;
6662             const QJsonObject error = receivedMessage.value("error").toObject();
6663             if (!error.isEmpty())
6664                 qDebug() << error;
6665             QVERIFY(error.isEmpty());
6666         } else if (msgType == "command-description") {
6667             const QString msg = receivedMessage.value("message").toString();
6668             if (msg.contains("compiling file1.cpp"))
6669                 compiledFile1 = true;
6670             else if (msg.contains("compiling file2.cpp"))
6671                 compiledFile2 = true;
6672             else if (msg.contains("compiling main.cpp"))
6673                 compiledMain = true;
6674             else if (msg.contains("compiling lib.cpp"))
6675                 compiledLib = true;
6676         } else if (msgType == "process-result") {
6677             QCOMPARE(receivedMessage.value("exit-code").toInt(), 0);
6678             receivedProcessResult = true;
6679         }
6680     }
6681     QVERIFY(receivedReply);
6682     QVERIFY(!receivedProcessResult);
6683     QVERIFY(compiledFile1);
6684     QVERIFY(compiledFile2);
6685     QVERIFY(!compiledLib);
6686     QVERIFY(!compiledMain);
6687 
6688     // Remove one of the newly added files again and re-build.
6689     WAIT_FOR_NEW_TIMESTAMP();
6690     touch("file1.cpp");
6691     touch("file2.cpp");
6692     touch("main.cpp");
6693     QJsonObject removeFilesRequest;
6694     removeFilesRequest.insert("type", "remove-files");
6695     removeFilesRequest.insert("product", "theLib");
6696     removeFilesRequest.insert("group", "sources");
6697     removeFilesRequest.insert("files",
6698                               QJsonArray::fromStringList({QDir::currentPath() + "/file1.cpp"}));
6699     sendPacket(removeFilesRequest);
6700     receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6701     QCOMPARE(receivedMessage.value("type").toString(), QString("files-removed"));
6702     error = receivedMessage.value("error").toObject();
6703     if (!error.isEmpty())
6704         qDebug() << error;
6705     QVERIFY(error.isEmpty());
6706     receivedReply = false;
6707     sendPacket(resolveMessage);
6708     while (!receivedReply) {
6709         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6710         QVERIFY(!receivedMessage.isEmpty());
6711         const QString msgType = receivedMessage.value("type").toString();
6712         if (msgType == "project-resolved") {
6713             receivedReply = true;
6714             const QJsonObject error = receivedMessage.value("error").toObject();
6715             if (!error.isEmpty())
6716                 qDebug() << error;
6717             QVERIFY(error.isEmpty());
6718             const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6719             QJsonArray products = projectData.value("products").toArray();
6720             bool file1 = false;
6721             bool file2 = false;
6722             for (const QJsonValue v : products) {
6723                 const QJsonObject product = v.toObject();
6724                 const QString productName = product.value("full-display-name").toString();
6725                 const QJsonArray groups = product.value("groups").toArray();
6726                 for (const QJsonValue &v : groups) {
6727                     const QJsonObject group = v.toObject();
6728                     const QString groupName = group.value("name").toString();
6729                     const QJsonArray sourceArtifacts = group.value("source-artifacts").toArray();
6730                     for (const QJsonValue &v : sourceArtifacts) {
6731                         const QString filePath = v.toObject().value("file-path").toString();
6732                         if (filePath.endsWith("file1.cpp")) {
6733                             file1 = true;
6734                         } else if (filePath.endsWith("file2.cpp")) {
6735                             QCOMPARE(productName, QString("theLib"));
6736                             QCOMPARE(groupName, QString("sources"));
6737                             file2 = true;
6738                         }
6739                     }
6740                 }
6741             }
6742             QVERIFY(!file1);
6743             QVERIFY(file2);
6744         }
6745     }
6746     QVERIFY(receivedReply);
6747 
6748     receivedReply = false;
6749     receivedProcessResult = false;
6750     compiledFile1 = false;
6751     compiledFile2 = false;
6752     compiledMain = false;
6753     compiledLib = false;
6754     sendPacket(buildRequest);
6755     while (!receivedReply) {
6756         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6757         QVERIFY(!receivedMessage.isEmpty());
6758         const QString msgType = receivedMessage.value("type").toString();
6759         if (msgType == "project-built") {
6760             receivedReply = true;
6761             const QJsonObject error = receivedMessage.value("error").toObject();
6762             if (!error.isEmpty())
6763                 qDebug() << error;
6764             QVERIFY(error.isEmpty());
6765         } else if (msgType == "command-description") {
6766             const QString msg = receivedMessage.value("message").toString();
6767             if (msg.contains("compiling file1.cpp"))
6768                 compiledFile1 = true;
6769             else if (msg.contains("compiling file2.cpp"))
6770                 compiledFile2 = true;
6771             else if (msg.contains("compiling main.cpp"))
6772                 compiledMain = true;
6773             else if (msg.contains("compiling lib.cpp"))
6774                 compiledLib = true;
6775         } else if (msgType == "process-result") {
6776             QCOMPARE(receivedMessage.value("exit-code").toInt(), 0);
6777             receivedProcessResult = true;
6778         }
6779     }
6780     QVERIFY(receivedReply);
6781     QVERIFY(receivedProcessResult);
6782     QVERIFY(!compiledFile1);
6783     QVERIFY(compiledFile2);
6784     QVERIFY(!compiledLib);
6785     QVERIFY(compiledMain);
6786 
6787     // Get generated files.
6788     QJsonObject genFilesRequestPerFile;
6789     genFilesRequestPerFile.insert("source-file", QDir::currentPath() + "/main.cpp");
6790     genFilesRequestPerFile.insert("tags", QJsonArray{QJsonValue("obj")});
6791     QJsonObject genFilesRequestPerProduct;
6792     genFilesRequestPerProduct.insert("full-display-name", "theApp");
6793     genFilesRequestPerProduct.insert("requests", QJsonArray({genFilesRequestPerFile}));
6794     QJsonObject genFilesRequest;
6795     genFilesRequest.insert("type", "get-generated-files-for-sources");
6796     genFilesRequest.insert("products", QJsonArray({genFilesRequestPerProduct}));
6797     sendPacket(genFilesRequest);
6798     receivedReply = false;
6799     while (!receivedReply) {
6800         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6801         QCOMPARE(receivedMessage.value("type").toString(), QString("generated-files-for-sources"));
6802         const QJsonArray products = receivedMessage.value("products").toArray();
6803         QCOMPARE(products.size(), 1);
6804         const QJsonArray results = products.first().toObject().value("results").toArray();
6805         QCOMPARE(results.size(), 1);
6806         const QJsonObject result = results.first().toObject();
6807         QCOMPARE(result.value("source-file"), QDir::currentPath() + "/main.cpp");
6808         const QJsonArray generatedFiles = result.value("generated-files").toArray();
6809         QCOMPARE(generatedFiles.count(), 1);
6810         QCOMPARE(QFileInfo(generatedFiles.first().toString()).fileName(),
6811                  objectFileName("main.cpp", profileName()));
6812         receivedReply = true;
6813     }
6814     QVERIFY(receivedReply);
6815 
6816     // Release project.
6817     const QJsonObject releaseRequest{qMakePair(QString("type"), QJsonValue("release-project"))};
6818     sendPacket(releaseRequest);
6819     receivedReply = false;
6820     while (!receivedReply) {
6821         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6822         QCOMPARE(receivedMessage.value("type").toString(), QString("project-released"));
6823         const QJsonObject error = receivedMessage.value("error").toObject();
6824         if (!error.isEmpty())
6825             qDebug() << error;
6826         QVERIFY(error.isEmpty());
6827         receivedReply = true;
6828     }
6829     QVERIFY(receivedReply);
6830 
6831     // Get build graph info.
6832     QJsonObject loadProjectMessage;
6833     loadProjectMessage.insert("type", "resolve-project");
6834     loadProjectMessage.insert("configuration-name", "my-config");
6835     loadProjectMessage.insert("build-root", QDir::currentPath());
6836     loadProjectMessage.insert("settings-dir", settings()->baseDirectory());
6837     loadProjectMessage.insert("restore-behavior", "restore-only");
6838     loadProjectMessage.insert("data-mode", "only-if-changed");
6839     sendPacket(loadProjectMessage);
6840     receivedReply = false;
6841     while (!receivedReply) {
6842         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6843         if (receivedMessage.value("type") != "project-resolved")
6844             continue;
6845         receivedReply = true;
6846         const QJsonObject error = receivedMessage.value("error").toObject();
6847         if (!error.isEmpty())
6848             qDebug() << error;
6849         QVERIFY(error.isEmpty());
6850         const QString bgFilePath = QDir::currentPath() + '/'
6851                 + relativeBuildGraphFilePath("my-config");
6852         const QJsonObject projectData = receivedMessage.value("project-data").toObject();
6853         QCOMPARE(projectData.value("build-graph-file-path").toString(), bgFilePath);
6854         QCOMPARE(projectData.value("overridden-properties"), overriddenValues);
6855     }
6856     QVERIFY(receivedReply);
6857 
6858     // Send unknown request.
6859     const QJsonObject unknownRequest({qMakePair(QString("type"), QJsonValue("blubb"))});
6860     sendPacket(unknownRequest);
6861     receivedReply = false;
6862     while (!receivedReply) {
6863         receivedMessage = getNextSessionPacket(sessionProc, incomingData);
6864         QCOMPARE(receivedMessage.value("type").toString(), QString("protocol-error"));
6865         receivedReply = true;
6866     }
6867     QVERIFY(receivedReply);
6868 
6869     QJsonObject quitRequest;
6870     quitRequest.insert("type", "quit");
6871     sendPacket(quitRequest);
6872     QVERIFY(sessionProc.waitForFinished(3000));
6873 }
6874 
radAfterIncompleteBuild_data()6875 void TestBlackbox::radAfterIncompleteBuild_data()
6876 {
6877     QTest::addColumn<QString>("projectFileName");
6878     QTest::newRow("Project with Rule") << "project_with_rule.qbs";
6879     QTest::newRow("Project with Transformer") << "project_with_transformer.qbs";
6880 }
6881 
radAfterIncompleteBuild()6882 void TestBlackbox::radAfterIncompleteBuild()
6883 {
6884     QDir::setCurrent(testDataDir + "/rad-after-incomplete-build");
6885     rmDirR(relativeBuildDir());
6886     const QString projectFileName = "project_with_rule.qbs";
6887 
6888     // Step 1: Have a directory where a file used to be.
6889     QbsRunParameters params(QStringList() << "-f" << projectFileName);
6890     QCOMPARE(runQbs(params), 0);
6891     WAIT_FOR_NEW_TIMESTAMP();
6892     REPLACE_IN_FILE(projectFileName, "oldfile", "oldfile/newfile");
6893     params.expectFailure = true;
6894     QVERIFY(runQbs(params) != 0);
6895     WAIT_FOR_NEW_TIMESTAMP();
6896     REPLACE_IN_FILE(projectFileName, "oldfile/newfile", "newfile");
6897     params.expectFailure = false;
6898     QCOMPARE(runQbs(params), 0);
6899     WAIT_FOR_NEW_TIMESTAMP();
6900     REPLACE_IN_FILE(projectFileName, "newfile", "oldfile/newfile");
6901     QCOMPARE(runQbs(params), 0);
6902 
6903     // Step 2: Have a file where a directory used to be.
6904     WAIT_FOR_NEW_TIMESTAMP();
6905     REPLACE_IN_FILE(projectFileName, "oldfile/newfile", "oldfile");
6906     params.expectFailure = true;
6907     QVERIFY(runQbs(params) != 0);
6908     WAIT_FOR_NEW_TIMESTAMP();
6909     REPLACE_IN_FILE(projectFileName, "oldfile", "newfile");
6910     params.expectFailure = false;
6911     QCOMPARE(runQbs(params), 0);
6912     WAIT_FOR_NEW_TIMESTAMP();
6913     REPLACE_IN_FILE(projectFileName, "newfile", "oldfile");
6914     QCOMPARE(runQbs(params), 0);
6915 }
6916 
subProfileChangeTracking()6917 void TestBlackbox::subProfileChangeTracking()
6918 {
6919     QDir::setCurrent(testDataDir + "/subprofile-change-tracking");
6920     const SettingsPtr s = settings();
6921     qbs::Internal::TemporaryProfile subProfile("qbs-autotests-subprofile", s.get());
6922     subProfile.p.setValue("baseProfile", profileName());
6923     subProfile.p.setValue("cpp.includePaths", QStringList("/tmp/include1"));
6924     s->sync();
6925     QCOMPARE(runQbs(), 0);
6926 
6927     subProfile.p.setValue("cpp.includePaths", QStringList("/tmp/include2"));
6928     s->sync();
6929     QbsRunParameters params;
6930     params.command = "resolve";
6931     QCOMPARE(runQbs(params), 0);
6932     params.command = "build";
6933     QCOMPARE(runQbs(params), 0);
6934     QVERIFY(!m_qbsStdout.contains("main1.cpp"));
6935     QVERIFY(m_qbsStdout.contains("main2.cpp"));
6936 }
6937 
successiveChanges()6938 void TestBlackbox::successiveChanges()
6939 {
6940     QDir::setCurrent(testDataDir + "/successive-changes");
6941     QCOMPARE(runQbs(), 0);
6942 
6943     QbsRunParameters params("resolve", QStringList() << "products.theProduct.type:output,blubb");
6944     QCOMPARE(runQbs(params), 0);
6945     QCOMPARE(runQbs(), 0);
6946 
6947     params.arguments << "project.version:2";
6948     QCOMPARE(runQbs(params), 0);
6949     QCOMPARE(runQbs(), 0);
6950     QFile output(relativeProductBuildDir("theProduct") + "/output.out");
6951     QVERIFY2(output.open(QIODevice::ReadOnly), qPrintable(output.errorString()));
6952     const QByteArray version = output.readAll();
6953     QCOMPARE(version.constData(), "2");
6954 }
6955 
installedApp()6956 void TestBlackbox::installedApp()
6957 {
6958     QDir::setCurrent(testDataDir + "/installed_artifact");
6959 
6960     QCOMPARE(runQbs(), 0);
6961     QVERIFY(regularFileExists(defaultInstallRoot
6962             + HostOsInfo::appendExecutableSuffix(QStringLiteral("/usr/bin/installedApp"))));
6963 
6964     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("qbs.installRoot:" + testDataDir
6965                                                             + "/installed-app"))), 0);
6966     QCOMPARE(runQbs(), 0);
6967     QVERIFY(regularFileExists(testDataDir
6968             + HostOsInfo::appendExecutableSuffix("/installed-app/usr/bin/installedApp")));
6969 
6970     QFile addedFile(defaultInstallRoot + QLatin1String("/blubb.txt"));
6971     QVERIFY(addedFile.open(QIODevice::WriteOnly));
6972     addedFile.close();
6973     QVERIFY(addedFile.exists());
6974     QCOMPARE(runQbs(QbsRunParameters("resolve")), 0);
6975     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "--clean-install-root")), 0);
6976     QVERIFY(regularFileExists(defaultInstallRoot
6977             + HostOsInfo::appendExecutableSuffix(QStringLiteral("/usr/bin/installedApp"))));
6978     QVERIFY(regularFileExists(defaultInstallRoot + QLatin1String("/usr/src/main.cpp")));
6979     QVERIFY(!addedFile.exists());
6980 
6981     // Check whether changing install parameters on the product causes re-installation.
6982     WAIT_FOR_NEW_TIMESTAMP();
6983     REPLACE_IN_FILE("installed_artifact.qbs", "qbs.installPrefix: \"/usr\"",
6984                     "qbs.installPrefix: '/usr/local'");
6985     QCOMPARE(runQbs(), 0);
6986     QVERIFY(regularFileExists(defaultInstallRoot
6987             + HostOsInfo::appendExecutableSuffix(QStringLiteral("/usr/local/bin/installedApp"))));
6988     QVERIFY(regularFileExists(defaultInstallRoot + QLatin1String("/usr/local/src/main.cpp")));
6989 
6990     // Check whether changing install parameters on the artifact causes re-installation.
6991     WAIT_FOR_NEW_TIMESTAMP();
6992     REPLACE_IN_FILE("installed_artifact.qbs", "qbs.installDir: \"bin\"",
6993                     "qbs.installDir: 'custom'");
6994     QCOMPARE(runQbs(), 0);
6995     QVERIFY(regularFileExists(defaultInstallRoot
6996             + HostOsInfo::appendExecutableSuffix(QStringLiteral("/usr/local/custom/installedApp"))));
6997 
6998     // Check whether changing install parameters on a source file causes re-installation.
6999     WAIT_FOR_NEW_TIMESTAMP();
7000     REPLACE_IN_FILE("installed_artifact.qbs", "qbs.installDir: \"src\"",
7001                     "qbs.installDir: 'source'");
7002     QCOMPARE(runQbs(), 0);
7003     QVERIFY(regularFileExists(defaultInstallRoot + QLatin1String("/usr/local/source/main.cpp")));
7004 
7005     // Check whether changing install parameters on the command line causes re-installation.
7006     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("qbs.installRoot:" + relativeBuildDir()
7007                                                  + "/blubb"))), 0);
7008     QCOMPARE(runQbs(), 0);
7009     QVERIFY(regularFileExists(relativeBuildDir() + "/blubb/usr/local/source/main.cpp"));
7010 
7011     // Check --no-install
7012     rmDirR(relativeBuildDir());
7013     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "--no-install")), 0);
7014     QCOMPARE(QDir(defaultInstallRoot).entryList(QDir::NoDotAndDotDot).size(), 0);
7015 
7016     // Check --no-build (with and without an existing build graph)
7017     QbsRunParameters params("install", QStringList() << "--no-build");
7018     QCOMPARE(runQbs(params), 0);
7019     rmDirR(relativeBuildDir());
7020     params.expectFailure = true;
7021     QVERIFY(runQbs(params) != 0);
7022     QVERIFY2(m_qbsStderr.contains("Build graph not found"), m_qbsStderr.constData());
7023 }
7024 
installDuplicates()7025 void TestBlackbox::installDuplicates()
7026 {
7027     QDir::setCurrent(testDataDir + "/install-duplicates");
7028 
7029     QbsRunParameters params;
7030     params.expectFailure = true;
7031     QVERIFY(runQbs(params) != 0);
7032     QVERIFY(m_qbsStderr.contains("Cannot install files"));
7033 }
7034 
installDuplicatesNoError()7035 void TestBlackbox::installDuplicatesNoError()
7036 {
7037     QDir::setCurrent(testDataDir + "/install-duplicates-no-error");
7038 
7039     QbsRunParameters params;
7040     QCOMPARE(runQbs(params), 0);
7041 }
7042 
installedSourceFiles()7043 void TestBlackbox::installedSourceFiles()
7044 {
7045     QDir::setCurrent(testDataDir + "/installed-source-files");
7046 
7047     QCOMPARE(runQbs(), 0);
7048     QVERIFY(regularFileExists(defaultInstallRoot + QLatin1String("/readme.txt")));
7049     QVERIFY(regularFileExists(defaultInstallRoot + QLatin1String("/main.cpp")));
7050 }
7051 
toolLookup()7052 void TestBlackbox::toolLookup()
7053 {
7054     QbsRunParameters params(QStringLiteral("setup-toolchains"), QStringList("--help"));
7055     params.profile.clear();
7056     QCOMPARE(runQbs(params), 0);
7057 }
7058 
topLevelSearchPath()7059 void TestBlackbox::topLevelSearchPath()
7060 {
7061     QDir::setCurrent(testDataDir + "/toplevel-searchpath");
7062 
7063     QbsRunParameters params;
7064     params.expectFailure = true;
7065     QVERIFY(runQbs(params) != 0);
7066     QVERIFY2(m_qbsStderr.contains("MyProduct"), m_qbsStderr.constData());
7067     params.arguments << ("project.qbsSearchPaths:" + QDir::currentPath() + "/qbs-resources");
7068     QCOMPARE(runQbs(params), 0);
7069 }
7070 
checkProjectFilePath()7071 void TestBlackbox::checkProjectFilePath()
7072 {
7073     QDir::setCurrent(testDataDir + "/project_filepath_check");
7074     QbsRunParameters params(QStringList("-f") << "project1.qbs");
7075     QCOMPARE(runQbs(params), 0);
7076     QVERIFY2(m_qbsStdout.contains("main.cpp"), m_qbsStdout.constData());
7077     QCOMPARE(runQbs(params), 0);
7078 
7079     params.arguments = QStringList("-f") << "project2.qbs";
7080     params.expectFailure = true;
7081     QVERIFY(runQbs(params) != 0);
7082     QVERIFY(m_qbsStderr.contains("project file"));
7083 
7084     params.arguments = QStringList("-f") << "project2.qbs";
7085     params.command = "resolve";
7086     params.expectFailure = false;
7087     QCOMPARE(runQbs(params), 0);
7088     params.command = "build";
7089     QCOMPARE(runQbs(params), 0);
7090     QVERIFY2(m_qbsStdout.contains("main2.cpp"), m_qbsStdout.constData());
7091 }
7092 
checkTimestamps()7093 void TestBlackbox::checkTimestamps()
7094 {
7095     QDir::setCurrent(testDataDir + "/check-timestamps");
7096     QCOMPARE(runQbs(), 0);
7097     QVERIFY2(m_qbsStdout.contains("compiling file.cpp"), m_qbsStdout.constData());
7098     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
7099     QVERIFY(QFile::remove(relativeBuildGraphFilePath()));
7100     WAIT_FOR_NEW_TIMESTAMP();
7101     touch("file.h");
7102     QCOMPARE(runQbs(QStringList("--check-timestamps")), 0);
7103     QVERIFY2(m_qbsStdout.contains("compiling file.cpp"), m_qbsStdout.constData());
7104     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
7105 }
7106 
chooseModuleInstanceByPriority()7107 void TestBlackbox::chooseModuleInstanceByPriority()
7108 {
7109     QFETCH(QString, idol);
7110     QFETCH(QStringList, expectedSubStrings);
7111     QFETCH(bool, expectSuccess);
7112     QDir::setCurrent(testDataDir + "/choose-module-instance");
7113     rmDirR(relativeBuildDir());
7114     QbsRunParameters params(QStringList("modules.qbs.targetPlatform:" + idol));
7115     params.expectFailure = !expectSuccess;
7116     if (expectSuccess) {
7117         QCOMPARE(runQbs(params), 0);
7118     } else {
7119         QVERIFY(runQbs(params) != 0);
7120         return;
7121     }
7122 
7123     const QString installRoot = relativeBuildDir() + "/install-root/";
7124     QVERIFY(QFile::exists(installRoot + "/gerbil.txt"));
7125     QFile file(installRoot + "/gerbil.txt");
7126     QVERIFY(file.open(QIODevice::ReadOnly));
7127     const QString content = QString::fromUtf8(file.readAll());
7128     for (const auto &str : expectedSubStrings) {
7129         if (content.contains(str))
7130             continue;
7131         qDebug() << "content:" << content;
7132         qDebug() << "substring:" << str;
7133         QFAIL("missing substring");
7134     }
7135 }
7136 
chooseModuleInstanceByPriority_data()7137 void TestBlackbox::chooseModuleInstanceByPriority_data()
7138 {
7139     QTest::addColumn<QString>("idol");
7140     QTest::addColumn<QStringList>("expectedSubStrings");
7141     QTest::addColumn<bool>("expectSuccess");
7142     QTest::newRow("ringo")
7143             << "Beatles" << QStringList() << false;
7144     QTest::newRow("ritchie1")
7145             << "Deep Purple" << QStringList{"slipped", "litchi", "ritchie"} << true;
7146     QTest::newRow("ritchie2")
7147             << "Rainbow" << QStringList{"slipped", "litchi", "ritchie"} << true;
7148     QTest::newRow("lord")
7149             << "Whitesnake" << QStringList{"chewed", "cord", "lord"} << true;
7150 }
7151 
7152 class TemporaryDefaultProfileRemover
7153 {
7154 public:
TemporaryDefaultProfileRemover(qbs::Settings * settings)7155     TemporaryDefaultProfileRemover(qbs::Settings *settings)
7156         : m_settings(settings), m_defaultProfile(settings->defaultProfile())
7157     {
7158         m_settings->remove(QStringLiteral("defaultProfile"));
7159     }
7160 
~TemporaryDefaultProfileRemover()7161     ~TemporaryDefaultProfileRemover()
7162     {
7163         if (!m_defaultProfile.isEmpty())
7164             m_settings->setValue(QStringLiteral("defaultProfile"), m_defaultProfile);
7165     }
7166 
7167 private:
7168     qbs::Settings *m_settings;
7169     const QString m_defaultProfile;
7170 };
7171 
assembly()7172 void TestBlackbox::assembly()
7173 {
7174     QDir::setCurrent(testDataDir + "/assembly");
7175     QVERIFY(runQbs() == 0);
7176 
7177     const QVariantMap properties = ([&]() {
7178         QFile propertiesFile(relativeProductBuildDir("assembly") + "/properties.json");
7179         if (propertiesFile.open(QIODevice::ReadOnly))
7180             return QJsonDocument::fromJson(propertiesFile.readAll()).toVariant().toMap();
7181         return QVariantMap{};
7182     })();
7183     QVERIFY(!properties.empty());
7184     const auto toolchain = properties.value("qbs.toolchain").toStringList();
7185     QVERIFY(!toolchain.empty());
7186     const bool haveGcc = toolchain.contains("gcc");
7187     const bool haveMSVC = toolchain.contains("msvc");
7188 
7189     QCOMPARE(m_qbsStdout.contains("assembling testa.s"), haveGcc);
7190     QCOMPARE(m_qbsStdout.contains("compiling testb.S"), haveGcc);
7191     QCOMPARE(m_qbsStdout.contains("compiling testc.sx"), haveGcc);
7192     QCOMPARE(m_qbsStdout.contains("creating libtesta.a"), haveGcc);
7193     QCOMPARE(m_qbsStdout.contains("creating libtestb.a"), haveGcc);
7194     QCOMPARE(m_qbsStdout.contains("creating libtestc.a"), haveGcc);
7195     QCOMPARE(m_qbsStdout.contains("creating testd.lib"), haveMSVC);
7196 }
7197 
autotestWithDependencies()7198 void TestBlackbox::autotestWithDependencies()
7199 {
7200     QDir::setCurrent(testDataDir + "/autotest-with-dependencies");
7201     QCOMPARE(runQbs({"resolve"}), 0);
7202     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7203         QSKIP("Cannot run binaries in cross-compiled build");
7204     QCOMPARE(runQbs(QStringList({"-p", "autotest-runner"})), 0);
7205     QVERIFY2(m_qbsStdout.contains("i am the test app")
7206              && m_qbsStdout.contains("i am the helper"), m_qbsStdout.constData());
7207 }
7208 
autotestTimeout()7209 void TestBlackbox::autotestTimeout()
7210 {
7211     QFETCH(QStringList, resolveParams);
7212     QFETCH(bool, expectFailure);
7213     QDir::setCurrent(testDataDir + "/autotest-timeout");
7214     QbsRunParameters resolveParameters("resolve", resolveParams);
7215     QCOMPARE(runQbs(resolveParameters), 0);
7216     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7217         QSKIP("Cannot run binaries in cross-compiled build");
7218     QbsRunParameters buildParameters(QStringList({"-p", "autotest-runner"}));
7219     buildParameters.expectFailure = expectFailure;
7220     if (expectFailure) {
7221         QVERIFY(runQbs(buildParameters) != 0);
7222         QVERIFY(m_qbsStderr.contains("cancelled") && m_qbsStderr.contains("timeout"));
7223     }
7224     else
7225         QVERIFY(runQbs(buildParameters) == 0);
7226 }
7227 
autotestTimeout_data()7228 void TestBlackbox::autotestTimeout_data()
7229 {
7230     QTest::addColumn<QStringList>("resolveParams");
7231     QTest::addColumn<bool>("expectFailure");
7232     QTest::newRow("no timeout") << QStringList() << false;
7233     QTest::newRow("timeout on test") << QStringList({"products.testApp.autotest.timeout:2"})
7234             << true;
7235     QTest::newRow("timeout on runner") << QStringList({"products.autotest-runner.timeout:2"})
7236             << true;
7237 }
7238 
autotests_data()7239 void TestBlackbox::autotests_data()
7240 {
7241     QTest::addColumn<QString>("evilPropertySpec");
7242     QTest::addColumn<QByteArray>("expectedErrorMessage");
7243     QTest::newRow("missing arguments") << QString("products.test1.autotest.arguments:[]")
7244                                        << QByteArray("This test needs exactly one argument");
7245     QTest::newRow("missing working dir") << QString("products.test2.autotest.workingDir:''")
7246                                          << QByteArray("Test resource not found");
7247     QTest::newRow("missing flaky specifier")
7248             << QString("products.test3.autotest.allowFailure:false")
7249             << QByteArray("I am an awful test");
7250     QTest::newRow("everything's fine") << QString() << QByteArray();
7251 }
7252 
autotests()7253 void TestBlackbox::autotests()
7254 {
7255     QDir::setCurrent(testDataDir + "/autotests");
7256     QFETCH(QString, evilPropertySpec);
7257     QFETCH(QByteArray, expectedErrorMessage);
7258     QbsRunParameters resolveParams("resolve");
7259     if (!evilPropertySpec.isEmpty())
7260         resolveParams.arguments << evilPropertySpec;
7261     QCOMPARE(runQbs(resolveParams), 0);
7262     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7263         QSKIP("Cannot run binaries in cross-compiled build");
7264     QbsRunParameters testParams(QStringList{"-p", "autotest-runner"});
7265     if (!evilPropertySpec.isEmpty())
7266         testParams.expectFailure = true;
7267     QCOMPARE(runQbs(testParams) == 0, !testParams.expectFailure);
7268     if (testParams.expectFailure) {
7269         QVERIFY2(m_qbsStderr.contains(expectedErrorMessage), m_qbsStderr.constData());
7270         return;
7271     }
7272     QVERIFY2(m_qbsStdout.contains("Running test test1")
7273              && m_qbsStdout.contains("Running test test2")
7274              && m_qbsStdout.contains("Running test test3"), m_qbsStdout.constData());
7275     QCOMPARE(m_qbsStdout.count("PASS"), 2);
7276     QCOMPARE(m_qbsStderr.count("FAIL"), 1);
7277 }
7278 
auxiliaryInputsFromDependencies()7279 void TestBlackbox::auxiliaryInputsFromDependencies()
7280 {
7281     QDir::setCurrent(testDataDir + "/aux-inputs-from-deps");
7282     QCOMPARE(runQbs(), 0);
7283     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
7284     QVERIFY2(m_qbsStdout.contains("generating dummy.out"), m_qbsStdout.constData());
7285     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("products.dep.sleep:false"))), 0);
7286     WAIT_FOR_NEW_TIMESTAMP();
7287     QCOMPARE(runQbs(), 0);
7288     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
7289     QVERIFY2(m_qbsStdout.contains("generating dummy.out"), m_qbsStdout.constData());
7290 }
7291 
explicitlyDependsOn()7292 void TestBlackbox::explicitlyDependsOn()
7293 {
7294     QFETCH(QString, useExplicitlyDependsOn);
7295     QFETCH(QString, useExplicitlyDependsOnFromDependencies);
7296     QFETCH(QString, useModule);
7297     QFETCH(bool, expectFailure);
7298 
7299     QDir::setCurrent(testDataDir + "/explicitly-depends-on");
7300     QbsRunParameters params("",
7301                 QStringList("products.prod1.useExplicitlyDependsOn:" + useExplicitlyDependsOn)
7302                 << "products.prod1.useExplicitlyDependsOnFromDependencies:"
7303                     + useExplicitlyDependsOnFromDependencies
7304                 << "projects.proj1.useModule:"
7305                     + useModule);
7306     params.expectFailure = expectFailure;
7307 
7308     rmDirR(relativeBuildDir());
7309 
7310     if (params.expectFailure) {
7311         // Build should fail because a rule cycle is created within the product when
7312         // explicitlyDependsOn is used.
7313         QVERIFY(runQbs(params) != 0);
7314         QVERIFY2(m_qbsStderr.contains("Cycle detected in rule dependencies"),
7315                  m_qbsStderr.constData());
7316     } else {
7317         // When explicitlyDependsOnFromDependencies is used, build should succeed due to the
7318         // "final" tag being pulled in from dependencies.
7319         QCOMPARE(runQbs(params), 0);
7320 
7321         if (useModule == QLatin1String("false")) {
7322             QVERIFY2(m_qbsStdout.contains("creating 'product-fish.txt' tagged with 'final'"),
7323                      m_qbsStdout.constData());
7324             QVERIFY2(m_qbsStdout.contains("Using explicitlyDependsOnArtifact: product-fish.txt"),
7325                      m_qbsStdout.constData());
7326             QVERIFY2(m_qbsStdout.contains("step1 -> step2"), m_qbsStdout.constData());
7327             QVERIFY2(m_qbsStdout.contains("step2 -> step3"), m_qbsStdout.constData());
7328             QVERIFY2(m_qbsStdout.contains("step3 -> final"), m_qbsStdout.constData());
7329         } else {
7330             QVERIFY2(!m_qbsStdout.contains("creating 'product-fish.txt' tagged with 'final'"),
7331                      m_qbsStdout.constData());
7332             QVERIFY2(m_qbsStdout.contains("Using explicitlyDependsOnArtifact: module-fish.txt"),
7333                      m_qbsStdout.constData());
7334             QVERIFY2(m_qbsStdout.contains("step1 -> step2"), m_qbsStdout.constData());
7335             QVERIFY2(m_qbsStdout.contains("step2 -> step3"), m_qbsStdout.constData());
7336             QVERIFY2(m_qbsStdout.contains("step3 -> final"), m_qbsStdout.constData());
7337         }
7338     }
7339 }
7340 
explicitlyDependsOn_data()7341 void TestBlackbox::explicitlyDependsOn_data()
7342 {
7343     QTest::addColumn<QString>("useExplicitlyDependsOn");
7344     QTest::addColumn<QString>("useExplicitlyDependsOnFromDependencies");
7345     QTest::addColumn<QString>("useModule");
7346     QTest::addColumn<bool>("expectFailure");
7347 
7348     QTest::newRow("useExplicitlyDependsOn -> causes cycle")
7349             << "true" << "false" << "false" << true;
7350     QTest::newRow("explicitlyDependsOnFromDependencies + product")
7351             << "false" << "true" << "false" << false;
7352     QTest::newRow("explicitlyDependsOnFromDependencies + module + filesAreTargets")
7353             << "false" << "true" << "true" << false;
7354 }
7355 
haveMakeNsis()7356 static bool haveMakeNsis()
7357 {
7358     QStringList regKeys;
7359     regKeys << QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\NSIS")
7360             << QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\NSIS");
7361 
7362     QStringList paths = QProcessEnvironment::systemEnvironment().value("PATH")
7363             .split(HostOsInfo::pathListSeparator(), QBS_SKIP_EMPTY_PARTS);
7364 
7365     for (const QString &key : qAsConst(regKeys)) {
7366         QSettings settings(key, QSettings::NativeFormat);
7367         QString str = settings.value(QStringLiteral(".")).toString();
7368         if (!str.isEmpty())
7369             paths.prepend(str);
7370     }
7371 
7372     bool haveMakeNsis = false;
7373     for (const QString &path : qAsConst(paths)) {
7374         if (regularFileExists(QDir::fromNativeSeparators(path) +
7375                           HostOsInfo::appendExecutableSuffix(QStringLiteral("/makensis")))) {
7376             haveMakeNsis = true;
7377             break;
7378         }
7379     }
7380 
7381     return haveMakeNsis;
7382 }
7383 
nsis()7384 void TestBlackbox::nsis()
7385 {
7386     if (!haveMakeNsis()) {
7387         QSKIP("makensis is not installed");
7388         return;
7389     }
7390 
7391     bool targetIsWindows = targetOs() == HostOsInfo::HostOsWindows;
7392     QDir::setCurrent(testDataDir + "/nsis");
7393     QVERIFY(runQbs() == 0);
7394     QCOMPARE((bool)m_qbsStdout.contains("compiling hello.nsi"), targetIsWindows);
7395     QCOMPARE((bool)m_qbsStdout.contains("SetCompressor ignored due to previous call with the /FINAL switch"), targetIsWindows);
7396     QVERIFY(!QFile::exists(defaultInstallRoot + "/you-should-not-see-a-file-with-this-name.exe"));
7397 }
7398 
nsisDependencies()7399 void TestBlackbox::nsisDependencies()
7400 {
7401     if (!haveMakeNsis()) {
7402         QSKIP("makensis is not installed");
7403         return;
7404     }
7405 
7406     bool targetIsWindows = targetOs() == HostOsInfo::HostOsWindows;
7407     QDir::setCurrent(testDataDir + "/nsisDependencies");
7408     QCOMPARE(runQbs(), 0);
7409     QCOMPARE(m_qbsStdout.contains("compiling hello.nsi"), targetIsWindows);
7410 }
7411 
outOfDateMarking()7412 void TestBlackbox::outOfDateMarking()
7413 {
7414     QDir::setCurrent(testDataDir + "/out-of-date-marking");
7415     for (int i = 0; i < 25; ++i) {
7416         QCOMPARE(runQbs(), 0);
7417         QVERIFY2(m_qbsStdout.contains("generating myheader.h"), qPrintable(QString::number(i)));
7418         QVERIFY2(m_qbsStdout.contains("compiling main.c"), qPrintable(QString::number(i)));
7419     }
7420 }
7421 
enableExceptions()7422 void TestBlackbox::enableExceptions()
7423 {
7424     QFETCH(QString, file);
7425     QFETCH(bool, enable);
7426     QFETCH(bool, expectSuccess);
7427 
7428     QDir::setCurrent(testDataDir + QStringLiteral("/enableExceptions"));
7429 
7430     QbsRunParameters params;
7431     params.arguments = QStringList() << "-f" << file
7432                                      << (QStringLiteral("modules.cpp.enableExceptions:")
7433                                          + (enable ? "true" : "false"));
7434     params.expectFailure = !expectSuccess;
7435     rmDirR(relativeBuildDir());
7436     if (!params.expectFailure)
7437         QCOMPARE(runQbs(params), 0);
7438     else
7439         QVERIFY(runQbs(params) != 0);
7440 }
7441 
enableExceptions_data()7442 void TestBlackbox::enableExceptions_data()
7443 {
7444     QTest::addColumn<QString>("file");
7445     QTest::addColumn<bool>("enable");
7446     QTest::addColumn<bool>("expectSuccess");
7447 
7448     QTest::newRow("no exceptions, enabled") << "none.qbs" << true << true;
7449     QTest::newRow("no exceptions, disabled") << "none.qbs" << false << true;
7450 
7451     QTest::newRow("C++ exceptions, enabled") << "exceptions.qbs" << true << true;
7452     QTest::newRow("C++ exceptions, disabled") << "exceptions.qbs" << false << false;
7453 
7454     if (HostOsInfo::isMacosHost()) {
7455         QTest::newRow("Objective-C exceptions, enabled") << "exceptions-objc.qbs" << true << true;
7456         QTest::newRow("Objective-C exceptions in Objective-C++ source, enabled") << "exceptions-objcpp.qbs" << true << true;
7457         QTest::newRow("C++ exceptions in Objective-C++ source, enabled") << "exceptions-objcpp-cpp.qbs" << true << true;
7458         QTest::newRow("Objective-C, disabled") << "exceptions-objc.qbs" << false << false;
7459         QTest::newRow("Objective-C exceptions in Objective-C++ source, disabled") << "exceptions-objcpp.qbs" << false << false;
7460         QTest::newRow("C++ exceptions in Objective-C++ source, disabled") << "exceptions-objcpp-cpp.qbs" << false << false;
7461     }
7462 }
7463 
enableRtti()7464 void TestBlackbox::enableRtti()
7465 {
7466     QDir::setCurrent(testDataDir + QStringLiteral("/enableRtti"));
7467 
7468     QbsRunParameters params;
7469 
7470     params.arguments = QStringList() << "modules.cpp.enableRtti:true";
7471     rmDirR(relativeBuildDir());
7472     QCOMPARE(runQbs(params), 0);
7473 
7474     if (HostOsInfo::isMacosHost()) {
7475         params.arguments = QStringList() << "modules.cpp.enableRtti:true"
7476                                          << "project.treatAsObjcpp:true";
7477         rmDirR(relativeBuildDir());
7478         QCOMPARE(runQbs(params), 0);
7479     }
7480 
7481     params.expectFailure = true;
7482 
7483     params.arguments = QStringList() << "modules.cpp.enableRtti:false";
7484     rmDirR(relativeBuildDir());
7485     QVERIFY(runQbs(params) != 0);
7486 
7487     if (HostOsInfo::isMacosHost()) {
7488         params.arguments = QStringList() << "modules.cpp.enableRtti:false"
7489                                          << "project.treatAsObjcpp:true";
7490         rmDirR(relativeBuildDir());
7491         QVERIFY(runQbs(params) != 0);
7492     }
7493 }
7494 
envMerging()7495 void TestBlackbox::envMerging()
7496 {
7497     QDir::setCurrent(testDataDir + "/env-merging");
7498     QbsRunParameters params("resolve");
7499     QString pathVal = params.environment.value("PATH");
7500     pathVal.prepend(HostOsInfo::pathListSeparator()).prepend("/opt/blackbox/bin");
7501     const QString keyName = HostOsInfo::isWindowsHost() ? "pATh" : "PATH";
7502     params.environment.insert(keyName, pathVal);
7503     QCOMPARE(runQbs(params), 0);
7504     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7505         QSKIP("Cannot run binaries in cross-compiled build");
7506     params.command = "build";
7507     QCOMPARE(runQbs(params), 0);
7508     QVERIFY2(m_qbsStdout.contains(QByteArray("PATH=/opt/tool/bin")
7509                                   + HostOsInfo::pathListSeparator().toLatin1())
7510              && m_qbsStdout.contains(HostOsInfo::pathListSeparator().toLatin1()
7511                                      + QByteArray("/opt/blackbox/bin")),
7512              m_qbsStdout.constData());
7513 }
7514 
envNormalization()7515 void TestBlackbox::envNormalization()
7516 {
7517     QDir::setCurrent(testDataDir + "/env-normalization");
7518     QbsRunParameters params;
7519     params.environment.insert("myvar", "x");
7520     QCOMPARE(runQbs(params), 0);
7521     if (HostOsInfo::isWindowsHost())
7522         QVERIFY2(m_qbsStdout.contains("\"MYVAR\":\"x\""), m_qbsStdout.constData());
7523     else
7524         QVERIFY2(m_qbsStdout.contains("\"myvar\":\"x\""), m_qbsStdout.constData());
7525 }
7526 
generatedArtifactAsInputToDynamicRule()7527 void TestBlackbox::generatedArtifactAsInputToDynamicRule()
7528 {
7529     QDir::setCurrent(testDataDir + "/generated-artifact-as-input-to-dynamic-rule");
7530     QCOMPARE(runQbs(), 0);
7531     const QString oldFile = relativeProductBuildDir("p") + "/old.txt";
7532     QVERIFY2(regularFileExists(oldFile), qPrintable(oldFile));
7533     WAIT_FOR_NEW_TIMESTAMP();
7534     QFile inputFile("input.txt");
7535     QVERIFY2(inputFile.open(QIODevice::WriteOnly), qPrintable(inputFile.errorString()));
7536     inputFile.resize(0);
7537     inputFile.write("new.txt");
7538     inputFile.close();
7539     QCOMPARE(runQbs(), 0);
7540     QVERIFY2(!regularFileExists(oldFile), qPrintable(oldFile));
7541     const QString newFile = relativeProductBuildDir("p") + "/new.txt";
7542     QVERIFY2(regularFileExists(newFile), qPrintable(oldFile));
7543     QVERIFY2(m_qbsStdout.contains("generating"), m_qbsStdout.constData());
7544     QCOMPARE(runQbs(), 0);
7545     QVERIFY2(!m_qbsStdout.contains("generating"), m_qbsStdout.constData());
7546 }
7547 
generateLinkerMapFile()7548 void TestBlackbox::generateLinkerMapFile()
7549 {
7550     QDir::setCurrent(testDataDir + "/generate-linker-map-file");
7551     QCOMPARE(runQbs(), 0);
7552     const bool isUsed = m_qbsStdout.contains("use test: true");
7553     const bool isNotUsed = m_qbsStdout.contains("use test: false");
7554     QVERIFY(isUsed != isNotUsed);
7555     if (isUsed) {
7556         QVERIFY(QFile::exists(relativeProductBuildDir("app-map")
7557             + "/app-map.map"));
7558         QVERIFY(!QFile::exists(relativeProductBuildDir("app-nomap")
7559             + "/app-nomap.map"));
7560         QVERIFY(!QFile::exists(relativeProductBuildDir("app-nomap-default")
7561             + "/app-nomap-default.map"));
7562     } else {
7563         QSKIP("Unsupported toolchain. Skipping.");
7564     }
7565 }
7566 
generator()7567 void TestBlackbox::generator()
7568 {
7569     QFETCH(QString, inputFile);
7570     QFETCH(QStringList, toBeCompiled);
7571     QDir::setCurrent(testDataDir + "/generator");
7572     if (!inputFile.isEmpty()) {
7573         WAIT_FOR_NEW_TIMESTAMP();
7574         QFile input(inputFile);
7575         QFile output("input.txt");
7576         QVERIFY2(!output.exists() || output.remove(), qPrintable(output.errorString()));
7577         QVERIFY2(input.copy(output.fileName()), qPrintable(input.errorString()));
7578         touch(output.fileName());
7579     }
7580     QCOMPARE(runQbs(), 0);
7581     QCOMPARE(toBeCompiled.contains("main.cpp"), m_qbsStdout.contains("compiling main.cpp"));
7582     QCOMPARE(toBeCompiled.contains("file1.cpp"), m_qbsStdout.contains("compiling file1.cpp"));
7583     QCOMPARE(toBeCompiled.contains("file2.cpp"), m_qbsStdout.contains("compiling file2.cpp"));
7584 }
7585 
generator_data()7586 void TestBlackbox::generator_data()
7587 {
7588     QTest::addColumn<QString>("inputFile");
7589     QTest::addColumn<QStringList>("toBeCompiled");
7590     QTest::newRow("both") << "input.both.txt" << QStringList{"main.cpp", "file1.cpp", "file2.cpp"};
7591     QTest::newRow("file1") << "input.file1.txt" << QStringList{"file1.cpp"};
7592     QTest::newRow("file2") << "input.file2.txt" << QStringList{"file2.cpp"};
7593     QTest::newRow("none") << "input.none.txt" << QStringList();
7594     QTest::newRow("both again") << "input.both.txt" << QStringList{"file1.cpp", "file2.cpp"};
7595     QTest::newRow("no update") << QString() << QStringList();
7596 }
7597 
nodejs()7598 void TestBlackbox::nodejs()
7599 {
7600     const SettingsPtr s = settings();
7601     Profile p(profileName(), s.get());
7602 
7603     int status;
7604     findNodejs(&status);
7605     QCOMPARE(status, 0);
7606 
7607     QDir::setCurrent(testDataDir + QLatin1String("/nodejs"));
7608 
7609     status = runQbs();
7610     if (p.value("nodejs.toolchainInstallPath").toString().isEmpty()
7611             && status != 0 && m_qbsStderr.contains("toolchainInstallPath")) {
7612         QSKIP("nodejs.toolchainInstallPath not set and automatic detection failed");
7613     }
7614 
7615     if (p.value("nodejs.packageManagerPrefixPath").toString().isEmpty()
7616             && status != 0 && m_qbsStderr.contains("nodejs.packageManagerPrefixPath")) {
7617         QSKIP("nodejs.packageManagerFilePath not set and automatic detection failed");
7618     }
7619 
7620     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7621         QSKIP("Cannot run binaries in cross-compiled build");
7622     QCOMPARE(status, 0);
7623 
7624     QbsRunParameters params;
7625     params.command = QLatin1String("run");
7626     QCOMPARE(runQbs(params), 0);
7627     QVERIFY((bool)m_qbsStdout.contains("hello world"));
7628     QVERIFY(regularFileExists(relativeProductBuildDir("hello") + "/hello.js"));
7629 }
7630 
typescript()7631 void TestBlackbox::typescript()
7632 {
7633     if (qEnvironmentVariableIsSet("GITHUB_ACTIONS"))
7634         QSKIP("Skip this test when running on GitHub");
7635 
7636     const SettingsPtr s = settings();
7637     Profile p(profileName(), s.get());
7638 
7639     int status;
7640     findTypeScript(&status);
7641     QCOMPARE(status, 0);
7642 
7643     QDir::setCurrent(testDataDir + QLatin1String("/typescript"));
7644 
7645     QbsRunParameters params;
7646     params.expectFailure = true;
7647     status = runQbs(params);
7648     if (p.value("typescript.toolchainInstallPath").toString().isEmpty() && status != 0) {
7649         if (m_qbsStderr.contains("Path\" must be specified"))
7650             QSKIP("typescript probe failed");
7651         if (m_qbsStderr.contains("typescript.toolchainInstallPath"))
7652             QSKIP("typescript.toolchainInstallPath not set and automatic detection failed");
7653         if (m_qbsStderr.contains("nodejs.interpreterFilePath"))
7654             QSKIP("nodejs.interpreterFilePath not set and automatic detection failed");
7655     }
7656 
7657     if (status != 0)
7658         qDebug() << m_qbsStderr;
7659     QCOMPARE(status, 0);
7660 
7661     params.expectFailure = false;
7662     params.command = QStringLiteral("run");
7663     params.arguments = QStringList() << "-p" << "animals";
7664     QCOMPARE(runQbs(params), 0);
7665 
7666     QVERIFY(regularFileExists(relativeProductBuildDir("animals") + "/animals.js"));
7667     QVERIFY(regularFileExists(relativeProductBuildDir("animals") + "/extra.js"));
7668     QVERIFY(regularFileExists(relativeProductBuildDir("animals") + "/main.js"));
7669 }
7670 
undefinedTargetPlatform()7671 void TestBlackbox::undefinedTargetPlatform()
7672 {
7673     QDir::setCurrent(testDataDir + "/undefined-target-platform");
7674     QCOMPARE(runQbs(), 0);
7675 }
7676 
importInPropertiesCondition()7677 void TestBlackbox::importInPropertiesCondition()
7678 {
7679     QDir::setCurrent(testDataDir + "/import-in-properties-condition");
7680     QCOMPARE(runQbs(), 0);
7681 }
7682 
importSearchPath()7683 void TestBlackbox::importSearchPath()
7684 {
7685     QDir::setCurrent(testDataDir + "/import-searchpath");
7686     QCOMPARE(runQbs(), 0);
7687     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
7688     QVERIFY2(!m_qbsStdout.contains("compiling somefile.cpp"), m_qbsStdout.constData());
7689 }
7690 
importingProduct()7691 void TestBlackbox::importingProduct()
7692 {
7693     QDir::setCurrent(testDataDir + "/importing-product");
7694     QCOMPARE(runQbs(), 0);
7695 }
7696 
importsConflict()7697 void TestBlackbox::importsConflict()
7698 {
7699     QDir::setCurrent(testDataDir + "/imports-conflict");
7700     QCOMPARE(runQbs(), 0);
7701 }
7702 
includeLookup()7703 void TestBlackbox::includeLookup()
7704 {
7705     QDir::setCurrent(testDataDir + "/includeLookup");
7706     QCOMPARE(runQbs({"resolve"}), 0);
7707     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7708         QSKIP("Cannot run binaries in cross-compiled build");
7709     QbsRunParameters params;
7710     params.command = "run";
7711     QCOMPARE(runQbs(params), 0);
7712     QVERIFY2(m_qbsStdout.contains("definition.."), m_qbsStdout.constData());
7713 }
7714 
inputTagsChangeTracking_data()7715 void TestBlackbox::inputTagsChangeTracking_data()
7716 {
7717     QTest::addColumn<QString>("generateInput");
7718     QTest::newRow("source artifact") << QString("no");
7719     QTest::newRow("generated artifact (static)") << QString("static");
7720     QTest::newRow("generated artifact (dynamic)") << QString("dynamic");
7721 }
7722 
inputTagsChangeTracking()7723 void TestBlackbox::inputTagsChangeTracking()
7724 {
7725     QDir::setCurrent(testDataDir + "/input-tags-change-tracking");
7726     const QString xOut = QDir::currentPath() + '/' + relativeProductBuildDir("p") + "/x.out";
7727     const QString yOut = QDir::currentPath() + '/' + relativeProductBuildDir("p") + "/y.out";
7728     QFETCH(QString, generateInput);
7729     const QbsRunParameters resolveParams("resolve",
7730                                          QStringList("products.p.generateInput:" + generateInput));
7731     QCOMPARE(runQbs(resolveParams), 0);
7732     QCOMPARE(runQbs(), 0);
7733     QVERIFY(m_qbsStdout.contains("generating input.txt") == (generateInput == "static"));
7734     QVERIFY2(!QFile::exists(xOut), qPrintable(xOut));
7735     QVERIFY2(!QFile::exists(yOut), qPrintable(yOut));
7736     WAIT_FOR_NEW_TIMESTAMP();
7737     REPLACE_IN_FILE("input-tags-change-tracking.qbs", "Tags: [\"txt\", \"empty\"]",
7738                     "Tags: \"txt\"");
7739     QCOMPARE(runQbs(), 0);
7740     QVERIFY2(QFile::exists(xOut), qPrintable(xOut));
7741     QVERIFY2(!QFile::exists(yOut), qPrintable(yOut));
7742     WAIT_FOR_NEW_TIMESTAMP();
7743     REPLACE_IN_FILE("input-tags-change-tracking.qbs", "Tags: \"txt\"",
7744                     "Tags: [\"txt\", \"y\"]");
7745     QCOMPARE(runQbs(), 0);
7746     QVERIFY2(!QFile::exists(xOut), qPrintable(xOut));
7747     QVERIFY2(QFile::exists(yOut), qPrintable(yOut));
7748     WAIT_FOR_NEW_TIMESTAMP();
7749     REPLACE_IN_FILE("input-tags-change-tracking.qbs", "Tags: [\"txt\", \"y\"]",
7750                     "Tags: [\"txt\", \"empty\"]");
7751     QCOMPARE(runQbs(), 0);
7752     QVERIFY2(!QFile::exists(xOut), qPrintable(xOut));
7753     QVERIFY2(!QFile::exists(yOut), qPrintable(yOut));
7754 }
7755 
outputArtifactAutoTagging()7756 void TestBlackbox::outputArtifactAutoTagging()
7757 {
7758     QDir::setCurrent(testDataDir + QLatin1String("/output-artifact-auto-tagging"));
7759 
7760     QCOMPARE(runQbs(), 0);
7761     QVERIFY(regularFileExists(relativeExecutableFilePath("output-artifact-auto-tagging")));
7762 }
7763 
outputRedirection()7764 void TestBlackbox::outputRedirection()
7765 {
7766     QDir::setCurrent(testDataDir + "/output-redirection");
7767     QCOMPARE(runQbs(), 0);
7768     TEXT_FILE_COMPARE("output.txt", relativeProductBuildDir("the-product") + "/output.txt");
7769     TEXT_FILE_COMPARE("output.bin", relativeProductBuildDir("the-product") + "/output.bin");
7770 }
7771 
wildCardsAndRules()7772 void TestBlackbox::wildCardsAndRules()
7773 {
7774     QDir::setCurrent(testDataDir + "/wildcards-and-rules");
7775     QCOMPARE(runQbs(), 0);
7776     QVERIFY(m_qbsStdout.contains("Creating output artifact"));
7777     QFile output(relativeProductBuildDir("wildcards-and-rules") + "/test.mytype");
7778     QVERIFY2(output.open(QIODevice::ReadOnly), qPrintable(output.errorString()));
7779     QCOMPARE(output.readAll().count('\n'), 1);
7780     output.close();
7781 
7782     // Add input.
7783     WAIT_FOR_NEW_TIMESTAMP();
7784     touch("input2.inp");
7785     QbsRunParameters params;
7786     params.expectFailure = true;
7787     QCOMPARE(runQbs(params), 0);
7788     QVERIFY(m_qbsStdout.contains("Creating output artifact"));
7789     QVERIFY2(output.open(QIODevice::ReadOnly), qPrintable(output.errorString()));
7790     QCOMPARE(output.readAll().count('\n'), 2);
7791     output.close();
7792 
7793     // Add "explicitlyDependsOn".
7794     WAIT_FOR_NEW_TIMESTAMP();
7795     touch("dep.dep");
7796     QCOMPARE(runQbs(), 0);
7797     QVERIFY(m_qbsStdout.contains("Creating output artifact"));
7798 
7799     // Add nothing.
7800     QCOMPARE(runQbs(), 0);
7801     QVERIFY(!m_qbsStdout.contains("Creating output artifact"));
7802 }
7803 
loadableModule()7804 void TestBlackbox::loadableModule()
7805 {
7806     QDir::setCurrent(testDataDir + QLatin1String("/loadablemodule"));
7807 
7808     QCOMPARE(runQbs({"resolve"}), 0);
7809     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7810         QSKIP("Cannot run binaries in cross-compiled build");
7811     QbsRunParameters params;
7812     params.command = "run";
7813     QCOMPARE(runQbs(params), 0);
7814     QVERIFY2(m_qbsStdout.contains("foo = 42"), m_qbsStdout.constData());
7815 }
7816 
localDeployment()7817 void TestBlackbox::localDeployment()
7818 {
7819     QDir::setCurrent(testDataDir + "/localDeployment");
7820     QFile main("main.cpp");
7821     QVERIFY(main.open(QIODevice::ReadOnly));
7822     QByteArray content = main.readAll();
7823     content.replace('\r', "");
7824     QCOMPARE(runQbs({"resolve"}), 0);
7825     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7826         QSKIP("Cannot run binaries in cross-compiled build");
7827 
7828     QbsRunParameters params;
7829     params.command = "run";
7830     QCOMPARE(runQbs(params), 0);
7831     QVERIFY2(m_qbsStdout.contains(content), m_qbsStdout.constData());
7832 }
7833 
makefileGenerator()7834 void TestBlackbox::makefileGenerator()
7835 {
7836     QDir::setCurrent(testDataDir + "/makefile-generator");
7837     const QbsRunParameters params("generate", QStringList{"-g", "makefile"});
7838     QCOMPARE(runQbs(params), 0);
7839     if (HostOsInfo::isWindowsHost())
7840         return;
7841     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7842         QSKIP("Cannot run binaries in cross-compiled build");
7843     QProcess make;
7844     make.setWorkingDirectory(QDir::currentPath() + '/' + relativeBuildDir());
7845     const QString customInstallRoot = QDir::currentPath() + "/my-install-root";
7846     make.start("make", QStringList{"INSTALL_ROOT=" + customInstallRoot, "install"});
7847     QVERIFY(waitForProcessSuccess(make));
7848     QVERIFY(QFile::exists(relativeExecutableFilePath("the app")));
7849     QVERIFY(!QFile::exists(relativeBuildGraphFilePath()));
7850     QProcess app;
7851     app.start(customInstallRoot + "/usr/local/bin/the app", QStringList());
7852     QVERIFY(waitForProcessSuccess(app));
7853     const QByteArray appStdout = app.readAllStandardOutput();
7854     QVERIFY2(appStdout.contains("Hello, World!"), appStdout.constData());
7855     make.start("make", QStringList("clean"));
7856     QVERIFY(waitForProcessSuccess(make));
7857     QVERIFY(!QFile::exists(relativeExecutableFilePath("the app")));
7858 }
7859 
maximumCLanguageVersion()7860 void TestBlackbox::maximumCLanguageVersion()
7861 {
7862     QDir::setCurrent(testDataDir + "/maximum-c-language-version");
7863     QCOMPARE(runQbs(QbsRunParameters("resolve",
7864                                      QStringList("products.app.enableNewestModule:true"))), 0);
7865     if (m_qbsStdout.contains("is msvc"))
7866         QSKIP("MSVC has no support for setting the C language version.");
7867     QCOMPARE(runQbs(QStringList({"--command-echo-mode", "command-line", "-n"})), 0);
7868     QVERIFY2(m_qbsStdout.contains("c11") || m_qbsStdout.contains("c1x"), m_qbsStdout.constData());
7869     QCOMPARE(runQbs(QbsRunParameters("resolve",
7870                                      QStringList("products.app.enableNewestModule:false"))), 0);
7871     QCOMPARE(runQbs(QStringList({"--command-echo-mode", "command-line", "-n"})), 0);
7872     QVERIFY2(m_qbsStdout.contains("c99"), m_qbsStdout.constData());
7873 }
7874 
maximumCxxLanguageVersion()7875 void TestBlackbox::maximumCxxLanguageVersion()
7876 {
7877     QDir::setCurrent(testDataDir + "/maximum-cxx-language-version");
7878     QCOMPARE(runQbs(QbsRunParameters("resolve",
7879                                      QStringList("products.app.enableNewestModule:true"))), 0);
7880     QCOMPARE(runQbs(QStringList({"--command-echo-mode", "command-line", "-n"})), 0);
7881     QVERIFY2(m_qbsStdout.contains("c++23") || m_qbsStdout.contains("c++2b")
7882              || m_qbsStdout.contains("c++latest"), m_qbsStdout.constData());
7883     QCOMPARE(runQbs(QbsRunParameters("resolve",
7884                                      QStringList("products.app.enableNewestModule:false"))), 0);
7885     QCOMPARE(runQbs(QStringList({"--command-echo-mode", "command-line", "-n"})), 0);
7886     QVERIFY2(m_qbsStdout.contains("c++14") || m_qbsStdout.contains("c++1y"),
7887              m_qbsStdout.constData());
7888 }
7889 
moduleProviders()7890 void TestBlackbox::moduleProviders()
7891 {
7892     QDir::setCurrent(testDataDir + "/module-providers");
7893 
7894     // Resolving in dry-run mode must not leave any data behind.
7895     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("-n"))), 0);
7896     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
7897         QSKIP("Cannot run binaries in cross-compiled build");
7898     QCOMPARE(m_qbsStdout.count("Running setup script for mygenerator"), 2);
7899     QVERIFY(!QFile::exists(relativeBuildDir()));
7900 
7901     // Initial build.
7902     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app1"})), 0);
7903     QVERIFY(QFile::exists(relativeBuildDir()));
7904     QCOMPARE(m_qbsStdout.count("Running setup script for mygenerator"), 2);
7905     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7906     QVERIFY2(m_qbsStdout.contains("The MY_DEFINE is app1"), m_qbsStdout.constData());
7907     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app2"})), 0);
7908     QVERIFY2(m_qbsStdout.contains("The letters are Z and Y"), m_qbsStdout.constData());
7909     QVERIFY2(m_qbsStdout.contains("The MY_DEFINE is app2"), m_qbsStdout.constData());
7910 
7911     // Rebuild with overridden module provider config. The output for product 2 must change,
7912     // but no setup script must be re-run, because both config values have already been
7913     // handled in the first run.
7914     const QStringList resolveArgs("moduleProviders.mygenerator.chooseLettersFrom:beginning");
7915     QCOMPARE(runQbs(QbsRunParameters("resolve", resolveArgs)), 0);
7916     QVERIFY2(!m_qbsStdout.contains("Running setup script"), m_qbsStdout.constData());
7917     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app1"})), 0);
7918     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7919     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app2"})), 0);
7920     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7921 
7922     // Forcing Probe execution triggers a re-run of the setup script. But only once,
7923     // because the module provider config is the same now.
7924     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList(resolveArgs)
7925                                      << "--force-probe-execution")), 0);
7926     QCOMPARE(m_qbsStdout.count("Running setup script for mygenerator"), 1);
7927     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app1"})), 0);
7928     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7929     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app2"})), 0);
7930     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7931 
7932     // Now re-run without the module provider config override. Again, the setup script must
7933     // run once, for the config value that was not present in the last run.
7934     QCOMPARE(runQbs(QbsRunParameters("resolve")), 0);
7935     QCOMPARE(m_qbsStdout.count("Running setup script for mygenerator"), 1);
7936     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app1"})), 0);
7937     QVERIFY2(m_qbsStdout.contains("The letters are A and B"), m_qbsStdout.constData());
7938     QCOMPARE(runQbs(QbsRunParameters("run", QStringList{"-p", "app2"})), 0);
7939     QVERIFY2(m_qbsStdout.contains("The letters are Z and Y"), m_qbsStdout.constData());
7940 }
7941 
fallbackModuleProvider_data()7942 void TestBlackbox::fallbackModuleProvider_data()
7943 {
7944     QTest::addColumn<bool>("fallbacksEnabledGlobally");
7945     QTest::addColumn<bool>("fallbacksEnabledInProduct");
7946     QTest::addColumn<QStringList>("pkgConfigLibDirs");
7947     QTest::addColumn<bool>("successExpected");
7948     QTest::newRow("without custom lib dir, fallbacks disabled globally and in product")
7949             << false << false << QStringList() << false;
7950     QTest::newRow("without custom lib dir, fallbacks disabled globally, enabled in product")
7951             << false << true << QStringList() << false;
7952     QTest::newRow("without custom lib dir, fallbacks enabled globally, disabled in product")
7953             << true << false << QStringList() << false;
7954     QTest::newRow("without custom lib dir, fallbacks enabled globally and in product")
7955             << true << true << QStringList() << false;
7956     QTest::newRow("with custom lib dir, fallbacks disabled globally and in product")
7957             << false << false << QStringList(testDataDir + "/fallback-module-provider/libdir")
7958             << false;
7959     QTest::newRow("with custom lib dir, fallbacks disabled globally, enabled in product")
7960             << false << true << QStringList(testDataDir + "/fallback-module-provider/libdir")
7961             << false;
7962     QTest::newRow("with custom lib dir, fallbacks enabled globally, disabled in product")
7963             << true << false << QStringList(testDataDir + "/fallback-module-provider/libdir")
7964             << false;
7965     QTest::newRow("with custom lib dir, fallbacks enabled globally and in product")
7966             << true << true << QStringList(testDataDir + "/fallback-module-provider/libdir")
7967             << true;
7968 }
7969 
fallbackModuleProvider()7970 void TestBlackbox::fallbackModuleProvider()
7971 {
7972     QFETCH(bool, fallbacksEnabledInProduct);
7973     QFETCH(bool, fallbacksEnabledGlobally);
7974     QFETCH(QStringList, pkgConfigLibDirs);
7975     QFETCH(bool, successExpected);
7976 
7977     QDir::setCurrent(testDataDir + "/fallback-module-provider");
7978     static const auto b2s = [](bool b) { return QString(b ? "true" : "false"); };
7979     QbsRunParameters resolveParams("resolve",
7980         QStringList{"modules.pkgconfig.libDirs:" + pkgConfigLibDirs.join(','),
7981                     "products.p.fallbacksEnabled:" + b2s(fallbacksEnabledInProduct),
7982                     "--force-probe-execution"});
7983     if (!fallbacksEnabledGlobally)
7984         resolveParams.arguments << "--no-fallback-module-provider";
7985     QCOMPARE(runQbs(resolveParams), 0);
7986     const bool pkgConfigPresent = m_qbsStdout.contains("pkg-config present: true");
7987     const bool pkgConfigNotPresent = m_qbsStdout.contains("pkg-config present: false");
7988     QVERIFY(pkgConfigPresent != pkgConfigNotPresent);
7989     if (pkgConfigNotPresent)
7990         successExpected = false;
7991     QbsRunParameters buildParams;
7992     buildParams.expectFailure = !successExpected;
7993     QCOMPARE(runQbs(buildParams) == 0, successExpected);
7994 }
7995 
minimumSystemVersion()7996 void TestBlackbox::minimumSystemVersion()
7997 {
7998     rmDirR(relativeBuildDir());
7999     QDir::setCurrent(testDataDir + "/minimumSystemVersion");
8000     QFETCH(QString, file);
8001     QFETCH(QString, output);
8002     QbsRunParameters params({ "-f", file + ".qbs" });
8003     params.command = "resolve";
8004     QCOMPARE(runQbs(params), 0);
8005     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
8006         QSKIP("Cannot run binaries in cross-compiled build");
8007     params.command = "run";
8008     QCOMPARE(runQbs(params), 0);
8009     if (m_qbsStdout.contains("Unsupported compiler"))
8010         QSKIP("Unsupported compiler");
8011     if (!m_qbsStdout.contains(output.toUtf8())) {
8012         qDebug() << "expected output:" << qPrintable(output);
8013         qDebug() << "actual output:" << m_qbsStdout.constData();
8014     }
8015     QVERIFY(m_qbsStdout.contains(output.toUtf8()));
8016 }
8017 
fromMinimumDeploymentTargetValue(int v,bool isMacOS)8018 static qbs::Version fromMinimumDeploymentTargetValue(int v, bool isMacOS)
8019 {
8020     if (isMacOS && v < 100000)
8021         return qbs::Version(v / 100, v / 10 % 10, v % 10);
8022     return qbs::Version(v / 10000, v / 100 % 100, v % 100);
8023 }
8024 
toMinimumDeploymentTargetValue(const qbs::Version & v,bool isMacOS)8025 static int toMinimumDeploymentTargetValue(const qbs::Version &v, bool isMacOS)
8026 {
8027     if (isMacOS && v < qbs::Version(10, 10))
8028         return (v.majorVersion() * 100) + (v.minorVersion() * 10) + v.patchLevel();
8029     return (v.majorVersion() * 10000) + (v.minorVersion() * 100) + v.patchLevel();
8030 }
8031 
defaultClangMinimumDeploymentTarget()8032 static qbs::Version defaultClangMinimumDeploymentTarget()
8033 {
8034     QProcess process;
8035     process.start("/usr/bin/xcrun", {"-sdk", "macosx", "clang++",
8036                                      "-target", "x86_64-apple-macosx-macho",
8037                                      "-dM", "-E", "-x", "objective-c++", "/dev/null"});
8038     if (waitForProcessSuccess(process)) {
8039         const auto lines = process.readAllStandardOutput().split('\n');
8040         for (const auto &line : lines) {
8041             static const QByteArray prefix =
8042                 "#define __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ ";
8043             if (line.startsWith(prefix)) {
8044                 bool ok = false;
8045                 int v = line.mid(prefix.size()).trimmed().toInt(&ok);
8046                 if (ok)
8047                     return fromMinimumDeploymentTargetValue(v, true);
8048                 break;
8049             }
8050         }
8051     }
8052 
8053     return qbs::Version();
8054 }
8055 
minimumSystemVersion_data()8056 void TestBlackbox::minimumSystemVersion_data()
8057 {
8058     QTest::addColumn<QString>("file");
8059     QTest::addColumn<QString>("output");
8060 
8061     // Don't check for the full "version X.Y.Z\n" on macOS as some older versions of otool don't
8062     // show the patch version. Instead, simply check for "version X.Y" with no trailing \n.
8063 
8064     const QString unspecified = []() -> QString {
8065         if (HostOsInfo::isMacosHost()) {
8066             const auto v = defaultClangMinimumDeploymentTarget();
8067             auto result = "__MAC_OS_X_VERSION_MIN_REQUIRED="
8068                     + QString::number(toMinimumDeploymentTargetValue(v, true));
8069             if (v >= qbs::Version(10, 14))
8070                 result += "\nminos ";
8071             else
8072                 result += "\nversion ";
8073             result += QString::number(v.majorVersion()) + "." + QString::number(v.minorVersion());
8074             return result;
8075         }
8076 
8077         if (HostOsInfo::isWindowsHost())
8078             return "WINVER is not defined\n";
8079 
8080         return "";
8081     }();
8082 
8083     const QString specific = []() -> QString {
8084         if (HostOsInfo::isMacosHost())
8085             return "__MAC_OS_X_VERSION_MIN_REQUIRED=1070\nversion 10.7\n";
8086 
8087         if (HostOsInfo::isWindowsHost())
8088             return "WINVER=1536\n6.00 operating system version\n6.00 subsystem version\n";
8089 
8090         return "";
8091     }();
8092 
8093     QTest::newRow("unspecified") << "unspecified" << unspecified;
8094     QTest::newRow("unspecified-forced") << "unspecified-forced" << unspecified;
8095     if (HostOsInfo::isWindowsHost() || HostOsInfo::isMacosHost())
8096         QTest::newRow("specific") << "specific" << specific;
8097     if (HostOsInfo::isWindowsHost())
8098         QTest::newRow("fakewindows") << "fakewindows" << "WINVER=1283\n5.03 operating system "
8099                                                          "version\n5.03 subsystem version\n";
8100     if (HostOsInfo::isMacosHost())
8101         QTest::newRow("macappstore") << "macappstore" << "__MAC_OS_X_VERSION_MIN_REQUIRED=1071\n"
8102                                                          "version 10.7";
8103 }
8104 
missingBuildGraph()8105 void TestBlackbox::missingBuildGraph()
8106 {
8107     QTemporaryDir tmpDir;
8108     QVERIFY(tmpDir.isValid());
8109     QDir::setCurrent(tmpDir.path());
8110     QFETCH(QString, configName);
8111     const QStringList commands({"clean", "dump-nodes-tree", "status", "update-timestamps"});
8112     const QString actualConfigName = configName.isEmpty() ? QString("default") : configName;
8113     QbsRunParameters params;
8114     params.expectFailure = true;
8115     params.arguments << QLatin1String("config:") + actualConfigName;
8116     for (const QString &command : qAsConst(commands)) {
8117         params.command = command;
8118         QVERIFY2(runQbs(params) != 0, qPrintable(command));
8119         const QString expectedErrorMessage = QString("Build graph not found for "
8120                                                      "configuration '%1'").arg(actualConfigName);
8121         if (!m_qbsStderr.contains(expectedErrorMessage.toLocal8Bit())) {
8122             qDebug() << command;
8123             qDebug() << expectedErrorMessage;
8124             qDebug() << m_qbsStderr;
8125             QFAIL("unexpected error message");
8126         }
8127     }
8128 }
8129 
missingBuildGraph_data()8130 void TestBlackbox::missingBuildGraph_data()
8131 {
8132     QTest::addColumn<QString>("configName");
8133     QTest::newRow("implicit config name") << QString();
8134     QTest::newRow("explicit config name") << QString("customConfig");
8135 }
8136 
missingDependency()8137 void TestBlackbox::missingDependency()
8138 {
8139     QDir::setCurrent(testDataDir + "/missing-dependency");
8140     QbsRunParameters params;
8141     params.expectFailure = true;
8142     params.arguments << "-p" << "theApp";
8143     QVERIFY(runQbs(params) != 0);
8144     QVERIFY2(!m_qbsStderr.contains("ASSERT"), m_qbsStderr.constData());
8145     QCOMPARE(runQbs(QbsRunParameters(QStringList() << "-p" << "theDep")), 0);
8146     params.expectFailure = false;
8147     params.arguments << "-vv";
8148     QCOMPARE(runQbs(params), 0);
8149     QVERIFY(m_qbsStderr.contains("false positive"));
8150 }
8151 
missingProjectFile()8152 void TestBlackbox::missingProjectFile()
8153 {
8154     QDir::setCurrent(testDataDir + "/missing-project-file/empty-dir");
8155     QbsRunParameters params;
8156     params.expectFailure = true;
8157     QVERIFY(runQbs(params) != 0);
8158     QVERIFY2(m_qbsStderr.contains("No project file given and none found in current directory"),
8159              m_qbsStderr.constData());
8160     QDir::setCurrent(testDataDir + "/missing-project-file");
8161     params.arguments << "-f" << "empty-dir";
8162     QVERIFY(runQbs(params) != 0);
8163     QVERIFY2(m_qbsStderr.contains("No project file found in directory"), m_qbsStderr.constData());
8164     params.arguments = QStringList() << "-f" << "ambiguous-dir";
8165     QVERIFY(runQbs(params) != 0);
8166     QVERIFY2(m_qbsStderr.contains("More than one project file found in directory"),
8167              m_qbsStderr.constData());
8168     params.expectFailure = false;
8169     params.arguments = QStringList() << "-f" << "project-dir";
8170     QCOMPARE(runQbs(params), 0);
8171     WAIT_FOR_NEW_TIMESTAMP();
8172     touch("project-dir/file.cpp");
8173     QCOMPARE(runQbs(), 0);
8174     QVERIFY2(m_qbsStdout.contains("compiling file.cpp"), m_qbsStdout.constData());
8175     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8176 }
8177 
missingOverridePrefix()8178 void TestBlackbox::missingOverridePrefix()
8179 {
8180     QDir::setCurrent(testDataDir + "/missing-override-prefix");
8181     QbsRunParameters params;
8182     params.expectFailure = true;
8183     params.arguments << "blubb.whatever:false";
8184     QVERIFY(runQbs(params) != 0);
8185     QVERIFY2(m_qbsStderr.contains("Property override key 'blubb.whatever' not understood"),
8186              m_qbsStderr.constData());
8187 }
8188 
moduleConditions()8189 void TestBlackbox::moduleConditions()
8190 {
8191     QDir::setCurrent(testDataDir + "/module-conditions");
8192     QCOMPARE(runQbs(), 0);
8193     QCOMPARE(m_qbsStdout.count("loaded m1"), 1);
8194     QCOMPARE(m_qbsStdout.count("loaded m2"), 2);
8195     QCOMPARE(m_qbsStdout.count("loaded m3"), 1);
8196     QCOMPARE(m_qbsStdout.count("loaded m4"), 1);
8197 }
8198 
movedFileDependency()8199 void TestBlackbox::movedFileDependency()
8200 {
8201     QDir::setCurrent(testDataDir + "/moved-file-dependency");
8202     const QString subdir2 = QDir::currentPath() + "/subdir2";
8203     QVERIFY(QDir::current().mkdir(subdir2));
8204     const QString oldHeaderFilePath = QDir::currentPath() + "/subdir1/theheader.h";
8205     const QString newHeaderFilePath = subdir2 + "/theheader.h";
8206     QCOMPARE(runQbs(), 0);
8207     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8208     QCOMPARE(runQbs(), 0);
8209     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8210 
8211     QFile f(oldHeaderFilePath);
8212     QVERIFY2(f.rename(newHeaderFilePath), qPrintable(f.errorString()));
8213     QCOMPARE(runQbs(), 0);
8214     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8215     QCOMPARE(runQbs(), 0);
8216     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8217 
8218     f.setFileName(newHeaderFilePath);
8219     QVERIFY2(f.rename(oldHeaderFilePath), qPrintable(f.errorString()));
8220     QCOMPARE(runQbs(), 0);
8221     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8222     QCOMPARE(runQbs(), 0);
8223     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8224 }
8225 
badInterpreter()8226 void TestBlackbox::badInterpreter()
8227 {
8228     if (!HostOsInfo::isAnyUnixHost())
8229         QSKIP("only applies on Unix");
8230 
8231     QDir::setCurrent(testDataDir + QLatin1String("/badInterpreter"));
8232     QCOMPARE(runQbs(), 0);
8233 
8234     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
8235         QSKIP("Cannot run binaries in cross-compiled build");
8236 
8237     QbsRunParameters params("run");
8238     params.expectFailure = true;
8239 
8240     const QRegularExpression reNoSuchFileOrDir("bad interpreter:.* No such file or directory");
8241     const QRegularExpression rePermissionDenied("bad interpreter:.* Permission denied");
8242 
8243     params.arguments = QStringList() << "-p" << "script-interp-missing";
8244     QCOMPARE(runQbs(params), 1);
8245     QString strerr = QString::fromLocal8Bit(m_qbsStderr);
8246     QVERIFY2(strerr.contains(reNoSuchFileOrDir), m_qbsStderr);
8247 
8248     params.arguments = QStringList() << "-p" << "script-interp-noexec";
8249     QCOMPARE(runQbs(params), 1);
8250     strerr = QString::fromLocal8Bit(m_qbsStderr);
8251     QVERIFY2(strerr.contains(reNoSuchFileOrDir) || strerr.contains(rePermissionDenied)
8252              || strerr.contains("script-noexec: bad interpreter: execve: Exec format error"),
8253              qPrintable(strerr));
8254 
8255     params.arguments = QStringList() << "-p" << "script-noexec";
8256     QCOMPARE(runQbs(params), 1);
8257     QCOMPARE(runQbs(QbsRunParameters("run", QStringList() << "-p" << "script-ok")), 0);
8258 }
8259 
bomSources()8260 void TestBlackbox::bomSources()
8261 {
8262     QDir::setCurrent(testDataDir + "/bom-sources");
8263     const bool success = runQbs() == 0;
8264     if (!success)
8265         QSKIP("Assuming compiler cannot deal with byte order mark");
8266     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8267     WAIT_FOR_NEW_TIMESTAMP();
8268     QCOMPARE(runQbs(), 0);
8269     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8270     touch("theheader.h");
8271     QCOMPARE(runQbs(), 0);
8272     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8273 }
8274 
buildDataOfDisabledProduct()8275 void TestBlackbox::buildDataOfDisabledProduct()
8276 {
8277     QDir::setCurrent(testDataDir + QLatin1String("/build-data-of-disabled-product"));
8278     QCOMPARE(runQbs(), 0);
8279     QVERIFY2(m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8280     QVERIFY2(m_qbsStdout.contains("compiling test.cpp"), m_qbsStdout.constData());
8281 
8282     // Touch a source file, disable the product, rebuild the project, verify nothing happens.
8283     WAIT_FOR_NEW_TIMESTAMP();
8284     touch("test.cpp");
8285     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("products.app.condition:false"))), 0);
8286     QCOMPARE(runQbs(), 0);
8287     QVERIFY2(!m_qbsStdout.contains("compiling"), m_qbsStdout.constData());
8288     QVERIFY2(!m_qbsStdout.contains("linking"), m_qbsStdout.constData());
8289 
8290     // Enable the product again, rebuild the project, verify that only the changed source file
8291     // is rebuilt.
8292     QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList("products.app.condition:true"))), 0);
8293     QCOMPARE(runQbs(), 0);
8294     QVERIFY2(!m_qbsStdout.contains("compiling main.cpp"), m_qbsStdout.constData());
8295     QVERIFY2(m_qbsStdout.contains("compiling test.cpp"), m_qbsStdout.constData());
8296 }
8297 
qbsVersion()8298 void TestBlackbox::qbsVersion()
8299 {
8300     const auto v = qbs::LanguageInfo::qbsVersion();
8301     QDir::setCurrent(testDataDir + QLatin1String("/qbsVersion"));
8302     QbsRunParameters params;
8303     params.arguments = QStringList()
8304             << "project.qbsVersion:" + v.toString()
8305             << "project.qbsVersionMajor:" + QString::number(v.majorVersion())
8306             << "project.qbsVersionMinor:" + QString::number(v.minorVersion())
8307             << "project.qbsVersionPatch:" + QString::number(v.patchLevel());
8308     QCOMPARE(runQbs(params), 0);
8309 
8310     params.arguments.push_back("project.qbsVersionPatch:" + QString::number(v.patchLevel() + 1));
8311     params.expectFailure = true;
8312     QVERIFY(runQbs(params) != 0);
8313 }
8314 
transitiveInvalidDependencies()8315 void TestBlackbox::transitiveInvalidDependencies()
8316 {
8317     QDir::setCurrent(testDataDir + "/transitive-invalid-dependencies");
8318     QbsRunParameters params;
8319     QCOMPARE(runQbs(params), 0);
8320     QVERIFY2(m_qbsStdout.contains("b.present = false"), m_qbsStdout);
8321     QVERIFY2(m_qbsStdout.contains("c.present = true"), m_qbsStdout);
8322     QVERIFY2(m_qbsStdout.contains("d.present = false"), m_qbsStdout);
8323 }
8324 
transitiveOptionalDependencies()8325 void TestBlackbox::transitiveOptionalDependencies()
8326 {
8327     QDir::setCurrent(testDataDir + "/transitive-optional-dependencies");
8328     QbsRunParameters params;
8329     QCOMPARE(runQbs(params), 0);
8330 }
8331 
groupsInModules()8332 void TestBlackbox::groupsInModules()
8333 {
8334     QDir::setCurrent(testDataDir + "/groups-in-modules");
8335     QCOMPARE(runQbs({"resolve"}), 0);
8336     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
8337         QSKIP("Cannot run binaries in cross-compiled build");
8338     QbsRunParameters params;
8339     QCOMPARE(runQbs(params), 0);
8340     QVERIFY(m_qbsStdout.contains("compile rock.coal => rock.diamond"));
8341     QVERIFY(m_qbsStdout.contains("compile chunk.coal => chunk.diamond"));
8342     QVERIFY(m_qbsStdout.contains("compiling helper2.c"));
8343     QVERIFY(!m_qbsStdout.contains("compiling helper3.c"));
8344     QVERIFY(m_qbsStdout.contains("compiling helper4.c"));
8345     QVERIFY(m_qbsStdout.contains("compiling helper5.c"));
8346     QVERIFY(!m_qbsStdout.contains("compiling helper6.c"));
8347 
8348     QCOMPARE(runQbs(params), 0);
8349     QVERIFY(!m_qbsStdout.contains("compile rock.coal => rock.diamond"));
8350     QVERIFY(!m_qbsStdout.contains("compile chunk.coal => chunk.diamond"));
8351 
8352     WAIT_FOR_NEW_TIMESTAMP();
8353     touch("modules/helper/diamondc.c");
8354 
8355     waitForFileUnlock();
8356     QCOMPARE(runQbs(params), 0);
8357     QVERIFY(m_qbsStdout.contains("compiling diamondc.c"));
8358     QVERIFY(m_qbsStdout.contains("compile rock.coal => rock.diamond"));
8359     QVERIFY(m_qbsStdout.contains("compile chunk.coal => chunk.diamond"));
8360     QVERIFY(regularFileExists(relativeProductBuildDir("groups-in-modules") + "/rock.diamond"));
8361     QFile output(relativeProductBuildDir("groups-in-modules") + "/rock.diamond");
8362     QVERIFY(output.open(QIODevice::ReadOnly));
8363     QCOMPARE(output.readAll().trimmed(), QByteArray("diamond"));
8364 }
8365 
grpc_data()8366 void TestBlackbox::grpc_data()
8367 {
8368     QTest::addColumn<QString>("projectFile");
8369     QTest::addColumn<QStringList>("arguments");
8370     QTest::addColumn<bool>("hasModules");
8371 
8372     QTest::newRow("cpp") << QString("grpc_cpp.qbs") << QStringList() << false;
8373 
8374     QStringList pkgConfigArgs("project.qbsModuleProviders:qbspkgconfig");
8375     // on macOS, openSSL is hidden from pkg-config by default
8376     if (qbs::Internal::HostOsInfo::isMacosHost()) {
8377         pkgConfigArgs
8378             << "moduleProviders.qbspkgconfig.extraPaths:/usr/local/opt/openssl@1.1/lib/pkgconfig";
8379     }
8380     QTest::newRow("cpp-pkgconfig") << QString("grpc_cpp.qbs") << pkgConfigArgs << true;
8381 }
8382 
grpc()8383 void TestBlackbox::grpc()
8384 {
8385     QDir::setCurrent(testDataDir + "/grpc");
8386     QFETCH(QString, projectFile);
8387     QFETCH(QStringList, arguments);
8388     QFETCH(bool, hasModules);
8389 
8390     rmDirR(relativeBuildDir());
8391     QbsRunParameters resolveParams("resolve", QStringList{"-f", projectFile});
8392     resolveParams.arguments << arguments;
8393     QCOMPARE(runQbs(resolveParams), 0);
8394     const bool withGrpc = m_qbsStdout.contains("has grpc: true");
8395     const bool withoutGrpc = m_qbsStdout.contains("has grpc: false");
8396     QVERIFY2(withGrpc || withoutGrpc, m_qbsStdout.constData());
8397     if (withoutGrpc)
8398         QSKIP("grpc module not present");
8399 
8400     const bool hasMods = m_qbsStdout.contains("has modules: true");
8401     const bool dontHaveMods = m_qbsStdout.contains("has modules: false");
8402     QVERIFY2(hasMods == !dontHaveMods, m_qbsStdout.constData());
8403     QCOMPARE(hasMods, hasModules);
8404 
8405     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
8406         QSKIP("Cannot run binaries in cross-compiled build");
8407 
8408     QbsRunParameters runParams;
8409     QCOMPARE(runQbs(runParams), 0);
8410 }
8411 
hostOsProperties()8412 void TestBlackbox::hostOsProperties()
8413 {
8414     QDir::setCurrent(testDataDir + "/host-os-properties");
8415     QCOMPARE(runQbs(QStringLiteral("resolve")), 0);
8416     if (m_qbsStdout.contains("targetPlatform differs from hostPlatform"))
8417         QSKIP("Cannot run binaries in cross-compiled build");
8418     QCOMPARE(runQbs(QStringLiteral("run")), 0);
8419     QVERIFY2(m_qbsStdout.contains(
8420                  ("HOST_ARCHITECTURE = " + HostOsInfo::hostOSArchitecture()).data()),
8421              m_qbsStdout.constData());
8422     QVERIFY2(m_qbsStdout.contains(("HOST_PLATFORM = " + HostOsInfo::hostOSIdentifier()).data()),
8423              m_qbsStdout.constData());
8424 }
8425 
ico()8426 void TestBlackbox::ico()
8427 {
8428     QDir::setCurrent(testDataDir + "/ico");
8429     QbsRunParameters params;
8430     params.expectFailure = true;
8431     params.arguments << "--command-echo-mode" << "command-line";
8432     const int status = runQbs(params);
8433     if (status != 0) {
8434         if (m_qbsStderr.contains("Could not find icotool in any of the following locations:"))
8435             QSKIP("icotool is not installed");
8436         if (!m_qbsStderr.isEmpty())
8437             qDebug("%s", m_qbsStderr.constData());
8438         if (!m_qbsStdout.isEmpty())
8439             qDebug("%s", m_qbsStdout.constData());
8440     }
8441     QCOMPARE(status, 0);
8442 
8443     QVERIFY(QFileInfo::exists(relativeProductBuildDir("icon") + "/icon.ico"));
8444     {
8445         QFile f(relativeProductBuildDir("icon") + "/icon.ico");
8446         QVERIFY(f.open(QIODevice::ReadOnly));
8447         const auto b = f.readAll().toStdString();
8448         QCOMPARE(b.at(2), '\x1'); // icon
8449         QCOMPARE(b.at(4), '\x2'); // 2 images
8450         QVERIFY(b.find("\x89PNG") == std::string::npos);
8451     }
8452 
8453     QVERIFY(QFileInfo::exists(relativeProductBuildDir("icon-alpha") + "/icon-alpha.ico"));
8454     {
8455         QFile f(relativeProductBuildDir("icon-alpha") + "/icon-alpha.ico");
8456         QVERIFY(f.open(QIODevice::ReadOnly));
8457         const auto b = f.readAll().toStdString();
8458         QCOMPARE(b.at(2), '\x1'); // icon
8459         QCOMPARE(b.at(4), '\x2'); // 2 images
8460         QVERIFY(b.find("\x89PNG") == std::string::npos);
8461         QVERIFY2(m_qbsStdout.contains("--alpha-threshold="), m_qbsStdout.constData());
8462     }
8463 
8464     QVERIFY(QFileInfo::exists(relativeProductBuildDir("icon-big") + "/icon-big.ico"));
8465     {
8466         QFile f(relativeProductBuildDir("icon-big") + "/icon-big.ico");
8467         QVERIFY(f.open(QIODevice::ReadOnly));
8468         const auto b = f.readAll().toStdString();
8469         QCOMPARE(b.at(2), '\x1'); // icon
8470         QCOMPARE(b.at(4), '\x5'); // 5 images
8471         QVERIFY(b.find("\x89PNG") != std::string::npos);
8472     }
8473 
8474     QVERIFY(QFileInfo::exists(relativeProductBuildDir("cursor") + "/cursor.cur"));
8475     {
8476         QFile f(relativeProductBuildDir("cursor") + "/cursor.cur");
8477         QVERIFY(f.open(QIODevice::ReadOnly));
8478         const auto b = f.readAll();
8479         QVERIFY(b.size() > 0);
8480         QCOMPARE(b.at(2), '\x2'); // cursor
8481         QCOMPARE(b.at(4), '\x2'); // 2 images
8482         QCOMPARE(b.at(10), '\0');
8483         QCOMPARE(b.at(12), '\0');
8484         QCOMPARE(b.at(26), '\0');
8485         QCOMPARE(b.at(28), '\0');
8486     }
8487 
8488     QVERIFY(QFileInfo::exists(relativeProductBuildDir("cursor-hotspot") + "/cursor-hotspot.cur"));
8489     {
8490         QFile f(relativeProductBuildDir("cursor-hotspot") + "/cursor-hotspot.cur");
8491         QVERIFY(f.open(QIODevice::ReadOnly));
8492         const auto b = f.readAll();
8493         QVERIFY(b.size() > 0);
8494         QCOMPARE(b.at(2), '\x2'); // cursor
8495         QCOMPARE(b.at(4), '\x2'); // 2 images
8496         const bool hasCursorHotspotBug = m_qbsStderr.contains(
8497                                                               "does not support setting the hotspot for cursor files with multiple images");
8498         if (hasCursorHotspotBug) {
8499             QCOMPARE(b.at(10), '\0');
8500             QCOMPARE(b.at(12), '\0');
8501             QCOMPARE(b.at(26), '\0');
8502             QCOMPARE(b.at(28), '\0');
8503             QWARN("this version of icoutil does not support setting the hotspot "
8504                   "for cursor files with multiple images");
8505         } else {
8506             QCOMPARE(b.at(10), '\x8');
8507             QCOMPARE(b.at(12), '\x9');
8508             QCOMPARE(b.at(26), '\x10');
8509             QCOMPARE(b.at(28), '\x11');
8510         }
8511     }
8512 
8513     QVERIFY(QFileInfo::exists(relativeProductBuildDir("cursor-hotspot-single")
8514                               + "/cursor-hotspot-single.cur"));
8515     {
8516         QFile f(relativeProductBuildDir("cursor-hotspot-single") + "/cursor-hotspot-single.cur");
8517         QVERIFY(f.open(QIODevice::ReadOnly));
8518         const auto b = f.readAll();
8519         QVERIFY(b.size() > 0);
8520         QCOMPARE(b.at(2), '\x2'); // cursor
8521         QCOMPARE(b.at(4), '\x1'); // 1 image
8522 
8523         // No version check needed because the hotspot can always be set if there's only one image
8524         QCOMPARE(b.at(10), '\x8');
8525         QCOMPARE(b.at(12), '\x9');
8526     }
8527 
8528     QVERIFY(QFileInfo::exists(relativeProductBuildDir("iconset") + "/dmg.ico"));
8529     {
8530         QFile f(relativeProductBuildDir("iconset") + "/dmg.ico");
8531         QVERIFY(f.open(QIODevice::ReadOnly));
8532         const auto b = f.readAll();
8533         QVERIFY(b.size() > 0);
8534         QCOMPARE(b.at(2), '\x1'); // icon
8535         QCOMPARE(b.at(4), '\x5'); // 5 images
8536     }
8537 
8538     QVERIFY(QFileInfo::exists(relativeProductBuildDir("iconset") + "/dmg.cur"));
8539     {
8540         QFile f(relativeProductBuildDir("iconset") + "/dmg.cur");
8541         QVERIFY(f.open(QIODevice::ReadOnly));
8542         const auto b = f.readAll();
8543         QVERIFY(b.size() > 0);
8544         QCOMPARE(b.at(2), '\x2'); // cursor
8545         QCOMPARE(b.at(4), '\x5'); // 5 images
8546         QCOMPARE(b.at(10), '\0');
8547         QCOMPARE(b.at(12), '\0');
8548         QCOMPARE(b.at(26), '\0');
8549         QCOMPARE(b.at(28), '\0');
8550     }
8551 }
8552 
importAssignment()8553 void TestBlackbox::importAssignment()
8554 {
8555     QDir::setCurrent(testDataDir + "/import-assignment");
8556     QCOMPARE(runQbs(QStringList("project.qbsSearchPaths:" + QDir::currentPath())), 0);
8557     QVERIFY2(m_qbsStdout.contains("key 1 = value1") && m_qbsStdout.contains("key 2 = value2"),
8558              m_qbsStdout.constData());
8559 }
8560 
importChangeTracking()8561 void TestBlackbox::importChangeTracking()
8562 {
8563     QDir::setCurrent(testDataDir + "/import-change-tracking");
8564     QCOMPARE(runQbs(QStringList({"-f", "import-change-tracking.qbs"})), 0);
8565     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8566     QVERIFY2(m_qbsStdout.contains("running probe1"), m_qbsStdout.constData());
8567     QVERIFY2(m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8568     QVERIFY2(m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8569     QVERIFY2(m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8570     QVERIFY2(m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8571     QVERIFY2(m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8572 
8573     // Change in imported file that is not used in any rule or command.
8574     WAIT_FOR_NEW_TIMESTAMP();
8575     touch("irrelevant.js");
8576     QCOMPARE(runQbs(), 0);
8577     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8578     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8579     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8580     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8581     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8582     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8583     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8584 
8585     // Change in directly imported file only used by one prepare script.
8586     WAIT_FOR_NEW_TIMESTAMP();
8587     touch("custom1prepare1.js");
8588     QCOMPARE(runQbs(), 0);
8589     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8590     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8591     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8592     QVERIFY2(m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8593     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8594     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8595     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8596 
8597     // Change in recursively imported file only used by one prepare script.
8598     WAIT_FOR_NEW_TIMESTAMP();
8599     touch("custom1prepare2.js");
8600     QCOMPARE(runQbs(), 0);
8601     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8602     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8603     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8604     QVERIFY2(m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8605     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8606     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8607     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8608 
8609     // Change in imported file used only by one command.
8610     WAIT_FOR_NEW_TIMESTAMP();
8611     touch("custom1command.js");
8612     QCOMPARE(runQbs(), 0);
8613     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8614     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8615     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8616     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8617     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8618     QVERIFY2(m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8619     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8620 
8621     // Change in file only used by one prepare script, using directory import.
8622     WAIT_FOR_NEW_TIMESTAMP();
8623     touch("custom2prepare/custom2prepare2.js");
8624     QCOMPARE(runQbs(), 0);
8625     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8626     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8627     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8628     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8629     QVERIFY2(m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8630     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8631     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8632 
8633     // Change in file used only by one command, imported via search path.
8634     WAIT_FOR_NEW_TIMESTAMP();
8635     touch("imports/custom2command/custom2command1.js");
8636     QCOMPARE(runQbs(), 0);
8637     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8638     QVERIFY2(!m_qbsStdout.contains("running probe1 "), m_qbsStdout.constData());
8639     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8640     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8641     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8642     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8643     QVERIFY2(m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8644 
8645     // Change in directly imported file only used by one Probe
8646     WAIT_FOR_NEW_TIMESTAMP();
8647     touch("probe1.js");
8648     QCOMPARE(runQbs(), 0);
8649     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8650     QVERIFY2(m_qbsStdout.contains("running probe1"), m_qbsStdout.constData());
8651     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8652     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8653     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8654     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8655     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8656 
8657     // Change in indirectly imported file only used by one Probe
8658     WAIT_FOR_NEW_TIMESTAMP();
8659     touch("probe2.js");
8660     QCOMPARE(runQbs(), 0);
8661     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8662     QVERIFY2(m_qbsStdout.contains("running probe1"), m_qbsStdout.constData());
8663     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8664     QVERIFY2(!m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8665     QVERIFY2(!m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8666     QVERIFY2(!m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8667     QVERIFY2(!m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8668 
8669     // Change everything at once.
8670     WAIT_FOR_NEW_TIMESTAMP();
8671     touch("irrelevant.js");
8672     touch("custom1prepare1.js");
8673     touch("custom1prepare2.js");
8674     touch("custom1command.js");
8675     touch("custom2prepare/custom2prepare1.js");
8676     touch("imports/custom2command/custom2command2.js");
8677     touch("probe2.js");
8678     QCOMPARE(runQbs(), 0);
8679     QVERIFY2(m_qbsStdout.contains("Resolving"), m_qbsStdout.constData());
8680     QVERIFY2(m_qbsStdout.contains("running probe1"), m_qbsStdout.constData());
8681     QVERIFY2(!m_qbsStdout.contains("running probe2"), m_qbsStdout.constData());
8682     QVERIFY2(m_qbsStdout.contains("running custom1 prepare script"), m_qbsStdout.constData());
8683     QVERIFY2(m_qbsStdout.contains("running custom2 prepare script"), m_qbsStdout.constData());
8684     QVERIFY2(m_qbsStdout.contains("running custom1 command"), m_qbsStdout.constData());
8685     QVERIFY2(m_qbsStdout.contains("running custom2 command"), m_qbsStdout.constData());
8686 }
8687 
probesInNestedModules()8688 void TestBlackbox::probesInNestedModules()
8689 {
8690     QDir::setCurrent(testDataDir + "/probes-in-nested-modules");
8691     QbsRunParameters params;
8692     QCOMPARE(runQbs(params), 0);
8693 
8694     QCOMPARE(m_qbsStdout.count("running probe a"), 1);
8695     QCOMPARE(m_qbsStdout.count("running probe b"), 1);
8696     QCOMPARE(m_qbsStdout.count("running probe c"), 1);
8697     QCOMPARE(m_qbsStdout.count("running second probe a"), 1);
8698 
8699     QVERIFY(m_qbsStdout.contains("product a, outer.somethingElse = goodbye"));
8700     QVERIFY(m_qbsStdout.contains("product b, inner.something = hahaha"));
8701     QVERIFY(m_qbsStdout.contains("product c, inner.something = hello"));
8702 
8703     QVERIFY(m_qbsStdout.contains("product a, inner.something = hahaha"));
8704     QVERIFY(m_qbsStdout.contains("product a, outer.something = hahaha"));
8705 }
8706 
8707 QTEST_MAIN(TestBlackbox)
8708