Тест-планы: расширенные возможности
Дополнение к Тест-планам. Здесь описаны функции
возможности уровня исполнения:
- типизированные условия зависимостей (
on_success/on_fail/
always/on_skipped); - пер-айтем ретраи, таймауты, флаги
criticalиcontinueOnError; - стабильные ключи зависимостей
itemUid; - живой вид запуска (Graph / Timeline / Items / Logs / Artifacts /
Report); - шесть форматов отчётов и сравнение запусков;
- опциональный путь распределённого раннера для айтемов плана;
- бэкенд поиска сущностей, на котором работают все пикеры в UI.
О URL в примерах: используется
http://localhost:5770как адрес
Mockarty по умолчанию. Замените на адрес вашей установки. См.
Полезные функции и советы.
Связанные страницы: Тест-планы ·
Рецепты API тест-планов ·
Тест-планы в CI/CD ·
Поиск сущностей ·
Руководство по SDK
Режимы исполнения
У каждого плана есть поле executionMode — одно из трёх значений.
| Режим | Когда использовать | Как работает |
|---|---|---|
fifo |
Легаси-последовательные планы; по одному шагу за раз. | Айтемы идут в порядке возрастания order; падение обрывает остальные (если не переопределено). |
parallel |
Независимые айтемы, которые можно запускать одновременно. | Все айтемы диспатчатся параллельно; рёбра зависимостей не учитываются. |
dag |
Типизированные зависимости с условиями (режим по умолчанию для новых). | Айтемы идут в порядке зависимостей; условие решает, когда айтем становится готов к запуску. |
Если executionMode не задан, сервер автоматически выбирает dag при
наличии хотя бы одного айтема с типизированными gates, иначе fifo.
Ради обратной совместимости по-прежнему принимается прежнее значение
schedule (parallel / dag).
Айтемы и зависимости
ItemUID — стабильный ключ зависимости
Каждый айтем плана несёт itemUid (UUID). Он генерируется на сервере
при первом сохранении и остаётся стабильным при правках. Рёбра
зависимостей указывают на itemUid, а не на UUID внешней сущности
(refId), поэтому:
- одну и ту же коллекцию / конфиг можно добавить дважды (например,
smoke + regression + cleanup) без неоднозначности; - подмена подлежащей сущности (правка
refId) не ломает граф плана; - переименование или удаление сущности влияет только на этот айтем, не
на соседей.
Легаси-планы, созданные до появления itemUid, продолжают работать:
слой исполнения на этапе построения DAG откатывается на refId, а UI
дозаполняет UID при следующем сохранении.
Условия зависимостей (gates)
Каждая зависимость — это пара { itemUid, condition }. Условие
определяет, когда зависимый айтем становится готовым к запуску.
| Gate | Зависимый запускается, если предок … | Типичное применение |
|---|---|---|
on_success |
завершился со статусом passed. |
Основной happy-path. По умолчанию. |
on_fail |
завершился со статусом failed. |
Ветки cleanup / rollback. |
always |
достиг любого терминального статуса (passed/failed/skipped/cancelled). | Teardown / очистка ресурсов. |
on_skipped |
был пропущен (например, его собственный gate не сработал). | Ветки восстановления. |
Пустое / отсутствующее condition трактуется как on_success —
безопасный исторический дефолт. Когда gate не сработал, зависимый
айтем помечается как skipped с skipReason: gate_not_met.
Пример — три айтема, два из которых делят шаг очистки:
{
"name": "API regression",
"executionMode": "dag",
"items": [
{
"itemUid": "11111111-1111-1111-1111-111111111111",
"type": "functional",
"refId": "aaaa...",
"order": 1,
"name": "Auth smoke"
},
{
"itemUid": "22222222-2222-2222-2222-222222222222",
"type": "fuzz",
"refId": "bbbb...",
"order": 2,
"name": "Auth fuzz",
"gates": [
{ "itemUid": "11111111-1111-1111-1111-111111111111", "condition": "on_success" }
]
},
{
"itemUid": "33333333-3333-3333-3333-333333333333",
"type": "functional",
"refId": "cccc...",
"order": 3,
"name": "Teardown",
"gates": [
{ "itemUid": "11111111-1111-1111-1111-111111111111", "condition": "always" },
{ "itemUid": "22222222-2222-2222-2222-222222222222", "condition": "always" }
]
}
]
}
Шаг teardown выполнится независимо от того, прошёл smoke или fuzz,
упал или был пропущен.
Легаси dependsOn
Планы, сохранённые до редизайна, использовали dependsOn: [UUID, ...]
в каждом айтеме. Сервер по-прежнему читает это поле: каждый элемент
раскрывается в типизированное ребро { itemUid, condition: on_success }
на рантайме. Новому коду стоит писать типизированный gates[] — он
строго более выразителен.
Политика ретраев
У любого айтема может быть объект retry, управляющий поведением при
падении.
"retry": {
"maxAttempts": 3,
"backoffMs": 5000,
"onlyOn": ["timeout", "connection reset"]
}
| Поле | Диапазон | Значение |
|---|---|---|
maxAttempts |
1–10 | Всего попыток, включая первую (т.е. 3 = исходная + до 2 ретраев). 1 отключает ретраи. |
backoffMs |
0–300000 мс | Базовая линейная задержка. Реальная пауза = backoffMs × номерПопытки. 0 — мгновенный повтор. |
onlyOn |
массив строк | Совпадение подстроки без учёта регистра в тексте ошибки. Пусто — ретраим на любую ошибку. |
Каждая повторная попытка получает свежий per-attempt контекст: timeoutMs
действует на попытку, а не на весь айтем. На успехе оставшиеся
попытки пропускаются. При отмене контекста (run cancel или
остановка шедулера) текущая попытка прерывается, а новые не
стартуют.
Таймаут на айтем
timeoutMs ограничивает каждую попытку через context.WithTimeout.
Когда deadline наступает:
- попытка помечается как
failedсerror = "timeout"; - в состоянии айтема выставляется
timeoutHit: true; - если остались ретраи и
onlyOnразрешает — следующая попытка
стартует после заданной паузы.
Допустимый диапазон: 0–14 400 000 мс (4 часа). 0 означает «нет
переопределения per-item дедлайна» (применится общий дефолт
оркестратора). Более длительные задачи стоит разбивать на отдельные
запланированные прогоны.
Критические айтемы
Пометьте айтем critical: true, когда его падение должно останавливать
весь прогон. Семантика:
- Если айтем завершился со статусом
failed, оркестратор отменяет
все ещё не начатые и работающие айтемы. - Отменённые соседи получают
status: cancelled,
cancelCause: dependency_critical_failure. - Сам прогон завершается со статусом
failed.
Это не то же самое, что просто зависеть от айтема: соседи, которые уже
успели завершиться, остаются нетронутыми — каскадится только ещё не
сделанная работа.
Используйте critical для предусловий (setup, auth, миграция), когда
запускать остальную часть плана после их падения бессмысленно или
небезопасно.
ContinueOnError
continueOnError: true работает наоборот: «даже если этот айтем
упал — downstream gate on_success должны вести себя так, будто он
прошёл». Статус прогона всё равно учитывает падение в счётчике
failedItems, но последующие работы не пропускаются, а выполняются.
Полезно для нестабильных айтемов, чьё падение вы хотите зафиксировать,
но не принудительно блокировать.
Условия on_fail и on_skipped не затрагиваются — они по-прежнему
читают реальный статус.
Живой вид запуска
Страница Run Detail (/ui/test-plans/<id>/runs/<runId>) открывает
шесть вкладок:
| Вкладка | Что видно |
|---|---|
| Overview | Конверт запуска (статус, старт/финиш, длительность, инициатор), итоги по айтемам, чип статуса. |
| Graph | DAG на Cytoscape с живой покраской статусов. Клик по ноде открывает боковую панель. |
| Timeline | Gantt: строки — айтемы, ось X — стеночное время. Зависимости — пунктирные линии. Лучший вид для параллельных прогонов. |
| Items | Плоская таблица: порядок, имя, тип, статус, попытки, длительность. |
| Logs | Живой tail per item. Фильтр по айтему + уровню + regex. Автопрокрутка с паузой. |
| Artifacts | Список вложений (allure.zip, har.json, скриншоты, JUnit). Inline-превью для мелких text / JSON. |
| Report | Allure-рендер дерева шагов + кнопки выгрузки для всех форматов. |
| Compare | Выбор второго прогона и per-item diff (доступно на любом завершённом прогоне). |
Обновления приходят по SSE. Если браузер разрывает соединение,
переподключение прозрачное — поток возобновляется с последнего
полученного event ID через заголовок Last-Event-ID, так что ни одно
событие не теряется.
Форматы экспорта
Каждый прогон может быть выгружен в шести форматах. Выберите тот,
который понимает ваша downstream-обвязка — сервер генерирует по
запросу.
| Эндпоинт | MIME | Когда нужен |
|---|---|---|
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report |
application/json |
Allure JSON summary. |
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.zip |
application/zip |
Allure-архив (result-*.json + вложения). |
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.junit.xml |
application/xml |
JUnit XML для Jenkins / GitLab / GitHub Actions. |
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.md |
text/markdown |
Summary для Slack / email / wiki. |
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.html |
text/html |
Автономный HTML, открывается в браузере, Save-as-PDF. |
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.unified.json |
application/json |
Родной конверт Mockarty (план + счётчики + per-item state). |
Все форматы побайтово детерминированы для одних и тех же входных
данных — удобно для snapshot-тестов. Эндпоинты Allure уважают
If-None-Match со строгим ETag; остальные регенерируются на каждый
запрос (это дёшево).
CLI
mockarty-cli testplan report <runID> --plan plan-abc --format allure -o report.json
mockarty-cli testplan report <runID> --plan plan-abc --format zip -o report.zip
mockarty-cli testplan report <runID> --plan plan-abc --format junit -o report.junit.xml
mockarty-cli testplan report <runID> --plan plan-abc --format markdown -o report.md
mockarty-cli testplan report <runID> --plan plan-abc --format html -o report.html
mockarty-cli testplan report <runID> --plan plan-abc --format unified -o report.unified.json
SDK
| Go | Python | Java |
|---|---|---|
GetRunReport |
get_run_report |
getRunReport |
GetRunReportZIP |
get_run_report_zip |
getRunReportZip |
GetRunReportJUnit |
get_run_report_junit |
getRunReportJUnit |
GetRunReportMarkdown |
get_run_report_markdown |
getRunReportMarkdown |
GetRunReportHTML |
get_run_report_html |
getRunReportHTML |
GetRunReportUnified |
get_run_report_unified |
getRunReportUnified |
Сравнение прогонов
Сравните любые два прогона. Оба должны жить в пространстве имён вызывающего.
Сравнение прогонов разных планов разрешено — в ответе ставится
summary.differentPlans: true, чтобы UI мог показать баннер.
GET /api/v1/test-plans/runs/compare?run_a=<runID>&run_b=<runID>
Передавайте более старый прогон как run_a, а более новый как
run_b — тогда знаки регрессий и улучшений читаются интуитивно.
Классификатор ставит каждому айтему одну из меток: pass_to_fail,
fail_to_pass, skipped_to_ran, ran_to_skipped, fail_to_fail,
pass_to_pass, added, removed, unchanged. Айтем pass_to_pass
дополнительно помечается durationWorsened, если новый прогон занял
минимум на 20 % дольше базового и абсолютная дельта превысила
250 мс. Мелкий шум на быстрых айтемах таким образом игнорируется.
CLI
mockarty-cli testplan compare-runs <runA> <runB> # таблица, скрывает unchanged
mockarty-cli testplan compare-runs <runA> <runB> --diff-only=false # показывает и unchanged
mockarty-cli testplan compare-runs <runA> <runB> -o json | jq .summary
Exit-код равен 1 при наличии хотя бы одной регрессии — удобно для
CI-гейта.
Поиск сущностей
Все пикеры в UI (Add Item, выбор зависимости, выбор запусков для
агрегированного отчёта, цель расписания) дергают единый search-эндпоинт:
GET /api/v1/entity-search?type=<kind>&q=<имя>&namespace=<ns>&limit=20&offset=0
Поддерживаемые значения type: mock, test_plan, perf_config,
fuzz_config, chaos_experiment, contract_pact. q — поиск
подстроки без учёта регистра по имени; ответ содержит
{ items: [{id, type, name, namespace, createdAt, numericId}], total }.
Полная форма ответа, примеры SDK + CLI и описание MCP-инструмента —
на отдельной странице Поиск сущностей.
Путь распределённого раннера (опция)
По умолчанию admin-нода выполняет каждый айтем тест-плана в своём
процессе. Когда нужно вынести исполнение айтемов на отдельный воркер
mockarty-runner — ради изоляции, сетевой топологии или просто
масштаба — включите claim-очередь:
- Admin-нода: установите
MOCKARTY_RUNNER_TESTPLAN_ENABLED=true.
Оркестратор теперь публикует айтемы во внутреннюю claim-очередь
вместо локального выполнения. - Нода-раннер: установите
RUNNER_TESTPLAN_ENABLED=trueи
COORDINATOR_URL=https://<admin>. Опционально:
RUNNER_TESTPLAN_CONCURRENCY(по умолчанию4) — сколько айтемов
раннер может брать параллельно. - Таргетирование per-item: выставьте
runnerLabels: ["gpu", "staging"]на айтемах, которые должен
подхватывать конкретный пул раннеров. Пустой список — «любой
раннер, чьё пространство имён совпадает». Для более выразительного
таргетинга (OR, NOT, regex) используйтеrunnerLabelExpr— см.
Метки раннеров и таргетинг. Поля
AND-комбинируются, если заданы оба; айтем с выражением минует
in-memory claim queue и идёт через координатор, чтобы DSL-evaluator
на стороне раннера мог корректно отработать AST.
Раннер long-poll-ит три эндпоинта:
| Метод | Путь | Назначение |
|---|---|---|
| POST | /api/v1/runner/testplan/claim |
Забрать следующий подходящий айтем (до 30 с). |
| POST | /api/v1/runner/testplan/report |
Отрапортовать терминальный статус + summary + артефакты. |
| POST | /api/v1/runner/testplan/heartbeat |
Удерживать клейм живым, пока айтем выполняется. |
При рестарте admin-ноды или истечении heartbeat-дедлайна не
подтверждённая работа автоматически переочередуется — путь через
раннер имеет at-least-once семантику, и сами раннеры должны делать
попытки идемпотентными.
Если на admin-ноде MOCKARTY_RUNNER_TESTPLAN_ENABLED не выставлен,
все три эндпоинта отвечают 503 Service Unavailable, и раннер
корректно откатывается на классический task-протокол — старые раннеры
не ломаются.
SDK / CLI / MCP
Каждый эндпоинт с этой страницы доступен через:
- SDK Go / Python / Java — типизированные методы для create, run,
report, compare, schedule и CRUD вебхуков. См.
Руководство по SDK. - CLI —
mockarty-cli testplan <create|run|report|compare-runs| schedule|webhook|stream>. См.
Руководство по CLI. - MCP-инструменты — агенты вызывают
list_test_plans,
run_test_plan,get_test_plan_run,compare_test_plan_runs,
search_entities, а также экспорты (get_test_plan_run_report,
…_junit,…_markdown,…_html,…_unified). См.
ИИ-функции.
Обратная совместимость
- В новых интеграциях предпочтительнее
executionMode
(fifo/parallel/dag). Прежнее значениеschedule
по-прежнему принимается и держится консистентным, поэтому существующие
интеграции продолжают работать без изменений. - Планы до редизайна использовали
dependsOn: [UUID, ...].
Оркестратор по-прежнему читает этот список и конвертит каждый
элемент в типизированный gateon_successна рантайме. Повторное
сохранение плана в новом UI материализует типизированную форму
gates[]. items[].itemUidгенерируется при первом сохранении. Легаси-планы,
не получившие UID, продолжают работать — оркестратор использует
refIdдля разрешения зависимостей до следующего сохранения.
Траблшутинг
/api/v1/runner/testplan/*возвращает503— claim-очередь
отключена. ВыставьтеMOCKARTY_RUNNER_TESTPLAN_ENABLED=trueна
admin-ноде и перезапустите.- Айтем остаётся в
pendingбесконечно — при заданных
runnerLabelsпроверьте, что хотя бы одинmockarty-runnerнесёт
подмножество лейблов. На admin/metricsвиден
testplan_claim_queue_depth, на раннере —testplan_runner_claims_total. - Ретраи не срабатывают — убедитесь, что
retry.maxAttempts > 1.
Если заданonlyOn, текст ошибки от вашего исполнителя должен
содержать одну из подстрок (сравнение без учёта регистра). - Критический каскад не сработал — каскадятся только айтемы,
которые на момент падения критического были ещё в pending или
running. Уже завершённые остаются в своём финальном состоянии. - Живой стрим рвётся и теряет события — браузер отправляет
Last-Event-IDпри переподключении, и сервер возобновляет поток.
Если вы за корпоративным прокси, который режет SSE-заголовки,
откатитесь на опросGET /runs/:idс коротким интервалом.
Таймлайн по попыткам (Gantt-стек)
Если у айтема настроена retry-политика, вкладка Timeline рисует одну
под-полосу на каждую попытку, выстраивая их слева направо вместо
единой агрегированной полосы. Цвет под-полосы соответствует статусу
именно этой попытки (passed / failed / timeout), так что транзитные
падения с последующим успешным ретраем видны сразу.
Что показывает UI:
- Ярлык под-полосы — номер попытки (
1,2,3, …). Ширина
пропорциональна окну выполнения этой попытки. - Тултип — статус, номер попытки / общее число, длительность,
отметка таймаута, идентификатор раннера, текст ошибки этой
попытки (усечённый до 200 символов). - Индикатор таймаута — под-полосы попыток, прерванных
per-item TimeoutMs, рисуются пунктирной рамкой danger-цвета. - Активные попытки — «пульсируют», пока не прилетит finish-событие
по SSE. Ручное обновление страницы не требуется.
Формат payload: каждая строка ItemState в GET /test-plan-runs/:id
теперь содержит массив attemptLog:
{
"itemUid": "…",
"status": "passed",
"attempts": 3,
"attemptLog": [
{"startedAt": "2026-04-20T10:00:00Z", "completedAt": "2026-04-20T10:00:02Z",
"durationMs": 2000, "status": "failed", "error": "transient"},
{"startedAt": "2026-04-20T10:00:03Z", "completedAt": "2026-04-20T10:00:04Z",
"durationMs": 1000, "status": "failed", "error": "transient"},
{"startedAt": "2026-04-20T10:00:05Z", "completedAt": "2026-04-20T10:00:06Z",
"durationMs": 1000, "status": "passed"}
]
}
Для прогонов, созданных до этого релиза, attemptLog пустой — UI в
таком случае показывает единую полосу с аннотацией счётчика попыток
(×3).
Сравнение таймлайнов (наложение прогонов)
Под основным таймлайном можно наложить один или несколько предыдущих
прогонов того же плана на общую ось времени. Кнопка Добавить прогон
тянет itemsState выбранного прогона и отрисовывает его отдельной
дорожкой под основным. Каждая дорожка использует ту же логику
под-полос (стек попыток, индикатор таймаута, цветовая кодировка), так
что расхождения между прогонами — шаг, который раньше выполнялся 2 с,
а сейчас 8 с, или ретрай, который в этом прогоне не сработал — видны
без переключения вкладок.
Сравнение работает клиентски: выбор прогона — это один
GET /api/v1/test-plans/runs/:id и локальный рендер. Никаких
серверных объединений и нагрузки на оркестратор.
Агрегированные отчёты по запускам (Aggregate reports)
Агрегированный отчёт сворачивает два и более существующих test run в
один релизный отчёт. Пригодится, когда одна релизная проверка состоит из
нескольких независимых запусков (fuzz-кампания + chaos-эксперимент +
функциональная регрессия), а вам нужно одно число pass/fail и один отчёт
для stakeholder’ов.
Отчёт без состояния: ничего нового не сохраняется. Вы передаёте список
run ID на каждый вызов, и сервер пересчитывает отчёт на лету. Исходные
запуски сохраняют свою историю, отчёты и retention — нетронутыми.
Построение агрегированного отчёта
# CLI — HTML самодостаточен и дружелюбен к print-to-PDF
mockarty-cli testplan aggregate-report <run-id-1> <run-id-2> [<run-id-3> ...] \
--name "Release 1.4 gate" --format html -o release.html
// Go SDK
data, err := client.TestRuns().AggregateRunsReport(ctx,
mockarty.AggregateRunsReportRequest{
Name: "Release 1.4 gate",
RunIDs: []string{runAID, runBID, runCID},
},
mockarty.AggregateReportFormatHTML)
Все run ID должны жить в namespace вызывающего (admin и support могут
кросс-namespace). Чтобы получить отчёт по другому набору запусков — просто
вызовите снова с новыми ID: создавать, обновлять или удалять нечего.
Форматы
| Формат | MIME | Назначение |
|---|---|---|
unified |
application/json |
Программные потребители (SDK / CLI / CI) |
markdown |
text/markdown |
Slack / wiki, ручной просмотр |
html |
text/html |
Релизный отчёт — самодостаточный, print-to-PDF |
junit |
application/xml |
Приём в CI (Jenkins / GitLab / и т. д.) |
mockarty-cli testplan aggregate-report r1 r2 --format junit -o ci.xml
mockarty-cli testplan aggregate-report r1 r2 --format markdown -o out.md
HTML-вывод инлайнит CSS и графики, поэтому открывается в air-gapped
браузере и чисто сохраняется в PDF через диалог печати браузера.
Когда использовать агрегированный отчёт, а когда Test Plan
- Test Plan — вы заранее решаете, что группа айтемов выполняется
вместе с явными зависимостями, retry, gate’ами и общим отчётом.
Подходит для релизных пайплайнов и nightly. - Агрегированный отчёт — вы хотите подвести итог постфактум. Запуски
уже есть, возможно из разных CI-job’ов или ad-hoc, и вам нужен один отчёт.
Без оркестрации, без сохранённой сущности — только пересчитываемое
представление.