diff --git a/kdecore/config/kconfig.cpp b/kdecore/config/kconfig.cpp index 7ea26a5480..b30584b302 100644 --- a/kdecore/config/kconfig.cpp +++ b/kdecore/config/kconfig.cpp @@ -1,918 +1,888 @@ /* This file is part of the KDE libraries Copyright (c) 2006, 2007 Thomas Braxton Copyright (c) 1999 Preston Brown Copyright (c) 1997-1999 Matthias Kalle Dalheimer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 Library 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 "kconfig.h" #include "kconfig_p.h" #include #include #include #include "kconfigbackend.h" #include "kconfiggroup.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include bool KConfigPrivate::mappingsRegistered=false; KConfigPrivate::KConfigPrivate(const KComponentData &componentData_, KConfig::OpenFlags flags, const char* resource) : openFlags(flags), resourceType(resource), mBackend(0), bDynamicBackend(true), bDirty(false), bReadDefaults(false), bFileImmutable(false), bForceGlobal(false), bSuppressGlobal(false), componentData(componentData_), configState(KConfigBase::NoAccess) { sGlobalFileName = componentData.dirs()->saveLocation("config", QString(), false) + QLatin1String("kdeglobals"); static int use_etc_kderc = -1; if (use_etc_kderc < 0) use_etc_kderc = getenv("KDE_SKIP_KDERC") != 0 ? 0 : 1; // for unit tests if (use_etc_kderc) { etc_kderc = #ifdef Q_WS_WIN QFile::decodeName( qgetenv("WINDIR") + "/kde4rc" ); #else QLatin1String("/etc/kde4rc"); #endif if (!KStandardDirs::checkAccess(etc_kderc, R_OK)) { etc_kderc.clear(); } } // if (!mappingsRegistered) { // KEntryMap tmp; // if (!etc_kderc.isEmpty()) { // KSharedPtr backend = KConfigBackend::create(componentData, etc_kderc, QLatin1String("INI")); // backend->parseConfig( "en_US", tmp, KConfigBackend::ParseDefaults); // } // const QString kde4rc(QDir::home().filePath(".kde4rc")); // if (KStandardDirs::checkAccess(kde4rc, R_OK)) { // KSharedPtr backend = KConfigBackend::create(componentData, kde4rc, QLatin1String("INI")); // backend->parseConfig( "en_US", tmp, KConfigBackend::ParseOptions()); // } // KConfigBackend::registerMappings(tmp); // mappingsRegistered = true; // } setLocale(KGlobal::hasLocale() ? KGlobal::locale()->language() : KLocale::defaultLanguage()); } bool KConfigPrivate::lockLocal() { if (mBackend) { return mBackend->lock(componentData); } // anonymous object - pretend we locked it return true; } void KConfigPrivate::copyGroup(const QByteArray& source, const QByteArray& destination, KConfigGroup *otherGroup, KConfigBase::WriteConfigFlags flags) const { KEntryMap& otherMap = otherGroup->config()->d_ptr->entryMap; const int len = source.length(); const bool sameName = (destination == source); // we keep this bool outside the foreach loop so that if // the group is empty, we don't end up marking the other config // as dirty erroneously bool dirtied = false; for (KEntryMap::ConstIterator entryMapIt( entryMap.constBegin() ); entryMapIt != entryMap.constEnd(); ++entryMapIt) { const QByteArray& group = entryMapIt.key().mGroup; if (!group.startsWith(source)) // nothing to do continue; // don't copy groups that start with the same prefix, but are not sub-groups if (group.length() > len && group[len] != '\x1d') continue; KEntryKey newKey = entryMapIt.key(); if (flags & KConfigBase::Localized) { newKey.bLocal = true; } if (!sameName) newKey.mGroup.replace(0, len, destination); KEntry entry = entryMap[ entryMapIt.key() ]; dirtied = entry.bDirty = flags & KConfigBase::Persistent; if (flags & KConfigBase::Global) { entry.bGlobal = true; } otherMap[newKey] = entry; } if (dirtied) { otherGroup->config()->d_ptr->bDirty = true; } } QString KConfigPrivate::expandString(const QString& value) { QString aValue = value; // check for environment variables and make necessary translations int nDollarPos = aValue.indexOf( QLatin1Char('$') ); while( nDollarPos != -1 && nDollarPos+1 < aValue.length()) { // there is at least one $ - if( aValue[nDollarPos+1] == QLatin1Char('(') ) { - int nEndPos = nDollarPos+1; - // the next character is not $ - while ( (nEndPos <= aValue.length()) && (aValue[nEndPos]!=QLatin1Char(')')) ) - nEndPos++; - nEndPos++; - QString cmd = aValue.mid( nDollarPos+2, nEndPos-nDollarPos-3 ); - - QString result; - QByteArray oldpath = qgetenv( "PATH" ); - QByteArray newpath; - if (KGlobal::hasMainComponent()) { - newpath = QFile::encodeName(KGlobal::dirs()->resourceDirs("exe").join(QChar::fromLatin1(KPATH_SEPARATOR))); - if (!newpath.isEmpty() && !oldpath.isEmpty()) - newpath += KPATH_SEPARATOR; - } - newpath += oldpath; - setenv( "PATH", newpath, 1/*overwrite*/ ); -// FIXME: wince does not have pipes -#ifndef _WIN32_WCE - FILE *fs = popen(QFile::encodeName(cmd).data(), "r"); - if (fs) { - QTextStream ts(fs, QIODevice::ReadOnly); - result = ts.readAll().trimmed(); - pclose(fs); - } -#endif - setenv( "PATH", oldpath, 1/*overwrite*/ ); - aValue.replace( nDollarPos, nEndPos-nDollarPos, result ); - nDollarPos += result.length(); - } else if( aValue[nDollarPos+1] != QLatin1Char('$') ) { + if( aValue[nDollarPos+1] != QLatin1Char('$') ) { int nEndPos = nDollarPos+1; // the next character is not $ QString aVarName; if ( aValue[nEndPos] == QLatin1Char('{') ) { while ( (nEndPos <= aValue.length()) && (aValue[nEndPos] != QLatin1Char('}')) ) nEndPos++; nEndPos++; aVarName = aValue.mid( nDollarPos+2, nEndPos-nDollarPos-3 ); } else { while ( nEndPos <= aValue.length() && (aValue[nEndPos].isNumber() || aValue[nEndPos].isLetter() || aValue[nEndPos] == QLatin1Char('_') ) ) nEndPos++; aVarName = aValue.mid( nDollarPos+1, nEndPos-nDollarPos-1 ); } QString env; if (!aVarName.isEmpty()) { #ifdef Q_OS_WIN if (aVarName == QLatin1String("HOME")) env = QDir::homePath(); else #endif { QByteArray pEnv = qgetenv( aVarName.toLatin1() ); if( !pEnv.isEmpty() ) // !!! Sergey A. Sukiyazov !!! // An environment variable may contain values in 8bit // locale specified encoding or UTF8 encoding env = KStringHandler::from8Bit( pEnv ); } aValue.replace(nDollarPos, nEndPos-nDollarPos, env); nDollarPos += env.length(); } else aValue.remove( nDollarPos, nEndPos-nDollarPos ); } else { // remove one of the dollar signs aValue.remove( nDollarPos, 1 ); nDollarPos++; } nDollarPos = aValue.indexOf( QLatin1Char('$'), nDollarPos ); } return aValue; } KConfig::KConfig( const QString& file, OpenFlags mode, const char* resourceType) : d_ptr(new KConfigPrivate(KGlobal::mainComponent(), mode, resourceType)) { d_ptr->changeFileName(file, resourceType); // set the local file name // read initial information off disk reparseConfiguration(); } KConfig::KConfig( const KComponentData& componentData, const QString& file, OpenFlags mode, const char* resourceType) : d_ptr(new KConfigPrivate(componentData, mode, resourceType)) { d_ptr->changeFileName(file, resourceType); // set the local file name // read initial information off disk reparseConfiguration(); } KConfig::KConfig(const QString& file, const QString& backend, const char* resourceType) : d_ptr(new KConfigPrivate(KGlobal::mainComponent(), SimpleConfig, resourceType)) { d_ptr->mBackend = KConfigBackend::create(d_ptr->componentData, file, backend); d_ptr->bDynamicBackend = false; d_ptr->changeFileName(file, ""); // set the local file name // read initial information off disk reparseConfiguration(); } KConfig::KConfig(KConfigPrivate &d) : d_ptr(&d) { } KConfig::~KConfig() { Q_D(KConfig); if (d->bDirty && d->mBackend.isUnique()) sync(); delete d; } const KComponentData& KConfig::componentData() const { Q_D(const KConfig); return d->componentData; } QStringList KConfig::groupList() const { Q_D(const KConfig); QSet groups; for (KEntryMap::ConstIterator entryMapIt( d->entryMap.constBegin() ); entryMapIt != d->entryMap.constEnd(); ++entryMapIt) { const KEntryKey& key = entryMapIt.key(); const QByteArray group = key.mGroup; if (key.mKey.isNull() && !group.isEmpty() && group != "" && group != "$Version") { const QString groupname = QString::fromUtf8(group); groups << groupname.left(groupname.indexOf(QLatin1Char('\x1d'))); } } return groups.toList(); } QStringList KConfigPrivate::groupList(const QByteArray& group) const { QByteArray theGroup = group + '\x1d'; QSet groups; for (KEntryMap::ConstIterator entryMapIt( entryMap.constBegin() ); entryMapIt != entryMap.constEnd(); ++entryMapIt) { const KEntryKey& key = entryMapIt.key(); if (key.mKey.isNull() && key.mGroup.startsWith(theGroup)) { const QString groupname = QString::fromUtf8(key.mGroup.mid(theGroup.length())); groups << groupname.left(groupname.indexOf(QLatin1Char('\x1d'))); } } return groups.toList(); } static bool isGroupOrSubGroupMatch(const QByteArray& potentialGroup, const QByteArray& group) { if (!potentialGroup.startsWith(group)) { return false; } return potentialGroup.length() == group.length() || potentialGroup[group.length()] == '\x1d'; } // List all sub groups, including subsubgroups QSet KConfigPrivate::allSubGroups(const QByteArray& parentGroup) const { QSet groups; for (KEntryMap::const_iterator entryMapIt = entryMap.begin(); entryMapIt != entryMap.end(); ++entryMapIt) { const KEntryKey& key = entryMapIt.key(); if (key.mKey.isNull() && isGroupOrSubGroupMatch(key.mGroup, parentGroup)) { groups << key.mGroup; } } return groups; } bool KConfigPrivate::hasNonDeletedEntries(const QByteArray& group) const { for (KEntryMap::const_iterator it = entryMap.begin(); it != entryMap.end(); ++it) { const KEntryKey& key = it.key(); // Check for any non-deleted entry if (isGroupOrSubGroupMatch(key.mGroup, group) && !key.mKey.isNull() && !it->bDeleted) return true; } return false; } QStringList KConfigPrivate::keyListImpl(const QByteArray& theGroup) const { QStringList keys; const KEntryMapConstIterator theEnd = entryMap.constEnd(); KEntryMapConstIterator it = entryMap.findEntry(theGroup); if (it != theEnd) { ++it; // advance past the special group entry marker QSet tmp; for (; it != theEnd && it.key().mGroup == theGroup; ++it) { const KEntryKey& key = it.key(); if (!key.mKey.isNull() && !it->bDeleted) tmp << QString::fromUtf8(key.mKey); } keys = tmp.toList(); } return keys; } QStringList KConfig::keyList(const QString& aGroup) const { Q_D(const KConfig); const QByteArray theGroup(aGroup.isEmpty() ? "" : aGroup.toUtf8()); return d->keyListImpl(theGroup); } QMap KConfig::entryMap(const QString& aGroup) const { Q_D(const KConfig); QMap theMap; const QByteArray theGroup(aGroup.isEmpty() ? "" : aGroup.toUtf8()); const KEntryMapConstIterator theEnd = d->entryMap.constEnd(); KEntryMapConstIterator it = d->entryMap.findEntry(theGroup, 0, 0); if (it != theEnd) { ++it; // advance past the special group entry marker for (; it != theEnd && it.key().mGroup == theGroup; ++it) { // leave the default values and deleted entries out if (!it->bDeleted && !it.key().bDefault) { const QString key = QString::fromUtf8(it.key().mKey.constData()); // the localized entry should come first, so don't overwrite it // with the non-localized entry if (!theMap.contains(key)) { if (it->bExpand) { theMap.insert(key,KConfigPrivate::expandString(QString::fromUtf8(it->mValue.constData()))); } else { theMap.insert(key,QString::fromUtf8(it->mValue.constData())); } } } } } return theMap; } // TODO KDE5: return a bool value void KConfig::sync() { Q_D(KConfig); if (isImmutable() || name().isEmpty()) { // can't write to an immutable or anonymous file. return; } if (d->bDirty && d->mBackend) { const QByteArray utf8Locale(locale().toUtf8()); // Create the containing dir, maybe it wasn't there d->mBackend->createEnclosing(); // lock the local file if (d->configState == ReadWrite && !d->lockLocal()) { qWarning() << "couldn't lock local file"; return; } // Rewrite global/local config only if there is a dirty entry in it. bool writeGlobals = false; bool writeLocals = false; foreach (const KEntry& e, d->entryMap) { if (e.bDirty) { if (e.bGlobal) { writeGlobals = true; } else { writeLocals = true; } if (writeGlobals && writeLocals) { break; } } } d->bDirty = false; // will revert to true if a config write fails if (d->wantGlobals() && writeGlobals) { KSharedPtr tmp = KConfigBackend::create(componentData(), d->sGlobalFileName); if (d->configState == ReadWrite && !tmp->lock(componentData())) { qWarning() << "couldn't lock global file"; d->bDirty = true; return; } if (!tmp->writeConfig(utf8Locale, d->entryMap, KConfigBackend::WriteGlobal, d->componentData)) { d->bDirty = true; // TODO KDE5: return false? (to tell the app that writing wasn't possible, e.g. // config file is immutable or disk full) } if (tmp->isLocked()) { tmp->unlock(); } } if (writeLocals) { if (!d->mBackend->writeConfig(utf8Locale, d->entryMap, KConfigBackend::WriteOptions(), d->componentData)) { d->bDirty = true; // TODO KDE5: return false? (to tell the app that writing wasn't possible, e.g. // config file is immutable or disk full) } } if (d->mBackend->isLocked()) { d->mBackend->unlock(); } } } void KConfig::markAsClean() { Q_D(KConfig); d->bDirty = false; // clear any dirty flags that entries might have set const KEntryMapIterator theEnd = d->entryMap.end(); for (KEntryMapIterator it = d->entryMap.begin(); it != theEnd; ++it) it->bDirty = false; } bool KConfig::isDirty() const { Q_D(const KConfig); return d->bDirty; } void KConfig::checkUpdate(const QString &id, const QString &updateFile) { const KConfigGroup cg(this, "$Version"); const QString cfg_id = updateFile+QLatin1Char(':')+id; const QStringList ids = cg.readEntry("update_info", QStringList()); if (!ids.contains(cfg_id)) { KToolInvocation::kdeinitExecWait(QString::fromLatin1("kconf_update"), QStringList() << QString::fromLatin1("--check") << updateFile); reparseConfiguration(); } } KConfig* KConfig::copyTo(const QString &file, KConfig *config) const { Q_D(const KConfig); if (!config) config = new KConfig(componentData(), QString(), SimpleConfig); config->d_func()->changeFileName(file, d->resourceType); config->d_func()->entryMap = d->entryMap; config->d_func()->bFileImmutable = false; const KEntryMapIterator theEnd = config->d_func()->entryMap.end(); for (KEntryMapIterator it = config->d_func()->entryMap.begin(); it != theEnd; ++it) it->bDirty = true; config->d_ptr->bDirty = true; return config; } QString KConfig::name() const { Q_D(const KConfig); return d->fileName; } void KConfigPrivate::changeFileName(const QString& name, const char* type) { fileName = name; QString file; if (name.isEmpty()) { if (wantDefaults()) { // accessing default app-specific config "appnamerc" const QString appName = componentData.aboutData()->appName(); if (!appName.isEmpty()) { fileName = appName + QLatin1String("rc"); if (type && *type) resourceType = type; // only change it if it's not empty file = KStandardDirs::locateLocal(resourceType, fileName, false, componentData); } } else if (wantGlobals()) { // accessing "kdeglobals" - XXX used anywhere? resourceType = "config"; fileName = QLatin1String("kdeglobals"); file = sGlobalFileName; } // else anonymous config. // KDE5: remove these magic overloads } else if (QDir::isAbsolutePath(fileName)) { fileName = KStandardDirs::realFilePath(fileName); file = fileName; } else { if (type && *type) resourceType = type; // only change it if it's not empty file = KStandardDirs::locateLocal(resourceType, fileName, false, componentData); } if (file.isEmpty()) { openFlags = KConfig::SimpleConfig; return; } #ifndef Q_OS_WIN bSuppressGlobal = (file == sGlobalFileName); #else bSuppressGlobal = (file.compare(sGlobalFileName, Qt::CaseInsensitive) == 0); #endif if (bDynamicBackend || !mBackend) // allow dynamic changing of backend mBackend = KConfigBackend::create(componentData, file); else mBackend->setFilePath(file); configState = mBackend->accessMode(); } void KConfig::reparseConfiguration() { Q_D(KConfig); if (d->fileName.isEmpty()) { return; } // Don't lose pending changes if (!d->isReadOnly() && d->bDirty) sync(); d->entryMap.clear(); d->bFileImmutable = false; // Parse all desired files from the least to the most specific. if (d->wantGlobals()) d->parseGlobalFiles(); d->parseConfigFiles(); } QStringList KConfigPrivate::getGlobalFiles() const { const KStandardDirs *const dirs = componentData.dirs(); QStringList globalFiles; foreach (const QString& dir1, dirs->findAllResources("config", QLatin1String("kdeglobals"))) globalFiles.push_front(dir1); foreach (const QString& dir2, dirs->findAllResources("config", QLatin1String("system.kdeglobals"))) globalFiles.push_front(dir2); if (!etc_kderc.isEmpty()) globalFiles.push_front(etc_kderc); return globalFiles; } void KConfigPrivate::parseGlobalFiles() { const QStringList globalFiles = getGlobalFiles(); // qDebug() << "parsing global files" << globalFiles; // TODO: can we cache the values in etc_kderc / other global files // on a per-application basis? const QByteArray utf8Locale = locale.toUtf8(); foreach(const QString& file, globalFiles) { KConfigBackend::ParseOptions parseOpts = KConfigBackend::ParseGlobal|KConfigBackend::ParseExpansions; #ifndef Q_OS_WIN if (file != sGlobalFileName) #else if (file.compare(sGlobalFileName, Qt::CaseInsensitive) != 0) #endif parseOpts |= KConfigBackend::ParseDefaults; KSharedPtr backend = KConfigBackend::create(componentData, file); if ( backend->parseConfig( utf8Locale, entryMap, parseOpts) == KConfigBackend::ParseImmutable) break; } } void KConfigPrivate::parseConfigFiles() { // can only read the file if there is a backend and a file name if (mBackend && !fileName.isEmpty()) { bFileImmutable = false; QList files; if (wantDefaults()) { if (bSuppressGlobal) { files = getGlobalFiles(); } else { foreach (const QString& f, componentData.dirs()->findAllResources( resourceType, fileName)) files.prepend(f); } } else { files << mBackend->filePath(); } if (!isSimple()) files = extraFiles.toList() + files; // qDebug() << "parsing local files" << files; const QByteArray utf8Locale = locale.toUtf8(); foreach(const QString& file, files) { #ifndef Q_OS_WIN if (file == mBackend->filePath()) { #else if (file.compare(mBackend->filePath(), Qt::CaseInsensitive) == 0) { #endif switch (mBackend->parseConfig(utf8Locale, entryMap, KConfigBackend::ParseExpansions)) { case KConfigBackend::ParseOk: break; case KConfigBackend::ParseImmutable: bFileImmutable = true; break; case KConfigBackend::ParseOpenError: configState = KConfigBase::NoAccess; break; } } else { KSharedPtr backend = KConfigBackend::create(componentData, file); bFileImmutable = (backend->parseConfig(utf8Locale, entryMap, KConfigBackend::ParseDefaults|KConfigBackend::ParseExpansions) == KConfigBackend::ParseImmutable); } if (bFileImmutable) break; } if (componentData.dirs()->isRestrictedResource(resourceType, fileName)) bFileImmutable = true; } } KConfig::AccessMode KConfig::accessMode() const { Q_D(const KConfig); return d->configState; } void KConfig::addConfigSources(const QStringList& files) { Q_D(KConfig); foreach(const QString& file, files) { d->extraFiles.push(file); } if (!files.isEmpty()) { reparseConfiguration(); } } QString KConfig::locale() const { Q_D(const KConfig); return d->locale; } bool KConfigPrivate::setLocale(const QString& aLocale) { if (aLocale != locale) { locale = aLocale; return true; } return false; } bool KConfig::setLocale(const QString& locale) { Q_D(KConfig); if (d->setLocale(locale)) { reparseConfiguration(); return true; } return false; } void KConfig::setReadDefaults(bool b) { Q_D(KConfig); d->bReadDefaults = b; } bool KConfig::readDefaults() const { Q_D(const KConfig); return d->bReadDefaults; } bool KConfig::isImmutable() const { Q_D(const KConfig); return d->bFileImmutable; } bool KConfig::isGroupImmutableImpl(const QByteArray& aGroup) const { Q_D(const KConfig); return isImmutable() || d->entryMap.getEntryOption(aGroup, 0, 0, KEntryMap::EntryImmutable); } #ifndef KDE_NO_DEPRECATED void KConfig::setForceGlobal(bool b) { Q_D(KConfig); d->bForceGlobal = b; } #endif #ifndef KDE_NO_DEPRECATED bool KConfig::forceGlobal() const { Q_D(const KConfig); return d->bForceGlobal; } #endif KConfigGroup KConfig::groupImpl(const QByteArray &group) { return KConfigGroup(this, group.constData()); } const KConfigGroup KConfig::groupImpl(const QByteArray &group) const { return KConfigGroup(this, group.constData()); } KEntryMap::EntryOptions convertToOptions(KConfig::WriteConfigFlags flags) { KEntryMap::EntryOptions options=0; if (flags&KConfig::Persistent) options |= KEntryMap::EntryDirty; if (flags&KConfig::Global) options |= KEntryMap::EntryGlobal; if (flags&KConfig::Localized) options |= KEntryMap::EntryLocalized; return options; } void KConfig::deleteGroupImpl(const QByteArray &aGroup, WriteConfigFlags flags) { Q_D(KConfig); KEntryMap::EntryOptions options = convertToOptions(flags)|KEntryMap::EntryDeleted; const QSet groups = d->allSubGroups(aGroup); foreach (const QByteArray& group, groups) { const QStringList keys = d->keyListImpl(group); foreach (const QString& _key, keys) { const QByteArray &key = _key.toUtf8(); if (d->canWriteEntry(group, key.constData())) { d->entryMap.setEntry(group, key, QByteArray(), options); d->bDirty = true; } } } } bool KConfig::isConfigWritable(bool warnUser) { Q_D(KConfig); bool allWritable = (d->mBackend.isNull()? false: d->mBackend->isWritable()); if (warnUser && !allWritable) { QString errorMsg; if (!d->mBackend.isNull()) // TODO how can be it be null? Set errorMsg appropriately errorMsg = d->mBackend->nonWritableErrorMessage(); // Note: We don't ask the user if we should not ask this question again because we can't save the answer. errorMsg += i18n("Please contact your system administrator."); QString cmdToExec = KStandardDirs::findExe(QString::fromLatin1("kdialog")); if (!cmdToExec.isEmpty() && componentData().isValid()) { QProcess::execute(cmdToExec, QStringList() << QString::fromLatin1("--title") << componentData().componentName() << QString::fromLatin1("--msgbox") << errorMsg); } } d->configState = allWritable ? ReadWrite : ReadOnly; // update the read/write status return allWritable; } bool KConfig::hasGroupImpl(const QByteArray& aGroup) const { Q_D(const KConfig); // No need to look for the actual group entry anymore, or for subgroups: // a group exists if it contains any non-deleted entry. return d->hasNonDeletedEntries(aGroup); } bool KConfigPrivate::canWriteEntry(const QByteArray& group, const char* key, bool isDefault) const { if (bFileImmutable || entryMap.getEntryOption(group, key, KEntryMap::SearchLocalized, KEntryMap::EntryImmutable)) return isDefault; return true; } void KConfigPrivate::putData( const QByteArray& group, const char* key, const QByteArray& value, KConfigBase::WriteConfigFlags flags, bool expand) { KEntryMap::EntryOptions options = convertToOptions(flags); if (bForceGlobal) options |= KEntryMap::EntryGlobal; if (expand) options |= KEntryMap::EntryExpansion; if (value.isNull()) // deleting entry options |= KEntryMap::EntryDeleted; bool dirtied = entryMap.setEntry(group, key, value, options); if (dirtied && (flags & KConfigBase::Persistent)) bDirty = true; } void KConfigPrivate::revertEntry(const QByteArray& group, const char* key) { bool dirtied = entryMap.revertEntry(group, key); if (dirtied) bDirty = true; } QByteArray KConfigPrivate::lookupData(const QByteArray& group, const char* key, KEntryMap::SearchFlags flags) const { if (bReadDefaults) flags |= KEntryMap::SearchDefaults; const KEntryMapConstIterator it = entryMap.findEntry(group, key, flags); if (it == entryMap.constEnd()) return QByteArray(); return it->mValue; } QString KConfigPrivate::lookupData(const QByteArray& group, const char* key, KEntryMap::SearchFlags flags, bool *expand) const { if (bReadDefaults) flags |= KEntryMap::SearchDefaults; return entryMap.getEntry(group, key, QString(), flags, expand); } void KConfig::virtual_hook(int /*id*/, void* /*data*/) { /* nothing */ } diff --git a/kdecore/doc/README.kiosk b/kdecore/doc/README.kiosk index b95002d040..d902c61ea9 100644 --- a/kdecore/doc/README.kiosk +++ b/kdecore/doc/README.kiosk @@ -1,817 +1,805 @@ In KDE-3 a kiosk-framework has been introduced. One of the driving forces behind KDE is to put the user in control and give him or her a large amount of possibilities to adjust KDE to his or her liking. However, in some situations it is required to reduce the possibilities of KDE, e.g. because the system is to be used for one or more specific dedicated tasks only. The kiosk-framework provides an easy way to disable certain features within KDE to create a more controlled environment. KDE's kiosk-framework builds on KDE's configuration framework and adds a simple application API that applications can query to get authorisation for certain operations. The KDE kiosk-framework should be used IN ADDITION to the standard UNIX security measures. The configuration framework in KDE ================================== Since the very beginning KDE makes use of file-hierarchy to store resources for its applications. Resources range from icons, wallpapers, fonts to sounds, menu-descriptions and configuration files. In KDE1 there were two locations were resources could be located: The resources provided by the system were located under $KDEDIR and user- specific resources were located under $HOME/.kde. In KDE2 resource management has been largely abstracted by the introduction of the KStandardDirs class and has become much more flexible. The user / administrator can now specify a variable number of locations where resources can be found. A list of locations can either be specified via $KDEDIRS (notice the extra 'S'), via /etc/kde4rc and even via the kdeglobals config file. The location where user-specific resources can be found can be set with $KDEHOME (The default is $HOME/.kde). Changes made by the user are always written back to $KDEHOME. Both KDE1 and KDE2 feature so called "cascading configuration files": There can be multiple configuration files with the same name in the various locations for (config) resources, when that is the case, the information of all these configuration files is combined on a key by key basis. If the same key (within a certain group) is defined in more than one place, the value of the key for the config file that was read last will override any previously read values. Configuration files under $KDEHOME are always read last. This ensures that after a configuration entry is written, the same value wil be read back. In KDE3 two important changes have been made: * Default values are no longer written. When a configuration file in a location other than $KDEHOME defines a value for a key and the application subsequently writes out a new configuration file to $KDEHOME, that configuration file will only contain an entry for the key if its value differs from the value read from the other file. This counters the problem that changing default configuration files under $KDEDIR would not take effect for users, since these users would most likely have their own copy of these settings under $KDEHOME. KDE will make sure not to copy these settings so changes made under $KDEDIR will affect all users that haven't explicitly changed the affected settings to something else. * Configuration entries can be marked "immutable". Starting with KDE-3, configuration entries can be marked "immutable". When a configuration entry is immutable it means that configuration files that are read later will not be able to override its value. Immutable entries cannot be changed via KConfig and if the entry is present under $KDEHOME it will be ignored. Entries can be marked immutable on 4 different levels: - On an entry by entry basis by appending "[$i]" after the key. Example: [MyGroup] someKey[$i]=42 - On a group by group basis by appending "[$i]" after the group. All entries specified in the group will be marked immutable and no new entries can be added to the group. Example: [MyGroup][$i] someKey=42 - On a file by file basis by starting the file with [$i]. Example: [$i] [MyGroup] someKey=42 [MyOtherGroup] someOtherKey=11 - On a directory basis. [Not yet implemented] - The filesystem can also be used to mark files immutable. If KDE does not have write-access to the user's version of a configuration file, the file will be automatically considered immutable. To make the configuration file of Dolphin immutable one could for example use the commands below. Example: chown root.root /home/user/.kde/share/config/dolphinrc chmod 644 /home/user/.kde/share/config/dolphinrc If you do this, the user will be warned that the configuration file is not writable. Since you will normally not want that, you can add the following two lines to the application's configuration file (or to kdeglobals to disable the warning for all applications): [KDE Action Restrictions] warn_unwritable_config=true Note that the above example is not fool-proof, the user can potentially still rename either the root-owned dolphinrc file or any of the directories in the path to another name and create a new dolphinrc _with_ write-access. KDE Action Restrictions ======================= Most functionality within KDE is coupled to so called actions. For example when a user selects the File->Open option in the menubar of a KDE application, the "file_open" action is activated. Likewise, toolbar icons are usually also coupled to actions. KDE makes it possible to disable functionality by restricting specific actions. By restricting the "file_open" action for example, the corresponding entry in the menubar and the corresponding icon on the toolbar, if any, will disappear. To restrict access to function the kdeglobals file should contain the group "[KDE Action Restrictions]", each action can then be restricted by adding "=false". E.g. to disable the action "shell_access" one would add: [KDE Action Restrictions][$i] shell_access=false Actions that refer to menu and toolbar actions are prefixed with 'action/'. The following standard actions are defined: action/file_new action/file_open action/file_open_recent action/file_save action/file_save_as action/file_revert action/file_close action/file_print action/file_print_preview action/file_mail action/file_quit action/edit_undo action/edit_redo action/edit_cut action/edit_copy action/edit_paste action/edit_select_all action/edit_deselect action/edit_find action/edit_find_next action/edit_find_last action/edit_replace action/view_actual_size action/view_fit_to_page action/view_fit_to_width action/view_fit_to_height action/view_zoom_in action/view_zoom_out action/view_zoom action/view_redisplay action/go_up action/go_back action/go_forward action/go_home action/go_previous action/go_next action/go_goto action/go_goto_page action/go_goto_line action/go_first action/go_last action/bookmarks // See note below action/bookmark_add action/bookmark_edit action/tools_spelling action/options_show_menubar action/options_show_toolbar // See note below action/options_show_statusbar action/options_save_options action/options_configure action/options_configure_keybinding action/options_configure_toolbars action/options_configure_notifications action/help // See note below action/help_contents action/help_whats_this action/help_report_bug action/help_about_app action/help_about_kde action/fullscreen Actions in the KDE File Dialog: action/home // Go to home directory action/up // Go to parent directory action/back // Go to previous directory action/forward // Go to next directory action/reload // Reload directory action/mkdir // Create new directory action/toggleSpeedbar // Show/hide sidebar action/sorting menu // Sorting options action/short view // Select short view action/detailed view // Select detailed view action/show hidden // Show/hide hidden files action/preview // Show/hide preview action/separate dirs // Show/hide separate directories Konqueror & KDesktop related: action/editfiletype action/properties action/openwith action/openintab action/kdesktop_rmb // RMB menu, see note below action/iconview_preview action/sharefile // File sharing, see note below action/sendURL // Send Link Address action/sendPage // Send File action/devnew // Create New -> Device action/incIconSize // Increase icon size action/decIconSize // Decrease icon size action/go // Entire go menu action/configdesktop // Configure desktop in RMB menu, see also Control Module Restrictions action/executeshellcommand // In Konqueror Tools menu, see also shell_access action/show_dot // Show Hidden Files, see note below Kicker related: action/kicker_rmb // RMB menu action/menuedit KWin related: action/kwin_rmb // RMB window context menu Konsole related: action/konsole_rmb // RMB context menu action/settings // Entire settings menu action/show_menubar action/show_toolbar action/scrollbar action/fullscreen action/bell action/font action/keyboard action/schema action/size action/history action/save_default action/save_sessions_profile action/options_configure_notifications action/options_configure_keybinding action/options_configure action/send_signal action/bookmarks action/add_bookmark action/edit_bookmarks action/clear_terminal action/reset_clear_terminal action/find_history action/find_next action/find_previous action/save_history action/clear_history action/clear_all_histories action/detach_session action/rename_session action/zmodem_upload action/monitor_activity action/monitor_silence action/send_input_to_all_sessions action/close_session action/new_session action/activate_menu action/list_sessions action/move_session_left action/move_session_right action/previous_session action/next_session action/switch_to_session_1 action/switch_to_session_2 action/switch_to_session_3 action/switch_to_session_4 action/switch_to_session_5 action/switch_to_session_6 action/switch_to_session_7 action/switch_to_session_8 action/switch_to_session_9 action/switch_to_session_10 action/switch_to_session_11 action/switch_to_session_12 action/bigger_font action/smaller_font action/toggle_bidi Notes: * action/options_show_toolbar will also disable the "Toolbars" submenu if present. * action/bookmarks also disables action/bookmark_add and action/bookmark_edit * action/help is not yet fully implemented * action/kdesktop_rmb disables the RMB menu but some actions may still be accesible via keyboard shortcuts: cut/copy/rename/trash/delete * action/iconview_preview disables the option to toggle previews on or off in icon mode but the actual preview settings remains unaffected. To disable previews you also need to add the following lines to konqiconviewrc: [Settings] PreviewsEnabled[$i]=false * action/show_dot disables the option to toggle showing hidden files, the actual setting remains unaffected. To disable showing hidden files, add the following lines to konqiconviewrc: [Settings] ShowDotFiles[$i]=false * action/sharefile disables file sharing from the UI, but you may also want to disable filesharing altogether. Applications may use additional actions that they defined themselves. You can get a list of the actions used by a certain applications by using the following qdbus command: qdbus org.kde.myapp-id | grep actions | cut -d '/' -f 4,5 Actions that refer to applications that need to be run as a different user are prefixed by user/ and identified by the username. For example: user/root=false will disable all application entries that require root access. Printing related action restrictions: print/system - disables the option to select the printing system (backend). It is recommended to disable this option once the correct printing system has been configured. print/properties - disables the button to change printer properties or to add a new printer. print/options - disables the button to select additional print options. print/copies - disables the panel that allows users to make more than one copy. print/selection - disables the options that allows selecting a (pseudo) printer or change any of the printer properties. Make sure that a proper default printer has been selected before disabling this option. Disabling this option also disables print/system, print/options and print/properties. print/dialog - disables the complete print dialog. Selecting the print option will immediately print the selected document using default settings. Make sure that a system wide default printer has been selected. No application specific settings are honored. Other defined actions: shell_access - defines whether a shell suitable for entering random commands may be started. This also determines whether the "Run Command" option (Alt-F2) can be used to run shell-commands and arbitrary executables. Likewise, executables placed in the user's Autostart folder will no longer be executed. Applications can still be autostarted by placing .desktop files in the $KDEHOME/Autostart or $KDEDIR/share/autostart directory. See also run_desktop_files. custom_config - defines whether the --config command line option should be honored. The --config command line option can be used to circumvent locked-down configuration files. logout - defines whether the user will be able to logout from KDE. lock_screen - defines whether the user will be able to lock the screen. run_command - defines whether the "Run Command" (Alt-F2) option is available. movable_toolbars - define whether toolbars may be moved around by the user. See also action/options_show_toolbar. editable_desktop_icons - define whether icons on the desktop can be moved, renamed, deleted or added. You might want to set the path for the Desktop to some read-only directory as well. (Instead of $HOME/Desktop) run_desktop_files - defines whether users may execute desktop files that are not part of the default desktop, KDE menu, registered services and autostarting services. * The default desktop includes the files under $KDEDIR/share/kdesktop/Desktop but _NOT_ the files under $HOME/Desktop. * The KDE menu includes all files under $KDEDIR/share/applnk and $XDGDIR/applications * Registered services includes all files under $KDEDIR/share/services. * Autostarting services include all files under $KDEDIR/share/autostart but _NOT_ the files under $KDEHOME/Autostart You probably also want to activate the following resource restictions: "appdata_kdesktop" - To restrict the default desktop. "apps" - To restrict the KDE menu. "xdgdata-apps" - To restrict the KDE menu. "services" - To restrict registered services. "autostart" - To restrict autostarting services. Otherwise users can still execute .desktop files by placing them in e.g. $KDEHOME/share/kdesktop/Desktop lineedit_text_completion - defines whether input lines should have the potential to remember any previously entered data and make suggestions based on this when typing. When a single account is shared by multiple people you may wish to disable this out of privacy concerns. start_new_session - defines whether the user may start a second X session. See also the kdm configuration. switch_user - defines whether user switching via kdm is allowed skip_drm - defines if the user may omit DRM checking. Currently only used by kpdf Screensaver related: opengl_screensavers - defines whether OpenGL screensavers are allowed to be used. manipulatescreen_screensavers - defines whether screensavers that manipulate an image of the screen (e.g. moving chunks of the screen around) are allowed to be used. When configuration files are marked immutable in whole or in part the user will no longer be able to make permanent changes to the settings that have been marked immutable. Ideally the application will recognize this and will no longer offer the user the possibility to change these settings. Unfortunately not all applications support this at the moment. It's therefor possible that the user will still be presented with an option in the user interface to change a setting that is immutable, changes made this way will not be saved though. In some cases the user may be able to use the changed setting till the application terminates, in other cases the changed setting will simply be ignored and the application will continue to work with the immutable setting. The following applications currently detect when their configuration files have been marked immutable and adjust their user interface accordingly: * kicker - By marking the kickerrc config file as immutable, the panel will be "locked down" and it will not be possible to make any changes to it. * kdesktop - By marking the kdesktoprc config file as immutable, the desktop will be "locked down" and it will no longer be possible to select "Configure Desktop" from its menus. * kcalc - By marking the kcalcrc config file as immutable, the "Configure" button will not be shown Application .desktop files can have an additional field "X-KDE-AuthorizeAction". If this field is present the .desktop file is only considered if the action(s) mentioned in this field has been authorized. If multiple actions are listed they should be separated by commas (','). So if the .desktop file of an application lists one or more actions this way and the user has no authorization for one of these actions then the application will not appear in the KDE menu and will not be used by KDE for opening files. IMPORTANT NOTE: Changing restrictions may influence the data that is cached in the ksycoca database. Since changes to .../share/config/kdeglobals do not trigger an automatic ksycoca update you need to force an update manually. To force an update of the ksycoca database touch the file .../share/services/update_ksycoca. This will force a user's sycoca database to be rebuild the next time the user logs in. KDE URL Restrictions ==================== It is also possible to restrict URL related actions. The restriction framework can disable URL actions based on the action, the URL in question and in some cases the referring URL. URLs can be matched based on protocol, host and path. The syntax for adding URL action restrictions to kdeglobals is as follows: [KDE URL Restrictions] rule_count= rule_1=,,,,,,, ... rule_N=,,,,,,, The following actions are supported: redirect - e.g. a html-page obtained via HTTP could redirect itself to file:/path/some-file. This is disabled by default but could be explicitly enabled for a specific HTTP host. This also applies to links contained in html documents. Example: rule_1=redirect,http,myhost.acme.com,,file,,,true list - This controls which directories can be browsed with KDE's file-dialogs. If a user should only be able to browse files under home directory one could use: rule_1=list,,,,file,,,false rule_2=list,,,,file,,$HOME,true The first rule disables browing any directories on the local filesystem. The second rule then enables browsing the users home directory. open - This controls which files can be opened by the user in applications. It also affects where users can save files. To only allow a user to open the files in his own home directory one could use: rule_1=open,,,,file,,,false rule_2=open,,,,file,,$HOME,true rule_3=open,,,,file,,$TMP,true Note that with the above, users would still be able to open files from the internet. Note that the user is also given access to $TMP in order to ensure correct operation of KDE applications. $TMP is replaced with the temporary directory that KDE uses for this user. Some remarks: * empty entries match everything * host names may start with a wildcard, e.g. "*.acme.com" * a protocol also matches similar protocols that start with the same name, e.g. "http" matches both http and https. You can use "http!" if you only want to match http (and not https) * specifying a path matches all URLs that start with the same path. For better results you should not include a trailing slash. If you want to specify one specific path, you can add an exclamation mark. E.g. "/srv" matches both "/srv" and "/srv/www" but "/srv!" only matches "/srv" and not "/srv/www". KDE Resource Restrictions ========================== Most KDE applications make use of additional resource files that are typically located in directories under $KDEDIR/share. By default KDE allows users to override any of these resources by placing files in the same location under $KDEHOME/share. For example, Konsole stores profiles under $KDEDIR/share/apps/konsole and users can add additional profiles by installing files in $KDEHOME/share/apps/konsole. KDE Resource Restrictions make it possible to restrict the lookup of files to directories outside of $KDEHOME only. The following resources are defined: autostart - share/autostart data - share/apps html - share/doc/HTML icon - share/icon config - share/config pixmap - share/pixmaps apps - share/applnk xdgdata-apps - share/applications sound - share/sounds locale - share/locale services - share/services servicetypes - share/servicetypes mime - share/mimelnk wallpaper - share/wallpapers templates - share/templates exe - bin lib - lib See http://api.kde.org/4.x-api/kdelibs-apidocs/kdecore/html/classKStandardDirs.html for a more up-to-date list of resources. For the purpose of resource restrictions there are two special resources: all - covers all resources data_ - covers the sub section for in the data resource. To restrict resources the kdeglobals file should contain the group "[KDE Resource Restrictions]", each resource can then be restricted by adding "=false". E.g. to restrict the "wallpaper" resource to $KDEDIR/share/wallpapers one would add: [KDE Resource Restrictions][$i] wallpaper=false And to prevent a user from adding additional konsole profiles, one would add: [KDE Resource Restrictions][$i] data_konsole=false Control Module Restrictions =========================== It is possible to restrict access to particular control modules. Although it is possible to remove control modules from the Control Center by editing the menu structure, such modules will then still be available to applications. A better way is to use the control module restrictions offered by KIOSK: [KDE Control Module Restrictions][$i] =false Some example menu-ids are: kde-display.desktop kde-proxy.desktop kde-screensaver.desktop See also kcmshell --list for a list of all the base names. Expansion of environment variables in KDE config files. ======================================================= Since KDE-3.1, arbitrary entries in configuration files can contain environment variables. In order to use this the entry must be marked with [$e]. Example: Name[$e]=$USER When the "Name" entry is read $USER will be replaced with the value of the $USER environment variable. Note that the application will replace $USER with the value of the environment variable after saving. To prevent this combine the $e option with $i (immmutable) option. Example: Name[$ei]=$USER The above will make that the "Name" entry will always return the value of the $USER environment variable. The user will not be able to change this entry. The following syntax is also supported: Name[$ei]=${USER} -Shell Commands in KDE config files. -=================================== - -Since KDE-3.1 arbitrary entries in configuration files can contain shell -commands. This way the value of a configuration entry can be determined -dynamically at runtime. In order to use this the entry must be marked -with [$e]. - -Example: -Host[$e]=$(hostname) - - KDE Kiosk Application API ========================== Three new methods have been added to KApplication: - bool authorize(QString action); // Generic actions - bool authorizeKAction(QString action); // For KActions exclusively - bool authorizeURLAction(QString, referringURL, destinationURL) // URL Handling Automatic Logout ================ Since KDE 3.4 it is possible to automatically logout users that have been idle for a certain period of time. WARNING: Be careful with this option, logging out a user may result in dataloss! In kdesktoprc you can use the following entry to enable automatic logout: [ScreenSaver] AutoLogout=true AutoLogoutTimeout=600 The AutoLogoutTimeout is the time in seconds that the user has to be idle before his session is logged out. Users can be associated with Profile(s) ======================================= A user can be associated with one or more profiles. A profile indicates a configuration set that applies to a group of users. Each profile has a name to identify it. If a user is associated with more than one profile then the order of the two profiles is important. Settings associated with one profile could override the settings in the other profile, depending on the order. Mapping profiles to users ========================= A mapping file determines which profile(s) should be used for which user. The mapping file can be configured in /etc/kde4rc in the [Directories] group: [Directories] userProfileMapFile=/etc/kde-user-profile Profiles can be mapped to individual users based on username, or profiles can be mapped to groups of users based on the UNIX group(s) the users are part of. (See man 1 groups) Mapping profiles to individual users ==================================== The mapping file can contain a [Users] section for mapping profiles to an individual user. The [Users] section contains the user's account name followed by one or more profiles as follow: [Users] bastian=developer adrians=developer,packager The above example assigns to user "bastian" the profile "developer". To user "adrians" it assigns the two profiles "developer" and "packager". The order in which the profiles are listed makes a difference, settings in earlier profiles overrule settings in profiles that are listed after it. In the above case of user "adrians", wherever the "developer" and "packager" profiles contain conflicting settings, the settings of the "developer" profile will take precedent. If a user has an entry under the [Users] section, this entry will determine all profiles that are applicable to the user. The user will not be assigned any additional profiles based on the groups the user is part of. Mapping profiles to user groups =============================== If a user has no entry under the [Users] section in the mapping file, the profiles that are applicable to the user will be based on the UNIX group(s) the user is part of. The groups and the order in which the groups are considered is determined by the following entry in the [General] section of the mapping file: [General] groups=pkgs,devel,bofh Each of these groups should have an entry under the [Groups] section that defines which profile(s) belongs to that group. This looks as follows: [Groups] pkgs=packager devel=developer bofh=admin,packager,developer For each group that a user is part of, the corresponding profile(s) are used. The order in which the groups are listed in the "groups" entry, determines the resulting order of all the applicable profiles. If multiple profiles are applicable to a particular user and a profile contains settings that conflict with settings in another profile then the settings in the earlier listed profile take precedent. So if, based on the example above, a user is part of the "pkgs" group then the "packager" profile will be used for that user. If the user is part of the "devel" group then the "developer" profile will be used. Users that are part of the "bofh" group will use the "admin", "packager" as well as the "developer" profile. In case of conflict, settings in the "admin" profile will take precedent over settings in the "packager" or "developer" profiles. If the user is part of both the "pkgs" and "devel" groups, then both the "packager" and "developer" profiles will be used. In case of conflicting settings between the two profiles, the "packager" profile will take precedent because the "pkgs" group associated with the profile was listed before the "devel" group. The "groups" command can be used to see to which groups a user belongs: > groups coolo coolo : users uucp dialout audio video cdrecording devel Note that in general only a few groups will have profiles associated with them. In the example above only the "devel" group has a profile associated with it, the other groups do not and will be ignored. If there is no profile defined for any of the groups that the user is in, the user will be assigned the "default" profile. The Profile determines the directory prefixes ============================================= The global KDE configuration file (e.g. kdeglobals or /etc/kde4rc) can contain config-groups that are associated with a certain user profile. Such a config-group is treated similar as the [Directories] config-group. The name of a such config-group is [Directories-] Integration with KIOSK Admin Tool ================================= The KIOSK Admin Tool uses /etc/kde4rc as source for all its profile information. For this it uses the following keys in the [Directories-] config-group: # Short text describing this profile ProfileDescription= # Files will be installed with the uid of this user ProfileInstallUser= The KIOSK Admin Tool uses the first directory from the prefixes= entry as default installation directory for this profile. Default setting as example ========================== The following snipped could be added to /etc/kde4rc to define a "default" profile: [Directories-default] ProfileDescription=Default profile ProfileDescription[de]=Defaultprofiel ProfileInstallUser=root prefixes=/var/run/kde-profile/default diff --git a/kdecore/tests/kconfigtest.cpp b/kdecore/tests/kconfigtest.cpp index 78e6ad180e..37ea3c2825 100644 --- a/kdecore/tests/kconfigtest.cpp +++ b/kdecore/tests/kconfigtest.cpp @@ -1,1630 +1,1626 @@ /* This file is part of the KDE libraries Copyright (C) 1997 Matthias Kalle Dalheimer (kalle@kde.org) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 Library 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 "kconfigtest.h" #include #include #include #include #include "kconfigtest.moc" #include #include #include #include #ifdef Q_OS_UNIX #include #endif KCONFIGGROUP_DECLARE_ENUM_QOBJECT(KConfigTest,Testing) KCONFIGGROUP_DECLARE_FLAGS_QOBJECT(KConfigTest,Flags) QTEST_KDEMAIN_CORE( KConfigTest ) #define BOOLENTRY1 true #define BOOLENTRY2 false #define STRINGENTRY1 "hello" #define STRINGENTRY2 " hello" #define STRINGENTRY3 "hello " #define STRINGENTRY4 " hello " #define STRINGENTRY5 " " #define STRINGENTRY6 "" #define UTF8BITENTRY "Hello äöü" #define TRANSLATEDSTRINGENTRY1 "bonjour" #define BYTEARRAYENTRY QByteArray( "\x00\xff\x7f\x3c abc\x00\x00", 10 ) #define ESCAPEKEY " []\0017[]==]" #define ESCAPEENTRY "[]\170[]]=3=]\\] " #define DOUBLEENTRY 123456.78912345 #define FLOATENTRY 123.567f #define POINTENTRY QPoint( 4351, 1235 ) #define SIZEENTRY QSize( 10, 20 ) #define RECTENTRY QRect( 10, 23, 5321, 13 ) #define DATETIMEENTRY QDateTime( QDate( 2002, 06, 23 ), QTime( 12, 55, 40 ) ) #define STRINGLISTENTRY (QStringList( "Hello," ) << " World") #define STRINGLISTEMPTYENTRY QStringList() #define STRINGLISTJUSTEMPTYELEMENT QStringList(QString()) #define STRINGLISTEMPTYTRAINLINGELEMENT (QStringList( "Hi" ) << QString()) #define STRINGLISTESCAPEODDENTRY (QStringList( "Hello\\\\\\" ) << "World") #define STRINGLISTESCAPEEVENENTRY (QStringList( "Hello\\\\\\\\" ) << "World") #define STRINGLISTESCAPECOMMAENTRY (QStringList( "Hel\\\\\\,\\\\,\\,\\\\\\\\,lo" ) << "World") #define INTLISTENTRY1 QList() << 1 << 2 << 3 << 4 #define BYTEARRAYLISTENTRY1 QList() << "" << "1,2" << "end" #define VARIANTLISTENTRY (QVariantList() << true << false << QString("joe") << 10023) #define VARIANTLISTENTRY2 (QVariantList() << POINTENTRY << SIZEENTRY) #define HOMEPATH QString(QDir::homePath()+"/foo") #define HOMEPATHESCAPE QString(QDir::homePath()+"/foo/$HOME") #define DOLLARGROUP "$i" void KConfigTest::initTestCase() { // Use a different directory for the config files created by this test, // so that cleanupTestCase doesn't delete kdebugrc // This makes the save location for the config resource, "$HOME/.kde-unit-test/kconfigtest/" KGlobal::dirs()->addResourceType("config", 0, "kconfigtest"); // to make sure all files from a previous failed run are deleted cleanupTestCase(); KConfig sc( "kconfigtest" ); KConfigGroup cg(&sc, "AAA"); cg.writeEntry("stringEntry1", STRINGENTRY1, KConfig::Persistent|KConfig::Global); cg.deleteEntry("stringEntry2", KConfig::Global); cg = KConfigGroup(&sc, "Hello"); cg.writeEntry( "boolEntry1", BOOLENTRY1 ); cg.writeEntry( "boolEntry2", BOOLENTRY2 ); QByteArray data( UTF8BITENTRY ); QCOMPARE( data.size(), 12 ); // the source file is in utf8 QCOMPARE( QString::fromUtf8(data).length(), 9 ); cg.writeEntry ("Test", data); cg.writeEntry( "bytearrayEntry", BYTEARRAYENTRY ); cg.writeEntry( ESCAPEKEY, ESCAPEENTRY ); cg.writeEntry( "emptyEntry", ""); cg.writeEntry( "stringEntry1", STRINGENTRY1 ); cg.writeEntry( "stringEntry2", STRINGENTRY2 ); cg.writeEntry( "stringEntry3", STRINGENTRY3 ); cg.writeEntry( "stringEntry4", STRINGENTRY4 ); cg.writeEntry( "stringEntry5", STRINGENTRY5 ); cg.writeEntry( "keywith=equalsign", STRINGENTRY1 ); cg.deleteEntry( "stringEntry5" ); cg.deleteEntry( "stringEntry6" ); // deleting a nonexistent entry cg.writeEntry( "byteArrayEntry1", QByteArray( STRINGENTRY1 ), KConfig::Global|KConfig::Persistent ); cg.writeEntry( "doubleEntry1", DOUBLEENTRY ); cg.writeEntry( "floatEntry1", FLOATENTRY ); sc.deleteGroup("deleteMe"); // deleting a nonexistent group cg = KConfigGroup(&sc, "Complex Types"); cg.writeEntry( "rectEntry", RECTENTRY ); cg.writeEntry( "pointEntry", POINTENTRY ); cg.writeEntry( "sizeEntry", SIZEENTRY ); cg.writeEntry( "dateTimeEntry", DATETIMEENTRY ); cg.writeEntry( "dateEntry", DATETIMEENTRY.date() ); KConfigGroup ct = cg; cg = KConfigGroup(&ct, "Nested Group 1"); cg.writeEntry("stringentry1", STRINGENTRY1); cg = KConfigGroup(&ct, "Nested Group 2"); cg.writeEntry( "stringEntry2", STRINGENTRY2 ); cg = KConfigGroup(&cg, "Nested Group 2.1"); cg.writeEntry( "stringEntry3", STRINGENTRY3 ); cg = KConfigGroup(&ct, "Nested Group 3"); cg.writeEntry( "stringEntry3", STRINGENTRY3 ); cg = KConfigGroup(&sc, "List Types" ); cg.writeEntry( "listOfIntsEntry1", INTLISTENTRY1 ); cg.writeEntry( "listOfByteArraysEntry1", BYTEARRAYLISTENTRY1 ); cg.writeEntry( "stringListEntry", STRINGLISTENTRY ); cg.writeEntry( "stringListEmptyEntry", STRINGLISTEMPTYENTRY ); cg.writeEntry( "stringListJustEmptyElement", STRINGLISTJUSTEMPTYELEMENT ); cg.writeEntry( "stringListEmptyTrailingElement", STRINGLISTEMPTYTRAINLINGELEMENT ); cg.writeEntry( "stringListEscapeOddEntry", STRINGLISTESCAPEODDENTRY ); cg.writeEntry( "stringListEscapeEvenEntry", STRINGLISTESCAPEEVENENTRY ); cg.writeEntry( "stringListEscapeCommaEntry", STRINGLISTESCAPECOMMAENTRY ); cg.writeEntry( "variantListEntry", VARIANTLISTENTRY ); cg = KConfigGroup(&sc, "Path Type" ); cg.writePathEntry( "homepath", HOMEPATH ); cg.writePathEntry( "homepathescape", HOMEPATHESCAPE ); cg = KConfigGroup(&sc, "Enum Types" ); writeEntry( cg, "enum-10", Tens ); writeEntry( cg, "enum-100", Hundreds ); writeEntry( cg, "flags-bit0", Flags(bit0)); writeEntry( cg, "flags-bit0-bit1", Flags(bit0|bit1) ); cg = KConfigGroup(&sc, "ParentGroup" ); KConfigGroup cg1(&cg, "SubGroup1" ); cg1.writeEntry( "somestring", "somevalue" ); cg.writeEntry( "parentgrpstring", "somevalue" ); KConfigGroup cg2(&cg, "SubGroup2" ); cg2.writeEntry( "substring", "somevalue" ); KConfigGroup cg3(&cg, "SubGroup/3"); cg3.writeEntry( "sub3string", "somevalue" ); QVERIFY(sc.isDirty()); sc.sync(); QVERIFY(!sc.isDirty()); KConfig sc1("kdebugrc", KConfig::SimpleConfig); KConfigGroup sg0(&sc1, "0"); sg0.writeEntry("AbortFatal", false); sg0.writeEntry("WarnOutput", 0); sg0.writeEntry("FatalOutput", 0); sc1.sync(); //Setup stuff to test KConfig::addConfigSources() KConfig devcfg("specificrc"); KConfigGroup devonlygrp(&devcfg, "Specific Only Group"); devonlygrp.writeEntry("ExistingEntry", "DevValue"); KConfigGroup devandbasegrp(&devcfg, "Shared Group"); devandbasegrp.writeEntry("SomeSharedEntry", "DevValue"); devandbasegrp.writeEntry("SomeSpecificOnlyEntry", "DevValue"); devcfg.sync(); KConfig basecfg("baserc"); KConfigGroup basegrp(&basecfg, "Base Only Group"); basegrp.writeEntry("ExistingEntry", "BaseValue"); KConfigGroup baseanddevgrp(&basecfg, "Shared Group"); baseanddevgrp.writeEntry("SomeSharedEntry", "BaseValue"); baseanddevgrp.writeEntry("SomeBaseOnlyEntry", "BaseValue"); basecfg.sync(); KConfig gecfg("groupescapetest", KConfig::SimpleConfig); cg = KConfigGroup(&gecfg, DOLLARGROUP); cg.writeEntry( "entry", "doesntmatter" ); } void KConfigTest::cleanupTestCase() { const QString localConfig = KGlobal::dirs()->saveLocation("config"); KTempDir::removeDir(localConfig); QVERIFY(!QFile::exists(localConfig)); } static QList readLinesFrom(const QString& path) { QFile file(path); const bool opened = file.open(QIODevice::ReadOnly|QIODevice::Text); Q_ASSERT(opened); Q_UNUSED(opened); QList lines; QByteArray line; do { line = file.readLine(); if (!line.isEmpty()) lines.append(line); } while(!line.isEmpty()); return lines; } static QList readLines(const char* fileName = "kconfigtest") { const QString path = KStandardDirs::locateLocal("config", fileName); Q_ASSERT(!path.isEmpty()); return readLinesFrom(path); } // see also testDefaults, which tests reverting with a defaults (global) file available void KConfigTest::testDirtyAfterRevert() { KConfig sc( "kconfigtest_revert" ); KConfigGroup cg(&sc, "Hello"); cg.revertToDefault( "does_not_exist" ); QVERIFY(!sc.isDirty()); cg.writeEntry("Test", "Correct"); QVERIFY(sc.isDirty()); sc.sync(); QVERIFY(!sc.isDirty()); cg.revertToDefault("Test"); QVERIFY(sc.isDirty()); sc.sync(); QVERIFY(!sc.isDirty()); cg.revertToDefault("Test"); QVERIFY(!sc.isDirty()); } void KConfigTest::testRevertAllEntries() { // this tests the case were we revert (delete) all entries in a file, // leaving a blank file { KConfig sc( "konfigtest2", KConfig::SimpleConfig ); KConfigGroup cg( &sc, "Hello" ); cg.writeEntry( "Test", "Correct" ); } { KConfig sc( "konfigtest2", KConfig::SimpleConfig ); KConfigGroup cg( &sc, "Hello" ); QCOMPARE( cg.readEntry( "Test", "Default" ), QString("Correct") ); cg.revertToDefault( "Test" ); } KConfig sc( "konfigtest2", KConfig::SimpleConfig ); KConfigGroup cg( &sc, "Hello" ); QCOMPARE( cg.readEntry( "Test", "Default" ), QString("Default") ); } void KConfigTest::testSimple() { KConfig sc2( "kconfigtest" ); QCOMPARE(sc2.name(), QString("kconfigtest")); // make sure groupList() isn't returning something it shouldn't foreach(const QString& group, sc2.groupList()) { QVERIFY(!group.isEmpty() && group != ""); QVERIFY(!group.contains(QChar(0x1d))); } KConfigGroup sc3( &sc2, "AAA"); QVERIFY( sc3.hasKey( "stringEntry1" ) ); QVERIFY( !sc3.isEntryImmutable("stringEntry1") ); QCOMPARE( sc3.readEntry( "stringEntry1" ), QString( STRINGENTRY1 ) ); QVERIFY( !sc3.hasKey( "stringEntry2" ) ); QCOMPARE( sc3.readEntry( "stringEntry2", QString("bla") ), QString( "bla" ) ); QVERIFY( !sc3.hasDefault( "stringEntry1" ) ); sc3 = KConfigGroup(&sc2, "Hello"); QCOMPARE( sc3.readEntry( "Test", QByteArray() ), QByteArray( UTF8BITENTRY ) ); QCOMPARE( sc3.readEntry( "bytearrayEntry", QByteArray() ), BYTEARRAYENTRY ); QCOMPARE( sc3.readEntry( ESCAPEKEY ), QString( ESCAPEENTRY ) ); QCOMPARE( sc3.readEntry( "Test", QString() ), QString::fromUtf8( UTF8BITENTRY ) ); QCOMPARE( sc3.readEntry( "emptyEntry"/*, QString("Fietsbel")*/), QString("") ); QCOMPARE( sc3.readEntry("emptyEntry", QString("Fietsbel")).isEmpty(), true ); QCOMPARE( sc3.readEntry( "stringEntry1" ), QString( STRINGENTRY1 ) ); QCOMPARE( sc3.readEntry( "stringEntry2" ), QString( STRINGENTRY2 ) ); QCOMPARE( sc3.readEntry( "stringEntry3" ), QString( STRINGENTRY3 ) ); QCOMPARE( sc3.readEntry( "stringEntry4" ), QString( STRINGENTRY4 ) ); QVERIFY( !sc3.hasKey( "stringEntry5" ) ); QCOMPARE( sc3.readEntry( "stringEntry5", QString("test") ), QString( "test" ) ); QVERIFY( !sc3.hasKey( "stringEntry6" ) ); QCOMPARE( sc3.readEntry( "stringEntry6", QString("foo") ), QString( "foo" ) ); QCOMPARE( sc3.readEntry( "boolEntry1", BOOLENTRY1 ), BOOLENTRY1 ); QCOMPARE( sc3.readEntry( "boolEntry2", false ), BOOLENTRY2 ); QCOMPARE( sc3.readEntry("keywith=equalsign", QString("wrong")), QString(STRINGENTRY1)); QCOMPARE( sc3.readEntry( "byteArrayEntry1", QByteArray() ), QByteArray( STRINGENTRY1 ) ); QCOMPARE( sc3.readEntry( "doubleEntry1", 0.0 ), DOUBLEENTRY ); QCOMPARE( sc3.readEntry( "floatEntry1", 0.0f ), FLOATENTRY ); } void KConfigTest::testDefaults() { KConfig config("defaulttest", KConfig::NoGlobals); const QString defaultsFile = "defaulttest.defaults"; KConfig defaults(defaultsFile, KConfig::SimpleConfig); const QString Default("Default"); const QString NotDefault("Not Default"); const QString Value1(STRINGENTRY1); const QString Value2(STRINGENTRY2); KConfigGroup group = defaults.group("any group"); group.writeEntry("entry1", Default); group.sync(); group = config.group("any group"); group.writeEntry("entry1", Value1); group.writeEntry("entry2", Value2); group.sync(); config.addConfigSources(QStringList() << KStandardDirs::locateLocal("config", defaultsFile)); config.setReadDefaults(true); QCOMPARE(group.readEntry("entry1", QString()), Default); QCOMPARE(group.readEntry("entry2", NotDefault), NotDefault); // no default for entry2 config.setReadDefaults(false); QCOMPARE(group.readEntry("entry1", Default), Value1); QCOMPARE(group.readEntry("entry2", NotDefault), Value2); group.revertToDefault("entry1"); QCOMPARE(group.readEntry("entry1", QString()), Default); group.revertToDefault("entry2"); QCOMPARE(group.readEntry("entry2", QString()), QString()); // TODO test reverting localized entries Q_ASSERT(config.isDirty()); group.sync(); // Check that everything is OK on disk, too KConfig reader("defaulttest", KConfig::NoGlobals); reader.addConfigSources(QStringList() << KStandardDirs::locateLocal("config", defaultsFile)); KConfigGroup readerGroup = reader.group("any group"); QCOMPARE(readerGroup.readEntry("entry1", QString()), Default); QCOMPARE(readerGroup.readEntry("entry2", QString()), QString()); } void KConfigTest::testLocale() { KConfig config("kconfigtest.locales", KConfig::SimpleConfig); const QString Translated(TRANSLATEDSTRINGENTRY1); const QString Untranslated(STRINGENTRY1); KConfigGroup group = config.group("Hello"); group.writeEntry("stringEntry1", Untranslated); config.setLocale("fr"); group.writeEntry("stringEntry1", Translated, KConfig::Localized|KConfig::Persistent); config.sync(); QCOMPARE(group.readEntry("stringEntry1", QString()), Translated); QCOMPARE(group.readEntryUntranslated("stringEntry1"), Untranslated); config.setLocale("C"); // strings written in the "C" locale are written as nonlocalized group.writeEntry("stringEntry1", Untranslated, KConfig::Localized|KConfig::Persistent); config.sync(); QCOMPARE(group.readEntry("stringEntry1", QString()), Untranslated); } void KConfigTest::testEncoding() { QString groupstr = QString::fromUtf8("UTF-8:\xc3\xb6l"); KConfig c( "kconfigtestencodings" ); KConfigGroup cg(&c, groupstr); cg.writeEntry("key", "value"); c.sync(); QList lines = readLines("kconfigtestencodings"); QCOMPARE(lines.count(), 2); QCOMPARE(lines.first(), QByteArray("[UTF-8:\xc3\xb6l]\n")); KConfig c2( "kconfigtestencodings" ); KConfigGroup cg2(&c2, groupstr); QVERIFY(cg2.readEntry("key") == QByteArray("value")); QVERIFY(c2.groupList().contains(groupstr)); } void KConfigTest::testLists() { KConfig sc2( "kconfigtest" ); KConfigGroup sc3(&sc2, "List Types"); QCOMPARE( sc3.readEntry( QString("stringListEntry"), QStringList()), STRINGLISTENTRY ); QCOMPARE( sc3.readEntry( QString("stringListEmptyEntry"), QStringList("wrong") ), STRINGLISTEMPTYENTRY ); QCOMPARE( sc3.readEntry( QString("stringListJustEmptyElement"), QStringList() ), STRINGLISTJUSTEMPTYELEMENT ); QCOMPARE( sc3.readEntry( QString("stringListEmptyTrailingElement"), QStringList() ), STRINGLISTEMPTYTRAINLINGELEMENT ); QCOMPARE( sc3.readEntry( QString("stringListEscapeOddEntry"), QStringList()), STRINGLISTESCAPEODDENTRY ); QCOMPARE( sc3.readEntry( QString("stringListEscapeEvenEntry"), QStringList()), STRINGLISTESCAPEEVENENTRY ); QCOMPARE( sc3.readEntry( QString("stringListEscapeCommaEntry"), QStringList()), STRINGLISTESCAPECOMMAENTRY ); QCOMPARE( sc3.readEntry( "listOfIntsEntry1" ), QString::fromLatin1( "1,2,3,4" ) ); QList expectedIntList = INTLISTENTRY1; QVERIFY( sc3.readEntry( "listOfIntsEntry1", QList() ) == expectedIntList ); QCOMPARE( QVariant(sc3.readEntry( "variantListEntry", VARIANTLISTENTRY )).toStringList(), QVariant(VARIANTLISTENTRY).toStringList() ); QCOMPARE( sc3.readEntry( "listOfByteArraysEntry1", QList()), BYTEARRAYLISTENTRY1 ); } void KConfigTest::testPath() { KConfig sc2( "kconfigtest" ); KConfigGroup sc3(&sc2, "Path Type"); QCOMPARE( sc3.readPathEntry( "homepath", QString() ), HOMEPATH ); QCOMPARE( sc3.readPathEntry( "homepathescape", QString() ), HOMEPATHESCAPE ); QCOMPARE( sc3.entryMap()["homepath"], HOMEPATH ); { QFile file(KStandardDirs::locateLocal("config", "pathtest")); file.open(QIODevice::WriteOnly|QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[Test Group]" << endl << "homePath=$HOME/foo" << endl << "homePath2=file://$HOME/foo" << endl << "withBraces[$e]=file://${HOME}/foo" << endl << "URL[$e]=file://${HOME}/foo" << endl << "hostname[$e]=$(hostname)" << endl << "noeol=foo"; // no EOL } KConfig cf2("pathtest"); KConfigGroup group = cf2.group("Test Group"); QVERIFY(group.hasKey("homePath")); QCOMPARE(group.readPathEntry("homePath", QString()), HOMEPATH); QVERIFY(group.hasKey("homePath2")); QCOMPARE(group.readPathEntry("homePath2", QString()), QString("file://" + HOMEPATH) ); QVERIFY(group.hasKey("withBraces")); QCOMPARE(group.readPathEntry("withBraces", QString()), QString("file://" + HOMEPATH) ); QVERIFY(group.hasKey("URL")); QCOMPARE(group.readEntry("URL", QString()), QString("file://" + HOMEPATH) ); -#if !defined(Q_OS_WIN32) && !defined(Q_OS_MAC) - // I don't know if this will work on windows - // This test hangs on OS X QVERIFY(group.hasKey("hostname")); - QCOMPARE(group.readEntry("hostname", QString()), QHostInfo::localHostName()); -#endif + QCOMPARE(group.readEntry("hostname", QString()), QString("(hostname)")); // the $ got removed because empty var name QVERIFY(group.hasKey("noeol")); QCOMPARE(group.readEntry("noeol", QString()), QString("foo")); } void KConfigTest::testPersistenceOfExpandFlagForPath() { // This test checks that a path entry starting with $HOME is still flagged // with the expand flag after the config was altered without rewriting the // path entry. // 1st step: Open the config, add a new dummy entry and then sync the config // back to the storage. { KConfig sc2( "kconfigtest" ); KConfigGroup sc3(&sc2, "Path Type"); sc3.writeEntry( "dummy", "dummy" ); sc2.sync(); } // 2nd step: Call testPath() again. Rewriting the config must not break // the testPath() test. testPath(); } void KConfigTest::testComplex() { KConfig sc2( "kconfigtest" ); KConfigGroup sc3(&sc2, "Complex Types"); QCOMPARE( sc3.readEntry( "pointEntry", QPoint() ), POINTENTRY ); QCOMPARE( sc3.readEntry( "sizeEntry", SIZEENTRY ), SIZEENTRY); QCOMPARE( sc3.readEntry( "rectEntry", QRect(1,2,3,4) ), RECTENTRY ); QCOMPARE( sc3.readEntry( "dateTimeEntry", QDateTime() ).toString(Qt::ISODate), DATETIMEENTRY.toString(Qt::ISODate) ); QCOMPARE( sc3.readEntry( "dateEntry", QDate() ).toString(Qt::ISODate), DATETIMEENTRY.date().toString(Qt::ISODate) ); QCOMPARE( sc3.readEntry( "dateTimeEntry", QDate() ), DATETIMEENTRY.date() ); } void KConfigTest::testEnums() { KConfig sc("kconfigtest"); KConfigGroup sc3(&sc, "Enum Types" ); QCOMPARE( sc3.readEntry( "enum-10" ), QString("Tens")); QVERIFY( readEntry( sc3, "enum-100", Ones) != Ones); QVERIFY( readEntry( sc3, "enum-100", Ones) != Tens); QCOMPARE( sc3.readEntry( "flags-bit0" ), QString("bit0")); QVERIFY( readEntry( sc3, "flags-bit0", Flags() ) == bit0 ); int eid = staticMetaObject.indexOfEnumerator( "Flags" ); QVERIFY( eid != -1 ); QMetaEnum me = staticMetaObject.enumerator( eid ); Flags bitfield = bit0|bit1; QCOMPARE( sc3.readEntry( "flags-bit0-bit1" ), QString( me.valueToKeys(bitfield) ) ); QVERIFY( readEntry( sc3, "flags-bit0-bit1", Flags() ) == bitfield ); } void KConfigTest::testEntryMap() { KConfig sc("kconfigtest"); KConfigGroup cg(&sc, "Hello"); QMap entryMap = cg.entryMap(); qDebug() << entryMap.keys(); QCOMPARE(entryMap.value("stringEntry1"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("stringEntry2"), QString(STRINGENTRY2)); QCOMPARE(entryMap.value("stringEntry3"), QString(STRINGENTRY3)); QCOMPARE(entryMap.value("stringEntry4"), QString(STRINGENTRY4)); QVERIFY(!entryMap.contains("stringEntry5")); QVERIFY(!entryMap.contains("stringEntry6")); QCOMPARE(entryMap.value("Test"), QString::fromUtf8(UTF8BITENTRY)); QCOMPARE(entryMap.value("bytearrayEntry"), QString::fromUtf8(BYTEARRAYENTRY)); QCOMPARE(entryMap.value("emptyEntry"), QString()); QVERIFY(entryMap.contains("emptyEntry")); QCOMPARE(entryMap.value("boolEntry1"), QString(BOOLENTRY1?"true":"false")); QCOMPARE(entryMap.value("boolEntry2"), QString(BOOLENTRY2?"true":"false")); QCOMPARE(entryMap.value("keywith=equalsign"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("byteArrayEntry1"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("doubleEntry1"), QString::number(DOUBLEENTRY, 'g', 15)); QCOMPARE(entryMap.value("floatEntry1"), QString::number(FLOATENTRY, 'g', 8)); } void KConfigTest::testInvalid() { KConfig sc( "kconfigtest" ); // all of these should print a message to the kdebug.dbg file KConfigGroup sc3(&sc, "Invalid Types" ); sc3.writeEntry( "badList", VARIANTLISTENTRY2 ); QList list; // 1 element list list << 1; sc3.writeEntry( QString("badList"), list); QVERIFY( sc3.readEntry( "badList", QPoint() ) == QPoint() ); QVERIFY( sc3.readEntry( "badList", QRect() ) == QRect() ); QVERIFY( sc3.readEntry( "badList", QSize() ) == QSize() ); QVERIFY( sc3.readEntry( "badList", QDate() ) == QDate() ); QVERIFY( sc3.readEntry( "badList", QDateTime() ) == QDateTime() ); // 2 element list list << 2; sc3.writeEntry( "badList", list); QVERIFY( sc3.readEntry( "badList", QRect() ) == QRect() ); QVERIFY( sc3.readEntry( "badList", QDate() ) == QDate() ); QVERIFY( sc3.readEntry( "badList", QDateTime() ) == QDateTime() ); // 3 element list list << 303; sc3.writeEntry( "badList", list); QVERIFY( sc3.readEntry( "badList", QPoint() ) == QPoint() ); QVERIFY( sc3.readEntry( "badList", QRect() ) == QRect() ); QVERIFY( sc3.readEntry( "badList", QSize() ) == QSize() ); QVERIFY( sc3.readEntry( "badList", QDate() ) == QDate() ); // out of bounds QVERIFY( sc3.readEntry( "badList", QDateTime() ) == QDateTime() ); // 4 element list list << 4; sc3.writeEntry( "badList", list ); QVERIFY( sc3.readEntry( "badList", QPoint() ) == QPoint() ); QVERIFY( sc3.readEntry( "badList", QSize() ) == QSize() ); QVERIFY( sc3.readEntry( "badList", QDate() ) == QDate() ); QVERIFY( sc3.readEntry( "badList", QDateTime() ) == QDateTime() ); // 5 element list list[2] = 3; list << 5; sc3.writeEntry( "badList", list); QVERIFY( sc3.readEntry( "badList", QPoint() ) == QPoint() ); QVERIFY( sc3.readEntry( "badList", QRect() ) == QRect() ); QVERIFY( sc3.readEntry( "badList", QSize() ) == QSize() ); QVERIFY( sc3.readEntry( "badList", QDate() ) == QDate() ); QVERIFY( sc3.readEntry( "badList", QDateTime() ) == QDateTime() ); // 6 element list list << 6; sc3.writeEntry( "badList", list); QVERIFY( sc3.readEntry( "badList", QPoint() ) == QPoint() ); QVERIFY( sc3.readEntry( "badList", QRect() ) == QRect() ); QVERIFY( sc3.readEntry( "badList", QSize() ) == QSize() ); } void KConfigTest::testChangeGroup() { KConfig sc( "kconfigtest" ); KConfigGroup sc3(&sc, "Hello"); QCOMPARE(sc3.name(), QString("Hello")); KConfigGroup newGroup(sc3); #ifndef KDE_NO_DEPRECATED newGroup.changeGroup("FooBar"); // deprecated! QCOMPARE(newGroup.name(), QString("FooBar")); QCOMPARE(sc3.name(), QString("Hello")); // unchanged // Write into the "changed group" and check that it works newGroup.writeEntry("InFooBar", "FB"); QCOMPARE(KConfigGroup(&sc, "FooBar").entryMap().value("InFooBar"), QString("FB")); QCOMPARE(KConfigGroup(&sc, "Hello").entryMap().value("InFooBar"), QString()); #endif KConfigGroup rootGroup(sc.group("")); QCOMPARE(rootGroup.name(), QString("")); KConfigGroup sc32(rootGroup.group("Hello")); QCOMPARE(sc32.name(), QString("Hello")); KConfigGroup newGroup2(sc32); #ifndef KDE_NO_DEPRECATED newGroup2.changeGroup("FooBar"); // deprecated! QCOMPARE(newGroup2.name(), QString("FooBar")); QCOMPARE(sc32.name(), QString("Hello")); // unchanged #endif } // Simple test for deleteEntry void KConfigTest::testDeleteEntry() { const char* configFile = "kconfigdeletetest"; { KConfig conf(configFile); conf.group("Hello").writeEntry("DelKey", "ToBeDeleted"); } const QList lines = readLines(configFile); Q_ASSERT(lines.contains("[Hello]\n")); Q_ASSERT(lines.contains("DelKey=ToBeDeleted\n")); KConfig sc(configFile); KConfigGroup group(&sc, "Hello"); group.deleteEntry("DelKey"); QCOMPARE( group.readEntry("DelKey", QString("Fietsbel")), QString("Fietsbel") ); group.sync(); Q_ASSERT(!readLines(configFile).contains("DelKey=ToBeDeleted\n")); QCOMPARE( group.readEntry("DelKey", QString("still deleted")), QString("still deleted") ); } void KConfigTest::testDelete() { KConfig sc( "kconfigtest" ); KConfigGroup ct(&sc, "Complex Types"); // First delete a nested group KConfigGroup delgr(&ct, "Nested Group 3"); QVERIFY(delgr.exists()); QVERIFY(ct.hasGroup("Nested Group 3")); delgr.deleteGroup(); QVERIFY(!delgr.exists()); QVERIFY(!ct.hasGroup("Nested Group 3")); QVERIFY(ct.groupList().contains("Nested Group 3")); KConfigGroup ng(&ct, "Nested Group 2"); QVERIFY(sc.hasGroup("Complex Types")); QVERIFY(!sc.hasGroup("Does not exist")); sc.deleteGroup("Complex Types"); QCOMPARE(sc.group("Complex Types").keyList().count(), 0); QVERIFY(!sc.hasGroup("Complex Types")); // #192266 QVERIFY(!sc.group("Complex Types").exists()); QVERIFY(!ct.hasGroup("Nested Group 1")); QCOMPARE(ct.group("Nested Group 1").keyList().count(), 0); QCOMPARE(ct.group("Nested Group 2").keyList().count(), 0); QCOMPARE(ng.group("Nested Group 2.1").keyList().count(), 0); KConfigGroup cg(&sc , "AAA" ); cg.deleteGroup(); QVERIFY( sc.entryMap("Complex Types").isEmpty() ); QVERIFY( sc.entryMap("AAA").isEmpty() ); QVERIFY( !sc.entryMap("Hello").isEmpty() ); //not deleted group QVERIFY( sc.entryMap("FooBar").isEmpty() ); //inexistant group cg.sync(); // Check what happens on disk const QList lines = readLines(); //qDebug() << lines; QVERIFY(!lines.contains("[Complex Types]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 1]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 2]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 2.1]\n")); QVERIFY(!lines.contains("[AAA]\n")); QVERIFY(lines.contains("[Hello]\n")); // a group that was not deleted // test for entries that are marked as deleted when there is no default KConfig cf("kconfigtest", KConfig::SimpleConfig); // make sure there are no defaults cg = cf.group("Portable Devices"); cg.writeEntry("devices|manual|(null)", "whatever"); cg.writeEntry("devices|manual|/mnt/ipod", "/mnt/ipod"); cf.sync(); int count=0; foreach(const QByteArray& item, readLines()) if (item.startsWith("devices|")) // krazy:exclude=strings count++; QCOMPARE(count, 2); cg.deleteEntry("devices|manual|/mnt/ipod"); cf.sync(); foreach(const QByteArray& item, readLines()) QVERIFY(!item.contains("ipod")); } void KConfigTest::testDefaultGroup() { KConfig sc( "kconfigtest" ); KConfigGroup defaultGroup(&sc, ""); QCOMPARE(defaultGroup.name(), QString("")); QVERIFY(!defaultGroup.exists()); defaultGroup.writeEntry("TestKey", "defaultGroup"); QVERIFY(defaultGroup.exists()); QCOMPARE(defaultGroup.readEntry("TestKey", QString()), QString("defaultGroup")); sc.sync(); { // Test reading it KConfig sc2("kconfigtest"); KConfigGroup defaultGroup2(&sc2, ""); QCOMPARE(defaultGroup2.name(), QString("")); QVERIFY(defaultGroup2.exists()); QCOMPARE(defaultGroup2.readEntry("TestKey", QString()), QString("defaultGroup")); } { // Test reading it KConfig sc2("kconfigtest"); KConfigGroup emptyGroup(&sc2, ""); QCOMPARE(emptyGroup.name(), QString("")); QVERIFY(emptyGroup.exists()); QCOMPARE(emptyGroup.readEntry("TestKey", QString()), QString("defaultGroup")); } QList lines = readLines(); QVERIFY(!lines.contains("[]\n")); QCOMPARE(lines.first(), QByteArray("TestKey=defaultGroup\n")); // Now that the group exists make sure it isn't returned from groupList() foreach(const QString& group, sc.groupList()) { QVERIFY(!group.isEmpty() && group != ""); } defaultGroup.deleteGroup(); sc.sync(); // Test if deleteGroup worked lines = readLines(); QVERIFY(lines.first() != QByteArray("TestKey=defaultGroup\n")); } void KConfigTest::testEmptyGroup() { KConfig sc( "kconfigtest" ); KConfigGroup emptyGroup(&sc, ""); QCOMPARE(emptyGroup.name(), QString("")); // confusing, heh? QVERIFY(!emptyGroup.exists()); emptyGroup.writeEntry("TestKey", "emptyGroup"); QVERIFY(emptyGroup.exists()); QCOMPARE(emptyGroup.readEntry("TestKey", QString()), QString("emptyGroup")); sc.sync(); { // Test reading it KConfig sc2("kconfigtest"); KConfigGroup defaultGroup(&sc2, ""); QCOMPARE(defaultGroup.name(), QString("")); QVERIFY(defaultGroup.exists()); QCOMPARE(defaultGroup.readEntry("TestKey", QString()), QString("emptyGroup")); } { // Test reading it KConfig sc2("kconfigtest"); KConfigGroup emptyGroup2(&sc2, ""); QCOMPARE(emptyGroup2.name(), QString("")); QVERIFY(emptyGroup2.exists()); QCOMPARE(emptyGroup2.readEntry("TestKey", QString()), QString("emptyGroup")); } QList lines = readLines(); QVERIFY(!lines.contains("[]\n")); // there's no support for the [] group, in fact. QCOMPARE(lines.first(), QByteArray("TestKey=emptyGroup\n")); // Now that the group exists make sure it isn't returned from groupList() foreach(const QString& group, sc.groupList()) { QVERIFY(!group.isEmpty() && group != ""); } emptyGroup.deleteGroup(); sc.sync(); // Test if deleteGroup worked lines = readLines(); QVERIFY(lines.first() != QByteArray("TestKey=defaultGroup\n")); } void KConfigTest::testCascadingWithLocale() { KTempDir middleDir; KGlobal::dirs()->addPrefix(middleDir.name()); KTempDir globalDir; KGlobal::dirs()->addPrefix(globalDir.name()); const QString globalConfigDir = globalDir.name() + "/share/config"; QVERIFY(QDir().mkpath(globalConfigDir)); QFile global(globalConfigDir + "/foo.desktop"); QVERIFY(global.open(QIODevice::WriteOnly|QIODevice::Text)); QTextStream globalOut(&global); globalOut << "[Group]" << endl << "FromGlobal=true" << endl << "FromGlobal[fr]=vrai" << endl << "Name=Testing" << endl << "Name[fr]=FR" << endl << "Other=Global" << endl << "Other[fr]=Global_FR" << endl; global.close(); const QString middleConfigDir = middleDir.name() + "/share/config"; QVERIFY(QDir().mkpath(middleConfigDir)); QFile local(middleConfigDir + "/foo.desktop"); QVERIFY(local.open(QIODevice::WriteOnly|QIODevice::Text)); QTextStream out(&local); out << "[Group]" << endl << "FromLocal=true" << endl << "FromLocal[fr]=vrai" << endl << "Name=Local Testing" << endl << "Name[fr]=FR" << endl << "Other=English Only" << endl; local.close(); KConfig config("foo.desktop"); KConfigGroup group = config.group("Group"); QCOMPARE(group.readEntry("FromGlobal"), QString("true")); QCOMPARE(group.readEntry("FromLocal"), QString("true")); QCOMPARE(group.readEntry("Name"), QString("Local Testing")); config.setLocale("fr"); QCOMPARE(group.readEntry("FromGlobal"), QString("vrai")); QCOMPARE(group.readEntry("FromLocal"), QString("vrai")); QCOMPARE(group.readEntry("Name"), QString("FR")); QCOMPARE(group.readEntry("Other"), QString("English Only")); // Global_FR is locally overriden } void KConfigTest::testMerge() { KConfig config("mergetest", KConfig::SimpleConfig); KConfigGroup cg = config.group("some group"); cg.writeEntry("entry", " random entry"); cg.writeEntry("another entry", "blah blah blah"); { // simulate writing by another process QFile file(KStandardDirs::locateLocal("config", "mergetest")); file.open(QIODevice::WriteOnly|QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[Merged Group]" << endl << "entry1=Testing" << endl << "entry2=More Testing" << endl << "[some group]" << endl << "entry[fr]=French" << endl << "entry[es]=Spanish" << endl << "entry[de]=German" << endl; } config.sync(); { QList lines; // this is what the file should look like lines << "[Merged Group]\n" << "entry1=Testing\n" << "entry2=More Testing\n" << "\n" << "[some group]\n" << "another entry=blah blah blah\n" << "entry=\\srandom entry\n" << "entry[de]=German\n" << "entry[es]=Spanish\n" << "entry[fr]=French\n"; QFile file(KStandardDirs::locateLocal("config", "mergetest")); file.open(QIODevice::ReadOnly|QIODevice::Text); foreach (const QByteArray& line, lines) { QCOMPARE(line, file.readLine()); } } } void KConfigTest::testImmutable() { { QFile file(KStandardDirs::locateLocal("config", "immutabletest")); file.open(QIODevice::WriteOnly|QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[$i]" << endl << "entry1=Testing" << endl << "[group][$i]" << endl << "[group][subgroup][$i]" << endl; } KConfig config("immutabletest", KConfig::SimpleConfig); QVERIFY(config.isGroupImmutable(QByteArray())); KConfigGroup cg = config.group(QByteArray()); QVERIFY(cg.isEntryImmutable("entry1")); KConfigGroup cg1 = config.group("group"); QVERIFY(cg1.isImmutable()); KConfigGroup cg1a = cg.group("group"); QVERIFY(cg1a.isImmutable()); KConfigGroup cg2 = cg1.group("subgroup"); QVERIFY(cg2.isImmutable()); } void KConfigTest::testOptionOrder() { { QFile file(KStandardDirs::locateLocal("config", "doubleattrtest")); file.open(QIODevice::WriteOnly|QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[group3]" << endl << "entry2=unlocalized" << endl << "entry2[$i][de_DE]=t2" << endl; } KConfig config("doubleattrtest", KConfig::SimpleConfig); config.setLocale("de_DE"); KConfigGroup cg3 = config.group("group3"); QVERIFY(!cg3.isImmutable()); QCOMPARE(cg3.readEntry("entry2",""), QString("t2")); QVERIFY(cg3.isEntryImmutable("entry2")); config.setLocale("C"); QCOMPARE(cg3.readEntry("entry2",""), QString("unlocalized")); QVERIFY(!cg3.isEntryImmutable("entry2")); cg3.writeEntry("entry2","modified"); config.sync(); { QList lines; // this is what the file should look like lines << "[group3]\n" << "entry2=modified\n" << "entry2[de_DE][$i]=t2\n"; QFile file(KStandardDirs::locateLocal("config", "doubleattrtest")); file.open(QIODevice::ReadOnly|QIODevice::Text); foreach (const QByteArray& line, lines) { QCOMPARE(line, file.readLine()); } } } void KConfigTest::testGroupEscape() { KConfig config("groupescapetest", KConfig::SimpleConfig); QVERIFY( config.group(DOLLARGROUP).exists() ); } void KConfigTest::testSubGroup() { KConfig sc( "kconfigtest" ); KConfigGroup cg( &sc, "ParentGroup" ); QCOMPARE(cg.readEntry( "parentgrpstring", ""), QString("somevalue") ); KConfigGroup subcg1( &cg, "SubGroup1"); QCOMPARE(subcg1.name(), QString("SubGroup1")); QCOMPARE(subcg1.readEntry( "somestring", ""), QString("somevalue") ); KConfigGroup subcg2( &cg, "SubGroup2"); QCOMPARE(subcg2.name(), QString("SubGroup2")); QCOMPARE(subcg2.readEntry( "substring", ""), QString("somevalue") ); KConfigGroup subcg3( &cg, "SubGroup/3"); QCOMPARE(subcg3.readEntry( "sub3string", ""), QString("somevalue") ); QCOMPARE(subcg3.name(), QString("SubGroup/3")); KConfigGroup rcg( &sc, "" ); KConfigGroup srcg( &rcg, "ParentGroup" ); QCOMPARE(srcg.readEntry( "parentgrpstring", ""), QString("somevalue") ); QCOMPARE(cg.groupList(), (QStringList() << "SubGroup/3" << "SubGroup1" << "SubGroup2")); const QStringList expectedSubgroup3Keys = (QStringList() << "sub3string"); QCOMPARE(subcg3.keyList(), expectedSubgroup3Keys); const QStringList expectedParentGroupKeys(QStringList() << "parentgrpstring"); QCOMPARE(cg.keyList(), expectedParentGroupKeys); QCOMPARE(QStringList(cg.entryMap().keys()), expectedParentGroupKeys); QCOMPARE(QStringList(subcg3.entryMap().keys()), expectedSubgroup3Keys); // Create A group containing only other groups. We want to make sure it // shows up in groupList of sc KConfigGroup neg(&sc, "NoEntryGroup"); KConfigGroup negsub1(&neg, "NEG Child1"); negsub1.writeEntry( "entry", "somevalue" ); KConfigGroup negsub2(&neg, "NEG Child2"); KConfigGroup negsub3(&neg, "NEG Child3"); KConfigGroup negsub31(&negsub3, "NEG Child3-1"); KConfigGroup negsub4(&neg, "NEG Child4"); KConfigGroup negsub41(&negsub4, "NEG Child4-1"); negsub41.writeEntry( "entry", "somevalue" ); // A group exists if it has content QVERIFY(negsub1.exists()); // But it doesn't exist if it has no content // Ossi and David say: this is how it's supposed to work. // However you could add a dummy entry for now, or we could add a "Persist" feature to kconfig groups // which would make it written out, much like "immutable" already makes them persistent. QVERIFY(!negsub2.exists()); // A subgroup does not qualify as content if it is also empty QVERIFY(!negsub3.exists()); // A subgroup with content is ok QVERIFY(negsub4.exists()); // Only subgroups with content show up in groupList() //QEXPECT_FAIL("", "Empty subgroups do not show up in groupList()", Continue); //QCOMPARE(neg.groupList(), QStringList() << "NEG Child1" << "NEG Child2" << "NEG Child3" << "NEG Child4"); // This is what happens QCOMPARE(neg.groupList(), QStringList() << "NEG Child1" << "NEG Child4"); // make sure groupList() isn't returning something it shouldn't foreach(const QString& group, sc.groupList()) { QVERIFY(!group.isEmpty() && group != ""); QVERIFY(!group.contains(QChar(0x1d))); QVERIFY(!group.contains("subgroup")); QVERIFY(!group.contains("SubGroup")); } sc.sync(); // Check that the empty groups are not written out. const QList lines = readLines(); QVERIFY(lines.contains("[NoEntryGroup][NEG Child1]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child2]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child3]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child4]\n")); // implicit group, not written out QVERIFY(lines.contains("[NoEntryGroup][NEG Child4][NEG Child4-1]\n")); } void KConfigTest::testAddConfigSources() { KConfig cf("specificrc"); cf.addConfigSources(QStringList() << KStandardDirs::locateLocal("config", "baserc")); cf.reparseConfiguration(); KConfigGroup specificgrp(&cf, "Specific Only Group"); QCOMPARE(specificgrp.readEntry("ExistingEntry", ""), QString("DevValue")); KConfigGroup sharedgrp(&cf, "Shared Group"); QCOMPARE(sharedgrp.readEntry("SomeSpecificOnlyEntry",""), QString("DevValue")); QCOMPARE(sharedgrp.readEntry("SomeBaseOnlyEntry",""), QString("BaseValue")); QCOMPARE(sharedgrp.readEntry("SomeSharedEntry",""), QString("DevValue")); KConfigGroup basegrp(&cf, "Base Only Group"); QCOMPARE(basegrp.readEntry("ExistingEntry", ""), QString("BaseValue")); basegrp.writeEntry("New Entry Base Only", "SomeValue"); KConfigGroup newgrp(&cf, "New Group"); newgrp.writeEntry("New Entry", "SomeValue"); cf.sync(); KConfig plaincfg("specificrc"); KConfigGroup newgrp2(&plaincfg, "New Group"); QCOMPARE(newgrp2.readEntry("New Entry", ""), QString("SomeValue")); KConfigGroup basegrp2(&plaincfg, "Base Only Group"); QCOMPARE(basegrp2.readEntry("New Entry Base Only", ""), QString("SomeValue")); } void KConfigTest::testGroupCopyTo() { KConfig cf1("kconfigtest"); KConfigGroup original = cf1.group("Enum Types"); KConfigGroup copy = cf1.group("Enum Types Copy"); original.copyTo(©); // copy from one group to another QCOMPARE(copy.entryMap(), original.entryMap()); KConfig cf2("copy_of_kconfigtest", KConfig::SimpleConfig); QVERIFY(!cf2.hasGroup(original.name())); QVERIFY(!cf2.hasGroup(copy.name())); KConfigGroup newGroup = cf2.group(original.name()); original.copyTo(&newGroup); // copy from one file to another QVERIFY(cf2.hasGroup(original.name())); QVERIFY(!cf2.hasGroup(copy.name())); // make sure we didn't copy more than we wanted QCOMPARE(newGroup.entryMap(), original.entryMap()); } void KConfigTest::testConfigCopyToSync() { KConfig cf1("kconfigtest"); // Prepare source file KConfigGroup group(&cf1, "CopyToTest"); group.writeEntry("Type", "Test"); cf1.sync(); // Copy to "destination" const QString destination = KStandardDirs::locateLocal("config", "kconfigcopytotest"); QFile::remove(destination); KConfig cf2("kconfigcopytotest"); KConfigGroup group2(&cf2, "CopyToTest"); group.copyTo(&group2); QString testVal = group2.readEntry("Type"); QCOMPARE(testVal, QString("Test")); // should write to disk the copied data from group cf2.sync(); QVERIFY(QFile::exists(destination)); } void KConfigTest::testConfigCopyTo() { KConfig cf1("kconfigtest"); { // Prepare source file KConfigGroup group(&cf1, "CopyToTest"); group.writeEntry("Type", "Test"); cf1.sync(); } { // Copy to "destination" const QString destination = KStandardDirs::locateLocal("config", "kconfigcopytotest"); QFile::remove(destination); KConfig cf2; cf1.copyTo(destination, &cf2); KConfigGroup group2(&cf2, "CopyToTest"); QString testVal = group2.readEntry("Type"); QCOMPARE(testVal, QString("Test")); cf2.sync(); QVERIFY(QFile::exists(destination)); } // Check copied config file on disk KConfig cf3("kconfigcopytotest"); KConfigGroup group3(&cf3, "CopyToTest"); QString testVal = group3.readEntry("Type"); QCOMPARE(testVal, QString("Test")); } void KConfigTest::testReparent() { KConfig cf("kconfigtest"); const QString name("Enum Types"); KConfigGroup group = cf.group(name); const QMap originalMap = group.entryMap(); KConfigGroup parent = cf.group("Parent Group"); QVERIFY(!parent.hasGroup(name)); QVERIFY(group.entryMap() == originalMap); group.reparent(&parent); // see if it can be made a sub-group of another group QVERIFY(parent.hasGroup(name)); QCOMPARE(group.entryMap(), originalMap); group.reparent(&cf); // see if it can make it a top-level group again // QVERIFY(!parent.hasGroup(name)); QCOMPARE(group.entryMap(), originalMap); } void KConfigTest::testKAboutDataOrganizationDomain() { KAboutData data( "app", 0, ki18n("program"), "version", ki18n("description"), KAboutData::License_LGPL, ki18n("copyright"), ki18n("hello world"), "http://www.koffice.org" ); QCOMPARE( data.organizationDomain(), QString::fromLatin1( "koffice.org" ) ); KAboutData data2( "app", 0, ki18n("program"), "version", ki18n("description"), KAboutData::License_LGPL, ki18n("copyright"), ki18n("hello world"), "http://edu.kde.org/kig" ); QCOMPARE( data2.organizationDomain(), QString::fromLatin1( "kde.org" ) ); } static void ageTimeStamp(const QString& path, int nsec) { #ifdef Q_OS_UNIX QDateTime mtime = QFileInfo(path).lastModified().addSecs(-nsec); struct utimbuf utbuf; utbuf.actime = mtime.toTime_t(); utbuf.modtime = utbuf.actime; utime(QFile::encodeName(path), &utbuf); #else QTest::qSleep(nsec * 1000); #endif } void KConfigTest::testWriteOnSync() { QDateTime oldStamp, newStamp; KConfig sc("kconfigtest", KConfig::IncludeGlobals); // Age the timestamp of global config file a few sec, and collect it. QString globFile = KStandardDirs::locateLocal("config", "kdeglobals"); ageTimeStamp(globFile, 2); // age 2 sec oldStamp = QFileInfo(globFile).lastModified(); // Add a local entry and sync the config. // Should not rewrite the global config file. KConfigGroup cgLocal(&sc, "Locals"); cgLocal.writeEntry("someLocalString", "whatever"); sc.sync(); // Verify that the timestamp of global config file didn't change. newStamp = QFileInfo(globFile).lastModified(); QCOMPARE(newStamp, oldStamp); // Age the timestamp of local config file a few sec, and collect it. QString locFile = KStandardDirs::locateLocal("config", "kconfigtest"); ageTimeStamp(locFile, 2); // age 2 sec oldStamp = QFileInfo(locFile).lastModified(); // Add a global entry and sync the config. // Should not rewrite the local config file. KConfigGroup cgGlobal(&sc, "Globals"); cgGlobal.writeEntry("someGlobalString", "whatever", KConfig::Persistent|KConfig::Global); sc.sync(); // Verify that the timestamp of local config file didn't change. newStamp = QFileInfo(locFile).lastModified(); QCOMPARE(newStamp, oldStamp); } void KConfigTest::testDirtyOnEqual() { QDateTime oldStamp, newStamp; KConfig sc("kconfigtest"); // Initialize value KConfigGroup cgLocal(&sc, "random"); cgLocal.writeEntry("theKey", "whatever"); sc.sync(); // Age the timestamp of local config file a few sec, and collect it. QString locFile = KStandardDirs::locateLocal("config", "kconfigtest"); ageTimeStamp(locFile, 2); // age 2 sec oldStamp = QFileInfo(locFile).lastModified(); // Write exactly the same again cgLocal.writeEntry("theKey", "whatever"); // This should be a no-op sc.sync(); // Verify that the timestamp of local config file didn't change. newStamp = QFileInfo(locFile).lastModified(); QCOMPARE(newStamp, oldStamp); } void KConfigTest::testDirtyOnEqualOverdo() { QByteArray val1("\0""one", 4); QByteArray val2("\0""two", 4); QByteArray defvalr; KConfig sc("kconfigtest"); KConfigGroup cgLocal(&sc, "random"); cgLocal.writeEntry("someKey", val1); QCOMPARE(cgLocal.readEntry("someKey", defvalr), val1); cgLocal.writeEntry("someKey", val2); QCOMPARE(cgLocal.readEntry("someKey", defvalr), val2); } void KConfigTest::testCreateDir() { // Test auto-creating the parent directory when needed (KConfigIniBackend::createEnclosing) QString kdehome = QDir::home().canonicalPath() + "/.kde-unit-test"; QString subdir = kdehome + "/newsubdir"; QString file = subdir + "/foo.desktop"; QFile::remove(file); QDir().rmdir(subdir); QVERIFY(!QDir().exists(subdir)); KDesktopFile desktopFile(file); desktopFile.desktopGroup().writeEntry("key", "value"); desktopFile.sync(); QVERIFY(QFile::exists(file)); // Cleanup QFile::remove(file); QDir().rmdir(subdir); } void KConfigTest::testSyncOnExit() { // Often, the KGlobalPrivate global static's destructor ends up calling ~KConfig -> // KConfig::sync ... and if that code triggers KGlobal code again then things could crash. // So here's a test for modifying KGlobal::config() and not syncing, the process exit will sync. KConfigGroup grp(KGlobal::config(), "syncOnExit"); grp.writeEntry("key", "value"); } void KConfigTest::testSharedConfig() { // Can I use a KConfigGroup even after the KSharedConfigPtr goes out of scope? KConfigGroup myConfigGroup; { KSharedConfigPtr config = KSharedConfig::openConfig("kconfigtest"); myConfigGroup = KConfigGroup(config, "Hello"); } QCOMPARE(myConfigGroup.readEntry("stringEntry1"), QString(STRINGENTRY1)); } void KConfigTest::testLocaleConfig() { // Initialize the testdata QDir dir; QString subdir = QDir::home().canonicalPath() + "/.kde-unit-test/"; dir.mkpath(subdir); QString file = subdir + "/localized.test"; QFile::remove(file); QFile f(file); QVERIFY(f.open(QIODevice::WriteOnly)); QTextStream ts(&f); ts << "[Test_Wrong]\n"; ts << "foo[ca]=5\n"; ts << "foostring[ca]=nice\n"; ts << "foobool[ca]=true\n"; ts << "[Test_Right]\n"; ts << "foo=5\n"; ts << "foo[ca]=5\n"; ts << "foostring=primary\n"; ts << "foostring[ca]=nice\n"; ts << "foobool=primary\n"; ts << "foobool[ca]=true\n"; f.close(); // Load the testdata QVERIFY(QFile::exists(file)); KConfig config(file); config.setLocale("ca"); // This group has only localized values. That is not supported. The values // should be dropped on loading. KConfigGroup cg(&config, "Test_Wrong"); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foo")); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foobool")); // Now check the correct config group KConfigGroup cg2(&config, "Test_Right"); QCOMPARE(cg2.readEntry("foo"), QString("5")); QCOMPARE(cg2.readEntry("foo", 3), 5); QCOMPARE(cg2.readEntry("foostring"), QString("nice")); QCOMPARE(cg2.readEntry("foostring", "ugly"), QString("nice")); QCOMPARE(cg2.readEntry("foobool"), QString("true")); QCOMPARE(cg2.readEntry("foobool", false), true); // Clean up after the testcase QFile::remove(file); } void KConfigTest::testDeleteWhenLocalized() { // Initialize the testdata QDir dir; QString subdir = QDir::home().canonicalPath() + "/.kde-unit-test/"; dir.mkpath(subdir); QString file = subdir + "/localized_delete.test"; QFile::remove(file); QFile f(file); QVERIFY(f.open(QIODevice::WriteOnly)); QTextStream ts(&f); ts << "[Test4711]\n"; ts << "foo=3\n"; ts << "foo[ca]=5\n"; ts << "foo[de]=7\n"; ts << "foostring=ugly\n"; ts << "foostring[ca]=nice\n"; ts << "foostring[de]=schoen\n"; ts << "foobool=false\n"; ts << "foobool[ca]=true\n"; ts << "foobool[de]=true\n"; f.close(); // Load the testdata. We start in locale "ca". QVERIFY(QFile::exists(file)); KConfig config(file); config.setLocale("ca"); KConfigGroup cg(&config, "Test4711"); // Delete a value. Once with localized, once with Normal cg.deleteEntry("foostring", KConfigBase::Persistent | KConfigBase::Localized); cg.deleteEntry("foobool"); config.sync(); // The value is now gone. The others are still there. Everything correct // here. QVERIFY(!cg.hasKey("foostring")); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // The current state is: (Just return before this comment.) // [...] // foobool[ca]=true // foobool[de]=wahr // foostring=ugly // foostring[de]=schoen // Now switch the locale to "de" and repeat the checks. Results should be // the same. But they currently are not. The localized value are // independent of each other. All values are still there in "de". config.setLocale("de"); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // Check where the wrong values come from. // We get the "de" value. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("schoen")); // We get the "de" value. QCOMPARE(cg.readEntry("foobool", false), true); // Now switch the locale back "ca" and repeat the checks. Results are // again different. config.setLocale("ca"); // This line worked above. But now it fails. QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); // This line worked above too. QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // Check where the wrong values come from. // We get the primary value because the "ca" value was deleted. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("ugly")); // We get the "ca" value. QCOMPARE(cg.readEntry("foobool", false), true); // Now test the deletion of a group. cg.deleteGroup(); config.sync(); // Current state: [ca] and [de] entries left... oops. //qDebug() << readLinesFrom(file); // Bug: The group still exists [because of the localized entries]... QVERIFY(cg.exists()); QVERIFY(!cg.hasKey("foo")); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Now switch the locale to "de" and repeat the checks. All values // still here because only the primary values are deleted. config.setLocale("de"); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foo")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Check where the wrong values come from. // We get the "de" value. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("schoen")); // We get the "de" value. QCOMPARE(cg.readEntry("foobool", false), true); // We get the "de" value. QCOMPARE(cg.readEntry("foo", 0), 7); // Now switch the locale to "ca" and repeat the checks // "foostring" is now really gone because both the primary value and the // "ca" value are deleted. config.setLocale("ca"); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foo")); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Check where the wrong values come from. // We get the "ca" value. QCOMPARE(cg.readEntry("foobool", false), true); // We get the "ca" value. QCOMPARE(cg.readEntry("foo", 0), 5); // Cleanup QFile::remove(file); } void KConfigTest::testKdeGlobals() { { KConfig glob("kdeglobals"); KConfigGroup general(&glob, "General"); general.writeEntry("testKG", "1"); glob.sync(); } KConfig globRead("kdeglobals"); const KConfigGroup general(&globRead, "General"); QCOMPARE(general.readEntry("testKG"), QString("1")); // Check we wrote into kdeglobals const QList lines = readLines("kdeglobals"); QVERIFY(lines.contains("[General]\n")); QVERIFY(lines.contains("testKG=1\n")); // Writing using NoGlobals { KConfig glob("kdeglobals", KConfig::NoGlobals); KConfigGroup general(&glob, "General"); general.writeEntry("testKG", "2"); glob.sync(); } globRead.reparseConfiguration(); QCOMPARE(general.readEntry("testKG"), QString("2")); // Reading using NoGlobals { KConfig globReadNoGlob("kdeglobals", KConfig::NoGlobals); const KConfigGroup generalNoGlob(&globReadNoGlob, "General"); QCOMPARE(generalNoGlob.readEntry("testKG"), QString("2")); } // TODO now use kconfigtest and writeEntry(,Global) -> should go into kdeglobals } void KConfigTest::testAnonymousConfig() { KConfig anonConfig(QString(), KConfig::SimpleConfig); KConfigGroup general(&anonConfig, "General"); QCOMPARE(general.readEntry("testKG"), QString()); // no kdeglobals merging general.writeEntry("Foo", "Bar"); QCOMPARE(general.readEntry("Foo"), QString("Bar")); } void KConfigTest::testNoKdeHome() { const QString kdeHome = QDir::homePath() + "/.kde-unit-test-does-not-exist"; setenv("KDEHOME", QFile::encodeName( kdeHome ), 1); KTempDir::removeDir(kdeHome); QVERIFY(!QFile::exists(kdeHome)); // Do what kde4-config does, and ensure kdehome doesn't get created (#233892) KComponentData componentData("KConfigTest"); QVERIFY(!QFile::exists(kdeHome)); componentData.dirs(); QVERIFY(!QFile::exists(kdeHome)); componentData.config(); QVERIFY(!QFile::exists(kdeHome)); // Now try to actually save something, see if it works. KConfigGroup group(componentData.config(), "Group"); group.writeEntry("Key", "Value"); group.sync(); QVERIFY(QFile::exists(kdeHome)); QVERIFY(QFile::exists(kdeHome + "/share/config/KConfigTestrc")); // Cleanup KTempDir::removeDir(kdeHome); } #include #include // To find multithreading bugs: valgrind --tool=helgrind --track-lockorders=no ./kconfigtest testThreads void KConfigTest::testThreads() { QThreadPool::globalInstance()->setMaxThreadCount(6); QList > futures; // Run in parallel some tests that work on different config files, // otherwise unexpected things might indeed happen. futures << QtConcurrent::run(this, &KConfigTest::testAddConfigSources); futures << QtConcurrent::run(this, &KConfigTest::testSimple); futures << QtConcurrent::run(this, &KConfigTest::testDefaults); // QEXPECT_FAIL triggers race conditions, it should be fixed to use QThreadStorage... //futures << QtConcurrent::run(this, &KConfigTest::testDeleteWhenLocalized); //futures << QtConcurrent::run(this, &KConfigTest::testEntryMap); Q_FOREACH(QFuture f, futures) // krazy:exclude=foreach f.waitForFinished(); }