#include "testenv.h" #include "fixtures.h" #include "nettest.h" #include "javaruntime.h" #include "javaruntimeservice.h" #include "launcherpaths.h" #include "localization.h" #include #include #include 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 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 jdks = service.entries(JavaRuntimeKind::Jdk); const QList 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 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 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"