302 lines
14 KiB
C++
302 lines
14 KiB
C++
#include "testenv.h"
|
||||
|
|
#include "fixtures.h"
|
|||
|
|
|
|||
|
|
#include "buildswitcher.h"
|
|||
|
|
#include "launcherpaths.h"
|
|||
|
|
#include "localization.h"
|
|||
|
|
|
|||
|
|
#include <QDir>
|
|||
|
|
#include <QJsonArray>
|
|||
|
|
#include <QSignalSpy>
|
|||
|
|
|
|||
|
|
class TestBuildSwitcher : public QObject
|
|||
|
|
{
|
|||
|
|
Q_OBJECT
|
|||
|
|
|
|||
|
|
QString m_game;
|
|||
|
|
|
|||
|
|
static QJsonObject index()
|
|||
|
|
{
|
|||
|
|
return Fixtures::readJsonFile(LauncherPaths::buildStorageDir() + "/index.json");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static QStringList zipsOf(int buildId)
|
|||
|
|
{
|
|||
|
|
return QDir(LauncherPaths::buildDir(buildId)).entryList({"*.zip"}, QDir::Files | QDir::Hidden);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ждёт finished или failed; возвращает true при finished(expectedId).
|
|||
|
|
static bool waitDone(QSignalSpy &finished, QSignalSpy &failed, int expectedId)
|
|||
|
|
{
|
|||
|
|
if (!QTest::qWaitFor([&] { return finished.count() + failed.count() > 0; }, 20000)) {
|
|||
|
|
qWarning("ни finished, ни failed за 20 с");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (!failed.isEmpty()) {
|
|||
|
|
qWarning("failed: %s", qPrintable(failed.constFirst().at(1).toString()));
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return finished.constFirst().constFirst().toInt() == expectedId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void writeShared()
|
|||
|
|
{
|
|||
|
|
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}");
|
|||
|
|
Fixtures::write(m_game + "/libraries/x/y.jar", "lib");
|
|||
|
|
Fixtures::write(m_game + "/assets/indexes/5.json", "{}");
|
|||
|
|
Fixtures::write(m_game + "/launcher_profiles.json", "{}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private slots:
|
|||
|
|
void init()
|
|||
|
|
{
|
|||
|
|
TestEnv::resetLauncherRoot();
|
|||
|
|
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void sharedTopLevelIsStable()
|
|||
|
|
{
|
|||
|
|
QStringList shared = BuildSwitcher::sharedTopLevel();
|
|||
|
|
shared.sort();
|
|||
|
|
QCOMPARE(shared, QStringList({"assets", "launcher_profiles.json", "libraries", "runtime", "versions"}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void switchBackAndForthKeepsEachBuildContent()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
Fixtures::write(m_game + "/mods/alpha.jar", "alpha");
|
|||
|
|
Fixtures::write(m_game + "/saves/A/level.dat", "A");
|
|||
|
|
Fixtures::write(m_game + "/options.txt", "alpha-options");
|
|||
|
|
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
|
|||
|
|
// 1 → 2: у сборки 2 архива ещё нет, папка игры очищается до общих файлов.
|
|||
|
|
switcher.switchTo(1, QStringLiteral("Alpha build"), 2, QStringLiteral("Beta"), m_game);
|
|||
|
|
QVERIFY(switcher.isRunning());
|
|||
|
|
QVERIFY(waitDone(finished, failed, 2));
|
|||
|
|
QVERIFY(!switcher.isRunning());
|
|||
|
|
QCOMPARE(Fixtures::listTree(m_game),
|
|||
|
|
QStringList({"assets/indexes/5.json", "launcher_profiles.json",
|
|||
|
|
"libraries/x/y.jar", "versions/1.20.1/1.20.1.json"}));
|
|||
|
|
QVERIFY(switcher.hasArchive(1));
|
|||
|
|
QVERIFY(!switcher.hasArchive(2));
|
|||
|
|
const QStringList zips = zipsOf(1);
|
|||
|
|
QCOMPARE(zips.size(), 1);
|
|||
|
|
QVERIFY(zips.constFirst().endsWith("_Alpha_build.zip")); // имя из названия сборки
|
|||
|
|
|
|||
|
|
QJsonObject idx = index();
|
|||
|
|
QCOMPARE(idx.value("state").toString(), QStringLiteral("idle"));
|
|||
|
|
QCOMPARE(idx.value("active").toInt(), 2);
|
|||
|
|
QCOMPARE(idx.value("builds").toObject().value("1").toObject().value("name").toString(),
|
|||
|
|
QStringLiteral("Alpha build"));
|
|||
|
|
|
|||
|
|
// Наполняем сборку 2 и возвращаемся в 1.
|
|||
|
|
Fixtures::write(m_game + "/mods/beta.jar", "beta");
|
|||
|
|
finished.clear();
|
|||
|
|
switcher.switchTo(2, QStringLiteral("Beta"), 1, QStringLiteral("Alpha build"), m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 1));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/mods/alpha.jar"), QByteArray("alpha"));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/saves/A/level.dat"), QByteArray("A"));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("alpha-options"));
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/beta.jar"));
|
|||
|
|
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json")); // общее не тронуто
|
|||
|
|
QVERIFY(switcher.hasArchive(2));
|
|||
|
|
|
|||
|
|
// И снова в 2 — его моды на месте.
|
|||
|
|
finished.clear();
|
|||
|
|
switcher.switchTo(1, QStringLiteral("Alpha build"), 2, QStringLiteral("Beta"), m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 2));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/mods/beta.jar"), QByteArray("beta"));
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/alpha.jar"));
|
|||
|
|
// У сборки хранится один актуальный архив, временных не остаётся.
|
|||
|
|
QCOMPARE(zipsOf(1).size(), 1);
|
|||
|
|
QVERIFY(zipsOf(1).filter(".tmp-").isEmpty());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void switchWithoutSourceBuildOnlyClears()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
Fixtures::write(m_game + "/mods/stray.jar", "x");
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
switcher.switchTo(-1, QString(), 5, QStringLiteral("Five"), m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 5));
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/stray.jar"));
|
|||
|
|
QVERIFY(QFile::exists(m_game + "/libraries/x/y.jar"));
|
|||
|
|
QVERIFY(QDir(LauncherPaths::buildStorageDir()).entryList({"*.zip"}).isEmpty());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void secondSwitchWhileBusyIsRejected()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
for (int i = 0; i < 50; ++i)
|
|||
|
|
Fixtures::write(m_game + QStringLiteral("/mods/m%1.jar").arg(i), QByteArray(4096, 'x'));
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
switcher.switchTo(1, "A", 2, "B", m_game);
|
|||
|
|
switcher.switchTo(1, "A", 3, "C", m_game);
|
|||
|
|
QCOMPARE(failed.count(), 1);
|
|||
|
|
QCOMPARE(failed.constFirst().at(0).toInt(), 3);
|
|||
|
|
QCOMPARE(failed.constFirst().at(1).toString(), Loc::text("common.error.switchInProgress"));
|
|||
|
|
QCOMPARE(failed.constFirst().at(2).toBool(), true);
|
|||
|
|
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 20000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void interruptedSwitchIsReportedAndResumed()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
Fixtures::write(m_game + "/mods/half-cleared.jar", "x");
|
|||
|
|
// Архив сборки 7 записан, переключение 3 → 7 оборвалось посреди очистки.
|
|||
|
|
QVERIFY(Fixtures::makeZip(LauncherPaths::buildDir(7) + "/2026-01-01_Seven.zip",
|
|||
|
|
{{"mods/seven.jar", "seven"}, {"options.txt", "seven-options"}}));
|
|||
|
|
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
|||
|
|
QJsonObject{{"state", "switching"}, {"from", 3}, {"to", 7},
|
|||
|
|
{"builds", QJsonObject{{"7", QJsonObject{{"name", "Seven"},
|
|||
|
|
{"archive", "2026-01-01_Seven.zip"}}}}}});
|
|||
|
|
// Мусор от прерванной архивации подметается конструктором.
|
|||
|
|
Fixtures::write(LauncherPaths::buildDir(3) + "/.tmp-deadbeef.zip", "partial");
|
|||
|
|
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QVERIFY(!QFile::exists(LauncherPaths::buildDir(3) + "/.tmp-deadbeef.zip"));
|
|||
|
|
const QString warning = switcher.interruptedSwitchWarning();
|
|||
|
|
QCOMPARE(warning, Loc::text("switch.status.switchInterrupted")
|
|||
|
|
.arg(Loc::text("switch.status.targetSuffix").arg("Seven")));
|
|||
|
|
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
switcher.resumeInterrupted(m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 7));
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/half-cleared.jar"));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/mods/seven.jar"), QByteArray("seven"));
|
|||
|
|
QCOMPARE(index().value("state").toString(), QStringLiteral("idle"));
|
|||
|
|
QVERIFY(switcher.interruptedSwitchWarning().isEmpty());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void applyPackAndRecordNote()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
Fixtures::write(m_game + "/mods/old.jar", "old");
|
|||
|
|
const QString pack = LauncherPaths::buildDir(9) + "/season-pack.zip";
|
|||
|
|
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"config/c.toml", "c"}}));
|
|||
|
|
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
const QJsonObject note{{"seasonalId", "season-5"}, {"revision", 3}};
|
|||
|
|
switcher.applyPack(9, QStringLiteral("Season 5"), pack, {"mods/old.jar"}, note, m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 9));
|
|||
|
|
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/old.jar"));
|
|||
|
|
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
|
|||
|
|
QVERIFY(!QFile::exists(pack)); // пакет удаляется после установки
|
|||
|
|
QStringList entries = switcher.lastPackEntries();
|
|||
|
|
entries.sort();
|
|||
|
|
QCOMPARE(entries, QStringList({"config/c.toml", "mods/new.jar"}));
|
|||
|
|
QCOMPARE(switcher.lastPackNote(), note);
|
|||
|
|
const QJsonObject idx = index();
|
|||
|
|
QCOMPARE(idx.value("state").toString(), QStringLiteral("idle"));
|
|||
|
|
QCOMPARE(idx.value("active").toInt(), 9);
|
|||
|
|
QVERIFY(!idx.contains("pack"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void applyPackErrors()
|
|||
|
|
{
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
const QString missing = LauncherPaths::buildDir(4) + "/nope.zip";
|
|||
|
|
switcher.applyPack(4, "Four", missing, {}, {}, m_game);
|
|||
|
|
QCOMPARE(failed.count(), 1);
|
|||
|
|
QCOMPARE(failed.constFirst().at(1).toString(),
|
|||
|
|
Loc::text("switch.error.archiveMissing").arg(QDir::toNativeSeparators(missing)));
|
|||
|
|
|
|||
|
|
// Пакет лезет в общие папки лаунчера — отказ, папка игры цела.
|
|||
|
|
const QString bad = LauncherPaths::buildDir(4) + "/bad.zip";
|
|||
|
|
QVERIFY(Fixtures::makeZip(bad, {{"versions/evil/evil.json", "{}"}}));
|
|||
|
|
failed.clear();
|
|||
|
|
switcher.applyPack(4, "Four", bad, {}, {}, m_game);
|
|||
|
|
QTRY_COMPARE_WITH_TIMEOUT(failed.count(), 1, 20000);
|
|||
|
|
QCOMPARE(failed.constFirst().at(1).toString(),
|
|||
|
|
Loc::text("switch.error.packTouchesLauncher").arg("versions/evil/evil.json"));
|
|||
|
|
QCOMPARE(failed.constFirst().at(2).toBool(), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void interruptedApplyIsResumedOrCleaned()
|
|||
|
|
{
|
|||
|
|
writeShared();
|
|||
|
|
const QString pack = LauncherPaths::buildDir(11) + "/pack.zip";
|
|||
|
|
QVERIFY(Fixtures::makeZip(pack, {{"mods/resumed.jar", "r"}}));
|
|||
|
|
const QJsonObject note{{"k", "v"}};
|
|||
|
|
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
|||
|
|
QJsonObject{{"state", "applying"}, {"to", 11}, {"pack", "pack.zip"},
|
|||
|
|
{"packName", "Eleven"}, {"packRemove", QJsonArray{"mods/gone.jar"}},
|
|||
|
|
{"packNote", note}});
|
|||
|
|
Fixtures::write(m_game + "/mods/gone.jar", "g");
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QCOMPARE(switcher.interruptedSwitchWarning(),
|
|||
|
|
Loc::text("switch.status.applyInterrupted")
|
|||
|
|
.arg(Loc::text("switch.status.nameSuffix").arg("Eleven")));
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
switcher.resumeInterrupted(m_game);
|
|||
|
|
QVERIFY(waitDone(finished, failed, 11));
|
|||
|
|
QVERIFY(QFile::exists(m_game + "/mods/resumed.jar"));
|
|||
|
|
QVERIFY(!QFile::exists(m_game + "/mods/gone.jar"));
|
|||
|
|
QCOMPARE(switcher.lastPackNote(), note);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Пакет пропал — индекс просто приводится в порядок, без сигналов.
|
|||
|
|
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
|||
|
|
QJsonObject{{"state", "applying"}, {"to", 12}, {"pack", "lost.zip"}});
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
|||
|
|
switcher.resumeInterrupted(m_game);
|
|||
|
|
QCOMPARE(index().value("state").toString(), QStringLiteral("idle"));
|
|||
|
|
QVERIFY(!index().contains("pack"));
|
|||
|
|
QCOMPARE(finished.count() + failed.count(), 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void forgetBuild()
|
|||
|
|
{
|
|||
|
|
QVERIFY(Fixtures::makeZip(LauncherPaths::buildDir(5) + "/a.zip", {{"x", "y"}}));
|
|||
|
|
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
|||
|
|
QJsonObject{{"state", "idle"}, {"active", 5},
|
|||
|
|
{"builds", QJsonObject{{"5", QJsonObject{{"name", "Five"}}},
|
|||
|
|
{"6", QJsonObject{{"name", "Six"}}}}}});
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QVERIFY(switcher.hasArchive(5));
|
|||
|
|
QVERIFY(switcher.forgetBuild(5));
|
|||
|
|
QVERIFY(!QDir(LauncherPaths::buildDir(5)).exists());
|
|||
|
|
QVERIFY(!switcher.hasArchive(5));
|
|||
|
|
const QJsonObject idx = index();
|
|||
|
|
QVERIFY(!idx.contains("active"));
|
|||
|
|
QVERIFY(!idx.value("builds").toObject().contains("5"));
|
|||
|
|
QVERIFY(idx.value("builds").toObject().contains("6"));
|
|||
|
|
QVERIFY(switcher.forgetBuild(-1));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void fractionAndStage()
|
|||
|
|
{
|
|||
|
|
BuildSwitcher switcher;
|
|||
|
|
QCOMPARE(switcher.fraction(), -1.0);
|
|||
|
|
QVERIFY(switcher.stage().isEmpty());
|
|||
|
|
writeShared();
|
|||
|
|
Fixtures::write(m_game + "/mods/a.jar", "a");
|
|||
|
|
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
|||
|
|
QSignalSpy progress(&switcher, &BuildSwitcher::progressChanged);
|
|||
|
|
switcher.switchTo(1, "A", 2, "B", m_game);
|
|||
|
|
QCOMPARE(switcher.stage(), Loc::text("switch.progress.saving").arg("A"));
|
|||
|
|
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 20000);
|
|||
|
|
QVERIFY(progress.count() > 2);
|
|||
|
|
QCOMPARE(switcher.fraction(), -1.0);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
BILAUNCHER_TEST_MAIN(TestBuildSwitcher)
|
|||
|
|
#include "tst_buildswitcher.moc"
|