Тесты проходят, а ошибка в коде есть: четыре класса проверок, которые ловят пропущенное
Иван Недомолков · Опубликовано:
Тесты проходят, а ошибка в коде есть, потому что тест проверяет ситуацию, которую автор себе представил. Дефект живёт в ситуации, которую он не представил, и дописать туда сценарий тот же человек не может. Такие ошибки ловят проверки другого устройства: инвариант на результат, перевод метрики обратно в штуки, аудит чужими руками с требованием показать вход, контроль обрезки выхода. Ниже каждая из них разобрана отдельно, с дефектом, который она поймала, и с тем, что она пропустит.
Случай, на котором это видно. У функции было 15 тестов, все зелёные. Потом пять независимых проверок разобрали тот же код, каждая с задачей назвать конкретный дефект и дать вход, на котором он воспроизводится. Нашлось 8 дефектов, все подтвердились, один из них тихо терял записи.
Граница сразу: функция написана не на встроенном языке 1С. Это служба вокруг языковой модели, которая применяет её решения к плану группировки записей: какие группы слить, какую запись куда перенести. Группы в тексте называются кластерами, с кластером серверов 1С это не связано. Переносится на 1С сама методика, цифры по токенам к 1С отношения не имеют.
Почему не помогает дописать тестов
Первый порыв после счёта 15:8 понятен: писать больше. Автор задним числом попробовал придумать тесты на найденные дефекты так, будто ещё о них не знает. Придумались 2 из 8. Весь набор до разбора насчитывал 195 прошедших тестов, и ни один из восьми дефектов он не поймал.
После разбора в набор легли 6 регрессионных тестов. Они фиксируют найденное, чтобы оно не вернулось. Новое они искать не помогут, потому что написаны тем же человеком. Отсюда рабочий вывод: тесты держат контракт и регрессии, а за неизвестными дефектами идут проверки из таблицы ниже.
| Класс проверки | Что ловит | Чего не ловит |
|---|---|---|
| Инвариант на результат | потерю записей, дубли, коллизии и висячие ссылки на любом входе, в том числе непридуманном | ошибку в смысле, если количество и связи сходятся |
| Метрика в штуках | единичный дефект за красивым процентом | дефект, который метрику не двигает |
| Независимый аудит с воспроизводимым входом | слепые зоны автора и унаследованные из образца ошибки | цену и долю ложных находок, их надо считать отдельно |
| Контроль обрезки выхода | ответ, срезанный по лимиту и выглядящий целым | ошибку внутри полного ответа |
Инвариант: свойство, которое держится на любом входе
Сценарный тест говорит “на таком входе будет такой выход”. Инвариант говорит “на любом входе выход обладает таким свойством”. Первый ограничен фантазией автора, второй нет.
Что поймал. Когда при слиянии кластеров выжившим оставался одиночный элемент, функция теряла записи без ошибки и без строки в логе. Модель по контракту имела право на такое слияние, так что формально всё было законно. Функция при этом обязана не терять ничего при любых решениях на входе, и этого свойства в тестах не было, потому что его никто не сформулировал.
Остальные находки того же рода, это нарушения целостности всего результата:
- один элемент попадал сразу в два итоговых списка;
- после перенумерации два разных объекта получали один идентификатор;
- в результате оставалась ссылка на кластер, которого уже нет после слияния;
- одна исходная запись приезжала в результат дважды.
Для любого кода, который перекладывает и группирует данные, набор почти всегда один: сохранность количества, уникальность ключей, отсутствие висячих ссылок, отсутствие пересечений между наборами. Четыре проверки, около часа работы.
Как применить в 1С. Та же механика работает в модуле обработки, в общем модуле, в проведении документа: везде, где строки сливаются, переносятся и перенумеровываются. Проверку ставят в конце процедуры, на её собственный результат. Набросок на таблице значений, не из разобранного случая:
// Инвариант: уникальных ключей на выходе столько же, сколько на входе
КлючиВхода = Вход.Скопировать(, "Ключ");
КлючиВхода.Свернуть("Ключ");
КлючиВыхода = Выход.Скопировать(, "Ключ");
КлючиВыхода.Свернуть("Ключ");
Если КлючиВыхода.Количество() <> КлючиВхода.Количество() Тогда
ВызватьИсключение СтрШаблон("Потеря записей: на входе %1, на выходе %2",
КлючиВхода.Количество(), КлючиВыхода.Количество());
КонецЕсли;
// Дубль: строк на выходе больше, чем уникальных ключей
Если Выход.Количество() <> КлючиВыхода.Количество() Тогда
ВызватьИсключение "Одна запись попала в результат дважды";
КонецЕсли;
Сравнивать надо уникальные ключи. В разобранном прогоне на входе было 307 записей и 294 уникальных идентификатора: 13 дублей пришли из источника до всякой обработки. Инвариант на сырое количество строк либо ругался бы на чужие дубли, либо прикрыл бы собственную потерю.
Где инвариант слепнет. За функцией в цепочке стоял валидатор, который чинил результат постфактум: возвращал потерянное, схлопывал дубли. На выходе цепочки всё сходилось, и восемь дефектов доехали до аудита живыми. Проверка, поставленная после такой страховки, видит уже исправленный результат. Ставьте инвариант до неё, а страховку заставьте считать свои срабатывания и писать счётчик в лог. В 1С это касается нормализаторов, “обработки исключений на всякий случай” и перепроведения по расписанию, которое молча правит остатки. Ноль срабатываний за неделю говорит, что механизм под страховкой здоров. Сотни говорят, что он давно сломан.
Арифметическая сверка: метрика обратно в штуки
Что это. Любой процент или коэффициент переводится в единицы, и дальше вопрос звучит так: может ли такое количество быть в принципе.
Что поймала. Покрытие по итогам прогона показало 0,997 при шести дублирующих размещениях. 99,7 процента выглядит отлично. Умножение даёт другое: 294 × 0,997 = 293,1, дробной записи не бывает, значит не хватает ровно одной. Разбор подтвердил, что модель выкинула одну запись. Метрика прятала единичный дефект, у которого есть конкретный вход, и такую задачу уже можно чинить.
Та же арифметика раньше отделила чужое от своего: 307 минус 294 даёт 13 дублей в источнике. Часть “ошибок” на выходе приехала со входа, и это надо знать до правки своего кода.
Как применить в 1С. Отчёт обработки, который пишет “загружено 99,7 %”, “совпало 0,98”, пересчитывайте в строки от числа уникальных объектов на входе. Если выходит “не хватает одной”, ищите её поимённо. Сама проверка ничего не стоит: одно умножение.
Чего не видит. Дефекта, который метрику не сдвигает. Если потерянная запись заменена дублем, счёт в штуках сойдётся, и тут нужен инвариант на уникальность из предыдущего раздела.
Независимый аудит: чужими руками и с входом на каждую находку
Что это. Код разбирают те, кто его не писал, и каждая находка обязана прийти с входом, на котором дефект воспроизводится. Без входа находка остаётся мнением, с ним она становится задачей. В случае проверок было пять, и все восемь находок подтвердились.
Что поймал. Слепые зоны автора, все восемь дефектов. И ещё одно: два из восьми сидели в эталонной реализации, с которой автор сверялся, когда писал функцию. Логика была честно повторена вместе с ошибкой. Дефект в своём коде живёт в одном месте, дефект в образце расходится копипастой по всем, кто образцу поверил. В 1С в эту категорию попадают типовые механизмы и чужие примеры, которые переходят из проекта в проект. Нашли у себя дефект, откройте источник, откуда взята логика: если он там тоже есть, задача больше вашей правки.
Самая дешёвая версия. Машина кода не писала, и в этом половина пользы: читает всё подряд и своему не делает скидок. Анализатор кода внешних обработок на семнадцати наших собственных обработках выдал 239 замечаний, из них 186 критичных, и два красных светофора. Ни одного из восьми дефектов этого случая он бы не нашёл: у него каталог правил, инвариантов и замысла он не знает. Он снимает очевидное до того, как звать человека, подробно про это в разборе анализа кода внешних обработок 1С. Если проверяющим будет языковая модель, её саму сначала принимают на своём коде: как это делается, разобрано в статье про нейросеть для проверки кода 1С.
Чего не знаем. Цена пяти проверок не измерена, и это главная дыра для решения “ставить ли такое в процесс”. Сколько находок было заявлено и сколько отсеялось, не посчитано. Хватило бы трёх проверок или восьмой дефект всплыл бы только на седьмой, неизвестно. Два дефекта в эталоне переданы владельцу, исправлены ли они, не отслежено. Поэтому до внедрения заведите два счётчика: находки заявленные и подтверждённые. Без второго метод не защитить перед тем, кто за него платит.
Контроль обрезки: выход, который упёрся в лимит и выглядит целым
Что это. Проверка, что ответ пришёл полностью. Обрезанный по длине ответ разбирается и читается без ошибок, просто в нём нет хвоста. Если признак завершения не читать, система работает, мониторинг спокоен, а часть решений пропала.
Что поймал. Шаг согласования в той же цепочке каждый раз выдавал план целиком заново, 50-65 тысяч токенов. Одна модель укладывалась впритык: 54 528 токенов, 481 секунда, первая попытка пришла пустой. Вторая обрывалась ровно на 64 000 с признаком завершения “по длине”, 859 секунд. Обрыв ровно на круглом лимите сам указывает на потолок.
Модель с большим лимитом задачу не решала: ни одна из проверенных не выполняла требование к размеру выхода, у рассуждающих объём съедали рассуждения, у обычных число действий. Смена модели двигала потолок на десятки процентов, нужно было в десять раз. Решил формат: шаг стал отдавать только дельты, что с чем слить и что куда переназначить, а применял их обычный код с одинаковым результатом на одинаковом входе. Выход упал до 4 951 токена, в 10-13 раз, первая попытка прошла за 243,9 секунды. Типичный прогон это 19 слияний и 72 переназначения, 91 действие. Детализация выросла: 60 кластеров против 21 у прежнего варианта.
Как применить в 1С. Если обработка зовёт модель по HTTP, признак завершения из ответа читается и ответ, срезанный по длине, в дело не идёт. Если выход растёт вместе с объёмом данных, просите изменения и применяйте их своим кодом.
Родственный тихий отказ. Там же сетевая библиотека держала зашитый таймаут в 30 секунд без настройки и падала с пустым текстом ошибки. Пишите в лог тип исключения рядом с текстом: пустое сообщение и есть подсказка, что виновата сетевая библиотека. И последнее из того разбора: флаг из переменных окружения не доходил до контейнера, а тесты это пропускали, потому что подменяли вызов уровнем выше того места, где флаг читается. Тест был зелёный, проверял он другой слой.
Порядок перед внедрением обработки
- Выписать свойства результата, которые обязаны держаться всегда, и поставить их проверку в конце процедуры, до любой страховки.
- Посчитать уникальные ключи на входе, чтобы отделить дубли источника от своих.
- Каждую относительную метрику отчёта пересчитать в строки.
- Прогнать код через анализатор, затем отдать тому, кто его не писал, с условием: находка без воспроизводимого входа не принимается.
- На каждый найденный дефект проверить образец, откуда взята логика.
- Для внешних вызовов читать признак завершения ответа и тип исключения.
Заложить такую приёмку можно ещё в ТЗ, до первой строки кода: в заказной разработке ТЗ со сметой идёт первым шагом. Открытым остаётся вопрос про эталоны. Если вы ловили дефект, унаследованный из типового механизма или авторитетного примера, чем он нашёлся: тестом, инцидентом в работе или случайно?