Files
BiLauncher/tests/net/tst_versioninstaller.cpp
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

384 lines
16 KiB
C++
Raw Permalink 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 "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"