1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -0,0 +1,16 @@
|
||||
# Модульные тесты: без сети, только файлы в песочнице и подставная java.
|
||||
foreach(test IN ITEMS
|
||||
tst_launcherpaths
|
||||
tst_javaruntime
|
||||
tst_modloader
|
||||
tst_localization
|
||||
tst_minecraftversion
|
||||
tst_authoffline
|
||||
tst_msaredirect
|
||||
tst_gamelauncher
|
||||
tst_javalocator
|
||||
tst_zlibreference
|
||||
tst_buildarchiveworker
|
||||
tst_buildswitcher)
|
||||
bilauncher_add_test(${test} LABEL unit SOURCES ${test}.cpp)
|
||||
endforeach()
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "testenv.h"
|
||||
|
||||
#include "authservice.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
class TestAuthOffline : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void emptyNicknameFails()
|
||||
{
|
||||
const AuthResult result = AuthService::offline(QString());
|
||||
QVERIFY(!result.ok);
|
||||
QCOMPARE(result.error, Loc::text("auth.offline.error.noNickname"));
|
||||
}
|
||||
|
||||
void uuidMatchesVanillaOfflineScheme()
|
||||
{
|
||||
// Так же UUID офлайн-игрока считает сервер в offline-mode:
|
||||
// UUID v3 от MD5("OfflinePlayer:<ник>").
|
||||
const AuthResult result = AuthService::offline(QStringLiteral("Notch"));
|
||||
QVERIFY(result.ok);
|
||||
QCOMPARE(result.playerName, QStringLiteral("Notch"));
|
||||
QCOMPARE(result.uuid, QStringLiteral("b50ad385829d3141a2167e7d7539ba7f"));
|
||||
QCOMPARE(result.accessToken, QStringLiteral("0"));
|
||||
QCOMPARE(result.clientToken, QStringLiteral("0"));
|
||||
QCOMPARE(result.userType, QStringLiteral("legacy"));
|
||||
}
|
||||
|
||||
void uuidIsStableAndWellFormed_data()
|
||||
{
|
||||
QTest::addColumn<QString>("nick");
|
||||
QTest::newRow("ascii") << "Steve";
|
||||
QTest::newRow("digits") << "player_123";
|
||||
QTest::newRow("cyrillic") << QStringLiteral("Игрок");
|
||||
}
|
||||
|
||||
void uuidIsStableAndWellFormed()
|
||||
{
|
||||
QFETCH(QString, nick);
|
||||
const AuthResult a = AuthService::offline(nick);
|
||||
const AuthResult b = AuthService::offline(nick);
|
||||
QCOMPARE(a.uuid, b.uuid);
|
||||
static const QRegularExpression hex(QStringLiteral("^[0-9a-f]{32}$"));
|
||||
QVERIFY(hex.match(a.uuid).hasMatch());
|
||||
QCOMPARE(a.uuid.at(12), QChar(u'3')); // версия 3
|
||||
QVERIFY(QStringLiteral("89ab").contains(a.uuid.at(16))); // вариант RFC 4122
|
||||
QVERIFY(AuthService::offline(nick + u'x').uuid != a.uuid);
|
||||
}
|
||||
|
||||
void clientTokenIsRandomUuid()
|
||||
{
|
||||
const QString a = AuthService::generateClientToken();
|
||||
const QString b = AuthService::generateClientToken();
|
||||
QVERIFY(a != b);
|
||||
QVERIFY(!QUuid::fromString(a).isNull());
|
||||
QVERIFY(!a.startsWith(u'{'));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestAuthOffline)
|
||||
#include "tst_authoffline.moc"
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "buildarchiveworker.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QSignalSpy>
|
||||
|
||||
class TestBuildArchiveWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_game;
|
||||
QString m_work;
|
||||
|
||||
void populateGame()
|
||||
{
|
||||
Fixtures::write(m_game + "/options.txt", "fov:90");
|
||||
Fixtures::write(m_game + "/mods/a.jar", "mod-a");
|
||||
Fixtures::write(m_game + "/mods/sub/b.jar", "mod-b");
|
||||
Fixtures::write(m_game + "/config/x/y.toml", "y=1");
|
||||
Fixtures::write(m_game + "/saves/World/level.dat", "level"); // общее — исключается
|
||||
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}"); // общее — исключается
|
||||
}
|
||||
|
||||
// Последний вызов finished(): (ok, error)
|
||||
static QPair<bool, QString> result(const QSignalSpy &spy)
|
||||
{
|
||||
if (spy.isEmpty())
|
||||
return {false, QStringLiteral("finished не пришёл")};
|
||||
return {spy.constLast().at(0).toBool(), spy.constLast().at(1).toString()};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
||||
m_work = TestEnv::freshDir(QStringLiteral("archives"));
|
||||
}
|
||||
|
||||
void archiveClearRestoreRoundTrip()
|
||||
{
|
||||
populateGame();
|
||||
const QStringList shared{"saves", "versions"};
|
||||
const QString zip = m_work + "/build.zip";
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
QSignalSpy progress(&worker, &BuildArchiveWorker::progress);
|
||||
|
||||
worker.archive(m_game, zip, shared);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::zipEntries(zip),
|
||||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar", "options.txt"}));
|
||||
// progress: (0,total) в начале и (total,total) в конце.
|
||||
QCOMPARE(progress.constFirst().at(0).toInt(), 0);
|
||||
QCOMPARE(progress.constLast().at(0).toInt(), 4);
|
||||
QCOMPARE(progress.constLast().at(1).toInt(), 4);
|
||||
|
||||
worker.clear(m_game, shared);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::listTree(m_game),
|
||||
QStringList({"saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||||
|
||||
worker.restore(zip, m_game);
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
QCOMPARE(Fixtures::listTree(m_game),
|
||||
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar",
|
||||
"options.txt", "saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/sub/b.jar"), QByteArray("mod-b"));
|
||||
}
|
||||
|
||||
void dotFilesSurviveRoundTrip()
|
||||
{
|
||||
// В .minecraft живут .fabric/, .mixin.out/, на macOS ещё .DS_Store.
|
||||
Fixtures::write(m_game + "/.fabric/remappedJars/x.jar", "cache");
|
||||
Fixtures::write(m_game + "/mods/a.jar", "a");
|
||||
const QString zip = m_work + "/dots.zip";
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.archive(m_game, zip, {});
|
||||
QVERIFY(result(finished).first);
|
||||
worker.clear(m_game, {});
|
||||
worker.restore(zip, m_game);
|
||||
QVERIFY(result(finished).first);
|
||||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||||
|
||||
// QZipReader при чтении срезает ведущие «.» и «/» в именах записей, поэтому
|
||||
// после переключения сборок .fabric/ возвращается как fabric/.
|
||||
QEXPECT_FAIL("", "QZipReader срезает ведущую точку: .fabric/ восстанавливается как fabric/", Continue);
|
||||
QVERIFY(QFile::exists(m_game + "/.fabric/remappedJars/x.jar"));
|
||||
}
|
||||
|
||||
void archiveMissingGameDir()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
const QString missing = m_game + "/nope";
|
||||
worker.archive(missing, m_work + "/x.zip", {});
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.gameDirMissing").arg(QDir::toNativeSeparators(missing))));
|
||||
}
|
||||
|
||||
void archiveCancelRemovesTempZip()
|
||||
{
|
||||
populateGame();
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.archive(m_game, m_work + "/cancel.zip", {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.archivingCancelled")));
|
||||
QVERIFY(!QFile::exists(m_work + "/cancel.zip"));
|
||||
|
||||
worker.clearCancel();
|
||||
worker.archive(m_game, m_work + "/ok.zip", {});
|
||||
QVERIFY(result(finished).first);
|
||||
}
|
||||
|
||||
void clearMissingDirIsOk()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.clear(m_game + "/nope", {});
|
||||
QVERIFY(result(finished).first);
|
||||
}
|
||||
|
||||
void restoreRejectsUnreadableAndUnsafeArchives()
|
||||
{
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
|
||||
const QString missing = m_work + "/missing.zip";
|
||||
worker.restore(missing, m_game);
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(missing))));
|
||||
|
||||
// Zip-slip: запись с «..» не должна вылезти из папки игры.
|
||||
const QString evil = m_work + "/evil.zip";
|
||||
// Ведущие «../» QZipReader срезает сам, а «..» в середине пути — нет.
|
||||
QVERIFY(Fixtures::makeZip(evil, {{"ok.txt", "ok"}, {"mods/../../escaped.txt", "pwned"}}));
|
||||
QVERIFY(Fixtures::zipEntries(evil).contains("mods/../../escaped.txt"));
|
||||
worker.restore(evil, m_game);
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.archiveBadPath").arg("mods/../../escaped.txt")));
|
||||
QVERIFY(!QFile::exists(QFileInfo(m_game).absolutePath() + "/escaped.txt"));
|
||||
}
|
||||
|
||||
void restoreCancel()
|
||||
{
|
||||
const QString zip = m_work + "/r.zip";
|
||||
QVERIFY(Fixtures::makeZip(zip, {{"a.txt", "a"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.restore(zip, m_game);
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.extractCancelled")));
|
||||
}
|
||||
|
||||
void applyPackRemovesThenWrites()
|
||||
{
|
||||
populateGame();
|
||||
Fixtures::write(m_game + "/mods/old/legacy.jar", "old");
|
||||
Fixtures::write(m_game + "/mods/keep.jar", "keep");
|
||||
|
||||
const QString pack = m_work + "/pack.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"},
|
||||
{"config/x/y.toml", "y=2"},
|
||||
{"options.txt", "fov:70"}}));
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
QSignalSpy entries(&worker, &BuildArchiveWorker::packEntries);
|
||||
worker.applyPack(pack, m_game,
|
||||
{"mods/old/legacy.jar", "mods/missing.jar",
|
||||
"../outside.txt", "versions/1.20.1/1.20.1.json"},
|
||||
{"versions", "saves"});
|
||||
QCOMPARE(result(finished), qMakePair(true, QString()));
|
||||
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/old/legacy.jar"));
|
||||
QVERIFY(!QDir(m_game + "/mods/old").exists()); // опустевшая папка убрана
|
||||
QVERIFY(QFile::exists(m_game + "/mods/keep.jar"));
|
||||
// Удаление в запретной зоне молча пропускается.
|
||||
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/config/x/y.toml"), QByteArray("y=2"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("fov:70"));
|
||||
|
||||
QCOMPARE(entries.count(), 1);
|
||||
QStringList applied = entries.constFirst().constFirst().toStringList();
|
||||
applied.sort();
|
||||
QCOMPARE(applied, QStringList({"config/x/y.toml", "mods/new.jar", "options.txt"}));
|
||||
}
|
||||
|
||||
void applyPackRefusesLauncherFolders()
|
||||
{
|
||||
populateGame();
|
||||
const QString pack = m_work + "/bad.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"versions/hack/hack.json", "{}"}}));
|
||||
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.applyPack(pack, m_game, {"mods/a.jar"}, {"versions"});
|
||||
QCOMPARE(result(finished),
|
||||
qMakePair(false, Loc::text("switch.error.packTouchesLauncher").arg("versions/hack/hack.json")));
|
||||
// Проверка до любых изменений: ничего не удалено и не записано.
|
||||
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/new.jar"));
|
||||
}
|
||||
|
||||
void applyPackRefusesUnsafePaths()
|
||||
{
|
||||
const QString pack = m_work + "/slip.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/../../../etc/x", "x"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.applyPack(pack, m_game, {}, {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.error.packBadPath").arg("mods/../../../etc/x")));
|
||||
}
|
||||
|
||||
void applyPackCancel()
|
||||
{
|
||||
const QString pack = m_work + "/c.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"a.txt", "a"}}));
|
||||
BuildArchiveWorker worker;
|
||||
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
|
||||
worker.requestCancel();
|
||||
worker.applyPack(pack, m_game, {}, {});
|
||||
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.installCancelled")));
|
||||
QVERIFY(!QFile::exists(m_game + "/a.txt"));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestBuildArchiveWorker)
|
||||
#include "tst_buildarchiveworker.moc"
|
||||
@@ -0,0 +1,302 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "buildswitcher.h"
|
||||
#include "launcherpaths.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QSignalSpy>
|
||||
|
||||
class TestBuildSwitcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_game;
|
||||
|
||||
static QJsonObject index()
|
||||
{
|
||||
return Fixtures::readJsonFile(LauncherPaths::buildStorageDir() + "/index.json");
|
||||
}
|
||||
|
||||
static QStringList zipsOf(int buildId)
|
||||
{
|
||||
return QDir(LauncherPaths::buildDir(buildId)).entryList({"*.zip"}, QDir::Files | QDir::Hidden);
|
||||
}
|
||||
|
||||
// Ждёт finished или failed; возвращает true при finished(expectedId).
|
||||
static bool waitDone(QSignalSpy &finished, QSignalSpy &failed, int expectedId)
|
||||
{
|
||||
if (!QTest::qWaitFor([&] { return finished.count() + failed.count() > 0; }, 20000)) {
|
||||
qWarning("ни finished, ни failed за 20 с");
|
||||
return false;
|
||||
}
|
||||
if (!failed.isEmpty()) {
|
||||
qWarning("failed: %s", qPrintable(failed.constFirst().at(1).toString()));
|
||||
return false;
|
||||
}
|
||||
return finished.constFirst().constFirst().toInt() == expectedId;
|
||||
}
|
||||
|
||||
void writeShared()
|
||||
{
|
||||
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}");
|
||||
Fixtures::write(m_game + "/libraries/x/y.jar", "lib");
|
||||
Fixtures::write(m_game + "/assets/indexes/5.json", "{}");
|
||||
Fixtures::write(m_game + "/launcher_profiles.json", "{}");
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
TestEnv::resetLauncherRoot();
|
||||
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
||||
}
|
||||
|
||||
void sharedTopLevelIsStable()
|
||||
{
|
||||
QStringList shared = BuildSwitcher::sharedTopLevel();
|
||||
shared.sort();
|
||||
QCOMPARE(shared, QStringList({"assets", "launcher_profiles.json", "libraries", "runtime", "versions"}));
|
||||
}
|
||||
|
||||
void switchBackAndForthKeepsEachBuildContent()
|
||||
{
|
||||
writeShared();
|
||||
Fixtures::write(m_game + "/mods/alpha.jar", "alpha");
|
||||
Fixtures::write(m_game + "/saves/A/level.dat", "A");
|
||||
Fixtures::write(m_game + "/options.txt", "alpha-options");
|
||||
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
|
||||
// 1 → 2: у сборки 2 архива ещё нет, папка игры очищается до общих файлов.
|
||||
switcher.switchTo(1, QStringLiteral("Alpha build"), 2, QStringLiteral("Beta"), m_game);
|
||||
QVERIFY(switcher.isRunning());
|
||||
QVERIFY(waitDone(finished, failed, 2));
|
||||
QVERIFY(!switcher.isRunning());
|
||||
QCOMPARE(Fixtures::listTree(m_game),
|
||||
QStringList({"assets/indexes/5.json", "launcher_profiles.json",
|
||||
"libraries/x/y.jar", "versions/1.20.1/1.20.1.json"}));
|
||||
QVERIFY(switcher.hasArchive(1));
|
||||
QVERIFY(!switcher.hasArchive(2));
|
||||
const QStringList zips = zipsOf(1);
|
||||
QCOMPARE(zips.size(), 1);
|
||||
QVERIFY(zips.constFirst().endsWith("_Alpha_build.zip")); // имя из названия сборки
|
||||
|
||||
QJsonObject idx = index();
|
||||
QCOMPARE(idx.value("state").toString(), QStringLiteral("idle"));
|
||||
QCOMPARE(idx.value("active").toInt(), 2);
|
||||
QCOMPARE(idx.value("builds").toObject().value("1").toObject().value("name").toString(),
|
||||
QStringLiteral("Alpha build"));
|
||||
|
||||
// Наполняем сборку 2 и возвращаемся в 1.
|
||||
Fixtures::write(m_game + "/mods/beta.jar", "beta");
|
||||
finished.clear();
|
||||
switcher.switchTo(2, QStringLiteral("Beta"), 1, QStringLiteral("Alpha build"), m_game);
|
||||
QVERIFY(waitDone(finished, failed, 1));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/alpha.jar"), QByteArray("alpha"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/saves/A/level.dat"), QByteArray("A"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("alpha-options"));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/beta.jar"));
|
||||
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json")); // общее не тронуто
|
||||
QVERIFY(switcher.hasArchive(2));
|
||||
|
||||
// И снова в 2 — его моды на месте.
|
||||
finished.clear();
|
||||
switcher.switchTo(1, QStringLiteral("Alpha build"), 2, QStringLiteral("Beta"), m_game);
|
||||
QVERIFY(waitDone(finished, failed, 2));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/beta.jar"), QByteArray("beta"));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/alpha.jar"));
|
||||
// У сборки хранится один актуальный архив, временных не остаётся.
|
||||
QCOMPARE(zipsOf(1).size(), 1);
|
||||
QVERIFY(zipsOf(1).filter(".tmp-").isEmpty());
|
||||
}
|
||||
|
||||
void switchWithoutSourceBuildOnlyClears()
|
||||
{
|
||||
writeShared();
|
||||
Fixtures::write(m_game + "/mods/stray.jar", "x");
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
switcher.switchTo(-1, QString(), 5, QStringLiteral("Five"), m_game);
|
||||
QVERIFY(waitDone(finished, failed, 5));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/stray.jar"));
|
||||
QVERIFY(QFile::exists(m_game + "/libraries/x/y.jar"));
|
||||
QVERIFY(QDir(LauncherPaths::buildStorageDir()).entryList({"*.zip"}).isEmpty());
|
||||
}
|
||||
|
||||
void secondSwitchWhileBusyIsRejected()
|
||||
{
|
||||
writeShared();
|
||||
for (int i = 0; i < 50; ++i)
|
||||
Fixtures::write(m_game + QStringLiteral("/mods/m%1.jar").arg(i), QByteArray(4096, 'x'));
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
switcher.switchTo(1, "A", 2, "B", m_game);
|
||||
switcher.switchTo(1, "A", 3, "C", m_game);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QCOMPARE(failed.constFirst().at(0).toInt(), 3);
|
||||
QCOMPARE(failed.constFirst().at(1).toString(), Loc::text("common.error.switchInProgress"));
|
||||
QCOMPARE(failed.constFirst().at(2).toBool(), true);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 20000);
|
||||
}
|
||||
|
||||
void interruptedSwitchIsReportedAndResumed()
|
||||
{
|
||||
writeShared();
|
||||
Fixtures::write(m_game + "/mods/half-cleared.jar", "x");
|
||||
// Архив сборки 7 записан, переключение 3 → 7 оборвалось посреди очистки.
|
||||
QVERIFY(Fixtures::makeZip(LauncherPaths::buildDir(7) + "/2026-01-01_Seven.zip",
|
||||
{{"mods/seven.jar", "seven"}, {"options.txt", "seven-options"}}));
|
||||
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
||||
QJsonObject{{"state", "switching"}, {"from", 3}, {"to", 7},
|
||||
{"builds", QJsonObject{{"7", QJsonObject{{"name", "Seven"},
|
||||
{"archive", "2026-01-01_Seven.zip"}}}}}});
|
||||
// Мусор от прерванной архивации подметается конструктором.
|
||||
Fixtures::write(LauncherPaths::buildDir(3) + "/.tmp-deadbeef.zip", "partial");
|
||||
|
||||
BuildSwitcher switcher;
|
||||
QVERIFY(!QFile::exists(LauncherPaths::buildDir(3) + "/.tmp-deadbeef.zip"));
|
||||
const QString warning = switcher.interruptedSwitchWarning();
|
||||
QCOMPARE(warning, Loc::text("switch.status.switchInterrupted")
|
||||
.arg(Loc::text("switch.status.targetSuffix").arg("Seven")));
|
||||
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
switcher.resumeInterrupted(m_game);
|
||||
QVERIFY(waitDone(finished, failed, 7));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/half-cleared.jar"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/seven.jar"), QByteArray("seven"));
|
||||
QCOMPARE(index().value("state").toString(), QStringLiteral("idle"));
|
||||
QVERIFY(switcher.interruptedSwitchWarning().isEmpty());
|
||||
}
|
||||
|
||||
void applyPackAndRecordNote()
|
||||
{
|
||||
writeShared();
|
||||
Fixtures::write(m_game + "/mods/old.jar", "old");
|
||||
const QString pack = LauncherPaths::buildDir(9) + "/season-pack.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"config/c.toml", "c"}}));
|
||||
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
const QJsonObject note{{"seasonalId", "season-5"}, {"revision", 3}};
|
||||
switcher.applyPack(9, QStringLiteral("Season 5"), pack, {"mods/old.jar"}, note, m_game);
|
||||
QVERIFY(waitDone(finished, failed, 9));
|
||||
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/old.jar"));
|
||||
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
|
||||
QVERIFY(!QFile::exists(pack)); // пакет удаляется после установки
|
||||
QStringList entries = switcher.lastPackEntries();
|
||||
entries.sort();
|
||||
QCOMPARE(entries, QStringList({"config/c.toml", "mods/new.jar"}));
|
||||
QCOMPARE(switcher.lastPackNote(), note);
|
||||
const QJsonObject idx = index();
|
||||
QCOMPARE(idx.value("state").toString(), QStringLiteral("idle"));
|
||||
QCOMPARE(idx.value("active").toInt(), 9);
|
||||
QVERIFY(!idx.contains("pack"));
|
||||
}
|
||||
|
||||
void applyPackErrors()
|
||||
{
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
const QString missing = LauncherPaths::buildDir(4) + "/nope.zip";
|
||||
switcher.applyPack(4, "Four", missing, {}, {}, m_game);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QCOMPARE(failed.constFirst().at(1).toString(),
|
||||
Loc::text("switch.error.archiveMissing").arg(QDir::toNativeSeparators(missing)));
|
||||
|
||||
// Пакет лезет в общие папки лаунчера — отказ, папка игры цела.
|
||||
const QString bad = LauncherPaths::buildDir(4) + "/bad.zip";
|
||||
QVERIFY(Fixtures::makeZip(bad, {{"versions/evil/evil.json", "{}"}}));
|
||||
failed.clear();
|
||||
switcher.applyPack(4, "Four", bad, {}, {}, m_game);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(failed.count(), 1, 20000);
|
||||
QCOMPARE(failed.constFirst().at(1).toString(),
|
||||
Loc::text("switch.error.packTouchesLauncher").arg("versions/evil/evil.json"));
|
||||
QCOMPARE(failed.constFirst().at(2).toBool(), true);
|
||||
}
|
||||
|
||||
void interruptedApplyIsResumedOrCleaned()
|
||||
{
|
||||
writeShared();
|
||||
const QString pack = LauncherPaths::buildDir(11) + "/pack.zip";
|
||||
QVERIFY(Fixtures::makeZip(pack, {{"mods/resumed.jar", "r"}}));
|
||||
const QJsonObject note{{"k", "v"}};
|
||||
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
||||
QJsonObject{{"state", "applying"}, {"to", 11}, {"pack", "pack.zip"},
|
||||
{"packName", "Eleven"}, {"packRemove", QJsonArray{"mods/gone.jar"}},
|
||||
{"packNote", note}});
|
||||
Fixtures::write(m_game + "/mods/gone.jar", "g");
|
||||
|
||||
{
|
||||
BuildSwitcher switcher;
|
||||
QCOMPARE(switcher.interruptedSwitchWarning(),
|
||||
Loc::text("switch.status.applyInterrupted")
|
||||
.arg(Loc::text("switch.status.nameSuffix").arg("Eleven")));
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
switcher.resumeInterrupted(m_game);
|
||||
QVERIFY(waitDone(finished, failed, 11));
|
||||
QVERIFY(QFile::exists(m_game + "/mods/resumed.jar"));
|
||||
QVERIFY(!QFile::exists(m_game + "/mods/gone.jar"));
|
||||
QCOMPARE(switcher.lastPackNote(), note);
|
||||
}
|
||||
|
||||
// Пакет пропал — индекс просто приводится в порядок, без сигналов.
|
||||
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
||||
QJsonObject{{"state", "applying"}, {"to", 12}, {"pack", "lost.zip"}});
|
||||
BuildSwitcher switcher;
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy failed(&switcher, &BuildSwitcher::failed);
|
||||
switcher.resumeInterrupted(m_game);
|
||||
QCOMPARE(index().value("state").toString(), QStringLiteral("idle"));
|
||||
QVERIFY(!index().contains("pack"));
|
||||
QCOMPARE(finished.count() + failed.count(), 0);
|
||||
}
|
||||
|
||||
void forgetBuild()
|
||||
{
|
||||
QVERIFY(Fixtures::makeZip(LauncherPaths::buildDir(5) + "/a.zip", {{"x", "y"}}));
|
||||
Fixtures::writeJson(LauncherPaths::buildStorageDir() + "/index.json",
|
||||
QJsonObject{{"state", "idle"}, {"active", 5},
|
||||
{"builds", QJsonObject{{"5", QJsonObject{{"name", "Five"}}},
|
||||
{"6", QJsonObject{{"name", "Six"}}}}}});
|
||||
BuildSwitcher switcher;
|
||||
QVERIFY(switcher.hasArchive(5));
|
||||
QVERIFY(switcher.forgetBuild(5));
|
||||
QVERIFY(!QDir(LauncherPaths::buildDir(5)).exists());
|
||||
QVERIFY(!switcher.hasArchive(5));
|
||||
const QJsonObject idx = index();
|
||||
QVERIFY(!idx.contains("active"));
|
||||
QVERIFY(!idx.value("builds").toObject().contains("5"));
|
||||
QVERIFY(idx.value("builds").toObject().contains("6"));
|
||||
QVERIFY(switcher.forgetBuild(-1));
|
||||
}
|
||||
|
||||
void fractionAndStage()
|
||||
{
|
||||
BuildSwitcher switcher;
|
||||
QCOMPARE(switcher.fraction(), -1.0);
|
||||
QVERIFY(switcher.stage().isEmpty());
|
||||
writeShared();
|
||||
Fixtures::write(m_game + "/mods/a.jar", "a");
|
||||
QSignalSpy finished(&switcher, &BuildSwitcher::finished);
|
||||
QSignalSpy progress(&switcher, &BuildSwitcher::progressChanged);
|
||||
switcher.switchTo(1, "A", 2, "B", m_game);
|
||||
QCOMPARE(switcher.stage(), Loc::text("switch.progress.saving").arg("A"));
|
||||
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 20000);
|
||||
QVERIFY(progress.count() > 2);
|
||||
QCOMPARE(switcher.fraction(), -1.0);
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestBuildSwitcher)
|
||||
#include "tst_buildswitcher.moc"
|
||||
@@ -0,0 +1,450 @@
|
||||
#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"
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "javalocator.h"
|
||||
#include "javaruntime.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
class TestJavaLocator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_game;
|
||||
int m_unique = 0;
|
||||
|
||||
// majorVersion() кеширует ответ по пути, поэтому каждая подставная java — в своей папке.
|
||||
QString fakeJava(const QString &version)
|
||||
{
|
||||
return Fixtures::installFakeJava(m_game + QStringLiteral("/jdk-%1").arg(++m_unique), version);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
TestEnv::resetLauncherRoot();
|
||||
m_game = TestEnv::freshDir(QStringLiteral("game"));
|
||||
}
|
||||
|
||||
void majorVersion_data()
|
||||
{
|
||||
QTest::addColumn<QString>("version");
|
||||
QTest::addColumn<int>("major");
|
||||
QTest::newRow("java8") << "1.8.0_392" << 8;
|
||||
QTest::newRow("java11") << "11.0.21" << 11;
|
||||
QTest::newRow("java17") << "17.0.8" << 17;
|
||||
QTest::newRow("java21 short") << "21" << 21;
|
||||
QTest::newRow("java25 ea") << "25-ea" << 25;
|
||||
}
|
||||
|
||||
void majorVersion()
|
||||
{
|
||||
QFETCH(QString, version);
|
||||
QFETCH(int, major);
|
||||
QCOMPARE(JavaLocator::majorVersion(fakeJava(version)), major);
|
||||
}
|
||||
|
||||
void majorVersionOfNonJava()
|
||||
{
|
||||
QCOMPARE(JavaLocator::majorVersion(m_game + QStringLiteral("/nope/java")), 0);
|
||||
}
|
||||
|
||||
void windowlessVariant()
|
||||
{
|
||||
const QString java = fakeJava("17.0.8");
|
||||
#if defined(Q_OS_WIN)
|
||||
QCOMPARE(JavaLocator::windowlessVariant(java), QFileInfo(java).absolutePath() + "/javaw.exe");
|
||||
QFile::remove(QFileInfo(java).absolutePath() + "/javaw.exe");
|
||||
QCOMPARE(JavaLocator::windowlessVariant(java), java);
|
||||
#else
|
||||
QCOMPARE(JavaLocator::windowlessVariant(java), java);
|
||||
#endif
|
||||
QCOMPARE(JavaLocator::windowlessVariant(QStringLiteral("/opt/custom/launcher")),
|
||||
QStringLiteral("/opt/custom/launcher"));
|
||||
}
|
||||
|
||||
void findAllSeesManagedAndGameRuntimes()
|
||||
{
|
||||
// Java, поставленная лаунчером (JavaRuntimeStore).
|
||||
InstalledJavaRuntime managed;
|
||||
managed.id = QStringLiteral("temurin-jdk-21");
|
||||
managed.kind = QStringLiteral("jdk");
|
||||
managed.major = 21;
|
||||
managed.version = QStringLiteral("21.0.2");
|
||||
managed.javaPath = Fixtures::installFakeJava(JavaRuntimeStore::dirFor(managed.id), "21.0.2");
|
||||
QVERIFY(JavaRuntimeStore::writeMeta(managed, nullptr));
|
||||
|
||||
// Java официального лаунчера внутри .minecraft/runtime.
|
||||
const QString mojang = Fixtures::installFakeJava(
|
||||
m_game + "/runtime/java-runtime-gamma/linux/java-runtime-gamma", "17.0.8");
|
||||
|
||||
const QStringList all = JavaLocator::findAll(m_game);
|
||||
QVERIFY(all.size() >= 2);
|
||||
QCOMPARE(QFileInfo(all.at(0)).canonicalFilePath(), QFileInfo(managed.javaPath).canonicalFilePath());
|
||||
bool hasMojang = false;
|
||||
for (const QString &path : all)
|
||||
hasMojang |= QFileInfo(path).canonicalFilePath() == QFileInfo(mojang).canonicalFilePath();
|
||||
QVERIFY(hasMojang);
|
||||
QStringList unique = all;
|
||||
QCOMPARE(unique.removeDuplicates(), 0);
|
||||
}
|
||||
|
||||
void selectPrefersPreferredWhenSuitable()
|
||||
{
|
||||
const QString java = fakeJava("21.0.2");
|
||||
QString error;
|
||||
QCOMPARE(JavaLocator::select(m_game, 17, java, &error), QFileInfo(java).absoluteFilePath());
|
||||
QVERIFY(error.isEmpty());
|
||||
|
||||
// Папка JDK тоже годится: берётся bin/java.
|
||||
const QString fromDir = JavaLocator::select(m_game, 17, QFileInfo(java).absolutePath() + "/..", &error);
|
||||
QCOMPARE(QFileInfo(fromDir).canonicalFilePath(), QFileInfo(java).canonicalFilePath());
|
||||
}
|
||||
|
||||
void selectReportsMissingPreferred()
|
||||
{
|
||||
QString error;
|
||||
const QString missing = m_game + "/no-such-java";
|
||||
QVERIFY(JavaLocator::select(m_game, 17, missing, &error).isEmpty());
|
||||
QCOMPARE(error, Loc::text("java.error.notAtPath").arg(QDir::toNativeSeparators(missing)));
|
||||
}
|
||||
|
||||
void selectFallsBackToManagedRuntime()
|
||||
{
|
||||
InstalledJavaRuntime managed;
|
||||
managed.id = QStringLiteral("temurin-jdk-21b");
|
||||
managed.kind = QStringLiteral("jdk");
|
||||
managed.major = 21;
|
||||
managed.version = QStringLiteral("21.0.2");
|
||||
managed.javaPath = Fixtures::installFakeJava(JavaRuntimeStore::dirFor(managed.id), "21.0.2");
|
||||
QVERIFY(JavaRuntimeStore::writeMeta(managed, nullptr));
|
||||
|
||||
const QString old = fakeJava("1.8.0_392");
|
||||
QString error;
|
||||
const QString chosen = JavaLocator::select(m_game, 21, old, &error);
|
||||
QCOMPARE(QFileInfo(chosen).canonicalFilePath(), QFileInfo(managed.javaPath).canonicalFilePath());
|
||||
}
|
||||
|
||||
void selectExplainsTooOld()
|
||||
{
|
||||
for (const QString &candidate : JavaLocator::findAll(m_game)) {
|
||||
if (JavaLocator::majorVersion(candidate) >= 99)
|
||||
QSKIP("в системе есть Java 99+");
|
||||
}
|
||||
const QString old = fakeJava("17.0.8");
|
||||
QString error;
|
||||
QVERIFY(JavaLocator::select(m_game, 99, old, &error).isEmpty());
|
||||
// Сообщение называет лучшую из найденных — она не ниже нашей 17.
|
||||
QVERIFY(!error.isEmpty());
|
||||
QVERIFY(error.contains(QStringLiteral("99")));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestJavaLocator)
|
||||
#include "tst_javalocator.moc"
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "javaruntime.h"
|
||||
#include "launcherpaths.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
class TestJavaRuntime : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
static QString fakeBinary(const QString &root, const QString &relativeBinDir)
|
||||
{
|
||||
const QString path = root + u'/' + relativeBinDir + u'/' + Fixtures::javaExecutableName();
|
||||
Fixtures::write(path, "#!/bin/sh\n");
|
||||
return path;
|
||||
}
|
||||
|
||||
static InstalledJavaRuntime makeRuntime(const QString &id, int major, const QString &version)
|
||||
{
|
||||
InstalledJavaRuntime runtime;
|
||||
runtime.id = id;
|
||||
runtime.kind = QStringLiteral("jdk");
|
||||
runtime.major = major;
|
||||
runtime.version = version;
|
||||
runtime.size = 12345;
|
||||
runtime.javaPath = fakeBinary(JavaRuntimeStore::dirFor(id), QStringLiteral("jdk/bin"));
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init() { TestEnv::resetLauncherRoot(); }
|
||||
|
||||
void kindKeysRoundTrip()
|
||||
{
|
||||
for (JavaRuntimeKind kind : {JavaRuntimeKind::Mojang, JavaRuntimeKind::Jdk, JavaRuntimeKind::Jre}) {
|
||||
const auto back = javaKindFromKey(javaKindKey(kind));
|
||||
QVERIFY(back.has_value());
|
||||
QCOMPARE(*back, kind);
|
||||
QVERIFY(!javaKindTitle(kind).isEmpty());
|
||||
}
|
||||
QCOMPARE(javaKindKey(JavaRuntimeKind::Mojang), QStringLiteral("java"));
|
||||
QVERIFY(!javaKindFromKey(QStringLiteral("graalvm")).has_value());
|
||||
QVERIFY(!javaKindFromKey(QString()).has_value());
|
||||
}
|
||||
|
||||
void entryValidity()
|
||||
{
|
||||
JavaRuntimeEntry entry;
|
||||
QVERIFY(!entry.isValid());
|
||||
entry.id = QStringLiteral("temurin-jdk-21");
|
||||
entry.major = 21;
|
||||
QVERIFY(!entry.isValid());
|
||||
entry.url = QUrl(QStringLiteral("https://example.org/jdk.zip"));
|
||||
QVERIFY(entry.isValid());
|
||||
}
|
||||
|
||||
void sanitizeId_data()
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<QString>("expected");
|
||||
QTest::newRow("plain") << "temurin-jdk-21.0.2_13" << "temurin-jdk-21.0.2_13";
|
||||
QTest::newRow("plus") << "jdk-21.0.2+13" << "jdk-21.0.2_13";
|
||||
QTest::newRow("slashes") << "../../etc/passwd" << "_.._etc_passwd";
|
||||
QTest::newRow("backslash") << "a\\b" << "a_b";
|
||||
QTest::newRow("leadingDots") << "...hidden" << "hidden";
|
||||
QTest::newRow("onlyDots") << ".." << "";
|
||||
QTest::newRow("empty") << "" << "";
|
||||
}
|
||||
|
||||
void sanitizeId()
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(QString, expected);
|
||||
QCOMPARE(JavaRuntimeStore::sanitizeId(input), expected);
|
||||
}
|
||||
|
||||
void dirForStaysInsideJavaDir()
|
||||
{
|
||||
QCOMPARE(JavaRuntimeStore::dirFor(QStringLiteral("jdk-17")),
|
||||
LauncherPaths::javaDir() + QStringLiteral("/jdk-17"));
|
||||
QVERIFY(JavaRuntimeStore::dirFor(QStringLiteral("..")).isEmpty());
|
||||
QVERIFY(JavaRuntimeStore::dirFor(QStringLiteral("../x")).startsWith(LauncherPaths::javaDir() + u'/'));
|
||||
}
|
||||
|
||||
void locateBinaryPrefersShortestBinPath()
|
||||
{
|
||||
const QString root = TestEnv::freshDir(QStringLiteral("locate"));
|
||||
QVERIFY(JavaRuntimeStore::locateBinary(root).isEmpty());
|
||||
QVERIFY(JavaRuntimeStore::locateBinary(root + QStringLiteral("/missing")).isEmpty());
|
||||
QVERIFY(JavaRuntimeStore::locateBinary(QString()).isEmpty());
|
||||
|
||||
// java вне папки bin не считается
|
||||
Fixtures::write(root + u'/' + Fixtures::javaExecutableName(), "x");
|
||||
QVERIFY(JavaRuntimeStore::locateBinary(root).isEmpty());
|
||||
|
||||
const QString deep = fakeBinary(root, QStringLiteral("jdk-21/lib/legal/jre/bin"));
|
||||
QCOMPARE(JavaRuntimeStore::locateBinary(root), deep);
|
||||
const QString macLayout = fakeBinary(root, QStringLiteral("jdk-21/Contents/Home/bin"));
|
||||
QCOMPARE(JavaRuntimeStore::locateBinary(root), macLayout);
|
||||
const QString flat = fakeBinary(root, QStringLiteral("jdk-21/bin"));
|
||||
QCOMPARE(JavaRuntimeStore::locateBinary(root), flat);
|
||||
}
|
||||
|
||||
void writeMetaFindInstalledRemove()
|
||||
{
|
||||
QString error;
|
||||
const InstalledJavaRuntime older = makeRuntime(QStringLiteral("temurin-jdk-17.0.9"), 17, QStringLiteral("17.0.9"));
|
||||
const InstalledJavaRuntime newer = makeRuntime(QStringLiteral("temurin-jdk-21.0.2"), 21, QStringLiteral("21.0.2"));
|
||||
const InstalledJavaRuntime newerPatch = makeRuntime(QStringLiteral("temurin-jdk-21.0.5"), 21, QStringLiteral("21.0.5"));
|
||||
QVERIFY2(JavaRuntimeStore::writeMeta(older, &error), qPrintable(error));
|
||||
QVERIFY2(JavaRuntimeStore::writeMeta(newer, &error), qPrintable(error));
|
||||
QVERIFY2(JavaRuntimeStore::writeMeta(newerPatch, &error), qPrintable(error));
|
||||
|
||||
// Путь хранится относительным: папку можно переносить.
|
||||
const QJsonObject meta = Fixtures::readJsonFile(JavaRuntimeStore::dirFor(newer.id) + "/runtime.json");
|
||||
QCOMPARE(meta.value("javaPath").toString(), "jdk/bin/" + Fixtures::javaExecutableName());
|
||||
|
||||
const auto found = JavaRuntimeStore::find(newer.id);
|
||||
QVERIFY(found.has_value());
|
||||
QCOMPARE(found->major, 21);
|
||||
QCOMPARE(found->version, QStringLiteral("21.0.2"));
|
||||
QCOMPARE(found->kind, QStringLiteral("jdk"));
|
||||
QCOMPARE(found->size, qint64(12345));
|
||||
QCOMPARE(QFileInfo(found->javaPath).canonicalFilePath(), QFileInfo(newer.javaPath).canonicalFilePath());
|
||||
|
||||
// Сортировка: свежая major первой, внутри — по версии по убыванию.
|
||||
const QList<InstalledJavaRuntime> all = JavaRuntimeStore::installed();
|
||||
QCOMPARE(all.size(), 3);
|
||||
QCOMPARE(all.at(0).id, newerPatch.id);
|
||||
QCOMPARE(all.at(1).id, newer.id);
|
||||
QCOMPARE(all.at(2).id, older.id);
|
||||
|
||||
QVERIFY(JavaRuntimeStore::remove(older.id, &error));
|
||||
QVERIFY(!QDir(JavaRuntimeStore::dirFor(older.id)).exists());
|
||||
QVERIFY(!JavaRuntimeStore::find(older.id).has_value());
|
||||
// Удаление несуществующего — не ошибка.
|
||||
QVERIFY(JavaRuntimeStore::remove(older.id, &error));
|
||||
QCOMPARE(JavaRuntimeStore::installed().size(), 2);
|
||||
}
|
||||
|
||||
void findRejectsBrokenMeta()
|
||||
{
|
||||
const InstalledJavaRuntime runtime = makeRuntime(QStringLiteral("jdk-x"), 21, QStringLiteral("21"));
|
||||
QVERIFY(JavaRuntimeStore::writeMeta(runtime, nullptr));
|
||||
QVERIFY(JavaRuntimeStore::find(runtime.id).has_value());
|
||||
|
||||
// Бинарник пропал — запись больше не считается установленной.
|
||||
QFile::remove(runtime.javaPath);
|
||||
QVERIFY(!JavaRuntimeStore::find(runtime.id).has_value());
|
||||
|
||||
// id в файле не совпадает с папкой.
|
||||
const InstalledJavaRuntime other = makeRuntime(QStringLiteral("jdk-y"), 17, QStringLiteral("17"));
|
||||
QVERIFY(JavaRuntimeStore::writeMeta(other, nullptr));
|
||||
QJsonObject meta = Fixtures::readJsonFile(JavaRuntimeStore::dirFor(other.id) + "/runtime.json");
|
||||
meta["id"] = "jdk-z";
|
||||
Fixtures::writeJson(JavaRuntimeStore::dirFor(other.id) + "/runtime.json", meta);
|
||||
QVERIFY(!JavaRuntimeStore::find(other.id).has_value());
|
||||
|
||||
// Мусор вместо json.
|
||||
Fixtures::write(JavaRuntimeStore::dirFor(other.id) + "/runtime.json", "{not json");
|
||||
QVERIFY(!JavaRuntimeStore::find(other.id).has_value());
|
||||
QVERIFY(JavaRuntimeStore::installed().isEmpty());
|
||||
}
|
||||
|
||||
void invalidIdsAreRejected()
|
||||
{
|
||||
QString error;
|
||||
InstalledJavaRuntime runtime;
|
||||
runtime.id = QStringLiteral("..");
|
||||
QVERIFY(!JavaRuntimeStore::writeMeta(runtime, &error));
|
||||
QCOMPARE(error, Loc::text("java.error.invalidRuntimeId"));
|
||||
error.clear();
|
||||
QVERIFY(!JavaRuntimeStore::remove(QStringLiteral(".."), &error));
|
||||
QCOMPARE(error, Loc::text("java.error.invalidRuntimeId"));
|
||||
QVERIFY(!JavaRuntimeStore::find(QString()).has_value());
|
||||
}
|
||||
|
||||
void minimumJavaFor_data()
|
||||
{
|
||||
QTest::addColumn<QString>("version");
|
||||
QTest::addColumn<int>("java");
|
||||
QTest::newRow("empty") << "" << 0;
|
||||
QTest::newRow("spaces") << " " << 0;
|
||||
QTest::newRow("1.7.10") << "1.7.10" << 8;
|
||||
QTest::newRow("1.12.2") << "1.12.2" << 8;
|
||||
QTest::newRow("1.16.5") << "1.16.5" << 8;
|
||||
QTest::newRow("1.17") << "1.17" << 16;
|
||||
QTest::newRow("1.17.1") << "1.17.1" << 16;
|
||||
QTest::newRow("1.18") << "1.18" << 17;
|
||||
QTest::newRow("1.19.4") << "1.19.4" << 17;
|
||||
QTest::newRow("1.20.1") << "1.20.1" << 17;
|
||||
QTest::newRow("1.20.4") << "1.20.4" << 17;
|
||||
QTest::newRow("1.20.5") << "1.20.5" << 21;
|
||||
QTest::newRow("1.20.6") << "1.20.6" << 21;
|
||||
QTest::newRow("1.21") << "1.21" << 21;
|
||||
QTest::newRow("1.21.8") << "1.21.8" << 21;
|
||||
QTest::newRow("forge-suffix") << "1.20.1-forge-47.4.0" << 17;
|
||||
QTest::newRow("newEpoch") << "26.1" << 21;
|
||||
QTest::newRow("snapshot20") << "20w45a" << 8;
|
||||
QTest::newRow("snapshot21") << "21w19a" << 16;
|
||||
QTest::newRow("snapshot22") << "22w45a" << 17;
|
||||
QTest::newRow("snapshot24") << "24w14a" << 21;
|
||||
QTest::newRow("snapshot25") << "25w33a" << 21;
|
||||
QTest::newRow("beta") << "b1.7.3" << 8;
|
||||
QTest::newRow("trimmed") << " 1.18.2 " << 17;
|
||||
}
|
||||
|
||||
void minimumJavaFor()
|
||||
{
|
||||
QFETCH(QString, version);
|
||||
QFETCH(int, java);
|
||||
QCOMPARE(JavaRequirement::minimumFor(version), java);
|
||||
}
|
||||
|
||||
void coverage()
|
||||
{
|
||||
QCOMPARE(JavaRequirement::coverage(25), Loc::text("java.range.modern"));
|
||||
QCOMPARE(JavaRequirement::coverage(21), Loc::text("java.range.modern"));
|
||||
QCOMPARE(JavaRequirement::coverage(17), Loc::text("java.range.mid"));
|
||||
QCOMPARE(JavaRequirement::coverage(16), Loc::text("java.range.seventeen"));
|
||||
QCOMPARE(JavaRequirement::coverage(11), Loc::text("java.range.unsuitable"));
|
||||
QCOMPARE(JavaRequirement::coverage(8), Loc::text("java.range.legacy"));
|
||||
QVERIFY(JavaRequirement::coverage(0).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestJavaRuntime)
|
||||
#include "tst_javaruntime.moc"
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "testenv.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
class TestLauncherPaths : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void init() { TestEnv::resetLauncherRoot(); }
|
||||
|
||||
void rootIsInsideContainer()
|
||||
{
|
||||
QVERIFY(LauncherPaths::rootDir().startsWith(LauncherPaths::containerDir() + u'/'));
|
||||
QCOMPARE(QFileInfo(LauncherPaths::rootDir()).fileName(), QStringLiteral("BiLauncher"));
|
||||
}
|
||||
|
||||
void containerPerPlatform()
|
||||
{
|
||||
const QString home = QDir::homePath();
|
||||
#if defined(Q_OS_WIN)
|
||||
QCOMPARE(LauncherPaths::containerDir(), home + QStringLiteral("/AppData/Roaming"));
|
||||
QCOMPARE(LauncherPaths::defaultMinecraftDir(), LauncherPaths::containerDir() + "/.minecraft");
|
||||
#elif defined(Q_OS_MACOS)
|
||||
QCOMPARE(LauncherPaths::containerDir(), home + QStringLiteral("/Library/Application Support"));
|
||||
QCOMPARE(LauncherPaths::defaultMinecraftDir(), LauncherPaths::containerDir() + "/minecraft");
|
||||
#else
|
||||
QCOMPARE(LauncherPaths::containerDir(), home);
|
||||
QCOMPARE(LauncherPaths::defaultMinecraftDir(), home + "/.minecraft");
|
||||
#endif
|
||||
}
|
||||
|
||||
void filesLayout_data()
|
||||
{
|
||||
QTest::addColumn<QString>("actual");
|
||||
QTest::addColumn<QString>("relative");
|
||||
|
||||
QTest::newRow("settings") << LauncherPaths::settingsFile() << "settings.json";
|
||||
QTest::newRow("profiles") << LauncherPaths::profilesFile() << "profiles.json";
|
||||
QTest::newRow("customBuilds") << LauncherPaths::customBuildsFile() << "customBuilds.json";
|
||||
QTest::newRow("legacyBuilds") << LauncherPaths::legacyCustomBuildsFile() << "versions.json";
|
||||
QTest::newRow("buildStorage") << LauncherPaths::buildStorageDir() << "builds";
|
||||
QTest::newRow("buildDir") << LauncherPaths::buildDir(7) << "builds/7";
|
||||
QTest::newRow("season") << LauncherPaths::seasonalStateFile(7) << "builds/7/season.json";
|
||||
QTest::newRow("runtime") << LauncherPaths::runtimeDir() << "runtime";
|
||||
QTest::newRow("java") << LauncherPaths::javaDir() << "java";
|
||||
QTest::newRow("cache") << LauncherPaths::cacheDir() << "cache";
|
||||
QTest::newRow("javaCatalog") << LauncherPaths::javaCatalogFile() << "cache/java_runtimes.json";
|
||||
QTest::newRow("javaDownload") << LauncherPaths::javaDownloadDir() << "cache/java";
|
||||
QTest::newRow("manifest") << LauncherPaths::versionManifestFile() << "cache/version_manifest.json";
|
||||
QTest::newRow("seasonal") << LauncherPaths::seasonalCatalogFile() << "cache/seasonal_builds.json";
|
||||
QTest::newRow("loaderCache") << LauncherPaths::loaderCacheFile("forge") << "cache/loader_forge.json";
|
||||
QTest::newRow("loaderDownload") << LauncherPaths::loaderDownloadDir() << "cache/loaders";
|
||||
}
|
||||
|
||||
void filesLayout()
|
||||
{
|
||||
QFETCH(QString, actual);
|
||||
QFETCH(QString, relative);
|
||||
QCOMPARE(actual, LauncherPaths::rootDir() + u'/' + relative);
|
||||
}
|
||||
|
||||
void ensureRootCreatesTree()
|
||||
{
|
||||
QString error;
|
||||
QVERIFY2(LauncherPaths::ensureRootExists(&error), qPrintable(error));
|
||||
for (const QString &dir : {LauncherPaths::rootDir(), LauncherPaths::runtimeDir(),
|
||||
LauncherPaths::cacheDir(), LauncherPaths::loaderDownloadDir(),
|
||||
LauncherPaths::buildStorageDir(), LauncherPaths::javaDir(),
|
||||
LauncherPaths::javaDownloadDir()}) {
|
||||
QVERIFY2(QFileInfo(dir).isDir(), qPrintable(dir));
|
||||
}
|
||||
// Повторный вызов на готовом дереве — тоже успех.
|
||||
QVERIFY(LauncherPaths::ensureRootExists(&error));
|
||||
}
|
||||
|
||||
void ensureRootFailsWhenFileBlocksFolder()
|
||||
{
|
||||
QDir().mkpath(LauncherPaths::rootDir());
|
||||
QFile blocker(LauncherPaths::cacheDir());
|
||||
QVERIFY(blocker.open(QIODevice::WriteOnly));
|
||||
blocker.close();
|
||||
|
||||
QString error;
|
||||
QVERIFY(!LauncherPaths::ensureRootExists(&error));
|
||||
QVERIFY(!error.isEmpty());
|
||||
QVERIFY(error.contains(QDir::toNativeSeparators(LauncherPaths::cacheDir())));
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestLauncherPaths)
|
||||
#include "tst_launcherpaths.moc"
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "testenv.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QSignalSpy>
|
||||
|
||||
class TestLocalization : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Localization &loc() { return Localization::instance(); }
|
||||
|
||||
// Значения каталога напрямую из файла — эталон для сравнения.
|
||||
static QJsonObject catalogEntry(const QString &key)
|
||||
{
|
||||
return Fixtures::readJsonFile(QStringLiteral(BILAUNCHER_SOURCE_DIR "/i18n/translations.json"))
|
||||
.value(QStringLiteral("strings"))
|
||||
.toObject()
|
||||
.value(key)
|
||||
.toObject();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
TestEnv::resetLauncherRoot();
|
||||
loc().setLanguage(QStringLiteral("en"));
|
||||
}
|
||||
|
||||
void textFollowsLanguage()
|
||||
{
|
||||
const QJsonObject entry = catalogEntry(QStringLiteral("app.tooltip.openMods"));
|
||||
QCOMPARE(Loc::text("app.tooltip.openMods"), entry.value("en").toString());
|
||||
|
||||
QSignalSpy spy(&loc(), &Localization::languageChanged);
|
||||
loc().setLanguage(QStringLiteral("ru"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QCOMPARE(loc().language(), QStringLiteral("ru"));
|
||||
QCOMPARE(loc().resolvedLanguage(), QStringLiteral("ru"));
|
||||
QCOMPARE(Loc::text("app.tooltip.openMods"), entry.value("ru").toString());
|
||||
|
||||
// Тот же язык ещё раз — без сигнала.
|
||||
loc().setLanguage(QStringLiteral("ru"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
}
|
||||
|
||||
void unknownLanguageFallsBackToSystem()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("klingon")));
|
||||
loc().setLanguage(QStringLiteral("klingon"));
|
||||
QCOMPARE(loc().language(), QStringLiteral("system"));
|
||||
const QString resolved = loc().resolvedLanguage();
|
||||
QVERIFY(resolved == QLatin1String("ru") || resolved == QLatin1String("en"));
|
||||
}
|
||||
|
||||
void missingKeyReturnsKey()
|
||||
{
|
||||
#ifndef QT_NO_DEBUG
|
||||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("no\\.such\\.key")));
|
||||
#endif
|
||||
QCOMPARE(Loc::text("no.such.key"), QStringLiteral("no.such.key"));
|
||||
#ifndef QT_NO_DEBUG
|
||||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("no\\.such\\.list")));
|
||||
#endif
|
||||
QCOMPARE(Loc::list("no.such.list"), QStringList{QStringLiteral("no.such.list")});
|
||||
}
|
||||
|
||||
void listsHaveSameShapeInAllLanguages()
|
||||
{
|
||||
const QJsonObject strings =
|
||||
Fixtures::readJsonFile(QStringLiteral(BILAUNCHER_SOURCE_DIR "/i18n/translations.json"))
|
||||
.value(QStringLiteral("strings"))
|
||||
.toObject();
|
||||
int lists = 0;
|
||||
for (auto it = strings.constBegin(); it != strings.constEnd(); ++it) {
|
||||
if (!it.value().toObject().value("en").isArray())
|
||||
continue;
|
||||
++lists;
|
||||
const QByteArray key = it.key().toLatin1();
|
||||
loc().setLanguage(QStringLiteral("en"));
|
||||
const QStringList en = Loc::list(key.constData());
|
||||
loc().setLanguage(QStringLiteral("ru"));
|
||||
const QStringList ru = Loc::list(key.constData());
|
||||
QCOMPARE(en.size(), ru.size());
|
||||
QCOMPARE(en.size(), it.value().toObject().value("en").toArray().size());
|
||||
}
|
||||
if (lists == 0)
|
||||
QSKIP("в каталоге нет списков");
|
||||
}
|
||||
|
||||
void availableLanguages()
|
||||
{
|
||||
const QVariantList languages = loc().availableLanguages();
|
||||
QVERIFY(languages.size() >= 3);
|
||||
QCOMPARE(languages.first().toMap().value("code").toString(), QStringLiteral("system"));
|
||||
QStringList codes;
|
||||
for (const QVariant &item : languages)
|
||||
codes << item.toMap().value("code").toString();
|
||||
QVERIFY(codes.contains("ru"));
|
||||
QVERIFY(codes.contains("en"));
|
||||
for (const QVariant &item : languages)
|
||||
QVERIFY(!item.toMap().value("name").toString().isEmpty());
|
||||
}
|
||||
|
||||
void storedLanguageIsReadOnLoad()
|
||||
{
|
||||
Fixtures::writeJson(LauncherPaths::settingsFile(), QJsonObject{{"language", "ru"}});
|
||||
QVERIFY(loc().load());
|
||||
QCOMPARE(loc().language(), QStringLiteral("ru"));
|
||||
|
||||
Fixtures::writeJson(LauncherPaths::settingsFile(), QJsonObject{{"language", "xx"}});
|
||||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("xx")));
|
||||
QVERIFY(loc().load());
|
||||
QCOMPARE(loc().language(), QStringLiteral("system"));
|
||||
|
||||
Fixtures::write(LauncherPaths::settingsFile(), "garbage");
|
||||
QVERIFY(loc().load());
|
||||
QCOMPARE(loc().language(), QStringLiteral("system"));
|
||||
}
|
||||
|
||||
void everyCppKeyExists()
|
||||
{
|
||||
// Тексты, которые код запрашивает через Loc::text("…"), должны быть в каталоге —
|
||||
// иначе пользователь увидит сам ключ. Полная проверка — tools/check_translations.py,
|
||||
// здесь — выборка ключей, которыми пользуются тесты.
|
||||
for (const char *key : {"storage.error.createFailed", "storage.error.notWritable",
|
||||
"java.error.invalidRuntimeId", "version.error.invalidId",
|
||||
"launch.error.filesMissing", "auth.offline.error.noNickname"}) {
|
||||
QVERIFY2(Loc::text(key) != QLatin1String(key), key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestLocalization)
|
||||
#include "tst_localization.moc"
|
||||
@@ -0,0 +1,353 @@
|
||||
#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"
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "testenv.h"
|
||||
|
||||
#include "modloader.h"
|
||||
|
||||
class TestModLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void keysRoundTrip()
|
||||
{
|
||||
for (ModLoader loader : {ModLoader::Forge, ModLoader::Fabric, ModLoader::NeoForge, ModLoader::Quilt}) {
|
||||
const auto back = loaderFromKey(loaderKey(loader));
|
||||
QVERIFY(back.has_value());
|
||||
QCOMPARE(*back, loader);
|
||||
QVERIFY(!loaderTitle(loader).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
void knownKeys()
|
||||
{
|
||||
QCOMPARE(loaderKey(ModLoader::Forge), QStringLiteral("forge"));
|
||||
QCOMPARE(loaderKey(ModLoader::Fabric), QStringLiteral("fabric"));
|
||||
QCOMPARE(loaderKey(ModLoader::NeoForge), QStringLiteral("neoforge"));
|
||||
QCOMPARE(loaderKey(ModLoader::Quilt), QStringLiteral("quilt"));
|
||||
QCOMPARE(loaderTitle(ModLoader::NeoForge), QStringLiteral("NeoForge"));
|
||||
}
|
||||
|
||||
void unknownKeys()
|
||||
{
|
||||
QVERIFY(!loaderFromKey(QString()).has_value());
|
||||
QVERIFY(!loaderFromKey(QStringLiteral("Forge")).has_value()); // регистр важен
|
||||
QVERIFY(!loaderFromKey(QStringLiteral("liteloader")).has_value());
|
||||
}
|
||||
|
||||
void entryDefaults()
|
||||
{
|
||||
const LoaderVersionEntry entry;
|
||||
QVERIFY(entry.stable);
|
||||
QVERIFY(!entry.recommended);
|
||||
QVERIFY(entry.installerUrl.isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestModLoader)
|
||||
#include "tst_modloader.moc"
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "testenv.h"
|
||||
|
||||
#include "msaauthservice.h"
|
||||
|
||||
#include <QUrlQuery>
|
||||
|
||||
class TestMsaRedirect : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void authorizationUrlCarriesClientAndRedirect()
|
||||
{
|
||||
const QUrl url = MsaAuthService::authorizationUrl();
|
||||
QCOMPARE(url.scheme(), QStringLiteral("https"));
|
||||
QCOMPARE(url.host(), QStringLiteral("login.live.com"));
|
||||
const QUrlQuery query(url);
|
||||
QCOMPARE(query.queryItemValue("client_id"), MsaAuthService::clientId());
|
||||
QCOMPARE(query.queryItemValue("response_type"), QStringLiteral("code"));
|
||||
QVERIFY(!query.queryItemValue("scope").isEmpty());
|
||||
// Код возвращается на тот адрес, который потом распознаёт matchRedirect.
|
||||
const QUrl redirect(query.queryItemValue("redirect_uri", QUrl::FullyDecoded));
|
||||
QString code;
|
||||
QVERIFY(MsaAuthService::matchRedirect(QUrl(redirect.toString() + "?code=abc"), &code, nullptr));
|
||||
QCOMPARE(code, QStringLiteral("abc"));
|
||||
}
|
||||
|
||||
void matchRedirect_data()
|
||||
{
|
||||
QTest::addColumn<QString>("url");
|
||||
QTest::addColumn<bool>("matches");
|
||||
QTest::addColumn<QString>("code");
|
||||
QTest::addColumn<QString>("error");
|
||||
|
||||
QTest::newRow("code in query")
|
||||
<< "https://login.live.com/oauth20_desktop.srf?code=M.C123-abc&lc=1049" << true << "M.C123-abc" << "";
|
||||
QTest::newRow("code in fragment")
|
||||
<< "https://login.live.com/oauth20_desktop.srf#code=FRAG&state=1" << true << "FRAG" << "";
|
||||
QTest::newRow("encoded code")
|
||||
<< "https://login.live.com/oauth20_desktop.srf?code=a%2Bb%3D" << true << "a+b=" << "";
|
||||
QTest::newRow("error with description")
|
||||
<< "https://login.live.com/oauth20_desktop.srf?error=access_denied&error_description=The%20user%20denied"
|
||||
<< true << "" << "The user denied";
|
||||
QTest::newRow("error only")
|
||||
<< "https://login.live.com/oauth20_desktop.srf?error=server_error" << true << "" << "server_error";
|
||||
QTest::newRow("host case-insensitive")
|
||||
<< "https://LOGIN.LIVE.COM/oauth20_desktop.srf?code=x" << true << "x" << "";
|
||||
QTest::newRow("redirect without params")
|
||||
<< "https://login.live.com/oauth20_desktop.srf" << true << "" << "";
|
||||
QTest::newRow("authorize page") << "https://login.live.com/oauth20_authorize.srf?code=x" << false << "" << "";
|
||||
QTest::newRow("foreign host") << "https://evil.example/oauth20_desktop.srf?code=x" << false << "" << "";
|
||||
QTest::newRow("empty") << "" << false << "" << "";
|
||||
}
|
||||
|
||||
void matchRedirect()
|
||||
{
|
||||
QFETCH(QString, url);
|
||||
QFETCH(bool, matches);
|
||||
QFETCH(QString, code);
|
||||
QFETCH(QString, error);
|
||||
|
||||
QString actualCode = QStringLiteral("untouched");
|
||||
QString actualError = QStringLiteral("untouched");
|
||||
QCOMPARE(MsaAuthService::matchRedirect(QUrl(url), &actualCode, &actualError), matches);
|
||||
if (matches) {
|
||||
QCOMPARE(actualCode, code);
|
||||
QCOMPARE(actualError, error);
|
||||
} else {
|
||||
QCOMPARE(actualCode, QStringLiteral("untouched"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestMsaRedirect)
|
||||
#include "tst_msaredirect.moc"
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "testenv.h"
|
||||
|
||||
#include "localization.h"
|
||||
#include "zlibreference.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcessEnvironment>
|
||||
|
||||
class TestZlibReference : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void nonLinuxIsNoop()
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
QSKIP("только для Windows и macOS");
|
||||
#else
|
||||
QVERIFY(!ZlibReference::systemIsZlibNg());
|
||||
QVERIFY(ZlibReference::bundledPath().isEmpty());
|
||||
QProcessEnvironment env;
|
||||
env.insert(QStringLiteral("LD_PRELOAD"), QStringLiteral("/x.so"));
|
||||
QString note = QStringLiteral("stale");
|
||||
QVERIFY(ZlibReference::applyTo(env, ¬e));
|
||||
QVERIFY(note.isEmpty());
|
||||
QCOMPARE(env.value(QStringLiteral("LD_PRELOAD")), QStringLiteral("/x.so"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void linuxPreloadFollowsSystemZlib()
|
||||
{
|
||||
#if !defined(Q_OS_LINUX)
|
||||
QSKIP("только для Linux");
|
||||
#else
|
||||
QProcessEnvironment env;
|
||||
env.insert(QStringLiteral("LD_PRELOAD"), QStringLiteral("/other.so"));
|
||||
QString note;
|
||||
const bool ok = ZlibReference::applyTo(env, ¬e);
|
||||
|
||||
if (!ZlibReference::systemIsZlibNg()) {
|
||||
// Обычный zlib: окружение не трогаем.
|
||||
QVERIFY(ok);
|
||||
QVERIFY(note.isEmpty());
|
||||
QCOMPARE(env.value(QStringLiteral("LD_PRELOAD")), QStringLiteral("/other.so"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString bundled = ZlibReference::bundledPath();
|
||||
if (bundled.isEmpty()) {
|
||||
QVERIFY(!ok);
|
||||
QCOMPARE(note, Loc::text("zlib.warning.referenceMissing"));
|
||||
QCOMPARE(env.value(QStringLiteral("LD_PRELOAD")), QStringLiteral("/other.so"));
|
||||
} else {
|
||||
QVERIFY(ok);
|
||||
// Наш zlib первым, чужой LD_PRELOAD сохраняется.
|
||||
QCOMPARE(env.value(QStringLiteral("LD_PRELOAD")), bundled + QStringLiteral(":/other.so"));
|
||||
QVERIFY(QFileInfo(bundled).isFile());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
BILAUNCHER_TEST_MAIN(TestZlibReference)
|
||||
#include "tst_zlibreference.moc"
|
||||
Reference in new issue
Block a user