1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Copyright (C) 2016 Christian Gagneraud.
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_clangdb.h"
31 
32 #include "../shared.h"
33 
34 #include <tools/hostosinfo.h>
35 #include <tools/installoptions.h>
36 
37 #include <QtCore/qdir.h>
38 #include <QtCore/qfile.h>
39 #include <QtCore/qregularexpression.h>
40 
41 #include <QtCore/qjsonarray.h>
42 #include <QtCore/qjsondocument.h>
43 #include <QtCore/qjsonobject.h>
44 
45 #include <QtTest/qtest.h>
46 
47 using qbs::InstallOptions;
48 using qbs::Internal::HostOsInfo;
49 
runProcess(const QString & exec,const QStringList & args,QByteArray & stdErr,QByteArray & stdOut)50 int TestClangDb::runProcess(const QString &exec, const QStringList &args, QByteArray &stdErr,
51                             QByteArray &stdOut)
52 {
53     QProcess process;
54     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
55     env.insert(processEnvironment);
56     process.setProcessEnvironment(env);
57 
58     process.start(exec, args);
59     const int waitTime = 10 * 60000;
60     if (!process.waitForStarted() || !process.waitForFinished(waitTime)) {
61         stdErr = process.readAllStandardError();
62         return -1;
63     }
64 
65     stdErr = process.readAllStandardError();
66     stdOut = process.readAllStandardOutput();
67     sanitizeOutput(&stdErr);
68     sanitizeOutput(&stdOut);
69 
70     if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
71         if (!stdErr.isEmpty())
72             qDebug("%s", stdErr.constData());
73         if (!stdOut.isEmpty())
74             qDebug("%s", stdOut.constData());
75     }
76 
77     return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
78 }
79 
clangVersion()80 qbs::Version TestClangDb::clangVersion()
81 {
82     QByteArray stdErr;
83     QByteArray stdOut;
84     if (runProcess("clang-check", QStringList("--version"), stdErr, stdOut) != 0)
85         return qbs::Version();
86     stdOut.remove(0, stdOut.indexOf("LLVM version ") + 13);
87     stdOut.truncate(stdOut.indexOf('\n'));
88     return qbs::Version::fromString(QString::fromLocal8Bit(stdOut));
89 }
90 
91 
TestClangDb()92 TestClangDb::TestClangDb() : TestBlackboxBase(SRCDIR "/testdata-clangdb", "blackbox-clangdb"),
93     projectDir(QDir::cleanPath(testDataDir + "/project1")),
94     projectFileName("project.qbs"),
95     buildDir(QDir::cleanPath(projectDir + "/" + relativeBuildDir())),
96     sourceFilePath(QDir::cleanPath(projectDir + "/i like spaces.cpp")),
97     dbFilePath(QDir::cleanPath(buildDir + "/compile_commands.json"))
98 {
99 }
100 
initTestCase()101 void TestClangDb::initTestCase()
102 {
103     TestBlackboxBase::initTestCase();
104     QDir::setCurrent(projectDir);
105 }
106 
ensureBuildTreeCreated()107 void TestClangDb::ensureBuildTreeCreated()
108 {
109     QCOMPARE(runQbs(), 0);
110     QVERIFY(QFile::exists(buildDir));
111 
112     if (m_qbsStdout.contains("is msvc") || m_qbsStdout.contains("is mingw")) {
113         sanitizeOutput(&m_qbsStdout);
114         const auto lines = m_qbsStdout.split('\n');
115         for (const auto &line : lines) {
116             static const QByteArray includeEnv = "INCLUDE=";
117             static const QByteArray libEnv = "LIB=";
118             static const QByteArray pathEnv = "PATH=";
119             if (line.startsWith(includeEnv))
120                 processEnvironment.insert("INCLUDE", line.mid(includeEnv.size()));
121             if (line.startsWith(libEnv))
122                 processEnvironment.insert("LIB", line.mid(libEnv.size()));
123             if (line.startsWith(pathEnv))
124                 processEnvironment.insert("PATH", line.mid(pathEnv.size()));
125         }
126     }
127 }
128 
checkCanGenerateDb()129 void TestClangDb::checkCanGenerateDb()
130 {
131     QbsRunParameters params;
132     params.command = "generate";
133     params.arguments << "--generator" << "clangdb";
134     QCOMPARE(runQbs(params), 0);
135     QVERIFY(QFile::exists(dbFilePath));
136 }
137 
checkDbIsValidJson()138 void TestClangDb::checkDbIsValidJson()
139 {
140     QFile file(dbFilePath);
141     QVERIFY(file.open(QFile::ReadOnly));
142     const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
143     QVERIFY(!doc.isNull());
144     QVERIFY(doc.isArray());
145 }
146 
checkDbIsConsistentWithProject()147 void TestClangDb::checkDbIsConsistentWithProject()
148 {
149     QFile file(dbFilePath);
150     QVERIFY(file.open(QFile::ReadOnly));
151     const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
152 
153     // We expect only one command for now
154     const QJsonArray array = doc.array();
155     QVERIFY(array.size() == 1);
156 
157     // Validate the "command object"
158     const QJsonObject entry = array.at(0).toObject();
159     QVERIFY(entry.contains("directory"));
160     QVERIFY(entry.value("directory").isString());
161     QVERIFY(entry.contains("arguments"));
162     QVERIFY(entry.value("arguments").isArray());
163     QVERIFY(entry.value("arguments").toArray().size() >= 2);
164     QVERIFY(entry.contains("file"));
165     QVERIFY(entry.value("file").isString());
166     QVERIFY(entry.value("file").toString() == sourceFilePath);
167 
168     // Validate the compile command itself, this requires a previous build since the command
169     // line contains 'deep' path that are created during Qbs build run
170     QByteArray stdErr;
171     QByteArray stdOut;
172     QStringList arguments;
173     const QJsonArray jsonArguments = entry.value("arguments").toArray();
174     QString executable = jsonArguments.at(0).toString();
175     for (int i=1; i<jsonArguments.size(); i++)
176         arguments.push_back(jsonArguments.at(i).toString());
177     QVERIFY(runProcess(executable, arguments, stdErr, stdOut) == 0);
178 }
179 
180 // Run clang-check, should give 2 warnings:
181 // <...>/i like spaces.cpp:11:5: warning: Assigned value is garbage or undefined
182 //     int unused = garbage;
183 //     ^~~~~~~~~~   ~~~~~~~
184 // <...>/i like spaces.cpp:11:9: warning: Value stored to 'unused' during its initialization is never read
185 //     int unused = garbage;
186 //         ^~~~~~   ~~~~~~~
187 // 2 warnings generated.
checkClangDetectsSourceCodeProblems()188 void TestClangDb::checkClangDetectsSourceCodeProblems()
189 {
190     QByteArray stdErr;
191     QByteArray stdOut;
192     QStringList arguments;
193     const QString executable = findExecutable(QStringList("clang-check"));
194     if (executable.isEmpty())
195         QSKIP("No working clang-check executable found");
196 
197     // Older clang versions do not support the "arguments" array in the compilation database.
198     // Should we really want to support them, we would have to fall back to "command" instead.
199     if (clangVersion() < qbs::Version(3, 7))
200         QSKIP("This test requires clang-check to be based on at least LLVM 3.7.0.");
201 
202     // clang-check.exe does not understand MSVC command-line syntax
203     const SettingsPtr s = settings();
204     qbs::Profile profile(profileName(), s.get());
205     if (profileToolchain(profile).contains("msvc")) {
206         arguments << "-extra-arg-before=--driver-mode=cl";
207     } else if (profileToolchain(profile).contains("mingw")) {
208         arguments << "-extra-arg-before=--driver-mode=g++";
209     }
210 
211     arguments << "-analyze" << "-p" << relativeBuildDir() << sourceFilePath;
212     QVERIFY(runProcess(executable, arguments, stdErr, stdOut) == 0);
213     const QString output = QString::fromLocal8Bit(stdErr);
214     QVERIFY(output.contains(QRegularExpression(QStringLiteral("warning.*undefined"),
215                                                QRegularExpression::CaseInsensitiveOption)));
216     QVERIFY(output.contains(QRegularExpression(QStringLiteral("warning.*never read"),
217                                                QRegularExpression::CaseInsensitiveOption)));
218 }
219 
220 QTEST_MAIN(TestClangDb)
221