Fix: crash when a button uninstalls its own package (#9558)
#### Brief overview of PR changes/additions
A toolbar or menu button whose Lua script calls `uninstallPackage()` on
its own package used to crash Mudlet. `ActionUnit::uninstall()` deleted
the package's buttons immediately, but the clicked button is still
inside `TAction::execute()`, which reads `this->mpHost` (to restore
command-line focus) *after* the script returns - a heap-use-after-free.
This applies the same processing-depth deferral already used for
triggers/aliases/keys and timers/scripts: while a button's script is
running, `uninstall()` deactivates and queues that package's actions
instead of deleting them, and the delete happens later in `doCleanup()`
once `execute()` has unwound. `ActionUnit` was the last object unit
still deleting immediately. The queued deletes are flushed by the button
dispatchers once the click returns and by the existing per-line /
periodic `doCleanup()` calls in `Host`, and a seen-set guards against a
double free from a re-entrant uninstall.
#### Motivation for adding to Mudlet
It is a hard crash reachable with a completely normal setup - a
"reload/update this package" button that reinstalls itself is a common
pattern.
#### Other info (issues closed, discussion etc)
Completes the self-uninstall use-after-free hardening across all object
units - a follow-up to #9557 (timers/scripts) and #9383
(triggers/aliases/keys); actions were the one remaining unguarded unit.
The crash is reported in Sentry as MUDLET-32 / MUDLET-2S / MUDLET-48.
**Test case:**
`test/functional_tests/ActionSelfUninstallTest.cpp` (Qt Test, built with
AddressSanitizer) builds a synthetic package whose toolbar button
uninstalls its own package, gives it a real toolbar widget, invokes the
button, and asserts no crash, the package removed, and the deferred
action freed cleanly with no double free. It trips a heap-use-after-free
in `TAction::execute()` on the pre-fix code and is ASan-clean with the
fix.
To reproduce by hand:
1. Create a package containing one button whose script is
`uninstallPackage("<that package's name>")`.
2. Click the button.
3. Before this PR: Mudlet crashes. After: the package uninstalls cleanly
and focus returns to the command line.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-30 12:02:32 +02:00
|
|
|
/***************************************************************************
|
|
|
|
|
* Copyright (C) 2026 by Vadim Peretokin - vadim.peretokin@mudlet.org *
|
|
|
|
|
* *
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
|
|
|
* it under the terms of the GNU General Public License as published by *
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
|
|
|
* (at your option) any later version. *
|
|
|
|
|
* *
|
|
|
|
|
* This program 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 General Public License for more details. *
|
|
|
|
|
* *
|
|
|
|
|
* You should have received a copy of the GNU General Public License *
|
|
|
|
|
* along with this program; if not, write to the *
|
|
|
|
|
* Free Software Foundation, Inc., *
|
|
|
|
|
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
|
|
#include <QtTest/QtTest>
|
|
|
|
|
|
|
|
|
|
#include <QPushButton>
|
|
|
|
|
|
|
|
|
|
#include "ActionUnit.h"
|
|
|
|
|
#include "Host.h"
|
|
|
|
|
#include "MudletInstanceCoordinator.h"
|
|
|
|
|
#include "TAction.h"
|
|
|
|
|
#include "TFlipButton.h"
|
|
|
|
|
#include "TLuaInterpreter.h"
|
|
|
|
|
#include "TMainConsole.h"
|
|
|
|
|
#include "TelnetServerStub.h"
|
|
|
|
|
#include "ctelnet.h"
|
|
|
|
|
#include "dlgConnectionProfiles.h"
|
|
|
|
|
#include "mudlet.h"
|
|
|
|
|
|
|
|
|
|
extern void qInitResources_mudlet();
|
|
|
|
|
extern void qInitResources_qm();
|
|
|
|
|
extern void qInitResources_additional_splash_screens();
|
|
|
|
|
extern void qInitResources_mudlet_fonts_common();
|
|
|
|
|
extern void qInitResources_mudlet_fonts_posix();
|
|
|
|
|
void initializeQRCResources();
|
|
|
|
|
|
|
|
|
|
// Regression tests for the self-uninstall use-after-free: a package toolbar
|
|
|
|
|
// button whose Lua script calls uninstallPackage() on its own package used to
|
|
|
|
|
// free the very TAction that TAction::execute() was running on, which then read
|
|
|
|
|
// this->mpHost after the Lua call returned (heap-use-after-free). ActionUnit now
|
|
|
|
|
// defers that delete until execute() has unwound.
|
infrastructure: rename the six tests Windows refuses to launch without elevation (#9753)
#### Brief overview of PR changes/additions
- Renames the six test executables whose filenames trip Windows' UAC
installer detection: `UpdaterChecksumTest` to
`ReleaseChecksumPairingTest`, `UpdaterPlatformAssetTest` to
`ReleasePlatformAssetTest`, `UpdaterTeardownTest` to
`NewReleaseDialogTeardownTest`, `PackageSelfUninstallTest` to
`PackageSelfRemovalTest`, `PackageUninstallSaveTeardownTest` to
`PackageRemovalSaveTeardownTest`, `ActionSelfUninstallTest` to
`ActionSelfRemovalTest`. Assertions are untouched.
- Adds a configure-time gate in `test/CMakeLists.txt` that fails with an
actionable message if any test executable name contains install, setup,
update or patch. It checks both the targets a configuration builds and
the test source filenames, so conditionally registered tests cannot slip
past it.
- Documents the naming rule in `test/README.md`.
#### Motivation for adding to Mudlet
Windows treats an unsigned executable named that way as an installer and
refuses to start it, so those six tests reported `BAD_COMMAND` for
anyone running the suite from an ordinary Windows shell - and because CI
runners are elevated, nothing caught it as more tests were added.
#### Other info (issues closed, discussion etc)
Fixes #9748
The gate was verified to fire on a target named after the guard
statement, on one in a subdirectory, on `EventDispatcherTest` (the
message names the offending substring, since "dispatch" contains
"patch"), on a test registered only under `USE_UPDATER` when configuring
with the updater off, and to fail loudly if the walk ever stops finding
executables.
**Test case:** `cmake --build build && ctest --test-dir build` - 92/92
pass; adding a test named e.g. `FooUpdateTest` fails the configure with
an explanation.
Assisted-by: Claude:claude-opus-5
2026-08-11 08:07:10 +02:00
|
|
|
class ActionSelfRemovalTest : public QObject
|
Fix: crash when a button uninstalls its own package (#9558)
#### Brief overview of PR changes/additions
A toolbar or menu button whose Lua script calls `uninstallPackage()` on
its own package used to crash Mudlet. `ActionUnit::uninstall()` deleted
the package's buttons immediately, but the clicked button is still
inside `TAction::execute()`, which reads `this->mpHost` (to restore
command-line focus) *after* the script returns - a heap-use-after-free.
This applies the same processing-depth deferral already used for
triggers/aliases/keys and timers/scripts: while a button's script is
running, `uninstall()` deactivates and queues that package's actions
instead of deleting them, and the delete happens later in `doCleanup()`
once `execute()` has unwound. `ActionUnit` was the last object unit
still deleting immediately. The queued deletes are flushed by the button
dispatchers once the click returns and by the existing per-line /
periodic `doCleanup()` calls in `Host`, and a seen-set guards against a
double free from a re-entrant uninstall.
#### Motivation for adding to Mudlet
It is a hard crash reachable with a completely normal setup - a
"reload/update this package" button that reinstalls itself is a common
pattern.
#### Other info (issues closed, discussion etc)
Completes the self-uninstall use-after-free hardening across all object
units - a follow-up to #9557 (timers/scripts) and #9383
(triggers/aliases/keys); actions were the one remaining unguarded unit.
The crash is reported in Sentry as MUDLET-32 / MUDLET-2S / MUDLET-48.
**Test case:**
`test/functional_tests/ActionSelfUninstallTest.cpp` (Qt Test, built with
AddressSanitizer) builds a synthetic package whose toolbar button
uninstalls its own package, gives it a real toolbar widget, invokes the
button, and asserts no crash, the package removed, and the deferred
action freed cleanly with no double free. It trips a heap-use-after-free
in `TAction::execute()` on the pre-fix code and is ASan-clean with the
fix.
To reproduce by hand:
1. Create a package containing one button whose script is
`uninstallPackage("<that package's name>")`.
2. Click the button.
3. Before this PR: Mudlet crashes. After: the package uninstalls cleanly
and focus returns to the command line.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-30 12:02:32 +02:00
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
TelnetServerStub* mpServer = nullptr;
|
infrastructure: rename the six tests Windows refuses to launch without elevation (#9753)
#### Brief overview of PR changes/additions
- Renames the six test executables whose filenames trip Windows' UAC
installer detection: `UpdaterChecksumTest` to
`ReleaseChecksumPairingTest`, `UpdaterPlatformAssetTest` to
`ReleasePlatformAssetTest`, `UpdaterTeardownTest` to
`NewReleaseDialogTeardownTest`, `PackageSelfUninstallTest` to
`PackageSelfRemovalTest`, `PackageUninstallSaveTeardownTest` to
`PackageRemovalSaveTeardownTest`, `ActionSelfUninstallTest` to
`ActionSelfRemovalTest`. Assertions are untouched.
- Adds a configure-time gate in `test/CMakeLists.txt` that fails with an
actionable message if any test executable name contains install, setup,
update or patch. It checks both the targets a configuration builds and
the test source filenames, so conditionally registered tests cannot slip
past it.
- Documents the naming rule in `test/README.md`.
#### Motivation for adding to Mudlet
Windows treats an unsigned executable named that way as an installer and
refuses to start it, so those six tests reported `BAD_COMMAND` for
anyone running the suite from an ordinary Windows shell - and because CI
runners are elevated, nothing caught it as more tests were added.
#### Other info (issues closed, discussion etc)
Fixes #9748
The gate was verified to fire on a target named after the guard
statement, on one in a subdirectory, on `EventDispatcherTest` (the
message names the offending substring, since "dispatch" contains
"patch"), on a test registered only under `USE_UPDATER` when configuring
with the updater off, and to fail loudly if the walk ever stops finding
executables.
**Test case:** `cmake --build build && ctest --test-dir build` - 92/92
pass; adding a test named e.g. `FooUpdateTest` fails the configure with
an explanation.
Assisted-by: Claude:claude-opus-5
2026-08-11 08:07:10 +02:00
|
|
|
const QString mpHostname = "Test-ActionSelfRemoval";
|
Fix: crash when a button uninstalls its own package (#9558)
#### Brief overview of PR changes/additions
A toolbar or menu button whose Lua script calls `uninstallPackage()` on
its own package used to crash Mudlet. `ActionUnit::uninstall()` deleted
the package's buttons immediately, but the clicked button is still
inside `TAction::execute()`, which reads `this->mpHost` (to restore
command-line focus) *after* the script returns - a heap-use-after-free.
This applies the same processing-depth deferral already used for
triggers/aliases/keys and timers/scripts: while a button's script is
running, `uninstall()` deactivates and queues that package's actions
instead of deleting them, and the delete happens later in `doCleanup()`
once `execute()` has unwound. `ActionUnit` was the last object unit
still deleting immediately. The queued deletes are flushed by the button
dispatchers once the click returns and by the existing per-line /
periodic `doCleanup()` calls in `Host`, and a seen-set guards against a
double free from a re-entrant uninstall.
#### Motivation for adding to Mudlet
It is a hard crash reachable with a completely normal setup - a
"reload/update this package" button that reinstalls itself is a common
pattern.
#### Other info (issues closed, discussion etc)
Completes the self-uninstall use-after-free hardening across all object
units - a follow-up to #9557 (timers/scripts) and #9383
(triggers/aliases/keys); actions were the one remaining unguarded unit.
The crash is reported in Sentry as MUDLET-32 / MUDLET-2S / MUDLET-48.
**Test case:**
`test/functional_tests/ActionSelfUninstallTest.cpp` (Qt Test, built with
AddressSanitizer) builds a synthetic package whose toolbar button
uninstalls its own package, gives it a real toolbar widget, invokes the
button, and asserts no crash, the package removed, and the deferred
action freed cleanly with no double free. It trips a heap-use-after-free
in `TAction::execute()` on the pre-fix code and is ASan-clean with the
fix.
To reproduce by hand:
1. Create a package containing one button whose script is
`uninstallPackage("<that package's name>")`.
2. Click the button.
3. Before this PR: Mudlet crashes. After: the package uninstalls cleanly
and focus returns to the command line.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-30 12:02:32 +02:00
|
|
|
const QString mpPort = "4009";
|
|
|
|
|
const QString mpLocalhost = "localhost";
|
|
|
|
|
|
|
|
|
|
// Builds a package the way an installed package with a toolbar button is laid
|
|
|
|
|
// out: a master-folder root carrying the package name, a toolbar under it (the
|
|
|
|
|
// TEasyButtonBar, on the top bar via mLocation 0), and the actual button under
|
|
|
|
|
// that. updateAllToolbars() then gives the button a real TFlipButton, letting a
|
|
|
|
|
// test drive the genuine click dispatch path. registerAction() assigns the ids,
|
|
|
|
|
// so it must run before setScript() (which bakes the id into the Lua funcname).
|
|
|
|
|
// Returns the leaf button whose script uninstalls the package.
|
|
|
|
|
TAction* buildSelfUninstallingPackage(Host* host, const QString& packageName)
|
|
|
|
|
{
|
|
|
|
|
auto* actionUnit = host->getActionUnit();
|
|
|
|
|
|
|
|
|
|
auto* master = new TAction(packageName, host);
|
|
|
|
|
master->mPackageName = packageName;
|
|
|
|
|
master->mModuleMasterFolder = true;
|
|
|
|
|
master->setIsFolder(true);
|
|
|
|
|
master->setIsActive(true);
|
|
|
|
|
actionUnit->registerAction(master);
|
|
|
|
|
|
|
|
|
|
auto* toolbar = new TAction(master, host);
|
|
|
|
|
toolbar->setName(qsl("selfUninstallToolbar"));
|
|
|
|
|
toolbar->mLocation = 0;
|
|
|
|
|
toolbar->setIsActive(true);
|
|
|
|
|
actionUnit->registerAction(toolbar);
|
|
|
|
|
|
|
|
|
|
auto* button = new TAction(toolbar, host);
|
|
|
|
|
button->setName(qsl("selfUninstallButton"));
|
|
|
|
|
button->setIsActive(true);
|
|
|
|
|
actionUnit->registerAction(button);
|
|
|
|
|
button->setScript(qsl("uninstallPackage([[%1]])").arg(packageName));
|
|
|
|
|
|
|
|
|
|
host->mInstalledPackages << packageName;
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TFlipButton* findButtonWidget(Host* host, const TAction* action)
|
|
|
|
|
{
|
|
|
|
|
// TFlipButton has no Q_OBJECT, so findChildren<> it as its QPushButton base
|
|
|
|
|
// and downcast.
|
|
|
|
|
for (auto* pushButton : host->mpConsole->findChildren<QPushButton*>()) {
|
|
|
|
|
auto* pB = dynamic_cast<TFlipButton*>(pushButton);
|
|
|
|
|
if (pB && pB->mpTAction == action) {
|
|
|
|
|
return pB;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private slots:
|
|
|
|
|
void initTestCase() { initializeQRCResources(); }
|
|
|
|
|
|
|
|
|
|
void init()
|
|
|
|
|
{
|
|
|
|
|
mpServer = new TelnetServerStub(qApp);
|
|
|
|
|
mpServer->start(mpLocalhost, mpPort.toUShort());
|
|
|
|
|
mudlet::start();
|
|
|
|
|
mudlet::self()->setupConfig();
|
|
|
|
|
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
|
|
|
|
|
mudlet::self()->init();
|
|
|
|
|
mudlet::self()->setStorePasswordsSecurely(false);
|
|
|
|
|
deleteProfileDirectory(mpHostname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A button whose script uninstalls its own package must not crash, must finish
|
|
|
|
|
// removing the package, and its deferred TActions must be freed cleanly by
|
|
|
|
|
// doCleanup() afterwards. Invokes the TAction::execute() path directly (the
|
|
|
|
|
// entry the review brief asks for) and, because the package is given a live
|
|
|
|
|
// toolbar first, also drives uninstallPackage()'s internal updateAllToolbars()
|
|
|
|
|
// over the half-uninstalled (deactivated but still-linked) package.
|
|
|
|
|
void test_selfUninstallingButtonDoesNotCrash()
|
|
|
|
|
{
|
|
|
|
|
startProfile(mpHostname, mpLocalhost, mpPort);
|
|
|
|
|
auto* host = mudlet::self()->getActiveHost();
|
|
|
|
|
QVERIFY2(host, "No active host available for the test.");
|
|
|
|
|
host->mEchoLuaErrors = true;
|
|
|
|
|
auto* actionUnit = host->getActionUnit();
|
|
|
|
|
|
|
|
|
|
const QString packageName = qsl("TestActionUninstallPkg");
|
|
|
|
|
auto* button = buildSelfUninstallingPackage(host, packageName);
|
|
|
|
|
const int buttonId = button->getID();
|
|
|
|
|
|
|
|
|
|
// Give the package a real TFlipButton/TEasyButtonBar so the uninstall runs
|
|
|
|
|
// over a live toolbar, not an empty one.
|
|
|
|
|
actionUnit->updateAllToolbars();
|
|
|
|
|
QVERIFY2(findButtonWidget(host, button), "The button should have a real toolbar widget before the test");
|
|
|
|
|
|
|
|
|
|
// The crash: pre-fix this frees `button` mid-call, then execute() reads
|
|
|
|
|
// this->mpHost. If the guard works we return here with everything intact.
|
|
|
|
|
button->execute();
|
|
|
|
|
|
|
|
|
|
QCOMPARE(actionUnit->processingDepth(), 0);
|
|
|
|
|
QVERIFY2(!host->mInstalledPackages.contains(packageName), "The package should have been uninstalled by the button's script");
|
|
|
|
|
QVERIFY2(!bufferContains(qsl("Lua error")), "The button's uninstallPackage() script must not have errored");
|
|
|
|
|
// Still alive but deferred - deletion was postponed until execute() unwound.
|
|
|
|
|
QVERIFY2(actionUnit->getAction(buttonId), "The self-uninstalling button must not be freed while execute() is on the stack");
|
|
|
|
|
|
|
|
|
|
// Flushing the deferred deletes (as the dispatchers and Host's per-line
|
|
|
|
|
// cleanup do) must actually remove them, with no double free.
|
|
|
|
|
actionUnit->doCleanup();
|
|
|
|
|
QVERIFY2(!actionUnit->getAction(buttonId), "The button should be gone after doCleanup()");
|
|
|
|
|
QVERIFY2(actionUnit->findItems(qsl("selfUninstallButton")).empty(), "No trace of the button should remain after cleanup");
|
|
|
|
|
|
|
|
|
|
// uninstallPackage() defers its profile save to the next event-loop cycle
|
|
|
|
|
// (QTimer::singleShot, see Host::uninstallPackage()) and that save runs its
|
|
|
|
|
// XML serialization on a background thread. Fire the deferred timer, then
|
|
|
|
|
// block until the save has fully finished, so no background save thread is
|
|
|
|
|
// still running when cleanup() destroys the host: tearing the host down
|
|
|
|
|
// underneath an in-flight save corrupted the heap and crashed on Windows.
|
|
|
|
|
QTest::qWait(50);
|
|
|
|
|
host->waitForProfileSave();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void cleanup()
|
|
|
|
|
{
|
|
|
|
|
// Defence for the failure path: if an assertion above aborted the test
|
|
|
|
|
// before its own drain ran, a profile save uninstallPackage() deferred
|
|
|
|
|
// could still be in flight. Let it finish before deleting the host, so
|
|
|
|
|
// destruction never races a background save thread (a Windows crash).
|
|
|
|
|
if (auto* self = mudlet::self()) {
|
|
|
|
|
if (auto* host = self->getActiveHost()) {
|
|
|
|
|
QTest::qWait(50);
|
|
|
|
|
host->waitForProfileSave();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
delete mpServer;
|
|
|
|
|
mpServer = nullptr;
|
|
|
|
|
deleteProfileDirectory(mpHostname);
|
|
|
|
|
delete mudlet::self();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Starts a profile the way a user would via the GUI (mirrors the helper in
|
|
|
|
|
// TFeedTriggersRecursionTest).
|
|
|
|
|
void startProfile(const QString& hostname, const QString& address, const QString& port)
|
|
|
|
|
{
|
|
|
|
|
QTimer::singleShot(0, qApp, [hostname, address, port]() {
|
|
|
|
|
mudlet::self()->startAutoLogin({});
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button, Qt::LeftButton);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClicks(QApplication::focusWidget(), hostname);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClicks(QApplication::focusWidget(), address);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClicks(QApplication::focusWidget(), port);
|
|
|
|
|
QTest::qWait(100);
|
|
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
|
|
|
|
|
if (!spy.wait(5000)) {
|
|
|
|
|
QFAIL("Profile took too long to load.");
|
|
|
|
|
}
|
|
|
|
|
auto host = mudlet::self()->getActiveHost();
|
|
|
|
|
if (!host) {
|
|
|
|
|
QFAIL("No active host available for the test.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSignalSpy spy2(&(host->mTelnet), &cTelnet::signal_connected);
|
|
|
|
|
if (!spy2.wait(2000)) {
|
|
|
|
|
QFAIL("Could not connect with the host.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString joinedBuffer()
|
|
|
|
|
{
|
|
|
|
|
auto console = mudlet::self()->getActiveHost()->mpConsole;
|
|
|
|
|
QString allText;
|
|
|
|
|
for (int i = 0; i <= console->buffer.getLastLineNumber(); ++i) {
|
|
|
|
|
allText.append(console->buffer.line(i)).append(QChar::Space);
|
|
|
|
|
}
|
|
|
|
|
return allText.simplified();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool bufferContains(const QString& needle) { return joinedBuffer().contains(needle); }
|
|
|
|
|
|
|
|
|
|
void deleteProfileDirectory(const QString& profileName)
|
|
|
|
|
{
|
|
|
|
|
const QString path = mudlet::getMudletPath(enums::profileHomePath, profileName);
|
|
|
|
|
QDir dir(path);
|
|
|
|
|
if (!dir.exists()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
dir.removeRecursively();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void initializeQRCResources()
|
|
|
|
|
{
|
|
|
|
|
#ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
|
|
|
|
|
qInitResources_additional_splash_screens();
|
|
|
|
|
#endif
|
|
|
|
|
#ifdef INCLUDE_FONTS
|
|
|
|
|
qInitResources_mudlet_fonts_common();
|
|
|
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
|
|
|
|
qInitResources_mudlet_fonts_posix();
|
|
|
|
|
#endif
|
|
|
|
|
#endif
|
|
|
|
|
qInitResources_mudlet();
|
|
|
|
|
qInitResources_qm();
|
|
|
|
|
}
|
|
|
|
|
|
infrastructure: rename the six tests Windows refuses to launch without elevation (#9753)
#### Brief overview of PR changes/additions
- Renames the six test executables whose filenames trip Windows' UAC
installer detection: `UpdaterChecksumTest` to
`ReleaseChecksumPairingTest`, `UpdaterPlatformAssetTest` to
`ReleasePlatformAssetTest`, `UpdaterTeardownTest` to
`NewReleaseDialogTeardownTest`, `PackageSelfUninstallTest` to
`PackageSelfRemovalTest`, `PackageUninstallSaveTeardownTest` to
`PackageRemovalSaveTeardownTest`, `ActionSelfUninstallTest` to
`ActionSelfRemovalTest`. Assertions are untouched.
- Adds a configure-time gate in `test/CMakeLists.txt` that fails with an
actionable message if any test executable name contains install, setup,
update or patch. It checks both the targets a configuration builds and
the test source filenames, so conditionally registered tests cannot slip
past it.
- Documents the naming rule in `test/README.md`.
#### Motivation for adding to Mudlet
Windows treats an unsigned executable named that way as an installer and
refuses to start it, so those six tests reported `BAD_COMMAND` for
anyone running the suite from an ordinary Windows shell - and because CI
runners are elevated, nothing caught it as more tests were added.
#### Other info (issues closed, discussion etc)
Fixes #9748
The gate was verified to fire on a target named after the guard
statement, on one in a subdirectory, on `EventDispatcherTest` (the
message names the offending substring, since "dispatch" contains
"patch"), on a test registered only under `USE_UPDATER` when configuring
with the updater off, and to fail loudly if the walk ever stops finding
executables.
**Test case:** `cmake --build build && ctest --test-dir build` - 92/92
pass; adding a test named e.g. `FooUpdateTest` fails the configure with
an explanation.
Assisted-by: Claude:claude-opus-5
2026-08-11 08:07:10 +02:00
|
|
|
#include "ActionSelfRemovalTest.moc"
|
|
|
|
|
QTEST_MAIN(ActionSelfRemovalTest)
|