1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
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
29
30#include <QtCore/QDir>
31#include <QtCore/QString>
32#include <QtTest/QtTest>
33#include <QtCore/QProcess>
34#include <QtCore/QByteArray>
35#include <QtCore/QLibraryInfo>
36#include <QtCore/QTemporaryDir>
37#include <QtCore/QRegularExpression>
38#include <QtCore/QStandardPaths>
39#include <QtCore/QVector>
40
41#include <cstdio>
42
43static const char keepEnvVar[] = "UIC_KEEP_GENERATED_FILES";
44static const char diffToStderrEnvVar[] = "UIC_STDERR_DIFF";
45
46struct TestEntry
47{
48 enum Flag
49 {
50 IdBasedTranslation = 0x1,
51 Python = 0x2, // Python baseline is present
52 DontTestPythonCompile = 0x4 // Do not test Python compilation
53 };
54 Q_DECLARE_FLAGS(Flags, Flag)
55
56 QByteArray name;
57 QString uiFileName;
58 QString baseLineFileName;
59 QString generatedFileName;
60 Flags flags;
61};
62
63Q_DECLARE_OPERATORS_FOR_FLAGS(TestEntry::Flags)
64
65class tst_uic : public QObject
66{
67 Q_OBJECT
68
69public:
70 using TestEntries = QVector<TestEntry>;
71
72 tst_uic();
73
74private Q_SLOTS:
75 void initTestCase();
76 void cleanupTestCase();
77
78 void stdOut();
79
80 void run();
81 void run_data() const;
82
83 void runTranslation();
84
85 void compare();
86 void compare_data() const;
87
88 void pythonCompile();
89 void pythonCompile_data() const;
90
91 void runCompare();
92
93private:
94 void populateTestEntries();
95
96 const QString m_command;
97 QString m_baseline;
98 QTemporaryDir m_generated;
99 TestEntries m_testEntries;
100 QRegularExpression m_versionRegexp;
101 QString m_python;
102};
103
104static const char versionRegexp[] =
105 R"([*#][*#] Created by: Qt User Interface Compiler version \d{1,2}\.\d{1,2}\.\d{1,2})";
106
107tst_uic::tst_uic()
108 : m_command(QLibraryInfo::location(QLibraryInfo::BinariesPath) + QLatin1String("/uic"))
109 , m_versionRegexp(QLatin1String(versionRegexp))
110{
111}
112
113static QByteArray msgProcessStartFailed(const QString &command, const QString &why)
114{
115 const QString result = QString::fromLatin1(str: "Could not start %1: %2")
116 .arg(a1: command, a2: why);
117 return result.toLocal8Bit();
118}
119
120// Locate Python and check whether PySide2 is installed
121static QString locatePython(QTemporaryDir &generatedDir)
122{
123 QString python = QStandardPaths::findExecutable(executableName: QLatin1String("python"));
124 if (python.isEmpty()) {
125 qWarning(msg: "Cannot locate python, skipping tests");
126 return QString();
127 }
128 QFile importTestFile(generatedDir.filePath(fileName: QLatin1String("import_test.py")));
129 if (!importTestFile.open(flags: QIODevice::WriteOnly| QIODevice::Text))
130 return QString();
131 importTestFile.write(data: "import PySide2.QtCore\n");
132 importTestFile.close();
133 QProcess process;
134 process.start(program: python, arguments: {importTestFile.fileName()});
135 if (!process.waitForStarted() || !process.waitForFinished())
136 return QString();
137 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
138 const QString stdErr = QString::fromLocal8Bit(str: process.readAllStandardError()).simplified();
139 qWarning(msg: "PySide2 is not installed (%s)", qPrintable(stdErr));
140 return QString();
141 }
142 importTestFile.remove();
143 return python;
144}
145
146void tst_uic::initTestCase()
147{
148 QVERIFY2(m_generated.isValid(), qPrintable(m_generated.errorString()));
149 QVERIFY(m_versionRegexp.isValid());
150 m_baseline = QFINDTESTDATA("baseline");
151 QVERIFY2(!m_baseline.isEmpty(), "Could not find 'baseline'.");
152 QProcess process;
153 process.start(program: m_command, arguments: QStringList(QLatin1String("-help")));
154 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
155 QVERIFY(process.waitForFinished());
156 QCOMPARE(process.exitStatus(), QProcess::NormalExit);
157 QCOMPARE(process.exitCode(), 0);
158 // Print version
159 const QString out = QString::fromLocal8Bit(str: process.readAllStandardError()).remove(c: QLatin1Char('\r'));
160 const QStringList outLines = out.split(sep: QLatin1Char('\n'));
161 // Print version
162 QString msg = QString::fromLatin1(str: "uic test running in '%1' using: ").
163 arg(a: QDir::currentPath());
164 if (!outLines.empty())
165 msg += outLines.front();
166 populateTestEntries();
167 QVERIFY(!m_testEntries.isEmpty());
168 qDebug(msg: "%s", qPrintable(msg));
169
170 m_python = locatePython(generatedDir&: m_generated);
171}
172
173void tst_uic::populateTestEntries()
174{
175 const QString generatedPrefix = m_generated.path() + QLatin1Char('/');
176 QDir baseline(m_baseline);
177 const QString baseLinePrefix = baseline.path() + QLatin1Char('/');
178 const QFileInfoList baselineFiles =
179 baseline.entryInfoList(nameFilters: QStringList(QString::fromLatin1(str: "*.ui")), filters: QDir::Files);
180 m_testEntries.reserve(asize: baselineFiles.size());
181 for (const QFileInfo &baselineFile : baselineFiles) {
182 const QString baseName = baselineFile.baseName();
183 TestEntry entry;
184 // qprintsettingsoutput: variable named 'from' clashes with Python
185 if (baseName == QLatin1String("qprintsettingsoutput"))
186 entry.flags.setFlag(flag: TestEntry::DontTestPythonCompile);
187 else if (baseName == QLatin1String("qttrid"))
188 entry.flags.setFlag(flag: TestEntry::IdBasedTranslation);
189 entry.name = baseName.toLocal8Bit();
190 entry.uiFileName = baselineFile.absoluteFilePath();
191 entry.baseLineFileName = entry.uiFileName + QLatin1String(".h");
192 const QString generatedFilePrefix = generatedPrefix + baselineFile.fileName();
193 entry.generatedFileName = generatedFilePrefix + QLatin1String(".h");
194 m_testEntries.append(t: entry);
195 // Check for a Python baseline
196 entry.baseLineFileName = entry.uiFileName + QLatin1String(".py");
197 if (QFileInfo::exists(file: entry.baseLineFileName)) {
198 entry.name.append(QByteArrayLiteral("-python"));
199 entry.flags.setFlag(flag: TestEntry::DontTestPythonCompile);
200 entry.flags.setFlag(flag: TestEntry::Python);
201 entry.generatedFileName = generatedFilePrefix + QLatin1String(".py");
202 m_testEntries.append(t: entry);
203 }
204 }
205}
206
207static const char helpFormat[] = R"(
208Note: The environment variable '%s' can be set to keep the temporary files
209for error analysis.
210The environment variable '%s' can be set to redirect the diff output to
211stderr.)";
212
213void tst_uic::cleanupTestCase()
214{
215 if (qEnvironmentVariableIsSet(varName: keepEnvVar)) {
216 m_generated.setAutoRemove(false);
217 qDebug(msg: "Keeping generated files in '%s'", qPrintable(QDir::toNativeSeparators(m_generated.path())));
218 } else {
219 qDebug(msg: helpFormat, keepEnvVar, diffToStderrEnvVar);
220 }
221}
222
223void tst_uic::stdOut()
224{
225 // Checks of everything works when using stdout and whether
226 // the OS file format conventions regarding newlines are met.
227 QDir baseline(m_baseline);
228 const QFileInfoList baselineFiles = baseline.entryInfoList(nameFilters: QStringList(QLatin1String("*.ui")), filters: QDir::Files);
229 QVERIFY(!baselineFiles.isEmpty());
230 QProcess process;
231 process.start(program: m_command, arguments: QStringList(baselineFiles.front().absoluteFilePath()));
232 process.closeWriteChannel();
233 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
234 QVERIFY(process.waitForFinished());
235 QCOMPARE(process.exitStatus(), QProcess::NormalExit);
236 QCOMPARE(process.exitCode(), 0);
237 const QByteArray output = process.readAllStandardOutput();
238 QByteArray expected = "/********************************************************************************";
239#ifdef Q_OS_WIN
240 expected += "\r\n";
241#else
242 expected += '\n';
243#endif
244 expected += "** ";
245 QVERIFY2(output.startsWith(expected), (QByteArray("Got: ") + output.toHex()).constData());
246}
247
248void tst_uic::run()
249{
250 QFETCH(QString, originalFile);
251 QFETCH(QString, generatedFile);
252 QFETCH(QStringList, options);
253
254 QProcess process;
255 process.start(program: m_command, arguments: QStringList(originalFile)
256 << QString(QLatin1String("-o")) << generatedFile << options);
257 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
258 QVERIFY(process.waitForFinished());
259 QCOMPARE(process.exitStatus(), QProcess::NormalExit);
260 QCOMPARE(process.exitCode(), 0);
261 QVERIFY(QFileInfo::exists(generatedFile));
262}
263
264void tst_uic::run_data() const
265{
266 QTest::addColumn<QString>(name: "originalFile");
267 QTest::addColumn<QString>(name: "generatedFile");
268 QTest::addColumn<QStringList>(name: "options");
269
270 for (const TestEntry &te : m_testEntries) {
271 QStringList options;
272 if (te.flags.testFlag(flag: TestEntry::IdBasedTranslation))
273 options.append(t: QLatin1String("-idbased"));
274 if (te.flags.testFlag(flag: TestEntry::Python))
275 options << QLatin1String("-g") << QLatin1String("python");
276 QTest::newRow(dataTag: te.name.constData()) << te.uiFileName
277 << te.generatedFileName << options;
278 }
279}
280
281// Helpers to generate a diff using the standard diff tool if present for failures.
282static inline QString diffBinary()
283{
284 QString binary = QLatin1String("diff");
285#ifdef Q_OS_WIN
286 binary += QLatin1String(".exe");
287#endif
288 return QStandardPaths::findExecutable(executableName: binary);
289}
290
291static QString generateDiff(const QString &originalFile, const QString &generatedFile)
292{
293 static const QString diff = diffBinary();
294 if (diff.isEmpty())
295 return QString();
296 const QStringList args = QStringList() << QLatin1String("-u")
297 << QDir::toNativeSeparators(pathName: originalFile)
298 << QDir::toNativeSeparators(pathName: generatedFile);
299 QProcess diffProcess;
300 diffProcess.start(program: diff, arguments: args);
301 return diffProcess.waitForStarted() && diffProcess.waitForFinished()
302 ? QString::fromLocal8Bit(str: diffProcess.readAllStandardOutput()) : QString();
303}
304
305static QByteArray msgCannotReadFile(const QFile &file)
306{
307 const QString result = QLatin1String("Could not read file: ")
308 + QDir::toNativeSeparators(pathName: file.fileName())
309 + QLatin1String(": ") + file.errorString();
310 return result.toLocal8Bit();
311}
312
313static void outputDiff(const QString &diff)
314{
315 // Use patch -p3 < diff to apply the obtained diff output in the baseline directory.
316 static const bool diffToStderr = qEnvironmentVariableIsSet(varName: diffToStderrEnvVar);
317 if (diffToStderr)
318 std::fputs(qPrintable(diff), stderr);
319 else
320 qWarning(msg: "Difference:\n%s", qPrintable(diff));
321}
322
323void tst_uic::compare()
324{
325 QFETCH(QString, originalFile);
326 QFETCH(QString, generatedFile);
327
328 QFile orgFile(originalFile);
329 QFile genFile(generatedFile);
330
331 QVERIFY2(orgFile.open(QIODevice::ReadOnly | QIODevice::Text), msgCannotReadFile(orgFile));
332
333 QVERIFY2(genFile.open(QIODevice::ReadOnly | QIODevice::Text), msgCannotReadFile(genFile));
334
335 QString originalFileContents = orgFile.readAll();
336 originalFileContents.replace(re: m_versionRegexp, after: QString());
337
338 QString generatedFileContents = genFile.readAll();
339 generatedFileContents.replace(re: m_versionRegexp, after: QString());
340
341 if (generatedFileContents != originalFileContents) {
342 const QString diff = generateDiff(originalFile, generatedFile);
343 if (!diff.isEmpty())
344 outputDiff(diff);
345 }
346
347 QCOMPARE(generatedFileContents, originalFileContents);
348}
349
350void tst_uic::compare_data() const
351{
352 QTest::addColumn<QString>(name: "originalFile");
353 QTest::addColumn<QString>(name: "generatedFile");
354
355 for (const TestEntry &te : m_testEntries) {
356 QTest::newRow(dataTag: te.name.constData()) << te.baseLineFileName
357 << te.generatedFileName;
358 }
359}
360
361void tst_uic::runTranslation()
362{
363 QProcess process;
364
365 const QDir baseline(m_baseline);
366
367 QDir generated(m_generated.path());
368 generated.mkdir(dirName: QLatin1String("translation"));
369 QString generatedFile = generated.absolutePath() + QLatin1String("/translation/Dialog_without_Buttons_tr.h");
370
371 process.start(program: m_command, arguments: QStringList(baseline.filePath(fileName: "Dialog_without_Buttons.ui"))
372 << QString(QLatin1String("-tr")) << "i18n"
373 << QString(QLatin1String("-include")) << "ki18n.h"
374 << QString(QLatin1String("-o")) << generatedFile);
375 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
376 QVERIFY(process.waitForFinished());
377 QCOMPARE(process.exitStatus(), QProcess::NormalExit);
378 QCOMPARE(process.exitCode(), 0);
379 QVERIFY(QFileInfo::exists(generatedFile));
380}
381
382
383void tst_uic::runCompare()
384{
385 const QString dialogFile = QLatin1String("/translation/Dialog_without_Buttons_tr.h");
386 const QString originalFile = m_baseline + dialogFile;
387 QFile orgFile(originalFile);
388
389 QDir generated(m_generated.path());
390 const QString generatedFile = generated.absolutePath() + dialogFile;
391 QFile genFile(generatedFile);
392
393 QVERIFY2(orgFile.open(QIODevice::ReadOnly | QIODevice::Text), msgCannotReadFile(orgFile));
394
395 QVERIFY2(genFile.open(QIODevice::ReadOnly | QIODevice::Text), msgCannotReadFile(genFile));
396
397 QString originalFileContents = orgFile.readAll();
398 originalFileContents.replace(re: m_versionRegexp, after: QString());
399
400 QString generatedFileContents = genFile.readAll();
401 generatedFileContents.replace(re: m_versionRegexp, after: QString());
402
403 if (generatedFileContents != originalFileContents) {
404 const QString diff = generateDiff(originalFile, generatedFile);
405 if (!diff.isEmpty())
406 outputDiff(diff);
407 }
408
409 QCOMPARE(generatedFileContents, originalFileContents);
410}
411
412// Let uic generate Python code and verify that it is syntactically
413// correct by compiling it into .pyc. This test is executed only
414// when python with an installed Qt for Python is detected (see locatePython()).
415
416static inline QByteArray msgCompilePythonFailed(const QByteArray &error)
417{
418 // If there is a line with blanks and caret indicating an error in the line
419 // above, insert the cursor into the offending line and remove the caret.
420 QByteArrayList lines = error.trimmed().split(sep: '\n');
421 for (int i = lines.size() - 1; i > 0; --i) {
422 const auto &line = lines.at(i);
423 const int caret = line.indexOf(c: '^');
424 if (caret == 0 || (caret > 0 && line.at(i: caret - 1) == ' ')) {
425 lines.removeAt(i);
426 lines[i - 1].insert(i: caret, c: '|');
427 break;
428 }
429 }
430 return lines.join(sep: '\n');
431}
432
433// Test Python code generation by compiling the file
434void tst_uic::pythonCompile_data() const
435{
436 QTest::addColumn<QString>(name: "originalFile");
437 QTest::addColumn<QString>(name: "generatedFile");
438
439 const int size = m_python.isEmpty()
440 ? qMin(a: 1, b: m_testEntries.size()) : m_testEntries.size();
441 for (int i = 0; i < size; ++i) {
442 const TestEntry &te = m_testEntries.at(i);
443 if (!te.flags.testFlag(flag: TestEntry::DontTestPythonCompile)) {
444 QTest::newRow(dataTag: te.name.constData())
445 << te.uiFileName
446 << te.generatedFileName;
447 }
448 }
449}
450
451void tst_uic::pythonCompile()
452{
453 QFETCH(QString, originalFile);
454 QFETCH(QString, generatedFile);
455 if (m_python.isEmpty())
456 QSKIP("Python was not found");
457
458 QStringList uicArguments{QLatin1String("-g"), QLatin1String("python"),
459 originalFile, QLatin1String("-o"), generatedFile};
460 QProcess process;
461 process.setWorkingDirectory(m_generated.path());
462 process.start(program: m_command, arguments: uicArguments);
463 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
464 QVERIFY(process.waitForFinished());
465 QCOMPARE(process.exitStatus(), QProcess::NormalExit);
466 QCOMPARE(process.exitCode(), 0);
467 QVERIFY(QFileInfo::exists(generatedFile));
468
469 // Test Python code generation by compiling the file
470 QStringList compileArguments{QLatin1String("-m"), QLatin1String("py_compile"), generatedFile};
471 process.start(program: m_python, arguments: compileArguments);
472 QVERIFY2(process.waitForStarted(), msgProcessStartFailed(m_command, process.errorString()));
473 QVERIFY(process.waitForFinished());
474 const bool compiled = process.exitStatus() == QProcess::NormalExit
475 && process.exitCode() == 0;
476 QVERIFY2(compiled, msgCompilePythonFailed(process.readAllStandardError()).constData());
477}
478
479QTEST_MAIN(tst_uic)
480#include "tst_uic.moc"
481

source code of qtbase/tests/auto/tools/uic/tst_uic.cpp