SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTest>
#include "kcoreaddons_debug.h"
#include <kaboutdata.h>
#include <kpluginmetadata.h>
#include <QLocale>
#include <QLoggingCategory>
class LibraryPathRestorer
{
public:
explicit LibraryPathRestorer(const QStringList &paths)
: mPaths(paths)
{
}
~LibraryPathRestorer()
{
QCoreApplication::setLibraryPaths(mPaths);
}
private:
QStringList mPaths;
};
class KPluginMetaDataTest : public QObject
{
Q_OBJECT
bool m_canMessage = false;
Q_REQUIRED_RESULT bool doMessagesWork()
{
auto internalCheck = [this] {
if (qEnvironmentVariableIsSet("QT_MESSAGE_PATTERN")) {
QSKIP("QT_MESSAGE_PATTERN prevents warning expectations from matching");
}
if (qEnvironmentVariableIsSet("QT_LOGGING_RULES")) {
QSKIP("QT_LOGGING_RULES prevents warning expectations from matching");
}
if (qEnvironmentVariableIsSet("QT_LOGGING_CONF")) {
QSKIP("QT_LOGGING_CONF prevents warning expectations from matching");
}
m_canMessage = true;
QLoggingCategory::setFilterRules(QStringLiteral("kf.*=true"));
};
internalCheck();
return m_canMessage;
}
private Q_SLOTS:
void testFromPluginLoader()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
QString location;
location = QPluginLoader(QStringLiteral("namespace/jsonplugin_cmake_macro")).fileName();
QVERIFY2(!location.isEmpty(), "Could not find jsonplugin");
QString jsonLocation = QFINDTESTDATA("data/jsonplugin.json");
QVERIFY2(!jsonLocation.isEmpty(), "Could not find jsonplugin.json");
QFile jsonFile(jsonLocation);
QVERIFY(jsonFile.open(QFile::ReadOnly));
QJsonParseError e;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll(), &e);
QCOMPARE(e.error, QJsonParseError::NoError);
location = QFileInfo(location).absoluteFilePath();
KPluginMetaData fromQPluginLoader(QPluginLoader(QStringLiteral("namespace/jsonplugin_cmake_macro")));
KPluginMetaData fromFullPath(location);
KPluginMetaData fromRelativePath(QStringLiteral("namespace/jsonplugin_cmake_macro"));
KPluginMetaData fromRawData(jsonDoc.object(), location);
auto description = QStringLiteral("This is a plugin");
QVERIFY(fromQPluginLoader.isValid());
QCOMPARE(fromQPluginLoader.description(), description);
QVERIFY(fromFullPath.isValid());
QCOMPARE(fromFullPath.description(), description);
QVERIFY(fromRelativePath.isValid());
QCOMPARE(fromRelativePath.description(), description);
QVERIFY(fromRawData.isValid());
QCOMPARE(fromRawData.description(), description);
QCOMPARE(fromRawData, fromRawData);
QCOMPARE(fromQPluginLoader, fromQPluginLoader);
QCOMPARE(fromFullPath, fromFullPath);
QCOMPARE(fromQPluginLoader, fromFullPath);
QCOMPARE(fromQPluginLoader, fromRawData);
QCOMPARE(fromFullPath, fromQPluginLoader);
QCOMPARE(fromFullPath, fromRawData);
QCOMPARE(fromRawData, fromQPluginLoader);
QCOMPARE(fromRawData, fromFullPath);
QVERIFY(!KPluginMetaData(QPluginLoader(QStringLiteral("doesnotexist"))).isValid());
QVERIFY(!KPluginMetaData(QJsonObject(), QString()).isValid());
}
void testAllKeys()
{
QJsonParseError e;
QJsonObject jo = QJsonDocument::fromJson(
"{\n"
" \"KPlugin\": {\n"
" \"Name\": \"Date and Time\",\n"
" \"Description\": \"Date and time by timezone\",\n"
" \"Icon\": \"preferences-system-time\",\n"
" \"Authors\": { \"Name\": \"Aaron Seigo\", \"Email\": \"aseigo@kde.org\" },\n"
" \"Translators\": { \"Name\": \"No One\", \"Email\": \"no.one@kde.org\" },\n"
" \"OtherContributors\": { \"Name\": \"No One\", \"Email\": \"no.one@kde.org\" },\n"
" \"Category\": \"Date and Time\",\n"
" \"EnabledByDefault\": \"true\",\n"
" \"ExtraInformation\": \"Something else\",\n"
" \"License\": \"LGPL\",\n"
" \"Copyright\": \"(c) Alex Richardson 2015\",\n"
" \"Id\": \"time\",\n"
" \"Version\": \"1.0\",\n"
" \"Website\": \"https://plasma.kde.org/\",\n"
" \"MimeTypes\": [ \"image/png\" ]\n"
" }\n}\n",
&e)
.object();
QCOMPARE(e.error, QJsonParseError::NoError);
KPluginMetaData m(jo, QString());
QVERIFY(m.isValid());
QCOMPARE(m.pluginId(), QStringLiteral("time"));
QCOMPARE(m.name(), QStringLiteral("Date and Time"));
QCOMPARE(m.description(), QStringLiteral("Date and time by timezone"));
QCOMPARE(m.iconName(), QStringLiteral("preferences-system-time"));
QCOMPARE(m.category(), QStringLiteral("Date and Time"));
QCOMPARE(m.authors().size(), 1);
QCOMPARE(m.authors().constFirst().name(), QStringLiteral("Aaron Seigo"));
QCOMPARE(m.authors().constFirst().emailAddress(), QStringLiteral("aseigo@kde.org"));
QCOMPARE(m.translators().size(), 1);
QCOMPARE(m.translators().constFirst().name(), QStringLiteral("No One"));
QCOMPARE(m.translators().constFirst().emailAddress(), QStringLiteral("no.one@kde.org"));
QCOMPARE(m.otherContributors().size(), 1);
QCOMPARE(m.otherContributors().constFirst().name(), QStringLiteral("No One"));
QCOMPARE(m.otherContributors().constFirst().emailAddress(), QStringLiteral("no.one@kde.org"));
QVERIFY(m.isEnabledByDefault());
QCOMPARE(m.license(), QStringLiteral("LGPL"));
QCOMPARE(m.copyrightText(), QStringLiteral("(c) Alex Richardson 2015"));
QCOMPARE(m.version(), QStringLiteral("1.0"));
QCOMPARE(m.website(), QStringLiteral("https://plasma.kde.org/"));
QCOMPARE(m.mimeTypes(), QStringList(QStringLiteral("image/png")));
}
void testTranslations()
{
QJsonParseError e;
QJsonObject jo = QJsonDocument::fromJson(
"{ \"KPlugin\": {\n"
"\"Name\": \"Name\",\n"
"\"Name[de]\": \"Name (de)\",\n"
"\"Name[de_DE]\": \"Name (de_DE)\",\n"
"\"Description\": \"Description\",\n"
"\"Description[de]\": \"Beschreibung (de)\",\n"
"\"Description[de_DE]\": \"Beschreibung (de_DE)\"\n"
"}\n}",
&e)
.object();
KPluginMetaData m(jo, QString());
QLocale::setDefault(QLocale::c());
QCOMPARE(m.name(), QStringLiteral("Name"));
QCOMPARE(m.description(), QStringLiteral("Description"));
QLocale::setDefault(QLocale(QStringLiteral("de_DE")));
QCOMPARE(m.name(), QStringLiteral("Name (de_DE)"));
QCOMPARE(m.description(), QStringLiteral("Beschreibung (de_DE)"));
QLocale::setDefault(QLocale(QStringLiteral("de_CH")));
QCOMPARE(m.name(), QStringLiteral("Name (de)"));
QCOMPARE(m.description(), QStringLiteral("Beschreibung (de)"));
QLocale::setDefault(QLocale(QStringLiteral("fr_FR")));
QCOMPARE(m.name(), QStringLiteral("Name"));
QCOMPARE(m.description(), QStringLiteral("Description"));
}
void testReadStringList()
{
if (!doMessagesWork()) {
return;
}
QJsonParseError e;
QJsonObject jo = QJsonDocument::fromJson(
"{\n"
"\"String\": \"foo\",\n"
"\"OneArrayEntry\": [ \"foo\" ],\n"
"\"Bool\": true,\n"
"\"QuotedBool\": \"true\",\n"
"\"Number\": 12345,\n"
"\"QuotedNumber\": \"12345\",\n"
"\"EmptyArray\": [],\n"
"\"NumberArray\": [1, 2, 3],\n"
"\"BoolArray\": [true, false, true],\n"
"\"StringArray\": [\"foo\", \"bar\"],\n"
"\"Null\": null,\n"
"\"QuotedNull\": \"null\",\n"
"\"ArrayWithNull\": [ \"foo\", null, \"bar\"],\n"
"\"Object\": { \"foo\": \"bar\" }\n"
"}",
&e)
.object();
QCOMPARE(e.error, QJsonParseError::NoError);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Expected JSON property ")));
KPluginMetaData data(jo, QStringLiteral("test"));
QCOMPARE(data.value(QStringLiteral("String"), QStringList()), QStringList(QStringLiteral("foo")));
QCOMPARE(data.value(QStringLiteral("OneArrayEntry"), QStringList()), QStringList(QStringLiteral("foo")));
QCOMPARE(data.value(QStringLiteral("Bool"), QStringList()), QStringList(QStringLiteral("true")));
QCOMPARE(data.value(QStringLiteral("QuotedBool"), QStringList()), QStringList(QStringLiteral("true")));
QCOMPARE(data.value(QStringLiteral("Number"), QStringList()), QStringList(QStringLiteral("12345")));
QCOMPARE(data.value(QStringLiteral("QuotedNumber"), QStringList()), QStringList(QStringLiteral("12345")));
QCOMPARE(data.value(QStringLiteral("EmptyArray"), QStringList()), QStringList());
QCOMPARE(data.value(QStringLiteral("NumberArray"), QStringList()), QStringList() << QStringLiteral("1") << QStringLiteral("2") << QStringLiteral("3"));
QCOMPARE(data.value(QStringLiteral("BoolArray"), QStringList()),
QStringList() << QStringLiteral("true") << QStringLiteral("false") << QStringLiteral("true"));
QCOMPARE(data.value(QStringLiteral("StringArray"), QStringList()), QStringList() << QStringLiteral("foo") << QStringLiteral("bar"));
QCOMPARE(data.value(QStringLiteral("Null"), QStringList()), QStringList());
QCOMPARE(data.value(QStringLiteral("QuotedNull"), QStringList()), QStringList(QStringLiteral("null")));
QCOMPARE(data.value(QStringLiteral("ArrayWithNull"), QStringList()), QStringList() << QStringLiteral("foo") << QString() << QStringLiteral("bar"));
QCOMPARE(data.value(QStringLiteral("Object"), QStringList()), QStringList());
}
void testJSONMetadata()
{
const QString inputPath = QFINDTESTDATA("data/testmetadata.json");
KPluginMetaData md = KPluginMetaData::fromJsonFile(inputPath);
QVERIFY(md.isValid());
QCOMPARE(md.name(), QStringLiteral("Test"));
QCOMPARE(md.value(QStringLiteral("X-Plasma-MainScript")), QStringLiteral("ui/main.qml"));
QJsonArray expected;
expected.append(QStringLiteral("Export"));
QCOMPARE(md.rawData().value(QStringLiteral("X-Purpose-PluginTypes")).toArray(), expected);
QCOMPARE(md.value(QStringLiteral("SomeInt"), 24), 42);
QCOMPARE(md.value(QStringLiteral("SomeIntAsString"), 24), 42);
QCOMPARE(md.value(QStringLiteral("SomeStringNotAInt"), 24), 24);
QCOMPARE(md.value(QStringLiteral("DoesNotExist"), 24), 24);
QVERIFY(md.value(QStringLiteral("SomeBool"), false));
QVERIFY(!md.value(QStringLiteral("SomeBoolThatIsFalse"), true));
QVERIFY(md.value(QStringLiteral("SomeBoolAsString"), false));
QVERIFY(md.value(QStringLiteral("DoesNotExist"), true));
}
void testPathIsAbsolute_data()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
QTest::addColumn<QString>("inputAbsolute");
QTest::addColumn<QString>("pluginPath");
QTest::newRow("json") << QFINDTESTDATA("data/testmetadata.json") << QFINDTESTDATA("data/testmetadata.json");
QPluginLoader shlibLoader(QCoreApplication::applicationDirPath() + QStringLiteral("/namespace/jsonplugin_cmake_macro"));
QVERIFY2(!shlibLoader.fileName().isEmpty(), "Could not find jsonplugin_cmake_macro");
QString shlibPath = QFileInfo(shlibLoader.fileName()).absoluteFilePath();
QTest::newRow("library") << shlibPath << shlibPath;
}
void testPathIsAbsolute()
{
QFETCH(QString, inputAbsolute);
QVERIFY2(QDir::isAbsolutePath(inputAbsolute), qPrintable(inputAbsolute));
QFETCH(QString, pluginPath);
const auto createMetaData = [](const QString &path) {
if (path.endsWith(QLatin1String(".json"))) {
return KPluginMetaData::fromJsonFile(path);
} else {
return KPluginMetaData(path);
}
};
KPluginMetaData mdAbsolute = createMetaData(inputAbsolute);
QVERIFY(mdAbsolute.isValid());
QCOMPARE(mdAbsolute.fileName(), pluginPath);
QString inputRelative;
if (QLibrary::isLibrary(inputAbsolute)) {
inputRelative = QStringLiteral("namespace/") + QFileInfo(inputAbsolute).baseName();
} else {
inputRelative = QDir::current().relativeFilePath(inputAbsolute);
}
QVERIFY2(QDir::isRelativePath(inputRelative), qPrintable(inputRelative));
KPluginMetaData mdRelative = createMetaData(inputRelative);
QVERIFY(mdRelative.isValid());
QCOMPARE(mdRelative.fileName(), pluginPath);
const QJsonObject json = mdAbsolute.rawData();
QString pluginRelative = QDir::current().relativeFilePath(pluginPath);
QVERIFY2(QDir::isRelativePath(pluginRelative), qPrintable(pluginRelative));
KPluginMetaData mdFromJson1(json, pluginRelative);
KPluginMetaData mdFromJson2(json, inputRelative);
QCOMPARE(mdFromJson1.fileName(), pluginRelative);
QCOMPARE(mdFromJson2.fileName(), inputRelative);
}
void testFindPlugins()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
auto sortPlugins = [](const KPluginMetaData &a, const KPluginMetaData &b) {
return a.pluginId() < b.pluginId();
};
auto plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"));
std::sort(plugins.begin(), plugins.end(), sortPlugins);
QCOMPARE(plugins.size(), 2);
QCOMPARE(plugins[0].pluginId(), QStringLiteral("jsonplugin_cmake_macro"));
QCOMPARE(plugins[0].description(), QStringLiteral("This is a plugin"));
QCOMPARE(plugins[1].pluginId(), QStringLiteral("qtplugin"));
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), [](const KPluginMetaData &) {
return false;
});
std::sort(plugins.begin(), plugins.end(), sortPlugins);
QCOMPARE(plugins.size(), 0);
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), [](const KPluginMetaData &) {
return true;
});
std::sort(plugins.begin(), plugins.end(), sortPlugins);
QCOMPARE(plugins.size(), 2);
auto supportTextPlain = [](const KPluginMetaData &metaData) {
return metaData.supportsMimeType(QStringLiteral("text/plain"));
};
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), supportTextPlain);
QCOMPARE(plugins.size(), 1);
QCOMPARE(plugins[0].pluginId(), QStringLiteral("jsonplugin_cmake_macro"));
auto supportTextHtml = [](const KPluginMetaData &metaData) {
return metaData.supportsMimeType(QStringLiteral("text/html"));
};
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), supportTextHtml);
std::sort(plugins.begin(), plugins.end(), sortPlugins);
QCOMPARE(plugins.size(), 2);
QCOMPARE(plugins[0].pluginId(), QStringLiteral("jsonplugin_cmake_macro"));
QCOMPARE(plugins[1].pluginId(), QStringLiteral("qtplugin"));
auto supportDoesNotExist = [](const KPluginMetaData &metaData) {
return metaData.supportsMimeType(QStringLiteral("does/not/exist"));
};
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), supportDoesNotExist);
QCOMPARE(plugins.size(), 0);
KPluginMetaData plugin = KPluginMetaData::findPluginById(QStringLiteral("namespace"), QStringLiteral("invalidid"));
QVERIFY(!plugin.isValid());
plugins = KPluginMetaData::findPlugins(QCoreApplication::applicationDirPath() + QStringLiteral("/namespace"));
std::sort(plugins.begin(), plugins.end(), sortPlugins);
QCOMPARE(plugins.size(), 2);
QCOMPARE(plugins[0].pluginId(), QStringLiteral("jsonplugin_cmake_macro"));
QCOMPARE(plugins[1].pluginId(), QStringLiteral("qtplugin"));
const KPluginMetaData validPlugin = KPluginMetaData::findPluginById(QStringLiteral("namespace"), QStringLiteral("jsonplugin_cmake_macro"));
QVERIFY(validPlugin.isValid());
QCOMPARE(plugins[0].pluginId(), QStringLiteral("jsonplugin_cmake_macro"));
}
void testStaticPlugins()
{
#if defined(QT_SHARED)
QCOMPARE(QPluginLoader::staticPlugins().count(), 0);
#endif
const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("staticnamespace"));
QCOMPARE(plugins.count(), 1);
QCOMPARE(plugins.first().description(), QStringLiteral("This is a plugin"));
QCOMPARE(plugins.first().fileName(), QStringLiteral("staticnamespace/static_jsonplugin_cmake_macro"));
}
void testPluginsWithoutMetaData()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
KPluginMetaData emptyMetaData(QStringLiteral("namespace/pluginwithoutmetadata"), KPluginMetaData::AllowEmptyMetaData);
QVERIFY(emptyMetaData.isValid());
QCOMPARE(emptyMetaData.pluginId(), QStringLiteral("pluginwithoutmetadata"));
const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), {}, KPluginMetaData::AllowEmptyMetaData);
QCOMPARE(plugins.count(), 3);
for (auto plugin : plugins) {
QVERIFY(plugin.isValid());
if (plugin.pluginId() == QLatin1String("pluginwithoutmetadata")) {
QVERIFY(plugin.rawData().isEmpty());
} else if (plugin.pluginId() == QLatin1String("jsonplugin_cmake_macro") || plugin.pluginId() == QLatin1String("qtplugin")) {
QVERIFY(!plugin.rawData().isEmpty());
} else {
QVERIFY2(false, "should not be reachable");
}
}
const auto pluginInvalid = KPluginMetaData::findPluginById(QStringLiteral("namespace"), QStringLiteral("pluginwithoutmetadata"));
const auto pluginValid = KPluginMetaData::findPluginById(QStringLiteral("namespace"),
QStringLiteral("pluginwithoutmetadata"),
KPluginMetaData::AllowEmptyMetaData);
QVERIFY(!pluginInvalid.isValid());
QVERIFY(pluginValid.isValid());
}
void testStaticPluginsWithoutMetadata()
{
QVERIFY(KPluginMetaData::findPlugins(QStringLiteral("staticnamespace3")).isEmpty());
const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("staticnamespace3"), {}, KPluginMetaData::AllowEmptyMetaData);
QCOMPARE(plugins.count(), 1);
QVERIFY(plugins.first().isValid());
QCOMPARE(plugins.first().pluginId(), QStringLiteral("static_plugin_without_metadata"));
}
void testReverseDomainNotationPluginId()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
KPluginMetaData data(QStringLiteral("org.kde.test"));
QVERIFY(data.isValid());
QCOMPARE(data.pluginId(), QStringLiteral("org.kde.test"));
}
void testReverseDomanNotationStaticPlugin()
{
KPluginMetaData data = KPluginMetaData::findPluginById(QStringLiteral("rdnstatic"), QStringLiteral("org.kde.test-staticplugin"));
QVERIFY(data.isValid());
QCOMPARE(data.pluginId(), QStringLiteral("org.kde.test-staticplugin"));
}
void testFindingPluginInAppDirFirst()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
const QString originalPluginPath = QPluginLoader(QStringLiteral("namespace/jsonplugin_cmake_macro")).fileName();
const QString pluginFileName = QFileInfo(originalPluginPath).fileName();
const QString pluginNamespace = QStringLiteral("somepluginnamespace");
const QString pluginAppDir = QCoreApplication::applicationDirPath() + QLatin1Char('/') + pluginNamespace;
QDir(pluginAppDir).mkpath(QStringLiteral("."));
const QString pluginAppPath = pluginAppDir + QLatin1Char('/') + pluginFileName;
QFile::remove(pluginAppPath);
QVERIFY(QFile::copy(originalPluginPath, pluginAppPath));
QTemporaryDir temp;
QVERIFY(temp.isValid());
QDir dir(temp.path());
QVERIFY(dir.mkdir(pluginNamespace));
QVERIFY(dir.cd(pluginNamespace));
const QString pluginInNamespacePath = dir.absoluteFilePath(pluginFileName);
QVERIFY(QFile::copy(originalPluginPath, pluginInNamespacePath));
LibraryPathRestorer restorer(QCoreApplication::libraryPaths());
QCoreApplication::setLibraryPaths(QStringList() << temp.path());
const QString relativePathWithNamespace = QStringLiteral("somepluginnamespace/jsonplugin_cmake_macro");
KPluginMetaData data(relativePathWithNamespace);
QVERIFY(data.isValid());
QCOMPARE(data.fileName(), pluginAppPath);
QVERIFY(KPluginMetaData(pluginInNamespacePath).isValid());
QVERIFY(QFile::remove(pluginAppPath));
QCOMPARE(KPluginMetaData(relativePathWithNamespace).fileName(), pluginInNamespacePath);
}
void testMetaDataQDebugOperator()
{
#if !defined(QT_SHARED)
QSKIP("Dynamic plugin loading not supported with a static Qt build");
#endif
const auto list = KPluginMetaData::findPlugins(QStringLiteral("namespace"));
qDebug() << list.first();
qDebug() << list;
qDebug() << (QList<KPluginMetaData>() << list << list << list);
}
void benchmarkFindPlugins()
{
QSKIP("Skipped by default");
int loopIterations = 10;
QBENCHMARK {
for (int i = 0; i < loopIterations; ++i) {
const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), {}, KPluginMetaData::AllowEmptyMetaData);
Q_UNUSED(plugins)
}
}
QList<KPluginMetaData> plugins =
KPluginMetaData::findPlugins(QStringLiteral("namespace"), {}, KPluginMetaData::AllowEmptyMetaData | KPluginMetaData::CacheMetaData);
QBENCHMARK {
for (int i = 0; i < loopIterations; ++i) {
plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), {}, KPluginMetaData::AllowEmptyMetaData | KPluginMetaData::CacheMetaData);
}
}
}
};
QTEST_MAIN(KPluginMetaDataTest)
#include "kpluginmetadatatest.moc"