Fix an empty XDG config directory hiding every profile (#9712)

#### Brief overview of PR changes/additions

- An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated
`~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran
its first-launch onboarding as though the user were new. It also stuck:
the first such launch wrote `Mudlet.ini` into that directory, which then
kept it winning.
- The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` >
exists > absent) and the stronger claim wins, with
`$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a
deliberate opt-in both still land there. A directory that cannot be
listed counts as populated rather than empty, so a permission bit cannot
re-enter the bug.
- Creating `profiles/` is now the opt-in a test harness uses; the
`mudlet` directory alone is not, because other tooling creates that by
accident. Where both roots hold profiles, `setupConfig()` names the one
it is ignoring instead of leaving those profiles apparently gone.

#### Motivation for adding to Mudlet

Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME
for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned
rather than destroyed, but a returning user sees "5.0 wiped my
profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p
"$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it.

#### Other info (issues closed, discussion etc)

Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`,
`mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the
onboarding dialog. After: both profiles listed.

`ConfigDirOverrideTest` covers the resolution table including the sticky
`Mudlet.ini` state, both-populated, symlinked and unreadable
directories; each new guard was mutation-checked. The busted suite
passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles`
root.

Not fixed here, and pre-existing rather than 5.0 regressions:
`CredentialManager` stores passwords and the OAuth reconnect token under
`AppConfigLocation` while the config root is `confPath`, so exporting
`XDG_CONFIG_HOME` strands them, and the plaintext-password migration
reads one path, writes the other and deletes the original. Both
reproduce identically on the 4.22.0 binary and need their own migration
path.

Assisted-by: Claude:claude-opus-5
This commit is contained in:
Vadim Peretokin 2026-08-10 22:17:09 +02:00 committed by GitHub
parent 72023317ed
commit 61c2afd406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 302 additions and 43 deletions

View file

@ -76,15 +76,17 @@ run is not cleaned up. To give a run its own pristine, isolated config root:
- `XDG_CONFIG_HOME` - Mudlet uses `$XDG_CONFIG_HOME/mudlet` as its config root
(profiles, sqlite databases, settings, and password storage). Because an
existing `~/.config/mudlet` otherwise wins (so a system-wide `XDG_CONFIG_HOME`
export never strands real profiles), a test harness must **pre-create**
`$XDG_CONFIG_HOME/mudlet` to opt in.
existing `~/.config/mudlet` holding profiles otherwise wins (so a system-wide
`XDG_CONFIG_HOME` export never strands real profiles), a test harness must
**pre-create** `$XDG_CONFIG_HOME/mudlet/profiles` to opt in. The `mudlet`
directory on its own is not enough - other tooling creates that by accident,
and treating it as an opt-in would hide the user's real profiles.
- `MUDLET_TEST_FAILURE_MARKER` - absolute path for the failure marker, so it is
not shared either.
```sh
CONFIG_DIR=$(mktemp -d)
mkdir -p "$CONFIG_DIR/mudlet" # pre-create to opt into the isolated config root
mkdir -p "$CONFIG_DIR/mudlet/profiles" # pre-create to opt into the isolated config root
AUTORUN_BUSTED_TESTS=true \
MUDLET_TEST_MODE=1 \
QUIT_MUDLET_AFTER_TESTS=true \

View file

@ -174,9 +174,11 @@ mudlet::mudlet()
// Initialisation happens later in setupConfig() and init()
}
static bool anyProfilesExist(const QString& profilesPath);
void mudlet::init()
{
smFirstLaunch = !QFile::exists(mudlet::getMudletPath(enums::profilesPath));
smFirstLaunch = !anyProfilesExist(mudlet::getMudletPath(enums::profilesPath));
// Must be after setupConfig() created mpSettings and before anything of this run is written
rememberFirstLaunch(*mpSettings, mudlet::getMudletPath(enums::profilesPath), QDateTime::currentDateTime());
@ -948,8 +950,12 @@ void mudlet::setupConfig()
const auto resolution = utils::xdgConfigDir(confDirDefault);
confPath = resolution.path;
if (resolution.migrationPending) {
qInfo().nospace() << "mudlet::setupConfig() INFO: XDG_CONFIG_HOME is set but $XDG_CONFIG_HOME/mudlet is not a Mudlet config directory yet, so the existing " << confPath
<< " is still in use. Move it to $XDG_CONFIG_HOME/mudlet to migrate.";
qInfo().nospace() << "mudlet::setupConfig() INFO: XDG_CONFIG_HOME is set but $XDG_CONFIG_HOME/mudlet holds no profiles, so the existing " << confPath
<< " is still in use. Move its contents into $XDG_CONFIG_HOME/mudlet to migrate.";
}
if (!resolution.shadowedProfilesPath.isEmpty()) {
qWarning().nospace() << "mudlet::setupConfig() WARN: using $XDG_CONFIG_HOME/mudlet (" << confPath << ") because it holds profiles, but " << resolution.shadowedProfilesPath
<< " holds profiles as well and they will not be listed. Unset XDG_CONFIG_HOME to use that directory instead.";
}
}
qDebug() << "mudlet::setupConfig() INFO:" << "using config dir:" << confPath;

View file

@ -171,43 +171,96 @@ public:
struct ConfigDirResolution
{
QString path;
// True only in the migration-guard case: XDG_CONFIG_HOME is set but
// $XDG_CONFIG_HOME/mudlet is not (yet) Mudlet's, so an existing legacy
// dir is used instead. The caller can then hint the user how to migrate.
// XDG_CONFIG_HOME is set, but an existing legacy dir was used anyway, so
// the caller can hint at the migration
bool migrationPending = false;
// legacyDefault, when it holds profiles that the chosen dir now hides. The
// caller has to name it, or those profiles read as gone.
QString shadowedProfilesPath;
};
// Resolve Mudlet's config root honoring XDG_CONFIG_HOME, with a migration
// guard. The caller handles portable.txt first (it still wins); this covers
// the rest:
// - XDG_CONFIG_HOME unset/empty/relative -> legacyDefault (~/.config/mudlet)
// - $XDG_CONFIG_HOME/mudlet is Mudlet's -> it (already migrated / opt-in)
// - not Mudlet's but legacyDefault exists -> legacyDefault, so exporting
// XDG_CONFIG_HOME never strands existing profiles
// - neither is usable -> $XDG_CONFIG_HOME/mudlet (fresh)
// "Mudlet's" means the dir holds a Mudlet.ini or profiles/, or is an empty
// opt-in dir a test harness pre-created. This deliberately ignores the stale
// $XDG_CONFIG_HOME/mudlet/Mudlet.conf that pre-4.19 Mudlet wrote there (its
// NativeFormat settings) while profiles stayed in ~/.config/mudlet - treating
// that leftover as the config root would hide such a user's profiles.
// How strongly a directory claims to be Mudlet's config root; the stronger
// claim wins in xdgConfigDir(), so the order is the contract.
enum class ConfigDirClaim {
absent = 0,
// Exists, but holds nothing Mudlet put there - including the stale
// Mudlet.conf pre-4.19 Mudlet left in $XDG_CONFIG_HOME/mudlet while its
// profiles stayed in ~/.config/mudlet
unclaimed = 1,
settings = 2,
profiles = 3,
};
// A directory that cannot be listed must never read as "nothing here": that
// inference is what hides profiles, so assume the strongest content instead.
static bool configDirHoldsProfiles(const QString& dir)
{
if (!QDir(dir).exists()) {
return false;
}
if (!QFileInfo(dir).isReadable()) {
return true;
}
const QDir profiles(qsl("%1/profiles").arg(dir));
if (!profiles.exists()) {
return false;
}
// Counted as mudlet.cpp's anyProfilesExist() does, so the two cannot disagree
return !QFileInfo(profiles.path()).isReadable() || !profiles.entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty();
}
static ConfigDirClaim configDirClaim(const QString& dir)
{
if (!QDir(dir).exists()) {
return ConfigDirClaim::absent;
}
if (configDirHoldsProfiles(dir)) {
return ConfigDirClaim::profiles;
}
if (QFileInfo::exists(qsl("%1/Mudlet.ini").arg(dir))) {
return ConfigDirClaim::settings;
}
return ConfigDirClaim::unclaimed;
}
// $XDG_CONFIG_HOME/mudlet claims more than it holds, because creating
// profiles/ there is the deliberate opt-in into an isolated config root. The
// legacy dir gets no such credit: an empty profiles/ left behind by deleting
// the last profile would otherwise outrank a config root in active use.
static ConfigDirClaim xdgConfigDirClaim(const QString& dir)
{
if (QDir(qsl("%1/profiles").arg(dir)).exists()) {
return ConfigDirClaim::profiles;
}
return configDirClaim(dir);
}
// cleanPath() is not enough: a symlinked ~/.config gives one directory two
// spellings, and dotfile managers produce exactly that
static QString configDirIdentity(const QString& dir)
{
const QString canonical = QFileInfo(dir).canonicalFilePath();
return canonical.isEmpty() ? QDir::cleanPath(dir) : canonical;
}
// Resolve Mudlet's config root honoring XDG_CONFIG_HOME; the caller handles
// portable.txt first, which still wins. $XDG_CONFIG_HOME/mudlet takes a tie so
// that a fresh install lands there.
static ConfigDirResolution xdgConfigDir(const QString& legacyDefault)
{
const QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME");
// The XDG base-dir spec requires an absolute path; a relative (or empty)
// value must be ignored, which also avoids a surprising CWD-relative root.
if (xdgConfigHome.isEmpty() || !QDir::isAbsolutePath(xdgConfigHome)) {
return {legacyDefault, false};
return {legacyDefault, false, QString()};
}
const QString xdgTarget = QDir::cleanPath(qsl("%1/mudlet").arg(xdgConfigHome));
const QDir xdgDir(xdgTarget);
const bool xdgIsMudlets = xdgDir.exists() && (QFileInfo::exists(qsl("%1/Mudlet.ini").arg(xdgTarget)) || QDir(qsl("%1/profiles").arg(xdgTarget)).exists() || xdgDir.isEmpty());
if (xdgIsMudlets) {
return {xdgTarget, false};
if (xdgConfigDirClaim(xdgTarget) < configDirClaim(legacyDefault)) {
return {legacyDefault, true, QString()};
}
if (QDir(legacyDefault).exists()) {
return {legacyDefault, true};
}
return {xdgTarget, false};
// XDG_CONFIG_HOME=$HOME/.config makes both candidates one directory
const bool shadowing = configDirIdentity(legacyDefault) != configDirIdentity(xdgTarget) && configDirHoldsProfiles(legacyDefault);
return {xdgTarget, false, shadowing ? legacyDefault : QString()};
}
inline static const auto scmfileSystemUnsafeChars = QRegularExpression(qsl(R"REGEX([/\\:*?"<>|])REGEX"));

View file

@ -25,6 +25,9 @@
* or the guard, which resurfaces as parallel-run sqlite flakiness or, worse,
* users' profiles appearing to vanish on upgrade.
*
* Creating $XDG_CONFIG_HOME/mudlet/profiles is the opt-in; the directory above it
* on its own is not, because that is a state other tooling creates by accident.
*
* The resolution logic lives in utils::xdgConfigDir(legacyDefault), which takes
* the legacy candidate as an argument, so most cases test it directly and stay
* platform-independent (no HOME/USERPROFILE juggling). A couple of cases drive
@ -47,6 +50,14 @@ private:
QString mudletUnder(const QString& dir) const { return QDir::cleanPath(qsl("%1/mudlet").arg(dir)); }
bool makeProfile(const QString& configDir, const QString& profileName) const { return QDir().mkpath(qsl("%1/profiles/%2").arg(configDir, profileName)); }
bool makeSettingsFile(const QString& configDir) const
{
QFile ini(qsl("%1/Mudlet.ini").arg(configDir));
return ini.open(QIODevice::WriteOnly);
}
// setupConfig() consults portable.txt beside the executable and in the home
// config dir before the XDG/default logic; the setupConfig() integration
// cases skip if one is present rather than report a baffling failure.
@ -101,13 +112,13 @@ private slots:
QVERIFY(!r.migrationPending);
}
void test_emptyXdgDirIsOptInAndWinsOverLegacy()
void test_emptyXdgDirWinsOverALegacyDirWithoutProfiles()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(target)); // empty opt-in dir
QVERIFY(QDir().mkpath(target));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(QDir().mkpath(legacy));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
@ -115,6 +126,78 @@ private slots:
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, target);
QVERIFY(!r.migrationPending);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
// A dotfile manager, container script or aborted move leaves this directory behind.
void test_emptyXdgDirDoesNotHideLegacyProfiles()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
QVERIFY(QDir().mkpath(mudletUnder(xdg.path())));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, legacy);
QVERIFY(r.migrationPending);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
// The state one bad launch leaves behind, since Mudlet writes its Mudlet.ini
// into whichever dir it chose. Deleting that dir has to be enough to recover.
void test_xdgSettingsFileDoesNotHideLegacyProfiles()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(qsl("%1/fonts").arg(target)));
QVERIFY(makeSettingsFile(target));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
QVERIFY(makeProfile(legacy, qsl("BetaGame")));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, legacy);
QVERIFY(r.migrationPending);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
void test_xdgProfilesDirWinsAndReportsShadowedLegacyProfiles()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(target)));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, target);
QVERIFY(!r.migrationPending);
QCOMPARE(r.shadowedProfilesPath, legacy);
}
void test_noShadowReportedWhenLegacyProfilesDirIsEmpty()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(target)));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(legacy)));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, target);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
void test_migratedXdgDirWinsOverLegacy()
@ -147,7 +230,7 @@ private slots:
QVERIFY(stale.open(QIODevice::WriteOnly));
stale.close();
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(legacy))); // real profiles live here
QVERIFY(makeProfile(legacy, qsl("AlphaGame"))); // real profiles live here
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
@ -155,6 +238,26 @@ private slots:
QVERIFY2(r.migrationPending, "a stale non-Mudlet XDG dir must not shadow real profiles");
}
// Deleting the last profile leaves an empty legacy profiles/ behind, which
// must not pull a config root in active use back out of $XDG_CONFIG_HOME.
void test_emptyLegacyProfilesDirDoesNotOutrankXdgSettings()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(target));
QVERIFY(makeSettingsFile(target));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(legacy)));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, target);
QVERIFY(!r.migrationPending);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
void test_guardKeepsLegacyWhenXdgTargetMissing()
{
QTemporaryDir xdg;
@ -168,6 +271,7 @@ private slots:
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, legacy);
QVERIFY(r.migrationPending);
QVERIFY(r.shadowedProfilesPath.isEmpty());
}
void test_freshInstallUsesXdgWhenNeitherExists()
@ -202,6 +306,81 @@ private slots:
QVERIFY2(!r.migrationPending, "no migration when the XDG target and legacy dir are the same");
}
// XDG_CONFIG_HOME=$HOME/.config is an ordinary export, and would otherwise
// warn on every startup about the directory it is using.
void test_noSelfShadowWhenXdgTargetEqualsLegacyWithProfiles()
{
QTemporaryDir cfg;
QVERIFY(cfg.isValid());
const QString legacy = mudletUnder(cfg.path());
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
qputenv("XDG_CONFIG_HOME", cfg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, legacy);
QVERIFY2(r.shadowedProfilesPath.isEmpty(), "a directory cannot shadow itself");
}
void test_noSelfShadowThroughASymlinkedConfigDir()
{
QTemporaryDir real;
QTemporaryDir linkHome;
QVERIFY(real.isValid() && linkHome.isValid());
const QString legacy = mudletUnder(real.path());
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
const QString linked = qsl("%1/config-link").arg(linkHome.path());
if (!QFile::link(real.path(), linked)) {
QSKIP("this filesystem does not support symlinks");
}
qputenv("XDG_CONFIG_HOME", linked.toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QVERIFY2(r.shadowedProfilesPath.isEmpty(), "one directory under two names is still one directory");
}
// The only case that observes the settings tier, and losing those settings
// drops firstLaunchDate, which re-runs onboarding.
void test_settingsOnlyLegacyOutranksEmptyXdgDir()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
QVERIFY(QDir().mkpath(mudletUnder(xdg.path())));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(QDir().mkpath(legacy));
QVERIFY(makeSettingsFile(legacy));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
QCOMPARE(r.path, legacy);
QVERIFY(r.migrationPending);
}
void test_unreadableLegacyDirStillOutranksAnEmptyXdgDir()
{
QTemporaryDir xdg;
QTemporaryDir legacyHome;
QVERIFY(xdg.isValid() && legacyHome.isValid());
QVERIFY(QDir().mkpath(mudletUnder(xdg.path())));
const QString legacy = mudletUnder(legacyHome.path() + qsl("/.config"));
QVERIFY(makeProfile(legacy, qsl("AlphaGame")));
// No traverse bit either, or QDir::exists() on profiles/ still answers and
// the ranking never has to fall back
if (!QFile::setPermissions(legacy, QFileDevice::Permissions())) {
QSKIP("cannot drop permissions on this filesystem");
}
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
const auto r = utils::xdgConfigDir(legacy);
const bool readableAnyway = QFileInfo(legacy).isReadable();
QVERIFY(QFile::setPermissions(legacy, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
if (readableAnyway) {
QSKIP("running as a user that bypasses permission bits");
}
QCOMPARE(r.path, legacy);
QVERIFY(r.migrationPending);
}
// --- mudlet::setupConfig() end-to-end wiring ------------------------------
void test_setupConfigUsesPreCreatedXdgTarget()
@ -212,13 +391,32 @@ private slots:
QTemporaryDir xdg;
QVERIFY(xdg.isValid());
const QString target = mudletUnder(xdg.path());
QVERIFY(QDir().mkpath(target));
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(target)));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
mudlet::self()->setupConfig();
QCOMPARE(mudlet::getMudletPath(enums::mainPath), target);
}
// The warning is all that tells an affected user where their other profiles went.
void test_setupConfigWarnsAboutShadowedLegacyProfiles()
{
if (portableMarkerPresent()) {
QSKIP("portable.txt present - setupConfig() takes the portable branch");
}
const QString legacy = qsl("%1/.config/mudlet").arg(QDir::homePath());
if (!utils::configDirHoldsProfiles(legacy)) {
QSKIP("no profiles in the real ~/.config/mudlet, so nothing can be shadowed");
}
QTemporaryDir xdg;
QVERIFY(xdg.isValid());
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(mudletUnder(xdg.path()))));
qputenv("XDG_CONFIG_HOME", xdg.path().toUtf8());
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(qsl("holds profiles as well")));
mudlet::self()->setupConfig();
}
// With XDG unset, the config root is the usual ~/.config/mudlet, so normal
// users are unaffected. Uses the real home dir - no HOME override.
void test_setupConfigUnsetUsesHomeConfigDir()

View file

@ -76,9 +76,9 @@ private slots:
initializeQRCResourcesForDefaultGameDeleteTest();
QVERIFY(mConfigDir.isValid());
// pre-create $XDG_CONFIG_HOME/mudlet so setupConfig() adopts it and the
// test never touches the real profiles or settings
QVERIFY(QDir().mkpath(qsl("%1/mudlet").arg(mConfigDir.path())));
// pre-create $XDG_CONFIG_HOME/mudlet/profiles so setupConfig() adopts it
// and the test never touches the real profiles or settings
QVERIFY(QDir().mkpath(qsl("%1/mudlet/profiles").arg(mConfigDir.path())));
mSavedXdg = qgetenv("XDG_CONFIG_HOME");
qputenv("XDG_CONFIG_HOME", mConfigDir.path().toUtf8());

View file

@ -296,17 +296,17 @@ private slots:
QSKIP("portable.txt present - setupConfig() takes the portable branch");
}
QVERIFY(mLiveConfig.isValid());
// An empty $XDG_CONFIG_HOME/mudlet is the opt-in marker, without which
// $XDG_CONFIG_HOME/mudlet/profiles is the opt-in marker, without which
// setupConfig() keeps using a legacy ~/.config/mudlet
const QString configDir = qsl("%1/mudlet").arg(mLiveConfig.path());
QVERIFY(QDir().mkpath(configDir));
QVERIFY(QDir().mkpath(qsl("%1/profiles").arg(configDir)));
qputenv("XDG_CONFIG_HOME", mLiveConfig.path().toUtf8());
initializeQRCResourcesForExperiencedPlayerGateTest();
mudlet::start();
mudlet::self()->setupConfig();
QCOMPARE(mudlet::getMudletPath(enums::mainPath), configDir);
QVERIFY(!QDir(mudlet::getMudletPath(enums::profilesPath)).exists());
QVERIFY2(QDir(mudlet::getMudletPath(enums::profilesPath)).entryList(QDir::Dirs | QDir::NoDotAndDotDot).isEmpty(), "the opt-in profiles/ dir has to be empty, or this is not a fresh install");
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();

View file

@ -129,7 +129,7 @@ private slots:
mSavedXdg = qgetenv("XDG_CONFIG_HOME");
QVERIFY(mXdgDir.isValid());
QVERIFY(QDir().mkpath(qsl("%1/mudlet").arg(mXdgDir.path()))); // empty dir = XDG opt-in
QVERIFY(QDir().mkpath(qsl("%1/mudlet/profiles").arg(mXdgDir.path()))); // profiles/ = XDG opt-in
qputenv("XDG_CONFIG_HOME", mXdgDir.path().toUtf8());
mudlet::start();