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

293 lines
12 KiB
C++

#include "testenv.h"
#include "fixtures.h"
#include "nettest.h"
#include "javainstaller.h"
#include "javalocator.h"
#include "javaruntime.h"
#include "launcherpaths.h"
#include "localization.h"
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <QSignalSpy>
namespace {
const QString kTemurin = QStringLiteral("https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21/");
const QString kMojangManifest = QStringLiteral("https://piston-meta.mojang.com/v1/packages/x/manifest.json");
const QString kMojangFiles = QStringLiteral("https://piston-data.mojang.com/v1/objects/");
JavaRuntimeEntry temurinEntry(const QString &archive, const QByteArray &data)
{
JavaRuntimeEntry entry;
entry.id = QStringLiteral("temurin-jdk-21.0.2_13-test-") + QString(archive).replace(u'.', u'-');
entry.kind = JavaRuntimeKind::Jdk;
entry.major = 21;
entry.version = QStringLiteral("21.0.2+13");
entry.archive = archive;
entry.url = QUrl(kTemurin + "OpenJDK21U-jdk." + archive);
entry.checksum = Fixtures::sha256(data);
entry.size = data.size();
return entry;
}
// Раскладка архива Temurin: один корневой каталог с bin/java внутри.
QMap<QString, QByteArray> jdkTree()
{
return {{QStringLiteral("jdk-21.0.2+13/bin/") + Fixtures::javaExecutableName(), "#!/bin/sh\necho java\n"},
{QStringLiteral("jdk-21.0.2+13/lib/modules"), QByteArray(2048, 'm')},
{QStringLiteral("jdk-21.0.2+13/release"), "JAVA_VERSION=\"21.0.2\""}};
}
} // namespace
class TestJavaInstaller : public QObject
{
Q_OBJECT
FakeInternet m_net;
QString m_work;
struct Run
{
explicit Run(JavaInstaller &installer)
: started(&installer, &JavaInstaller::started)
, finished(&installer, &JavaInstaller::finished)
, failed(&installer, &JavaInstaller::failed)
, canceled(&installer, &JavaInstaller::canceled)
{}
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(); }
QSignalSpy started, finished, failed, canceled;
};
void verifyRegistered(const JavaRuntimeEntry &entry, const Run &run)
{
QCOMPARE(run.finished.count(), 1);
QCOMPARE(run.finished.constFirst().at(0).toString(), entry.id);
const QString javaPath = run.finished.constFirst().at(1).toString();
QVERIFY(QFileInfo(javaPath).isFile());
QVERIFY(javaPath.startsWith(JavaRuntimeStore::dirFor(entry.id) + u'/'));
#if !defined(Q_OS_WIN)
QVERIFY(QFileInfo(javaPath).isExecutable());
#endif
const auto installed = JavaRuntimeStore::find(entry.id);
QVERIFY(installed.has_value());
QCOMPARE(installed->major, entry.major);
QCOMPARE(installed->version, entry.version);
QCOMPARE(installed->kind, javaKindKey(entry.kind));
// Лаунчер видит её при поиске Java.
QStringList canonical;
for (const QString &path : JavaLocator::findAll(m_work))
canonical << QFileInfo(path).canonicalFilePath();
QVERIFY(canonical.contains(QFileInfo(javaPath).canonicalFilePath()));
// Скачанный архив за собой не оставляем.
QVERIFY(QDir(LauncherPaths::javaDownloadDir()).entryList(QDir::Files).isEmpty());
}
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
void init()
{
TestEnv::resetLauncherRoot();
m_net.reset();
m_work = TestEnv::freshDir(QStringLiteral("game"));
}
void installsZipArchive()
{
const QByteArray zip = Fixtures::zipBytes(jdkTree());
const JavaRuntimeEntry entry = temurinEntry("zip", zip);
m_net.route(entry.url.toString(), FakeResponse::bytes(zip));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(installer.isRunning());
QCOMPARE(run.started.count(), 1);
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyRegistered(entry, run);
QVERIFY(!installer.isRunning());
}
void installsTarGzArchive()
{
#if defined(Q_OS_WIN)
QSKIP("Temurin под Windows — только zip; tar.gz распаковывается системным tar из PATH");
#endif
const QString archive = m_work + "/jdk.tar.gz";
if (!Fixtures::makeTarGz(archive, jdkTree()))
QSKIP("системный tar недоступен");
const QByteArray data = Fixtures::readFile(archive);
const JavaRuntimeEntry entry = temurinEntry("tar.gz", data);
m_net.route(entry.url.toString(), FakeResponse::bytes(data));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyRegistered(entry, run);
}
void archiveChecksumMismatch()
{
const QByteArray zip = Fixtures::zipBytes(jdkTree());
JavaRuntimeEntry entry = temurinEntry("zip", zip);
entry.checksum = QString(64, u'0');
m_net.route(entry.url.toString(), FakeResponse::bytes(zip));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("java.error.archiveChecksum"));
QVERIFY(!QDir(JavaRuntimeStore::dirFor(entry.id)).exists());
}
void archiveWithoutJavaFails()
{
const QByteArray zip = Fixtures::zipBytes({{"jdk/README", "no binaries here"}});
const JavaRuntimeEntry entry = temurinEntry("zip", zip);
m_net.route(entry.url.toString(), FakeResponse::bytes(zip));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("java.error.noExecutable"));
QVERIFY(!JavaRuntimeStore::find(entry.id).has_value());
}
void zipSlipIsRejected()
{
const QByteArray zip = Fixtures::zipBytes({{"jdk/../../../escaped", "x"}});
const JavaRuntimeEntry entry = temurinEntry("zip", zip);
m_net.route(entry.url.toString(), FakeResponse::bytes(zip));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("java.error.archiveBadPath").arg("jdk/../../../escaped"));
QVERIFY(!QFile::exists(LauncherPaths::rootDir() + "/escaped"));
}
void unknownFormatAndInvalidEntry()
{
JavaInstaller installer;
Run run(installer);
installer.install(JavaRuntimeEntry{});
QCOMPARE(run.failure(), Loc::text("java.error.buildUnavailable"));
JavaRuntimeEntry entry = temurinEntry("7z", "x");
Run run2(installer);
installer.install(entry);
QCOMPARE(run2.failure(), Loc::text("java.error.unknownArchiveFormat").arg("7z"));
}
void mojangComponent()
{
const QByteArray java = "#!/bin/sh\necho mojang-java\n";
const QByteArray lib = QByteArray(1024, 'l');
const QString binary = QStringLiteral("bin/") + Fixtures::javaExecutableName();
QJsonObject files{
{"bin", QJsonObject{{"type", "directory"}}},
{binary, QJsonObject{{"type", "file"}, {"executable", true},
{"downloads", QJsonObject{{"raw", QJsonObject{
{"url", kMojangFiles + Fixtures::sha1(java) + "/java"},
{"sha1", Fixtures::sha1(java)}, {"size", java.size()}}}}}}},
{"lib/modules", QJsonObject{{"type", "file"},
{"downloads", QJsonObject{{"raw", QJsonObject{
{"url", kMojangFiles + Fixtures::sha1(lib) + "/modules"},
{"sha1", Fixtures::sha1(lib)}, {"size", lib.size()}}}}}}},
{"lib/modules-link", QJsonObject{{"type", "link"}, {"target", "modules"}}}};
m_net.route(kMojangManifest, FakeResponse::json(QJsonObject{{"files", files}}));
m_net.route(kMojangFiles + Fixtures::sha1(java) + "/java", FakeResponse::bytes(java));
int libAttempts = 0;
m_net.route(kMojangFiles + Fixtures::sha1(lib) + "/modules", [&](const FakeRequest &) {
return ++libAttempts == 1 ? FakeResponse::error(503) : FakeResponse::bytes(lib); // временный сбой
});
JavaRuntimeEntry entry;
entry.id = QStringLiteral("mojang-java-runtime-delta");
entry.kind = JavaRuntimeKind::Mojang;
entry.major = 21;
entry.version = QStringLiteral("21.0.7");
entry.component = QStringLiteral("java-runtime-delta");
entry.archive = QStringLiteral("mojang");
entry.url = QUrl(kMojangManifest);
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
verifyRegistered(entry, run);
QCOMPARE(libAttempts, 2);
const QString dir = JavaRuntimeStore::dirFor(entry.id);
QCOMPARE(Fixtures::readFile(dir + "/lib/modules"), lib);
QVERIFY(QFileInfo::exists(dir + "/lib/modules-link"));
}
void mojangManifestEscapingIsRejected()
{
m_net.route(kMojangManifest, FakeResponse::json(QJsonObject{
{"files", QJsonObject{{"../../evil", QJsonObject{{"type", "directory"}}}}}}));
JavaRuntimeEntry entry;
entry.id = QStringLiteral("mojang-evil");
entry.kind = JavaRuntimeKind::Mojang;
entry.major = 17;
entry.version = QStringLiteral("17");
entry.archive = QStringLiteral("mojang");
entry.url = QUrl(kMojangManifest);
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QCOMPARE(run.failure(), Loc::text("java.error.manifestBadPath").arg("../../evil"));
}
void cancelDuringDownload()
{
FakeResponse hang;
hang.hang = true;
const JavaRuntimeEntry entry = temurinEntry("zip", "whatever");
m_net.route(entry.url.toString(), hang);
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QTRY_COMPARE_WITH_TIMEOUT(m_net.hits(entry.url.toString()), 1, 15000);
installer.cancel();
QVERIFY(run.waitEnd());
QVERIFY(!QDir(JavaRuntimeStore::dirFor(entry.id)).exists());
QVERIFY(!installer.isRunning());
QVERIFY(run.failed.isEmpty());
// abort() синхронно доводит ответ до handleArchiveFinished(), который уже
// шлёт canceled; затем cancel() видит m_reply == nullptr и шлёт его ещё раз.
QEXPECT_FAIL("", "canceled приходит дважды: из handleArchiveFinished() и из cancel()", Continue);
QCOMPARE(run.canceled.count(), 1);
}
void reinstallReplacesOld()
{
const QByteArray zip = Fixtures::zipBytes(jdkTree());
const JavaRuntimeEntry entry = temurinEntry("zip", zip);
Fixtures::write(JavaRuntimeStore::dirFor(entry.id) + "/stale/file", "old");
m_net.route(entry.url.toString(), FakeResponse::bytes(zip));
JavaInstaller installer;
Run run(installer);
installer.install(entry);
QVERIFY(run.waitEnd());
QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure()));
QVERIFY(!QFile::exists(JavaRuntimeStore::dirFor(entry.id) + "/stale/file"));
}
};
BILAUNCHER_TEST_MAIN(TestJavaInstaller)
#include "tst_javainstaller.moc"