1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -0,0 +1,292 @@
|
||||
#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"
|
||||
Reference in new issue
Block a user