tests/auto/maketestselftest/tst_maketestselftest.cpp
changeset 30 5dc02b23752f
parent 19 fcece45ef507
child 33 3e2da88830cd
--- a/tests/auto/maketestselftest/tst_maketestselftest.cpp	Wed Jun 23 19:07:03 2010 +0300
+++ b/tests/auto/maketestselftest/tst_maketestselftest.cpp	Tue Jul 06 15:10:48 2010 +0300
@@ -44,16 +44,62 @@
 #include <QRegExp>
 #include <QStringList>
 #include <QTest>
+#include <QSet>
+#include <QProcess>
+#include <QDebug>
+
+enum FindSubdirsMode {
+    Flat = 0,
+    Recursive
+};
 
 class tst_MakeTestSelfTest: public QObject
 {
     Q_OBJECT
 
 private slots:
+    void tests_auto_pro();
+
     void tests_pro_files();
     void tests_pro_files_data();
+
+    void naming_convention();
+    void naming_convention_data();
+
+private:
+    QStringList find_subdirs(QString const&, FindSubdirsMode, QString const& = QString());
+
+    QSet<QString> all_test_classes;
 };
 
+bool looks_like_testcase(QString const&,QString*);
+bool looks_like_subdirs(QString const&);
+QStringList find_test_class(QString const&);
+
+/*
+    Verify that auto.pro only contains other .pro files (and not directories).
+    We enforce this so that we can process every .pro file other than auto.pro
+    independently and get all the tests.
+    If tests were allowed to appear directly in auto.pro, we'd have the problem
+    that we need to somehow run these tests from auto.pro while preventing
+    recursion into the other .pro files.
+*/
+void tst_MakeTestSelfTest::tests_auto_pro()
+{
+    QStringList subdirsList = find_subdirs(SRCDIR "/../auto.pro", Flat);
+    if (QTest::currentTestFailed()) {
+        return;
+    }
+
+    foreach (QString const& subdir, subdirsList) {
+        QVERIFY2(subdir.endsWith(".pro"), qPrintable(QString(
+            "auto.pro contains a subdir `%1'.\n"
+            "auto.pro must _only_ contain other .pro files, not actual subdirs.\n"
+            "Please move `%1' into some other .pro file referenced by auto.pro."
+        ).arg(subdir)));
+    }
+}
+
 /* Verify that all tests are listed somewhere in one of the autotest .pro files */
 void tst_MakeTestSelfTest::tests_pro_files()
 {
@@ -82,6 +128,8 @@
         }
     }
 
+
+
     QFAIL(qPrintable(QString(
         "Subdir `%1' is missing from tests/auto/*.pro\n"
         "This means the test won't be compiled or run on any platform.\n"
@@ -106,5 +154,355 @@
     }
 }
 
+QString format_list(QStringList const& list)
+{
+    if (list.count() == 1) {
+        return list.at(0);
+    }
+    return QString("one of (%1)").arg(list.join(", "));
+}
+
+void tst_MakeTestSelfTest::naming_convention()
+{
+    QFETCH(QString, subdir);
+    QFETCH(QString, target);
+
+    QDir dir(SRCDIR "/../" + subdir);
+
+    QStringList cppfiles = dir.entryList(QStringList() << "*.h" << "*.cpp");
+    if (cppfiles.isEmpty()) {
+        // Common convention is to have test/test.pro and source files in parent dir
+        if (dir.dirName() == "test") {
+            dir.cdUp();
+            cppfiles = dir.entryList(QStringList() << "*.h" << "*.cpp");
+        }
+
+        if (cppfiles.isEmpty()) {
+            QSKIP("Couldn't locate source files for test", SkipSingle);
+        }
+    }
+
+    QStringList possible_test_classes;
+    foreach (QString const& file, cppfiles) {
+        possible_test_classes << find_test_class(dir.path() + "/" + file);
+    }
+
+    if (possible_test_classes.isEmpty()) {
+        QSKIP(qPrintable(QString("Couldn't locate test class in %1").arg(format_list(cppfiles))), SkipSingle);
+    }
+
+    QVERIFY2(possible_test_classes.contains(target), qPrintable(QString(
+        "TARGET is %1, while test class appears to be %2.\n"
+        "TARGET and test class _must_ match so that all testcase names can be accurately "
+        "determined even if a test fails to compile or run.")
+        .arg(target)
+        .arg(format_list(possible_test_classes))
+    ));
+
+    QVERIFY2(!all_test_classes.contains(target), qPrintable(QString(
+        "It looks like there are multiple tests named %1.\n"
+        "This makes it impossible to separate results for these tests.\n"
+        "Please ensure all tests are uniquely named.")
+        .arg(target)
+    ));
+
+    all_test_classes << target;
+}
+
+void tst_MakeTestSelfTest::naming_convention_data()
+{
+    QTest::addColumn<QString>("subdir");
+    QTest::addColumn<QString>("target");
+
+    foreach (const QString& subdir, find_subdirs(SRCDIR "/../auto.pro", Recursive)) {
+        if (QFileInfo(SRCDIR "/../" + subdir).isDir()) {
+            QString target;
+            if (looks_like_testcase(SRCDIR "/../" + subdir + "/" + QFileInfo(subdir).baseName() + ".pro", &target)) {
+                QTest::newRow(qPrintable(subdir)) << subdir << target.toLower();
+            }
+        }
+    }
+}
+
+/*
+    Returns true if a .pro file seems to be for an autotest.
+    Running qmake to figure this out takes too long.
+*/
+bool looks_like_testcase(QString const& pro_file, QString* target)
+{
+    QFile file(pro_file);
+    if (!file.open(QIODevice::ReadOnly)) {
+        return false;
+    }
+
+    *target = QString();
+
+    bool loaded_qttest = false;
+
+    do {
+        QByteArray line = file.readLine();
+        if (line.isEmpty()) {
+            break;
+        }
+
+        line = line.trimmed();
+        line.replace(' ', "");
+
+        if (line == "load(qttest_p4)") {
+            loaded_qttest = true;
+        }
+
+        if (line.startsWith("TARGET=")) {
+            *target = QString::fromLatin1(line.mid(sizeof("TARGET=")-1));
+            if (target->contains('/')) {
+                *target = target->right(target->lastIndexOf('/')+1);
+            }
+        }
+
+        if (loaded_qttest && !target->isEmpty()) {
+            break;
+        }
+    } while(1);
+
+    if (!loaded_qttest) {
+        return false;
+    }
+
+    if (!target->isEmpty() && !target->startsWith("tst_")) {
+        return false;
+    }
+
+    // If no target was set, default to tst_<dirname>
+    if (target->isEmpty()) {
+        *target = "tst_" + QFileInfo(pro_file).baseName();
+    }
+
+    return true;
+}
+
+/*
+    Returns true if a .pro file seems to be a subdirs project.
+    Running qmake to figure this out takes too long.
+*/
+bool looks_like_subdirs(QString const& pro_file)
+{
+    QFile file(pro_file);
+    if (!file.open(QIODevice::ReadOnly)) {
+        return false;
+    }
+
+    do {
+        QByteArray line = file.readLine();
+        if (line.isEmpty()) {
+            break;
+        }
+
+        line = line.trimmed();
+        line.replace(' ', "");
+
+        if (line == "TEMPLATE=subdirs") {
+            return true;
+        }
+    } while(1);
+
+    return false;
+}
+
+/*
+    Returns a list of all subdirs in a given .pro file
+*/
+QStringList tst_MakeTestSelfTest::find_subdirs(QString const& pro_file, FindSubdirsMode mode, QString const& prefix)
+{
+    QStringList out;
+
+    QByteArray features = qgetenv("QMAKEFEATURES");
+
+    if (features.isEmpty()) {
+        features = SRCDIR "/features";
+    }
+    else {
+        features.prepend(SRCDIR "/features"
+#ifdef Q_OS_WIN32
+                ";"
+#else
+                ":"
+#endif
+        );
+    }
+
+    QStringList args;
+    args << pro_file << "-o" << SRCDIR "/dummy_output" << "CONFIG+=dump_subdirs";
+
+    /* Turn on every option there is, to ensure we process every single directory */
+    args
+        << "QT_CONFIG+=dbus"
+        << "QT_CONFIG+=declarative"
+        << "QT_CONFIG+=egl"
+        << "QT_CONFIG+=multimedia"
+        << "QT_CONFIG+=OdfWriter"
+        << "QT_CONFIG+=opengl"
+        << "QT_CONFIG+=openvg"
+        << "QT_CONFIG+=phonon"
+        << "QT_CONFIG+=private_tests"
+        << "QT_CONFIG+=pulseaudio"
+        << "QT_CONFIG+=qt3support"
+        << "QT_CONFIG+=script"
+        << "QT_CONFIG+=svg"
+        << "QT_CONFIG+=webkit"
+        << "QT_CONFIG+=xmlpatterns"
+        << "CONFIG+=mac"
+        << "CONFIG+=embedded"
+        << "CONFIG+=symbian"
+    ;
+
+
+
+    QString cmd_with_args = QString("qmake %1").arg(args.join(" "));
+
+    QProcess proc;
+
+    proc.setProcessChannelMode(QProcess::MergedChannels);
+
+    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+    env.insert("QMAKEFEATURES", features);
+    proc.setProcessEnvironment(env);
+
+    proc.start("qmake", args);
+    if (!proc.waitForStarted(10000)) {
+        QTest::qFail(qPrintable(QString("Failed to run qmake: %1\nCommand: %2")
+            .arg(proc.errorString())
+            .arg(cmd_with_args)),
+            __FILE__, __LINE__
+        );
+        return out;
+    }
+    if (!proc.waitForFinished(30000)) {
+        QTest::qFail(qPrintable(QString("qmake did not finish within 30 seconds\nCommand: %1\nOutput: %2")
+            .arg(proc.errorString())
+            .arg(cmd_with_args)
+            .arg(QString::fromLocal8Bit(proc.readAll()))),
+            __FILE__, __LINE__
+        );
+        return out;
+    }
+
+    if (proc.exitStatus() != QProcess::NormalExit) {
+        QTest::qFail(qPrintable(QString("qmake crashed\nCommand: %1\nOutput: %2")
+            .arg(cmd_with_args)
+            .arg(QString::fromLocal8Bit(proc.readAll()))),
+            __FILE__, __LINE__
+        );
+        return out;
+    }
+
+    if (proc.exitCode() != 0) {
+        QTest::qFail(qPrintable(QString("qmake exited with code %1\nCommand: %2\nOutput: %3")
+            .arg(proc.exitCode())
+            .arg(cmd_with_args)
+            .arg(QString::fromLocal8Bit(proc.readAll()))),
+            __FILE__, __LINE__
+        );
+        return out;
+    }
+
+    QList<QByteArray> lines = proc.readAll().split('\n');
+    if (!lines.count()) {
+        QTest::qFail(qPrintable(QString("qmake seems to have not output anything\nCommand: %1\n")
+            .arg(cmd_with_args)),
+            __FILE__, __LINE__
+        );
+        return out;
+    }
+
+    foreach (QByteArray const& line, lines) {
+        static const QByteArray marker = "Project MESSAGE: subdir: ";
+        if (line.startsWith(marker)) {
+            QString subdir = QString::fromLocal8Bit(line.mid(marker.size()).trimmed());
+            out << prefix + subdir;
+
+            if (mode == Flat) {
+                continue;
+            }
+
+            // Need full path to subdir
+            QString subdir_filepath = subdir;
+            subdir_filepath.prepend(QFileInfo(pro_file).path() + "/");
+
+            // Add subdirs recursively
+            if (subdir.endsWith(".pro") && looks_like_subdirs(subdir_filepath)) {
+                // Need full path to .pro file
+                out << find_subdirs(subdir_filepath, mode, prefix);
+            }
+
+            if (QFileInfo(subdir_filepath).isDir()) {
+                subdir_filepath += "/" + subdir + ".pro";
+                if (looks_like_subdirs(subdir_filepath)) {
+                    out << find_subdirs(subdir_filepath, mode, prefix + subdir + "/");
+                }
+            }
+        }
+    }
+
+    return out;
+}
+
+QStringList find_test_class(QString const& filename)
+{
+    QStringList out;
+
+    QFile file(filename);
+    if (!file.open(QIODevice::ReadOnly)) {
+        return out;
+    }
+
+    static char const* klass_indicators[] = {
+        "QTEST_MAIN(",
+        "QTEST_APPLESS_MAIN(",
+        "class",
+        "staticconstcharklass[]=\"",  /* hax0r tests which define their own metaobject */
+        0
+    };
+
+    do {
+        QByteArray line = file.readLine();
+        if (line.isEmpty()) {
+            break;
+        }
+
+        line = line.trimmed();
+        line.replace(' ', "");
+
+        for (int i = 0; klass_indicators[i]; ++i) {
+            char const* prefix = klass_indicators[i];
+            if (!line.startsWith(prefix)) {
+                continue;
+            }
+            QByteArray klass = line.mid(strlen(prefix));
+            if (!klass.startsWith("tst_")) {
+                continue;
+            }
+            for (int j = 0; j < klass.size(); ++j) {
+                char c = klass[j];
+                if (c == '_'
+                        || (c >= '0' && c <= '9')
+                        || (c >= 'A' && c <= 'Z')
+                        || (c >= 'a' && c <= 'z')) {
+                    continue;
+                }
+                else {
+                    klass.truncate(j);
+                    break;
+                }
+            }
+            QString klass_str = QString::fromLocal8Bit(klass).toLower();
+            if (!out.contains(klass_str))
+                out << klass_str;
+            break;
+        }
+    } while(1);
+
+    return out;
+}
+
 QTEST_MAIN(tst_MakeTestSelfTest)
 #include "tst_maketestselftest.moc"