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