mudlet/test/functional_tests/ResetProfileTest.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

932 lines
33 KiB
C++
Raw Permalink Normal View History

/***************************************************************************
* Copyright (C) 2026 by Mudlet Developers *
* *
* 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. *
***************************************************************************/
/*
* Comprehensive tests for Host::resetProfile_phase1() / resetProfile_phase2().
* Covers temp item removal, permanent item survival, stopwatches, events,
* Lua state reinit, sysLoadEvent, flags, UI cleanup, two-phase correctness,
* ANSI colors, map survival, telnet connection, Geyser reload, double reset,
* and script event handler re-registration.
*
* The mudlet instance and profile are created once in initTestCase().
* Each test calls resetProfile() at the end (via cleanup()) to restore
* a clean state for the next test.
*
* Run with: ctest -R ResetProfileTest -V
*/
#include <QtTest/QtTest>
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
#include <chrono>
#include <QMouseEvent>
#include "Host.h"
#include "LuaInterface.h"
Infrastructure: Swap out QtConcurrent module header for sub-module ones (#9246) #### Brief overview of PR changes/additions The Qt documentation for `QtConcurrent` points out: > If you include the `<QtConcurrent>` header, the entire Qt Concurrent module with the entire Qt Core module will be included, which may increase compilation times and binary sizes. To use individual functions from the QtConcurrent namespace, you can include more specific headers. > > The table below lists the functions in the QtConcurrent namespace and their corresponding headers: |Function|Header| |--------|------| |`QtConcurrent::run()`|`<QtConcurrentRun>`| |`QtConcurrent::task()`| `<QtConcurrentTask>`| |`QtConcurrent::filter()`,<br>`QtConcurrent::filtered()`,<br>`QtConcurrent::filteredReduced()`|`<QtConcurrentFilter>`| |`QtConcurrent::map()`,<br>`QtConcurrent::mapped()`,<br>`QtConcurrent::mappedReduced()`|`<QtConcurrentMap>`| #### Motivation for adding to Mudlet To speed up the build a little by removing stuff that isn't needed. #### Other info (issues closed, discussion etc) In doing this I happened to start cleaning up a couple of header files `T2DMap.h` and then `mudlet.h`, I then got into converting some `#include`s into forward declarations in a "include-what-you-use" move. This then rippled through into a (more than 10!) number of files but should "improve" things. Note that the ordering of `#include` in many files seems to be rather haphazard and is due for some serious overhaul - I suggest that we should actually declare an "official" style for this project so that everyone knows what it is. **During the CI/CB process I discovered that Linux and then MacOS builds were failing because the file referred to by the `#include <QtConcurrentTask>` header file was missing, yet was present on my local PC when I was using the Qt framework from the On-line installer. Initially I suspected a Debian (and then Devuan - as the packaged version on my own machine also had this defect AND Ubuntu) package problem; however it now seems to be an upstream Qt issue as the various Qt versions & OS combinations suggest that Qt themselves fixed it for Qt 6.10:** | OS | QtVersion | Missing header | |--------|-----------------------|----------------| | Windows| 6.11.0 package | No | | Devuan | 6.8.2 package | Yes | | Devuan | 6.10.0 online install | No | | Ubuntu | 6.9.0 package | Yes | | Debian | 6.8.2 package | Yes | | MacOS | 6.9.0 package | Yes | **To fix this I reverted to an `#include <qtconcurrenttask.h>` for Linux and MacOS builds - although it would probably have been better to make it conditional on the Qt Version instead...** *I have reported this upstream to Debian - see: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135197* --------- Signed-off-by: Stephen Lyons <slysven@virginmedia.com>
2026-04-29 13:35:29 +01:00
#include "MudletInstanceCoordinator.h"
#include "TEvent.h"
#include "TKey.h"
#include "TLabel.h"
#include "TLuaInterpreter.h"
#include "TMainConsole.h"
#include "TMap.h"
#include "TRoomDB.h"
#include "TScript.h"
#include "TTimer.h"
#include "TTrigger.h"
#include "TVar.h"
#include "TelnetServerStub.h"
#include "VarUnit.h"
#include "XMLexport.h"
#include "ctelnet.h"
#include "dlgConnectionProfiles.h"
#include "mudlet.h"
extern "C" {
#if defined(INCLUDE_VERSIONED_LUA_HEADERS)
#include <lua5.1/lauxlib.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#else
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#endif
}
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
using namespace std::chrono_literals;
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 initializeQRCResourcesForResetProfileTest();
class ResetProfileTest : public QObject {
Q_OBJECT
private:
TelnetServerStub *mpServer = nullptr;
Host *mpHost = nullptr;
const QString mHostname = "ResetProfile-Test";
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
const QString mLocalhost = "localhost";
void performReset() {
mpHost->resetProfile_phase1();
QCoreApplication::processEvents();
}
int countTempTriggers() {
int count = 0;
for (auto *trigger : mpHost->getTriggerUnit()->getTriggerRootNodeList()) {
if (trigger->isTemporary()) {
++count;
}
}
return count;
}
int countTempKeys() {
int count = 0;
for (auto *key : mpHost->getKeyUnit()->getKeyRootNodeList()) {
if (key->isTemporary()) {
++count;
}
}
return count;
}
private slots:
void initTestCase() {
initializeQRCResourcesForResetProfileTest();
mpServer = new TelnetServerStub(qApp);
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
mPort = QString::number(mpServer->serverPort());
mudlet::start();
mudlet::self()->setupConfig();
mudlet::self()->takeOwnershipOfInstanceCoordinator(
std::make_unique<MudletInstanceCoordinator>(
"MudletInstanceCoordinator"));
mudlet::self()->init();
mudlet::self()->setStorePasswordsSecurely(false);
deleteProfileDirectory(mHostname);
startProfile(mHostname, mLocalhost, mPort);
mpHost = mudlet::self()->getActiveHost();
QVERIFY2(mpHost, "No active host after profile creation");
}
void cleanupTestCase() {
mpHost = nullptr;
delete mpServer;
mpServer = nullptr;
deleteProfileDirectory(mHostname);
delete mudlet::self();
}
// Per-test cleanup: reset the profile so each test starts from clean state.
// Tests that exercise resetProfile() themselves may leave the profile already
// reset, which is fine — a second reset is harmless.
void cleanup() {
if (mpHost && !mpHost->mResetProfile) {
performReset();
} else if (mpHost && mpHost->mResetProfile) {
QCoreApplication::processEvents();
}
}
// -----------------------------------------------------------------------
// Group 1: Temporary Item Removal (#761)
// -----------------------------------------------------------------------
void test_tempTriggersRemovedAfterReset() {
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
// preinstalled packages (the starter UI) register their own temp triggers
// and re-register them when the profile comes back, so the count after a
// reset returns to that baseline rather than zero
const int baseline = countTempTriggers();
int id = mpHost->mLuaInterpreter.startTempTrigger(qsl("test_pattern"),
qsl(""), -1);
QVERIFY(id > 0);
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
QVERIFY(countTempTriggers() > baseline);
performReset();
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
QCOMPARE(countTempTriggers(), baseline);
}
void test_tempAliasesRemovedAfterReset() {
int id =
mpHost->mLuaInterpreter.startTempAlias(qsl("^test_alias$"), qsl(""));
QVERIFY(id > 0);
QVERIFY(mpHost->getAliasUnit()->getAlias(id) != nullptr);
performReset();
// Our specific temp alias should be gone (Lua scripts may create their own
// via sysLoadEvent)
QVERIFY2(mpHost->getAliasUnit()->getAlias(id) == nullptr,
"Temp alias should be removed after reset");
}
void test_tempTimersRemovedAfterReset() {
auto result = mpHost->mLuaInterpreter.startTempTimer(60.0, qsl(""));
int id = result.first;
QVERIFY(id > 0);
QVERIFY(mpHost->getTimerUnit()->getTimer(id) != nullptr);
performReset();
QVERIFY2(mpHost->getTimerUnit()->getTimer(id) == nullptr,
"Temp timer should be removed after reset");
}
void test_tempKeysRemovedAfterReset() {
int modifier = Qt::NoModifier;
int keycode = Qt::Key_F12;
int id = mpHost->mLuaInterpreter.startTempKey(modifier, keycode, qsl(""));
QVERIFY(id > 0);
QVERIFY(countTempKeys() > 0);
performReset();
QCOMPARE(countTempKeys(), 0);
}
// -----------------------------------------------------------------------
// Group 2: Permanent Item Survival
// -----------------------------------------------------------------------
void test_permanentTriggerSurvivesReset() {
QStringList patterns;
patterns << qsl("perm_test_pattern");
auto [id, msg] = mpHost->mLuaInterpreter.startPermSubstringTrigger(
qsl("perm_trigger"), qsl(""), patterns, qsl(""));
QVERIFY2(id > 0, qPrintable(msg));
performReset();
bool found = false;
for (auto *trigger : mpHost->getTriggerUnit()->getTriggerRootNodeList()) {
if (trigger->getID() == id) {
found = true;
QVERIFY(trigger->isActive());
break;
}
}
QVERIFY2(found, "Permanent trigger not found after reset");
}
void test_permanentAliasSurvivesReset() {
auto [id, msg] = mpHost->mLuaInterpreter.startPermAlias(
qsl("perm_alias"), qsl(""), qsl("^perm_test$"), qsl(""));
QVERIFY2(id > 0, qPrintable(msg));
performReset();
bool found = false;
for (auto *alias : mpHost->getAliasUnit()->getAliasRootNodeList()) {
if (alias->getID() == id) {
found = true;
QVERIFY(alias->isActive());
break;
}
}
QVERIFY2(found, "Permanent alias not found after reset");
}
void test_permanentTimerSurvivesReset() {
auto [id, msg] = mpHost->mLuaInterpreter.startPermTimer(
qsl("perm_timer"), qsl(""), 300.0, qsl(""));
QVERIFY2(id > 0, qPrintable(msg));
performReset();
bool found = false;
for (auto *timer : mpHost->getTimerUnit()->getTimerRootNodeList()) {
if (timer->getID() == id) {
found = true;
break;
}
}
QVERIFY2(found, "Permanent timer not found after reset");
}
// -----------------------------------------------------------------------
// Group 3: Stopwatch Handling (#4715)
// -----------------------------------------------------------------------
void test_nonPersistentStopWatchRemovedAfterReset() {
mpHost->mBlockStopWatchCreation = false;
auto result = mpHost->createStopWatch(qsl("test_sw"));
int id = result.first;
QVERIFY2(id > 0, qPrintable(result.second));
QVERIFY(mpHost->getStopWatch(id) != nullptr);
performReset();
QVERIFY2(mpHost->getStopWatch(id) == nullptr,
"Non-persistent stopwatch should be removed after reset");
}
void test_persistentStopWatchSurvivesReset() {
mpHost->mBlockStopWatchCreation = false;
auto result = mpHost->createStopWatch(qsl("persistent_sw"));
int id = result.first;
QVERIFY2(id > 0, qPrintable(result.second));
mpHost->makeStopWatchPersistent(id, true);
performReset();
auto *sw = mpHost->getStopWatch(id);
QVERIFY2(sw != nullptr, "Persistent stopwatch should survive reset");
QVERIFY(sw->persistent());
}
void test_stopWatchCreationBlockedDuringReset() {
mpHost->mBlockStopWatchCreation = false;
mpHost->resetProfile_phase1();
// mResetProfile is now true — stopwatch creation should be blocked
auto result = mpHost->createStopWatch(qsl("blocked_sw"));
QCOMPARE(result.first, 0);
QCoreApplication::processEvents();
// After phase2, creation should work again
mpHost->mBlockStopWatchCreation = false;
auto result2 = mpHost->createStopWatch(qsl("unblocked_sw"));
QVERIFY2(result2.first > 0,
"Stopwatch creation should work after reset completes");
}
// -----------------------------------------------------------------------
// Group 4: Event System Cleanup (#4970, #7698)
// -----------------------------------------------------------------------
void test_eventHandlerMapClearedAfterReset() {
mpHost->mEventHandlerMap[qsl("testEvent")] = QList<TScript *>();
QVERIFY(!mpHost->mEventHandlerMap.isEmpty());
performReset();
// mEventHandlerMap is cleared, then repopulated by compileAll
// re-registering script handlers. Our manually-inserted "testEvent" entry
// should be gone.
QVERIFY2(!mpHost->mEventHandlerMap.contains(qsl("testEvent")),
"Manually inserted event handler should be cleared after reset");
}
void test_anonymousEventHandlersClearedAfterReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(
L, "function anonHandlerTestFunc() anonHandlerCalled = true end");
mpHost->registerAnonymousEventHandler(qsl("testAnonymousEvent"),
qsl("anonHandlerTestFunc"));
performReset();
// The Lua function no longer exists in the new state
lua_State *newL = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(newL, "anonHandlerTestFunc");
QVERIFY2(lua_isnil(newL, -1), "Lua function referenced by anonymous "
"handler should not exist in new Lua state");
lua_pop(newL, 1);
// Raise the event - if the anonymous handler map was properly cleared,
// this won't attempt to call the now-nonexistent function
TEvent event{};
event.mArgumentList.append(qsl("testAnonymousEvent"));
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(event);
}
// -----------------------------------------------------------------------
// Group 5: Lua State Reinitialization (#3692)
// -----------------------------------------------------------------------
void test_luaCustomGlobalsClearedAfterReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(L);
luaL_dostring(L, "resetProfileTestGlobal = 42");
lua_getglobal(L, "resetProfileTestGlobal");
QVERIFY(!lua_isnil(L, -1));
lua_pop(L, 1);
performReset();
lua_State *newL = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(newL, "resetProfileTestGlobal");
QVERIFY2(lua_isnil(newL, -1),
"Custom Lua global should be gone after reset");
lua_pop(newL, 1);
}
void test_luaStandardFunctionsAvailableAfterReset() {
performReset();
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(L);
lua_getglobal(L, "assert");
QVERIFY2(!lua_isnil(L, -1), "assert should be available after reset");
lua_pop(L, 1);
lua_getglobal(L, "pcall");
QVERIFY2(!lua_isnil(L, -1), "pcall should be available after reset");
lua_pop(L, 1);
lua_getglobal(L, "string");
QVERIFY2(!lua_isnil(L, -1),
"string library should be available after reset");
lua_pop(L, 1);
lua_getglobal(L, "table");
QVERIFY2(!lua_isnil(L, -1),
"table library should be available after reset");
lua_pop(L, 1);
}
void test_luaStateIsNewAfterReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(L);
luaL_dostring(L, "resetTestSentinel = 42");
performReset();
lua_State *newL = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(newL);
lua_getglobal(newL, "resetTestSentinel");
QVERIFY2(lua_isnil(newL, -1), "Lua state should be fresh after reset");
lua_pop(newL, 1);
}
// -----------------------------------------------------------------------
// Group 6: sysLoadEvent (#4779, #5005)
// -----------------------------------------------------------------------
void test_sysLoadEventFiredExactlyOnce() {
auto *pScript = new TScript(qsl("sysLoadEventCounter"), mpHost);
pScript->setScript(qsl("sysLoadCounter = (sysLoadCounter or 0) + 1"));
QStringList events;
events << qsl("sysLoadEvent");
pScript->setEventHandlerList(events);
mpHost->getScriptUnit()->registerScript(pScript);
pScript->setIsActive(true);
pScript->compile();
performReset();
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(L, "sysLoadCounter");
QVERIFY2(!lua_isnil(L, -1),
"sysLoadCounter should be set by the event handler");
int counter = lua_tointeger(L, -1);
lua_pop(L, 1);
QCOMPARE(counter, 1);
}
// -----------------------------------------------------------------------
// Group 7: Flag Lifecycle
// -----------------------------------------------------------------------
void test_mResetProfileFlagLifecycle() {
QVERIFY2(!mpHost->mResetProfile,
"mResetProfile should be false before reset");
mpHost->resetProfile_phase1();
QVERIFY2(mpHost->mResetProfile,
"mResetProfile should be true after phase1");
QCoreApplication::processEvents();
QVERIFY2(!mpHost->mResetProfile,
"mResetProfile should be false after phase2");
}
void test_mBlockScriptCompileFalseAfterReset() {
mpHost->mBlockScriptCompile = true;
performReset();
QVERIFY2(!mpHost->mBlockScriptCompile,
"mBlockScriptCompile should be false after reset");
}
void test_mEmergencyStopNotSetDuringReset() {
QVERIFY2(!mpHost->mEmergencyStop,
"mEmergencyStop should be false before reset");
mpHost->resetProfile_phase1();
QVERIFY2(!mpHost->mEmergencyStop,
"mEmergencyStop should stay false during phase1 (phase1 calls "
"unit-level stop, not Host::stopAllTriggers)");
QCoreApplication::processEvents();
QVERIFY2(!mpHost->mEmergencyStop,
"mEmergencyStop should stay false after phase2");
}
// -----------------------------------------------------------------------
// Group 8: UI Cleanup
// -----------------------------------------------------------------------
void test_miniConsolesRemovedAfterReset() {
auto [ok, msg] =
mpHost->createMiniConsole(qsl("main"), qsl("test_mc"), 0, 0, 100, 100);
QVERIFY2(ok, qPrintable(msg));
QVERIFY(mpHost->mpConsole->mSubConsoleMap.contains(qsl("test_mc")));
performReset();
QVERIFY2(!mpHost->mpConsole->mSubConsoleMap.contains(qsl("test_mc")),
"Mini console should be removed after reset");
}
void test_labelsRemovedAfterReset() {
auto [ok, msg] = mpHost->createLabel(qsl("main"), qsl("test_label"), 0, 0,
100, 100, true, false);
QVERIFY2(ok, qPrintable(msg));
QVERIFY(mpHost->mpConsole->mLabelMap.contains(qsl("test_label")));
performReset();
QVERIFY2(!mpHost->mpConsole->mLabelMap.contains(qsl("test_label")),
"Label should be removed after reset");
}
// -----------------------------------------------------------------------
// Group 9: Two-Phase Correctness
// -----------------------------------------------------------------------
void test_phase2RunsDeferredNotImmediate() {
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
const int baseline = countTempTriggers();
int triggerId = mpHost->mLuaInterpreter.startTempTrigger(
qsl("deferred_test"), qsl(""), -1);
QVERIFY(triggerId > 0);
mpHost->resetProfile_phase1();
// Phase2 has NOT run yet
QVERIFY2(mpHost->mResetProfile,
"mResetProfile should be true before processEvents");
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
QVERIFY2(countTempTriggers() > baseline,
"Temp triggers should still exist before processEvents");
QCoreApplication::processEvents();
QVERIFY2(!mpHost->mResetProfile,
"mResetProfile should be false after processEvents");
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
QCOMPARE(countTempTriggers(), baseline);
}
// -----------------------------------------------------------------------
// Group 10: ANSI Color Table
// -----------------------------------------------------------------------
void test_ansiColorTableUpdatedAfterReset() {
mpHost->mBlack = QColor(10, 20, 30);
performReset();
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(L);
lua_getglobal(L, "color_table");
QVERIFY2(!lua_isnil(L, -1), "color_table should exist after reset");
lua_getfield(L, -1, "ansi_000");
QVERIFY2(!lua_isnil(L, -1), "color_table.ansi_000 should exist");
lua_rawgeti(L, -1, 1);
int r = lua_tointeger(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 2);
int g = lua_tointeger(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 3);
int b = lua_tointeger(L, -1);
lua_pop(L, 1);
lua_pop(L, 2);
QCOMPARE(r, 10);
QCOMPARE(g, 20);
QCOMPARE(b, 30);
}
// -----------------------------------------------------------------------
// Group 11: Map System Survival
// -----------------------------------------------------------------------
void test_mapDataSurvivesReset() {
QVERIFY(mpHost->mpMap);
int roomId = mpHost->mpMap->createNewRoomID();
QVERIFY(roomId > 0);
QVERIFY(mpHost->mpMap->addRoom(roomId));
performReset();
QVERIFY2(mpHost->mpMap, "TMap pointer should survive reset");
auto *room = mpHost->mpMap->mpRoomDB->getRoom(roomId);
QVERIFY2(room != nullptr, "Room added before reset should still exist");
}
// -----------------------------------------------------------------------
// Group 12: Telnet Connection Survives
// -----------------------------------------------------------------------
void test_telnetConnectionSurvivesReset() {
auto stateBefore = mpHost->mTelnet.getConnectionState();
QCOMPARE(stateBefore, QAbstractSocket::ConnectedState);
performReset();
auto stateAfter = mpHost->mTelnet.getConnectionState();
QCOMPARE(stateAfter, QAbstractSocket::ConnectedState);
}
// -----------------------------------------------------------------------
// Group 13: Geyser/LuaGlobal Framework Reload
// -----------------------------------------------------------------------
void test_geyserFrameworkAvailableAfterReset() {
performReset();
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(L);
lua_getglobal(L, "Geyser");
QVERIFY2(!lua_isnil(L, -1),
"Geyser framework should be loaded after reset");
lua_pop(L, 1);
}
void test_gmcpTableFreshAfterReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(L, "gmcp.testField = 'stale_data'");
performReset();
lua_State *newL = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(newL, "gmcp");
QVERIFY2(!lua_isnil(newL, -1), "gmcp table should exist after reset");
lua_getfield(newL, -1, "testField");
QVERIFY2(lua_isnil(newL, -1),
"gmcp.testField should be nil after reset (fresh table)");
lua_pop(newL, 2);
}
// -----------------------------------------------------------------------
// Group 14: Double Reset Safety
// -----------------------------------------------------------------------
void test_doubleResetIsGuarded() {
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
const int baseline = countTempTriggers();
mpHost->mLuaInterpreter.startTempTrigger(qsl("double_reset_test"), qsl(""),
-1);
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(L, "doubleResetSentinel = 99");
// Call phase1 twice - the second call should be a no-op
QVERIFY2(mpHost->resetProfile_phase1(),
"First resetProfile_phase1() should succeed");
QVERIFY2(!mpHost->resetProfile_phase1(),
"Second resetProfile_phase1() should be guarded");
QCoreApplication::processEvents();
// Verify reset actually happened by checking the Lua state is fresh
lua_State *afterL = mpHost->mLuaInterpreter.getLuaGlobalState();
QVERIFY(afterL);
lua_getglobal(afterL, "doubleResetSentinel");
QVERIFY2(lua_isnil(afterL, -1), "Lua state should be fresh after reset");
lua_pop(afterL, 1);
QVERIFY2(!mpHost->mResetProfile,
"mResetProfile should be false after reset");
add: starter interface with health bars, map and chat for new players (#9454) #### Brief overview of PR changes/additions - New "Mudlet base UI" package auto-installed for new players on every game: adjustable right-side dock with map (placeholder until the game is mapped), tabbed chat (All/Tells/Channels with unread badges) and themed HP/MP/MV/XP gauges - only ever built from data the game actually provides - Chat capture: GMCP Comm.Channel.Text when available, otherwise a generic additive trigger layer (line shapes distilled from an audit of all 216 packages in the Mudlet package repository); nothing is ever gagged, and the trigger layer retires itself the moment GMCP chat appears - Vitals ladder for the gauges: GMCP (several dialect spellings) > MSDP (negotiated automatically) > self-sufficient prompts (labeled cur/max or percents) > score-screen harvesting; when a prompt only shows bare currents, Mudlet sends `score` once - visibly, with an announcement - to learn the maxima. Maxima are never guessed. An enemy-health bar appears during fights on games that report it - `baseui hide` / `baseui show` opt-out persists across restarts; first-run announcement defers to the UI tour; experienced players never get the package - Plays nice with games that provide their own interface: profiles for games whose bundled loader installs the game's official UI (flagged in TGameDetails: Carrion Fields, Icesus, MorgenGrauen, Medievia) skip the starter UI entirely, and when a game pushes a `Client.GUI` package the starter UI quietly stands aside (via the new `sysServerGuiInstalled` event) - `baseui show` brings it back for players who prefer it - `docs/package-capture-audits/` records the chat + vitals capture audits (with provenance) behind the design #### Motivation for adding to Mudlet New players currently get a bare text screen; this gives every game - GMCP-rich, MSDP-only or plain telnet - an immediate, honest starter UI out of the box. #### Other info (issues closed, discussion etc) Progresses #3071 and #3070 (phase 1 - not closing them). **Test case:** Fresh install → create a profile for any game → connect. On GMCP/MSDP games the dock builds with ticking gauges and tabbed chat; on a game whose prompt carries cur/max values, gauges appear once the prompt repeats; on a bare-number prompt, `score` is sent once (announced) to size the gauges; on pure chat games, tells/says/channels are captured with no gauges invented. `baseui hide` removes it and survives restart. https://github.com/user-attachments/assets/56d207cc-5bad-4464-867c-1a810d41cf2f --------- Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-26 08:43:21 +02:00
QCOMPARE(countTempTriggers(), baseline);
}
// -----------------------------------------------------------------------
// Group 15: Script Recompilation and Event Handler Re-registration
// -----------------------------------------------------------------------
void test_permanentScriptEventHandlerReregisteredAfterReset() {
const QString eventName = qsl("customResetTestEvent");
auto *pScript = new TScript(qsl("eventHandlerScript"), mpHost);
pScript->setScript(qsl("-- handler for customResetTestEvent"));
QStringList events;
events << eventName;
pScript->setEventHandlerList(events);
mpHost->getScriptUnit()->registerScript(pScript);
pScript->setIsActive(true);
pScript->compile();
QVERIFY2(mpHost->mEventHandlerMap.contains(eventName),
"Event handler should be registered before reset");
performReset();
QVERIFY2(
mpHost->mEventHandlerMap.contains(eventName),
"Event handler should be re-registered after reset via compileAll");
}
// -----------------------------------------------------------------------
// Group 16: Lua Registry Corruption (TLabel destructor timing)
// -----------------------------------------------------------------------
// PR #9141 added a TLabel destructor that frees its 7 callback indices
// via luaL_unref. resetMainConsole() destroys labels via deleteLater(),
// so without an explicit drain those destructors fire AFTER phase2 has
// swapped in a new Lua state via initLuaGlobals(). The luaL_unref calls
// then write into the new state's registry — any slots already populated
// by compileAll() during the same phase2 (e.g. Adjustable.Container's
// many label callbacks) get overwritten with freelist-chain numbers,
// surfacing as "attempt to call a number value" the first time the user
// hovers a label.
//
// The fix in Host::resetProfile_phase2() drains DeferredDelete events
// between resetMainConsole() and initLuaGlobals(), so the old destructors
// run their unrefs against the still-live original state. This test
// reproduces the conditions: a script that creates many labels with
// click callbacks at top level, so compileAll re-executes it during
// phase2 and populates the new registry with the exact slot indices the
// queued old destructors would have corrupted.
void test_labelCallbacksWorkAfterResetWithManyLabels() {
const int kNumLabels = 50;
auto *pScript = new TScript(qsl("labelCallbackRegressionScript"), mpHost);
pScript->setScript(qsl("labelCallbackHits = 0\n"
"for i = 1, %1 do\n"
" local n = 'cb_label_'..i\n"
" createLabel(n, 0, 0, 10, 10, true)\n"
" setLabelClickCallback(n, function()\n"
" labelCallbackHits = labelCallbackHits + 1\n"
" end)\n"
"end\n")
.arg(kNumLabels));
mpHost->getScriptUnit()->registerScript(pScript);
pScript->setIsActive(true);
pScript->compile();
performReset();
// Force any DeferredDelete events still pending after phase2 to fire
// before we trigger callbacks. With the fix, phase2 already drained
// them; without the fix, this is when corruption would land.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
for (int i = 1; i <= kNumLabels; ++i) {
auto *pL = mpHost->mpConsole->mLabelMap.value(qsl("cb_label_%1").arg(i));
QVERIFY2(pL, qPrintable(qsl("post-reset label cb_label_%1 missing").arg(i)));
QMouseEvent ev(QEvent::MouseButtonPress, QPointF(1, 1), QPointF(1, 1),
QPointF(1, 1), Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
QCoreApplication::sendEvent(pL, &ev);
}
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(L, "labelCallbackHits");
QVERIFY2(lua_isnumber(L, -1),
"labelCallbackHits should be a number after firing callbacks");
int hits = lua_tointeger(L, -1);
lua_pop(L, 1);
QCOMPARE(hits, kNumLabels);
// Neutralize the script so it doesn't keep recreating labels in
// subsequent tests' compileAll passes.
pScript->setScript(qsl(""));
pScript->setIsActive(false);
}
// -----------------------------------------------------------------------
// Group 17: VarUnit saved/hidden variable bookkeeping (#9430 follow-up)
// -----------------------------------------------------------------------
// The VarUnit's savedVars/hiddenByUser sets are name-keyed and independent
// of the lua_State (VarUnit::clear() deliberately preserves them), and
// nothing repopulates them after a reset - only XMLimport at profile open
// does. If replacing the LuaInterface in resetProfile_phase2() loses them,
// the first profile save after a reset silently drops every user-saved
// variable and hidden-variable preference from the profile XML.
void test_savedAndHiddenVarSetsSurviveReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(L, "resetSavedTestVar = 'important'");
LuaInterface *lI = mpHost->getLuaInterface();
VarUnit *vu = lI->getVarUnit();
lI->getVars(false);
TVar *var = findGlobalVar(vu, qsl("resetSavedTestVar"));
QVERIFY2(var, "test variable not found in the variable tree");
// as the Variables view does when the user ticks the save checkbox:
vu->addSavedVar(var);
// and as it does when the user hides a variable:
vu->addHidden(qsl("resetHiddenTestVar"));
QVERIFY(vu->savedVars.contains(qsl("resetSavedTestVar")));
QVERIFY(vu->hiddenByUser.contains(qsl("resetHiddenTestVar")));
performReset();
VarUnit *newVu = mpHost->getLuaInterface()->getVarUnit();
QVERIFY2(newVu->savedVars.contains(qsl("resetSavedTestVar")),
"user's saved-variable marking should survive resetProfile()");
QVERIFY2(newVu->hiddenByUser.contains(qsl("resetHiddenTestVar")),
"user's hidden-variable preference should survive resetProfile()");
}
// End-to-end version of the above: the saved variable must still be
// written out to profile XML after a reset.
void test_savedVariableExportedToXmlAfterReset() {
lua_State *L = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(L, "xmlSavedTestVar = 'survives'");
LuaInterface *lI = mpHost->getLuaInterface();
VarUnit *vu = lI->getVarUnit();
lI->getVars(false);
TVar *var = findGlobalVar(vu, qsl("xmlSavedTestVar"));
QVERIFY2(var, "test variable not found in the variable tree");
vu->addSavedVar(var);
performReset();
// the reset wiped the Lua value; user scripts recreate it on
// sysLoadEvent, and opening the Variables view rebuilds the tree
lua_State *newL = mpHost->mLuaInterpreter.getLuaGlobalState();
luaL_dostring(newL, "xmlSavedTestVar = 'survives'");
mpHost->getLuaInterface()->getVars(false);
const QString xmlPath =
mudlet::getMudletPath(enums::profileHomePath, mHostname) +
qsl("/reset-var-test.xml");
auto writer = std::make_shared<XMLexport>(mpHost);
QVERIFY(writer->exportPackage(xmlPath, true, false));
QFile file(xmlPath);
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QString xml = QString::fromUtf8(file.readAll());
file.close();
QFile::remove(xmlPath);
QVERIFY2(xml.contains(qsl("xmlSavedTestVar")),
"saved variable should still be exported to profile XML after "
"a reset");
}
// A fresh profile load runs hideMudletsVariables() right after
// loadGlobal() so the Variables view only shows the user's variables;
// a reset must do the same or the view fills up with Mudlet's entire
// internal Lua API.
void test_mudletInternalVariablesHiddenAfterReset() {
mpHost->hideMudletsVariables();
VarUnit *vu = mpHost->getLuaInterface()->getVarUnit();
QVERIFY(vu->hidden.contains(qsl("color_table")));
QVERIFY(vu->hidden.contains(qsl("Geyser")));
performReset();
VarUnit *newVu = mpHost->getLuaInterface()->getVarUnit();
QVERIFY2(newVu->hidden.contains(qsl("color_table")),
"Mudlet's internal variables should be re-hidden after reset, "
"as on profile load");
QVERIFY2(newVu->hidden.contains(qsl("Geyser")),
"Mudlet's internal variables should be re-hidden after reset, "
"as on profile load");
}
// -----------------------------------------------------------------------
// Helpers (reused from TOscTerminatorTest pattern)
// -----------------------------------------------------------------------
TVar *findGlobalVar(VarUnit *vu, const QString &name) {
for (auto *child : vu->getBase()->getChildren(false)) {
if (child->getName() == name) {
return child;
}
}
return nullptr;
}
void startProfile(const QString &hostname, const QString &address,
const QString &port) {
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTimer::singleShot(0ms, qApp, [hostname, address, port]() {
mudlet::self()->startAutoLogin({});
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button,
Qt::LeftButton);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), hostname);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), address);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), port);
infrastructure: use std::chrono literals for time durations (#9493) #### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
});
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
if (!spy.wait(1000)) {
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(500)) {
QFAIL("Could not connect with the host.");
}
}
void deleteProfileDirectory(const QString &profileName) {
const QString path =
mudlet::getMudletPath(enums::profileHomePath, profileName);
QDir dir(path);
if (!dir.exists()) {
return;
}
dir.removeRecursively();
}
};
void initializeQRCResourcesForResetProfileTest() {
#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();
}
#include "ResetProfileTest.moc"
QTEST_MAIN(ResetProfileTest)