Тема
Результаты тестов из CI
Kvalli принимает JUnit XML (JUnit, pytest, Jest, Go, .NET, Cypress и большинство других раннеров) и Playwright JSON. Каждая загрузка становится прогоном с веткой, коммитом и ссылкой на сборку того CI-задания, откуда она пришла. Тест связывается с тест-кейсом по ключу кейса в названии, аннотации, теге или (в JUnit) свойстве case: TC-101, @case: TC-101.
Что нужно
Два значения, заданных как секреты CI:
KVALLI_URL— адрес вашего Kvalli, напримерhttps://kvalli.example.com;KVALLI_API_KEY— ключ из Настройки → API-ключи.
Ключ принадлежит одному проекту, и прогон попадает туда. Права по умолчанию (testrun:write, cases:read) — ровно то, что нужно для загрузки, и всё, что ключ умеет: присылать отчёты и их файлы. Закрывать, открывать заново, удалять и перемечать прогоны, трогать майлстоуны и сессии он не может — это runs:write.
API-ключи — часть модуля ci, поэтому загрузка из CI требует тарифа Pro или Enterprise. На бесплатном Starter импортируйте файлы результатов вручную в рабочем пространстве.
Клиенты
Клиенты раздаёт сам ваш Kvalli, поэтому их версия всегда совпадает с установкой и ничего не нужно ставить из публичного реестра:
| Клиент | Где взять |
|---|---|
| CLI (Node 18+) | $KVALLI_URL/api/downloads/kvalli.mjs |
| Репортер Playwright | $KVALLI_URL/api/downloads/kvalli-playwright-reporter.mjs |
| Плагин pytest | $KVALLI_URL/api/downloads/pytest_kvalli.py |
Вебхук вместо шага загрузки
Репозиторий может сам позвать Kvalli, когда пайплайн закончился, — в пайплайн ничего добавлять не нужно. В Источники откройте источник репозитория и нажмите Создать вебхук в блоке «Вебхук пайплайна». Нужны право settings:write, модуль ci и подключение GitLab или GitHub в настройках. Kvalli покажет URL и секрет. Секрет показывается один раз; Новый секрет заменяет его.
GitLab: Settings → Webhooks → Add new webhook. URL, секрет как Secret token, триггер Pipeline events. Тесты читаются из тест-отчёта пайплайна, поэтому публикуйте его:
yamle2e: script: npx playwright test --reporter=junit artifacts: when: always reports: junit: results.xmlGitHub: Settings → Webhooks → Add webhook. URL как Payload URL, content type
application/json, секрет и в Let me select individual events только Workflow runs. GitHub не хранит тест-отчёты, поэтому упавший запуск приходит своими упавшими джобами. Чтобы получать результаты по каждому тесту, оставьте шаг загрузки ниже сbuildId, равным id запуска (${{ github.run_id }}).
Что приходит: пайплайн, который прошёл или упал (не идущий и не отменённый), становится прогоном под источником «CI Pipeline» с веткой, коммитом и ссылкой. Результаты — по тестам из тест-отчёта, а если отчёта нет и пайплайн упал, — каждая упавшая джоба как упавший тест с концом её лога. Так даже сборка, сломавшаяся до запуска тестов, становится падением, которое можно разобрать.
Id пайплайна — это build id прогона, поэтому пайплайн, который ещё и загружает результаты с buildId=$CI_PIPELINE_ID, даёт один прогон, а не два. Прогон запускает те же воркфлоу, что и загрузка из CI: шаблон «Разбор падений» — это «каждое падение в main разбирает Агент 3».
Вызов проверяется до того, как что-либо прочитано: X-Gitlab-Token у GitLab должен совпадать с секретом, X-Hub-Signature-256 у GitHub должен подписывать тело этим секретом, а событие должно относиться к репозиторию источника. Kvalli отвечает сразу и читает результаты потом; источник показывает последний вызов и что из него вышло. Сам Kvalli никуда ничего не отправляет — только отвечает на вызов.
Любой раннер: CLI
sh
curl -sfL "$KVALLI_URL/api/downloads/kvalli.mjs" -o kvalli.mjs
node kvalli.mjs upload test-results/ --name "Unit tests" --env ci- Можно передать файлы и каталоги; в каталоге ищутся
.xmlи.json. Несколько JUnit-файлов (по одному на набор или воркер) становятся одним прогоном. - Каталог
allure-resultsзагружается так же, как его загружалallurectl:node kvalli.mjs upload allure-results. Контейнеры, вложения и служебные файлы (executor.json,categories.json,environment.*) пропускаются молча; из повторов теста остаётся последняя попытка; отправляются только поля, которые читает сервер, поэтому каталог большого набора укладывается в одну загрузку. Скриншоты и другие вложения упавших тестов прикрепляются к их результатам, как у Playwright. --env,--releaseи--nameописывают прогон.--no-failне даёт неудачной загрузке уронить джобу.- Код выхода говорит, удалась ли загрузка, а не прошли ли тесты.
- Для отчёта Playwright скриншоты, видео и трейсы упавших попыток отправляются вместе с прогоном и видны у каждого результата:
--attachments failed(по умолчанию),all— ещё и файлы прошедших тестов,none— ничего; илиKVALLI_ATTACHMENTS. Файлы читаются по путям из отчёта, поэтому загружайте из той же джобы, что запускала тесты. Файл больше 25 МБ или пропавший с диска пропускается и называется в логе. - В GitHub Actions, GitLab CI, Jenkins, Azure Pipelines и CircleCI ветка, коммит, ссылка на сборку и build id берутся из окружения. Build id делает загрузку идемпотентной: та же джоба, загрузившая дважды (повторённый шаг, второй репортер), даёт один прогон. Build id также различает ветви матрицы и параллельные узлы: имя раннера в GitHub, стадию в Jenkins, индекс узла в CircleCI, попытку в Azure и
--shardу Playwright. - Если CI не различает ветви — матрица GitHub на одном self-hosted раннере, ячейки
matrixв Jenkins на одной стадии, — задайте--shardилиKVALLI_SHARDдля каждой ветви (в GitHub —KVALLI_SHARD: ${{ strategy.job-index }}) или дайте каждой свой--nameили--env. Иначе каждая ветвь после первой будет принята за её повтор, и её результаты отбросятся. --build-idзаменяет определённый id; shard к нему всё равно добавляется.
Результаты по ходу прогона
kvalli watch запускает тесты и отправляет каждый результат Allure, как только тест закончился, — как allurectl watch в Allure TestOps:
sh
node kvalli.mjs watch allure-results --name "E2E" -- npx playwright test- Прогон открывается до начала тестов и заполняется по ходу; его страница в Kvalli обновляется сама и показывает «Результаты поступают». Вердикт прогон получает, когда команда закончилась.
- Если джобу убил таймаут или упал раннер, в Kvalli остаётся всё, что успело пройти. Загрузка в конце в таком случае не оставляла ничего.
- Повтор теста заменяет его прежнюю попытку. Скриншоты и другие вложения упавших тестов прикрепляются к результатам, как при
upload. - Новые результаты отправляются раз в 3 секунды (
--interval). Файлы, которые лежали в каталоге до запуска, не отправляются — они от прошлого прогона. - Код выхода — код тестовой команды. Если тесты прошли, а результаты отправить не удалось, — 1, если не задан
--no-fail. Если Kvalli был недоступен при старте, тесты всё равно идут, а результаты загружаются одним прогоном в конце.
Связь результатов без ключей кейсов
Тест, в котором не указан ключ кейса, связывается с кейсом, чьим автотестом он является — по полю кейса «Автотест», которое заполняет импорт из Allure TestOps. Полное имя записывается так, как его пишет Allure: module#test_name для pytest, package.Class.method для JUnit и TestNG, path/file.spec.ts › Describe › title или path/file.spec.ts:21:3 для Playwright, — и сравнивается с тем, что сказано в отчёте. Результаты Allure с id TestOps (метка AS_ID) связываются по нему. Ключ кейса, указанный в тесте, всегда важнее.
Кейс запоминает, как его называют привязанные к нему результаты: testCaseId из результата Allure (он не зависит от строки, на которой стоит тест) и полное имя без номера строки. Поэтому полное имя вида file.spec.ts:21:3, которое сдвигается с каждой правкой выше теста, перестаёт быть проблемой после первого же прогона: результат находит кейс по testCaseId, даже если в файле два теста с одним названием. Хэш, который Allure TestOps хранит у кейса, импорт переносит туда же.
Автотесты тест-плана в CI
Как Allure TestOps, Kvalli запускает пайплайн для тест-плана, и джоба выполняет только его автотесты. На странице плана — «Запустить автотесты в CI» (нужно право pipelines:trigger и подключённый CI, как для обычного запуска пайплайна) или POST /api/plans/<план>/pipeline. В GitLab пайплайн получает переменную KVALLI_PLAN с ключом плана. В GitHub Actions inputs не передаются (GitHub отклоняет input, который workflow не объявил), поэтому в workflow ничего менять не нужно: kvalli testplan сам спрашивает у Kvalli, для какого плана запущен run, по GITHUB_RUN_ID (GET /api/pipelines/<id>/plan). Если Kvalli ещё не связал run с запуском, он подбирает запуск по ветке и файлу workflow за последние 15 минут; два плана, запущенные одновременно на одной ветке и workflow, не различить — шаг падает, а не угадывает.
Джоба записывает автотесты плана в файл, который читают адаптеры Allure (allure-playwright, allure-jest, allure-pytest, allure-java и другие), и запускает тесты как обычно:
yaml
e2e:
variables:
ALLURE_TESTPLAN_PATH: $CI_PROJECT_DIR/testplan.json
script:
- curl -sfL "$KVALLI_URL/api/downloads/kvalli.mjs" -o kvalli.mjs
- node kvalli.mjs testplan # без KVALLI_PLAN ничего не пишет: идут все тесты
- npx playwright test || true
after_script:
- node kvalli.mjs upload allure-results --name "E2E"- Тест попадает в план по Allure ID (id кейса в TestOps для перенесённых кейсов) или по полному имени: полю «Автотест» и тем именам без номера строки, которые кейс узнал из результатов. Поэтому план работает и для кейсов, созданных в Kvalli и связанных с тестом по ключу
TC-…, — после первого прогона. - Кейсы без автотеста в файл не попадают; страница плана и команда называют, сколько их, — их проходят вручную.
kvalli testplanберёт план из аргумента илиKVALLI_PLAN, ключ — изKVALLI_API_KEY(хватает обычного ключа для загрузки). Если план прочитать не удалось, команда завершается с ошибкой, а не пускает весь набор под именем плана.- Результаты, загруженные из этого пайплайна, относятся к плану и видны в его прогрессе.
Метки прогона из окружения
Все три клиента берут три метки прогона из окружения, если они не заданы иначе. Это единственный способ задать их из пайплайна, не меняя код загрузки:
| Переменная | Что задаёт | По умолчанию |
|---|---|---|
KVALLI_RUN_NAME | Название прогона в списке | CLI оставляет пустым; репортер Playwright — Playwright, плагин pytest — pytest |
KVALLI_ENVIRONMENT | Какое окружение проверялось (staging, prod). Нестабильность считается внутри одного окружения, поэтому пишите его одинаково | — |
KVALLI_RELEASE | Что проверялось, например 1.4.2 или коммит | — |
sh
export KVALLI_ENVIRONMENT=staging
export KVALLI_RELEASE="$CI_COMMIT_TAG"
export KVALLI_RUN_NAME="E2E ($CI_JOB_NAME)"Явное значение всегда важнее: --name, --env и --release у CLI, параметры name, environment и release у репортера Playwright, --kvalli-name, --kvalli-env и --kvalli-release у pytest. Репортер Playwright настраивается в playwright.config.ts и своих флагов командной строки не имеет, поэтому пайплайн, которому нужна разная метка на джобу, задаёт эти переменные, а не переписывает конфиг.
Playwright
ts
// playwright.config.ts — после того как репортер скачан рядом
reporter: [['list'], ['./kvalli-playwright-reporter.mjs', { name: 'E2E', environment: 'staging' }]]Каждый проект (браузер) — отдельный результат. Тест, прошедший с повтора, записывается как прошедший с пометкой «нестабильный». Ключи кейсов читаются из названия, аннотаций (test.info().annotations.push({ type: 'case', description: 'TC-101' })) и тегов ({ tag: '@TC-101' }). Неудачная загрузка пишется в лог и никогда не роняет прогон тестов.
Скриншоты, видео и трейсы упавших тестов уходят с прогоном так же, как у CLI (attachments: 'failed' | 'all' | 'none' в параметрах репортера). Чтобы они были, задайте в use screenshot: 'only-on-failure', video: 'retain-on-failure' и trace: 'retain-on-failure'.
pytest
sh
curl -sfL "$KVALLI_URL/api/downloads/pytest_kvalli.py" -o pytest_kvalli.py
pytest -p pytest_kvalli --kvalli --kvalli-name "API tests" --kvalli-env stagingpython
@pytest.mark.case("TC-101")
def test_checkout(): ...Маркер добавляет ключ в JUnit-отчёт как свойство, поэтому названия тестов не меняются. --kvalli отправляет отчёт в конце сессии (если --junitxml не задан, JUnit-файл пишется во временное место). Код выхода pytest сохраняется; о неудачной загрузке только сообщается.
GitHub Actions
yaml
- name: Send results to Kvalli
if: always() # упавшие тесты — самые ценные результаты
env:
KVALLI_URL: ${{ secrets.KVALLI_URL }}
KVALLI_API_KEY: ${{ secrets.KVALLI_API_KEY }}
run: |
curl -sfL "$KVALLI_URL/api/downloads/kvalli.mjs" -o kvalli.mjs
node kvalli.mjs upload test-results/ --name "E2E" --env stagingGitLab CI
yaml
e2e:
script:
- npx playwright test --reporter=junit > results.xml || true
after_script:
- curl -sfL "$KVALLI_URL/api/downloads/kvalli.mjs" -o kvalli.mjs
- node kvalli.mjs upload results.xml --name "E2E" --env stagingБез клиента
POST /api/runs/import с заголовком x-api-key принимает отчёт целым телом или обёрнутым в { rawContent, fileType, name, environment, release, buildId, branch, commit, buildUrl }. ?milestone=MS-3 (ключ или точное название) кладёт прогон в открытый майлстоун; клиенты этот параметр не задают. Подробнее — в API.
Запуск пайплайна из Kvalli — другое право: pipelines:trigger, которого нет у testrun:write ключа загрузки, и только на ветке по умолчанию или ветке autotest/*.
Агент 2 проверяет свои автотесты
Проверить в CI у merge request Агента 2 запускает эту ветку в вашем пайплайне и читает, что упало. Что нужно от пайплайна:
- GitHub Actions: workflow с
on: workflow_dispatch, указанный в CI-воркфлоу источника репозитория. GitHub запускает файл workflow из самой ветки, поэтому в ветку, созданную до появления workflow, сначала нужно влить ветку по умолчанию; Kvalli скажет об этом, а не запустит. GitLab CI: пайплайн, который можно создать через API на веткеautotest/*. - Пайплайн запускает сгенерированный тест и поднимает всё, что тесту нужно. Пайплайн, который только линтит или собирает, станет зелёным, ничего не доказав.
- Результаты загружаются в Kvalli (любым клиентом выше, с build id) — тогда чтение точное: берётся результат именно этого теста с ошибкой и стеком. Без них Kvalli читает конец логов упавших джоб (до трёх; джобы, которым разрешено падать, пропускаются).
Падение, которое модель относит к коду теста, исправляется и коммитится в ту же ветку, не больше двух раз. Падение в продукте, окружении или других тестах сообщается, а тест остаётся как есть.