320 lines
13 KiB
C++
320 lines
13 KiB
C++
#include "testenv.h"
|
|
#include "fixtures.h"
|
|
#include "nettest.h"
|
|
|
|
#include "javaruntime.h"
|
|
#include "javaruntimeservice.h"
|
|
#include "launcherpaths.h"
|
|
#include "localization.h"
|
|
|
|
#include <QJsonArray>
|
|
#include <QSignalSpy>
|
|
#include <QSysInfo>
|
|
|
|
namespace {
|
|
|
|
const QString kReleases = QStringLiteral("https://api.adoptium.net/v3/info/available_releases");
|
|
const QString kFeature = QStringLiteral("https://api.adoptium.net/v3/assets/feature_releases/*");
|
|
const QString kMojang = QStringLiteral(
|
|
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json");
|
|
|
|
bool VersionLoaderOsIsWindows()
|
|
{
|
|
#if defined(Q_OS_WIN)
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
QString hostAdoptiumArch()
|
|
{
|
|
const QString arch = QSysInfo::currentCpuArchitecture();
|
|
if (arch == QLatin1String("arm64"))
|
|
return QStringLiteral("aarch64");
|
|
if (arch == QLatin1String("x86_64"))
|
|
return QStringLiteral("x64");
|
|
return arch;
|
|
}
|
|
|
|
// Один релиз Temurin в формате Adoptium API.
|
|
QJsonObject release(int major, const QString &arch, const QString &openjdk, int security,
|
|
const QString ×tamp, const QString &os)
|
|
{
|
|
const QString ext = os == QLatin1String("windows") ? QStringLiteral("zip") : QStringLiteral("tar.gz");
|
|
QJsonArray binaries;
|
|
for (const QString type : {QStringLiteral("jdk"), QStringLiteral("jre"), QStringLiteral("testimage")}) {
|
|
const QString name = QStringLiteral("OpenJDK%1U-%2_%3_%4_hotspot.%5").arg(major).arg(type, arch, os, ext);
|
|
binaries << QJsonObject{
|
|
{"architecture", arch}, {"image_type", type}, {"os", os},
|
|
{"package", QJsonObject{{"name", name},
|
|
{"link", "https://github.com/adoptium/temurin/releases/download/x/" + name},
|
|
{"checksum", QString(64, u'c')},
|
|
{"size", 190000000}}}};
|
|
}
|
|
return QJsonObject{
|
|
{"release_name", QStringLiteral("jdk-%1").arg(openjdk)},
|
|
{"timestamp", timestamp},
|
|
{"binaries", binaries},
|
|
{"version_data", QJsonObject{{"major", major}, {"security", security},
|
|
{"openjdk_version", openjdk + "-LTS"}, {"semver", openjdk}}}};
|
|
}
|
|
|
|
QByteArray mojangAll()
|
|
{
|
|
const QJsonObject components{
|
|
{"java-runtime-delta", QJsonArray{QJsonObject{
|
|
{"manifest", QJsonObject{{"url", "https://piston-meta.mojang.com/v1/packages/d/manifest.json"},
|
|
{"sha1", "dddd"}, {"size", 1000}}},
|
|
{"version", QJsonObject{{"name", "21.0.7"}, {"released", "2025-04-15T00:00:00+00:00"}}}}}},
|
|
{"jre-legacy", QJsonArray{QJsonObject{
|
|
{"manifest", QJsonObject{{"url", "https://piston-meta.mojang.com/v1/packages/l/manifest.json"},
|
|
{"sha1", "llll"}, {"size", 1000}}},
|
|
{"version", QJsonObject{{"name", "8u51"}, {"released", "2015-07-15T00:00:00+00:00"}}}}}},
|
|
{"java-runtime-alpha", QJsonArray{}},
|
|
{"minecraft-java-exe", QJsonArray{QJsonObject{
|
|
{"manifest", QJsonObject{{"url", "https://piston-meta.mojang.com/v1/packages/e/manifest.json"}}},
|
|
{"version", QJsonObject{{"name", "7.0"}}}}}},
|
|
{"insecure", QJsonArray{QJsonObject{
|
|
{"manifest", QJsonObject{{"url", "http://piston-meta.mojang.com/x.json"}}},
|
|
{"version", QJsonObject{{"name", "17.0.1"}}}}}}};
|
|
QJsonObject root;
|
|
for (const char *platform : {"gamecore", "linux", "linux-i386", "mac-os", "mac-os-arm64",
|
|
"windows-arm64", "windows-x64", "windows-x86"})
|
|
root.insert(QLatin1String(platform), components);
|
|
return QJsonDocument(root).toJson(QJsonDocument::Compact);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class TestJavaRuntimeService : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
FakeInternet m_net;
|
|
|
|
// Отвечает релизами на запрошенную архитектуру/ОС; без «missingForArch» — для всех.
|
|
void routeAdoptium(const QString &missingForArch = QString())
|
|
{
|
|
m_net.route(kReleases, FakeResponse::json(QJsonObject{
|
|
{"available_lts_releases", QJsonArray{8, 17, 21}},
|
|
{"available_releases", QJsonArray{8, 17, 21, 24}}}));
|
|
m_net.route(kFeature, [missingForArch](const FakeRequest &request) {
|
|
const int major = request.path.section(u'/', -2, -2).toInt();
|
|
const QString arch = request.queryItems().queryItemValue("architecture");
|
|
const QString os = request.queryItems().queryItemValue("os");
|
|
if (arch == missingForArch)
|
|
return FakeResponse::json("[]");
|
|
QJsonArray list;
|
|
if (major == 8) {
|
|
list << release(8, arch, "1.8.0_452-b09", 452, "2025-04-20T00:00:00Z", os);
|
|
} else {
|
|
list << release(major, arch, QStringLiteral("%1.0.2+13").arg(major), 2, "2025-01-20T00:00:00Z", os)
|
|
<< release(major, arch, QStringLiteral("%1.0.1+12").arg(major), 1, "2024-10-20T00:00:00Z", os);
|
|
}
|
|
return FakeResponse::json(QJsonDocument(list).toJson());
|
|
});
|
|
}
|
|
|
|
static bool load(JavaRuntimeService &service, CallResult &r, bool force = false)
|
|
{
|
|
service.ensureLoaded(r.callback(), force);
|
|
return QTest::qWaitFor([&] { return r.calls > 0; }, 20000);
|
|
}
|
|
|
|
private slots:
|
|
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
|
|
|
|
void init()
|
|
{
|
|
TestEnv::resetLauncherRoot();
|
|
m_net.reset();
|
|
}
|
|
|
|
void mergesAdoptiumAndMojang()
|
|
{
|
|
routeAdoptium();
|
|
m_net.route(kMojang, FakeResponse::json(mojangAll()));
|
|
|
|
JavaRuntimeService service;
|
|
QSignalSpy changed(&service, &JavaRuntimeService::catalogChanged);
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY2(r.ok, qPrintable(r.warning));
|
|
QVERIFY(r.warning.isEmpty());
|
|
QCOMPARE(changed.count(), 1);
|
|
QVERIFY(!service.isRefreshing());
|
|
|
|
// Запросы к Adoptium — с параметрами текущей машины.
|
|
const QList<FakeRequest> requests = m_net.requests();
|
|
int featureRequests = 0;
|
|
for (const FakeRequest &request : requests) {
|
|
if (!request.path.startsWith("/v3/assets/feature_releases/"))
|
|
continue;
|
|
++featureRequests;
|
|
QCOMPARE(request.queryItems().queryItemValue("architecture"), hostAdoptiumArch());
|
|
QCOMPARE(request.queryItems().queryItemValue("image_type"), QString());
|
|
QCOMPARE(request.queryItems().queryItemValue("vendor"), QStringLiteral("eclipse"));
|
|
}
|
|
QCOMPARE(featureRequests, 4);
|
|
|
|
const QList<JavaRuntimeEntry> jdks = service.entries(JavaRuntimeKind::Jdk);
|
|
const QList<JavaRuntimeEntry> jres = service.entries(JavaRuntimeKind::Jre);
|
|
QCOMPARE(jdks.size(), 7); // 8 — один релиз, 17/21/24 — по два
|
|
QCOMPARE(jres.size(), 7);
|
|
// Свежие major первыми, внутри — по дате.
|
|
QCOMPARE(jdks.first().major, 24);
|
|
QCOMPARE(jdks.first().version, QStringLiteral("24.0.2+13"));
|
|
QCOMPARE(jdks.last().major, 8);
|
|
QCOMPARE(jdks.last().version, QStringLiteral("8u452"));
|
|
|
|
const JavaRuntimeEntry jdk21 = jdks.at(2);
|
|
QCOMPARE(jdk21.major, 21);
|
|
QCOMPARE(jdk21.version, QStringLiteral("21.0.2+13")); // «-LTS» отрезан
|
|
QVERIFY(jdk21.lts);
|
|
QVERIFY(!jdks.first().lts); // 24 — не LTS
|
|
QCOMPARE(jdk21.architecture, hostAdoptiumArch());
|
|
QCOMPARE(jdk21.size, qint64(190000000));
|
|
QCOMPARE(jdk21.archive, VersionLoaderOsIsWindows() ? QStringLiteral("zip") : QStringLiteral("tar.gz"));
|
|
QCOMPARE(jdk21.id, JavaRuntimeStore::sanitizeId("temurin-jdk-21.0.2+13-" + hostAdoptiumArch()));
|
|
QVERIFY(service.find(jdk21.id).has_value());
|
|
|
|
// Mojang: java-runtime-delta (21) и jre-legacy (8); пустые, exe и http — выброшены.
|
|
const QList<JavaRuntimeEntry> mojang = service.entries(JavaRuntimeKind::Mojang);
|
|
QCOMPARE(mojang.size(), 2);
|
|
const auto delta = service.find("mojang-java-runtime-delta");
|
|
QVERIFY(delta.has_value());
|
|
QCOMPARE(delta->major, 21);
|
|
QCOMPARE(delta->component, QStringLiteral("java-runtime-delta"));
|
|
QCOMPARE(delta->archive, QStringLiteral("mojang"));
|
|
QCOMPARE(delta->checksum, QStringLiteral("dddd"));
|
|
QCOMPARE(service.find("mojang-jre-legacy")->major, 8);
|
|
|
|
QVERIFY(QFile::exists(LauncherPaths::javaCatalogFile()));
|
|
}
|
|
|
|
void bestForPrefersSmallestSuitableMajorThenJdk()
|
|
{
|
|
routeAdoptium();
|
|
m_net.route(kMojang, FakeResponse::json(mojangAll()));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
|
|
QVERIFY(!service.bestFor(0).has_value());
|
|
QVERIFY(!service.bestFor(99).has_value());
|
|
const auto for17 = service.bestFor(17);
|
|
QVERIFY(for17.has_value());
|
|
QCOMPARE(for17->major, 17);
|
|
QCOMPARE(for17->kind, JavaRuntimeKind::Jdk);
|
|
QCOMPARE(for17->version, QStringLiteral("17.0.2+13")); // самый свежий релиз
|
|
QCOMPARE(service.bestFor(18)->major, 21);
|
|
QCOMPARE(service.bestFor(8)->major, 8);
|
|
}
|
|
|
|
void partialFailureStillProducesCatalog()
|
|
{
|
|
routeAdoptium();
|
|
m_net.route(kMojang, FakeResponse::error(500));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY(r.ok);
|
|
QVERIFY(r.warning.startsWith(Loc::text("java.error.mojangRuntimesFailed").section(u'%', 0, 0)));
|
|
QVERIFY(service.entries(JavaRuntimeKind::Mojang).isEmpty());
|
|
QVERIFY(!service.entries(JavaRuntimeKind::Jdk).isEmpty());
|
|
}
|
|
|
|
void adoptiumDownMojangUp()
|
|
{
|
|
m_net.route(kReleases, FakeResponse::error(503));
|
|
m_net.route(kMojang, FakeResponse::json(mojangAll()));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY(r.ok);
|
|
QVERIFY(!r.warning.isEmpty());
|
|
QCOMPARE(service.entries(JavaRuntimeKind::Mojang).size(), 2);
|
|
QVERIFY(service.entries(JavaRuntimeKind::Jdk).isEmpty());
|
|
}
|
|
|
|
void everythingDownWithoutCacheFails()
|
|
{
|
|
m_net.route(kReleases, FakeResponse::error(503));
|
|
m_net.route(kMojang, FakeResponse::error(503));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY(!r.ok);
|
|
QVERIFY(r.warning.contains("; ")); // оба предупреждения склеены
|
|
QVERIFY(!service.hasData());
|
|
}
|
|
|
|
void everythingDownWithCacheUsesCache()
|
|
{
|
|
routeAdoptium();
|
|
m_net.route(kMojang, FakeResponse::json(mojangAll()));
|
|
{
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
}
|
|
m_net.reset();
|
|
m_net.route(kReleases, FakeResponse::error(503));
|
|
m_net.route(kMojang, FakeResponse::error(503));
|
|
|
|
JavaRuntimeService service;
|
|
CallResult cached;
|
|
QVERIFY(load(service, cached));
|
|
QVERIFY(cached.ok);
|
|
QVERIFY(m_net.requests().isEmpty()); // кеш свежий — сети нет
|
|
QCOMPARE(service.entries(JavaRuntimeKind::Jdk).size(), 7);
|
|
|
|
CallResult forced;
|
|
QVERIFY(load(service, forced, true));
|
|
QVERIFY(forced.ok);
|
|
QVERIFY(!forced.warning.isEmpty());
|
|
QCOMPARE(service.entries(JavaRuntimeKind::Jdk).size(), 7);
|
|
}
|
|
|
|
void cacheForOtherPlatformIsIgnored()
|
|
{
|
|
QJsonObject entry;
|
|
entry["id"] = "x";
|
|
entry["kind"] = "jdk";
|
|
entry["major"] = 17;
|
|
entry["url"] = "https://x/y.zip";
|
|
QJsonObject cache;
|
|
cache["platform"] = "amiga-os";
|
|
cache["fetchedAt"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
|
cache["entries"] = QJsonArray{entry};
|
|
Fixtures::writeJson(LauncherPaths::javaCatalogFile(), cache);
|
|
m_net.route(kReleases, FakeResponse::error(503));
|
|
m_net.route(kMojang, FakeResponse::error(503));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY(!service.hasData());
|
|
QVERIFY(!r.ok);
|
|
}
|
|
|
|
void aarch64FallsBackToX64()
|
|
{
|
|
if (hostAdoptiumArch() != QLatin1String("aarch64"))
|
|
QSKIP("запасной x64 запрашивается только на ARM-машинах");
|
|
routeAdoptium(QStringLiteral("aarch64"));
|
|
m_net.route(kMojang, FakeResponse::json(mojangAll()));
|
|
JavaRuntimeService service;
|
|
CallResult r;
|
|
QVERIFY(load(service, r));
|
|
QVERIFY(r.ok);
|
|
const QList<JavaRuntimeEntry> jdks = service.entries(JavaRuntimeKind::Jdk);
|
|
QVERIFY(!jdks.isEmpty());
|
|
for (const JavaRuntimeEntry &entry : jdks)
|
|
QCOMPARE(entry.architecture, QStringLiteral("x64"));
|
|
}
|
|
};
|
|
|
|
BILAUNCHER_TEST_MAIN(TestJavaRuntimeService)
|
|
#include "tst_javaruntimeservice.moc"
|