mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions Saving a map at format <= 19 was leaking internal `system.fallback_*` keys into the live map's user-visible userData, permanently. A format-19 save polluted room userData; a save at format < 19 tainted map userData forever. - The first commit serialises a locally-augmented copy, so saves never mutate the live map. Old-format **file** output stays byte-identical, so older Mudlets still receive their compatibility keys. - The second commit strips stale leaked keys on format >= 19 loads, so already-tainted maps self-clean. #### Motivation for adding to Mudlet Users' maps were silently accumulating internal keys they never set, and those keys survived across save/load cycles. This stops new pollution and cleans up existing damage. #### Other info (issues closed, discussion etc) Stacked on top of #9468 (base branch `add-persistence-roundtrip-tests`); GitHub will auto-retarget to `development` once that PR merges. Please merge after #9468. These are the two bugs the round-trip tests caught, so this PR also flips their `QEXPECT_FAIL` markers to hard assertions. Human build/test pending; DCO sign-off to be added at squash time. https://github.com/user-attachments/assets/3931edb7-4daf-4277-bac4-0c6680d5bfe3
581 lines
24 KiB
C++
581 lines
24 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 the binary map format (TMap::serialize -> restore).
|
|
*
|
|
* A small map is built in code on a source profile: two areas, rooms with
|
|
* normal exits, exit locks, exit stubs, special exits (locked and unlocked),
|
|
* custom exit lines, doors, exit weights, room/area/map-level userData, room
|
|
* environments, non-ASCII names/symbols, negative coordinates and z-levels
|
|
* plus an area map label. It is saved with TMap::serialize (the same
|
|
* QDataStream setup TMainConsole::saveMap uses), loaded into a fresh Host's
|
|
* map via TMap::restore + audit (the production load path minus the mapper
|
|
* UI) and compared field by field.
|
|
*
|
|
* The map is additionally saved at every user-selectable older format
|
|
* version (TMap::mMinVersion .. mDefaultVersion) and each file is verified
|
|
* to load back correctly.
|
|
*
|
|
* Run with: ctest -R MapRoundTripTest -V
|
|
*/
|
|
|
|
#include <QtTest/QtTest>
|
|
|
|
#include <QFile>
|
|
#include <QSaveFile>
|
|
#include <QTemporaryDir>
|
|
|
|
#include "Host.h"
|
|
#include "HostManager.h"
|
|
#include "MudletInstanceCoordinator.h"
|
|
#include "TArea.h"
|
|
#include "TMap.h"
|
|
#include "TRoom.h"
|
|
#include "TRoomDB.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 initializeQRCResourcesForMapRoundTripTest();
|
|
|
|
namespace {
|
|
const int scmRoom1 = 101;
|
|
const int scmRoom2 = 102;
|
|
const int scmRoom3 = 103;
|
|
const int scmRoom4 = 104;
|
|
|
|
const QString scmAreaAName = qsl("日本語エリア <&\"'>");
|
|
const QString scmAreaBName = qsl("émoji🎉 zone");
|
|
const QString scmRoom1Name = qsl("起点の部屋 <start> & \"here\"");
|
|
const QString scmRoom3Name = qsl("隠し部屋 émoji🎉");
|
|
const QString scmSpecialExitLocked = qsl("拉动 <lever> & \"push\" ]]>");
|
|
// Deliberately starts with a '1' as the pre-version-21 format encodes the
|
|
// special exit lock state as a '0'/'1' prefix on the command:
|
|
const QString scmSpecialExitUnlocked = qsl("1press <plate>");
|
|
const QString scmLabelText = qsl("ラベル <text> & \"stuff\"");
|
|
const QColor scmLabelFg(200, 50, 50);
|
|
const QColor scmLabelBg(20, 20, 60);
|
|
const QColor scmLabelOutline(1, 2, 3);
|
|
const QColor scmSymbolColor(200, 100, 50);
|
|
const QColor scmBorderColor(10, 200, 30, 128);
|
|
const QColor scmCustomLineColor(10, 20, 30);
|
|
} // namespace
|
|
|
|
class MapRoundTripTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private:
|
|
Host* mpSource = nullptr;
|
|
Host* mpTarget = nullptr;
|
|
const QString mSourceName = qsl("MapRoundTripSource-Test");
|
|
const QString mTargetName = qsl("MapRoundTripTarget-Test");
|
|
QTemporaryDir mSaveDir;
|
|
|
|
int mAreaA = -1;
|
|
int mAreaB = -1;
|
|
int mLabelId = -1;
|
|
|
|
// Derived per-area values captured from the source map after building it;
|
|
// TMap::restore() recalculates these via TArea::calcSpan() so the loaded
|
|
// map must reproduce them exactly:
|
|
struct AreaBounds
|
|
{
|
|
QSet<int> rooms;
|
|
QList<int> zLevels;
|
|
int minX = 0;
|
|
int maxX = 0;
|
|
int minY = 0;
|
|
int maxY = 0;
|
|
int minZ = 0;
|
|
int maxZ = 0;
|
|
};
|
|
AreaBounds mBoundsA;
|
|
AreaBounds mBoundsB;
|
|
QImage mLabelImage;
|
|
QSizeF mLabelSize;
|
|
|
|
static QMap<QString, QString> expectedMapUserData() { return {{qsl("map.author 日本語"), qsl("величина <>&\"' ]]>")}, {qsl("plain"), qsl("value")}}; }
|
|
|
|
static QMap<QString, QString> expectedAreaAUserData() { return {{qsl("zone.type"), qsl("都市 <city> & \"stuff\"")}, {qsl("custom"), qsl("value ]]>")}}; }
|
|
|
|
static QMap<QString, QString> expectedRoom1UserData() { return {{qsl("quest.stage 日本語"), qsl("величина <>&\"' ]]>")}, {qsl("plain"), qsl("value")}}; }
|
|
|
|
static QMap<QString, QString> expectedRoom2UserData()
|
|
{
|
|
// The border presentation properties travel inside userData (see
|
|
// TMap::serialize()/TRoom::restore()) so they are part of the
|
|
// expected representation on both sides of the round trip:
|
|
return {{qsl("note"), qsl("second <room> & 'data'")}, {ROOM_UI_BORDERCOLOR, scmBorderColor.name(QColor::HexArgb)}, {ROOM_UI_BORDERTHICKNESS, qsl("4")}};
|
|
}
|
|
|
|
static AreaBounds captureBounds(TArea* pArea)
|
|
{
|
|
pArea->calcSpan();
|
|
AreaBounds bounds;
|
|
bounds.rooms = pArea->rooms;
|
|
bounds.zLevels = pArea->zLevels;
|
|
bounds.minX = pArea->min_x;
|
|
bounds.maxX = pArea->max_x;
|
|
bounds.minY = pArea->min_y;
|
|
bounds.maxY = pArea->max_y;
|
|
bounds.minZ = pArea->min_z;
|
|
bounds.maxZ = pArea->max_z;
|
|
return bounds;
|
|
}
|
|
|
|
void buildSourceMap()
|
|
{
|
|
TMap* pMap = mpSource->mpMap.data();
|
|
TRoomDB* pDB = pMap->mpRoomDB.get();
|
|
|
|
mAreaA = pDB->addArea(scmAreaAName);
|
|
QVERIFY(mAreaA > 0);
|
|
mAreaB = pDB->addArea(scmAreaBName);
|
|
QVERIFY(mAreaB > 0);
|
|
|
|
TArea* pAreaA = pDB->getArea(mAreaA);
|
|
QVERIFY(pAreaA);
|
|
pAreaA->gridMode = true;
|
|
pAreaA->mUserData = expectedAreaAUserData();
|
|
|
|
TArea* pAreaB = pDB->getArea(mAreaB);
|
|
QVERIFY(pAreaB);
|
|
pAreaB->isZone = true;
|
|
pAreaB->zoneAreaRef = mAreaA;
|
|
|
|
for (const int id : {scmRoom1, scmRoom2, scmRoom3, scmRoom4}) {
|
|
QVERIFY(pMap->addRoom(id));
|
|
QVERIFY(pMap->setRoomArea(id, id == scmRoom4 ? mAreaB : mAreaA, false));
|
|
}
|
|
QVERIFY(pMap->setRoomCoordinates(scmRoom1, 0, 0, 0));
|
|
QVERIFY(pMap->setRoomCoordinates(scmRoom2, -3, 7, 0));
|
|
QVERIFY(pMap->setRoomCoordinates(scmRoom3, 2, -5, -3));
|
|
QVERIFY(pMap->setRoomCoordinates(scmRoom4, 0, 0, 5));
|
|
|
|
QVERIFY(pMap->setExit(scmRoom1, scmRoom2, DIR_NORTH));
|
|
QVERIFY(pMap->setExit(scmRoom1, scmRoom3, DIR_SOUTH));
|
|
QVERIFY(pMap->setExit(scmRoom1, scmRoom4, DIR_UP));
|
|
QVERIFY(pMap->setExit(scmRoom3, scmRoom1, DIR_WEST));
|
|
QVERIFY(pMap->setExit(scmRoom4, scmRoom1, DIR_IN));
|
|
QVERIFY(pMap->setExit(scmRoom4, scmRoom2, DIR_OUT));
|
|
QVERIFY(pMap->setExit(scmRoom4, scmRoom3, DIR_NORTHWEST));
|
|
|
|
TRoom* pR1 = pDB->getRoom(scmRoom1);
|
|
QVERIFY(pR1);
|
|
pR1->name = scmRoom1Name;
|
|
pR1->environment = 300;
|
|
pR1->setWeight(3);
|
|
pR1->mSymbol = qsl("⚔");
|
|
pR1->mSymbolColor = scmSymbolColor;
|
|
pR1->setExitLock(DIR_NORTH, true);
|
|
QVERIFY(pR1->setDoor(qsl("n"), 2));
|
|
pR1->setExitWeight(qsl("n"), 7);
|
|
pR1->userData = expectedRoom1UserData();
|
|
pR1->customLines.insert(qsl("n"), {QPointF(1.5, -2.5), QPointF(3.0, 4.0)});
|
|
pR1->customLinesColor.insert(qsl("n"), scmCustomLineColor);
|
|
pR1->customLinesStyle.insert(qsl("n"), Qt::DashLine);
|
|
pR1->customLinesArrow.insert(qsl("n"), true);
|
|
|
|
TRoom* pR2 = pDB->getRoom(scmRoom2);
|
|
QVERIFY(pR2);
|
|
pR2->environment = 12;
|
|
pR2->setWeight(2);
|
|
pR2->isLocked = true;
|
|
pR2->setExitStub(DIR_EAST, true);
|
|
pR2->setExitStub(DIR_SOUTHWEST, true);
|
|
pR2->setSpecialExit(scmRoom1, scmSpecialExitLocked);
|
|
QVERIFY(pR2->setSpecialExitLock(scmSpecialExitLocked, true));
|
|
pR2->setSpecialExit(scmRoom3, scmSpecialExitUnlocked);
|
|
QVERIFY(pR2->setDoor(scmSpecialExitLocked, 3));
|
|
pR2->mBorderColor = scmBorderColor;
|
|
pR2->mBorderThickness = 4;
|
|
pR2->userData = expectedRoom2UserData();
|
|
|
|
TRoom* pR3 = pDB->getRoom(scmRoom3);
|
|
QVERIFY(pR3);
|
|
pR3->name = scmRoom3Name;
|
|
pR3->environment = 5;
|
|
pR3->setWeight(10);
|
|
pR3->setHidden(true);
|
|
|
|
TRoom* pR4 = pDB->getRoom(scmRoom4);
|
|
QVERIFY(pR4);
|
|
pR4->setWeight(1);
|
|
|
|
mLabelId = pMap->createMapLabel(mAreaA, scmLabelText, 1.5f, -2.5f, 0.0f, scmLabelFg, scmLabelBg, true, false, false, 30.0, 12, qsl("DejaVu Sans"), scmLabelOutline);
|
|
QVERIFY(mLabelId >= 0);
|
|
const TMapLabel sourceLabel = pAreaA->mMapLabels.value(mLabelId);
|
|
mLabelImage = sourceLabel.pix.toImage().convertToFormat(QImage::Format_ARGB32);
|
|
mLabelSize = sourceLabel.size;
|
|
|
|
pMap->mUserData = expectedMapUserData();
|
|
pMap->mEnvColors[5] = 2;
|
|
pMap->mEnvColors[12] = 7;
|
|
// Ids 257-272 are reserved for the 16 ANSI colors and get rewritten
|
|
// from the Host palette on every load (TMap::restore16ColorSet()), so
|
|
// only ids outside that range are expected to round-trip:
|
|
pMap->mCustomEnvColors[300] = QColor(12, 34, 56);
|
|
pMap->mCustomEnvColors[301] = QColor(255, 0, 255);
|
|
pMap->mRoomIdHash[mSourceName] = scmRoom1;
|
|
pMap->mRoomIdHash[qsl("OtherProfile")] = scmRoom3;
|
|
pDB->hashToRoomID[qsl("hash-abc")] = scmRoom1;
|
|
pDB->hashToRoomID[qsl("hash-日本語")] = scmRoom2;
|
|
pMap->mMapSymbolFont = QFont(qsl("DejaVu Serif"), 14);
|
|
pMap->mMapSymbolFontFudgeFactor = 1.25;
|
|
pMap->mIsOnlyMapSymbolFontToBeUsed = true;
|
|
|
|
mBoundsA = captureBounds(pAreaA);
|
|
mBoundsB = captureBounds(pAreaB);
|
|
}
|
|
|
|
bool saveMapToFile(TMap* pMap, const QString& fileName, int saveVersion)
|
|
{
|
|
QSaveFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
return false;
|
|
}
|
|
QDataStream out(&file);
|
|
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
|
|
out.setVersion(mudlet::scmQDataStreamFormat_5_12);
|
|
}
|
|
if (!pMap->serialize(out, saveVersion)) {
|
|
return false;
|
|
}
|
|
return file.commit();
|
|
}
|
|
|
|
// QDataStream stores QStrings as a length prefix plus the string encoded as UTF-16BE,
|
|
// so the raw file can be scanned for a serialized string's bytes:
|
|
static bool fileContainsSerializedString(const QString& fileName, const QString& needle)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
const QByteArray raw = file.readAll();
|
|
QByteArray needleBytes;
|
|
QDataStream out(&needleBytes, QIODevice::WriteOnly);
|
|
out << needle;
|
|
// The length prefix stays in the needle so a key cannot match a longer key it is
|
|
// a byte prefix of ("system.fallback_mapSymbolFont" vs. "...FontFudgeFactor"):
|
|
return raw.contains(needleBytes);
|
|
}
|
|
|
|
void verifyArea(TArea* pArea, const AreaBounds& bounds, const QString& areaLabel)
|
|
{
|
|
QVERIFY2(pArea, qPrintable(qsl("%1 is missing").arg(areaLabel)));
|
|
QCOMPARE(pArea->rooms, bounds.rooms);
|
|
QCOMPARE(pArea->zLevels, bounds.zLevels);
|
|
QCOMPARE(pArea->min_x, bounds.minX);
|
|
QCOMPARE(pArea->max_x, bounds.maxX);
|
|
QCOMPARE(pArea->min_y, bounds.minY);
|
|
QCOMPARE(pArea->max_y, bounds.maxY);
|
|
QCOMPARE(pArea->min_z, bounds.minZ);
|
|
QCOMPARE(pArea->max_z, bounds.maxZ);
|
|
}
|
|
|
|
void verifyMap(TMap* pMap, int savedVersion, const QString& ownProfileKey)
|
|
{
|
|
QCOMPARE(pMap->mVersion, savedVersion);
|
|
TRoomDB* pDB = pMap->mpRoomDB.get();
|
|
|
|
QCOMPARE(pDB->getAreaNamesMap().value(mAreaA), scmAreaAName);
|
|
QCOMPARE(pDB->getAreaNamesMap().value(mAreaB), scmAreaBName);
|
|
|
|
TArea* pAreaA = pDB->getArea(mAreaA);
|
|
QVERIFY(pAreaA);
|
|
verifyArea(pAreaA, mBoundsA, qsl("area A"));
|
|
if (QTest::currentTestFailed()) {
|
|
return;
|
|
}
|
|
QVERIFY(pAreaA->gridMode);
|
|
QCOMPARE(pAreaA->mUserData, expectedAreaAUserData());
|
|
|
|
TArea* pAreaB = pDB->getArea(mAreaB);
|
|
QVERIFY(pAreaB);
|
|
verifyArea(pAreaB, mBoundsB, qsl("area B"));
|
|
if (QTest::currentTestFailed()) {
|
|
return;
|
|
}
|
|
QVERIFY(pAreaB->isZone);
|
|
QCOMPARE(pAreaB->zoneAreaRef, mAreaA);
|
|
|
|
QCOMPARE(pAreaA->mMapLabels.size(), 1);
|
|
QVERIFY(pAreaA->mMapLabels.contains(mLabelId));
|
|
const TMapLabel label = pAreaA->mMapLabels.value(mLabelId);
|
|
QCOMPARE(label.text, scmLabelText);
|
|
QCOMPARE(label.pos, QVector3D(1.5f, -2.5f, 0.0f));
|
|
QCOMPARE(label.size, mLabelSize);
|
|
QCOMPARE(label.fgColor, scmLabelFg);
|
|
QCOMPARE(label.bgColor, scmLabelBg);
|
|
QCOMPARE(label.outlineColor, scmLabelOutline);
|
|
QVERIFY(label.showOnTop);
|
|
QVERIFY(!label.noScaling);
|
|
QCOMPARE(label.font.family(), qsl("DejaVu Sans"));
|
|
QCOMPARE(label.font.pointSize(), 12);
|
|
QCOMPARE(label.font.weight(), QFont(qsl("DejaVu Sans"), 12).weight());
|
|
QVERIFY(!label.font.italic());
|
|
QCOMPARE(label.pix.toImage().convertToFormat(QImage::Format_ARGB32), mLabelImage);
|
|
|
|
TRoom* pR1 = pDB->getRoom(scmRoom1);
|
|
QVERIFY(pR1);
|
|
QCOMPARE(pR1->getArea(), mAreaA);
|
|
QCOMPARE(pR1->x(), 0);
|
|
QCOMPARE(pR1->y(), 0);
|
|
QCOMPARE(pR1->z(), 0);
|
|
QCOMPARE(pR1->name, scmRoom1Name);
|
|
QCOMPARE(pR1->environment, 300);
|
|
QCOMPARE(pR1->getWeight(), 3);
|
|
QCOMPARE(pR1->mSymbol, qsl("⚔"));
|
|
QCOMPARE(pR1->mSymbolColor, scmSymbolColor);
|
|
QCOMPARE(pR1->getNorth(), scmRoom2);
|
|
QCOMPARE(pR1->getSouth(), scmRoom3);
|
|
QCOMPARE(pR1->getUp(), scmRoom4);
|
|
QCOMPARE(pR1->getEast(), -1);
|
|
QCOMPARE((QSet<int>{pR1->exitLocks.begin(), pR1->exitLocks.end()}), QSet<int>{DIR_NORTH});
|
|
QCOMPARE(pR1->doors, (QMap<QString, int>{{qsl("n"), 2}}));
|
|
QCOMPARE(pR1->getExitWeights(), (QMap<QString, int>{{qsl("n"), 7}}));
|
|
QCOMPARE(pR1->customLines, (QMap<QString, QList<QPointF>>{{qsl("n"), {QPointF(1.5, -2.5), QPointF(3.0, 4.0)}}}));
|
|
QCOMPARE(pR1->customLinesColor, (QMap<QString, QColor>{{qsl("n"), scmCustomLineColor}}));
|
|
QCOMPARE(pR1->customLinesStyle, (QMap<QString, Qt::PenStyle>{{qsl("n"), Qt::DashLine}}));
|
|
QCOMPARE(pR1->customLinesArrow, (QMap<QString, bool>{{qsl("n"), true}}));
|
|
// The format 19 leg runs after the format 17/18 ones, so this also
|
|
// guards against a < 19 save leaving a stray system.fallback_symbol
|
|
// entry behind in the live source room's user data:
|
|
QCOMPARE(pR1->userData, expectedRoom1UserData());
|
|
|
|
TRoom* pR2 = pDB->getRoom(scmRoom2);
|
|
QVERIFY(pR2);
|
|
QCOMPARE(pR2->getArea(), mAreaA);
|
|
QCOMPARE(pR2->x(), -3);
|
|
QCOMPARE(pR2->y(), 7);
|
|
QCOMPARE(pR2->z(), 0);
|
|
QCOMPARE(pR2->environment, 12);
|
|
QCOMPARE(pR2->getWeight(), 2);
|
|
QVERIFY(pR2->isLocked);
|
|
// TRoomDB::auditRooms() deduplicates exit stubs by passing them
|
|
// through a QSet, which does not preserve order - so only the
|
|
// membership is stable across a load:
|
|
QCOMPARE((QSet<int>{pR2->exitStubs.begin(), pR2->exitStubs.end()}), (QSet<int>{DIR_EAST, DIR_SOUTHWEST}));
|
|
QCOMPARE(pR2->getSpecialExits(), (QMap<QString, int>{{scmSpecialExitLocked, scmRoom1}, {scmSpecialExitUnlocked, scmRoom3}}));
|
|
QCOMPARE(pR2->getSpecialExitLocks(), QSet<QString>{scmSpecialExitLocked});
|
|
QCOMPARE(pR2->doors, (QMap<QString, int>{{scmSpecialExitLocked, 3}}));
|
|
QCOMPARE(pR2->mBorderColor, scmBorderColor);
|
|
QCOMPARE(pR2->mBorderThickness, 4);
|
|
QCOMPARE(pR2->userData, expectedRoom2UserData());
|
|
|
|
TRoom* pR3 = pDB->getRoom(scmRoom3);
|
|
QVERIFY(pR3);
|
|
QCOMPARE(pR3->getArea(), mAreaA);
|
|
QCOMPARE(pR3->x(), 2);
|
|
QCOMPARE(pR3->y(), -5);
|
|
QCOMPARE(pR3->z(), -3);
|
|
QCOMPARE(pR3->name, scmRoom3Name);
|
|
QCOMPARE(pR3->environment, 5);
|
|
QCOMPARE(pR3->getWeight(), 10);
|
|
QVERIFY(pR3->isHidden());
|
|
QCOMPARE(pR3->getWest(), scmRoom1);
|
|
|
|
TRoom* pR4 = pDB->getRoom(scmRoom4);
|
|
QVERIFY(pR4);
|
|
QCOMPARE(pR4->getArea(), mAreaB);
|
|
QCOMPARE(pR4->z(), 5);
|
|
QCOMPARE(pR4->getIn(), scmRoom1);
|
|
QCOMPARE(pR4->getOut(), scmRoom2);
|
|
QCOMPARE(pR4->getNorthwest(), scmRoom3);
|
|
|
|
// The format 19 leg runs after the format 17/18 ones, so this also
|
|
// guards against a < 19 save leaving stray
|
|
// system.fallback_mapSymbolFont* entries behind in the live source
|
|
// map's user data:
|
|
QCOMPARE(pMap->mUserData, expectedMapUserData());
|
|
QCOMPARE(pMap->mEnvColors, (QMap<int, int>{{5, 2}, {12, 7}}));
|
|
QCOMPARE(pMap->mCustomEnvColors.value(300), QColor(12, 34, 56));
|
|
QCOMPARE(pMap->mCustomEnvColors.value(301), QColor(255, 0, 255));
|
|
QCOMPARE(pDB->hashToRoomID, (QMap<QString, int>{{qsl("hash-abc"), scmRoom1}, {qsl("hash-日本語"), scmRoom2}}));
|
|
if (savedVersion >= 18) {
|
|
// Since format 18 the player location is stored per profile:
|
|
QCOMPARE(pMap->mRoomIdHash, (QHash<QString, int>{{mSourceName, scmRoom1}, {qsl("OtherProfile"), scmRoom3}}));
|
|
} else {
|
|
// Format 17 only stores the saving profile's own location and the
|
|
// loader files it under the loading profile's name:
|
|
QCOMPARE(pMap->mRoomIdHash.value(ownProfileKey), scmRoom1);
|
|
}
|
|
QCOMPARE(pMap->mMapSymbolFont.family(), qsl("DejaVu Serif"));
|
|
QCOMPARE(pMap->mMapSymbolFont.pointSize(), 14);
|
|
QCOMPARE(pMap->mMapSymbolFontFudgeFactor, 1.25);
|
|
QVERIFY(pMap->mIsOnlyMapSymbolFontToBeUsed);
|
|
}
|
|
|
|
void roundTripAtVersion(int saveVersion)
|
|
{
|
|
const QString fileName = qsl("%1/map_v%2.dat").arg(mSaveDir.path()).arg(saveVersion);
|
|
QVERIFY2(saveMapToFile(mpSource->mpMap.data(), fileName, saveVersion), qPrintable(qsl("failed to save map at format version %1").arg(saveVersion)));
|
|
|
|
// Saving at any format must not leak system.fallback_* keys into the
|
|
// live source map's or rooms' user data - room 1 carries a symbol and
|
|
// a symbol color, room 3 is hidden:
|
|
QCOMPARE(mpSource->mpMap->mUserData, expectedMapUserData());
|
|
QCOMPARE(mpSource->mpMap->mpRoomDB->getRoom(scmRoom1)->userData, expectedRoom1UserData());
|
|
QVERIFY(mpSource->mpMap->mpRoomDB->getRoom(scmRoom3)->userData.isEmpty());
|
|
|
|
TMap* pTargetMap = mpTarget->mpMap.data();
|
|
pTargetMap->mapClear();
|
|
QVERIFY2(pTargetMap->restore(fileName), qPrintable(qsl("failed to restore map saved at format version %1").arg(saveVersion)));
|
|
pTargetMap->audit();
|
|
|
|
verifyMap(pTargetMap, saveVersion, mTargetName);
|
|
}
|
|
|
|
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()
|
|
{
|
|
initializeQRCResourcesForMapRoundTripTest();
|
|
|
|
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());
|
|
|
|
auto& hostManager = mudlet::self()->getHostManager();
|
|
QVERIFY2(hostManager.addHost(mSourceName, qsl("23"), QString(), QString()), "failed to create the source Host");
|
|
mpSource = hostManager.getHost(mSourceName);
|
|
QVERIFY(mpSource);
|
|
QVERIFY2(hostManager.addHost(mTargetName, qsl("23"), QString(), QString()), "failed to create the target Host");
|
|
mpTarget = hostManager.getHost(mTargetName);
|
|
QVERIFY(mpTarget);
|
|
|
|
buildSourceMap();
|
|
if (QTest::currentTestFailed()) {
|
|
return;
|
|
}
|
|
|
|
// Sanity check: the freshly built source map itself matches all
|
|
// expectations before any save/load is involved:
|
|
verifyMap(mpSource->mpMap.data(), mpSource->mpMap->mDefaultVersion, mSourceName);
|
|
}
|
|
|
|
void cleanupTestCase()
|
|
{
|
|
mpSource = nullptr;
|
|
mpTarget = nullptr;
|
|
deleteProfileDirectory(mSourceName);
|
|
deleteProfileDirectory(mTargetName);
|
|
delete mudlet::self();
|
|
}
|
|
|
|
void test_roundTripAtDefaultVersion() { roundTripAtVersion(mpSource->mpMap->mDefaultVersion); }
|
|
|
|
void test_roundTripAtOlderVersions_data()
|
|
{
|
|
QTest::addColumn<int>("saveVersion");
|
|
for (int version = mpSource->mpMap->mMinVersion; version < mpSource->mpMap->mDefaultVersion; ++version) {
|
|
QTest::newRow(qPrintable(qsl("format %1").arg(version))) << version;
|
|
}
|
|
}
|
|
|
|
void test_roundTripAtOlderVersions()
|
|
{
|
|
QFETCH(int, saveVersion);
|
|
roundTripAtVersion(saveVersion);
|
|
}
|
|
|
|
void test_taintedMapSelfCleansOnFormat19PlusLoad()
|
|
{
|
|
// Simulate a map already tainted in the wild by past versions whose
|
|
// saving in a format below 19 left the fallback keys behind in the
|
|
// live user data - which then rode along in every format >= 19 save:
|
|
TMap* pSourceMap = mpSource->mpMap.data();
|
|
TRoom* pSourceR1 = pSourceMap->mpRoomDB->getRoom(scmRoom1);
|
|
QVERIFY(pSourceR1);
|
|
pSourceMap->mUserData.insert(qsl("system.fallback_mapSymbolFont"), qsl("Stale Font,10,-1,5,400,0,0,0,0,0"));
|
|
pSourceMap->mUserData.insert(qsl("system.fallback_mapSymbolFontFudgeFactor"), qsl("9.99"));
|
|
pSourceMap->mUserData.insert(qsl("system.fallback_onlyUseMapSymbolFont"), qsl("false"));
|
|
// The unique value doubles as the proof below that this key really
|
|
// made it into the file:
|
|
pSourceR1->userData.insert(qsl("system.fallback_symbol"), qsl("stale-room-symbol-junk"));
|
|
|
|
const int saveVersion = pSourceMap->mDefaultVersion;
|
|
QVERIFY(saveVersion >= 19);
|
|
const QString fileName = qsl("%1/map_tainted_v%2.dat").arg(mSaveDir.path()).arg(saveVersion);
|
|
QVERIFY(saveMapToFile(pSourceMap, fileName, saveVersion));
|
|
|
|
// Undo the tainting of the live source map:
|
|
pSourceMap->mUserData = expectedMapUserData();
|
|
pSourceR1->userData = expectedRoom1UserData();
|
|
|
|
// The junk keys really did make it into the serialized stream:
|
|
QVERIFY(fileContainsSerializedString(fileName, qsl("system.fallback_mapSymbolFont")));
|
|
QVERIFY(fileContainsSerializedString(fileName, qsl("system.fallback_onlyUseMapSymbolFont")));
|
|
QVERIFY(fileContainsSerializedString(fileName, qsl("stale-room-symbol-junk")));
|
|
|
|
TMap* pTargetMap = mpTarget->mpMap.data();
|
|
pTargetMap->mapClear();
|
|
QVERIFY(pTargetMap->restore(fileName));
|
|
pTargetMap->audit();
|
|
|
|
// Loading strips the junk keys while the legitimate user data - and
|
|
// the authoritative values stored directly in the stream - survive:
|
|
QCOMPARE(pTargetMap->mUserData, expectedMapUserData());
|
|
QCOMPARE(pTargetMap->mMapSymbolFont.family(), qsl("DejaVu Serif"));
|
|
QCOMPARE(pTargetMap->mMapSymbolFontFudgeFactor, 1.25);
|
|
QVERIFY(pTargetMap->mIsOnlyMapSymbolFontToBeUsed);
|
|
TRoom* pTargetR1 = pTargetMap->mpRoomDB->getRoom(scmRoom1);
|
|
QVERIFY(pTargetR1);
|
|
QCOMPARE(pTargetR1->userData, expectedRoom1UserData());
|
|
QCOMPARE(pTargetR1->mSymbol, qsl("⚔"));
|
|
}
|
|
};
|
|
|
|
void initializeQRCResourcesForMapRoundTripTest()
|
|
{
|
|
#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 "MapRoundTripTest.moc"
|
|
QTEST_MAIN(MapRoundTripTest)
|