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