Files

383 lines
16 KiB
C++
Raw Permalink Normal View History

2026-10-12 13:14:08 +03:00
#include "testenv.h"
#include "fakemojang.h"
#include "fixtures.h"
#include "nettest.h"
#include "gamelauncher.h"
#include "localization.h"
#include "minecraftversion.h"
#include "versioninstaller.h"
#include "versionmanifestservice.h"
#include <QDir>
#include <QSignalSpy>
class TestVersionInstaller : public QObject
{
Q_OBJECT
FakeInternet m_net;
QString m_game;
struct Run
{
explicit Run(VersionInstaller &installer)
: started(&installer, &VersionInstaller::started)
, finished(&installer, &VersionInstaller::finished)
, failed(&installer, &VersionInstaller::failed)
, canceled(&installer, &VersionInstaller::canceled)
, progress(&installer, &VersionInstaller::progressChanged)
{}
bool waitEnd(int expectedEvents = 1)
{
return QTest::qWaitFor([&] { return finished.count() + failed.count() + canceled.count() >= expectedEvents; },
30000);
}
QString failure() const { return failed.isEmpty() ? QString() : failed.constLast().at(1).toString(); }
QSignalSpy started, finished, failed, canceled, progress;
};
void verifyInstalled(const FakeMojang &mojang, const QString &id)
{
const auto expected = mojang.expectedFiles(id);
QVERIFY(!expected.isEmpty());
for (auto it = expected.cbegin(); it != expected.cend(); ++it) {
const QString path = m_game + u'/' + it.key();
QVERIFY2(QFile::exists(path), qPrintable("нет файла " + it.key()));
QCOMPARE(Fixtures::readFile(path), it.value());
}
}
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
void init()
{
TestEnv::resetLauncherRoot();
m_net.reset();
m_game = TestEnv::freshDir(QStringLiteral("game"));
}
void installsModernVersionCompletely()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(installer.isRunning());
QCOMPARE(installer.versionId(), QStringLiteral("1.20.1"));
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
QCOMPARE(run.started.count(), 1);
QCOMPARE(run.finished.constFirst().constFirst().toString(), QStringLiteral("1.20.1"));
QVERIFY(!installer.isRunning());
QVERIFY(run.progress.count() > 0);
QVERIFY(m_net.unmatched().isEmpty());
verifyInstalled(mojang, "1.20.1");
// Результат пригоден для запуска: GameLauncher не видит недостающих файлов.
QString error;
const MinecraftVersion version = VersionLoader::load(m_game, "1.20.1", {}, &error);
QVERIFY2(version.isValid(), qPrintable(error));
LaunchOptions options;
options.gameDir = m_game;
QVERIFY(GameLauncher::missingFiles(options, version).isEmpty());
}
void secondInstallSkipsExistingFiles()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
{
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY(run.failed.isEmpty());
}
const int firstRequests = m_net.requests().size();
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY(run.failed.isEmpty());
// Повторно скачивается только json версии; файлы нужного размера пропускаются.
const int extra = m_net.requests().size() - firstRequests;
QVERIFY2(extra <= 1, qPrintable(QString::number(extra)));
}
void legacyAssetsAreCopiedToVirtual()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.5.2", .legacyAssets = true, .javaMajor = 8});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.5.2");
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyInstalled(mojang, "1.5.2"); // включая assets/virtual/legacy/*
}
void inheritedVersionPullsParentFirst()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
mojang.publish({.id = "fabric-loader-0.16.10-1.20.1", .inheritsFrom = "1.20.1"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "fabric-loader-0.16.10-1.20.1");
QVERIFY(run.waitEnd(2));
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
// started приходит на каждую версию, которую очередь берёт «с нуля».
QCOMPARE(run.started.constFirst().constFirst().toString(), QStringLiteral("fabric-loader-0.16.10-1.20.1"));
QStringList finishedIds;
for (const auto &call : run.finished)
finishedIds << call.constFirst().toString();
QCOMPARE(finishedIds, QStringList({"1.20.1", "fabric-loader-0.16.10-1.20.1"}));
verifyInstalled(mojang, "1.20.1");
verifyInstalled(mojang, "fabric-loader-0.16.10-1.20.1");
}
void localJsonUsedWhenNotInManifest()
{
// Версия не из манифеста (например, профиль модлоадера), но json уже лежит локально.
FakeMojang mojang(m_net);
mojang.publish({.id = "custom-1", .listed = false});
Fixtures::write(m_game + "/versions/custom-1/custom-1.json", mojang.versionJson("custom-1"));
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "custom-1");
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyInstalled(mojang, "custom-1");
QCOMPARE(m_net.hits(mojang.versionJsonUrl("custom-1")), 0);
}
void unknownVersionFails()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "9.9.9");
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("version.error.notInManifest").arg("9.9.9"));
}
void invalidNamesRejected_data()
{
QTest::addColumn<QString>("id");
QTest::newRow("traversal") << "../evil";
QTest::newRow("slash") << "a/b";
QTest::newRow("dot") << ".";
QTest::newRow("colon") << "c:x";
}
void invalidNamesRejected()
{
QFETCH(QString, id);
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, id);
QCOMPARE(run.failed.count(), 1);
QCOMPARE(run.failure(), Loc::text("version.error.invalidName").arg(id));
QVERIFY(m_net.requests().isEmpty());
installer.install(m_game, " "); // пустой id молча игнорируется
QCOMPARE(run.failed.count(), 1);
}
void versionJsonChecksumMismatch()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
m_net.route(mojang.versionJsonUrl("1.20.1"), FakeResponse::json("{\"tampered\":true}"));
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("version.error.descriptionChecksum").arg("1.20.1"));
QVERIFY(!QFile::exists(m_game + "/versions/1.20.1/1.20.1.json"));
}
void transientErrorIsRetried()
{
// Сбой не на последнем файле: пока ждём повтора, остальные ещё качаются.
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
const QString lib = QStringLiteral("libraries/com/mojang/brigadier/1.1.8/brigadier-1.1.8.jar");
const QString jar = QStringLiteral("versions/1.20.1/1.20.1.jar");
FakeResponse slowJar = FakeResponse::bytes(mojang.expectedFiles("1.20.1").value(jar));
slowJar.delayMs = 1500;
mojang.overrideFile(jar, slowJar);
int attempts = 0;
m_net.route(mojang.fileUrl(lib), [&attempts](const FakeRequest &) {
return ++attempts == 1 ? FakeResponse::error(503) : FakeResponse::bytes("brigadier");
});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
QCOMPARE(attempts, 2);
verifyInstalled(mojang, "1.20.1");
}
void retryOfLastFileIsNotLost()
{
// Сбой на последнем файле: остальные уже скачаны, повтор стоит в таймере.
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
const QString lib = QStringLiteral("libraries/com/mojang/brigadier/1.1.8/brigadier-1.1.8.jar");
int attempts = 0;
m_net.route(mojang.fileUrl(lib), [&attempts](const FakeRequest &) {
if (++attempts > 1)
return FakeResponse::bytes("brigadier");
FakeResponse late = FakeResponse::error(503);
late.delayMs = 1000; // все прочие файлы успевают докачаться
return late;
});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
// retryOrFail() откладывает повтор на 400 мс, а handleReplyFinished() тут же
// зовёт pump(): очередь и активные загрузки пусты — установка «завершается»
// и падает на проверке файлов, повтор так и не происходит.
QEXPECT_FAIL("", "повтор последнего файла теряется: pump() завершает установку раньше таймера", Abort);
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
QCOMPARE(attempts, 2);
}
void checksumMismatchRetriesThenFails()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
const QString lib = QStringLiteral("libraries/com/mojang/brigadier/1.1.8/brigadier-1.1.8.jar");
const QString jar = QStringLiteral("versions/1.20.1/1.20.1.jar");
// client.jar держим долго, чтобы все повторы библиотеки шли при живой установке.
FakeResponse slowJar = FakeResponse::bytes(mojang.expectedFiles("1.20.1").value(jar));
slowJar.delayMs = 4000;
mojang.overrideFile(jar, slowJar);
mojang.overrideFile(lib, FakeResponse::bytes("corrupted"));
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY2(run.failure().startsWith(Loc::text("common.error.downloadFailed")
.arg("com.mojang:brigadier:1.1.8", Loc::text("common.error.checksumMismatch"))),
qPrintable(run.failure()));
QCOMPARE(m_net.hits(mojang.fileUrl(lib)), 3); // kMaxAttempts
QVERIFY(!QFile::exists(m_game + u'/' + lib)); // битый файл не сохранён
}
void permanentErrorFailsImmediately()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
const QString lib = QStringLiteral("libraries/com/mojang/brigadier/1.1.8/brigadier-1.1.8.jar");
mojang.overrideFile(lib, FakeResponse::error(404));
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY(!run.failed.isEmpty());
QCOMPARE(m_net.hits(mojang.fileUrl(lib)), 1);
}
void cancelStopsDownloads()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
FakeResponse hang;
hang.hang = true;
mojang.overrideFile("versions/1.20.1/1.20.1.jar", hang);
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QTRY_VERIFY_WITH_TIMEOUT(m_net.hits(mojang.fileUrl("versions/1.20.1/1.20.1.jar")) == 1, 15000);
installer.cancel();
QVERIFY(run.waitEnd());
QCOMPARE(run.canceled.count(), 1);
QCOMPARE(run.canceled.constFirst().constFirst().toString(), QStringLiteral("1.20.1"));
QVERIFY(run.finished.isEmpty());
QVERIFY(!installer.isRunning());
QVERIFY(!QFile::exists(m_game + "/versions/1.20.1/1.20.1.jar"));
}
void queueRunsSequentiallyAndDeduplicates()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
mojang.publish({.id = "1.19.4", .releaseTime = "2023-03-14T12:00:00+00:00"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
installer.install(m_game, "1.19.4");
installer.install(m_game, "1.19.4"); // дубль в очереди
installer.install(m_game, "1.20.1"); // уже ставится
QVERIFY(installer.isQueued("1.19.4"));
QVERIFY(!installer.isQueued("1.20.1"));
QVERIFY(run.waitEnd(2));
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
QCOMPARE(run.finished.count(), 2);
QCOMPARE(run.started.count(), 2);
verifyInstalled(mojang, "1.19.4");
}
void offlineWithLocalJsonStillInstalls()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", mojang.versionJson("1.20.1"));
m_net.route(FakeMojang::manifestUrl(), FakeResponse::error(503));
VersionManifestService manifest;
VersionInstaller installer(&manifest);
Run run(installer);
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyInstalled(mojang, "1.20.1");
}
void fractionReportsProgress()
{
FakeMojang mojang(m_net);
mojang.publish({.id = "1.20.1"});
VersionManifestService manifest;
VersionInstaller installer(&manifest);
QCOMPARE(installer.fraction(), -1.0);
Run run(installer);
double maxFraction = -1;
connect(&installer, &VersionInstaller::progressChanged, this, [&] {
maxFraction = std::max(maxFraction, installer.fraction());
QVERIFY(installer.fraction() <= 1.0);
});
installer.install(m_game, "1.20.1");
QVERIFY(run.waitEnd());
QVERIFY(run.failed.isEmpty());
QVERIFY(maxFraction > 0.0);
}
};
BILAUNCHER_TEST_MAIN(TestVersionInstaller)
#include "tst_versioninstaller.moc"