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.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
Проект: /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`.
|
||||
|
||||
Правило: каждая строка таблицы — либо убитая мутация, либо честное объяснение в комментарии,
|
||||
почему цель недостижима (с доказательством прогоном).
|
||||
|
||||
## Обязательная проверка (приложить вывод)
|
||||
|
||||
```bash
|
||||
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`.
|
||||
Reference in New Issue
Block a user