Files

353 lines
17 KiB
C++
Raw Permalink Normal View History

2026-10-12 13:14:08 +03:00
#include "testenv.h"
#include "fixtures.h"
#include "localization.h"
#include "minecraftversion.h"
#include <QDir>
#include <QJsonArray>
#include <QSysInfo>
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<QByteArray>("rules");
QTest::addColumn<QStringList>("features");
QTest::addColumn<bool>("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<QString> 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", "<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<QString> 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<QString>("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"