Foundry: граничные значения чисел и байтов
В предыдущем уроке мы проследили жизненный цикл номера 17. Теперь изменим сами входные значения. Один привычный номер и отпечаток строки не показывают, как метод относится к нулю и крайним величинам типа.
Подготовим таблицу границ и отдельные функции для допустимых и запрещённых значений. Они дополняют CourseCatalogTest, не меняя общий setUp. Код не запускался; ниже описан ожидаемый договор, а не измеренный результат работы компилятора или EVM.
Граница определяется типом и требованием
Параметр courseId имеет тип uint256. В него входят целые числа от нуля до 2²⁵⁶ − 1. Контракт исключает только ноль. Следовательно, 1 и максимальное значение остаются допустимыми идентификаторами, даже если внешний каталог обычно использует небольшие номера.
Поле contentHash имеет фиксированную длину bytes32. Контракт запрещает полностью нулевую последовательность, но не проверяет происхождение любого другого значения. Любой ненулевой набор из 32 байтов допустим для этого метода. Название поля не превращает валидацию в проверку существования файла или правильности выбранного алгоритма хеширования.
| Номер | Отпечаток | Отправитель | Требуемый результат |
|---|---|---|---|
0 |
Ненулевой | Издатель | InvalidCourse() |
1 |
Ненулевой | Издатель | Активная запись |
type(uint256).max |
Ненулевой | Издатель | Активная запись |
17 |
bytes32(0) |
Издатель | InvalidCourse() |
17 |
Ненулевой | Читатель | Unauthorized() |
Сначала отделяем роль, затем валидность данных. Если одновременно передать нулевой номер и чужого отправителя, первой сработает проверка доступа. Такой случай полезен для договора порядка ошибок, но не заменяет проверку нулевого номера разрешённым издателем.
Записываем минимальное и максимальное значения
Вспомогательная функция assertCourse появилась в девятом уроке. Здесь она помогает сравнить два допустимых края. Для верхней границы не набираем вручную длинную десятичную строку: выражение типа задаёт значение непосредственно.
function testMinimumAndMaximumIdsAreAllowed() public {
bytes32 minimumDigest = bytes32(uint256(1));
bytes32 maximumDigest = bytes32(type(uint256).max);
vm.startPrank(publisher);
catalog.publish(1, minimumDigest);
catalog.publish(type(uint256).max, maximumDigest);
vm.stopPrank();
assertCourse(1, minimumDigest, true);
assertCourse(type(uint256).max, maximumDigest, true);
}
Преобразование uint256(1) в bytes32 даёт фиксированный ненулевой набор байтов. Это учебная величина для проверки валидатора. Мы не утверждаем, что она является хешем опубликованного учебника. Максимальный набор также служит входом, который разрешён текущим условием.
Особенность mapping позволяет обращаться к огромному ключу без предварительного создания массива длиной до этого номера. Поэтому максимум идентификатора не означает, что контракт выделит триллионы записей. Для массива требования были бы другими, и такой сценарий нельзя механически переносить на другую структуру данных.
Нулевой отпечаток проверяем отдельно
Сценарий нулевого номера уже был введён в шестом уроке. Теперь добавим проверку второго аргумента, оставляя номер заведомо допустимым. Разделение помогает понять, какая часть валидации исчезла при будущей правке.
function testZeroDigestIsRejected() public {
vm.expectRevert(CourseCatalog.InvalidCourse.selector);
vm.prank(publisher);
catalog.publish(17, bytes32(0));
assertCourse(17, bytes32(0), false);
}
Если общий setUp по-прежнему пустой, после отклонённого вызова курс 17 остаётся пустым. Для сценария обновления существующего курса ожидание было бы другим: нужно сначала опубликовать допустимый хеш и после отказа увидеть его сохранение. Нулевая запись не является универсальным результатом любого отката.
Не объединяем два запрещённых аргумента в единственную проверку publish(0, 0). Реализация, проверяющая только один из них, всё равно отклонит такой вызов. Два независимых случая выражают оба требования и лучше обнаруживают неполную валидацию.
Проверяем ограничения преобразований
Solidity не принимает отрицательное число как корректное значение uint256 через обычную типизированную передачу. Однако интерфейс сайта может получить строку "-1" и попытаться преобразовать её. Ошибка этого преобразования относится к клиентскому вводу, а не к дополнительному состоянию реестра.
JavaScript Number также не представляет точно весь диапазон uint256. Для внешней формы нужны разбор строки и целочисленная модель, например bigint. Контрактные сценарии работают уже с типизированными величинами и сами по себе не доказывают, что браузер передаст нужный номер. Это рассмотрено в курсе Web3-интерфейса.
При преобразовании байтов важно не спутать фиксированную длину с текстом. Строка "0" не равна bytes32(0): она содержит символ с собственным кодом. Если проверка использует хеш строки, нулевая ветвь не исследуется. Поэтому отрицательный сценарий задаёт запрещённую последовательность явно.
Один шаг возле нуля
Для проверки нижней границы важна пара соседних случаев: запрещённый 0 и разрешённый 1. Если написать только отказ для нуля, реализация с ошибочным условием courseId <= 1 останется незамеченной. Успешная запись минимального допустимого номера обнаруживает такое расширение запрета.
Максимальное значение исследует другую ошибку — сужение допустимого диапазона или неявное преобразование к меньшему типу. Если внутри программы номер случайно приводится к uint32, большой ключ может обратиться к совершенно иной записи. Сравнение через исходный полный номер должно это показать. Таким образом, обе границы связаны с конкретными возможными дефектами, а не добавлены ради количества сценариев. Для нормального учебного номера 17 подобное усечение могло бы оставаться незаметным.
Разбираем ошибочную правку
Допустим, автор решит ограничить номер условием courseId < 1000, хотя требования каталога такого ограничения не содержат. Сценарий с максимальным номером обнаружит изменение допустимой области. Это не повод бездумно удалить сценарий: сначала нужно согласовать новый договор с потребителями.
Другой пример — замена проверки contentHash == 0 на проверку только первого байта. Некоторые ненулевые значения тогда будут отклонены. Наш минимальный ненулевой отпечаток полезен, потому что его значащая единица находится в конце, а множество предшествующих байтов нулевые. Полноценную область входов далее расширим fuzz-проверкой.
Граничные случаи выбираются по смыслу реализации, а не по количеству строк. Для метода с ценой понадобились бы переходы округления и величины единиц; для времени — равенство порогу и соседние моменты; для длины массива — пустой, одиночный и предельный набор. Здесь рассматриваем только существующие номер и отпечаток.
Сохранённая таблица станет основой следующего шага: генератор будет исследовать допустимые величины между выбранными границами. В следующем уроке запишем свойство публикации для параметров функции. Диапазоны типов описаны в Solidity 0.8.30. Оглавление курса.