# Автотесты Тесты лежат в `tests/` и собираются только по опции `BILAUNCHER_BUILD_TESTS`. В обычную сборку и в релизы они не попадают, код лаунчера ради них не менялся. ## Как запустить ```bash cmake -S . -B build-tests -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBILAUNCHER_BUILD_TESTS=ON -DCMAKE_PREFIX_PATH=$HOME/Qt/6.11.1/macos ``` ```bash cmake --build build-tests ``` ```bash ctest --test-dir build-tests --output-on-failure -j 6 ``` Отдельный слой — по метке: `ctest --test-dir build-tests -L unit` (ещё `net`, `backend`, `qml`). Отдельный тест — напрямую, с фильтром по функции: ```bash build-tests/tests/net/tst_versioninstaller installsModernVersionCompletely ``` Статические проверки (переводы, qmllint, раздел CHANGELOG для текущей версии): ```bash python3 tests/scripts/static_checks.py build-tests ``` В Qt Creator проект с тестами открывается как обычно: включите `BILAUNCHER_BUILD_TESTS` в настройках CMake, тесты появятся в панели «Тесты». ## Что покрыто | Метка | Тест | Что проверяет | |-------|------|---------------| | unit | `tst_launcherpaths` | раскладка папки лаунчера, `ensureRootExists` и её отказ, если на месте папки файл | | unit | `tst_javaruntime` | ключи видов Java, `JavaRuntimeStore`, какая Java нужна какой версии игры | | unit | `tst_modloader` | ключи и названия модлоадеров | | unit | `tst_localization` | переключение языков, запасной язык, отсутствующий ключ, язык из `settings.json` | | unit | `tst_minecraftversion` | правила Mojang, загрузка json версии, `inheritsFrom`, нативы, удаление версий | | unit | `tst_authoffline` | офлайн-UUID как у сервера в offline-mode | | unit | `tst_msaredirect` | разбор адреса возврата после входа Microsoft | | unit | `tst_gamelauncher` | командная строка игры, нативы, запуск, падение и остановка процесса | | unit | `tst_javalocator` | версия java по `-version`, выбор подходящей, поиск в папке лаунчера | | unit | `tst_zlibreference` | `LD_PRELOAD` с эталонным zlib на Linux | | unit | `tst_buildarchiveworker` | архив, очистка, восстановление, раскатка пака, zip-slip, отмена | | unit | `tst_buildswitcher` | переключение сборок туда и обратно, продолжение после обрыва, пак | | net | `tst_fakeinternet` | самопроверка перехвата HTTPS (см. ниже) | | net | `tst_versionmanifestservice` | каталог Mojang: разбор, кеш, офлайн, общие запросы | | net | `tst_seasonalbuildservice` | каталог сезонных сборок: проверки полей, формат, кеш по адресу | | net | `tst_modloaderversionservice` | Fabric, Quilt, Forge (с рекомендованными), NeoForge обе схемы | | net | `tst_javaruntimeservice` | Adoptium и Mojang, частичные отказы, кеш, запасной x64 на ARM | | net | `tst_authservice` | Ely.by: пароль, 2FA, продление токена, authlib-injector | | net | `tst_msaauthservice` | вся цепочка Microsoft → Xbox → XSTS → Minecraft, коды ошибок XSTS | | net | `tst_versioninstaller` | установка версии целиком, повторы, контрольные суммы, отмена, очередь | | net | `tst_seasonalpackdownloader` | загрузка пака: sha256, обрыв, отмена | | net | `tst_javainstaller` | Temurin zip и tar.gz, дерево Mojang, zip-slip, отмена | | net | `tst_modloaderinstaller` | Fabric по сети, Forge через подставную java, откат после сбоя | | backend | `tst_launcherbackend` | всё, что зовёт QML: профили, сборки, настройки, миграция, битые файлы, каталог версий, запуск офлайн / Ely.by с 2FA / Microsoft, Java, сезонная сборка от каталога до раскатки обновления | | qml | `tst_qml` | Qt Quick Test: `DarkCombo`, `LabelledField`, `ProgressPanel`, `LoaderRow`, диалоги с настоящим `LauncherBackend` | | qml | `tst_qml_smoke` | `Main.qml` поднимается, как в `main.cpp`, без единого предупреждения QML, в том числе после смены языка | Ночью и перед релизом ещё собирается Release, ставится через `cmake --install` и запускается на 10 секунд (`tests/scripts/smoke_installed.sh` / `.ps1`). Это ловит то, чего не видят тесты: недоложенные деплоем модули Qt и плагины, ссылки на библиотеки вне дерева, неподписанный бандл. ## Как тесты изолированы **Файлы.** `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 общие на процесс, и код лаунчера их не трогает. `FakeInternet` (`tests/common/fakeinternet.h`) ставит прокси на локальный порт и отключает проверку сертификата. Каждое https-соединение приходит к нему как `CONNECT host:443`, переводится в TLS с тестовым сертификатом из `tests/fixtures/tls/` и обслуживается по таблице маршрутов. Запрос на адрес без маршрута получает 404 и попадает в `unmatched()`. При старте сервер проверяет себя сам. Если TLS-бэкенд Qt не умеет серверный режим, сетевые тесты пропускаются с понятным сообщением, а в CI на Linux (`BILAUNCHER_REQUIRE_FAKE_INTERNET=1`) это ошибка. `FakeMojang` (`tests/common/fakemojang.h`) публикует через `FakeInternet` крошечную, но устроенную как настоящая версию игры: json, `client.jar`, библиотеки, натив под текущую ОС, индекс ресурсов, сами ресурсы, конфиг логирования. **Java.** `fake_java` (`tests/fake_java/main.cpp`, без Qt) копируется в нужную папку как `bin/java` (`java.exe`/`javaw.exe`). На `-version` она отвечает версией из `fake-java.cfg` рядом с собой, а при обычном запуске пишет аргументы в файл, печатает заданные строки, создаёт файлы (так изображается установщик Forge) и выходит с нужным кодом либо падает. **Код лаунчера.** `tests/CMakeLists.txt` собирает все `.cpp` из корня, кроме `main.cpp`, в статическую библиотеку-QML-модуль `bilauncher_testlib`. Новый файл попадёт в тесты сам. QML-файлы берутся тоже все, ресурсы — из строк `RESOURCES` корневого `CMakeLists.txt`. Тестовая библиотека собирается без Qt WebEngine, так что `backend.microsoftAvailable` в тестах всегда `false`. ## Найденные проблемы (QEXPECT_FAIL) Тест, который описывает правильное поведение, но пока падает, помечен `QEXPECT_FAIL`. В отчёте он выглядит как `XFAIL` и прогон не валит. Когда ошибку исправят, тест станет `XPASS` и прогон упадёт: тогда пометку нужно снять. | Тест | Проблема | |------|----------| | `tst_versioninstaller::retryOfLastFileIsNotLost` | если временный сбой (503, таймаут) или неверная контрольная сумма пришлись на последний файл, `retryOrFail()` откладывает повтор на 400 мс, а `pump()` сразу видит пустую очередь и завершает установку ошибкой «файлы отсутствуют» | | `tst_modloaderversionservice::neoForgeLegacyArtifactArrivesFirst` | если ответ старого артефакта `net.neoforged:forge` приходит раньше основного, основной затирает слитые записи (`state->merged = parsed`), и версии NeoForge для 1.20.1 пропадают | | `tst_buildarchiveworker::dotFilesSurviveRoundTrip` | `QZipReader` срезает ведущую точку в именах записей, и после переключения сборок `.fabric/`, `.mixin.out/` возвращаются как `fabric/`, `mixin.out/` | | `tst_javainstaller::cancelDuringDownload` | `JavaInstaller::cancel()` шлёт `canceled` дважды: `abort()` синхронно доводит ответ до обработчика, который уже шлёт сигнал | | `tst_gamelauncher::extractNatives` | битый jar с нативами не считается ошибкой: `QZipReader::isReadable()` проверяет только, что файл открылся | Ещё одна особенность, которую тесты обходят: синглтон `Localization` запоминает JS-движок, который его создал. Второй `QQmlApplicationEngine` после уничтожения первого оставляет этот указатель висячим, поэтому дымовой тест держит один движок на весь прогон, как и приложение. ## CI: `.gitea/workflows/tests.yml` Тесты идут на тех же host-раннерах, что собирают релизы. Отдельные машины и Docker не нужны: Qt, CMake и Ninja там уже есть, пути берутся из `QT_ROOT` и `QT_TOOLS` в `config.yaml` раннера. | Событие | Что запускается | |---------|-----------------| | push в любую ветку, pull request | Linux: сборка с тестами, статические проверки, все тесты | | ночью (02:00 UTC) и вручную (Run workflow) | Linux, Windows и macOS, плюс дымовая проверка установленного дерева на всех трёх | Windows и macOS на каждый коммит не запускаются: вместимость у раннеров 1, и часовая сборка с WebEngine задерживала бы релизы. Перед выпуском версии запустите workflow вручную и дождитесь зелёного прогона. `release.yml` от тестов не зависит. Новый push в ту же ветку отменяет незаконченный прогон (`concurrency`). Отчёт JUnit (`junit.xml`), лог qmllint и полный лог ctest прикладываются к прогону как артефакты. На Linux-раннере полезен `ccache`: с ним повторная сборка занимает минуты. `ci/setup-runner-linux.sh` ставит его вместе с `xvfb`, который нужен дымовой проверке, если деплой Qt не положил плагин `offscreen`. **qmllint.** В коде уже есть 53 предупреждения, а код ради тестов не правится. Поэтому проверка работает как храповик: она падает, только если предупреждений стало больше числа в `tests/qmllint-baseline.txt`. Если часть исправили, опустите число. ## Как добавить тест 1. Создайте `tests/<слой>/tst_<имя>.cpp` с классом на QtTest и в конце файла напишите `BILAUNCHER_TEST_MAIN(Класс)` (или `BILAUNCHER_GUI_TEST_MAIN`, если нужен `QGuiApplication`). Затем `#include "tst_<имя>.moc"`. 2. Добавьте имя в список в `tests/<слой>/CMakeLists.txt`. 3. В `init()` вызывайте `TestEnv::resetLauncherRoot()`, папку игры берите из `TestEnv::freshDir("game")`. 4. Для сети поднимите `FakeInternet` в `initTestCase()` через `BILAUNCHER_START_FAKE_INTERNET(m_net)` и опишите маршруты: `m_net.route("https://host/path", FakeResponse::json(...))`. Для префикса поставьте `*` в конце, для ошибок, задержек и обрывов есть `FakeResponse::error`, `delayMs`, `hang`, `drop`, `truncateTo`. 5. Крупные ответы серверов кладите в `tests/fixtures/`. Если в json нужны имя ОС, архитектура или классификатор нативов, используйте подстановки `%OS%`, `%OTHER%`, `%ARCH%`, `%NATIVE%` и `Fixtures::readTemplate()`. У moc есть ограничение: сырые строки `R"(...)"` и длинные вложенные инициализаторы `{{...}}`, разбитые на строки, внутри файла с `Q_OBJECT` сбивают его разбор («Not a signal or slot declaration» или пустой `.moc`). Такие данные выносите в фикстуры или собирайте по шагам. --- При создании этого документа использовался ИИ.