test fixes
tests / windows (push) Skipped
tests / macos (push) Skipped
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped
tests / linux (push) Successful in 1m59s

Signed-off-by: galeon <dertiellis@gmail.com>
This commit is contained in:
galeon committed 2026-10-13 08:51:07 +03:00
1 parent 41599fa82f
commit 0dd6bef715
12 files changed
+161 -64

No files matched your search

+13
View File
@@ -21,6 +21,19 @@
На Linux папка лаунчера получается `~/BiLauncher`, без точки, то есть видимая.
## BILAUNCHER_HOME
`homeOverride()` читает переменную окружения `BILAUNCHER_HOME`. Если в ней абсолютный путь, он
заменяет домашнюю папку (`~` и `%USERPROFILE%` в таблице выше) для всех путей лаунчера, включая
папку игры по умолчанию. Пустое или относительное значение не учитывается. Остальной Qt
(`QDir::homePath()`, `QStandardPaths`) эту переменную не видит.
Переменная нужна тестам и дымовой проверке на Windows: там `QDir::homePath()` спрашивает профиль
у системы и `USERPROFILE` игнорирует, так что иначе их нельзя увести из настоящего
`%APPDATA%\BiLauncher`. Новых возможностей она не даёт: кто управляет окружением процесса, на
Linux и macOS и так подменит `HOME`. Чтобы случайно оставленную переменную было видно, `main.cpp`
при старте пишет в лог предупреждение с итоговой папкой лаунчера.
Внутри `rootDir()`:
```
+8 -7
View File
@@ -70,13 +70,14 @@ CMake, тесты появятся в панели «Тесты».
## Как тесты изолированы
**Файлы.** `LauncherPaths` строит пути от `QDir::homePath()`. Общий `main` тестов
(`tests/common/testenv.h`) до создания приложения подменяет `HOME` на временную папку, и каждый
процесс работает в своей. Если путь лаунчера всё же оказался вне песочницы, процесс сразу
завершается. На Windows `QDir::homePath()` переменные окружения не читает, поэтому там тесты идут
в настоящей `%APPDATA%\BiLauncher` учётной записи раннера, строго по одному, а за собой папку
стирают. Если папка уже есть и создана не тестами (нет метки `.bilauncher-test-sandbox`), тесты
отказываются запускаться.
**Файлы.** `LauncherPaths` строит пути от домашней папки. Общий `main` тестов
(`tests/common/testenv.h`) до создания приложения направляет `BILAUNCHER_HOME` (а заодно `HOME`
и `USERPROFILE`) на временную папку, и каждый процесс работает в своей, на всех ОС, параллельно.
`BILAUNCHER_HOME` нужна ради Windows: там `QDir::homePath()` берёт профиль у системы и `HOME`
с `USERPROFILE` не читает. Дочерние процессы наследуют переменную. Если папка лаунчера или папка
игры по умолчанию всё же оказалась вне песочницы, процесс сразу завершается с кодом 2 и ничего не
трогает. Дымовая проверка установленной сборки на Windows (`smoke_installed.ps1`) тоже запускает
лаунчер с `BILAUNCHER_HOME` на временную папку.
**Сеть.** Адреса серверов в сервисах зашиты константами, а свои `QNetworkAccessManager` они
создают сами. Зато прокси и SSL-настройки у Qt общие на процесс, и код лаунчера их не трогает.
+6 -1
View File
@@ -8,6 +8,7 @@
#include <QFileInfo>
#include <QHash>
#include <QProcess>
#include <QRegularExpression>
#include <QtCore/private/qzipreader_p.h>
namespace {
@@ -46,7 +47,11 @@ bool isNativeLibraryFile(const QString &fileName)
for (const QString &suffix : suffixes)
if (fileName.endsWith(suffix, Qt::CaseInsensitive))
return true;
return fileName.contains(QStringLiteral(".so.")); // libfoo.so.1
// Версионированные so (libfoo.so.1, libfoo.so.1.2.3). Хвост — строго цифры,
// иначе в натив попадают служебные файлы LWJGL вроде liblwjgl.so.sha1 / .so.git.
static const QRegularExpression versionedSo(QStringLiteral("\\.so(\\.\\d+)+$"),
QRegularExpression::CaseInsensitiveOption);
return versionedSo.match(fileName).hasMatch();
}
} // namespace
+20 -3
View File
@@ -3,21 +3,38 @@
#include <QDir>
#include <QFileInfo>
#include <QtGlobal>
namespace {
const QString kFolderName = QStringLiteral("BiLauncher");
// Домашняя папка, от которой строятся все пути (см. LauncherPaths::homeOverride()).
QString homeBase()
{
const QString overridden = LauncherPaths::homeOverride();
return overridden.isEmpty() ? QDir::homePath() : overridden;
}
} // namespace
QString LauncherPaths::homeOverride()
{
// Относительный путь зависел бы от текущей папки процесса — такой не принимаем.
const QString value = QDir::fromNativeSeparators(qEnvironmentVariable("BILAUNCHER_HOME"));
if (value.isEmpty() || !QDir::isAbsolutePath(value))
return QString();
return QDir::cleanPath(value);
}
QString LauncherPaths::containerDir()
{
#if defined(Q_OS_WIN)
return QDir::homePath() + QStringLiteral("/AppData/Roaming");
return homeBase() + QStringLiteral("/AppData/Roaming");
#elif defined(Q_OS_MACOS)
return QDir::homePath() + QStringLiteral("/Library/Application Support");
return homeBase() + QStringLiteral("/Library/Application Support");
#else // Linux и прочие Unix-системы
return QDir::homePath();
return homeBase();
#endif
}
+8
View File
@@ -4,6 +4,14 @@
namespace LauncherPaths {
// Все пути строятся от домашней папки пользователя (QDir::homePath()). Переменная
// окружения BILAUNCHER_HOME с абсолютным путём подменяет её — только для путей
// лаунчера (папка лаунчера и папка игры по умолчанию), остальной Qt её не видит.
// Нужна тестам и дымовой проверке на Windows: там QDir::homePath() спрашивает
// профиль у системы и HOME/USERPROFILE игнорирует. Пустое или относительное
// значение не учитывается. Возвращает подменённую папку или пустую строку.
QString homeOverride();
QString containerDir();
QString rootDir();
QString defaultMinecraftDir();
+7
View File
@@ -1,5 +1,8 @@
#include "launcherpaths.h"
#include "localization.h"
#include <QDebug>
#include <QDir>
#include <QGuiApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
@@ -33,6 +36,10 @@ int main(int argc, char *argv[])
QGuiApplication::setDesktopFileName(QStringLiteral("bilauncher"));
// Случайно оставленная BILAUNCHER_HOME увела бы данные в чужую папку — пусть это будет видно в логе.
if (!LauncherPaths::homeOverride().isEmpty())
qWarning().noquote() << "BILAUNCHER_HOME: данные лаунчера в" << QDir::toNativeSeparators(LauncherPaths::rootDir());
if (!Localization::instance().load())
return -1;
-2
View File
@@ -116,8 +116,6 @@ function(bilauncher_add_test name)
TIMEOUT ${ARG_TIMEOUT}
ENVIRONMENT "${BILAUNCHER_TEST_ENV}")
if(WIN32)
# На Windows все тесты делят настоящую %APPDATA%\BiLauncher (см. common/testenv.h).
set_tests_properties(${name} PROPERTIES RESOURCE_LOCK launcher_profile)
# DLL Qt — из установки Qt, без windeployqt для каждого теста.
set_property(TEST ${name} APPEND PROPERTY ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:Qt6::Core>")
+47 -29
View File
@@ -10,16 +10,33 @@
#include <cstdio>
#include <cstdlib>
#if defined(Q_OS_WIN)
#include <qt_windows.h>
#include <stdlib.h> // _wputenv_s
#endif
namespace {
const char kMarker[] = ".bilauncher-test-sandbox";
QTemporaryDir *g_sandbox = nullptr;
bool g_realProfile = false; // Windows: работаем в настоящем профиле под меткой
[[noreturn]] void abortSetup(const QString &message)
{
std::fprintf(stderr, "TestEnv: %s\n", qPrintable(message));
const QString line = QStringLiteral("TestEnv: ") + message + u'\n';
#if defined(Q_OS_WIN)
// В живую консоль пишем UTF-16 напрямую: кодовая страница консоли тогда не важна.
const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE);
DWORD mode = 0;
if (handle != INVALID_HANDLE_VALUE && GetConsoleMode(handle, &mode)) {
DWORD written = 0;
WriteConsoleW(handle, reinterpret_cast<const wchar_t *>(line.utf16()),
DWORD(line.size()), &written, nullptr);
std::exit(2);
}
#endif
// Вывод в трубу (ctest, CI) — UTF-8, как его и читает ctest. Локальная 8-битная
// кодировка Windows (cp1252/cp1251) превращала кириллицу в «???».
std::fputs(line.toUtf8().constData(), stderr);
std::fflush(stderr);
std::exit(2);
}
@@ -31,15 +48,16 @@ void TestEnv::prepareSandbox()
if (!g_sandbox->isValid())
abortSetup(QStringLiteral("не удалось создать временную папку"));
const QByteArray home = QDir::toNativeSeparators(g_sandbox->path()).toLocal8Bit();
qputenv("HOME", home);
qputenv("USERPROFILE", home);
const QString home = QDir::toNativeSeparators(g_sandbox->path());
setEnvPath("HOME", home);
setEnvPath("USERPROFILE", home);
// LauncherPaths: на Windows QDir::homePath() HOME/USERPROFILE не читает, а эту
// переменную лаунчер читает везде. Дочерние процессы получают её по наследству.
setEnvPath("BILAUNCHER_HOME", home);
// Чужая Java из окружения машины не должна влиять на выбор Java в тестах.
qunsetenv("JAVA_HOME");
std::atexit([] {
if (g_realProfile)
QDir(LauncherPaths::rootDir()).removeRecursively();
delete g_sandbox;
g_sandbox = nullptr;
});
@@ -52,19 +70,13 @@ void TestEnv::finishSetup()
const QString root = QDir::cleanPath(LauncherPaths::rootDir());
const QString sandbox = QDir::cleanPath(g_sandbox->path());
if (!root.startsWith(sandbox + u'/')) {
#if defined(Q_OS_WIN)
if (QDir(root).exists() && !QFile::exists(root + u'/' + QLatin1String(kMarker))) {
abortSetup(QStringLiteral(
"%1 уже существует и создана не тестами — отказываюсь её трогать. "
"Запускайте тесты под отдельной учётной записью.")
.arg(QDir::toNativeSeparators(root)));
}
g_realProfile = true;
resetLauncherRoot();
#else
abortSetup(QStringLiteral("папка лаунчера %1 вне песочницы %2").arg(root, sandbox));
#endif
const QString game = QDir::cleanPath(LauncherPaths::defaultMinecraftDir());
// Ни одна папка лаунчера не должна указывать на настоящий профиль: resetLauncherRoot()
// стирает rootDir() целиком.
for (const QString &path : {root, game}) {
if (!path.startsWith(sandbox + u'/'))
abortSetup(QStringLiteral("путь лаунчера %1 вне песочницы %2 — отказываюсь запускаться")
.arg(QDir::toNativeSeparators(path), QDir::toNativeSeparators(sandbox)));
}
if (!Localization::instance().load())
@@ -72,6 +84,18 @@ void TestEnv::finishSetup()
Localization::instance().setLanguage(QStringLiteral("en"));
}
void TestEnv::setEnvPath(const char *name, const QString &path)
{
// На Windows qputenv() пишет в ANSI-кодировке и испортил бы, например,
// кириллическое имя пользователя во временном пути.
#if defined(Q_OS_WIN)
_wputenv_s(reinterpret_cast<const wchar_t *>(QString::fromLatin1(name).utf16()),
reinterpret_cast<const wchar_t *>(path.utf16()));
#else
qputenv(name, QFile::encodeName(path));
#endif
}
QString TestEnv::homeDir()
{
return g_sandbox->path();
@@ -87,11 +111,5 @@ QString TestEnv::freshDir(const QString &name)
void TestEnv::resetLauncherRoot()
{
const QString root = LauncherPaths::rootDir();
QDir(root).removeRecursively();
if (g_realProfile) {
QDir().mkpath(root);
QFile marker(root + u'/' + QLatin1String(kMarker));
marker.open(QIODevice::WriteOnly);
}
QDir(LauncherPaths::rootDir()).removeRecursively();
}
+13 -12
View File
@@ -2,16 +2,14 @@
// Общая обвязка тестов: песочница для домашней папки и main для QtTest.
//
// LauncherPaths строит все пути от QDir::homePath(). На Linux и macOS он берёт
// HOME, поэтому каждый тестовый процесс до создания QCoreApplication
// подменяет HOME на свою временную папку — реальные ~/BiLauncher и .minecraft
// машины, на которой гоняются тесты, не трогаются.
//
// На Windows QDir::homePath() спрашивает профиль у системы и переменные
// окружения игнорирует. Там тесты работают в настоящей
// %APPDATA%\BiLauncher, но только если её ещё нет либо она создана тестами
// (в ней лежит метка .bilauncher-test-sandbox). Иначе процесс отказывается
// запускаться, чтобы не испортить данные живого лаунчера.
// LauncherPaths строит все пути от домашней папки. Каждый тестовый процесс до
// создания QCoreApplication заводит свою временную папку и направляет туда
// BILAUNCHER_HOME (её LauncherPaths читает на всех ОС), а заодно HOME и
// USERPROFILE (их читает QDir::homePath() на Linux и macOS). На Windows
// QDir::homePath() переменные окружения игнорирует, поэтому без BILAUNCHER_HOME
// там не обойтись. Реальные папка лаунчера и .minecraft машины, на которой гоняются
// тесты, не трогаются: если путь лаунчера всё же оказался вне песочницы,
// процесс сразу завершается с кодом 2.
#include <QCoreApplication>
#include <QString>
@@ -19,14 +17,17 @@
namespace TestEnv {
// До QCoreApplication: временная папка и подмена HOME/USERPROFILE.
// До QCoreApplication: временная папка и подмена BILAUNCHER_HOME/HOME/USERPROFILE.
void prepareSandbox();
// После QCoreApplication: проверка, что пути лаунчера ушли в песочницу,
// загрузка переводов, английский язык (тексты ошибок не зависят от локали машины).
void finishSetup();
// Корень песочницы — то, что сейчас возвращает QDir::homePath() в тестах.
// Переменная окружения с путём, без потерь символов вне ANSI-кодировки на Windows.
void setEnvPath(const char *name, const QString &path);
// Корень песочницы — домашняя папка, от которой LauncherPaths строит пути в тестах.
QString homeDir();
// Свежая пустая папка внутри песочницы (для .minecraft, архивов и т. п.).
+11 -8
View File
@@ -6,9 +6,9 @@
# лаунчер запускается без окна и через 10 секунд ещё жив, а в логе нет ошибок
# загрузки модулей.
#
# На Windows лаунчер берёт папку профиля у системы, а не из переменных окружения,
# поэтому работает в настоящей %APPDATA%\BiLauncher учётной записи раннера. Если
# её до запуска не было, скрипт убирает её за собой.
# QDir::homePath() на Windows берёт профиль у системы и USERPROFILE не читает,
# поэтому лаунчер запускается с BILAUNCHER_HOME на временную папку (см.
# launcherpaths.h) — настоящая %APPDATA%\BiLauncher раннера не трогается.
param(
[Parameter(Mandatory = $true)][string]$Prefix,
@@ -28,14 +28,17 @@ if (-not (Get-ChildItem (Join-Path $Prefix "bin\tls") -Filter "*.dll" -ErrorActi
Fail "нет TLS-плагина (bin\tls) — лаунчер не сможет ходить по https"
}
$profileRoot = Join-Path $env:APPDATA "BiLauncher"
$existedBefore = Test-Path $profileRoot
$realProfile = Join-Path $env:APPDATA "BiLauncher"
$realExisted = Test-Path $realProfile
$log = Join-Path $env:TEMP ("bilauncher-smoke-" + [guid]::NewGuid().ToString("N"))
$sandbox = Join-Path $env:TEMP ("bilauncher-smoke-" + [guid]::NewGuid().ToString("N"))
$log = Join-Path $sandbox "log"
New-Item -ItemType Directory -Force $log | Out-Null
$stdout = Join-Path $log "stdout.log"
$stderr = Join-Path $log "stderr.log"
$profileRoot = Join-Path $sandbox "AppData\Roaming\BiLauncher"
$env:BILAUNCHER_HOME = $sandbox
$env:QT_QPA_PLATFORM = "offscreen"
$env:QT_FORCE_STDERR_LOGGING = "1"
$env:QTWEBENGINE_CHROMIUM_FLAGS = "--disable-gpu"
@@ -58,9 +61,9 @@ try {
Fail "в логе ошибки загрузки модулей"
}
if (-not (Test-Path (Join-Path $profileRoot "settings.json"))) { Fail "лаунчер не создал $profileRoot\settings.json" }
if (-not $realExisted -and (Test-Path $realProfile)) { Fail "лаунчер создал настоящую $realProfile мимо BILAUNCHER_HOME" }
Write-Host "SMOKE OK: $bin"
}
finally {
Remove-Item -Recurse -Force $log -ErrorAction SilentlyContinue
if (-not $existedBefore) { Remove-Item -Recurse -Force $profileRoot -ErrorAction SilentlyContinue }
Remove-Item -Recurse -Force $sandbox -ErrorAction SilentlyContinue
}
+5 -1
View File
@@ -99,7 +99,11 @@ class TestGameLauncher : public QObject
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"}});
{QStringLiteral("skip/") + kNativeName + ".sha1", "hash"},
// Служебные файлы LWJGL рядом с .so: не должны приниматься
// за версионированную библиотеку (libfoo.so.1) ни на одной ОС.
{QStringLiteral("skip/liblwjgl.so.sha1"), "hash"},
{QStringLiteral("skip/liblwjgl.so.git"), "git"}});
Fixtures::write(m_game + "/assets/indexes/5.json", "{}");
QDir().mkpath(m_game + "/assets/objects");
}
+23 -1
View File
@@ -6,6 +6,7 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QScopeGuard>
class TestLauncherPaths : public QObject
{
@@ -22,7 +23,9 @@ private slots:
void containerPerPlatform()
{
const QString home = QDir::homePath();
// Не QDir::homePath(): на Windows он смотрит в настоящий профиль, а пути
// лаунчера в тестах идут от BILAUNCHER_HOME (песочницы).
const QString home = QDir::cleanPath(TestEnv::homeDir());
#if defined(Q_OS_WIN)
QCOMPARE(LauncherPaths::containerDir(), home + QStringLiteral("/AppData/Roaming"));
QCOMPARE(LauncherPaths::defaultMinecraftDir(), LauncherPaths::containerDir() + "/.minecraft");
@@ -35,6 +38,25 @@ private slots:
#endif
}
void homeOverrideFromEnvironment()
{
const QString saved = qEnvironmentVariable("BILAUNCHER_HOME");
const auto restore = qScopeGuard([&] { TestEnv::setEnvPath("BILAUNCHER_HOME", saved); });
QCOMPARE(LauncherPaths::homeOverride(), QDir::cleanPath(TestEnv::homeDir()));
const QString other = TestEnv::freshDir("other-home");
TestEnv::setEnvPath("BILAUNCHER_HOME", QDir::toNativeSeparators(other));
QCOMPARE(LauncherPaths::homeOverride(), QDir::cleanPath(other));
QVERIFY(LauncherPaths::rootDir().startsWith(QDir::cleanPath(other) + u'/'));
// Относительный путь зависел бы от текущей папки — игнорируется, как и пустой.
for (const char *ignored : {"relative/home", ""}) {
qputenv("BILAUNCHER_HOME", ignored);
QVERIFY2(LauncherPaths::homeOverride().isEmpty(), ignored);
}
}
void filesLayout_data()
{
QTest::addColumn<QString>("actual");