Files
memo/docs/orders/13-modelstore-coverage.md
T
subochev 69a0ebe0fa tests: ModelStoreTest — закрыты дыры покрытия Range и Content-Range
resumesPartialDownload: счётчики Range-запросов и байт; сервер отдаёт 400
без Range для .part — запасной путь «начать заново» закрыт.

failsOnSizeMismatch разделена на два:
  - rejectsWrongTotalFromContentRange — сервер соврал в Content-Range,
    бьёт в проверку actualTotal != expectedTotal (вторая, достижимая);
  - truncatedResponseDoesNotProduceFile — оборванный ответ, по факту ловит
    ветку IOException → «не удалось скачать» (а не first size check).

В mutations.tsv убрана строка про if (declaredBody >= 0 && written !=
declaredBody) — через java.net/http недостижимо (оборванный ответ даёт
IOException, не EOF). Добавлена строка про if (expectedTotal >= 0 &&
actualTotal != expectedTotal) — теперь убивает rejectsWrongTotalFromContentRange.

Все 18 мутаций (4 из ModelStore) убиты прогоном mutation_check.py.
2026-10-02 04:48:35 +03:00

8.2 KiB
Raw Blame History

Проект: /root/WORK/memo (Kotlin/JVM). Заказ: закрыть ДВЕ дыры в покрытии ModelStoreTest, доказанные мутационной проверкой. Правки только в тестовом файле, боевой код НЕ трогать.

Прогон, который их нашёл: python3 e2e/mutation_check.py --gradle ./gradlew

Выжившие мутации:

  1. ModelStore.kt: if (startAt > 0L) requestBuilder.header("Range", "bytes=$startAt-") → if (false) ... | ожидался провал resumesPartialDownload — НЕ упал.
  2. ModelStore.kt: if (declaredBody >= 0 && written != declaredBody) { → if (false) { | ожидался провал failsOnSizeMismatch — НЕ упал.

Почему они выжили (разобрано, не догадка)

1. resumesPartialDownload. Тестовый сервер (serveBytes) отдаёт 206 с хвостом только если в запросе есть заголовок Range; без него он отдаёт 200 и полное содержимое. Поэтому при мутации (Range не отправляется) срабатывает штатный запасной путь «начать заново», итоговый файл получается правильным — и тест, проверяющий только «файл в итоге верный», проходит. Тест не проверяет того, ради чего написан: что докачка действительно шла хвостом.

2. failsOnSizeMismatch. Сервер объявляет Content-Length больше, чем пишет, соединение обрывается — у клиента вылетает IOException, который download превращает в IllegalStateException("не удалось скачать ..."). Тест принимает msg.contains("не удалось скачать"), поэтому проверка размера не проверяется вообще: исключение приходит из другого места.

Важное наблюдение по бою. В ModelStore.download две проверки размера: written != declaredBody (первая) и part.length() != expectedTotal (вторая, из Content-Range). Через java.net.http первая на практике недостижима: оборванный ответ всегда даёт IOException, а не чистый EOF. Значит это защитный код, а не дыра в покрытии — в таблице мутаций его надо заменить на мутацию ВТОРОЙ проверки, которая достижима (см. ниже).

Что сделать

Правка теста resumesPartialDownload

Сделать так, чтобы «докачка» была доказана, а не предположена:

  • сервер записывает в счётчики: сколько запросов пришло, у скольких был заголовок Range, и сколько всего байт тела он отдал;
  • если заголовка Range нет для файла, у которого уже есть .part — сервер отвечает кодом 400 и тела не отдаёт (докачки без Range не бывает; тест не должен иметь запасного пути);
  • после ensure тест обязан утверждать:
    • rangeRequests >= 1 — докачка действительно была запрошена;
    • файл в итоге побайтно равен исходному (оставить);
    • .part переименован (оставить).

При мутации №1 (Range не отправляется) сервер ответит 400 → ensure бросит исключение → тест упадёт.

Замена сценария failsOnSizeMismatch на два теста

rejectsWrongTotalFromContentRange (новый, закрывает мутацию №2) — «сервер соврал про общий размер, файл принимать нельзя»:

  • в директории лежит .part = первые 4000 байт файла из 8000;
  • сервер на запрос с Range: bytes=4000- отвечает 206:
    • Content-Length: 4000, тело — реальный хвост 4000 байт (то есть транспорт отдаёт ровно столько, сколько объявил — никакой IOException);
    • Content-Range: bytes 4000-7999/999999 — итог соврал;
  • ожидание: ModelStore.ensure бросает IllegalStateException, сообщение содержит размер, целевого файла нет, .part удалён.

Проверить, что сценарий действительно бьёт в нужную проверку: при if (false) на part.length() != expectedTotal тест обязан провалиться (файл будет установлен, исключения не будет).

truncatedResponseDoesNotProduceFile (переименовать бывший failsOnSizeMismatch) — оставить как проверку поведения «оборванный ответ не оставляет файла», но убрать из принимаемых сообщение «не удалось скачать», чтобы тест не «зеленел» за счёт сетевой ошибки. Ожидать явно любое IllegalStateException с непустым сообщением и отсутствие целевого файла и .part. Отдельно проверить и записать в отчёте: какую ветку кода реально ловит этот тест (по сообщению) — то есть является ли он проверкой размера или сетевого обрыва. В отчёте написать прямо.

Таблица мутаций e2e/mutations.tsv

  • Убрать строку, целящуюся в if (declaredBody >= 0 && written != declaredBody) { (по разбору выше — недостижимо через java.net.http; ложная цель).
  • Добавить мутацию во вторую проверку: if (expectedTotal >= 0 && actualTotal != expectedTotal) { → if (false) {, обязанный уронить rejectsWrongTotalFromContentRange.
  • Строку про Range оставить, но теперь она обязана валить resumesPartialDownload.

Правило: каждая строка таблицы — либо убитая мутация, либо честное объяснение в комментарии, почему цель недостижима (с доказательством прогоном).

Обязательная проверка (приложить вывод)

cd /root/WORK/memo
python3 e2e/mutation_check.py --gradle ./gradlew 2>&1 | tail -25
echo "--- ожидается: ВСЕ МУТАЦИИ УБИТЫ"
./gradlew test --rerun-tasks -q 2>&1 | tail -3

Плюс отдельно, для каждого из двух новых/изменённых тестов — доказательство, что он валит мутацию: применить мутацию руками, прогнать только ModelStoreTest, показать FAIL, откатить (git checkout -- memo-core/src/main/kotlin/memo/core/ModelStore.kt), убедиться в PASS.

СТРОГИЕ ЗАПРЕТЫ

  • Не менять боевой код ModelStore.kt (и вообще ничего в src/main). Заказ — только тесты и таблица мутаций.
  • Не менять другие тесты.
  • Не добавлять зависимости.
  • Не выводить план текстом; сразу правь файлы.
  • Не удалять .gitignore, не коммитить models/ и *.db.