#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 #include #include #include 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 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 --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"