1/****************************************************************************
2**
3** Copyright (C) 2012 Giuseppe D'Angelo <dangelog@gmail.com>
4** Copyright (C) 2016 The Qt Company Ltd.
5** Contact: https://www.qt.io/licensing/
6**
7** This file is part of the test suite of the Qt Toolkit.
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 <QtTest/QtTest>
31#include <QtCore/QString>
32#include <QtCore/QCoreApplication>
33#include <QtCore/QByteArray>
34#include <QtCore/QDir>
35#include <QtCore/QFile>
36#include <QtCore/QProcess>
37#include <QtCore/QDirIterator>
38#include <QtCore/QMap>
39#include <QtCore/QList>
40#include <QtCore/QResource>
41#include <QtCore/QLocale>
42#include <QtCore/QtGlobal>
43
44#include <algorithm>
45
46typedef QMap<QString, QString> QStringMap;
47Q_DECLARE_METATYPE(QStringMap)
48
49static QByteArray msgProcessStartFailed(const QProcess &p)
50{
51 const QString result = QLatin1String("Could not start \"")
52 + QDir::toNativeSeparators(pathName: p.program()) + QLatin1String("\": ")
53 + p.errorString();
54 return result.toLocal8Bit();
55}
56
57static QByteArray msgProcessTimeout(const QProcess &p)
58{
59 return '"' + QDir::toNativeSeparators(pathName: p.program()).toLocal8Bit()
60 + "\" timed out.";
61}
62
63static QByteArray msgProcessCrashed(QProcess &p)
64{
65 return '"' + QDir::toNativeSeparators(pathName: p.program()).toLocal8Bit()
66 + "\" crashed.\n" + p.readAllStandardError();
67}
68
69static QByteArray msgProcessFailed(QProcess &p)
70{
71 return '"' + QDir::toNativeSeparators(pathName: p.program()).toLocal8Bit()
72 + "\" returned " + QByteArray::number(p.exitCode()) + ":\n"
73 + p.readAllStandardError();
74}
75
76class tst_rcc : public QObject
77{
78 Q_OBJECT
79
80private slots:
81 void initTestCase();
82
83 void rcc_data();
84 void rcc();
85
86 void binary_data();
87 void binary();
88
89 void readback_data();
90 void readback();
91
92 void depFileGeneration_data();
93 void depFileGeneration();
94
95 void python();
96
97 void cleanupTestCase();
98
99private:
100 QString m_rcc;
101 QString m_dataPath;
102};
103
104void tst_rcc::initTestCase()
105{
106 // rcc uses a QHash to store files in the resource system.
107 // we must force a certain hash order when testing or tst_rcc will fail, see QTBUG-25078
108 QVERIFY(qputenv("QT_RCC_TEST", "1"));
109 m_rcc = QLibraryInfo::location(QLibraryInfo::BinariesPath) + QLatin1String("/rcc");
110
111 m_dataPath = QFINDTESTDATA("data");
112 QVERIFY(!m_dataPath.isEmpty());
113}
114
115
116static inline bool isPythonComment(const QString &line)
117{
118 return line.startsWith(c: QLatin1Char('#'));
119}
120
121static QString doCompare(const QStringList &actual, const QStringList &expected,
122 const QString &timeStampPath)
123{
124 if (actual.size() != expected.size()) {
125 return QString("Length count different: actual: %1, expected: %2")
126 .arg(a: actual.size()).arg(a: expected.size());
127 }
128
129 QByteArray ba;
130 const bool isPython = isPythonComment(line: expected.constFirst());
131 for (int i = 0, n = expected.size(); i != n; ++i) {
132 QString expectedLine = expected.at(i);
133 if (expectedLine.startsWith(s: "IGNORE:"))
134 continue;
135 if (isPython && isPythonComment(line: expectedLine) && isPythonComment(line: actual.at(i)))
136 continue;
137 if (expectedLine.startsWith(s: "TIMESTAMP:")) {
138 const QString relativePath = expectedLine.mid(position: strlen(s: "TIMESTAMP:"));
139 const QFileInfo fi(timeStampPath + QLatin1Char('/') + relativePath);
140 if (!fi.isFile()) {
141 ba.append(a: "File " + fi.absoluteFilePath().toUtf8() + " does not exist!");
142 break;
143 }
144 const quint64 timeStamp = quint64(fi.lastModified().toMSecsSinceEpoch());
145 expectedLine.clear();
146 for (int shift = 56; shift >= 0; shift -= 8) {
147 expectedLine.append(s: QLatin1String("0x"));
148 expectedLine.append(s: QString::number(quint8(timeStamp >> shift), base: 16));
149 expectedLine.append(c: QLatin1Char(','));
150 }
151 }
152 if (expectedLine != actual.at(i)) {
153 qDebug() << "LINES" << (i + 1) << "DIFFER";
154 ba.append(
155 a: "\n<<<<<< actual\n" + actual.at(i).toUtf8() + "\n======\n" + expectedLine.toUtf8()
156 + "\n>>>>>> expected\n"
157 );
158 }
159 }
160 return ba;
161}
162
163void tst_rcc::rcc_data()
164{
165 QTest::addColumn<QString>(name: "directory");
166 QTest::addColumn<QString>(name: "qrcfile");
167 QTest::addColumn<QString>(name: "expected");
168
169 const QString imagesPath = m_dataPath + QLatin1String("/images");
170 QTest::newRow(dataTag: "images") << imagesPath << "images.qrc" << "images.expected";
171
172 const QString sizesPath = m_dataPath + QLatin1String("/sizes");
173 QTest::newRow(dataTag: "size-0") << sizesPath << "size-0.qrc" << "size-0.expected";
174 QTest::newRow(dataTag: "size-1") << sizesPath << "size-1.qrc" << "size-1.expected";
175 QTest::newRow(dataTag: "size-2-0-35-1") << sizesPath << "size-2-0-35-1.qrc" << "size-2-0-35-1.expected";
176}
177
178static QStringList readLinesFromFile(const QString &fileName,
179 Qt::SplitBehavior splitBehavior)
180{
181 QFile file(fileName);
182
183 bool ok = file.open(flags: QIODevice::ReadOnly | QIODevice::Text);
184 if (!ok) {
185 QWARN(qPrintable(QString::fromLatin1("Could not open testdata file %1: %2")
186 .arg(fileName, file.errorString())));
187 }
188
189 return QString::fromUtf8(str: file.readAll()).split(sep: QLatin1Char('\n'), behavior: splitBehavior);
190}
191
192void tst_rcc::rcc()
193{
194 QFETCH(QString, directory);
195 QFETCH(QString, qrcfile);
196 QFETCH(QString, expected);
197
198 // If the file expectedoutput.txt exists, compare the
199 // console output with the content of that file
200
201 // Launch; force no compression, otherwise the output would be different
202 // depending on the compression algorithm we're using
203 QProcess process;
204 process.setWorkingDirectory(directory);
205 process.start(program: m_rcc, arguments: { "-no-compress", qrcfile });
206 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(process).constData());
207 if (!process.waitForFinished()) {
208 process.kill();
209 QFAIL(msgProcessTimeout(process).constData());
210 }
211 QVERIFY2(process.exitStatus() == QProcess::NormalExit,
212 msgProcessCrashed(process).constData());
213 QVERIFY2(process.exitCode() == 0,
214 msgProcessFailed(process).constData());
215
216 const QChar cr = QLatin1Char('\r');
217 const QString err = QString::fromLocal8Bit(str: process.readAllStandardError()).remove(c: cr);
218 const QString out = QString::fromLatin1(str: process.readAllStandardOutput()).remove(c: cr);
219
220 if (!err.isEmpty()) {
221 qDebug() << "UNEXPECTED STDERR CONTENTS: " << err;
222 QFAIL("UNEXPECTED STDERR CONTENTS");
223 }
224
225 const QChar nl = QLatin1Char('\n');
226 const QStringList actualLines = out.split(sep: nl);
227
228 const QStringList expectedLines =
229 readLinesFromFile(fileName: directory + QLatin1Char('/') + expected, splitBehavior: Qt::KeepEmptyParts);
230 QVERIFY(!expectedLines.isEmpty());
231
232 const QString diff = doCompare(actual: actualLines, expected: expectedLines, timeStampPath: directory);
233 if (diff.size())
234 QFAIL(qPrintable(diff));
235}
236
237static QStringMap readExpectedFiles(const QString &fileName)
238{
239 QStringMap expectedFiles;
240
241 QStringList lines = readLinesFromFile(fileName, splitBehavior: Qt::SkipEmptyParts);
242 foreach (const QString &line, lines) {
243 QString resourceFileName = line.section(asep: QLatin1Char(' '), astart: 0, aend: 0, aflags: QString::SectionSkipEmpty);
244 QString actualFileName = line.section(asep: QLatin1Char(' '), astart: 1, aend: 1, aflags: QString::SectionSkipEmpty);
245 expectedFiles[resourceFileName] = actualFileName;
246 }
247
248 return expectedFiles;
249}
250
251/*
252 The following test looks for all *.qrc files under data/binary/. For each
253 .qrc file found, these files are processed (assuming the file found is
254 called "base.qrc"):
255
256 - base.qrc : processed by rcc; creates base.rcc
257 - base.locale : (optional) list of locales to test, one per line
258 - base.expected : list of pairs (file path in resource, path to real file),
259 one per line; the pair separated by a whitespace; the paths to real files
260 relative to data/binary/ (for testing the C locale)
261 - base.localeName.expected : for each localeName in the base.locale file,
262 as the above .expected file
263*/
264
265void tst_rcc::binary_data()
266{
267 QTest::addColumn<QString>(name: "resourceFile");
268 QTest::addColumn<QLocale>(name: "locale");
269 QTest::addColumn<QString>(name: "baseDirectory");
270 QTest::addColumn<QStringMap>(name: "expectedFiles");
271
272 QString dataPath = m_dataPath + QLatin1String("/binary/");
273
274 QDirIterator iter(dataPath, QStringList() << QLatin1String("*.qrc"));
275 while (iter.hasNext())
276 {
277 iter.next();
278 QFileInfo qrcFileInfo = iter.fileInfo();
279 QString absoluteBaseName = QFileInfo(qrcFileInfo.absolutePath(), qrcFileInfo.baseName()).absoluteFilePath();
280 QString rccFileName = absoluteBaseName + QLatin1String(".rcc");
281
282 // same as above: force no compression
283 QProcess rccProcess;
284 rccProcess.setWorkingDirectory(dataPath);
285 rccProcess.start(program: m_rcc, arguments: { "-binary", "-no-compress", "-o", rccFileName, qrcFileInfo.absoluteFilePath() });
286 QVERIFY2(rccProcess.waitForStarted(), msgProcessStartFailed(rccProcess).constData());
287 if (!rccProcess.waitForFinished()) {
288 rccProcess.kill();
289 QFAIL(msgProcessTimeout(rccProcess).constData());
290 }
291 QVERIFY2(rccProcess.exitStatus() == QProcess::NormalExit,
292 msgProcessCrashed(rccProcess).constData());
293 QVERIFY2(rccProcess.exitCode() == 0,
294 msgProcessFailed(rccProcess).constData());
295
296 QByteArray output = rccProcess.readAllStandardOutput();
297 if (!output.isEmpty())
298 qWarning(msg: "rcc stdout: %s", output.constData());
299
300 output = rccProcess.readAllStandardError();
301 if (!output.isEmpty())
302 qWarning(msg: "rcc stderr: %s", output.constData());
303
304 QString localeFileName = absoluteBaseName + QLatin1String(".locale");
305 QFile localeFile(localeFileName);
306 if (localeFile.exists()) {
307 QStringList locales = readLinesFromFile(fileName: localeFileName, splitBehavior: Qt::SkipEmptyParts);
308 foreach (const QString &locale, locales) {
309 QString expectedFileName = QString::fromLatin1(str: "%1.%2.%3").arg(args&: absoluteBaseName, args: locale, args: QLatin1String("expected"));
310 QStringMap expectedFiles = readExpectedFiles(fileName: expectedFileName);
311 QTest::newRow(qPrintable(qrcFileInfo.baseName() + QLatin1Char('_') + locale)) << rccFileName
312 << QLocale(locale)
313 << dataPath
314 << expectedFiles;
315 }
316 }
317
318 // always test for the C locale as well
319 QString expectedFileName = absoluteBaseName + QLatin1String(".expected");
320 QStringMap expectedFiles = readExpectedFiles(fileName: expectedFileName);
321 QTest::newRow(qPrintable(qrcFileInfo.baseName() + QLatin1String("_C"))) << rccFileName
322 << QLocale::c()
323 << dataPath
324 << expectedFiles;
325 }
326}
327
328void tst_rcc::binary()
329{
330 QFETCH(QString, baseDirectory);
331 QFETCH(QString, resourceFile);
332 QFETCH(QLocale, locale);
333 QFETCH(QStringMap, expectedFiles);
334
335 const QString rootPrefix = QLatin1String("/test_root/");
336 const QString resourceRootPrefix = QLatin1Char(':') + rootPrefix;
337
338 QLocale oldDefaultLocale;
339 QLocale::setDefault(locale);
340 QVERIFY(QFile::exists(resourceFile));
341 QVERIFY(QResource::registerResource(resourceFile, rootPrefix));
342
343 { // need to destroy the iterators on the resource, in order to be able to unregister it
344
345 // read all the files inside the resources
346 QDirIterator iter(resourceRootPrefix, QDir::Files, QDirIterator::Subdirectories);
347 QList<QString> filesFound;
348 while (iter.hasNext())
349 filesFound << iter.next();
350
351 // add the test root prefix to the expected file names
352 QList<QString> expectedFileNames = expectedFiles.keys();
353 for (QList<QString>::iterator i = expectedFileNames.begin(); i < expectedFileNames.end(); ++i) {
354 // poor man's canonicalPath, which doesn't work with resources
355 if ((*i).startsWith(c: QLatin1Char('/')))
356 (*i).remove(i: 0, len: 1);
357 *i = resourceRootPrefix + *i;
358 }
359
360 // check that we have all (and only) the expected files
361 std::sort(first: filesFound.begin(), last: filesFound.end());
362 std::sort(first: expectedFileNames.begin(), last: expectedFileNames.end());
363 QCOMPARE(filesFound, expectedFileNames);
364
365 // now actually check the file contents
366 QDir directory(baseDirectory);
367 for (QStringMap::const_iterator i = expectedFiles.constBegin(); i != expectedFiles.constEnd(); ++i) {
368 QString resourceFileName = i.key();
369 QString actualFileName = i.value();
370
371 QFile resourceFile(resourceRootPrefix + resourceFileName);
372 QVERIFY(resourceFile.open(QIODevice::ReadOnly));
373 QByteArray resourceData = resourceFile.readAll();
374 resourceFile.close();
375
376 QFile actualFile(QFileInfo(directory, actualFileName).absoluteFilePath());
377 QVERIFY(actualFile.open(QIODevice::ReadOnly));
378 QByteArray actualData = actualFile.readAll();
379 actualFile.close();
380 QCOMPARE(resourceData, actualData);
381 }
382
383 }
384
385 QVERIFY(QResource::unregisterResource(resourceFile, rootPrefix));
386 QLocale::setDefault(oldDefaultLocale);
387}
388
389void tst_rcc::readback_data()
390{
391 QTest::addColumn<QString>(name: "resourceName");
392 QTest::addColumn<QString>(name: "fileSystemName");
393
394 QTest::newRow(dataTag: "data-0") << ":data/data-0.txt" << "sizes/data/data-0.txt";
395 QTest::newRow(dataTag: "data-1") << ":data/data-1.txt" << "sizes/data/data-1.txt";
396 QTest::newRow(dataTag: "data-2") << ":data/data-2.txt" << "sizes/data/data-2.txt";
397 QTest::newRow(dataTag: "data-35") << ":data/data-35.txt" << "sizes/data/data-35.txt";
398 QTest::newRow(dataTag: "circle") << ":images/circle.png" << "images/images/circle.png";
399 QTest::newRow(dataTag: "square") << ":images/square.png" << "images/images/square.png";
400 QTest::newRow(dataTag: "triangle") << ":images/subdir/triangle.png"
401 << "images/images/subdir/triangle.png";
402}
403
404void tst_rcc::readback()
405{
406 QFETCH(QString, resourceName);
407 QFETCH(QString, fileSystemName);
408
409 QFile resourceFile(resourceName);
410 QVERIFY(resourceFile.open(QIODevice::ReadOnly));
411 QByteArray resourceData = resourceFile.readAll();
412 resourceFile.close();
413
414 QFile fileSystemFile(m_dataPath + QLatin1Char('/') + fileSystemName);
415 QVERIFY(fileSystemFile.open(QIODevice::ReadOnly));
416 QByteArray fileSystemData = fileSystemFile.readAll();
417 fileSystemFile.close();
418
419 QCOMPARE(resourceData, fileSystemData);
420}
421
422void tst_rcc::depFileGeneration_data()
423{
424 QTest::addColumn<QString>(name: "qrcfile");
425 QTest::addColumn<QString>(name: "depfile");
426 QTest::addColumn<QString>(name: "expected");
427
428 QTest::newRow(dataTag: "simple") << "simple.qrc" << "simple.d" << "simple.d.expected";
429 QTest::newRow(dataTag: "specialchar") << "specialchar.qrc" << "specialchar.d" << "specialchar.d.expected";
430}
431
432void tst_rcc::depFileGeneration()
433{
434 QFETCH(QString, qrcfile);
435 QFETCH(QString, depfile);
436 QFETCH(QString, expected);
437 const QString directory = m_dataPath + QLatin1String("/depfile");
438
439 QProcess process;
440 process.setWorkingDirectory(directory);
441 process.start(program: m_rcc, arguments: { "-d", depfile, "-o", qrcfile + ".cpp", qrcfile });
442 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(process).constData());
443 if (!process.waitForFinished()) {
444 process.kill();
445 QFAIL(msgProcessTimeout(process).constData());
446 }
447 QVERIFY2(process.exitStatus() == QProcess::NormalExit,
448 msgProcessCrashed(process).constData());
449 QVERIFY2(process.exitCode() == 0,
450 msgProcessFailed(process).constData());
451
452 QFile depFileOutput(directory + QLatin1String("/") + depfile);
453 QVERIFY(depFileOutput.open(QIODevice::ReadOnly | QIODevice::Text));
454 QByteArray depFileData = depFileOutput.readAll();
455 depFileOutput.close();
456
457 QFile depFileExpected(directory + QLatin1String("/") + expected);
458 QVERIFY(depFileExpected.open(QIODevice::ReadOnly | QIODevice::Text));
459 QByteArray expectedData = depFileExpected.readAll();
460 depFileExpected.close();
461
462 QCOMPARE(depFileData, expectedData);
463}
464
465void tst_rcc::python()
466{
467 const QString path = m_dataPath + QLatin1String("/sizes");
468 const QString testFileRoot = path + QLatin1String("/size-2-0-35-1");
469 const QString qrcFile = testFileRoot + QLatin1String(".qrc");
470 const QString expectedFile = testFileRoot + QLatin1String("_python.expected");
471 const QString actualFile = testFileRoot + QLatin1String(".rcc");
472
473 QProcess process;
474 process.setWorkingDirectory(path);
475 process.start(program: m_rcc, arguments: { "-g", "python", "-o", actualFile, qrcFile});
476 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(process).constData());
477 if (!process.waitForFinished()) {
478 process.kill();
479 QFAIL(msgProcessTimeout(process).constData());
480 }
481 QVERIFY2(process.exitStatus() == QProcess::NormalExit,
482 msgProcessCrashed(process).constData());
483 QVERIFY2(process.exitCode() == 0,
484 msgProcessFailed(process).constData());
485
486 const auto actualLines = readLinesFromFile(fileName: actualFile, splitBehavior: Qt::KeepEmptyParts);
487 QVERIFY(!actualLines.isEmpty());
488 const auto expectedLines = readLinesFromFile(fileName: expectedFile, splitBehavior: Qt::KeepEmptyParts);
489 QVERIFY(!expectedLines.isEmpty());
490 const QString diff = doCompare(actual: actualLines, expected: expectedLines, timeStampPath: path);
491 if (!diff.isEmpty())
492 QFAIL(qPrintable(diff));
493}
494
495void tst_rcc::cleanupTestCase()
496{
497 QDir dataDir(m_dataPath + QLatin1String("/binary"));
498 QFileInfoList entries = dataDir.entryInfoList(nameFilters: QStringList() << QLatin1String("*.rcc"));
499 QDir dataDepDir(m_dataPath + QLatin1String("/depfile"));
500 entries += dataDepDir.entryInfoList(nameFilters: {QLatin1String("*.d"), QLatin1String("*.qrc.cpp")});
501 foreach (const QFileInfo &entry, entries)
502 QFile::remove(fileName: entry.absoluteFilePath());
503}
504
505QTEST_MAIN(tst_rcc)
506
507#include "tst_rcc.moc"
508

source code of qtbase/tests/auto/tools/rcc/tst_rcc.cpp