Перейти к содержанию

Foundry: контрпример и регрессионный тест

В предыдущем уроке мы подготовили бюджет и seed. Теперь рассмотрим, как возможное нарушение превращается в понятную регрессию. Для примера возьмём намеренно ошибочную правку: отключение удаляет всю запись вместо изменения флага.

Это учебный разбор предполагаемого дефекта, а не ошибка, найденная запущенным генератором. Программы не исполнялись, реальный отчёт уменьшения не получен, базовый CourseCatalog не меняем. Результатом станет короткий исходник, который выражает требование сохранения последнего отпечатка.

Формулируем нарушение до сокращения

По договору deactivate переводит активный курс в неактивный, сохраняя contentHash. В ошибочном варианте разработчик заменил присваивание courses[id].active = false на delete courses[id]. Последний оператор возвращает оба поля struct к значениям по умолчанию.

Поэтому нарушение относится не к самому факту отключения: active действительно становится false. Теряется хеш последнего выпуска. Если проверять только флаг, ошибочная правка может выглядеть правильной. Независимая модель из двадцать первого урока ожидает прежний digest и предназначена для обнаружения этого различия.

Для самостоятельного эксперимента такую правку можно рассматривать в отдельном типе BrokenCourseCatalog или отдельной копии проекта. Нельзя незаметно вносить её в основной реестр, по которому строятся остальные уроки. Здесь достаточно показать сам ошибочный оператор и его отношение к договору.

// Намеренно неверная замена в отдельном учебном варианте:
delete courses[courseId];

// Требуемое поведение базового CourseCatalog:
courses[courseId].active = false;

Обе строки находятся после проверок роли и активности соответствующего метода. Различие проявляется только тогда, когда запись уже содержит ненулевой выпуск. На пустом mapping они дали бы одинаковые значения, но до них базовая деактивация пустого курса вообще не должна дойти.

Ищем необходимую историю

Предположим, длинная случайная последовательность содержит десятки публикаций и чужих попыток. Для рассматриваемого дефекта важны только два разрешённых действия над одним номером: сначала создать ненулевой выпуск, затем отключить его. Остальные шаги могут быть несущественными.

Сокращение контрпримера стремится убрать лишние действия и упростить входы, сохранив нарушение свойства. Однако результат инструмента всегда следует прочитать по смыслу. Маленькая последовательность может зависеть от скрытого начального состояния или преобразований handler, которые нужно перенести в объяснение.

В нашем мысленном примере выбираем номер 17 и отпечаток keccak256("edition-one") как различимую фикстуру. Он не является реальным SHA-256-манифестом библиотеки. Для дефекта сохранения достаточно любого допустимого ненулевого bytes32; происхождение учебного входа не меняет требование.

Шаг Ожидание модели Ошибочное состояние
Публикация (digest, true) (digest, true)
Отключение (digest, false) (0, false)

Нарушение появляется на втором шаге. Третий вызов не требуется. Если затем снова опубликовать курс, новый digest может скрыть потерю в конечном состоянии, поэтому сравнение после каждого существенного перехода важнее одного чтения в самом конце длинной истории.

Сохраняем понятную регрессию

Добавим функцию в обычный CourseCatalogTest, который использует правильный базовый реестр и пустой setUp. Имя отражает прикладное требование, а не историю происхождения случайного seed. Вспомогательная assertCourse уже определена в девятом уроке.

function testRegressionDeactivationPreservesDigest() public {
    bytes32 digest = keccak256("edition-one");
    vm.prank(publisher);
    catalog.publish(17, digest);
    assertCourse(17, digest, true);

    vm.prank(publisher);
    catalog.deactivate(17);
    assertCourse(17, digest, false);
}

При правильной реализации эта функция должна сохранить ненулевой digest и выключить активность. При намеренном delete последнее сравнение хеша должно нарушиться. Это ожидаемое различие, которое следует подтвердить отдельным будущим исполнением; текущая статья не заявляет, что оно уже наблюдалось.

Функция похожа на часть жизненного цикла из девятого урока. Регрессия полезна как более узкий самостоятельный договор и диагностическое имя. Не обязательно плодить полностью одинаковые сценарии: в реальном наборе можно сохранить один ясный вариант или переименовать существующий, объяснив его назначение.

Убираем лишние условия осторожно

Чужая попытка публикации не нужна для этого дефекта, потому что проблема проявляется у законного издателя. Нулевой номер тоже не подходит: он отклоняется до записи. Передача второго курса не требуется, если нарушение касается только удаления выбранной записи.

Но если другой найденный дефект зависит от взаимодействия двух номеров, такое сокращение изменит предмет. Например, ошибочная общая переменная последнего хеша может проявляться только после публикации соседнего курса. Нельзя механически уменьшать любую историю до одного объекта без повторного подтверждения нарушения.

Также сохраняем роль издателя. Если убрать prank, базовый реестр отклонит вызов, и сценарий перестанет исследовать ошибочное отключение. Минимальный контрпример должен содержать всю необходимую подготовку, даже если она выглядит повторяемой и знакомой читателю.

Записываем происхождение находки

Для настоящего результата кампании отчёт должен включать нарушенное свойство, исходную историю, уменьшенный вариант, версии, seed, параметры и фактический вывод. Только после такого запуска можно написать, что нарушение действительно найдено и воспроизведено.

Для текущей подготовки статус иной: дефект сконструирован для объяснения, ожидаемый контрпример выведен из семантики delete, регрессионный исходник записан, исполнение отсутствует. Такое различие не уменьшает учебную ценность: читатель понимает механизм и границу имеющихся свидетельств.

После исправления модели не следует удалять регрессию только потому, что случайный генератор больше не выбирает прежнюю историю. Короткий сценарий защищает конкретное требование при будущих правках. Новые кампании продолжают искать иные последовательности, дополняя, а не заменяя сохранённую находку.

Не меняем ожидание ради успеха

Исправление и регрессия должны сохранять один предмет обсуждения. Если одновременно переименовать ошибки, изменить права и заменить формат записи, будет трудно понять, какая правка устранила потерю отпечатка. Для учебного дефекта достаточно вернуть присваивание флага и оставить остальные требования прежними. После будущего исполнения в отчёте отдельно фиксируются ожидаемое нарушение ошибочного варианта и сохранение договора правильного экземпляра.

Если ошибочный реестр возвращает ноль, соблазнительно исправить модель так, чтобы она тоже ожидала удаление. Тогда сравнение станет согласованным, но исходный договор изменится без решения. Сначала выясняем, какое поведение требуется продукту, затем исправляем реализацию или явно выпускаем новую несовместимую модель.

В нашей библиотеке сохранённый отпечаток позволяет различать пустой и ранее отключённый курс. Это причина требования, а не случайная особенность теста. Регрессионная функция связывает оператор кода с наблюдаемым смыслом записи и делает будущую потерю этого свойства заметной.

В следующем уроке перейдём к отдельному читателю внешнего реестра. Удаление значений описано в Solidity 0.8.30, работа с последовательностями — в руководстве Foundry. Оглавление курса.

Оглавление серии