// 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 #include 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("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"