Files
BiLauncher/tests/net/tst_modloaderinstaller.cpp
T
galeon 41599fa82f
tests / linux (push) Failing after 2m18s
tests / windows (push) Skipped
tests / macos (push) Skipped
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped
new unit tests, hehe 67
Signed-off-by: galeon <dertiellis@gmail.com>
2026-10-12 13:14:08 +03:00

274 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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"