#include "testenv.h" #include "fixtures.h" #include "localization.h" #include "minecraftversion.h" #include #include #include namespace { void writeVersion(const QString &gameDir, const QString &id, const QByteArray &json, bool withJar) { const QString dir = gameDir + "/versions/" + id; Fixtures::write(dir + u'/' + id + ".json", json); if (withJar) Fixtures::write(dir + u'/' + id + ".jar", "jar-" + id.toUtf8()); } QString otherOs() { return Fixtures::otherOsName(); } // Современный ванильный json (1.19+): arguments, natives-классификаторы, logging, quickPlay. QByteArray modernVanilla() { return Fixtures::readTemplate(QStringLiteral("versions/modern-1.20.1.json")); } } // namespace class TestMinecraftVersion : public QObject { Q_OBJECT QString m_game; private slots: void init() { m_game = TestEnv::freshDir(QStringLiteral("game")); } // --- правила --------------------------------------------------------------- void rulesAllow_data() { QTest::addColumn("rules"); QTest::addColumn("features"); QTest::addColumn("allowed"); QTest::newRow("no rules") << QByteArray("[]") << QStringList{} << true; QTest::newRow("allow all") << QByteArray("[{\"action\":\"allow\"}]") << QStringList{} << true; QTest::newRow("disallow all") << QByteArray("[{\"action\":\"disallow\"}]") << QStringList{} << false; QTest::newRow("allow this os") << QByteArray("[{\"action\":\"allow\",\"os\":{\"name\":\"%OS%\"}}]") << QStringList{} << true; QTest::newRow("allow other os only") << QByteArray("[{\"action\":\"allow\",\"os\":{\"name\":\"%OTHER%\"}}]") << QStringList{} << false; QTest::newRow("allow all but this os") << QByteArray("[{\"action\":\"allow\"},{\"action\":\"disallow\",\"os\":{\"name\":\"%OS%\"}}]") << QStringList{} << false; QTest::newRow("allow all but other os") << QByteArray("[{\"action\":\"allow\"},{\"action\":\"disallow\",\"os\":{\"name\":\"%OTHER%\"}}]") << QStringList{} << true; QTest::newRow("arch matches") << QByteArray("[{\"action\":\"allow\",\"os\":{\"arch\":\"%ARCH%\"}}]") << QStringList{} << true; QTest::newRow("arch differs") << QByteArray("[{\"action\":\"allow\",\"os\":{\"arch\":\"sparc\"}}]") << QStringList{} << false; QTest::newRow("os version regex any") << QByteArray("[{\"action\":\"allow\",\"os\":{\"version\":\".*\"}}]") << QStringList{} << true; QTest::newRow("os version regex none") << QByteArray("[{\"action\":\"allow\",\"os\":{\"version\":\"^nope$\"}}]") << QStringList{} << false; QTest::newRow("os version invalid regex") << QByteArray("[{\"action\":\"allow\",\"os\":{\"version\":\"(\"}}]") << QStringList{} << false; QTest::newRow("feature missing") << QByteArray("[{\"action\":\"allow\",\"features\":{\"is_demo_user\":true}}]") << QStringList{} << false; QTest::newRow("feature present") << QByteArray("[{\"action\":\"allow\",\"features\":{\"is_demo_user\":true}}]") << QStringList{"is_demo_user"} << true; QTest::newRow("feature must be absent") << QByteArray("[{\"action\":\"allow\",\"features\":{\"has_custom_resolution\":false}}]") << QStringList{} << true; QTest::newRow("last match wins") << QByteArray("[{\"action\":\"disallow\"},{\"action\":\"allow\"}]") << QStringList{} << true; } void rulesAllow() { QFETCH(QByteArray, rules); QFETCH(QStringList, features); QFETCH(bool, allowed); rules = Fixtures::applyTemplate(rules); const QSet set(features.cbegin(), features.cend()); QCOMPARE(VersionLoader::rulesAllow(QJsonDocument::fromJson(rules).array(), set), allowed); } void platformNames() { const QString os = VersionLoader::osName(); QVERIFY(QStringList({"windows", "osx", "linux"}).contains(os)); QVERIFY(!VersionLoader::osArch().isEmpty()); const QString classifier = VersionLoader::nativeClassifier(); QVERIFY(classifier.startsWith(QStringLiteral("natives-"))); #if defined(Q_OS_MACOS) QCOMPARE(os, QStringLiteral("osx")); QVERIFY(classifier.startsWith(QStringLiteral("natives-macos"))); #elif defined(Q_OS_WIN) QCOMPARE(os, QStringLiteral("windows")); #else QCOMPARE(os, QStringLiteral("linux")); #endif if (QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) QVERIFY(classifier.endsWith(QStringLiteral("-arm64"))); } // --- load ------------------------------------------------------------------- void loadModernVanilla() { writeVersion(m_game, "1.20.1", modernVanilla(), true); Fixtures::write(m_game + "/assets/log_configs/client-1.12.xml", ""); QString error; const MinecraftVersion v = VersionLoader::load(m_game, "1.20.1", {}, &error); QVERIFY2(error.isEmpty(), qPrintable(error)); QVERIFY(v.isValid()); QCOMPARE(v.id, QStringLiteral("1.20.1")); QCOMPARE(v.mainClass, QStringLiteral("net.minecraft.client.main.Main")); QCOMPARE(v.type, QStringLiteral("release")); QCOMPARE(v.assetIndexId, QStringLiteral("5")); QCOMPARE(v.assetsKind, QStringLiteral("5")); QCOMPARE(v.javaMajor, 17); QCOMPARE(v.clientJarPath, m_game + "/versions/1.20.1/1.20.1.jar"); QCOMPARE(v.loggingArgument, QStringLiteral("-Dlog4j.configurationFile=${path}")); QCOMPARE(v.loggingConfigPath, m_game + "/assets/log_configs/client-1.12.xml"); QVERIFY(v.supportsQuickPlay); QVERIFY(v.hasCustomResolutionArgs); // jvm: правило по ОС, своего -cp у версии есть — лаунчер своё не добавляет. QVERIFY(v.jvmArgs.contains("-Dthis.os=yes")); QVERIFY(!v.jvmArgs.contains("-Dother.os=yes")); QCOMPARE(v.jvmArgs.count("-cp"), 1); QVERIFY(!v.jvmArgs.contains("-Dminecraft.launcher.brand=${launcher_name}")); // game: без фич — ни --demo, ни --width, ни quickPlay. QVERIFY(v.gameArgs.contains("--username")); QVERIFY(!v.gameArgs.contains("--demo")); QVERIFY(!v.gameArgs.contains("--width")); QVERIFY(!v.gameArgs.contains("--quickPlayMultiplayer")); // Библиотеки: чужая ОС отфильтрована, натив только текущей платформы. QStringList names; for (const MinecraftLibrary &lib : v.libraries) names << lib.name; QVERIFY(names.contains("com.mojang:brigadier:1.1.8")); QVERIFY(names.contains("org.lwjgl:lwjgl:3.3.1")); QVERIFY(names.contains("org.lwjgl:lwjgl:3.3.1:" + VersionLoader::nativeClassifier())); QVERIFY(!names.contains("org.lwjgl:lwjgl:3.3.1:natives-other")); QVERIFY(!names.contains("only.other:lib:1.0")); for (const MinecraftLibrary &lib : v.libraries) { if (lib.name == "com.mojang:brigadier:1.1.8") { QCOMPARE(lib.path, QStringLiteral("com/mojang/brigadier/1.1.8/brigadier-1.1.8.jar")); QCOMPARE(lib.sha1, QStringLiteral("5244ce82c3337bba4a196a3ce858bfaecc74404a")); QCOMPARE(lib.size, qint64(77392)); QVERIFY(!lib.native); } if (lib.name.endsWith(VersionLoader::nativeClassifier())) QVERIFY(lib.native); } } void featuresEnableArguments() { writeVersion(m_game, "1.20.1", modernVanilla(), true); const QSet features{"has_custom_resolution", "is_quick_play_multiplayer", "is_demo_user"}; const MinecraftVersion v = VersionLoader::load(m_game, "1.20.1", features, nullptr); QVERIFY(v.gameArgs.contains("--demo")); const qsizetype width = v.gameArgs.indexOf("--width"); QVERIFY(width >= 0); QCOMPARE(v.gameArgs.at(width + 1), QStringLiteral("${resolution_width}")); QVERIFY(v.gameArgs.contains("--quickPlayMultiplayer")); } void legacyMinecraftArguments() { writeVersion(m_game, "1.7.10", Fixtures::read(QStringLiteral("versions/legacy-1.7.10.json")), true); const MinecraftVersion v = VersionLoader::load(m_game, "1.7.10", {}, nullptr); QVERIFY(v.isValid()); QCOMPARE(v.gameArgs, QStringList({"--username", "${auth_player_name}", "--session", "${auth_session}", "--gameDir", "${game_directory}"})); QCOMPARE(v.assetIndexId, QStringLiteral("1.7.10")); // нет assetIndex — берётся assets QCOMPARE(v.type, QStringLiteral("release")); // нет type — release QCOMPARE(v.javaMajor, 8); // нет javaVersion — 8 QVERIFY(!v.supportsQuickPlay); // Нет -cp в jvm — лаунчер добавляет стандартный набор. QVERIFY(v.jvmArgs.contains("-cp")); QVERIFY(v.jvmArgs.contains("${classpath}")); QVERIFY(v.jvmArgs.contains("-Djava.library.path=${natives_directory}")); QVERIFY(v.jvmArgs.contains("-Dminecraft.launcher.brand=${launcher_name}")); bool sawNative = false; for (const MinecraftLibrary &lib : v.libraries) { if (lib.name == "net.minecraft:launchwrapper:1.12") { // Без downloads: путь из координат, url = репозиторий + путь. QCOMPARE(lib.path, QStringLiteral("net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar")); QCOMPARE(lib.url, QStringLiteral("https://libraries.example/maven/net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar")); } if (lib.native) { sawNative = true; QCOMPARE(lib.extractExclude, QStringList{"META-INF/"}); #if defined(Q_OS_WIN) QVERIFY(lib.path == "lw/natives-windows-64.jar" || lib.path == "lw/natives-windows-32.jar"); #elif defined(Q_OS_MACOS) QCOMPARE(lib.path, QStringLiteral("lw/natives-osx.jar")); #else QCOMPARE(lib.path, QStringLiteral("lw/natives-linux.jar")); #endif } } QVERIFY(sawNative); } void inheritanceMergesChain() { writeVersion(m_game, "1.20.1", modernVanilla(), true); writeVersion(m_game, "fabric-loader-0.15.0-1.20.1", Fixtures::read(QStringLiteral("versions/fabric-loader-0.15.0-1.20.1.json")), false); QString error; const MinecraftVersion v = VersionLoader::load(m_game, "fabric-loader-0.15.0-1.20.1", {}, &error); QVERIFY2(error.isEmpty(), qPrintable(error)); QCOMPARE(v.id, QStringLiteral("fabric-loader-0.15.0-1.20.1")); QCOMPARE(v.mainClass, QStringLiteral("net.fabricmc.loader.impl.launch.knot.KnotClient")); QCOMPARE(v.javaMajor, 17); // от родителя QCOMPARE(v.assetIndexId, QStringLiteral("5")); // от родителя QCOMPARE(v.clientJarPath, m_game + "/versions/1.20.1/1.20.1.jar"); // своего jar нет QVERIFY(v.jvmArgs.contains("-DFabricMcEmu= net.minecraft.client.main.Main ")); QVERIFY(v.jvmArgs.contains("-Dthis.os=yes")); // jvm родителя QVERIFY(v.gameArgs.contains("--username")); // Дочерние библиотеки первыми и вытесняют те же group:artifact родителя. QCOMPARE(v.libraries.first().name, QStringLiteral("net.fabricmc:fabric-loader:0.15.0")); QStringList names; for (const MinecraftLibrary &lib : v.libraries) names << lib.name; QVERIFY(names.contains("com.mojang:brigadier:9.9.9")); QVERIFY(!names.contains("com.mojang:brigadier:1.1.8")); QCOMPARE(v.libraries.first().url, QStringLiteral("https://maven.fabricmc.net/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar")); } void childJarWinsOverParent() { writeVersion(m_game, "1.20.1", modernVanilla(), true); writeVersion(m_game, "custom", "{\"id\":\"custom\",\"inheritsFrom\":\"1.20.1\"}", true); const MinecraftVersion v = VersionLoader::load(m_game, "custom", {}, nullptr); QCOMPARE(v.clientJarPath, m_game + "/versions/custom/custom.jar"); QCOMPARE(v.mainClass, QStringLiteral("net.minecraft.client.main.Main")); } void loadErrors() { QString error; MinecraftVersion v = VersionLoader::load(m_game, "missing", {}, &error); QVERIFY(!v.isValid()); QVERIFY(error.contains(QDir::toNativeSeparators(m_game + "/versions/missing/missing.json"))); writeVersion(m_game, "broken", "{ nope", false); error.clear(); v = VersionLoader::load(m_game, "broken", {}, &error); QVERIFY(!v.isValid()); QVERIFY(!error.isEmpty()); writeVersion(m_game, "a", "{\"id\":\"a\",\"inheritsFrom\":\"b\",\"mainClass\":\"M\"}", false); writeVersion(m_game, "b", "{\"id\":\"b\",\"inheritsFrom\":\"a\"}", false); error.clear(); v = VersionLoader::load(m_game, "a", {}, &error); QVERIFY(!v.isValid()); QCOMPARE(error, Loc::text("version.error.inheritsCycle").arg("a")); // Цепочка длиннее 10 уровней. for (int i = 0; i < 12; ++i) writeVersion(m_game, QStringLiteral("v%1").arg(i), QStringLiteral("{\"inheritsFrom\":\"v%1\",\"mainClass\":\"M\"}").arg(i + 1).toUtf8(), false); writeVersion(m_game, "v12", "{\"mainClass\":\"M\"}", false); error.clear(); v = VersionLoader::load(m_game, "v0", {}, &error); QVERIFY(!v.isValid()); QCOMPARE(error, Loc::text("common.error.inheritsChainTooLong").arg("v0")); writeVersion(m_game, "nomain", "{\"id\":\"nomain\"}", false); error.clear(); v = VersionLoader::load(m_game, "nomain", {}, &error); QVERIFY(!v.isValid()); QCOMPARE(error, Loc::text("version.error.noMainClass").arg("nomain")); } // --- инвентарь -------------------------------------------------------------- void installedDependentsSizeRemove() { writeVersion(m_game, "1.20.1", modernVanilla(), true); writeVersion(m_game, "forge-1", "{\"inheritsFrom\":\"1.20.1\",\"mainClass\":\"F\"}", false); writeVersion(m_game, "fabric-1", "{\"inheritsFrom\":\"1.20.1\",\"mainClass\":\"F\"}", false); writeVersion(m_game, "other", "{\"inheritsFrom\":\"1.19\",\"mainClass\":\"F\"}", false); QDir().mkpath(m_game + "/versions/emptydir"); // без json — не версия QCOMPARE(VersionLoader::installedVersions(m_game), QStringList({"1.20.1", "fabric-1", "forge-1", "other"})); QStringList dependents = VersionLoader::dependentsOf(m_game, "1.20.1"); dependents.sort(); QCOMPARE(dependents, QStringList({"fabric-1", "forge-1"})); QVERIFY(VersionLoader::dependentsOf(m_game, QString()).isEmpty()); const qint64 expected = QFileInfo(m_game + "/versions/1.20.1/1.20.1.json").size() + QFileInfo(m_game + "/versions/1.20.1/1.20.1.jar").size(); QCOMPARE(VersionLoader::installedSize(m_game, "1.20.1"), expected); QCOMPARE(VersionLoader::installedSize(m_game, "../.."), qint64(0)); QString error; QVERIFY(VersionLoader::remove(m_game, "forge-1", &error)); QVERIFY(!QDir(m_game + "/versions/forge-1").exists()); QVERIFY(VersionLoader::remove(m_game, "forge-1", &error)); // уже нет — не ошибка QCOMPARE(VersionLoader::installedVersions(m_game), QStringList({"1.20.1", "fabric-1", "other"})); } void removeRejectsUnsafeIds_data() { QTest::addColumn("id"); QTest::newRow("empty") << ""; QTest::newRow("dotdot") << ".."; QTest::newRow("hidden") << ".git"; QTest::newRow("slash") << "a/b"; QTest::newRow("backslash") << "a\\b"; } void removeRejectsUnsafeIds() { QFETCH(QString, id); Fixtures::write(m_game + "/keep.txt", "keep"); QString error; QVERIFY(!VersionLoader::remove(m_game, id, &error)); QCOMPARE(error, Loc::text("version.error.invalidId")); QVERIFY(QFile::exists(m_game + "/keep.txt")); error.clear(); QVERIFY(!VersionLoader::remove(QString(), "1.20.1", &error)); } }; BILAUNCHER_TEST_MAIN(TestMinecraftVersion) #include "tst_minecraftversion.moc"