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 | |
43 | static const char keepEnvVar[] = "UIC_KEEP_GENERATED_FILES" ; |
44 | static const char diffToStderrEnvVar[] = "UIC_STDERR_DIFF" ; |
45 | |
46 | struct 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 | |
63 | Q_DECLARE_OPERATORS_FOR_FLAGS(TestEntry::Flags) |
64 | |
65 | class tst_uic : public QObject |
66 | { |
67 | Q_OBJECT |
68 | |
69 | public: |
70 | using TestEntries = QVector<TestEntry>; |
71 | |
72 | tst_uic(); |
73 | |
74 | private 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 | |
93 | private: |
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 | |
104 | static const char versionRegexp[] = |
105 | R"([*#][*#] Created by: Qt User Interface Compiler version \d{1,2}\.\d{1,2}\.\d{1,2})" ; |
106 | |
107 | tst_uic::tst_uic() |
108 | : m_command(QLibraryInfo::location(QLibraryInfo::BinariesPath) + QLatin1String("/uic" )) |
109 | , m_versionRegexp(QLatin1String(versionRegexp)) |
110 | { |
111 | } |
112 | |
113 | static 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 |
121 | static 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 | |
146 | void 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 | |
173 | void 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 | |
207 | static const char helpFormat[] = R"( |
208 | Note: The environment variable '%s' can be set to keep the temporary files |
209 | for error analysis. |
210 | The environment variable '%s' can be set to redirect the diff output to |
211 | stderr.)" ; |
212 | |
213 | void 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 | |
223 | void 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 | |
248 | void 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 | |
264 | void 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. |
282 | static 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 | |
291 | static 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 | |
305 | static 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 | |
313 | static 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 | |
323 | void 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 | |
350 | void 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 | |
361 | void 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 | |
383 | void 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 | |
416 | static 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 |
434 | void 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 | |
451 | void 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 | |
479 | QTEST_MAIN(tst_uic) |
480 | #include "tst_uic.moc" |
481 | |