Files
BiLauncher/tests/net/tst_modloaderversionservice.cpp
T

274 lines
12 KiB
C++
Raw Normal View History

2026-10-12 13:14:08 +03:00
#include "testenv.h"
#include "fixtures.h"
#include "nettest.h"
#include "launcherpaths.h"
#include "localization.h"
#include "modloaderversionservice.h"
#include <QSignalSpy>
namespace {
const QString kFabric = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/1.20.1");
const QString kQuilt = QStringLiteral("https://meta.quiltmc.org/v3/versions/loader/1.20.1");
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 kNeoMeta = QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
const QString kNeoLegacyMeta = QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/forge/maven-metadata.xml");
FakeResponse xml(const QByteArray &body)
{
FakeResponse response = FakeResponse::bytes(body);
response.contentType = "application/xml";
return response;
}
QStringList loaderVersions(const QList<LoaderVersionEntry> &list)
{
QStringList out;
for (const LoaderVersionEntry &entry : list)
out << entry.loaderVersion;
return out;
}
} // namespace
class TestModLoaderVersionService : public QObject
{
Q_OBJECT
FakeInternet m_net;
void routeAll()
{
m_net.route(kFabric, FakeResponse::json(Fixtures::read("loaders/fabric-1.20.1.json")));
m_net.route(kQuilt, FakeResponse::json(Fixtures::read("loaders/quilt-1.20.1.json")));
m_net.route(kForgeMeta, xml(Fixtures::read("loaders/forge-maven-metadata.xml")));
m_net.route(kForgePromos, FakeResponse::json(Fixtures::read("loaders/forge-promotions_slim.json")));
m_net.route(kNeoMeta, xml(Fixtures::read("loaders/neoforge-maven-metadata.xml")));
m_net.route(kNeoLegacyMeta, xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml")));
}
static bool load(ModLoaderVersionService &service, ModLoader loader, const QString &game,
CallResult &r, bool force = false)
{
service.ensureLoaded(loader, game, r.callback(), force);
return QTest::qWaitFor([&] { return r.calls > 0; }, 15000);
}
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
void init()
{
TestEnv::resetLauncherRoot();
m_net.reset();
routeAll();
}
void fabric()
{
ModLoaderVersionService service;
QSignalSpy changed(&service, &ModLoaderVersionService::versionsChanged);
CallResult r;
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", r));
QVERIFY2(r.ok, qPrintable(r.warning));
const QList<LoaderVersionEntry> list = service.versions(ModLoader::Fabric, "1.20.1");
// Новые первыми: 0.16.10 > 0.16.9 (числовое сравнение, не строковое); пустые версии выброшены.
QCOMPARE(loaderVersions(list), QStringList({"0.17.0-beta.1", "0.16.10", "0.16.9"}));
QCOMPARE(list.at(1).versionId, QStringLiteral("fabric-loader-0.16.10-1.20.1"));
QCOMPARE(list.at(1).gameVersion, QStringLiteral("1.20.1"));
QVERIFY(list.at(1).stable);
QVERIFY(!list.at(0).stable);
QVERIFY(list.at(1).installerUrl.isEmpty());
QCOMPARE(changed.count(), 1);
QCOMPARE(changed.constFirst().at(0).toString(), QStringLiteral("fabric"));
QCOMPARE(changed.constFirst().at(1).toString(), QStringLiteral("1.20.1"));
QVERIFY(service.find(ModLoader::Fabric, "1.20.1", "0.16.9").has_value());
QVERIFY(!service.find(ModLoader::Fabric, "1.20.1", "9.9.9").has_value());
QVERIFY(service.versions(ModLoader::Fabric, "1.19.2").isEmpty());
QVERIFY(QFile::exists(LauncherPaths::loaderCacheFile("fabric")));
}
void quiltStabilityHeuristic()
{
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::Quilt, "1.20.1", r));
QVERIFY(r.ok);
const QList<LoaderVersionEntry> list = service.versions(ModLoader::Quilt, "1.20.1");
QCOMPARE(loaderVersions(list), QStringList({"0.26.4-beta.1", "0.26.3"}));
QCOMPARE(list.at(0).versionId, QStringLiteral("quilt-loader-0.26.4-beta.1-1.20.1"));
QVERIFY(!list.at(0).stable); // поля stable нет — решает «beta» в номере
QVERIFY(list.at(1).stable);
}
void forgeMergesPromotions()
{
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::Forge, "1.20.1", r));
QVERIFY2(r.ok, qPrintable(r.warning));
QCOMPARE(m_net.hits(kForgeMeta), 1);
QCOMPARE(m_net.hits(kForgePromos), 1);
const QList<LoaderVersionEntry> v1201 = service.versions(ModLoader::Forge, "1.20.1");
QCOMPARE(loaderVersions(v1201), QStringList({"47.4.1", "47.4.0", "47.3.0"}));
for (const LoaderVersionEntry &entry : v1201)
QCOMPARE(entry.recommended, entry.loaderVersion == QLatin1String("47.4.0"));
QCOMPARE(v1201.at(1).installerUrl,
QUrl("https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.4.0/forge-1.20.1-47.4.0-installer.jar"));
QVERIFY(v1201.at(1).versionId.isEmpty()); // id Forge узнаём только после установки
// Весь каталог Forge — одним запросом, по всем версиям игры.
const QList<LoaderVersionEntry> v1211 = service.versions(ModLoader::Forge, "1.21.1");
QCOMPARE(loaderVersions(v1211), QStringList({"52.0.10", "52.0.2-beta"}));
QVERIFY(!v1211.at(1).stable);
QCOMPARE(loaderVersions(service.versions(ModLoader::Forge, "1.7.10")),
QStringList{"10.13.4.1614-1.7.10"});
// Другая версия игры у Forge не требует нового запроса.
CallResult again;
QVERIFY(load(service, ModLoader::Forge, "1.21.1", again));
QCOMPARE(m_net.hits(kForgeMeta), 1);
}
void forgeWithoutPromotionsStillWorks()
{
m_net.route(kForgePromos, FakeResponse::error(500));
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::Forge, "1.20.1", r));
QVERIFY(r.ok);
for (const LoaderVersionEntry &entry : service.versions(ModLoader::Forge, "1.20.1"))
QVERIFY(!entry.recommended);
}
void neoForgeBothSchemes()
{
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.21.1", r));
QVERIFY2(r.ok, qPrintable(r.warning));
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.21.1")),
QStringList({"21.1.248", "21.1.200"}));
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.21")), QStringList{"21.0.10"});
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "1.20.4")),
QStringList({"20.4.237", "20.4.80-beta"}));
// Новая нумерация (год.выпуск.патч.сборка).
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "26.1")), QStringList{"26.1.0.5"});
QCOMPARE(loaderVersions(service.versions(ModLoader::NeoForge, "26.1.1")), QStringList{"26.1.1.3"});
QCOMPARE(service.find(ModLoader::NeoForge, "1.21.1", "21.1.248")->installerUrl,
QUrl("https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.248/neoforge-21.1.248-installer.jar"));
}
void neoForgeLegacyArtifactArrivesLast()
{
// Ранние сборки под 1.20.1 — из старого артефакта net.neoforged:forge.
FakeResponse legacySlow = xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml"));
legacySlow.delayMs = 400;
m_net.route(kNeoLegacyMeta, legacySlow);
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r));
QVERIFY(r.ok);
const QList<LoaderVersionEntry> legacy = service.versions(ModLoader::NeoForge, "1.20.1");
QCOMPARE(loaderVersions(legacy), QStringList({"47.1.106", "47.1.79"}));
QCOMPARE(legacy.at(0).installerUrl,
QUrl("https://maven.neoforged.net/releases/net/neoforged/forge/1.20.1-47.1.106/forge-1.20.1-47.1.106-installer.jar"));
}
void neoForgeLegacyArtifactArrivesFirst()
{
// Тот же каталог, но маленький legacy-ответ приходит раньше основного.
FakeResponse primarySlow = xml(Fixtures::read("loaders/neoforge-maven-metadata.xml"));
primarySlow.delayMs = 400;
m_net.route(kNeoMeta, primarySlow);
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r));
QVERIFY(r.ok);
QCOMPARE(service.versions(ModLoader::NeoForge, "1.21.1").size(), 2);
// В startRefresh() основной ответ делает state->merged = parsed и затирает
// уже слитые записи legacy — версии NeoForge для 1.20.1 пропадают.
QEXPECT_FAIL("", "гонка: основной maven-metadata NeoForge затирает уже пришедший legacy", Continue);
QCOMPARE(service.versions(ModLoader::NeoForge, "1.20.1").size(), 2);
}
void primaryFailureIsError()
{
m_net.route(kNeoMeta, FakeResponse::error(503));
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.21.1", r));
QVERIFY(!r.ok);
QVERIFY(r.warning.contains(loaderTitle(ModLoader::NeoForge)));
m_net.route(kForgeMeta, xml("<metadata><versioning><versions></versions></versioning></metadata>"));
CallResult empty;
QVERIFY(load(service, ModLoader::Forge, "1.20.1", empty));
QVERIFY(!empty.ok);
QCOMPARE(empty.warning, Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(ModLoader::Forge)));
m_net.route(kFabric, FakeResponse::json("{\"not\": \"an array\"}"));
CallResult notArray;
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", notArray));
QVERIFY(!notArray.ok);
QCOMPARE(notArray.warning, Loc::text("loader.error.versionsUnparsable").arg(loaderTitle(ModLoader::Fabric)));
}
void cachePerLoaderAndOfflineFallback()
{
{
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", r));
}
QCOMPARE(m_net.hits(kFabric), 1);
ModLoaderVersionService service;
CallResult cached;
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", cached));
QVERIFY(cached.ok);
QCOMPARE(m_net.hits(kFabric), 1); // свежий кеш
QCOMPARE(service.versions(ModLoader::Fabric, "1.20.1").size(), 3);
// Сеть легла, принудительное обновление: данные остаются, ok с предупреждением.
m_net.route(kFabric, FakeResponse::error(502));
CallResult offline;
QVERIFY(load(service, ModLoader::Fabric, "1.20.1", offline, true));
QVERIFY(offline.ok);
QVERIFY(!offline.warning.isEmpty());
QCOMPARE(service.versions(ModLoader::Fabric, "1.20.1").size(), 3);
}
void emptyGameVersionIsNoop()
{
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::Fabric, " ", r));
QVERIFY(r.ok);
QVERIFY(m_net.requests().isEmpty());
}
void inFlightRequestsAreShared()
{
FakeResponse slow = FakeResponse::json(Fixtures::read("loaders/fabric-1.20.1.json"));
slow.delayMs = 300;
m_net.route(kFabric, slow);
ModLoaderVersionService service;
CallResult a, b;
service.ensureLoaded(ModLoader::Fabric, "1.20.1", a.callback());
QVERIFY(service.isRefreshing(ModLoader::Fabric, "1.20.1"));
QVERIFY(!service.isRefreshing(ModLoader::Fabric, "1.19.2"));
service.ensureLoaded(ModLoader::Fabric, "1.20.1", b.callback());
QTRY_VERIFY_WITH_TIMEOUT(a.calls == 1 && b.calls == 1, 15000);
QCOMPARE(m_net.hits(kFabric), 1);
QVERIFY(!service.isRefreshing(ModLoader::Fabric, "1.20.1"));
}
};
BILAUNCHER_TEST_MAIN(TestModLoaderVersionService)
#include "tst_modloaderversionservice.moc"