Files
BiLauncher/tests/unit/tst_gamelauncher.cpp
T
galeon 41599fa82f
tests / linux (push) Failing after 2m18s
tests / windows (push) Skipped
tests / macos (push) Skipped
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped
new unit tests, hehe 67
Signed-off-by: galeon <dertiellis@gmail.com>
2026-10-12 13:14:08 +03:00

451 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "testenv.h"
#include "fixtures.h"
#include "gamelauncher.h"
#include "javalocator.h"
#include "localization.h"
#include "minecraftversion.h"
#include <QDir>
#include <QSignalSpy>
namespace {
#if defined(Q_OS_WIN)
const QChar kSep = u';';
const char *kNativeName = "lwjgl.dll";
#elif defined(Q_OS_MACOS)
const QChar kSep = u':';
const char *kNativeName = "liblwjgl.dylib";
#else
const QChar kSep = u':';
const char *kNativeName = "liblwjgl.so";
#endif
QString nat(const QString &path)
{
return QDir::toNativeSeparators(path);
}
MinecraftLibrary lib(const QString &name, const QString &path, bool native = false)
{
MinecraftLibrary library;
library.name = name;
library.path = path;
library.native = native;
return library;
}
} // namespace
class TestGameLauncher : public QObject
{
Q_OBJECT
QString m_game;
MinecraftVersion modernVersion() const
{
MinecraftVersion v;
v.id = QStringLiteral("1.20.1");
v.mainClass = QStringLiteral("net.minecraft.client.main.Main");
v.type = QStringLiteral("release");
v.assetIndexId = QStringLiteral("5");
v.assetsKind = QStringLiteral("5");
v.clientJarPath = m_game + QStringLiteral("/versions/1.20.1/1.20.1.jar");
v.javaMajor = 17;
v.jvmArgs = QStringList{"-Djava.library.path=${natives_directory}",
"-Dminecraft.launcher.brand=${launcher_name}",
"-Dminecraft.launcher.version=${launcher_version}",
"-Dunknown=${not_a_variable}",
"-cp", "${classpath}"};
v.gameArgs = QStringList{"--username", "${auth_player_name}", "--version", "${version_name}",
"--gameDir", "${game_directory}", "--assetsDir", "${assets_root}",
"--assetIndex", "${assets_index_name}", "--uuid", "${auth_uuid}",
"--accessToken", "${auth_access_token}", "--clientId", "${clientid}",
"--xuid", "${auth_xuid}", "--userType", "${user_type}",
"--versionType", "${version_type}", "--mystery", "${unknown_thing}"};
v.libraries << lib("com.mojang:brigadier:1.1.8", "com/mojang/brigadier-1.1.8.jar")
<< lib("org.lwjgl:lwjgl:3.3.1", "org/lwjgl/lwjgl-3.3.1.jar")
<< lib("org.lwjgl:lwjgl:3.3.1:natives", "org/lwjgl/lwjgl-3.3.1-natives.jar", true);
return v;
}
LaunchOptions options() const
{
LaunchOptions o;
o.gameDir = m_game;
o.versionId = QStringLiteral("1.20.1");
o.playerName = QStringLiteral("Steve");
o.uuid = QStringLiteral("0123456789abcdef0123456789abcdef");
o.accessToken = QStringLiteral("secret-token");
o.userType = QStringLiteral("msa");
o.minMemoryMb = 1024;
o.maxMemoryMb = 6144;
return o;
}
static QString valueAfter(const QStringList &args, const QString &flag)
{
const qsizetype i = args.indexOf(flag);
return (i >= 0 && i + 1 < args.size()) ? args.at(i + 1) : QString();
}
void createGameFiles(const MinecraftVersion &v)
{
Fixtures::write(v.clientJarPath, "client");
Fixtures::write(m_game + "/libraries/com/mojang/brigadier-1.1.8.jar", "lib");
Fixtures::write(m_game + "/libraries/org/lwjgl/lwjgl-3.3.1.jar", "lib");
Fixtures::makeZip(m_game + "/libraries/org/lwjgl/lwjgl-3.3.1-natives.jar",
{{QStringLiteral("META-INF/MANIFEST.MF"), "x"},
{QStringLiteral("linux/x64/org/lwjgl/") + kNativeName, "native-bytes"},
{QStringLiteral("skip/") + kNativeName + ".sha1", "hash"}});
Fixtures::write(m_game + "/assets/indexes/5.json", "{}");
QDir().mkpath(m_game + "/assets/objects");
}
private slots:
void init()
{
TestEnv::resetLauncherRoot();
m_game = TestEnv::freshDir(QStringLiteral("game"));
}
// --- buildArguments ---------------------------------------------------------
void substitutesVariables()
{
const MinecraftVersion v = modernVersion();
const QString natives = m_game + "/versions/1.20.1/natives";
const QStringList args = GameLauncher::buildArguments(options(), v, natives);
QCOMPARE(args.at(0), QStringLiteral("-Xms1024M"));
QCOMPARE(args.at(1), QStringLiteral("-Xmx6144M"));
QVERIFY(args.contains("-Djava.library.path=" + nat(natives)));
QVERIFY(args.contains("-Dminecraft.launcher.brand=BiLauncher"));
QVERIFY(args.contains(QStringLiteral("-Dminecraft.launcher.version=") + QStringLiteral(LAUNCHER_VERSION)));
// Неизвестный плейсхолдер — аргумент выбрасывается целиком.
QVERIFY(!args.join(' ').contains("not_a_variable"));
QVERIFY(!args.contains("${unknown_thing}"));
QVERIFY(args.contains("--mystery")); // сам флаг остаётся, как у официального лаунчера
// classpath: обычные библиотеки, без нативов, клиентский jar последним.
const QStringList cp = valueAfter(args, "-cp").split(kSep);
QCOMPARE(cp, QStringList({nat(m_game + "/libraries/com/mojang/brigadier-1.1.8.jar"),
nat(m_game + "/libraries/org/lwjgl/lwjgl-3.3.1.jar"),
nat(v.clientJarPath)}));
// mainClass — после jvm-аргументов, перед игровыми.
const qsizetype main = args.indexOf(v.mainClass);
QVERIFY(main > args.indexOf("-cp"));
QVERIFY(main < args.indexOf("--username"));
QCOMPARE(valueAfter(args, "--username"), QStringLiteral("Steve"));
QCOMPARE(valueAfter(args, "--version"), QStringLiteral("1.20.1"));
QCOMPARE(valueAfter(args, "--gameDir"), nat(m_game));
QCOMPARE(valueAfter(args, "--assetsDir"), nat(m_game + "/assets"));
QCOMPARE(valueAfter(args, "--assetIndex"), QStringLiteral("5"));
QCOMPARE(valueAfter(args, "--uuid"), QStringLiteral("0123456789abcdef0123456789abcdef"));
QCOMPARE(valueAfter(args, "--accessToken"), QStringLiteral("secret-token"));
QCOMPARE(valueAfter(args, "--clientId"), QStringLiteral("0")); // пустой → «0»
QCOMPARE(valueAfter(args, "--xuid"), QStringLiteral("0"));
QCOMPARE(valueAfter(args, "--userType"), QStringLiteral("msa"));
QCOMPARE(valueAfter(args, "--versionType"), QStringLiteral("release"));
QVERIFY(!args.contains("--width"));
QVERIFY(!args.contains("--fullscreen"));
QVERIFY(!args.contains("--server"));
}
void legacyAssetsAndSession()
{
MinecraftVersion v = modernVersion();
v.assetsKind = QStringLiteral("legacy");
v.gameArgs = QStringList{"--session", "${auth_session}", "--assetsDir", "${game_assets}"};
const QStringList args = GameLauncher::buildArguments(options(), v, m_game + "/n");
QCOMPARE(valueAfter(args, "--session"),
QStringLiteral("token:secret-token:0123456789abcdef0123456789abcdef"));
QCOMPARE(valueAfter(args, "--assetsDir"), nat(m_game + "/assets/virtual/legacy"));
v.assetsKind = QStringLiteral("pre-1.6");
QCOMPARE(valueAfter(GameLauncher::buildArguments(options(), v, m_game + "/n"), "--assetsDir"),
nat(m_game + "/assets/virtual/legacy"));
v.assetsKind = QStringLiteral("5");
QCOMPARE(valueAfter(GameLauncher::buildArguments(options(), v, m_game + "/n"), "--assetsDir"),
nat(m_game + "/assets"));
}
void memoryAndExtraJvmArgs()
{
LaunchOptions o = options();
o.minMemoryMb = 0;
o.maxMemoryMb = 0;
o.extraJvmArgs = QStringList{"-XX:+UseG1GC", "-Dfoo=bar"};
const QStringList args = GameLauncher::buildArguments(o, modernVersion(), m_game + "/n");
QVERIFY(!args.join(' ').contains("-Xms"));
QVERIFY(!args.join(' ').contains("-Xmx"));
QCOMPARE(args.at(0), QStringLiteral("-XX:+UseG1GC"));
QCOMPARE(args.at(1), QStringLiteral("-Dfoo=bar"));
}
void authlibInjectorGoesFirst()
{
LaunchOptions o = options();
o.authlibInjectorPath = m_game + "/authlib-injector.jar";
const QStringList args = GameLauncher::buildArguments(o, modernVersion(), m_game + "/n");
QCOMPARE(args.at(0), "-javaagent:" + nat(o.authlibInjectorPath) + "=ely.by");
QCOMPARE(args.at(1), QStringLiteral("-Dauthlibinjector.side=client"));
QCOMPARE(args.at(2), QStringLiteral("-Xms1024M"));
}
void windowSizeAndFullscreen()
{
LaunchOptions o = options();
o.windowWidth = 1280;
o.windowHeight = 720;
o.fullscreen = true;
MinecraftVersion v = modernVersion();
QStringList args = GameLauncher::buildArguments(o, v, m_game + "/n");
QCOMPARE(valueAfter(args, "--width"), QStringLiteral("1280"));
QCOMPARE(valueAfter(args, "--height"), QStringLiteral("720"));
QCOMPARE(args.count("--fullscreen"), 1);
// У версии свои аргументы разрешения — лаунчер свои не дублирует.
v.hasCustomResolutionArgs = true;
v.gameArgs << "--width" << "${resolution_width}" << "--height" << "${resolution_height}";
args = GameLauncher::buildArguments(o, v, m_game + "/n");
QCOMPARE(args.count("--width"), 1);
QCOMPARE(valueAfter(args, "--width"), QStringLiteral("1280"));
// Только ширина — не передаём ничего.
o.windowHeight = 0;
o.fullscreen = false;
args = GameLauncher::buildArguments(o, modernVersion(), m_game + "/n");
QVERIFY(!args.contains("--width"));
QVERIFY(!args.contains("--fullscreen"));
}
void serverAutoJoin_data()
{
QTest::addColumn<bool>("quickPlay");
QTest::addColumn<QString>("address");
QTest::addColumn<QStringList>("expectedTail");
QTest::newRow("quickplay") << true << "play.example.com:25570"
<< QStringList{"--quickPlayMultiplayer", "play.example.com:25570"};
QTest::newRow("legacy with port") << false << "play.example.com:25570"
<< QStringList{"--server", "play.example.com", "--port", "25570"};
QTest::newRow("legacy default port") << false << "play.example.com"
<< QStringList{"--server", "play.example.com", "--port", "25565"};
}
void serverAutoJoin()
{
QFETCH(bool, quickPlay);
QFETCH(QString, address);
QFETCH(QStringList, expectedTail);
LaunchOptions o = options();
o.serverAddress = address;
MinecraftVersion v = modernVersion();
v.supportsQuickPlay = quickPlay;
const QStringList args = GameLauncher::buildArguments(o, v, m_game + "/n");
QCOMPARE(args.mid(args.size() - expectedTail.size()), expectedTail);
}
void forgeIgnoreListGetsClientJar()
{
MinecraftVersion v = modernVersion();
v.jvmArgs.prepend("-DignoreList=bootstraplauncher,securejarhandler,${version_name}.jar");
QStringList args = GameLauncher::buildArguments(options(), v, m_game + "/n");
// ${version_name} = 1.20.1 совпадает с именем jar — дописывать нечего.
QCOMPARE(args.filter("-DignoreList=").constFirst(),
QStringLiteral("-DignoreList=bootstraplauncher,securejarhandler,1.20.1.jar"));
// Профиль Forge со своим id, а jar родительский — имя jar дописывается.
v.id = QStringLiteral("1.20.1-forge-47.4.0");
args = GameLauncher::buildArguments(options(), v, m_game + "/n");
QCOMPARE(args.filter("-DignoreList=").constFirst(),
QStringLiteral("-DignoreList=bootstraplauncher,securejarhandler,1.20.1-forge-47.4.0.jar,1.20.1.jar"));
}
void loggingArgumentOnlyWhenConfigExists()
{
MinecraftVersion v = modernVersion();
v.loggingArgument = QStringLiteral("-Dlog4j.configurationFile=${path}");
v.loggingConfigPath = m_game + "/assets/log_configs/client-1.12.xml";
QVERIFY(GameLauncher::buildArguments(options(), v, m_game + "/n").filter("log4j").isEmpty());
Fixtures::write(v.loggingConfigPath, "<xml/>");
const QStringList args = GameLauncher::buildArguments(options(), v, m_game + "/n");
QCOMPARE(args.filter("log4j"), QStringList{"-Dlog4j.configurationFile=" + nat(v.loggingConfigPath)});
QVERIFY(args.indexOf(args.filter("log4j").constFirst()) < args.indexOf(v.mainClass));
}
// --- missingFiles / extractNatives -------------------------------------------
void missingFiles()
{
const MinecraftVersion v = modernVersion();
LaunchOptions o = options();
o.gameDir = m_game + "/does-not-exist";
QCOMPARE(GameLauncher::missingFiles(o, v), QStringList{nat(o.gameDir)});
o = options();
QStringList missing = GameLauncher::missingFiles(o, v);
QVERIFY(missing.contains(nat(v.clientJarPath)));
QVERIFY(missing.contains(nat(m_game + "/libraries/com/mojang/brigadier-1.1.8.jar")));
QVERIFY(missing.contains(nat(m_game + "/assets/indexes/5.json")));
QCOMPARE(GameLauncher::missingFiles(o, v, 2).size(), 2);
createGameFiles(v);
QVERIFY(GameLauncher::missingFiles(o, v).isEmpty());
QDir(m_game + "/assets/objects").removeRecursively();
QCOMPARE(GameLauncher::missingFiles(o, v), QStringList{nat(m_game + "/assets/objects")});
}
void extractNatives()
{
MinecraftVersion v = modernVersion();
createGameFiles(v);
const QString natives = m_game + "/versions/1.20.1/natives";
QString error;
QVERIFY2(GameLauncher::extractNatives(options(), v, natives, &error), qPrintable(error));
// Только сама библиотека: без META-INF и без .sha1, в плоскую папку.
QCOMPARE(Fixtures::listTree(natives), QStringList{QString::fromLatin1(kNativeName)});
QCOMPARE(Fixtures::readFile(natives + u'/' + kNativeName), QByteArray("native-bytes"));
// Повторный запуск поверх готовой папки — успех.
QVERIFY(GameLauncher::extractNatives(options(), v, natives, &error));
// extractExclude по префиксу пути внутри архива.
QDir(natives).removeRecursively();
v.libraries[2].extractExclude = QStringList{"linux/"};
QVERIFY(GameLauncher::extractNatives(options(), v, natives, &error));
QVERIFY(Fixtures::listTree(natives).isEmpty());
// Архива нет вовсе.
const QString jar = m_game + "/libraries/org/lwjgl/lwjgl-3.3.1-natives.jar";
QFile::remove(jar);
error.clear();
QVERIFY(!GameLauncher::extractNatives(options(), v, natives, &error));
QCOMPARE(error, Loc::text("launch.error.nativeReadFailed").arg(nat(jar)));
// Битый архив: QZipReader::isReadable() проверяет только, что файл открылся,
// поэтому испорченный jar «распаковывается» в ничто без ошибки, и игра
// падает уже на загрузке LWJGL. Фиксируем текущее поведение.
Fixtures::write(jar, "not a zip");
error.clear();
QEXPECT_FAIL("", "битый jar с нативами не распознаётся как ошибка (QZipReader::isReadable)", Continue);
QVERIFY(!GameLauncher::extractNatives(options(), v, natives, &error));
}
// --- launch (с подставной java) ----------------------------------------------
void launchRunsJavaAndReportsExit()
{
MinecraftVersion v = modernVersion();
createGameFiles(v);
const QString argsFile = m_game + "/java-args.txt";
const QString java = Fixtures::installFakeJava(m_game + "/jdk", "17.0.8",
{"args_file=" + argsFile, "stdout=[main] Setting user: Steve",
"stdout=Game crashed? no", "exit=0"});
LaunchOptions o = options();
o.javaPath = java;
GameLauncher launcher;
QSignalSpy started(&launcher, &GameLauncher::gameStarted);
QSignalSpy finished(&launcher, &GameLauncher::gameFinished);
QSignalSpy output(&launcher, &GameLauncher::output);
QSignalSpy progress(&launcher, &GameLauncher::progress);
QString error;
QVERIFY2(launcher.launch(o, v, &error), qPrintable(error));
QCOMPARE(started.count(), 1);
// Токен скрыт в показанной строке запуска.
const QString shown = started.constFirst().constFirst().toString();
QVERIFY(!shown.contains("secret-token"));
QVERIFY(shown.contains("--accessToken"));
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 15000);
QCOMPARE(finished.constFirst().at(0).toInt(), 0);
QCOMPARE(finished.constFirst().at(1).toBool(), false);
QVERIFY(!launcher.isRunning());
QVERIFY(progress.count() >= 3);
QStringList lines;
for (const QList<QVariant> &call : output)
lines << call.constFirst().toString();
QVERIFY(lines.contains("[main] Setting user: Steve"));
// Java получила те же аргументы, что строит buildArguments, и в рабочей папке игры.
const QStringList received = QString::fromLocal8Bit(Fixtures::readFile(argsFile)).split(u'\n', Qt::SkipEmptyParts);
QCOMPARE(received, GameLauncher::buildArguments(o, v, m_game + "/versions/1.20.1/natives"));
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/natives/" + kNativeName));
}
void launchReportsCrash()
{
MinecraftVersion v = modernVersion();
createGameFiles(v);
LaunchOptions o = options();
o.javaPath = Fixtures::installFakeJava(m_game + "/jdk-crash", "17.0.8", {"crash=1"});
GameLauncher launcher;
QSignalSpy finished(&launcher, &GameLauncher::gameFinished);
QString error;
QVERIFY2(launcher.launch(o, v, &error), qPrintable(error));
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 15000);
QCOMPARE(finished.constFirst().at(1).toBool(), true);
}
void launchRefusesWhileRunningAndTerminate()
{
MinecraftVersion v = modernVersion();
createGameFiles(v);
LaunchOptions o = options();
o.javaPath = Fixtures::installFakeJava(m_game + "/jdk-slow", "17.0.8", {"sleep_ms=30000"});
GameLauncher launcher;
QSignalSpy finished(&launcher, &GameLauncher::gameFinished);
QString error;
QVERIFY(launcher.launch(o, v, &error));
QVERIFY(launcher.isRunning());
QVERIFY(!launcher.launch(o, v, &error));
QCOMPARE(error, Loc::text("common.error.gameRunning"));
launcher.terminate();
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 15000);
QVERIFY(!launcher.isRunning());
}
void launchFailsOnMissingFilesAndOldJava()
{
MinecraftVersion v = modernVersion();
LaunchOptions o = options();
GameLauncher launcher;
QSignalSpy started(&launcher, &GameLauncher::gameStarted);
QString error;
QVERIFY(!launcher.launch(o, v, &error));
QVERIFY(error.startsWith(Loc::text("launch.error.filesMissing").section(u'%', 0, 0)));
createGameFiles(v);
// На машине может стоять своя Java 17+: тогда лаунчер законно выберет её.
for (const QString &candidate : JavaLocator::findAll(m_game)) {
if (JavaLocator::majorVersion(candidate) >= 17)
QSKIP("в системе есть Java 17+, проверка «слишком старой Java» невозможна");
}
o.javaPath = Fixtures::installFakeJava(m_game + "/jdk8", "1.8.0_392");
error.clear();
QVERIFY(!launcher.launch(o, v, &error));
QCOMPARE(error, Loc::text("java.error.tooOld").arg(17).arg(8).arg(nat(o.javaPath)));
QCOMPARE(started.count(), 0);
}
};
BILAUNCHER_TEST_MAIN(TestGameLauncher)
#include "tst_gamelauncher.moc"