1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qbs.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 #ifndef QBS_TEST_SHARED_H
29 #define QBS_TEST_SHARED_H
30 
31 #include <tools/hostosinfo.h>
32 #include <tools/profile.h>
33 #include <tools/settings.h>
34 #include <tools/toolchains.h>
35 
36 #include <QtCore/qbytearray.h>
37 #include <QtCore/qcryptographichash.h>
38 #include <QtCore/qdatetime.h>
39 #include <QtCore/qdebug.h>
40 #include <QtCore/qdir.h>
41 #include <QtCore/qfile.h>
42 #include <QtCore/qfileinfo.h>
43 #include <QtCore/qstring.h>
44 #include <QtCore/qtemporaryfile.h>
45 
46 #include <QtTest/qtest.h>
47 
48 #include <memory>
49 
50 
51 
52 #define REPLACE_IN_FILE(filePath, oldContent, newContent)                           \
53     do {                                                                            \
54         QFile f((filePath));                                                        \
55         QVERIFY2(f.open(QIODevice::ReadWrite), qPrintable(f.errorString()));        \
56         QByteArray content = f.readAll();                                           \
57         const QByteArray savedContent = content;                                    \
58         content.replace((oldContent), (newContent));                                \
59         QVERIFY(content != savedContent);                                           \
60         f.resize(0);                                                                \
61         f.write(content);                                                           \
62     } while (false)
63 
testTimeoutInMsecs()64 inline int testTimeoutInMsecs()
65 {
66     bool ok;
67     int timeoutInSecs = qEnvironmentVariableIntValue("QBS_AUTOTEST_TIMEOUT", &ok);
68     if (!ok)
69         timeoutInSecs = 600;
70     return timeoutInSecs * 1000;
71 }
72 
73 // On Windows, it appears that a lock is sometimes held on files for a short while even after
74 // they are closed. The likelihood for that seems to increase with the slowness of the machine.
waitForFileUnlock()75 inline void waitForFileUnlock()
76 {
77     bool ok;
78     int timeoutInSecs = qEnvironmentVariableIntValue("QBS_AUTOTEST_IO_GRACE_PERIOD", &ok);
79     if (!ok)
80         timeoutInSecs = qbs::Internal::HostOsInfo::isWindowsHost() ? 1 : 0;
81     if (timeoutInSecs > 0)
82         QTest::qWait(timeoutInSecs * 1000);
83 }
84 
85 using SettingsPtr = std::unique_ptr<qbs::Settings>;
settings()86 inline SettingsPtr settings()
87 {
88     const QString settingsDir = QLatin1String(qgetenv("QBS_AUTOTEST_SETTINGS_DIR"));
89     return std::make_unique<qbs::Settings>(settingsDir);
90 }
91 
profileName()92 inline QString profileName()
93 {
94     const QString suiteProfile = QLatin1String(
95                 qgetenv("QBS_AUTOTEST_PROFILE_" QBS_TEST_SUITE_NAME));
96     if (!suiteProfile.isEmpty())
97         return suiteProfile;
98     const QString profile = QLatin1String(qgetenv("QBS_AUTOTEST_PROFILE"));
99     return !profile.isEmpty() ? profile : QLatin1String("none");
100 }
101 
102 inline QString relativeBuildDir(const QString &configurationName = QString())
103 {
104     return !configurationName.isEmpty() ? configurationName : QLatin1String("default");
105 }
106 
107 inline QString relativeBuildGraphFilePath(const QString &configName = QString()) {
108     return relativeBuildDir(configName) + QLatin1Char('/') + relativeBuildDir(configName)
109             + QLatin1String(".bg");
110 }
111 
regularFileExists(const QString & filePath)112 inline bool regularFileExists(const QString &filePath)
113 {
114     const QFileInfo fi(filePath);
115     return fi.exists() && fi.isFile();
116 }
117 
directoryExists(const QString & dirPath)118 inline bool directoryExists(const QString &dirPath)
119 {
120     const QFileInfo fi(dirPath);
121     return fi.exists() && fi.isDir();
122 }
123 
124 struct ReadFileContentResult
125 {
126     QByteArray content;
127     QString errorString;
128 };
129 
readFileContent(const QString & filePath)130 inline ReadFileContentResult readFileContent(const QString &filePath)
131 {
132     ReadFileContentResult result;
133     QFile file(filePath);
134     if (!file.open(QIODevice::ReadOnly)) {
135         result.errorString = file.errorString();
136         return result;
137     }
138     result.content = file.readAll();
139     return result;
140 }
141 
diffText(const QByteArray & actual,const QByteArray & expected)142 inline QByteArray diffText(const QByteArray &actual, const QByteArray &expected)
143 {
144     QByteArray result;
145     QList<QByteArray> actualLines = actual.split('\n');
146     QList<QByteArray> expectedLines = expected.split('\n');
147     int n = 1;
148     while (!actualLines.isEmpty() && !expectedLines.isEmpty()) {
149         QByteArray actualLine = actualLines.takeFirst();
150         QByteArray expectedLine = expectedLines.takeFirst();
151         if (actualLine != expectedLine) {
152             result += QStringLiteral("%1:  actual: %2\n%1:expected: %3\n")
153                     .arg(n, 2)
154                     .arg(QString::fromUtf8(actualLine))
155                     .arg(QString::fromUtf8(expectedLine))
156                     .toUtf8();
157         }
158         n++;
159     }
160     auto addLines = [&result, &n] (const QList<QByteArray> &lines) {
161         for (const QByteArray &line : qAsConst(lines)) {
162             result += QStringLiteral("%1:          %2\n")
163                     .arg(n)
164                     .arg(QString::fromUtf8(line))
165                     .toUtf8();
166             n++;
167         }
168     };
169     if (!actualLines.isEmpty()) {
170         result += "Extra unexpected lines:\n";
171         addLines(actualLines);
172     }
173     if (!expectedLines.isEmpty()) {
174         result += "Missing expected lines:\n";
175         addLines(expectedLines);
176     }
177     return result;
178 }
179 
180 #define READ_TEXT_FILE(filePath, contentVariable)                                                  \
181     QByteArray contentVariable;                                                                    \
182     {                                                                                              \
183         auto c = readFileContent(filePath);                                                        \
184         QVERIFY2(c.errorString.isEmpty(),                                                          \
185                  qUtf8Printable(QStringLiteral("Cannot open file %1. %2")                          \
186                                 .arg(filePath, c.errorString)));                                   \
187         contentVariable = std::move(c.content);                                                    \
188     }
189 
190 #define TEXT_FILE_COMPARE(actualFilePath, expectedFilePath)                                        \
191     {                                                                                              \
192         READ_TEXT_FILE(actualFilePath, ba1);                                                       \
193         READ_TEXT_FILE(expectedFilePath, ba2);                                                     \
194         if (ba1 != ba2) {                                                                          \
195             QByteArray msg = "File contents differ:\n" + diffText(ba1, ba2);                       \
196             QFAIL(msg.constData());                                                                \
197         }                                                                                          \
198     }
199 
200 template <typename T>
prefixedIfNonEmpty(const T & prefix,const QString & str)201 inline QString prefixedIfNonEmpty(const T &prefix, const QString &str)
202 {
203     if (str.isEmpty())
204         return QString();
205     return prefix + str;
206 }
207 
uniqueProductName(const QString & productName,const QString & multiplexConfigurationId)208 inline QString uniqueProductName(const QString &productName,
209                                  const QString &multiplexConfigurationId)
210 {
211     return productName + prefixedIfNonEmpty(QLatin1Char('.'), multiplexConfigurationId);
212 }
213 
214 inline QString relativeProductBuildDir(const QString &productName,
215                                        const QString &configurationName = QString(),
216                                        const QString &multiplexConfigurationId = QString())
217 {
218     const QString fullName = uniqueProductName(productName, multiplexConfigurationId);
219     QString dirName = qbs::Internal::HostOsInfo::rfc1034Identifier(fullName);
220     const QByteArray hash = QCryptographicHash::hash(fullName.toUtf8(), QCryptographicHash::Sha1);
221     dirName.append('.').append(hash.toHex().left(8));
222     return relativeBuildDir(configurationName) + '/' + dirName;
223 }
224 
225 inline QString relativeExecutableFilePath(const QString &productName,
226                                           const QString &configName = QString())
227 {
228     return relativeProductBuildDir(productName, configName) + '/'
229             + qbs::Internal::HostOsInfo::appendExecutableSuffix(productName);
230 }
231 
waitForNewTimestamp(const QString & testDir)232 inline void waitForNewTimestamp(const QString &testDir)
233 {
234     // Waits for the time that corresponds to the host file system's time stamp granularity.
235     if (qbs::Internal::HostOsInfo::isWindowsHost()) {
236         QTest::qWait(1);        // NTFS has 100 ns precision. Let's ignore exFAT.
237     } else {
238         const QString nameTemplate = testDir + "/XXXXXX";
239         QTemporaryFile f1(nameTemplate);
240         if (!f1.open())
241             qFatal("Failed to open temp file");
242         const QDateTime initialTime = QFileInfo(f1).lastModified();
243         int totalMsPassed = 0;
244         while (totalMsPassed <= 2000) {
245             static const int increment = 50;
246             QTest::qWait(increment);
247             totalMsPassed += increment;
248             QTemporaryFile f2(nameTemplate);
249             if (!f2.open())
250                 qFatal("Failed to open temp file");
251             if (QFileInfo(f2).lastModified() > initialTime)
252                 return;
253         }
254         qWarning("Got no new timestamp after two seconds, going ahead anyway. Subsequent "
255                  "test failure might not be genuine.");
256     }
257 }
258 
touch(const QString & fn)259 inline void touch(const QString &fn)
260 {
261     QFile f(fn);
262     int s = f.size();
263     if (!f.open(QFile::ReadWrite))
264         qFatal("cannot open file %s", qPrintable(fn));
265     f.resize(s+1);
266     f.resize(s);
267 }
268 
copyFileAndUpdateTimestamp(const QString & source,const QString & target)269 inline void copyFileAndUpdateTimestamp(const QString &source, const QString &target)
270 {
271     QFile::remove(target);
272     if (!QFile::copy(source, target))
273         qFatal("Failed to copy '%s' to '%s'", qPrintable(source), qPrintable(target));
274     touch(target);
275 }
276 
profileToolchain(const qbs::Profile & profile)277 inline QStringList profileToolchain(const qbs::Profile &profile)
278 {
279     const auto toolchainType = profile.value(QStringLiteral("qbs.toolchainType")).toString();
280     if (!toolchainType.isEmpty())
281         return qbs::canonicalToolchain(toolchainType);
282     return profile.value(QStringLiteral("qbs.toolchain")).toStringList();
283 }
284 
objectFileName(const QString & baseName,const QString & profileName)285 inline QString objectFileName(const QString &baseName, const QString &profileName)
286 {
287     const SettingsPtr s = settings();
288     qbs::Profile profile(profileName, s.get());
289 
290     const auto tcList = profileToolchain(profile);
291     const bool isMsvc = tcList.contains("msvc")
292             || (tcList.isEmpty() && qbs::Internal::HostOsInfo::isWindowsHost());
293     const QString suffix = isMsvc ? "obj" : "o";
294     return baseName + '.' + suffix;
295 }
296 
inputDirHash(const QString & dir)297 inline QString inputDirHash(const QString &dir)
298 {
299     return QCryptographicHash::hash(dir.toLatin1(), QCryptographicHash::Sha1).toHex().left(16);
300 }
301 
testDataSourceDir(const QString & dir)302 inline QString testDataSourceDir(const QString &dir)
303 {
304     QDir result;
305     QString testSourceRootDirFromEnv = QDir::fromNativeSeparators(qEnvironmentVariable("QBS_TEST_SOURCE_ROOT"));
306     if (testSourceRootDirFromEnv.isEmpty()) {
307         result.setPath(dir);
308     } else {
309         QDir testSourceRootDir(dir);
310         while (testSourceRootDir.dirName() != "tests")
311             testSourceRootDir = QFileInfo(testSourceRootDir, "").dir();
312 
313         QString relativeDataPath = testSourceRootDir.relativeFilePath(dir);
314         QString absoluteDataPath = QDir(testSourceRootDirFromEnv).absoluteFilePath(relativeDataPath);
315         result.setPath(absoluteDataPath);
316     }
317 
318     if (!result.exists())
319         qFatal("Expected data folder '%s' to be present, but it does not exist. You may set "
320                "QBS_TEST_SOURCE_ROOT to the 'tests' folder in your qbs repository to configure "
321                "a custom location.", qPrintable(result.absolutePath()));
322 
323     return result.absolutePath();
324 }
325 
testWorkDir(const QString & testName)326 inline QString testWorkDir(const QString &testName)
327 {
328     QString dir = QDir::fromNativeSeparators(QString::fromLocal8Bit(qgetenv("QBS_TEST_WORK_ROOT")));
329     if (dir.isEmpty()) {
330         dir = QCoreApplication::applicationDirPath() + QStringLiteral("/../tests/auto/");
331     } else {
332         if (!dir.endsWith(QLatin1Char('/')))
333             dir += QLatin1Char('/');
334     }
335     return QDir::cleanPath(dir + testName + "/testWorkDir");
336 }
337 
copyDllExportHeader(const QString & srcDataDir,const QString & targetDataDir)338 inline bool copyDllExportHeader(const QString &srcDataDir, const QString &targetDataDir)
339 {
340     QFile sourceFile(srcDataDir + "/../../dllexport.h");
341     if (!sourceFile.exists())
342         return true;
343     const QString targetPath = targetDataDir + "/dllexport.h";
344     QFile::remove(targetPath);
345     return sourceFile.copy(targetPath);
346 }
347 
targetOs()348 inline qbs::Internal::HostOsInfo::HostOs targetOs()
349 {
350     const SettingsPtr s = settings();
351     const qbs::Profile buildProfile(profileName(), s.get());
352     const QString targetPlatform = buildProfile.value("qbs.targetPlatform").toString();
353     if (!targetPlatform.isEmpty()) {
354         const std::vector<std::string> targetOS = qbs::Internal::HostOsInfo::canonicalOSIdentifiers(
355                     targetPlatform.toStdString());
356         if (qbs::Internal::contains(targetOS, "windows"))
357             return qbs::Internal::HostOsInfo::HostOsWindows;
358         if (qbs::Internal::contains(targetOS, "linux"))
359             return qbs::Internal::HostOsInfo::HostOsLinux;
360         if (qbs::Internal::contains(targetOS, "macos"))
361             return qbs::Internal::HostOsInfo::HostOsMacos;
362         if (qbs::Internal::contains(targetOS, "unix"))
363             return qbs::Internal::HostOsInfo::HostOsOtherUnix;
364         return qbs::Internal::HostOsInfo::HostOsOther;
365     }
366     return qbs::Internal::HostOsInfo::hostOs();
367 }
368 
369 #endif // Include guard.
370