Revision control
++ // Copyright (C) 2017 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
++
++ #include <QtCore/QtCore>
++ #include <QtTest/QtTest>
++
++ class tst_Headers: public QObject
++ {
++ Q_OBJECT
++ private slots:
++ void initTestCase();
++
++ void privateSlots_data() { allHeadersData(); }
++ void privateSlots();
++
++ void macros_data() { allHeadersData(); }
++ void macros();
++
++ private:
++ static QString explainPrivateSlot(const QString &line);
++
++ void allHeadersData();
++ QStringList headers;
++ QString qtModuleDir;
++ };
++
++ static QByteArray captureOutput(const QString &program, const QStringList &arguments, const QString &workingDirectory = QString())
++ {
++ QProcess process;
++ process.setWorkingDirectory(workingDirectory);
++ process.start(program, arguments);
++
++ if (!process.waitForFinished(30000))
++ qFatal("'%s' did not complete within 30 seconds: %s", qPrintable(program), qPrintable(process.errorString()));
++
++ if (0 != process.exitCode())
++ qFatal("Error running '%s': %s", qPrintable(program), process.readAllStandardError().constData());
++
++ return process.readAll();
++ }
++
++ static QStringList getHeaders(const QString &path)
++ {
++ // Create a QStringList of files out of the standard output
++ QString string(captureOutput("git", QStringList() << "ls-files", path));
++ QStringList entries = string.split( "\n" );
++
++ // We just want to check header files
++ entries = entries.filter(QRegularExpression("\\.h$"));
++ entries = entries.filter(QRegularExpression("^(?!ui_)"));
++
++ // Recreate the whole file path so we can open the file from disk
++ QStringList result;
++ for (const QString &entry : std::as_const(entries))
++ result += path + "/" + entry;
++
++ return result;
++ }
++
++ static QStringList getModuleHeaders(const QString &moduleRoot)
++ {
++ // Read the sync.profile file of the module and test headers that syncqt will consider deploying.
++ const QLatin1String perlReadSyncProfileExpr(
++ "use File::Spec; use Cwd 'abs_path';"
++ "$basedir = $ARGV[0];"
++ "do File::Spec->catfile($basedir, 'sync.profile');"
++ "foreach my $lib (keys(%modules)) {"
++ "my $module = $modules{$lib};"
++ "my $moduleheader = $moduleheaders{$lib};"
++ "my $is_qt = !($module =~ s/^!//);"
++ "my $joined = abs_path(File::Spec->catdir($module, $moduleheader));"
++ "push @searchPaths, $joined if ($is_qt);"
++ "}"
++ "print join(\"\\n\", @searchPaths);");
++
++ QString string(captureOutput("perl", QStringList() << "-e" << perlReadSyncProfileExpr << moduleRoot));
++ const QStringList entries = string.split("\n");
++ QStringList headers;
++ for (const QString &headersPath : entries)
++ headers += getHeaders(headersPath);
++ return headers;
++ }
++
++ void tst_Headers::initTestCase()
++ {
++ qtModuleDir = QString::fromLocal8Bit(qgetenv("QT_MODULE_TO_TEST"));
++ if (qtModuleDir.isEmpty()) {
++ QSKIP("$QT_MODULE_TO_TEST is unset - nothing to test. Set QT_MODULE_TO_TEST to the path "
++ "of a Qt module to test.");
++ }
++
++ QDir dir(qtModuleDir);
++ QString module = dir.dirName(); // git module name, e.g. qtbase, qtdeclarative
++
++ if (module != "phonon" && module != "qttools") {
++ if (dir.exists("src")) {
++
++ /*
++ Let all paths be relative to the directory containing the module.
++ For example, if the full path is:
++
++ /home/qt/build/qt5/qtbase/src/corelib/tools/qstring.h
++
++ ... the first part of the path is useless noise, and causes the
++ test log to look different on different machines.
++ Cut it down to only the important part:
++
++ qtbase/src/corelib/tools/qstring.h
++
++ */
++
++ QVERIFY(QDir::setCurrent(dir.absolutePath() + "/.."));
++
++ headers = getModuleHeaders(module);
++ }
++ if (headers.isEmpty()) {
++ QVERIFY2(module != "qtbase",
++ "qtbase not containing any header? Something might be wrong with this test.");
++ QSKIP("It seems there are no headers in this module; this test is "
++ "not applicable");
++ }
++ } else {
++ qWarning("Some test functions will be skipped, because we ignore them for phonon and qttools.");
++ }
++ }
++
++ void tst_Headers::allHeadersData()
++ {
++ QTest::addColumn<QString>("header");
++
++ if (headers.isEmpty())
++ QSKIP("can't find any headers in your $QT_MODULE_TO_TEST/src.");
++
++ for (const QString &hdr : std::as_const(headers)) {
++ if (hdr.contains("/3rdparty/") || hdr.endsWith("/src/tools/uic/qclass_lib_map.h"))
++ continue;
++
++ QTest::newRow(qPrintable(hdr)) << hdr;
++ }
++ }
++
++ QString tst_Headers::explainPrivateSlot(const QString& line)
++ {
++ // Extract private slot from a line like:
++ // Q_PRIVATE_SLOT(d_func(), void fooBar(...))
++ QRegularExpression re("^\\s+Q_PRIVATE_SLOT\\([^,]+,\\s*(.+)\\)\\s*$");
++ QString slot = line;
++ QRegularExpressionMatch match = re.match(slot);
++ if (match.hasMatch())
++ slot = match.captured(1).simplified();
++
++ return QString(
++ "Private slot `%1' should be named starting with _q_, to reduce the risk of collisions "
++ "with signals/slots in user classes"
++ ).arg(slot);
++ }
++
++ void tst_Headers::privateSlots()
++ {
++ QFETCH(QString, header);
++
++ if (header.endsWith("_p.h"))
++ return;
++
++ QFile f(header);
++ QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(f.errorString()));
++
++ const QStringList content = QString::fromLocal8Bit(f.readAll()).split("\n");
++ for (const QString &line : content) {
++ if (line.contains("Q_PRIVATE_SLOT(") && !line.contains("define Q_PRIVATE_SLOT"))
++ QVERIFY2(line.contains("_q_"), qPrintable(explainPrivateSlot(line)));
++ }
++ }
++
++ void tst_Headers::macros()
++ {
++ QFETCH(QString, header);
++
++ if (header.endsWith("_p.h") || header.endsWith("_pch.h")
++ || header.contains("global/qconfig-") || header.endsWith("/qconfig.h")
++ || header.endsWith("src/corelib/global/qcompilerdetection.h")
++ || header.endsWith("src/corelib/global/qprocessordetection.h")
++ || header.endsWith("src/corelib/global/qsystemdetection.h")
++ || header.endsWith("src/gui/opengl/qopengles2ext.h")
++ || header.endsWith("src/gui/opengl/qopenglext.h")
++ || header.contains("/snippets/")
++ || header.contains("/src/tools/") || header.contains("/src/plugins/")
++ || header.contains("/src/imports/")
++ || header.contains("/src/uitools/")
++ || header.contains("/src/daemon")
++ || header.endsWith("/qiconset.h") || header.endsWith("/qfeatures.h")
++ || header.endsWith("qt_windows.h")
++ // qtsvg.git files
++ || header.endsWith("src/svg/qsvgfunctions_wince.h"))
++ return;
++
++ QFile f(header);
++ QVERIFY2(f.open(QIODevice::ReadOnly), qPrintable(f.errorString()));
++
++ QByteArray data = f.readAll();
++ QStringList content = QString::fromLocal8Bit(data.replace('\r', "")).split("\n");
++
++ // "signals" and "slots" should be banned in public headers
++ // headers which use signals/slots wouldn't compile if Qt is configured with QT_NO_KEYWORDS
++ QVERIFY2(content.indexOf(QRegularExpression("\\bslots\\s*:")) == -1, "Header contains `slots' - use `Q_SLOTS' instead!");
++ QVERIFY2(content.indexOf(QRegularExpression("\\bsignals\\s*:")) == -1, "Header contains `signals' - use `Q_SIGNALS' instead!");
++
++ if (header.contains("/sql/drivers/") || header.contains("/arch/qatomic")
++ || header.contains(QRegularExpression("q.*global\\.h$"))
++ || header.endsWith("qwindowdefs_win.h"))
++ return;
++
++ int beginNamespace = content.indexOf(QRegularExpression("QT_BEGIN_NAMESPACE(_[A-Z_]+)?"));
++ int endNamespace = content.lastIndexOf(QRegularExpression("QT_END_NAMESPACE(_[A-Z_]+)?"));
++ QVERIFY(beginNamespace != -1);
++ QVERIFY(endNamespace != -1);
++ QVERIFY(beginNamespace < endNamespace);
++ }
++
++ QTEST_MAIN(tst_Headers)
++ #include "tst_headers.moc"