mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
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:
parent
72023317ed
commit
61c2afd406
7 changed files with 302 additions and 43 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
103
src/utils.h
103
src/utils.h
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue