reindex: пересборка витрины проигрыванием журнала
- `healthlog reindex` собирает витрину из журнала (тела архива + учёт доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую базу читает без наката миграций и не трогает вовсе. Подмену делает человек при остановленном сервисе: переименование поверх открытого дескриптора портит базу молча. - Журналом считается архив, а не таблица доставок: тело без учётной записи заводится заново (метка из ULID, размер и хеш по распакованному телу), запись без тела переносится, но не сворачивается. Оракул сходимости встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не считается. - Прогон живого архива переехал на новый пакет: второго проигрывателя журнала в проекте не осталось, а его утверждение о ключе сна перестало быть константой, протухающей с каждой доставкой.
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Пересборка витрины проигрыванием журнала
|
||||
|
||||
Система SHALL уметь собрать витрину заново, проиграв журнал целиком:
|
||||
`import(снапшот) + replay(доставки)`. Стадия снапшота в этой дельте пуста —
|
||||
пересборка из архива есть вырожденный случай с пустым снапшотом, — и отдельной
|
||||
операции «пересборка из архива» рядом с импортом экспорта заводить MUST NOT.
|
||||
|
||||
Проигрываться SHALL **все** доставки журнала, а не только те, чей
|
||||
`parse_status` говорит о неразобранности. Отбор по учётному статусу сделал бы
|
||||
результат функцией предыдущего прогона, а не журнала; дешевизну повторного
|
||||
проигрывания обеспечивает хеш-детектор объекта, а не пропуск доставок.
|
||||
|
||||
Пересборка собственного разбора и собственного слияния иметь MUST NOT: она
|
||||
зовёт тот же код, что и приём, по идентификатору доставки, и тело читает из
|
||||
архива тем же путём, с тем же пределом размера распакованного тела. Второй путь
|
||||
разбора разошёлся бы с первым молча, а другой предел означал бы, что тело,
|
||||
принятое со `200`, вечно отказывает на каждой пересборке.
|
||||
|
||||
#### Scenario: Пересобранная витрина совпадает с накопленной приёмом
|
||||
|
||||
- **GIVEN** рабочая витрина накоплена тем же разбором, приём во время
|
||||
накопления шёл последовательно, и за время пересборки новых доставок не
|
||||
приезжало
|
||||
- **WHEN** журнал проигрывается заново с пустой витрины
|
||||
- **THEN** отпечаток пересобранной витрины совпадает с отпечатком накопленной
|
||||
|
||||
#### Scenario: Повторная пересборка ничего не меняет
|
||||
|
||||
- **WHEN** пересборка того же журнала выполняется второй раз
|
||||
- **THEN** отпечаток витрины не меняется
|
||||
|
||||
#### Scenario: Разобранная доставка проигрывается наравне с неразобранной
|
||||
|
||||
- **WHEN** в журнале есть доставки со статусом `parsed` и со статусом `pending`
|
||||
- **THEN** проигрываются обе
|
||||
|
||||
### Requirement: Порядок проигрывания задаётся журналом
|
||||
|
||||
Система SHALL проигрывать доставки строго в порядке `(received_at, id)`, а не
|
||||
в порядке обхода каталога архива.
|
||||
|
||||
Порядок входит в результат: слой доставки без плотных метрик наследуется от
|
||||
**предшествующей** доставки той же автоматизации, то есть слой есть функция
|
||||
префикса журнала. Обход каталога совпадает с хронологией только по датам
|
||||
каталогов и внутри суток не упорядочивает ничего.
|
||||
|
||||
Второй ключ обязателен, а не для красоты: `received_at` хранится с секундной
|
||||
точностью, и доставки одной секунды без него шли бы в неопределённом порядке —
|
||||
а значит два прогона одного журнала могли бы разойтись.
|
||||
|
||||
Проигрывание SHALL быть последовательным. Распараллеливать его MUST NOT: слой
|
||||
есть функция префикса журнала, а запись часового объекта — чтение, слияние и
|
||||
запись обратно.
|
||||
|
||||
#### Scenario: Порядок не зависит от раскладки файлов в архиве
|
||||
|
||||
- **WHEN** тела одного журнала лежат в архиве так, что порядок обхода каталога
|
||||
не совпадает с хронологией приёма
|
||||
- **THEN** доставка без плотных метрик получает слой предшествующей ей по
|
||||
`received_at` доставки той же автоматизации, а не слой соседа по каталогу
|
||||
|
||||
#### Scenario: Доставки одной секунды упорядочены идентификатором
|
||||
|
||||
- **WHEN** две доставки имеют одинаковый `received_at`
|
||||
- **THEN** порядок между ними задаётся идентификатором и одинаков в каждом
|
||||
прогоне
|
||||
|
||||
### Requirement: Журналом считается архив, а не таблица доставок
|
||||
|
||||
Система SHALL брать состав журнала из **сырого архива**, сверяя его с учётом
|
||||
доставок, а не перечислять только строки `delivery`. Иначе витрина
|
||||
пересобиралась бы из витрины.
|
||||
|
||||
Тело, у которого учётной записи нет, SHALL заводиться заново и проигрываться
|
||||
наравне с остальными: приём кладёт тело на диск раньше строки в базе — обратный
|
||||
порядок дал бы учтённую доставку без данных, — поэтому отказ на вставке строки
|
||||
оставляет тело в архиве без учёта.
|
||||
|
||||
Для такого тела идентификатор берётся из имени файла, метка приёма — из времени
|
||||
создания в ULID, приведённого к **UTC**. Дата каталога меткой служить MUST NOT:
|
||||
она задаёт сутки, а порядок нужен внутри суток. Размер и хеш пересчитываются по
|
||||
**распакованному** телу — так же, как их считает приём; иначе в тех же колонках
|
||||
появились бы значения другой природы.
|
||||
|
||||
Заголовки доставки при этом восстановлены быть не могут — **в архиве их нет**.
|
||||
Подобранное тело SHALL проигрываться без них, с честной деградацией вывода
|
||||
слоя: наследовать не от чего и подтверждать нечем.
|
||||
|
||||
Строка учёта, у которой тела в архиве нет, отказом быть MUST NOT: она станет
|
||||
штатной, когда появится ретеншен архива. Такая строка SHALL считаться и
|
||||
называться в отчёте.
|
||||
|
||||
Файл архива, не подходящий под форму тела (чужое расширение, остаток `*.tmp`
|
||||
от прерванной записи, имя не разбирается как ULID), SHALL пропускаться и
|
||||
учитываться счётчиком. Молчаливый пропуск недопустим: тело есть, а в отчёте его
|
||||
нет. Тело, чей идентификатор уже встретился в другом каталоге, SHALL
|
||||
пропускаться тем же порядком: вторая запись журнала с тем же ключом сорвала бы
|
||||
весь прогон, то есть один посторонний файл лишал бы пересборки всё остальное.
|
||||
|
||||
Каждый пропуск, повтор и неудачный подбор SHALL оставлять запись в логе с путём
|
||||
файла — путь в архиве это дата и идентификатор, измерений в нём нет. Без неё
|
||||
счётчик в отчёте не на что раскрыть, а отчёт при этом отсылает человека
|
||||
разбираться по логу.
|
||||
|
||||
Ошибка **чтения** каталога архива, включая отсутствие самого корня, SHALL быть
|
||||
отказом всей пересборки, а не пустым журналом. Нечитаемый каталог означает
|
||||
«неизвестно, есть ли там тела», а не «тел нет». Каталог архива пересборка
|
||||
создавать MUST NOT — создание превратило бы запуск не из того каталога в
|
||||
успешный прогон по пустому журналу.
|
||||
|
||||
#### Scenario: Тело без учётной записи подбирается
|
||||
|
||||
- **WHEN** в архиве лежит тело, для которого строки `delivery` нет
|
||||
- **THEN** доставка заводится заново с идентификатором из имени файла и меткой
|
||||
приёма из ULID в UTC
|
||||
- **AND** её размер и хеш посчитаны по распакованному телу
|
||||
- **AND** её точки попадают в витрину
|
||||
|
||||
#### Scenario: Учётная запись без тела не роняет пересборку
|
||||
|
||||
- **WHEN** у строки `delivery` нет тела в архиве
|
||||
- **THEN** пересборка продолжается
|
||||
- **AND** факт учитывается счётчиком в отчёте
|
||||
- **AND** сама запись переносится в базу назначения, но не сворачивается
|
||||
|
||||
#### Scenario: Файл, не являющийся телом, считается отдельно
|
||||
|
||||
- **WHEN** в архиве лежит файл, чьё имя не разбирается как ULID либо чьё
|
||||
расширение не соответствует форме тела
|
||||
- **THEN** он пропускается и учитывается счётчиком, а пересборка продолжается
|
||||
|
||||
#### Scenario: Повтор идентификатора не срывает прогон
|
||||
|
||||
- **WHEN** одно и то же имя тела встречается в двух каталогах суток
|
||||
- **THEN** проигрывается первое, второе учитывается счётчиком
|
||||
- **AND** пересборка доходит до конца
|
||||
|
||||
#### Scenario: Каталог архива не читается
|
||||
|
||||
- **WHEN** корня архива нет либо подкаталог не читается
|
||||
- **THEN** команда завершается ошибкой и витрину не собирает
|
||||
|
||||
### Requirement: Пересборка не трогает рабочую базу
|
||||
|
||||
Система SHALL собирать витрину в **отдельный файл базы** и MUST NOT записывать
|
||||
в рабочую базу ничего — ни объектов, ни строк учёта, ни миграций схемы.
|
||||
|
||||
Пересборка обязана начинаться с пустой витрины: точки из объекта не удаляются
|
||||
никогда, поэтому проигрывание поверх накопленного оставило бы результат
|
||||
прежнего, неверного разбора. Но очистка рабочей витрины необратима и наступает
|
||||
**до** того, как известно, что пересборка удалась: отказ на середине (битое
|
||||
тело, отменённый контекст, кончившееся место) оставил бы витрину пустой
|
||||
наполовину в состоянии, неотличимом от нормального.
|
||||
|
||||
Рабочая база SHALL открываться **только для чтения и без наката миграций**.
|
||||
Обычное открытие накатывает миграции безусловно, а миграции меняют и данные (та,
|
||||
что ввела частичный разбор, переписала `parse_status` у всех строк) — то есть
|
||||
штатный путь чтения нарушал бы запрет выше. Хуже: свежий бинарь мигрировал бы
|
||||
схему под работающим старым сервисом.
|
||||
|
||||
Расхождение версии схемы рабочей базы с версией, которую знает бинарь, SHALL
|
||||
быть отказом с указанием обеих версий, а не поводом мигрировать.
|
||||
|
||||
#### Scenario: Рабочая база остаётся нетронутой
|
||||
|
||||
- **WHEN** пересборка отработала успешно
|
||||
- **THEN** отпечаток рабочей витрины не изменился
|
||||
- **AND** учёт доставок в рабочей базе не изменился
|
||||
- **AND** собранная витрина лежит в отдельном файле
|
||||
|
||||
#### Scenario: Отказ посреди пересборки не портит рабочую базу
|
||||
|
||||
- **WHEN** пересборка прерывается на середине журнала
|
||||
- **THEN** рабочая витрина и учёт доставок в рабочей базе остаются такими же,
|
||||
какими были
|
||||
|
||||
#### Scenario: Схема рабочей базы старше бинаря
|
||||
|
||||
- **WHEN** версия схемы рабочей базы не совпадает с версией бинаря
|
||||
- **THEN** команда завершается ошибкой, называя обе версии
|
||||
- **AND** не пишет в рабочую базу ни одной строки
|
||||
|
||||
### Requirement: Файл назначения и его жизненный цикл
|
||||
|
||||
Файл назначения по умолчанию SHALL быть соседом рабочей базы — так подмена
|
||||
остаётся переименованием внутри одной файловой системы.
|
||||
|
||||
Тождество файла назначения с рабочей базой SHALL определяться **по файлу, а не
|
||||
по строке пути**: путь через `..`, симлинк или другой префикс монтирования
|
||||
дают то же тождество при разных строках. Совпадение MUST быть отказом: иначе
|
||||
проигрывание пошло бы прямо в живую рабочую базу — ровно то, что запрещено выше.
|
||||
|
||||
Сборка SHALL идти под временным именем, а переименование в файл назначения быть
|
||||
**последним шагом успешного прогона**. При любом ином исходе — отказ, отмена,
|
||||
падение — файла по пути назначения появляться MUST NOT, а временный SHALL
|
||||
убираться вместе со спутниками журнала SQLite.
|
||||
|
||||
Полусобранная база выглядит как обычная: это ровно то состояние, ради отрицания
|
||||
которого отвергнута очистка рабочей витрины. Обломок по пути назначения ещё и
|
||||
приучил бы обходить защиту от перезаписи флагом принудительности.
|
||||
|
||||
Существующий файл назначения перезаписываться молча MUST NOT: это отказ, если
|
||||
человек явно не потребовал перезаписи. Затребованная перезапись SHALL давать ту
|
||||
же витрину, что и сборка в отсутствующий файл, — сборка всегда начинается с
|
||||
пустой витрины, а не дописывается в чужое содержимое.
|
||||
|
||||
#### Scenario: Файл назначения совпадает с рабочей базой
|
||||
|
||||
- **WHEN** файл назначения — тот же файл, что рабочая база, пусть и по другому
|
||||
пути
|
||||
- **THEN** команда завершается ошибкой и не пишет ничего
|
||||
|
||||
#### Scenario: Файл назначения уже существует
|
||||
|
||||
- **WHEN** файл назначения существует, а перезапись не затребована явно
|
||||
- **THEN** команда завершается ошибкой и существующий файл не трогает
|
||||
|
||||
#### Scenario: Затребованная перезапись даёт ту же витрину
|
||||
|
||||
- **WHEN** пересборка выполняется поверх существующего файла назначения с
|
||||
явно затребованной перезаписью
|
||||
- **THEN** отпечаток собранной витрины совпадает с отпечатком сборки того же
|
||||
журнала в отсутствующий файл
|
||||
|
||||
#### Scenario: Прерванная пересборка не оставляет файла назначения
|
||||
|
||||
- **WHEN** пересборка прерывается на середине журнала
|
||||
- **THEN** файла по пути назначения не существует
|
||||
|
||||
### Requirement: База назначения пригодна к подмене
|
||||
|
||||
База назначения SHALL нести полноценный учёт доставок, а не только объекты
|
||||
витрины: подменяется файл базы **целиком**, а не одна таблица.
|
||||
|
||||
Состав переноса нормируется явно, потому что колонки `delivery` двух разных
|
||||
родов:
|
||||
|
||||
```
|
||||
факты журнала id, received_at, automation_name, automation_id, aggregation,
|
||||
period, session_id, bytes, sha256, raw_path, headers
|
||||
← переносятся дословно
|
||||
производные parse_status, points, derived_layer, uncovered_sections
|
||||
← начинаются пустыми
|
||||
```
|
||||
|
||||
Факты журнала SHALL переноситься дословно, включая записи, тела которых в
|
||||
архиве уже нет. Заголовки восстановлению не подлежат ничем — в архиве их нет, —
|
||||
и неполный перенос уничтожил бы их первой же подменой, а с ними и вывод слоя
|
||||
для **всех** доставок, не только подобранных. Запись без тела при этом не
|
||||
сворачивается и в наследовании слоя не участвует: выведенного слоя у неё нет.
|
||||
|
||||
Производные от разбора поля MUST начинаться пустыми. Перенос `derived_layer`
|
||||
особенно опасен и незаметен: доставка, чей повторный разбор отказал (штатный
|
||||
исход, когда слой не выводится), сохранила бы слой **прежнего** разбора, и
|
||||
следующая доставка той же автоматизации унаследовала бы его. Витрина снова стала
|
||||
бы функцией предыдущего прогона, а не журнала, причём оба прогона были бы
|
||||
самосогласованы — проверка «повторная пересборка ничего не меняет» этого не
|
||||
ловит.
|
||||
|
||||
#### Scenario: Учёт переносится полностью
|
||||
|
||||
- **WHEN** пересборка завершилась
|
||||
- **THEN** число строк учёта в базе назначения равно числу строк рабочей базы
|
||||
плюс число подобранных тел
|
||||
- **AND** заголовки перенесённых доставок совпадают с рабочей базой дословно
|
||||
|
||||
#### Scenario: Слой прошлого разбора в наследование не попадает
|
||||
|
||||
- **WHEN** в рабочей базе у доставок проставлен `derived_layer`
|
||||
- **THEN** отпечаток пересобранной витрины совпадает с отпечатком пересборки
|
||||
того же журнала из учёта без проставленных слоёв
|
||||
|
||||
### Requirement: Подмену рабочей базы делает человек
|
||||
|
||||
Система SHALL оставлять замену рабочей базы пересобранной человеку и
|
||||
выполнять её сама MUST NOT.
|
||||
|
||||
Файл базы держит открытым процесс сервиса, а переименование не касается уже
|
||||
открытого дескриптора: процесс продолжит писать в отвязанный inode, читатели
|
||||
увидят новый файл, и данные разойдутся молча. Документация SQLite называет
|
||||
переименование используемого файла прямой причиной порчи базы. Безопасная
|
||||
подмена требует остановленного сервиса, а остановить его команда не может:
|
||||
сервисом управляет окружение снаружи.
|
||||
|
||||
Отчёт SHALL печатать процедуру подмены буквально — команды, а не намёк, — и
|
||||
только тогда, когда прогон признан успешным (см. «Отчёт, оракул и исход
|
||||
команды»).
|
||||
|
||||
#### Scenario: Отчёт называет процедуру подмены
|
||||
|
||||
- **WHEN** прогон признан успешным
|
||||
- **THEN** отчёт содержит путь собранного файла и команды подмены
|
||||
|
||||
### Requirement: Отчёт, оракул и исход команды
|
||||
|
||||
Система SHALL завершать пересборку отчётом, который несёт счётчики
|
||||
(проиграно, свёрнуто, отказов по классам, тел без учётной записи, строк без
|
||||
тела, пропущенных файлов, повторов, объектов **до и после**) и **два
|
||||
отпечатка** — рабочей витрины и пересобранной, — с прямым ответом, совпали они
|
||||
или нет.
|
||||
|
||||
Отказы SHALL считаться **по классам**: слой не выводится, содержимое не
|
||||
разбирается, всё прочее. Невыведенный слой есть в каждом журнале и штатен;
|
||||
общий счётчик отправлял бы человека искать дефект там, где его нет. Отдельно
|
||||
называть человеку следует только нештатные отказы.
|
||||
|
||||
Число объектов «было и стало» SHALL печататься рядом с отпечатками: отпечатки
|
||||
отвечают «да/нет», а решение о подмене необратимо, и по «да/нет» нельзя
|
||||
судить о **направлении** расхождения. Именно пара чисел — 1737 против 1742 —
|
||||
поймала прошлый дефект наследования слоя.
|
||||
|
||||
Отпечаток здесь оракул, а не украшение: число объектов к правилу разрешения
|
||||
столкновений нечувствительно — на координате всегда ровно одна точка, и правило
|
||||
выбирает, какая, а не сколько. «Объектов столько же» совпало бы и при заведомо
|
||||
сломанном правиле.
|
||||
|
||||
Отпечаток рабочей витрины SHALL сниматься **до** начала проигрывания, а число
|
||||
доставок в рабочей базе — до и после. Ненулевая разница SHALL называться в
|
||||
отчёте, и при ней процедура подмены печататься MUST NOT: доставки, приехавшие за
|
||||
время прогона, есть в рабочей базе и в архиве, но не в собранном файле, и
|
||||
подмена стёрла бы их учёт вместе с заголовками, которых в архиве нет.
|
||||
|
||||
Величины, которые не снимались, отчёт печатать MUST NOT. При отмене отпечаток
|
||||
пересобранной витрины и число доставок после прогона не измеряются вовсе —
|
||||
печатать их сравнение значило бы выдать неизмеренное за измеренное, причём в
|
||||
единственном оракуле задачи. Ожидаемые классы расхождения (новые доставки за время прогона,
|
||||
непереносимый признак запечатанного часа, исправленный разбор) SHALL называться
|
||||
отдельно от самого факта расхождения.
|
||||
|
||||
**Исход команды.** Расхождение отпечатков отказом быть MUST NOT: после
|
||||
исправления разбора оно ожидаемо и есть сам смысл пересборки. Отказ отдельной
|
||||
доставки отказом команды тоже MUST NOT быть: доставка, слой которой не
|
||||
выводится, — штатный исход.
|
||||
|
||||
Отказом команды SHALL быть: пустой журнал, отсутствие хотя бы одной свёрнутой
|
||||
доставки, отмена и любая ошибка окружения. Пустая витрина совпадает по
|
||||
отпечатку с пустой витриной, поэтому прогон по пустому журналу выглядит
|
||||
идеальной сходимостью — а все умолчания подыгрывают такому запуску: конфига
|
||||
может не быть вовсе, и тогда пути указывают в рабочий каталог процесса. Человек,
|
||||
выполнивший напечатанную процедуру, заменил бы витрину пустой.
|
||||
|
||||
Отчёт значений точек, имён метрик, имён устройств и содержимого тел содержать
|
||||
MUST NOT: отпечаток берёт содержимое хешем. Ограничение относится к отчёту в
|
||||
стандартном выводе; лог свёртки живёт по правилам спеки хранения, где координаты
|
||||
столкновения (метрика, слой, час) разрешены явно.
|
||||
|
||||
Отчёт идёт в стандартный вывод человеческим текстом. Прогресс длинного прогона
|
||||
SHALL идти в поток ошибок, а не смешиваться с отчётом: прогон на полном архиве
|
||||
молчит минутами, и зависший неотличим от идущего.
|
||||
|
||||
#### Scenario: Отчёт сравнивает отпечатки
|
||||
|
||||
- **WHEN** пересборка завершилась
|
||||
- **THEN** отчёт содержит отпечаток рабочей витрины и отпечаток пересобранной
|
||||
- **AND** прямо называет, совпали они или нет
|
||||
- **AND** называет, изменилось ли число доставок в рабочей базе за время прогона
|
||||
|
||||
#### Scenario: Расхождение отпечатков не является отказом
|
||||
|
||||
- **WHEN** отпечаток пересобранной витрины отличается от рабочей, и при этом
|
||||
хотя бы одна доставка свёрнута
|
||||
- **THEN** команда завершается успешно, а расхождение названо в отчёте
|
||||
|
||||
#### Scenario: Пустой журнал — отказ, а не идеальная сходимость
|
||||
|
||||
- **WHEN** в архиве не нашлось ни одного тела
|
||||
- **THEN** команда завершается ненулевым кодом
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Ни одна доставка не свернулась
|
||||
|
||||
- **WHEN** журнал непуст, но свернуть не удалось ни одной доставки
|
||||
- **THEN** команда завершается ненулевым кодом
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Приезд доставок за время прогона отменяет подмену
|
||||
|
||||
- **WHEN** число доставок в рабочей базе за время прогона изменилось
|
||||
- **THEN** отчёт называет разницу
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Отчёт после отмены не сравнивает неизмеренного
|
||||
|
||||
- **WHEN** прогон отменён
|
||||
- **THEN** отчёт не содержит ни ответа о совпадении отпечатков, ни разницы
|
||||
числа доставок
|
||||
|
||||
#### Scenario: Рабочей базы нет вовсе
|
||||
|
||||
- **WHEN** файла рабочей базы не существует
|
||||
- **THEN** пересборка идёт по одним подобранным телам
|
||||
- **AND** отчёт называет, что сверять не с чем и что заголовки доставок не
|
||||
восстанавливаются
|
||||
|
||||
#### Scenario: Отчёт не раскрывает данных о здоровье
|
||||
|
||||
- **WHEN** отчёт напечатан
|
||||
- **THEN** он не содержит ни значений точек, ни имён метрик, ни имён устройств
|
||||
|
||||
### Requirement: Отказ на одной доставке не останавливает пересборку
|
||||
|
||||
Система SHALL продолжать проигрывание, когда отдельная доставка не сворачивается
|
||||
(тело не читается, тело больше предела, слой не выводится, содержимое не
|
||||
разбирается), и учитывать такие доставки счётчиком отказов.
|
||||
|
||||
Останавливаться на первой нельзя: журнал заведомо содержит доставки, слой
|
||||
которых не выводится, — это штатный исход, а не поломка, и он не должен лишать
|
||||
пересборки остальные тела.
|
||||
|
||||
Отмена, наоборот, останавливать проигрывание SHALL: это требование прекратить
|
||||
работу, а не свойство доставки. Источник отмены SHALL быть назван: команду
|
||||
прерывает человек, и без перевода сигнала прерывания в отмену контекста
|
||||
требование к поведению по отмене недостижимо в эксплуатации — процесс умирает
|
||||
мимо всей логики. По отмене команда SHALL напечатать частичный отчёт и
|
||||
завершиться ненулевым кодом.
|
||||
|
||||
#### Scenario: Битое тело не срывает прогон
|
||||
|
||||
- **WHEN** одно из тел архива не распаковывается
|
||||
- **THEN** остальные доставки проигрываются
|
||||
- **AND** отказ учитывается счётчиком в отчёте
|
||||
|
||||
#### Scenario: Отмена прекращает проигрывание
|
||||
|
||||
- **WHEN** сигнал прерывания приходит посреди журнала
|
||||
- **THEN** проигрывание прекращается, печатается частичный отчёт
|
||||
- **AND** команда завершается ненулевым кодом
|
||||
- **AND** файла по пути назначения не остаётся
|
||||
Reference in New Issue
Block a user