mudlet/test/functional_tests/ProfileRoundTripTest.cpp
Vadim Peretokin 652d30fcf0
Infrastructure: add profile and map save/load round-trip tests (#9468)
#### Brief overview of PR changes/additions

Adds two functional round-trip tests.

- **ProfileRoundTripTest** builds a 31-item tree across all five object
types (with non-ASCII names and XML-hostile content), saves it via the
production `XMLexport`, reimports via `XMLimport`, and compares ~40
fields.
- **MapRoundTripTest** builds a map with rooms, areas, exits, special
exits, custom lines, userData and labels, then round-trips it at every
map format (17-20).

Both tests were proven able to fail via temporary sabotage.

#### Motivation for adding to Mudlet

Profile and map persistence had no automated coverage, so regressions in
save/load could ship unnoticed. These tests lock down the round-trip
behaviour. They immediately caught two real map-persistence bugs.

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

The two map bugs these tests caught are fixed in the stacked follow-up
PR #9469 (`fix-map-userdata-pollution` branch), which flips these tests'
`QEXPECT_FAIL` markers to hard assertions.

Tested by hand. Squash-merge with:
```
Assisted-by: Claude:claude-fable-5
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
```
2026-07-25 17:41:15 +02:00

646 lines
30 KiB
C++

/***************************************************************************
* 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. *
***************************************************************************/
/*
* Round-trip tests for profile XML persistence (XMLexport -> XMLimport).
*
* A tree of triggers, aliases, timers, keys and scripts is built on a source
* profile - including nested groups, duplicate names at the same level,
* non-ASCII names, multiline scripts and patterns full of XML-hostile
* characters (< > & " ' and "]]>"). The profile is saved through the
* production save path (Host::saveProfile -> XMLexport) and imported into a
* fresh Host (XMLimport, same code path as profile load), then every field
* that the XML format persists is compared between the two trees.
*
* Run with: ctest -R ProfileRoundTripTest -V
*/
#include <QtTest/QtTest>
#include <QTemporaryDir>
#include <functional>
#include "AliasUnit.h"
#include "Host.h"
#include "HostManager.h"
#include "KeyUnit.h"
#include "MudletInstanceCoordinator.h"
#include "ScriptUnit.h"
#include "TAlias.h"
#include "TKey.h"
#include "TScript.h"
#include "TTimer.h"
#include "TTrigger.h"
#include "TelnetServerStub.h"
#include "TimerUnit.h"
#include "TriggerUnit.h"
#include "XMLimport.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 initializeQRCResourcesForProfileRoundTripTest();
namespace {
template <typename T>
QString dbgString(const T& value)
{
QString text;
QDebug(&text) << value;
return text.trimmed();
}
// A multiline Lua script stuffed with every character class the XML encoding
// has to escape or that has historically been mangled (see issue #500):
const QString scmHostileScript = qsl("-- 特殊 characters: <tag> & \"double\" 'single' ]]> done\n"
"local weird = \"<b>&amp;</b> \\\" ' ]]> 日本語 émoji🎉\"\n"
"local function profileRoundTripHelper()\n"
"\treturn weird .. [[literal <&> block]]\n"
"end\n");
const QString scmHostileCommand = qsl("say <hi> & \"there\" 'pal' ]]>");
} // namespace
// Compares one field of the pair of tree nodes held in the local variables
// original/imported, reporting the node path on mismatch:
#define COMPARE_MEMBER(accessor) \
do { \
if (!(imported->accessor == original->accessor)) { \
QFAIL(qPrintable(qsl("%1: " #accessor " mismatch: imported %2, original %3").arg(path, dbgString(imported->accessor), dbgString(original->accessor)))); \
} \
} while (false)
class ProfileRoundTripTest : public QObject
{
Q_OBJECT
private:
TelnetServerStub* mpServer = nullptr;
Host* mpSource = nullptr;
Host* mpTarget = nullptr;
const QString mSourceName = qsl("ProfileRoundTrip-Test");
const QString mTargetName = qsl("ProfileRoundTripTarget-Test");
const QString mPort = qsl("4013");
const QString mLocalhost = qsl("localhost");
QTemporaryDir mSaveDir;
// Expected item totals, kept explicit so an "everything got lost and both
// sides are empty" scenario cannot pass the pairwise comparison:
static const int scmTriggerCount = 7;
static const int scmAliasCount = 6;
static const int scmTimerCount = 6;
static const int scmKeyCount = 6;
static const int scmScriptCount = 6;
// -----------------------------------------------------------------------
// Tree builders - these mirror the construction order XMLimport uses
// -----------------------------------------------------------------------
TTrigger* addTrigger(TTrigger* parent, const QString& name, bool folder, bool active, const QStringList& patterns, const QList<int>& kinds, const QString& script)
{
auto pT = new TTrigger(parent, mpSource);
mpSource->getTriggerUnit()->registerTrigger(pT);
pT->setIsFolder(folder);
pT->setName(name);
pT->setScript(script);
pT->setRegexCodeList(patterns, kinds);
pT->setIsActive(active);
return pT;
}
TAlias* addAlias(TAlias* parent, const QString& name, bool folder, bool active, const QString& regex, const QString& command, const QString& script)
{
auto pT = new TAlias(parent, mpSource);
mpSource->getAliasUnit()->registerAlias(pT);
pT->setIsFolder(folder);
pT->setName(name);
pT->setScript(script);
pT->setCommand(command);
pT->setRegexCode(regex);
pT->setIsActive(active);
return pT;
}
TTimer* addTimer(TTimer* parent, const QString& name, bool folder, bool active, const QTime& time, const QString& command, const QString& script)
{
auto pT = new TTimer(parent, mpSource);
pT->setIsFolder(folder);
mpSource->getTimerUnit()->registerTimer(pT);
pT->setName(name);
pT->setScript(script);
pT->setCommand(command);
pT->setTime(time);
pT->setIsActive(active);
return pT;
}
TKey* addKey(TKey* parent, const QString& name, bool folder, bool active, const Qt::Key keyCode, const Qt::KeyboardModifiers modifiers, const QString& command, const QString& script)
{
auto pT = new TKey(parent, mpSource);
mpSource->getKeyUnit()->registerKey(pT);
pT->setIsFolder(folder);
pT->setName(name);
pT->setScript(script);
pT->setCommand(command);
pT->setKeyCode(keyCode);
pT->setKeyModifiers(modifiers);
pT->setIsActive(active);
return pT;
}
TScript* addScript(TScript* parent, const QString& name, bool folder, bool active, const QStringList& events, const QString& script)
{
auto pT = new TScript(parent, mpSource);
mpSource->getScriptUnit()->registerScript(pT);
pT->setIsFolder(folder);
pT->setName(name);
pT->setScript(script);
pT->setEventHandlerList(events);
pT->setIsActive(active);
return pT;
}
void buildSourceTree()
{
// Triggers: nested groups, duplicate names, non-ASCII names, hostile
// patterns, a multiline+filter trigger and an ANSI color pattern:
auto trGroup = addTrigger(nullptr, qsl("日本語トリガー"), true, true, {}, {}, QString());
addTrigger(trGroup, qsl("dup <>&\"' trigger"), false, true, {qsl("<tag> & \"quoted\" 'single'"), qsl("^(\\w+) <[^>]*>&\"']]>$")}, {REGEX_SUBSTRING, REGEX_PERL}, scmHostileScript);
auto trDup2 = addTrigger(trGroup, qsl("dup <>&\"' trigger"), false, false, {qsl("simple")}, {REGEX_SUBSTRING}, qsl("-- second twin\n"));
trDup2->setIsMultiline(true);
trDup2->setConditionLineDelta(2);
trDup2->mStayOpen = 3;
auto trInner = addTrigger(trGroup, qsl("inner <group> & \"stuff\""), true, false, {}, {}, QString());
auto trEmoji = addTrigger(trInner, qsl("émoji🎉 trigger"), false, true, {qsl("^prefix"), qsl("exact & <line>")}, {REGEX_BEGIN_OF_LINE_SUBSTRING, REGEX_EXACT_MATCH}, qsl("-- colorize\n"));
trEmoji->setIsColorizerTrigger(true);
trEmoji->setColorizerFgColor(QColor(64, 160, 255));
trEmoji->setColorizerBgColor(QColor(20, 20, 20));
trEmoji->setCommand(scmHostileCommand);
auto trLone = addTrigger(
nullptr, qsl("lone & <trigger>"), false, true, {qsl("total recall"), qsl("3"), qsl("return true")}, {REGEX_EXACT_MATCH, REGEX_LINE_SPACER, REGEX_LUA_CODE}, qsl("-- lone\n"));
trLone->mPerlSlashGOption = true;
trLone->mFilterTrigger = true;
trLone->mSoundTrigger = true;
trLone->mSoundFile = qsl("/tmp/beep 日本語 & <sound>.wav");
addTrigger(nullptr,
qsl("ansi color trigger"),
false,
true,
{TTrigger::createColorPatternText(5, 2), TTrigger::createColorPatternText(TTrigger::scmDefault, TTrigger::scmIgnored)},
{REGEX_COLOR_PATTERN, REGEX_COLOR_PATTERN},
qsl("-- color\n"));
// Aliases:
auto alGroup = addAlias(nullptr, qsl("Ålias Gruppe"), true, true, QString(), QString(), QString());
addAlias(alGroup, qsl("dup alias"), false, true, qsl("^say (.+) & <em>\"$"), qsl("say %1 <verbatim> & \"quoted\""), scmHostileScript);
addAlias(alGroup, qsl("dup alias"), false, false, qsl("^другой$"), QString(), qsl("-- twin\n"));
auto alInner = addAlias(alGroup, qsl("sub <alias> & 'group'"), true, true, QString(), QString(), QString());
addAlias(alInner, qsl("émoji🎉 alias"), false, true, qsl("^x ]]> \"quote\" & <tag>$"), QString(), qsl("-- emoji\n"));
addAlias(nullptr, qsl("lone alias"), false, true, qsl("^\\d+ <[^>]+>$"), scmHostileCommand, qsl("-- lone\n"));
// Timers - intervals are long enough that nothing fires during the
// test; the child of a real (non-folder) timer becomes an offset timer:
auto tiGroup = addTimer(nullptr, qsl("타이머 그룹"), true, true, QTime(0, 0, 0, 0), QString(), QString());
auto tiDup1 = addTimer(tiGroup, qsl("dup timer"), false, true, QTime(1, 30, 0, 0), qsl("look"), qsl("-- tick\n"));
addTimer(tiGroup, qsl("dup timer"), false, false, QTime(0, 45, 10, 250), QString(), scmHostileScript);
addTimer(tiDup1, qsl("offset ⏱ timer"), false, false, QTime(0, 0, 30, 500), QString(), qsl("-- offset child\n"));
addTimer(tiGroup, qsl("empty subgroup <>&"), true, false, QTime(0, 0, 0, 0), QString(), QString());
addTimer(nullptr, qsl("solo & <timer>"), false, false, QTime(2, 3, 4, 5), scmHostileCommand, qsl("-- solo\n"));
// Keys:
auto keGroup = addKey(nullptr, qsl("キーグループ"), true, true, Qt::Key_unknown, Qt::NoModifier, QString(), QString());
addKey(keGroup, qsl("dup key"), false, true, Qt::Key_F5, Qt::ControlModifier | Qt::ShiftModifier, qsl("kick"), scmHostileScript);
addKey(keGroup, qsl("dup key"), false, false, Qt::Key_F5, Qt::NoModifier, QString(), qsl("-- twin\n"));
auto keInner = addKey(keGroup, qsl("nested <key> & group"), true, false, Qt::Key_unknown, Qt::NoModifier, QString(), QString());
addKey(keInner, qsl("émoji🎉 key"), false, true, Qt::Key_Eacute, Qt::AltModifier, scmHostileCommand, qsl("-- emoji key\n"));
addKey(nullptr, qsl("lone key"), false, false, Qt::Key_ScrollLock, Qt::KeypadModifier, QString(), qsl("-- lone\n"));
// Scripts, with event handler registrations:
auto scGroup = addScript(nullptr, qsl("脚本组"), true, true, {}, QString());
addScript(scGroup, qsl("dup script"), false, true, {qsl("sysDataEvent"), qsl("custom<>&\"'Event"), qsl("日本語イベント")}, scmHostileScript);
addScript(scGroup, qsl("dup script"), false, false, {}, qsl("-- twin ]]> script\n"));
auto scInner = addScript(scGroup, qsl("scripts <sub> & 'group'"), true, true, {}, QString());
addScript(scInner, qsl("émoji🎉 script"), false, true, {qsl("emojiEvent🎉")}, qsl("-- emoji script\n"));
addScript(nullptr, qsl("lone script"), false, false, {}, QString());
}
// -----------------------------------------------------------------------
// Recursive comparators, one per item type
// -----------------------------------------------------------------------
void compareTriggerNodes(TTrigger* original, TTrigger* imported, const QString& parentPath)
{
const QString path = qsl("%1/%2").arg(parentPath, original->getName());
COMPARE_MEMBER(getName());
COMPARE_MEMBER(isFolder());
COMPARE_MEMBER(shouldBeActive());
COMPARE_MEMBER(isActive());
COMPARE_MEMBER(isTemporary());
COMPARE_MEMBER(getScript());
COMPARE_MEMBER(mPatterns);
COMPARE_MEMBER(getRegexCodePropertyList());
COMPARE_MEMBER(isMultiline());
COMPARE_MEMBER(getConditionLineDelta());
COMPARE_MEMBER(mStayOpen);
COMPARE_MEMBER(getCommand());
COMPARE_MEMBER(mFilterTrigger);
COMPARE_MEMBER(mPerlSlashGOption);
COMPARE_MEMBER(mSoundTrigger);
COMPARE_MEMBER(mSoundFile);
COMPARE_MEMBER(isColorizerTrigger());
COMPARE_MEMBER(getFgColor());
COMPARE_MEMBER(getBgColor());
COMPARE_MEMBER(mColorTrigger);
COMPARE_MEMBER(mColorTriggerFgAnsi);
COMPARE_MEMBER(mColorTriggerBgAnsi);
COMPARE_MEMBER(getTriggerType());
if (original->mpMyChildrenList->size() != imported->mpMyChildrenList->size()) {
QFAIL(qPrintable(qsl("%1: child count mismatch: imported %2, original %3").arg(path).arg(imported->mpMyChildrenList->size()).arg(original->mpMyChildrenList->size())));
}
auto itOriginal = original->mpMyChildrenList->begin();
auto itImported = imported->mpMyChildrenList->begin();
while (itOriginal != original->mpMyChildrenList->end()) {
compareTriggerNodes(*itOriginal, *itImported, path);
if (QTest::currentTestFailed()) {
return;
}
++itOriginal;
++itImported;
}
}
void compareAliasNodes(TAlias* original, TAlias* imported, const QString& parentPath)
{
const QString path = qsl("%1/%2").arg(parentPath, original->getName());
COMPARE_MEMBER(getName());
COMPARE_MEMBER(isFolder());
COMPARE_MEMBER(shouldBeActive());
COMPARE_MEMBER(isActive());
COMPARE_MEMBER(getScript());
COMPARE_MEMBER(getRegexCode());
COMPARE_MEMBER(getCommand());
if (original->mpMyChildrenList->size() != imported->mpMyChildrenList->size()) {
QFAIL(qPrintable(qsl("%1: child count mismatch: imported %2, original %3").arg(path).arg(imported->mpMyChildrenList->size()).arg(original->mpMyChildrenList->size())));
}
auto itOriginal = original->mpMyChildrenList->begin();
auto itImported = imported->mpMyChildrenList->begin();
while (itOriginal != original->mpMyChildrenList->end()) {
compareAliasNodes(*itOriginal, *itImported, path);
if (QTest::currentTestFailed()) {
return;
}
++itOriginal;
++itImported;
}
}
void compareTimerNodes(TTimer* original, TTimer* imported, const QString& parentPath)
{
const QString path = qsl("%1/%2").arg(parentPath, original->getName());
COMPARE_MEMBER(getName());
COMPARE_MEMBER(isFolder());
// Unlike the other item types the import path only fully activates
// root timers (children are activated at runtime when their parent
// fires or the profile connects), so only the persisted user intent -
// shouldBeActive() - is compared here:
COMPARE_MEMBER(shouldBeActive());
COMPARE_MEMBER(isTemporary());
COMPARE_MEMBER(getScript());
COMPARE_MEMBER(getCommand());
COMPARE_MEMBER(getTime());
COMPARE_MEMBER(isOffsetTimer());
if (original->mpMyChildrenList->size() != imported->mpMyChildrenList->size()) {
QFAIL(qPrintable(qsl("%1: child count mismatch: imported %2, original %3").arg(path).arg(imported->mpMyChildrenList->size()).arg(original->mpMyChildrenList->size())));
}
auto itOriginal = original->mpMyChildrenList->begin();
auto itImported = imported->mpMyChildrenList->begin();
while (itOriginal != original->mpMyChildrenList->end()) {
compareTimerNodes(*itOriginal, *itImported, path);
if (QTest::currentTestFailed()) {
return;
}
++itOriginal;
++itImported;
}
}
void compareKeyNodes(TKey* original, TKey* imported, const QString& parentPath)
{
const QString path = qsl("%1/%2").arg(parentPath, original->getName());
COMPARE_MEMBER(getName());
COMPARE_MEMBER(isFolder());
COMPARE_MEMBER(shouldBeActive());
COMPARE_MEMBER(isActive());
COMPARE_MEMBER(getScript());
COMPARE_MEMBER(getCommand());
COMPARE_MEMBER(getKeyCode());
COMPARE_MEMBER(getKeyModifiers());
if (original->mpMyChildrenList->size() != imported->mpMyChildrenList->size()) {
QFAIL(qPrintable(qsl("%1: child count mismatch: imported %2, original %3").arg(path).arg(imported->mpMyChildrenList->size()).arg(original->mpMyChildrenList->size())));
}
auto itOriginal = original->mpMyChildrenList->begin();
auto itImported = imported->mpMyChildrenList->begin();
while (itOriginal != original->mpMyChildrenList->end()) {
compareKeyNodes(*itOriginal, *itImported, path);
if (QTest::currentTestFailed()) {
return;
}
++itOriginal;
++itImported;
}
}
void compareScriptNodes(TScript* original, TScript* imported, const QString& parentPath)
{
const QString path = qsl("%1/%2").arg(parentPath, original->getName());
COMPARE_MEMBER(getName());
COMPARE_MEMBER(isFolder());
COMPARE_MEMBER(shouldBeActive());
COMPARE_MEMBER(isActive());
COMPARE_MEMBER(getScript());
COMPARE_MEMBER(getEventHandlerList());
if (original->mpMyChildrenList->size() != imported->mpMyChildrenList->size()) {
QFAIL(qPrintable(qsl("%1: child count mismatch: imported %2, original %3").arg(path).arg(imported->mpMyChildrenList->size()).arg(original->mpMyChildrenList->size())));
}
auto itOriginal = original->mpMyChildrenList->begin();
auto itImported = imported->mpMyChildrenList->begin();
while (itOriginal != original->mpMyChildrenList->end()) {
compareScriptNodes(*itOriginal, *itImported, path);
if (QTest::currentTestFailed()) {
return;
}
++itOriginal;
++itImported;
}
}
// The source profile may contain pre-installed package items (and items
// flagged as not exportable), so the comparison is restricted to the roots
// this test created - identified by their (unique to this test) names:
template <typename T>
static QList<T*> collectRootsByName(const std::list<T*>& rootNodes, const QStringList& names)
{
QList<T*> collected;
for (auto* node : rootNodes) {
if (names.contains(node->getName())) {
collected << node;
}
}
return collected;
}
template <typename T>
static int countNodes(const QList<T*>& rootNodes)
{
int total = 0;
std::function<void(T*)> walk = [&](T* node) {
++total;
for (auto* child : *node->mpMyChildrenList) {
walk(child);
}
};
for (auto* node : rootNodes) {
walk(node);
}
return total;
}
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(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()) {
dir.removeRecursively();
}
}
private slots:
void initTestCase()
{
initializeQRCResourcesForProfileRoundTripTest();
mpServer = new TelnetServerStub(qApp);
mpServer->start(mLocalhost, mPort.toUShort());
mudlet::start();
mudlet::self()->setupConfig();
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();
mudlet::self()->setStorePasswordsSecurely(false);
deleteProfileDirectory(mSourceName);
deleteProfileDirectory(mTargetName);
QVERIFY(mSaveDir.isValid());
// The source profile needs the full production boot (with a console)
// as XMLexport::writeHost() saves console state too:
startProfile(mSourceName, mLocalhost, mPort);
if (QTest::currentTestFailed()) {
return;
}
mpSource = mudlet::self()->getActiveHost();
QVERIFY2(mpSource, "No active host after profile creation");
buildSourceTree();
if (QTest::currentTestFailed()) {
return;
}
auto [saved, xmlPath, saveError] = mpSource->saveProfile(mSaveDir.path(), qsl("roundtrip"));
QVERIFY2(saved, qPrintable(saveError));
mpSource->waitForProfileSave();
QVERIFY2(QFileInfo::exists(xmlPath), qPrintable(qsl("profile XML was not written to %1").arg(xmlPath)));
// The import target is a bare Host, matching the state a profile is
// in when mudlet::loadProfile() imports its XML at startup:
auto& hostManager = mudlet::self()->getHostManager();
QVERIFY2(hostManager.addHost(mTargetName, mPort, QString(), QString()), "failed to create the target Host");
mpTarget = hostManager.getHost(mTargetName);
QVERIFY(mpTarget);
QFile file(xmlPath);
QVERIFY2(file.open(QFile::ReadOnly | QFile::Text), qPrintable(file.errorString()));
XMLimport importer(mpTarget);
auto [imported, importError] = importer.importPackage(&file);
QVERIFY2(imported, qPrintable(importError));
}
void cleanupTestCase()
{
mpSource = nullptr;
mpTarget = nullptr;
delete mpServer;
mpServer = nullptr;
deleteProfileDirectory(mSourceName);
deleteProfileDirectory(mTargetName);
delete mudlet::self();
}
void test_triggersRoundTrip()
{
const QStringList rootNames{qsl("日本語トリガー"), qsl("lone & <trigger>"), qsl("ansi color trigger")};
const auto originalRoots = collectRootsByName(mpSource->getTriggerUnit()->getTriggerRootNodeList(), rootNames);
const auto importedRoots = collectRootsByName(mpTarget->getTriggerUnit()->getTriggerRootNodeList(), rootNames);
QCOMPARE(originalRoots.size(), 3);
QCOMPARE(importedRoots.size(), 3);
QCOMPARE(countNodes(originalRoots), scmTriggerCount);
QCOMPARE(countNodes(importedRoots), scmTriggerCount);
for (qsizetype i = 0; i < originalRoots.size(); ++i) {
compareTriggerNodes(originalRoots.at(i), importedRoots.at(i), qsl("triggers"));
if (QTest::currentTestFailed()) {
return;
}
}
}
void test_aliasesRoundTrip()
{
const QStringList rootNames{qsl("Ålias Gruppe"), qsl("lone alias")};
const auto originalRoots = collectRootsByName(mpSource->getAliasUnit()->getAliasRootNodeList(), rootNames);
const auto importedRoots = collectRootsByName(mpTarget->getAliasUnit()->getAliasRootNodeList(), rootNames);
QCOMPARE(originalRoots.size(), 2);
QCOMPARE(importedRoots.size(), 2);
QCOMPARE(countNodes(originalRoots), scmAliasCount);
QCOMPARE(countNodes(importedRoots), scmAliasCount);
for (qsizetype i = 0; i < originalRoots.size(); ++i) {
compareAliasNodes(originalRoots.at(i), importedRoots.at(i), qsl("aliases"));
if (QTest::currentTestFailed()) {
return;
}
}
}
void test_timersRoundTrip()
{
const QStringList rootNames{qsl("타이머 그룹"), qsl("solo & <timer>")};
const auto originalRoots = collectRootsByName(mpSource->getTimerUnit()->getTimerRootNodeList(), rootNames);
const auto importedRoots = collectRootsByName(mpTarget->getTimerUnit()->getTimerRootNodeList(), rootNames);
QCOMPARE(originalRoots.size(), 2);
QCOMPARE(importedRoots.size(), 2);
QCOMPARE(countNodes(originalRoots), scmTimerCount);
QCOMPARE(countNodes(importedRoots), scmTimerCount);
for (qsizetype i = 0; i < originalRoots.size(); ++i) {
compareTimerNodes(originalRoots.at(i), importedRoots.at(i), qsl("timers"));
if (QTest::currentTestFailed()) {
return;
}
}
}
void test_keysRoundTrip()
{
const QStringList rootNames{qsl("キーグループ"), qsl("lone key")};
const auto originalRoots = collectRootsByName(mpSource->getKeyUnit()->getKeyRootNodeList(), rootNames);
const auto importedRoots = collectRootsByName(mpTarget->getKeyUnit()->getKeyRootNodeList(), rootNames);
QCOMPARE(originalRoots.size(), 2);
QCOMPARE(importedRoots.size(), 2);
QCOMPARE(countNodes(originalRoots), scmKeyCount);
QCOMPARE(countNodes(importedRoots), scmKeyCount);
for (qsizetype i = 0; i < originalRoots.size(); ++i) {
compareKeyNodes(originalRoots.at(i), importedRoots.at(i), qsl("keys"));
if (QTest::currentTestFailed()) {
return;
}
}
}
void test_scriptsRoundTrip()
{
const QStringList rootNames{qsl("脚本组"), qsl("lone script")};
const auto originalRoots = collectRootsByName(mpSource->getScriptUnit()->getScriptRootNodeList(), rootNames);
const auto importedRoots = collectRootsByName(mpTarget->getScriptUnit()->getScriptRootNodeList(), rootNames);
QCOMPARE(originalRoots.size(), 2);
QCOMPARE(importedRoots.size(), 2);
QCOMPARE(countNodes(originalRoots), scmScriptCount);
QCOMPARE(countNodes(importedRoots), scmScriptCount);
for (qsizetype i = 0; i < originalRoots.size(); ++i) {
compareScriptNodes(originalRoots.at(i), importedRoots.at(i), qsl("scripts"));
if (QTest::currentTestFailed()) {
return;
}
}
}
// The imported scripts registered their event handlers in the fresh Host:
void test_scriptEventHandlersRegisteredAfterImport()
{
QVERIFY(mpTarget->mEventHandlerMap.contains(qsl("sysDataEvent")));
QVERIFY(mpTarget->mEventHandlerMap.contains(qsl("custom<>&\"'Event")));
QVERIFY(mpTarget->mEventHandlerMap.contains(qsl("日本語イベント")));
QVERIFY(mpTarget->mEventHandlerMap.contains(qsl("emojiEvent🎉")));
}
};
void initializeQRCResourcesForProfileRoundTripTest()
{
#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 "ProfileRoundTripTest.moc"
QTEST_MAIN(ProfileRoundTripTest)