workspace/tests/
Декларативные сценарии интеграционных тестов (dwe test).
Содержание
Заголовок раздела «Содержание»- Назначение
- Расположение файлов
- Схема сценария
- Что делает запуск сценария
- Модель изоляции
- Структура
.dwe/tests/ - Teardown
- Отчёты об ошибках
dwe test rundwe test listdwe test cleandwe validate tests- Сканер изоляции compose
- Коды выхода
- JSON-вывод
- Документированные ограничения
- Связанные команды
Назначение
Заголовок раздела «Назначение»Deploy-пайплайн проекта dwe обычно проверяется только на одном рабочем окружении разработчика — нет способа ответить на вопрос “переживёт ли чистый деплой эти изменения конфига?” без того, чтобы затронуть (или подвергнуть риску) окружение, в котором вы работаете.
dwe test запускает deploy-пайплайн проекта — и любые последующие проверки или проектные команды, которые вы объявите — внутри свежей, полностью изолированной, одноразовой копии проекта. Каждый сценарий — это один YAML-файл под workspace/tests/: одно изолированное окружение, один чистый деплой с параметрами этого сценария, за которым следуют упорядоченные шаги по той же схеме шагов, что и в deploy.yml.
Изоляция затрагивает только состояние, управляемое dwe (compose-проект, порты, volume’ы, файлы под .dwe/) — см. Документированные ограничения о том, что она не покрывает.
Расположение файлов
Заголовок раздела «Расположение файлов»workspace/tests/<scenario>.yml — один файл на сценарий. Имя сценария — это базовое имя файла, а не поле (симметрично с workspace/services/<name>/service.yml): файл workspace/tests/redis-off.yml определяет сценарий с именем redis-off.
Имена сценариев должны уже быть валидными фрагментами имени compose-проекта: строчные алфанумерики, -, _, без ведущего разделителя (^[a-z0-9][a-z0-9_-]*$). Имена никогда не санитизируются — несоответствующее имя файла является жёсткой ошибкой на этапе обнаружения, для каждого файла под workspace/tests/*.yml/*.yaml.
Загрузчик строгий (KnownFields(true), как и у семейства pipeline-загрузчиков), с одним намеренным отличием от этого семейства: пустой или полностью закомментированный файл сценария — это ошибка, а не отсутствующий-и-задефолченный документ — у сценария нет осмысленного значения по умолчанию.
Отсутствие директории workspace/tests/ не является ошибкой: dwe test list сообщает об отсутствии сценариев, а dwe test run (без аргументов) ничего не запускает.
Схема сценария
Заголовок раздела «Схема сценария»description: "Deploy with redis disabled"
env: services: disable: [redis] # и/или enable: [...] vars: app.http_port: auto # auto = выделить свободный host-порт перед деплоем db.port: auto db.password: "test-pw" # любое переопределение vars; попадает в local.yml копии
timeout: 15m # бюджет по времени на весь сценарий
steps: # упорядоченные, выполняются ПОСЛЕ неявного деплоя; формат шагов deploy.yml - name: "app answers" type: builtin cmd: http_check with: { url: "http://localhost:${vars.app.http_port}/up", status: 200, contains: "OK" }
- name: "create dump" type: command # обычная пользовательская команда; private-команды разрешены cmd: db:dump
- name: "dump exists" type: builtin cmd: file_exists with: { path: "dumps/db-latest.sql.gz" }description
Заголовок раздела «description»Человекочитаемое описание, показываемое dwe test list дословно — не переводится (строки отображения в остальном dwe локализуются, но описания сценариев — это пользовательский контент и остаются как написано, spec §8).
env.services
Заголовок раздела «env.services»env: services: enable: [worker] disable: [redis]Принудительно включает или выключает названные сервисы в копии, поверх того, что уже включено в workspace/local.yml. Отображается в services.<name>.enabled: true/false в сгенерированном local.yml (ниже) — тот же эффект, что и dwe services enable/disable --apply, только в рамках копии.
env.vars и auto-порты
Заголовок раздела «env.vars и auto-порты»env: vars: app.http_port: auto db.password: "test-pw"Каждый ключ — это dot-путь относительно vars. (app.http_port → vars: { app: { http_port: … } } в local.yml копии), переопределяющий то, что установлено в проекте или в собственном local.yml разработчика.
auto — единственное магическое значение: перед деплоем runner выделяет свободный host-порт и записывает конкретное число на его место. Шаги сценария видят выделенное значение через ${vars.app.http_port} (рендеринг на этапе резолва, ниже).
auto не требуется просто чтобы изолировать host-порт сервиса: каждый host-порт, объявленный включённым сервисом под services.<name>.ports, переназначается на свободный порт автоматически (см. Автоматическая изоляция host-портов ниже). env.vars: { …: auto } — для остаточного случая: compose-файл, который подставляет host-порт из var, не объявленного в конфигурации сервиса, — когда вы хотите, чтобы раннер выделил и подставил это значение; шаг тогда читает его через ${vars.<path>}.
timeout
Заголовок раздела «timeout»Бюджет по времени на весь сценарий (деплой + все шаги), например 15m. Парсится через time.ParseDuration и должен быть строго положительным. Приоритет: флаг dwe test run --timeout (переопределяет любой сценарий) > это поле > значение по умолчанию 30 минут. При истечении таймаута выполняющийся подпроцесс или шаг убивается, сценарий помечается как проваленный, а teardown всё равно выполняется (со своим собственным свежим дедлайном, а не истёкшим).
Обычные шаги пайплайна, по той же схеме, что использует deploy.yml — type: shell / command / dwe / builtin, с when: — резолвятся и выполняются тем же движком. У шага нет поля service:/dir: (у deploy его тоже нет); шаг, которому нужно выполниться в контексте сервиса, использует type: command и отдаёт эту деталь пользовательской команде. Сценарий без steps: валиден и уже полезен: “деплой с этими параметрами проходит успешно, и контейнеры поднимаются здоровыми”.
Шаг также может задать собственный timeout: (например, timeout: 5s на шаге http_check) — то же общее, опциональное поле движка, описанное в Полях шага; отсутствие поля или 0 оставляет шаг неограниченным.
Шаги type: command диспетчеризуются через обычный реестр команд проекта, включая private-команды — так что проект может определить тест-специфичные команды (например, db:dump выше) и держать их вне повседневного листинга команд, продолжая при этом выполнять их из сценария. hide-команды — другое дело: пайплайны пропускают их согласно существующему контракту, поэтому тест-специфичная команда должна быть private, а не hide.
Любой билтин-предикат (KindPredicate) (file_exists, executable_in_path, tcp_reachable, containers_running, env_keys_present, config_keys_present, предикат shell, и билтин http_check ниже) может использоваться напрямую как тело шага, где он ведёт себя как проверка (assertion): false проваливает шаг с собственным сообщением предиката, true проходит молча. Это общая возможность движка (применима и к шагам deploy.yml/reset.yml/lifecycle.yml), а не тест-специфичная — полную механику, включая исключение always-run, которое не даёт гейтам актуальности деплоя пропустить проверку, см. в справочнике билтинов.
http_check (новый билтин, вид predicate) дополняет tcp_reachable для веб-стеков, которым нужно несколько секунд после up, прежде чем они начнут отвечать:
- name: "app answers" type: builtin cmd: http_check with: url: "http://localhost:${vars.app.http_port}/up" status: 200 contains: "OK" # опциональная подстрока в теле retries: 10 # опционально; по умолчанию 0 interval: 1s timeout: 2sПолный список параметров и семантику повторов см. в справочнике билтинов.
Рендеринг ${...} происходит в движке пайплайна, ровно один раз. Шаги сценария резолвятся тем же путём, что и шаги deploy.yml, поэтому cmd, строковые листья with, check, timeout и shell-when: рендерятся на этапе резолва по резолвленному конфигу копии — ${vars.app.http_port} резолвится в конкретный выделенный порт. Отдельного прохода на стороне сценария нет: рендеринг не идемпотентен, и второй проход развернул бы дважды переменную, значение которой само является ссылкой ${...}, — сценарий проверял бы то, чего деплой-пайплайн никогда не выполняет.
Поэтому здесь действуют те же правила, что и для шагов деплоя: нераспознанная голова (${HOME}, опечатка) остаётся литеральным ${...}, а не схлопывается в пустую строку, а у ${param.*} / ${context.*} / ${files.*} / ${generated.*} / ${args} на этом пути нет источника — их использование валит сценарий на этапе резолва с сообщением, называющим конкретное пространство имён. Пути в параметрах билтина (например, path: у file_exists) резолвятся относительно корня копии — проверки всегда осматривают тестовое окружение, а не исходное дерево.
Проваленный шаг проваливает сценарий; оставшиеся шаги пропускаются.
Что делает запуск сценария
Заголовок раздела «Что делает запуск сценария»По порядку, для каждого сценария:
- Захватить per-scenario flock, загрузить и провалидировать файл сценария.
- Скопировать проект в изолированное дерево.
- Сгенерировать
local.ymlкопии (seed +env:этого сценария + свежая идентичность) и docker-файл идентичности, затем записать durable-манифест запуска. - Выполнить
dwe validateв копии (дешёвый fail-fast). - Выполнить
dwe deploy run --silentв копии — реальный deploy-пайплайн, а не пересобранное приближение. - Выполнить
steps:in-process по конфигу копии. - Снести всё (если не указан
--keep).
Модель изоляции
Заголовок раздела «Модель изоляции»Копирование дерева. Runner копирует проект в .dwe/tests/runs/<scenario>/ (внутри корня проекта, чтобы остаться в области видимости file-sharing Docker Desktop/OrbStack для bind-монтирования). Отбор файлов для копии основан на git: отслеживаемые + неотслеживаемые-но-не-игнорируемые файлы (git ls-files -co --exclude-standard), всегда исключая .dwe/, .env и .git/. Путь, который git перечисляет, но которого нет в рабочем дереве (незакоммиченное удаление), пропускается — побеждает состояние рабочего дерева, поэтому локально удалённый файл остаётся удалённым и в копии. Симлинки воссоздаются как симлинки; права сохраняются. Без git (или при ошибке git) runner переключается на полное копирование директории с теми же исключениями плюс предупреждение о том, что игнорируемые артефакты теперь включены.
Собственный workspace/local.yml разработчика — gitignored и поэтому не копируется — вместо этого он сидируется в сгенерированный local.yml (ниже), так что локально-обязательные vars и локально-включённые сервисы всё равно доходят до тестового деплоя.
Идентичность compose. Каждый запуск получает <base>-t-<scenario>-<run-id> в качестве имени compose-проекта (<base> = project.prefix, если задан, иначе project.name; <run-id> — 6 случайных hex-символов — само имя сценария не является границей уникальности при двух одновременных клонах одного проекта). Это автоматически разделяет контейнеры, сети и не-общие volume’ы; поиск контейнеров остаётся label-based (com.docker.compose.project), никогда не по угадыванию имени. Идентичность записывается в копию двумя способами:
- если в копии есть
workspace/docker.yml→ генерируетсяworkspace/docker.local.ymlтолько сproject_name:(локальный слой всегда побеждает); - если в копии нет
workspace/docker.yml→ генерируетсяworkspace/docker.ymlсproject_name:плюс явным пустым[]для каждого ключаargs:, чтобы сгенерированный файл оставался семантически нейтральным (отсутствиеdocker.ymlсегодня означает нулевую конфигурацию args — сгенерированный файл безargs:молча включил бы значения по умолчанию для каждой команды, что было бы изменением поведения). Любой случайно скопированныйdocker.local.ymlв этой ветке удаляется, поскольку иначе он бы впервые активировался.
Приоритет генерируемого local.yml (от низшего к высшему):
- seed =
local.ymlисходного проекта (отсутствующий файл → пустой), с вырезаннымиcompose.extraиservices.<name>.compose.extra(каждое вырезание сопровождается предупреждением — они ссылаются на gitignored overlay-файлы, которых в копии нет); env.vars/env.servicesэтого сценария;- идентичность:
project: { prefix: <имя compose-проекта> }иupdate: { mode: "off" }(без промптов самообновления внутри одноразового тестового запуска).
Автоматическая изоляция host-портов. Каждый host-порт, объявленный под services.<name>.ports сервисом, который будет включён в копии, переназначается на свежевыделенный свободный порт, записываемый в сгенерированный local.yml как переопределение services.<name>.ports.<x> (scheme исходного порта сохраняется). Поскольку preflight ports_free читает то же поле — и проект, который берёт свои compose-биндинги host-портов из services.<name>.ports (напрямую или через запись exports.env from: services.<name>.ports.<x>), биндится из него же — preflight и фактический бинд двигаются вместе, поэтому сценарий работает параллельно с рабочим окружением без конфигурации портов. Любые env.vars: { …: auto }-порты выделяются в том же пакете. Все порты берутся из одного прохода выделения (все listener’ы открываются до закрытия любого из них, гарантируя уникальность внутри пакета); preflight ports_free копии всё равно ловит гонки на уровне хоста, и при провале деплоя, похожем на конфликт привязки host-порта (при наличии любого выделенного порта), раннер переаллоцирует все порты и повторяет деплой ровно один раз, прежде чем провалить сценарий.
Volume’ы shared: true резолвятся в свои буквальные имена и переиспользуются как есть — намеренное исключение для кэшей пакетов (composer, npm, …). shared-volume, хранящий реальные (не кэш) данные, поэтому виден каждому тестовому запуску.
Состояние. Журнал, локи, хранилище сгенерированных значений, prompt-кэш, логи — всё живёт под собственным .dwe/ копии, отдельно от исходного. .env регенерируется внутри копии самим деплоем. Docker-демон (кэши образов/сборки) и ~/.config/dwe (пользовательские настройки, переопределения бинарей) общие по замыслу — “всё состояние локально для проекта” верно для runtime-состояния, но не для демона или пользовательского конфига.
Структура .dwe/tests/
Заголовок раздела «Структура .dwe/tests/»Все пути ниже — относительно корня исходного проекта (никогда не копии):
| Путь | Назначение |
|---|---|
.dwe/tests/runs/<scenario>/ | Одноразовая копия для текущего/последнего запуска сценария |
.dwe/tests/locks/<scenario>.lock | Per-scenario flock (никогда не общий на проект deploy.lock/snapshot.lock) |
.dwe/tests/manifests/<scenario>-<run-id>.yml | Durable-манифест запуска — записывается до любого взаимодействия с Docker |
.dwe/tests/reports/<scenario>/ | Артефакты сбоя последнего непройденного (и не --keep) запуска сценария (см. Отчёты об ошибках) |
Манифест (scenario, run_id, compose_project, copy_path, bridge_dir, report_dir, created_at) — единственный вход для teardown: запуск, оборвавшийся на середине (падение, --keep, убитый процесс), всё равно полностью описывается своим манифестом и содержимым копии, без обращения к рабочему окружению и без угадывания имён.
Teardown
Заголовок раздела «Teardown»Выполняется по умолчанию после каждого сценария (успех/провал/таймаут/Ctrl+C), управляется только манифестом, по порядку: docker compose down --remove-orphans (никогда не -v, и политика args.down копии игнорируется, чтобы проект с args.down: ["-v"] не мог удалить общий кэш-volume, на который в сыром compose-файле ссылаются как на обычный именованный volume) → добор оставшихся контейнеров с меткой, точно равной com.docker.compose.project из манифеста → удаление собственных volume’ов тестового проекта (по префиксу имени compose-проекта; shared:-volume’ы выживают, та же семантика, что у dwe reset) → остановка bridge-демона, если деплой запустил его в копии → удаление директории копии → удаление манифеста → освобождение flock. Каждый шаг best-effort — сбой логируется, а последующие шаги всё равно выполняются.
--keep пропускает все шаги выше, оставляет манифест и копию на месте и печатает имя compose-проекта, путь копии и подсказку по очистке. Последующий dwe test run того же имени сценария сразу проваливается (манифест сохранённого запуска ещё существует), а не тихо удаляет сохранённое окружение из-под вас — очистите вручную, либо выполните dwe test clean.
Непройденный запуск с --keep печатает вторую строку — где на самом деле лежат улики: <copy>/.dwe/logs/ и docker compose -p <project> logs. Отчёт об ошибке под --keep не собирается (окружение всё ещё живо, снимок с него был бы избыточен), а без этой строки его отсутствие читается как упущение, а не как осознанный размен.
Отчёты об ошибках
Заголовок раздела «Отчёты об ошибках»Когда сценарий не проходит (сбой деплоя, сбой шага или таймаут) и --keep не был указан, runner пытается собрать отчёт об ошибке в .dwe/tests/reports/<scenario>/ до того, как teardown уничтожит окружение — так материал для отладки переживает снос. Директория очищается и перезаписывается на каждом непройденном запуске (отлаживается последний сбой); успешный сценарий или запуск с --keep её не трогают.
| Файл | Источник |
|---|---|
pipeline.log | копия лога пайплайна сценария (.dwe/logs/test.log внутри одноразовой копии) |
compose-ps.txt | docker compose ps --all по копии (--all, чтобы сервис, упавший или завершившийся во время деплоя, всё равно был виден — running-only по умолчанию отбросил бы именно тот сервис, ради которого и существует отчёт об ошибке) |
container-logs.txt | docker compose logs --no-color --tail 200 по контейнерам копии, объединённые в один файл |
Сбор — best-effort на всём протяжении и выполняется под собственным свежим таймаутом (никогда не по дедлайну самого сценария, который может быть уже истёк): отсутствующий лог пайплайна, частично упавшая docker-команда или таймаут самого сборщика — предупреждаются и никогда не меняют результат прохождения сценария. Если конфиг docker копии не удаётся загрузить (то же условие, что может сломать и сам деплой), сбор переключается на docker ps -a / docker logs, отфильтрованные по точному значению метки com.docker.compose.project этого запуска, так что отчёт формируется даже когда compose неработоспособен.
Путь отчёта отображается в текстовом выводе dwe test run (строка report: <dir> под непройденным сценарием) и в JSON-выводе как report_dir (пусто для успешного сценария, запуска с --keep или когда директорию отчёта создать не удалось).
dwe test run
Заголовок раздела «dwe test run»dwe test run [scenario...] --keep # пропустить teardown; напечатать имя проекта, путь копии, подсказку по очистке --timeout <duration> # переопределить собственный timeout каждого сценария (например, 15m) --skip-isolation-check # понизить блокирующие находки изоляции до предупреждений --parallel N # запускать до N сценариев одновременно (по умолчанию 1)Без аргументов запускаются все сценарии под workspace/tests/*.yml, в отсортированном по имени порядке. Именованные аргументы запускают ровно эти сценарии (неизвестное имя проваливается до того, как что-либо запустится, код выхода 2). По умолчанию сценарии выполняются последовательно. Ctrl+C (SIGINT/SIGTERM) отменяет выполняющиеся в данный момент сценарии, сносит их и пропускает остальные — уже завершённые сценарии всё равно попадают в отчёт.
Вывод — это стандартный живой репортер пайплайна на каждый сценарий (тот же вид, что у dwe deploy run), за которым следует строка сводки, например:
2 passed, 1 failed (redis-off: step "tests/app answers")dwe test требует проект — в отличие от read-only команд документации, он не работает вне проекта.
Глобальные флаги --verbose / --debug прокидываются в подпроцессы dwe validate и dwe deploy run внутри копии, поэтому диагностический уровень применяется к тому самому деплою, который проверяет сценарий, а не только к учётной логике самого раннера. В обычном последовательном текстовом режиме трейс подпроцесса выводится в stderr в реальном времени; при --parallel или --output json (где вывод подпроцесса не транслируется) он попадает в лог запуска этой копии (.dwe/tests/runs/<scenario>/.dwe/logs/test.log). --debug перекрывает --verbose (это надмножество), как и во всех остальных командах.
--parallel N
Заголовок раздела «--parallel N»--parallel N (по умолчанию 1) запускает до N сценариев одновременно. Эффективный параллелизм — min(N, число сценариев): --parallel 8 при двух сценариях запускает два воркера; --parallel 8 при одном сценарии — один. Порядок вывода (текстовая сводка и JSON-массив scenarios) всегда соответствует исходному порядку имён, независимо от порядка завершения.
--parallel 1(по умолчанию) байт-в-байт совпадает с сегодняшним поведением. Когда эффективный параллелизм равен1— флаг отсутствует, задан1, или сценариев меньше, чем запрошено воркеров — выполняется прежний потоковый последовательный путь: стандартный живой репортер пайплайна на каждый сценарий, ровно как уdwe deploy run.- При эффективном параллелизме > 1 потоковый вывод заменяется компактным агрегированным видом. Одна закреплённая строка на сценарий показывает спиннер, имя сценария, грубую фазу (
preparing…,validating…,deploying…,deploy retry…,running steps…,collecting report…,tearing down…) и секундомер; по завершении строка финализируется в✓ <name> passed,✗ <name> failed — step "…"либо✗ <name> error(сценарий, который не удалось подготовить). Футер отслеживаетrunning k/n scenarios…. Когда сценариев больше, чем помещается в терминале (закреплённый блок ограничен высотой терминала), сценарии сверх этого лимита отображаются теми же плоскими строкамиscenario <name>: started/scenario <name>: <status>, а не закреплённой строкой. Пошаговый вывод деплоя/пайплайна каждого сценария не транслируется в терминал — он идёт только в собственный лог этой копии (.dwe/tests/runs/<scenario>/.dwe/logs/test.log), а отчёт об ошибке непройденного сценария собирается как обычно. Предупреждения снабжаются префиксом[<scenario>] warning: …и печатаются в stderr, не нарушая блок. - Запуски через pipe / без TTY (CI) деградируют до плоских строк
scenario <name>: started/scenario <name>: <status>на каждый сценарий вместо живого блока — сводка и код выхода не меняются. - JSON-режим (
--output json) не зависит от--parallel: форма payload идентична, и, как на любой read-only поверхности, живой вывод и предупреждения подавляются независимо от параллелизма.
Коды выхода не меняются (см. Коды выхода): любой сценарий, который не удалось подготовить → 2, иначе любой проваленный сценарий → 1, иначе 0.
Изоляция сохраняется без изменений при параллелизме. Каждый сценарий уже выполняется в собственной директории-копии, под собственным per-scenario flock, со своим per-run-id compose-проектом и манифестом, и с каждым host-портом, перемапленным на свежевыделенный свободный порт (см. Модель изоляции). Выделение портов дополнительно защищено от гонок в пределах процесса: набор аренд гарантирует, что два одновременных сценария в одном dwe test run никогда не получат один и тот же host-порт (межпроцессные гонки между отдельными вызовами по-прежнему покрываются preflight ports_free каждой копии плюс одной повторной попыткой деплоя).
Конкуренция за общий кэш пакетов. Сценарии, переиспользующие один и тот же shared: true кэш-volume (кэш composer или npm), могут конкурировать при параллельном запуске — пакетные менеджеры берут lock-файлы, а одновременные установки на холодный кэш против одного volume могут замедлять друг друга или споткнуться о собственную блокировку менеджера. Предпочтительно не параллелить сценарии, каждый из которых выполняет тяжёлую установку на холодный кэш против общего кэша; сценарии с тёплым или непересекающимся кэшем параллелятся без проблем. Нагрузка на Docker-демон от N одновременных деплоев (pull образов, сборки, старты контейнеров) — на ваше усмотрение; подбирайте N под хост.
dwe test list
Заголовок раздела «dwe test list»dwe test listПеречисляет каждый сценарий под workspace/tests/*.yml с его description:, дословно. Отсутствие директории workspace/tests/ ничего не перечисляет и не является ошибкой.
Профиль стоимости (--output json)
Заголовок раздела «Профиль стоимости (--output json)»dwe test list --output json отдаёт для каждого сценария объект cost_profile — во что обойдётся его запуск и насколько далеко простирается его изоляция:
{ "scenarios": [ { "name": "redis-off", "description": "Deploy with redis disabled", "cost_profile": { "enabled_services": 3, "build_services": ["app"], "external_images": ["postgres:16", "redis:7"], "max_start_period_seconds": 30, "shared_volumes": 1, "isolation_findings": [{"kind": "external_volume", "resource": "composer-cache"}], "host_steps": 2 } } ]}| Поле | Значение |
|---|---|
enabled_services | Сервисы dwe, включённые после наложения env.services этого сценария — то самое число, которым сценарии одного проекта реально различаются. Сервис с required: true остаётся включённым, даже если сценарий его выключает: ровно так загрузчик конфигурации разрешает сгенерированный local.yml. |
build_services | Compose-сервисы с build: во включённой цепочке, отсортированные. |
external_images | Различные image: тех compose-сервисов, которые не собираются локально, отсортированные — то, что холодный запуск будет тянуть. Собираемый сервис несёт локальный тег (image: myproject-app:dev), тянуть его неоткуда, поэтому он исключён. |
max_start_period_seconds | Наибольший start_period healthcheck во включённой цепочке. Максимум, не сумма: docker compose up --wait ждёт параллельно, поэтому сумма завышает оценку тем сильнее, чем больше сервисов. Отключённый или неразбираемый healthcheck не учитывается. |
shared_volumes | Количество томов docker.yml с shared: true. Они разрешаются в свои дословные имена, и тестовый запуск в них пишет. |
isolation_findings | Неблокирующие находки сканера изоляции compose — named_volume / external_volume / named_network / external_network, то есть ресурсы, разделяемые с рабочим окружением. Блокирующие виды (container_name, raw_host_port) не включаются: они и так прерывают сценарий до деплоя. Полные сообщения даёт dwe validate tests. |
host_steps | Шаги, которые сценарий выполнит на хосте, вне контейнерной песочницы: его собственные, деплой-пайплайн, который он вызывает (проектный и включённых сервисов), и проверки workspace/validate.yml, которые прогон запускает через dwe validate и preflight dwe deploy run в копии — проверка считается, если это type: builtin с cmd: shell либо type: command (загрузчик допускает там только команды типа shell / script), с тем же гейтом services:, который применяет загрузчик проверок. Шаги пайплайна считаются с заходом внутрь групп parallel:. Считаются формы type: shell, type: builtin с cmd: shell, type: dwe, чья подкоманда снова входит в код проекта (cmd, run, restart, deploy, reset, test), type: command, чья команда разрешается в хостовую, и любой шаг с shell-условием when: или хостовым check: (включая check: auto). Команда считается хостовой, если она объявляет argv_append_from: (выражение выполняется на хосте даже у контейнерной команды), либо — в его отсутствие — если её тип не service_exec и не service_run, причём правила type: dwe / type: builtin применяются рекурсивно, а для workflow считается подшаг, который сам хостовый или несёт when: вида cmd:. Собственные подкоманды dwe (docker up --wait, info, render … — весь встроенный пайплайн по умолчанию) не считаются: это машинерия dwe поверх одноразовой копии, ради запуска которой сценарий и существует. Шаг считается один раз, сколько бы форм он ни использовал; shell-условие when: на уровне фазы считается за один. Ссылка type: command, которая не разрешается, считается хостовым шагом — поле управляет запуском без присмотра, поэтому неизвестный случай закрывает ворота. Побочные эффекты на хосте не изолируются. |
Два свойства заданы намеренно:
- Только факты — поля-вердикта
cheap/expensiveнет. Что считать достаточно дешёвым для запуска без присмотра — политика вызывающей стороны (для ИИ-агентов правило формулирует skill dwe), а не CLI. - Профиль сообщает, есть ли сборка, а не сколько она стоит. Главный фактор — тёплый ли кэш слоёв Docker, секунды против многих минут — статического источника не имеет и не моделируется.
Профиль опускается (не пустой, не ошибка), если нужное состояние проекта не загружается: dwe test list не берёт локов, не трогает Docker и не требует загружаемой конфигурации — это команда, к которой обращаются, когда конфигурация в процессе правки, и там она продолжает работать. Текстовый вывод профиль вообще не вычисляет.
dwe test clean
Заголовок раздела «dwe test clean»dwe test clean [scenario...] --dry-run # сообщить, что было бы снесено, ничего не трогаяУдаляет тестовые окружения, оставшиеся после dwe test run --keep или прерванного/упавшего запуска. clean строго манифест-управляемый: он перечисляет .dwe/tests/manifests/*.yml и переиспользует тот же самый Teardown, что выполняется в конце обычного dwe test run — ничто и никогда не сносится по угаданному по паттерну имени compose-проекта.
Без аргументов сносится каждый сценарий с манифестом. Именованные аргументы ограничивают снос ими. Сценарий, чей flock (.dwe/tests/locks/<scenario>.lock) в данный момент удерживается живым запуском, пропускается, никогда не сносится — clean никогда не соперничает с выполняющимся запуском. --dry-run сообщает, что было бы снесено, ничего не трогая (flock каждого сценария всё равно захватывается и освобождается, чтобы корректно классифицировать живой запуск как пропущенный, а не подлежащий сносу).
Если teardown манифеста не завершился чисто (например, шаг удаления контейнера или volume’а провалился на середине), эта запись отражается как failed, никогда как swept — Teardown best-effort и мог удалить часть ресурсов (даже сам манифест) до того, как наткнулся на сбой, поэтому засчитать его как swept означало бы скрыть реальный остаток от следующего запуска.
Best-effort, report-only сканирование дополнительно перечисляет Docker compose-проекты, совпадающие по префиксу тестового имени этого проекта (<base>-t-, та же основа, что использует dwe test run), у которых вообще нет манифеста — они отражаются как orphans и никогда не сносятся автоматически; удалите их вручную, убедившись, что это безопасно. Если собственный корневой конфиг проекта не удаётся загрузить, скан orphans пропускается (с предупреждением), но каждое манифестированное окружение всё равно сносится — сломанный или недоредактированный конфиг не должен блокировать восстановительный снос.
JSON-вывод
Заголовок раздела «JSON-вывод»dwe test clean --output json{ "dry_run": false, "swept": [{"scenario": "smoke", "compose_project": "myapp-t-smoke-a1b2c3", "copy_path": ".dwe/tests/runs/smoke"}], "skipped": [{"scenario": "redis-off", "compose_project": "myapp-t-redis-off-9f1e2d", "copy_path": ".dwe/tests/runs/redis-off", "reason": "live"}], "failed": [], "orphans": [{"compose_project": "myapp-t-old-9f8e7d", "note": "no manifest — remove manually"}]}Как и в run/list, живой вывод (предупреждения по каждой записи) в JSON-режиме молчит на stderr.
dwe validate tests
Заголовок раздела «dwe validate tests»dwe validate testsДомен dwe validate (Domain() == "tests"), который статически проверяет каждый файл сценария workspace/tests/*.yml, не трогая Docker — только валидация, никогда не подключается к preflight. Запускайте его в CI перед dwe test run, чтобы отловить ошибки авторства сценария без поднятия одноразовой копии.
Для каждого файла, по порядку:
- загрузка —
LoadScenario(строгийKnownFields(true), пустой файл отвергается); это также покрывает валидацию имени (ValidateScenarioNameотвергает — а не переписывает — некорректное базовое имя файла), так что некорректное имя файла всплывает здесь. timeout:— собственное полеtimeout:сценария должно парситься черезtime.ParseDurationи быть строго положительным (повторяет рантайм-контрактresolveScenarioTimeout); ошибка парсинга или неположительная длительность — это ошибка.env.services— каждая записьenable/disableдолжна называть сервис, существующий в смёрженном конфиге проекта; неизвестное имя — ошибка.steps— все шаги рендерятся и резолвятся как одна цельная фаза, точно как при реальном запуске (pipeline.ResolvePhaseStepsнад одной синтетической фазой) — это ловит ошибки схемы шага, некорректные параметрыwith:билтина, сломанные условияwhen:и дублирующиеся имена шагов верхнего уровня (проверка по-шагово упустила бы последнее, поскольку уникальность — это инвариант всей фазы). Рендеринг подставляет вместо любой записиenv.varsсо значением-литераломautoвалидный плейсхолдер host-порта, так что${vars.db.port}рендерится в валидное число и шагtcp_reachable/http_checkпроходит валидацию как обычно — по-настоящему некорректный параметр (например,status: nope) всё равно даёт ошибку, даже рядом с шаблонизированнымurl:. Var, который заполняется только после деплоя (секрет${generated.*}, или var, который создаёт сам деплой), на момент валидации отсутствует и может дать ложную диагностику — задайте для него дефолт на уровне проекта, чтобы этого избежать: валидатор видит конфиг до деплоя, а реальный запуск — конфиг после деплоя.- шаги
type: command— каждый ID команды ищется в реестре команд проекта; неизвестный ID — ошибка. - изоляция compose — см. Сканер изоляции compose ниже; находки выводятся один раз на проект как предупреждения (никогда как ошибки — многоуровневая политика fail/warn применяется только к
dwe test run, а не к статической валидации).
Сканер изоляции compose
Заголовок раздела «Сканер изоляции compose»Модель изоляции dwe test (выше) разграничивает контейнеры, сети и не-общие volume’ы по имени compose-проекта — но горстка конструкций сырого compose полностью обходит это разграничение и может столкнуться с рабочим окружением или прикрепиться к нему. Сканер (config.ScanComposeIsolation) разбирает активные compose-файлы проекта (cfg.ComposeFiles()) на предмет этих конструкций и отмечает их:
| Конструкция | Kind | Серьёзность |
|---|---|---|
container_name: (любое вхождение) | container_name | Блокирующая — буквальное имя контейнера напрямую сталкивается с рабочим окружением |
Буквальный host-порт (одиночный, например "8080:80", или диапазон, например "8080-8090:80-90"), не смоделированный через services.<name>.ports | raw_host_port | Блокирующая — обходит и автоматическое переназначение портов, и ports_free |
Volume/сеть с external: true | external_volume / external_network | Предупреждение — риск общего ресурса, а не жёсткое столкновение |
Явное name: у volume/сети | named_volume / named_network | Предупреждение — тот же класс риска, что и external: |
Токены host-порта с интерполяцией ${...}/переменными окружения и записи только с container-портом (случайный host-порт) не отмечаются — только буквальное число порта или диапазон. IPv6-хосты в скобках ([::1]:8080:80) вне области действия и не отмечаются (редки в дев-compose). Находка container_name: выдаётся всегда, независимо от того, к какому compose-сервису она относится — сопоставление compose-сервиса с ключом dwe-сервиса ненадёжно, так что ложные срабатывания снимаются через --skip-isolation-check, а не подавляются у источника.
У самого сканера нет мнения о серьёзности сверх встроенного флага Blocking — каждый вызывающий сам решает, что делать с находкой:
dwe test run— запускает скан на копии прямо перед подпроцессомdwe validate. Каждая находка печатается как предупреждение. Блокирующая находка немедленно проваливает сценарий (teardown всё равно выполняется; подпроцесс деплоя не запускается), если не передан--skip-isolation-check— в этом случае каждая находка, блокирующая или нет, становится только предупреждением, и запуск продолжается. См. предпосылку по портам, как избежать находкиraw_host_portв принципе: смоделируйте порт черезservices.<name>.ports.dwe validate tests— выводит каждую находку как предупреждение, независимо отBlocking; статическая валидация никогда не проваливает сборку из-за риска изоляции, а только заранее его показывает.
Коды выхода
Заголовок раздела «Коды выхода»| Код | Значение |
|---|---|
0 | Все сценарии прошли (run); снос завершён, включая случай “нечего сносить” (clean — пропущенные-живые записи и orphans на это не влияют) |
1 | Хотя бы один сценарий провалился — сбой деплоя, сбой шага или таймаут (run); teardown хотя бы одного манифеста не завершился чисто (clean) |
2 | Сценарий — или сам запуск — не удалось даже подготовить: неизвестное имя сценария, ошибка загрузки/парсинга сценария, flock захвачен параллельным запуском, манифест сохранённого предыдущего запуска всё ещё существует (run) |
clean отображает жёсткую ошибку (например, нечитаемую директорию манифестов) в ненулевой код выхода через стандартный конверт ошибок CLI, отдельно от кодов 0/1 результата сноса выше.
JSON-вывод (run / list)
Заголовок раздела «JSON-вывод (run / list)»dwe test run --output jsondwe test list --output json{ "scenarios": [ {"name": "redis-off", "status": "passed", "duration_seconds": 4.213}, {"name": "cache-on", "status": "failed", "failed_step": "tests/http_check", "duration_seconds": 2.101, "report_dir": ".dwe/tests/reports/cache-on"} ], "summary": "1 passed, 1 failed"}status — одно из passed, failed, error (error = сценарий не удалось подготовить — сбой копии/конфига/манифеста/validate; отличается от сбоя деплоя или шага, который отражается как failed). failed_step и report_dir опускаются, когда пусты (у успешного сценария нет ни того, ни другого). report_dir — директория отчёта об ошибке для непройденного сценария; опускается для успешного сценария, запуска с --keep или когда сбор не смог создать директорию отчёта. Как и в любой другой read-only/отчётной поверхности, живой вывод пайплайна и строка сводки в JSON-режиме молчат — файловый лог под .dwe/logs/ всё равно фиксирует всё.
dwe test list --output json дополнительно отдаёт для каждого сценария профиль стоимости.
Документированные ограничения
Заголовок раздела «Документированные ограничения».git/исключён из копии. Deploy- или тестовый шаг, вызывающийgitв корне проекта, провалится или поведёт себя иначе внутри копии.- Именованные compose-ресурсы обходят изоляцию.
container_name:, явно именованные сети/volume’ы иexternal: trueв сырых compose-файлах игнорируют разграничение по имени compose-проекта и могут столкнуться — или прикрепиться — к рабочему окружению. Именно поэтому teardown никогда не используетcompose down -v. Сканер изоляции compose обнаруживает эти конструкции и дляcontainer_name:и буквальных host-портов проваливает сценарий ещё до деплоя (можно понизить через--skip-isolation-check) — он не делает их безопасными, он показывает их до того, как они вызовут столкновение. - Host-порты, не смоделированные в
services.<name>.ports, не изолируются. Автоматическое переназначение и preflightports_freeвидят только порты, объявленные черезservices.<name>.ports; host-порт, захардкоженный прямо в сыром compose-файле (8080:8080), обходит и то, и другое. Объявите его подservices.<name>.portsлибо проведите подстановку в compose через var, заданный черезenv.vars: { …: auto }. Сканер изоляции отмечает это как блокирующую находкуraw_host_port. - Побочные эффекты на хосте от собственных deploy-/тестовых шагов проекта не изолируются. Шаг
shell, затрагивающий абсолютные пути,~или bind-монтирования вне проекта, влияет на реальный хост так же, как это было бы при реальном деплое.dwe testизолирует управляемое dwe состояние (файлы, контейнеры, volume’ы, сети, порты) — а не произвольные побочные эффекты, которые выбирает себе шаг. - Копирование неатомарно. Ничто не блокирует исходный проект во время работы
git ls-filesи копирования; редактирование файлов во время тестового запуска может дать смешанный снимок. ~/.config/dweи кэши образов/сборки Docker-демона общие, по замыслу (см. Модель изоляции).
Связанные команды
Заголовок раздела «Связанные команды»dwe deploy run— реальная команда, выполняемая внутри копии каждого сценарияdwe validate— fail-fast проверка, выполняемая перед деплоем внутри каждой копииdwe validate tests— статическая валидация сценариев без Docker (см. выше)dwe reset run— использует ту же семантику удаления volume’ов, что переиспользует teardown- deploy.yml / reset.yml — схема шагов, которую переиспользует
steps:(типы,with:,when:) - Билтины —
http_checkи предикаты-как-проверки в теле шага - Условия и действия — типизированные условия/действия, доступные
when:и телам шагов