diff --git a/src/libs/extensionsystem/iplugin.cpp b/src/libs/extensionsystem/iplugin.cpp index 6df04c640644f3b48fbd0a3ec30574c2de3edad0..66e1e9ce0ade34dc9de95d677dfa3f3c535d83c8 100644 --- a/src/libs/extensionsystem/iplugin.cpp +++ b/src/libs/extensionsystem/iplugin.cpp @@ -204,6 +204,20 @@ IPlugin::~IPlugin() d = 0; } +/*! + \fn QList<QObject *> IPlugin::createTestObjects() const + + Returns objects that are meant to be passed on to QTest::qExec(). + + This function will be called if the user starts \QC with '-test PluginName' or '-test all'. + + The ownership of returned objects is transferred to caller. +*/ +QList<QObject *> IPlugin::createTestObjects() const +{ + return QList<QObject *>(); +} + /*! \fn PluginSpec *IPlugin::pluginSpec() const Returns the PluginSpec corresponding to this plugin. diff --git a/src/libs/extensionsystem/iplugin.h b/src/libs/extensionsystem/iplugin.h index 04c6d3cc0c683f66517870fd7aa0225ba24f6836..a7da247bd9c6ba4c2dac10b48de568a8614aa5bf 100644 --- a/src/libs/extensionsystem/iplugin.h +++ b/src/libs/extensionsystem/iplugin.h @@ -64,6 +64,7 @@ public: virtual bool delayedInitialize() { return false; } virtual ShutdownFlag aboutToShutdown() { return SynchronousShutdown; } virtual QObject *remoteCommand(const QStringList & /* options */, const QStringList & /* arguments */) { return 0; } + virtual QList<QObject *> createTestObjects() const; PluginSpec *pluginSpec() const; diff --git a/src/libs/extensionsystem/pluginmanager.cpp b/src/libs/extensionsystem/pluginmanager.cpp index 3548adf480559b005bf876e2275c5263e6b9ea9b..cd248abcb1ea09688fdcb5bbd2f14133f4a7c8cf 100644 --- a/src/libs/extensionsystem/pluginmanager.cpp +++ b/src/libs/extensionsystem/pluginmanager.cpp @@ -51,10 +51,15 @@ #include <QTimer> #include <QSysInfo> +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + #ifdef WITH_TESTS #include <QTest> #endif +#include <functional> + Q_LOGGING_CATEGORY(pluginLog, "qtc.extensionsystem") const char C_IGNORED_PLUGINS[] = "Plugins/Ignored"; @@ -870,76 +875,200 @@ void PluginManagerPrivate::deleteAll() } #ifdef WITH_TESTS + +typedef QMap<QObject *, QStringList> TestPlan; // Object -> selected test functions +typedef QMapIterator<QObject *, QStringList> TestPlanIterator; + +static bool isTestFunction(const QMetaMethod &metaMethod) +{ + static const QByteArrayList blackList = QByteArrayList() + << "initTestCase()" << "cleanupTestCase()" << "init()" << "cleanup()"; + + if (metaMethod.methodType() != QMetaMethod::Slot) + return false; + + if (metaMethod.access() != QMetaMethod::Private) + return false; + + const QByteArray signature = metaMethod.methodSignature(); + if (blackList.contains(signature)) + return false; + + if (!signature.startsWith("test")) + return false; + + if (signature.endsWith("_data()")) + return false; + + return true; +} + static QStringList testFunctions(const QMetaObject *metaObject) { - QStringList testFunctions; + + QStringList functions; for (int i = metaObject->methodOffset(); i < metaObject->methodCount(); ++i) { - const QByteArray signature = metaObject->method(i).methodSignature(); - if (signature.startsWith("test") && !signature.endsWith("_data()")) { + const QMetaMethod metaMethod = metaObject->method(i); + if (isTestFunction(metaMethod)) { + const QByteArray signature = metaMethod.methodSignature(); const QString method = QString::fromLatin1(signature); const QString methodName = method.left(method.size() - 2); - testFunctions.append(methodName); + functions.append(methodName); } } - return testFunctions; + return functions; } -static QStringList testFunctionsWantedByUser(const PluginSpec *pluginSpec, - const QStringList &availableTestFunctions, - const QStringList &testFunctionsSpecs) +static QStringList matchingTestFunctions(const QStringList &testFunctions, + const QString &matchText) { - QStringList testFunctions; - - foreach (const QString &userTestFunction, testFunctionsSpecs) { - // There might be a test data suffix like in "testfunction:testdata1". - QString testFunctionName = userTestFunction; - QString testDataSuffix; - const int index = testFunctionName.indexOf(QLatin1Char(':')); - if (index != -1) { - testDataSuffix = testFunctionName.mid(index); - testFunctionName = testFunctionName.left(index); - } + // There might be a test data suffix like in "testfunction:testdata1". + QString testFunctionName = matchText; + QString testDataSuffix; + const int index = testFunctionName.indexOf(QLatin1Char(':')); + if (index != -1) { + testDataSuffix = testFunctionName.mid(index); + testFunctionName = testFunctionName.left(index); + } - const QRegExp regExp(testFunctionName, Qt::CaseSensitive, QRegExp::Wildcard); - QStringList matchingFunctions; - foreach (const QString &testFunction, availableTestFunctions) { - if (regExp.exactMatch(testFunction)) - matchingFunctions.append(testFunction); - } - if (!matchingFunctions.isEmpty()) { + const QRegExp regExp(testFunctionName, Qt::CaseSensitive, QRegExp::Wildcard); + QStringList matchingFunctions; + foreach (const QString &testFunction, testFunctions) { + if (regExp.exactMatch(testFunction)) { // If the specified test data is invalid, the QTest framework will // print a reasonable error message for us. - foreach (const QString &matchingFunction, matchingFunctions) - testFunctions.append(matchingFunction + testDataSuffix); + matchingFunctions.append(testFunction + testDataSuffix); + } + } + + return matchingFunctions; +} + +static QObject *objectWithClassName(const QList<QObject *> &objects, const QString &className) +{ + return Utils::findOr(objects, 0, [className] (QObject *object) { + QString candidate = QString::fromUtf8(object->metaObject()->className()); + const int colonIndex = candidate.lastIndexOf(QLatin1Char(':')); + if (colonIndex != -1 && colonIndex < candidate.size() - 1) + candidate = candidate.mid(colonIndex + 1); + return candidate == className; + }); +} + +static int executeTestPlan(const TestPlan &testPlan) +{ + int failedTests = 0; + + TestPlanIterator it(testPlan); + while (it.hasNext()) { + it.next(); + QObject *testObject = it.key(); + QStringList functions = it.value(); + + // Don't run QTest::qExec without any test functions, that'd run *all* slots as tests. + if (functions.isEmpty()) + continue; + + functions.removeDuplicates(); + + // QTest::qExec() expects basically QCoreApplication::arguments(), + QStringList qExecArguments = QStringList() + << QLatin1String("arg0") // fake application name + << QLatin1String("-maxwarnings") << QLatin1String("0"); // unlimit output + qExecArguments << functions; + failedTests += QTest::qExec(testObject, qExecArguments); + } + + return failedTests; +} + +/// Resulting plan consists of all test functions of the plugin object and +/// all test functions of all test objects of the plugin. +static TestPlan generateCompleteTestPlan(IPlugin *plugin, const QList<QObject *> &testObjects) +{ + TestPlan testPlan; + + testPlan.insert(plugin, testFunctions(plugin->metaObject())); + foreach (QObject *testObject, testObjects) { + const QStringList allFunctions = testFunctions(testObject->metaObject()); + testPlan.insert(testObject, allFunctions); + } + + return testPlan; +} + +/// Resulting plan consists of all matching test functions of the plugin object +/// and all matching functions of all test objects of the plugin. However, if a +/// match text denotes a test class, all test functions of that will be +/// included and the class will not be considered further. +/// +/// Since multiple match texts can match the same function, a test function might +/// be included multiple times for a test object. +static TestPlan generateCustomTestPlan(IPlugin *plugin, const QList<QObject *> &testObjects, + const QStringList &matchTexts) +{ + TestPlan testPlan; + + const QStringList testFunctionsOfPluginObject = testFunctions(plugin->metaObject()); + QStringList matchedTestFunctionsOfPluginObject; + QStringList remainingMatchTexts = matchTexts; + QList<QObject *> remainingTestObjectsOfPlugin = testObjects; + + while (!remainingMatchTexts.isEmpty()) { + const QString matchText = remainingMatchTexts.takeFirst(); + bool matched = false; + + if (QObject *testObject = objectWithClassName(remainingTestObjectsOfPlugin, matchText)) { + // Add all functions of the matching test object + matched = true; + testPlan.insert(testObject, testFunctions(testObject->metaObject())); + remainingTestObjectsOfPlugin.removeAll(testObject); + } else { + // Add all matching test functions of all remaining test objects + foreach (QObject *testObject, remainingTestObjectsOfPlugin) { + const QStringList allFunctions = testFunctions(testObject->metaObject()); + const QStringList matchingFunctions = matchingTestFunctions(allFunctions, + matchText); + if (!matchingFunctions.isEmpty()) { + matched = true; + testPlan[testObject] += matchingFunctions; + } + } + } + + const QStringList currentMatchedTestFunctionsOfPluginObject + = matchingTestFunctions(testFunctionsOfPluginObject, matchText); + if (!currentMatchedTestFunctionsOfPluginObject.isEmpty()) { + matched = true; + matchedTestFunctionsOfPluginObject += currentMatchedTestFunctionsOfPluginObject; + } + + if (!matched) { QTextStream out(stdout); - out << "No test function matches \"" << testFunctionName - << "\" for plugin \"" << pluginSpec->name() << "\"." << endl - << " Available test functions for plugin \"" << pluginSpec->name() - << "\" are:" << endl; - foreach (const QString &testFunction, availableTestFunctions) - out << " " << testFunction << endl; + out << "No test function or class matches \"" << matchText + << "\" in plugin \"" << plugin->metaObject()->className() << "\"." << endl; } } - return testFunctions; + // Add all matching test functions of plugin + if (!matchedTestFunctionsOfPluginObject.isEmpty()) + testPlan.insert(plugin, matchedTestFunctionsOfPluginObject); + + return testPlan; } -static int executeTestFunctions(QObject *testObject, const QStringList &functions) +class ExecuteOnDestruction { - // Don't run QTest::qExec without any test functions, that'd run *all* slots as tests. - if (functions.isEmpty()) - return 0; +public: + ExecuteOnDestruction(std::function<void()> code) : destructionCode(code) {} + ~ExecuteOnDestruction() { if (destructionCode) destructionCode(); } - // QTest::qExec() expects basically QCoreApplication::arguments(), - QStringList qExecArguments = QStringList() - << QLatin1String("arg0") // fake application name - << QLatin1String("-maxwarnings") << QLatin1String("0"); // unlimit output - qExecArguments << functions; - return QTest::qExec(testObject, qExecArguments); -} +private: + const std::function<void()> destructionCode; +}; void PluginManagerPrivate::startTests() { @@ -951,25 +1080,21 @@ void PluginManagerPrivate::startTests() } foreach (const PluginManagerPrivate::TestSpec &testSpec, testSpecs) { - const PluginSpec * const pluginSpec = testSpec.pluginSpec; - if (!pluginSpec->plugin()) - continue; + IPlugin *plugin = testSpec.pluginSpec->plugin(); + QTC_ASSERT(plugin, continue); - // Collect all test functions of the plugin. - const QStringList allTestFunctions = testFunctions(pluginSpec->plugin()->metaObject()); + const QList<QObject *> testObjects = plugin->createTestObjects(); + ExecuteOnDestruction deleteTestObjects([&]() { qDeleteAll(testObjects); }); - QStringList testFunctionsToExecute; - // User did not specify any test functions, so add every test function. - if (testSpec.testFunctions.isEmpty()) { - testFunctionsToExecute = allTestFunctions; + const bool hasDuplicateTestObjects = testObjects.size() != testObjects.toSet().size(); + QTC_ASSERT(!hasDuplicateTestObjects, continue); + QTC_ASSERT(!testObjects.contains(plugin), continue); - // User specified test functions. Add them if they are valid. - } else { - testFunctionsToExecute = testFunctionsWantedByUser(pluginSpec, allTestFunctions, - testSpec.testFunctions); - } + const TestPlan testPlan = testSpec.testFunctionsOrObjects.isEmpty() + ? generateCompleteTestPlan(plugin, testObjects) + : generateCustomTestPlan(plugin, testObjects, testSpec.testFunctionsOrObjects); - m_failedTests += executeTestFunctions(pluginSpec->plugin(), testFunctionsToExecute); + m_failedTests += executeTestPlan(testPlan); } if (!testSpecs.isEmpty()) QTimer::singleShot(1, this, SLOT(exitWithNumberOfFailedTests())); diff --git a/src/libs/extensionsystem/pluginmanager_p.h b/src/libs/extensionsystem/pluginmanager_p.h index bb456c86beda4527b3c60e79c75e40063355da26..dcc69e92ab85bb750685a6837fdbcfaa5de77a3e 100644 --- a/src/libs/extensionsystem/pluginmanager_p.h +++ b/src/libs/extensionsystem/pluginmanager_p.h @@ -83,10 +83,10 @@ public: class TestSpec { public: - TestSpec(PluginSpec *pluginSpec, const QStringList &testFunctions = QStringList()) - : pluginSpec(pluginSpec), testFunctions(testFunctions) {} + TestSpec(PluginSpec *pluginSpec, const QStringList &testFunctionsOrObjects = QStringList()) + : pluginSpec(pluginSpec), testFunctionsOrObjects(testFunctionsOrObjects) {} PluginSpec *pluginSpec; - QStringList testFunctions; + QStringList testFunctionsOrObjects; }; bool containsTestSpec(PluginSpec *pluginSpec) const