274 lines
12 KiB
C++
274 lines
12 KiB
C++
#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"
|