/* This file is part of the KDE libraries Copyright (c) 2008 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include "kmimeassociations.h" #include #include // We need a factory that returns the same KService::Ptr every time it's asked for a given service. // Otherwise the changes to the service's serviceTypes by KMimeAssociationsTest have no effect class FakeServiceFactory : public KServiceFactory { public: FakeServiceFactory() : KServiceFactory() {} virtual KService::Ptr findServiceByMenuId(const QString &name) { //kDebug() << name; KService::Ptr result = m_cache.value(name); if (!result) { result = KServiceFactory::findServiceByMenuId(name); m_cache.insert(name, result); } //kDebug() << name << result.data(); return result; } virtual KService::Ptr findServiceByDesktopPath(const QString &name) { KService::Ptr result = m_cache.value(name); // yeah, same cache, I don't care :) if (!result) { result = KServiceFactory::findServiceByDesktopPath(name); m_cache.insert(name, result); } return result; } private: QMap m_cache; }; // Helper method for all the trader tests, comes from kmimetypetest.cpp static bool offerListHasService( const KService::List& offers, const QString& entryPath, bool expected /* if set, show error if not found */ ) { bool found = false; Q_FOREACH(const KService::Ptr &serv, offers) { if ( serv->entryPath() == entryPath ) { if( found ) { // should be there only once qWarning( "ERROR: %s was found twice in the list", qPrintable( entryPath ) ); return false; // make test fail } found = true; } } if (!found && expected) { kWarning() << "ERROR:" << entryPath << "not found in offer list. Here's the full list:"; Q_FOREACH(const KService::Ptr &serv, offers) { kDebug() << serv->entryPath(); } } return found; } static void writeAppDesktopFile(const QString& path, const QStringList& mimeTypes, int initialPreference = 1) { KDesktopFile file(path); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "FakeApplication"); group.writeEntry("Type", "Application"); group.writeEntry("Exec", "ls"); group.writeEntry("InitialPreference", initialPreference); group.writeXdgListEntry("MimeType", mimeTypes); } /** * This unit test verifies the parsing of mimeapps.list files, both directly * and via kbuildsycoca (and making trader queries). */ class KMimeAssociationsTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { QString kdehome = QDir::home().canonicalPath() + "/.kde-unit-test"; m_localApps = KStandardDirs::locateLocal("xdgdata-apps", ""); QVERIFY(m_localApps.startsWith(kdehome)); // Create factory on the heap and don't delete it. // It registers to KSycoca, which deletes it at end of program execution. KServiceFactory* factory = new FakeServiceFactory; QCOMPARE(KServiceFactory::self(), factory); // ctor sets s_self bool mustUpdateKSycoca = false; if ( !KSycoca::isAvailable() ) { mustUpdateKSycoca = true; } if (QFile::exists(m_localApps + "/mimeapps.list")) { QFile::remove(m_localApps + "/mimeapps.list"); mustUpdateKSycoca = true; } // Create fake application (associated with text/plain in mimeapps.list) fakeTextApplication = m_localApps + "faketextapplication.desktop"; if (!QFile::exists(fakeTextApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeTextApplication, QStringList() << "text/plain"); } // A fake "default" application for text/plain (high initial preference, but not in mimeapps.list) fakeDefaultTextApplication = m_localApps + "fakedefaulttextapplication.desktop"; if (!QFile::exists(fakeDefaultTextApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeDefaultTextApplication, QStringList() << "text/plain", 9); } // An app (like emacs) listing explicitely the derived mimetype (c-src); not in mimeapps.list // This interacted badly with mimeapps.list listing another app for text/plain, but the // lookup found this app first, due to c-src. The fix: ignoring derived mimetypes when // the base mimetype is already listed. // // Also include aliases (msword), to check they don't cancel each other out. fakeCSrcApplication = m_localApps + "fakecsrcapplication.desktop"; if (!QFile::exists(fakeCSrcApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeCSrcApplication, QStringList() << "text/plain" << "text/c-src" << "application/vnd.ms-word" << "application/msword", 8); } fakeJpegApplication = m_localApps + "fakejpegapplication.desktop"; if (!QFile::exists(fakeJpegApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeJpegApplication, QStringList() << "image/jpeg"); } fakeArkApplication = m_localApps + "fakearkapplication.desktop"; if (!QFile::exists(fakeArkApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeArkApplication, QStringList() << "application/zip"); } fakeHtmlApplication = m_localApps + "fakehtmlapplication.desktop"; if (!QFile::exists(fakeHtmlApplication)) { mustUpdateKSycoca = true; writeAppDesktopFile(fakeHtmlApplication, QStringList() << "text/html"); } if ( mustUpdateKSycoca ) { // Update ksycoca in ~/.kde-unit-test after creating the above runKBuildSycoca(); } // For debugging: print all services and their storageId #if 0 const KService::List lst = KService::allServices(); QVERIFY( !lst.isEmpty() ); Q_FOREACH(const KService::Ptr& serv, lst) { qDebug() << serv->entryPath() << serv->storageId() /*<< serv->desktopEntryName()*/; } #endif KService::Ptr fakeApplicationService = KService::serviceByStorageId("faketextapplication.desktop"); QVERIFY(fakeApplicationService); m_mimeAppsFileContents = "[Added Associations]\n" "image/jpeg=fakejpegapplication.desktop;\n" "text/html=fakehtmlapplication.desktop;\n" // konsole.desktop is without kde4- to test fallback lookup "text/plain=faketextapplication.desktop;kde4-kwrite.desktop;konsole.desktop;idontexist.desktop;\n" // test alias resolution "application/x-pdf=fakejpegapplication.desktop;\n" "[Added KParts/ReadOnlyPart Associations]\n" "text/plain=katepart.desktop;\n" "[Removed Associations]\n" "image/jpeg=firefox.desktop;\n" "text/html=kde4-dolphin.desktop;kde4-kwrite.desktop;\n"; // Expected results preferredApps["image/jpeg"] << "fakejpegapplication.desktop"; preferredApps["application/pdf"] << "fakejpegapplication.desktop"; preferredApps["text/plain"] << "faketextapplication.desktop" << "kde4-kwrite.desktop"; preferredApps["text/x-csrc"] << "faketextapplication.desktop" << "kde4-kwrite.desktop"; preferredApps["text/html"] << "fakehtmlapplication.desktop"; preferredApps["application/msword"] << "fakecsrcapplication.desktop"; removedApps["image/jpeg"] << "firefox.desktop"; removedApps["text/html"] << "kde4-dolphin.desktop" << "kde4-kwrite.desktop"; // Clean-up non-existing apps removeNonExisting(preferredApps); removeNonExisting(removedApps); } void cleanupTestCase() { QFile::remove(m_localApps + "/mimeapps.list"); runKBuildSycoca(); } void testParseSingleFile() { KOfferHash offerHash; KMimeAssociations parser(offerHash); KTemporaryFile tempFile; QVERIFY(tempFile.open()); tempFile.write(m_mimeAppsFileContents); const QString fileName = tempFile.fileName(); tempFile.close(); //QTest::ignoreMessage(QtDebugMsg, "findServiceByDesktopPath: idontexist.desktop not found"); parser.parseMimeAppsList(fileName, 100); for (ExpectedResultsMap::const_iterator it = preferredApps.constBegin(), end = preferredApps.constEnd() ; it != end ; ++it) { const QString mime = it.key(); // The data for derived types and aliases isn't for this test (which only looks at mimeapps.list) if (mime == QLatin1String("text/x-csrc") || mime == QLatin1String("application/msword")) continue; const QList offers = offerHash.offersFor(mime); Q_FOREACH(const QString& service, it.value()) { KService::Ptr serv = KService::serviceByStorageId(service); if (serv && !offersContains(offers, serv)) { kDebug() << "expected offer" << serv->entryPath() << "not in offers for" << mime << ":"; Q_FOREACH(const KServiceOffer& offer, offers) { kDebug() << offer.service()->storageId(); } QFAIL("offer does not have servicetype"); } } } for (ExpectedResultsMap::const_iterator it = removedApps.constBegin(), end = removedApps.constEnd() ; it != end ; ++it) { const QString mime = it.key(); const QList offers = offerHash.offersFor(mime); Q_FOREACH(const QString& service, it.value()) { KService::Ptr serv = KService::serviceByStorageId(service); if (serv && offersContains(offers, serv)) { kDebug() << serv.data() << serv->entryPath() << "does not have" << mime; QFAIL("offer should not have servicetype"); } } } } void testGlobalAndLocalFiles() { KOfferHash offerHash; KMimeAssociations parser(offerHash); // Write global file KTemporaryFile tempFileGlobal; QVERIFY(tempFileGlobal.open()); QByteArray globalAppsFileContents = "[Added Associations]\n" "image/jpeg=firefox.desktop;\n" // removed by local config "text/html=firefox.desktop;\n" // mdv "image/png=fakejpegapplication.desktop;\n"; tempFileGlobal.write(globalAppsFileContents); const QString globalFileName = tempFileGlobal.fileName(); tempFileGlobal.close(); // We didn't keep it, so we need to write the local file again KTemporaryFile tempFile; QVERIFY(tempFile.open()); tempFile.write(m_mimeAppsFileContents); const QString fileName = tempFile.fileName(); tempFile.close(); parser.parseMimeAppsList(globalFileName, 1000); parser.parseMimeAppsList(fileName, 1050); // += 50 is correct. QList offers = offerHash.offersFor("image/jpeg"); qStableSort(offers); // like kbuildservicefactory.cpp does const QStringList expectedJpegApps = preferredApps["image/jpeg"]; QCOMPARE(assembleOffers(offers), expectedJpegApps); offers = offerHash.offersFor("text/html"); qStableSort(offers); QStringList textHtmlApps = preferredApps["text/html"]; if (KService::serviceByStorageId("firefox.desktop")) textHtmlApps.append("firefox.desktop"); qDebug() << assembleOffers(offers); QCOMPARE(assembleOffers(offers), textHtmlApps); offers = offerHash.offersFor("image/png"); qStableSort(offers); QCOMPARE(assembleOffers(offers), QStringList() << "fakejpegapplication.desktop"); } void testSetupRealFile() { writeToMimeApps(m_mimeAppsFileContents); // Test a trader query KService::List offers = KMimeTypeTrader::self()->query("image/jpeg"); //kDebug() << m_mimeAppsFileContents; //kDebug() << "preferred apps for jpeg: " << preferredApps.value("image/jpeg"); //for (int i = 0; i < offers.count(); ++i) { // kDebug() << "offers for" << "image/jpeg" << ":" << i << offers[i]->storageId(); //} QCOMPARE(offers.first()->storageId(), QString("fakejpegapplication.desktop")); // Now the generic variant of the above test: // for each mimetype, check that the preferred apps are as specified for (ExpectedResultsMap::const_iterator it = preferredApps.constBegin(), end = preferredApps.constEnd() ; it != end ; ++it) { const QString mime = it.key(); kDebug() << "offers for" << mime << ":"; const KService::List offers = KMimeTypeTrader::self()->query(mime); for (int i = 0; i < offers.count(); ++i) { kDebug() << " " << i << ":" << offers[i]->storageId(); } const QStringList offerIds = assembleServices(offers, it.value().count()); kDebug() << " Expected:" << it.value(); QCOMPARE(offerIds, it.value()); //const QStringList expectedPreferredServices = it.value(); //for (int i = 0; i < expectedPreferredServices.count(); ++i) { //kDebug() << mime << i << expectedPreferredServices[i]; // QCOMPARE(expectedPreferredServices[i], offers[i]->storageId()); //} } } void testMultipleInheritance() { // application/x-shellscript inherits from both text/plain and application/x-executable KService::List offers = KMimeTypeTrader::self()->query("application/x-shellscript"); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); } void testRemoveAssociationFromParent() { // I removed kate from text/plain, and it would still appear in text/x-java. // First, let's check our fake app is associated with text/plain KService::List offers = KMimeTypeTrader::self()->query("text/plain"); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "text/plain=faketextapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query("text/plain"); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); offers = KMimeTypeTrader::self()->query("text/x-java"); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); } void testRemovedImplicitAssociation() // remove (implicit) assoc from derived mimetype { // #164584: Removing ark from opendocument.text didn't work const QString opendocument = "application/vnd.oasis.opendocument.text"; KService::List offers = KMimeTypeTrader::self()->query(opendocument); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "application/vnd.oasis.opendocument.text=fakearkapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(opendocument); QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); offers = KMimeTypeTrader::self()->query("application/zip"); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); } void testRemovedImplicitAssociation178560() { // #178560: Removing ark from interface/x-winamp-skin didn't work // Using application/x-kns (another zip-derived mimetype) nowadays. const QString mime = "application/x-kns"; KService::List offers = KMimeTypeTrader::self()->query(mime); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); writeToMimeApps(QByteArray("[Removed Associations]\n" "application/x-kns=fakearkapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query(mime); QVERIFY(!offerListHasService(offers, fakeArkApplication, false)); offers = KMimeTypeTrader::self()->query("application/zip"); QVERIFY(offerListHasService(offers, fakeArkApplication, true)); } // remove assoc from a mime which is both a parent and a derived mimetype void testRemovedMiddleAssociation() { // More tricky: x-theme inherits x-desktop inherits text/plain, // if we remove an association for x-desktop then x-theme shouldn't // get it from text/plain... KService::List offers; writeToMimeApps(QByteArray("[Removed Associations]\n" "application/x-desktop=faketextapplication.desktop;\n")); offers = KMimeTypeTrader::self()->query("text/plain"); QVERIFY(offerListHasService(offers, fakeTextApplication, true)); offers = KMimeTypeTrader::self()->query("application/x-desktop"); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); offers = KMimeTypeTrader::self()->query("application/x-theme"); QVERIFY(!offerListHasService(offers, fakeTextApplication, false)); } private: typedef QMap ExpectedResultsMap; void runKBuildSycoca() { //kDebug(); // Wait for notifyDatabaseChanged DBus signal // (The real KCM code simply does the refresh in a slot, asynchronously) QEventLoop loop; QObject::connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), &loop, SLOT(quit())); KProcess proc; const QString kbuildsycoca = KStandardDirs::findExe(KBUILDSYCOCA_EXENAME); QVERIFY(!kbuildsycoca.isEmpty()); proc << kbuildsycoca; proc.setOutputChannelMode(KProcess::MergedChannels); // silence kbuildsycoca output proc.execute(); loop.exec(); } void writeToMimeApps(const QByteArray& contents) { QString mimeAppsPath = m_localApps + "/mimeapps.list"; QFile mimeAppsFile(mimeAppsPath); QVERIFY(mimeAppsFile.open(QIODevice::WriteOnly)); mimeAppsFile.write(contents); mimeAppsFile.close(); runKBuildSycoca(); } static bool offersContains(const QList& offers, KService::Ptr serv) { Q_FOREACH(const KServiceOffer& offer, offers) { if (offer.service()->storageId() == serv->storageId()) return true; } return false; } static QStringList assembleOffers(const QList& offers) { QStringList lst; Q_FOREACH(const KServiceOffer& offer, offers) { lst.append(offer.service()->storageId()); } return lst; } static QStringList assembleServices(const QList& services, int maxCount = -1) { QStringList lst; Q_FOREACH(const KService::Ptr& service, services) { lst.append(service->storageId()); if (maxCount > -1 && lst.count() == maxCount) break; } return lst; } void removeNonExisting(ExpectedResultsMap& erm) { for (ExpectedResultsMap::iterator it = erm.begin(), end = erm.end() ; it != end ; ++it) { QMutableStringListIterator serv_it( it.value() ); while (serv_it.hasNext()) { if (!KService::serviceByStorageId(serv_it.next())) { kDebug() << "removing non-existing entry" << serv_it.value(); serv_it.remove(); } } } } QString m_localApps; QByteArray m_mimeAppsFileContents; QString fakeTextApplication; QString fakeDefaultTextApplication; QString fakeCSrcApplication; QString fakeJpegApplication; QString fakeHtmlApplication; QString fakeArkApplication; ExpectedResultsMap preferredApps; ExpectedResultsMap removedApps; }; QTEST_KDEMAIN_CORE( KMimeAssociationsTest ) #include "kmimeassociationstest.moc"