mudlet/test/functional_tests/NewReleaseDialogTeardownTest.cpp
Vadim Peretokin 669c586f62
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

122 lines
5.5 KiB
C++

/***************************************************************************
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@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 "updater.h"
#include "updater/UpdateDialog.h"
#include "utils.h"
#include <QtTest/QtTest>
#include <QApplication>
#include <QPointer>
#include <QSettings>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTimer>
#include <QWidget>
#include <memory>
/*
* Regression test for https://github.com/Mudlet/Mudlet/issues/9122:
* STATUS_HEAP_CORRUPTION at application close on Windows.
*
* The Updater is parented to the application object (#9388) and used to delete
* its unparented top-level UpdateDialog in ~Updater. That destructor only runs
* inside the application's own destructor - after ~QApplication has torn down
* all widget infrastructure - and deleting a QWidget that late corrupts the
* heap. The dialog must instead be destroyed on aboutToQuit, while the
* application is still fully alive.
*
* The timing is pinned from both sides: the dialog must still be alive when
* the last window closes (its purpose is to offer an update at exactly that
* point, #9388), and must be gone once the event loop has exited. Note that
* with no update to offer the dialog's own last-window-closed handler calls
* quit(), which in Qt 6 emits aboutToQuit synchronously - so the dialog is
* destroyed inside that cascade, before close() even returns.
*
* QTEST_APPLESS_MAIN is used because the test itself must own the
* QApplication lifetime to walk it through quit and destruction.
*/
class NewReleaseDialogTeardownTest : public QObject
{
Q_OBJECT
private slots:
void updateDialogDestroyedBeforeApplicationTeardown();
};
void NewReleaseDialogTeardownTest::updateDialogDestroyedBeforeApplicationTeardown()
{
// Keeps checkUpdatesOnStart() away from real user data - on Windows it
// deletes stale installer files from the genuine GenericDataLocation
QStandardPaths::setTestModeEnabled(true);
QTemporaryDir settingsDir;
QVERIFY(settingsDir.isValid());
QSettings settings(settingsDir.filePath(qsl("updater-test.ini")), QSettings::IniFormat);
int argc = 1;
char appName[] = "NewReleaseDialogTeardownTest";
char* argv[] = {appName, nullptr};
const auto app = std::make_unique<QApplication>(argc, argv);
auto* updater = new Updater(app.get(), &settings);
// Connected before checkUpdatesOnStart() creates the dialog, so with
// direct connections firing in connection order this probe runs before
// the dialog's own last-window-closed handler - the last moment the
// dialog is guaranteed to exist, as that handler quits when there is no
// update to offer and the quit destroys the dialog
QPointer<dblsqd::UpdateDialog> dialog;
bool dialogAliveAtLastWindowClosed = false;
connect(app.get(), &QGuiApplication::lastWindowClosed, this, [&dialog, &dialogAliveAtLastWindowClosed]() {
dialogAliveAtLastWindowClosed = !dialog.isNull();
});
// Also fires the feed's update check; the request is torn down with the
// application before any response arrives and nothing below depends on it
updater->checkUpdatesOnStart();
const auto topLevels = QApplication::topLevelWidgets();
for (auto* widget : topLevels) {
if ((dialog = qobject_cast<dblsqd::UpdateDialog*>(widget))) {
break;
}
}
QVERIFY2(dialog, "expected the Updater to have created its UpdateDialog");
auto* window = new QWidget;
window->show();
QTimer::singleShot(0, app.get(), [&window]() {
window->close();
delete window;
// The dialog's own last-window-closed handler quits when no update is
// available; quit explicitly so the test cannot hang if an update is
// available (the dialog then shows itself and waits for the user)
QCoreApplication::quit();
});
app->exec();
QVERIFY2(dialogAliveAtLastWindowClosed, "the UpdateDialog must still be alive when the last window closes so it can offer an update at that point - see #9388");
QVERIFY2(dialog.isNull(), "UpdateDialog must be destroyed when the application quits: deleting it any later (from ~Updater, inside the application's destructor) corrupts the heap - see #9122");
}
QTEST_APPLESS_MAIN(NewReleaseDialogTeardownTest)
#include "NewReleaseDialogTeardownTest.moc"