Files

273 lines
12 KiB
C++
Raw Permalink Normal View History

2026-10-12 13:14:08 +03:00
#include "testenv.h"
#include "fakemojang.h"
#include "fixtures.h"
#include "nettest.h"
#include "launcherpaths.h"
#include "localization.h"
#include "minecraftversion.h"
#include "modloaderinstaller.h"
#include "modloaderversionservice.h"
#include "versioninstaller.h"
#include "versionmanifestservice.h"
#include "zlibreference.h"
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QSignalSpy>
namespace {
const QString kFabricList = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/1.20.1");
const QString kFabricProfile = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/1.20.1/0.16.10/profile/json");
const QString kForgeMeta = QStringLiteral("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml");
const QString kForgePromos = QStringLiteral("https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json");
const QString kForgeInstaller = QStringLiteral(
"https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.4.0/forge-1.20.1-47.4.0-installer.jar");
QByteArray fabricProfile(const QString &inherits = QStringLiteral("1.20.1"))
{
// Профиль Fabric: дочерняя версия с собственной библиотекой из Maven Fabric.
const QJsonObject json{
{"id", "fabric-loader-0.16.10-1.20.1"},
{"inheritsFrom", inherits},
{"type", "release"},
{"mainClass", "net.fabricmc.loader.impl.launch.knot.KnotClient"},
{"arguments", QJsonObject{{"game", QJsonArray{}}, {"jvm", QJsonArray{"-DFabricMcEmu= net.minecraft.client.main.Main "}}}},
{"libraries", QJsonArray{QJsonObject{{"name", "net.fabricmc:fabric-loader:0.16.10"},
{"url", "https://maven.fabricmc.net/"}}}}};
return QJsonDocument(json).toJson(QJsonDocument::Compact);
}
} // namespace
class TestModLoaderInstaller : public QObject
{
Q_OBJECT
FakeInternet m_net;
QString m_game;
struct Rig
{
VersionManifestService manifest;
ModLoaderVersionService meta;
VersionInstaller versions{&manifest};
ModLoaderInstaller installer{&meta, &versions};
QSignalSpy finished{&installer, &ModLoaderInstaller::finished};
QSignalSpy failed{&installer, &ModLoaderInstaller::failed};
QSignalSpy canceled{&installer, &ModLoaderInstaller::canceled};
QSignalSpy log{&installer, &ModLoaderInstaller::log};
bool waitEnd()
{
return QTest::qWaitFor([&] { return finished.count() + failed.count() + canceled.count() > 0; }, 30000);
}
QString failure() const { return failed.isEmpty() ? QString() : failed.constFirst().at(1).toString(); }
};
void routeForge()
{
m_net.route(kForgeMeta, FakeResponse::bytes(Fixtures::read("loaders/forge-maven-metadata.xml")));
m_net.route(kForgePromos, FakeResponse::json(Fixtures::read("loaders/forge-promotions_slim.json")));
m_net.route(kForgeInstaller, FakeResponse::bytes("PK fake forge installer"));
}
// Подставная java, которая ведёт себя как установщик Forge: кладёт профиль версии.
QString forgeLikeJava(const QStringList &extra = {})
{
const QString id = QStringLiteral("1.20.1-forge-47.4.0");
const QString json = QStringLiteral(
"{\"id\":\"%1\",\"inheritsFrom\":\"1.20.1\",\"mainClass\":\"cpw.mods.bootstraplauncher.BootstrapLauncher\"}").arg(id);
QStringList config{"args_file=" + m_game + "/installer-args.txt",
"dump_env=LD_PRELOAD",
"stdout=Extracting json",
"stdout=The client installed successfully"};
config << "create=" + m_game + "/versions/" + id + u'/' + id + ".json\t" + json;
config += extra;
return Fixtures::installFakeJava(m_game + "/jdk17", "17.0.8", config);
}
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
void init()
{
TestEnv::resetLauncherRoot();
m_net.reset();
m_game = TestEnv::freshDir(QStringLiteral("game"));
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
m_net.route(kFabricList, FakeResponse::json(Fixtures::read("loaders/fabric-1.20.1.json")));
m_net.route("https://maven.fabricmc.net/net/fabricmc/fabric-loader/0.16.10/fabric-loader-0.16.10.jar",
FakeResponse::bytes("fabric-loader-jar"));
}
void fabricInstallsBaseThenProfile()
{
m_net.route(kFabricProfile, FakeResponse::json(fabricProfile()));
Rig rig;
QSignalSpy started(&rig.installer, &ModLoaderInstaller::started);
rig.installer.install(m_game, ModLoader::Fabric, "1.20.1", "0.16.10", QString());
QVERIFY(rig.installer.isRunning());
QCOMPARE(started.count(), 1);
QVERIFY(rig.waitEnd());
QVERIFY2(rig.failed.isEmpty(), qPrintable(rig.failure()));
const QList<QVariant> done = rig.finished.constFirst();
QCOMPARE(done.at(0).toString(), QStringLiteral("fabric"));
QCOMPARE(done.at(1).toString(), QStringLiteral("1.20.1"));
QCOMPARE(done.at(2).toString(), QStringLiteral("0.16.10"));
QCOMPARE(done.at(3).toString(), QStringLiteral("fabric-loader-0.16.10-1.20.1"));
QVERIFY(!rig.installer.isRunning());
// Ваниль докачана, профиль сохранён, библиотека загрузчика на месте.
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.jar"));
QVERIFY(QFile::exists(m_game + "/versions/fabric-loader-0.16.10-1.20.1/fabric-loader-0.16.10-1.20.1.json"));
QCOMPARE(Fixtures::readFile(m_game + "/libraries/net/fabricmc/fabric-loader/0.16.10/fabric-loader-0.16.10.jar"),
QByteArray("fabric-loader-jar"));
const MinecraftVersion v = VersionLoader::load(m_game, "fabric-loader-0.16.10-1.20.1", {}, nullptr);
QVERIFY(v.isValid());
}
void fabricProfileForWrongGameIsRejected()
{
m_net.route(kFabricProfile, FakeResponse::json(fabricProfile("1.19.4")));
Rig rig;
rig.installer.install(m_game, ModLoader::Fabric, "1.20.1", "0.16.10", QString());
QVERIFY(rig.waitEnd());
QCOMPARE(rig.failure(), Loc::text("loader.error.descriptionWrongMinecraft")
.arg(loaderTitle(ModLoader::Fabric), "1.19.4", "1.20.1"));
QVERIFY(!QDir(m_game + "/versions/fabric-loader-0.16.10-1.20.1").exists());
}
void fabricProfileWithUnsafeIdIsRejected()
{
m_net.route(kFabricProfile, FakeResponse::json(QJsonObject{{"id", "../evil"}, {"inheritsFrom", "1.20.1"}}));
Rig rig;
rig.installer.install(m_game, ModLoader::Fabric, "1.20.1", "0.16.10", QString());
QVERIFY(rig.waitEnd());
QCOMPARE(rig.failure(), Loc::text("loader.error.descriptionBadId").arg(loaderTitle(ModLoader::Fabric)));
}
void validationErrors()
{
Rig rig;
rig.installer.install(m_game, ModLoader::Fabric, "1.20.1", " ", QString());
QCOMPARE(rig.failure(), Loc::text("loader.error.noVersionSelected").arg(loaderTitle(ModLoader::Fabric)));
QVERIFY(!rig.installer.isRunning());
}
void secondInstallWhileBusy()
{
FakeResponse slow = FakeResponse::json(fabricProfile());
slow.delayMs = 500;
m_net.route(kFabricProfile, slow);
Rig rig;
rig.installer.install(m_game, ModLoader::Fabric, "1.20.1", "0.16.10", QString());
rig.installer.install(m_game, ModLoader::Quilt, "1.20.1", "0.26.3", QString());
QCOMPARE(rig.failed.count(), 1);
QCOMPARE(rig.failure(), Loc::text("loader.error.installInProgress"));
QTRY_COMPARE_WITH_TIMEOUT(rig.finished.count(), 1, 30000);
}
void forgeRunsInstallerWithJava()
{
routeForge();
const QString java = forgeLikeJava();
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "47.4.0", java);
QVERIFY(rig.waitEnd());
QVERIFY2(rig.failed.isEmpty(), qPrintable(rig.failure()));
QCOMPARE(rig.finished.constFirst().at(3).toString(), QStringLiteral("1.20.1-forge-47.4.0"));
// Установщик запущен как java -jar <installer> --installClient <папка игры>.
const QStringList args = QString::fromLocal8Bit(Fixtures::readFile(m_game + "/installer-args.txt"))
.split(u'\n', Qt::SkipEmptyParts);
QCOMPARE(args.value(0), QStringLiteral("-jar"));
QCOMPARE(QFileInfo(args.value(1)).fileName(), QStringLiteral("forge-47.4.0-installer.jar"));
QCOMPARE(args.value(2), QStringLiteral("--installClient"));
QCOMPARE(args.value(3), QDir::toNativeSeparators(m_game));
// Заглушка launcher_profiles.json, без которой установщик Forge отказывается работать.
QVERIFY(Fixtures::readJsonFile(m_game + "/launcher_profiles.json").contains("profiles"));
// Вывод установщика идёт в журнал.
QStringList lines;
for (const auto &call : rig.log)
lines << call.constFirst().toString();
QVERIFY(lines.contains("The client installed successfully"));
QVERIFY(QFile::exists(LauncherPaths::loaderDownloadDir() + "/forge-47.4.0-install.log"));
}
void forgeInstallerFailureCleansUp()
{
routeForge();
const QString java = forgeLikeJava({"exit=1", "stdout=Processor failed, invalid outputs"});
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "47.4.0", java);
QVERIFY(rig.waitEnd());
const QString failure = rig.failure();
QVERIFY(failure.startsWith(Loc::text("loader.error.installerExitCode").arg(loaderTitle(ModLoader::Forge)).arg(1)));
QVERIFY(failure.contains("Processor failed")); // хвост лога в сообщении
// Версию, которую успел создать упавший установщик, убираем.
QVERIFY(!QDir(m_game + "/versions/1.20.1-forge-47.4.0").exists());
QVERIFY(QDir(m_game + "/versions/1.20.1").exists());
}
void forgeInstallerProducingNothing()
{
routeForge();
const QString java = Fixtures::installFakeJava(m_game + "/jdk17-empty", "17.0.8", {"stdout=did nothing"});
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "47.4.0", java);
QVERIFY(rig.waitEnd());
QVERIFY(rig.failure().startsWith(Loc::text("loader.error.noVersionProduced").section(u'%', 0, 0)));
}
void forgeUnknownVersionIsIncompatible()
{
routeForge();
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "1.0.0", QString());
QVERIFY(rig.waitEnd());
QCOMPARE(rig.failure(), Loc::text("loader.error.incompatible").arg(loaderTitle(ModLoader::Forge), "1.0.0", "1.20.1"));
}
void zlibNgEnvironmentOnLinux()
{
#if !defined(Q_OS_LINUX)
QSKIP("LD_PRELOAD с эталонным zlib — только Linux");
#else
routeForge();
const QString java = forgeLikeJava();
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "47.4.0", java);
QVERIFY(rig.waitEnd());
const QStringList args = QString::fromLocal8Bit(Fixtures::readFile(m_game + "/installer-args.txt"))
.split(u'\n', Qt::SkipEmptyParts);
const QString preload = args.filter("env:LD_PRELOAD=").value(0).mid(int(sizeof("env:LD_PRELOAD=") - 1));
if (ZlibReference::systemIsZlibNg() && !ZlibReference::bundledPath().isEmpty())
QVERIFY(preload.startsWith(ZlibReference::bundledPath()));
else
QCOMPARE(preload, qEnvironmentVariable("LD_PRELOAD"));
#endif
}
void cancelDuringInstallerDownload()
{
routeForge();
FakeResponse hang;
hang.hang = true;
m_net.route(kForgeInstaller, hang);
Rig rig;
rig.installer.install(m_game, ModLoader::Forge, "1.20.1", "47.4.0", QString());
QTRY_COMPARE_WITH_TIMEOUT(m_net.hits(kForgeInstaller), 1, 30000);
rig.installer.cancel();
QVERIFY(rig.waitEnd());
QCOMPARE(rig.canceled.count(), 1);
QVERIFY(rig.failed.isEmpty());
QVERIFY(!rig.installer.isRunning());
}
};
BILAUNCHER_TEST_MAIN(TestModLoaderInstaller)
#include "tst_modloaderinstaller.moc"