Files
BiLauncher/doc/testing.md
T

172 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-10-12 13:14:08 +03:00
# Автотесты
Тесты лежат в `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` | переключение сборок туда и обратно, продолжение после обрыва, пак |
| unit | `tst_gamedirimport` | выбор версии по `launcher_profiles.json` и `versions/`, модлоадер по библиотекам и id, папка не меняется |
2026-10-12 13:14:08 +03:00
| 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 и плагины, ссылки на библиотеки вне дерева, неподписанный бандл.
## Как тесты изолированы
2026-10-13 08:51:07 +03:00
**Файлы.** `LauncherPaths` строит пути от домашней папки. Общий `main` тестов
(`tests/common/testenv.h`) до создания приложения направляет `BILAUNCHER_HOME` (а заодно `HOME`
и `USERPROFILE`) на временную папку, и каждый процесс работает в своей, на всех ОС, параллельно.
`BILAUNCHER_HOME` нужна ради Windows: там `QDir::homePath()` берёт профиль у системы и `HOME`
с `USERPROFILE` не читает. Дочерние процессы наследуют переменную. Если папка лаунчера или папка
игры по умолчанию всё же оказалась вне песочницы, процесс сразу завершается с кодом 2 и ничего не
трогает. Дымовая проверка установленной сборки на Windows (`smoke_installed.ps1`) тоже запускает
лаунчер с `BILAUNCHER_HOME` на временную папку.
2026-10-12 13:14:08 +03:00
**Сеть.** Адреса серверов в сервисах зашиты константами, а свои `QNetworkAccessManager` они
создают сами. Зато прокси и SSL-настройки у Qt общие на процесс, и код лаунчера их не трогает.
`FakeInternet` (`tests/common/fakeinternet.h`) ставит прокси на локальный порт и отключает
проверку сертификата. Каждое https-соединение приходит к нему как `CONNECT host:443`, переводится
в TLS с тестовым сертификатом из `tests/fixtures/tls/` и обслуживается по таблице маршрутов.
Запрос на адрес без маршрута получает 404 и попадает в `unmatched()`. При старте сервер проверяет
себя сам. Если TLS-бэкенд Qt не умеет серверный режим, сетевые тесты пропускаются с понятным
2026-11-01 21:07:03 +03:00
сообщением, а в CI на всех ОС (`BILAUNCHER_REQUIRE_FAKE_INTERNET=1`) это ошибка.
QML-тесты также завершаются ошибкой, если стенд не поднялся.
2026-10-12 13:14:08 +03:00
`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_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`,
2026-11-01 21:07:03 +03:00
`hang`, `drop`, `truncateTo`. Для заданного порядка ответов установите `deferred = true`
и вызовите `releaseResponse(url)` после обработки другого ответа клиентом.
2026-10-12 13:14:08 +03:00
5. Крупные ответы серверов кладите в `tests/fixtures/`. Если в json нужны имя ОС, архитектура или
классификатор нативов, используйте подстановки `%OS%`, `%OTHER%`, `%ARCH%`, `%NATIVE%` и
`Fixtures::readTemplate()`.
У moc есть ограничение: сырые строки `R"(...)"` и длинные вложенные инициализаторы `{{...}}`,
разбитые на строки, внутри файла с `Q_OBJECT` сбивают его разбор («Not a signal or slot
declaration» или пустой `.moc`). Такие данные выносите в фикстуры или собирайте по шагам.
---
При создании этого документа использовался ИИ.