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.
8.2 KiB
Проект: /root/WORK/memo (Kotlin/JVM). Заказ: закрыть ДВЕ дыры в покрытии ModelStoreTest,
доказанные мутационной проверкой. Правки только в тестовом файле, боевой код НЕ трогать.
Прогон, который их нашёл: python3 e2e/mutation_check.py --gradle ./gradlew
Выжившие мутации:
ModelStore.kt:if (startAt > 0L) requestBuilder.header("Range", "bytes=$startAt-")→if (false) ...| ожидался провалresumesPartialDownload— НЕ упал.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.