# Decrusting the quickcheck crate

## Метаданные

- **Канал:** Jon Gjengset
- **YouTube:** https://www.youtube.com/watch?v=64t-gPC33cc

## Содержание

### [0:00](https://www.youtube.com/watch?v=64t-gPC33cc) Segment 1 (00:00 - 05:00)

Привет всем, добро пожаловать на очередной стрим! Сегодня мы снова будем разбирать ящики, чтобы понять, как ими пользоваться, как они работают, как устроены изнутри, и, в частности, чтобы вы лучше понимали принцип их работы и могли эффективно использовать их сами. Небольшое объявление перед началом: во-первых, если вы ещё не знаете, есть Discord, к которому вы можете присоединиться, где я публикую объявления о предстоящих стримах. Там я также объявляю о других событиях. Так что, если мы... вот он, но вы также можете просто зайти в Discord. j. u, и вас перенаправят сюда, на приглашение. Там нет чата или чего-то подобного, если вы не спонсор. Это просто канал для объявлений. Одно из объявлений, которое я сделал, — это то, что я выступлю с вступительной речью на EuroRust в Вене 10 октября. Доклад называется «Через огонь и пламя», и я собираюсь за 30 минут объяснить ряд распространенных препятствий на пути к продуктивной работе с Rust. Посмотрим, как всё получится. Но сегодня мы, в частности, рассмотрим крейт Quick Check. Крейт Quick Check, для тех, кто ещё не знает, — это крейт для тестирования свойств. Обещаю, я расскажу вам всё о том, что такое тестирование свойств, прежде чем углублюсь в это, потому что не все из вас, возможно, слышали об этом. У меня был предыдущий стрим, где я говорил о тестировании свойств с использованием другой библиотеки, называемой proptest. Это настройка тестирования свойств в CI для крейта Rust. видео есть глава, которая называется « Тестирование на основе свойств», где я начал рассказывать, что это значит и как использовать другой крейт. Я дам ссылку на него здесь для тех, кто смотрит это видео после просмотра. Между тестированием свойств и QuickCheck есть много различий, но одно из главных отличий заключается в том, что крейт QuickCheck намного проще. И это одновременно и хорошо, и плохо. Простота означает, что сборка, генерация примеров и тому подобное происходит немного быстрее, но он, как правило, менее продвинутый, немного сложнее в выполнении задач, он не так прост, как генерация строк с определенными шаблонами или числами в определенных диапазонах. Мы подробнее рассмотрим это, когда будем изучать сам крейт. Но одна из причин, по которой я хотел использовать QuickCheck, заключается в том, что QuickCheck — это... Достаточно просто, но при этом достаточно полезно, чтобы мы могли понять всю структуру программы и то, как она работает "под капотом", в этом единственном стриме. Я надеюсь, что этот стрим займет всего час, и я, пожалуй, возьму свои слова обратно. Я надеюсь, что он займет около часа, что, вероятно, сделает этот стрим самым коротким в моей жизни. Но мы узнаем, мы узнаем. Итак, очень кратко, что такое тестирование свойств? Идея тестирования свойств заключается в том, чтобы, или давайте сделаем шаг назад, тестирование свойств — это продвинутая форма фаззинга, если вы еще не знаете, что такое фаззинг. По сути, это взять программу, которую вы имеете, и подавать на нее случайные входные данные, а затем посмотреть, зависнет ли она. Вот что вы делаете с фаззингом. Это особенно распространено в криптографических библиотеках и тому подобном, но используется для всех видов программного обеспечения. Таким образом, идея заключается в том, что У вас есть программа, которая просто генерирует случайные входные данные любого типа, которые она принимает, а затем вы запускаете свою программу на ней и смотрите, какой результат она выдает. В целом, наблюдение за тестированием методом FUSS заключается в том, что ваша программа никогда не должна, например, вызывать ошибку сегментации (SEG) и никогда не должна паниковать. Она может вернуть ошибку, если входные данные недействительны, но не должна вызывать ошибку сегментации (SEG), которая указывает на проблему безопасности. И она, вероятно, никогда не должна паниковать, потому что это означает, что в вашей программе есть какой-то инвариант, который не соблюдается, и вместо этого должна была быть ошибка. Тестирование методом FUSS в некотором смысле является особой подкатегорией тестирования свойств. Тестирование свойств — это, опять же, генерация случайных входных данных для вашей программы, но разница в том, что при тестировании свойств вы проверяете определенное свойство выходных данных вашей программы в ответ на эти входные данные. Так что при тестировании методом FUSS обычно проверяется свойство, которое вызывает сбой программы. При тестировании свойств вы можете проверить дополнительные свойства, например, сбой программы. Вы, вероятно, обнаружили ошибку, но вы также можете проверить свойства, например, представьте, что у вас есть функция сортировки. Свойство, которое вы, возможно, захотите проверить, это: отсортирован ли результат после вызова функции сортировки? Кажется, это очевидное свойство для проверки. Есть и другие свойства, которые вы можете проверить, например, если у меня есть вектор длиной 10, и я его сортирую, после сортировки у меня все равно должен остаться

### [5:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=300s) Segment 2 (05:00 - 10:00)

вектор длиной 10. Таким образом, проверку свойств следует рассматривать как проверку конкретных свойств выходных данных вашей программы при случайно сгенерированных входных данных. Что же делает библиотека Quick Check в этом контексте? Библиотека Quick Check предоставляет примерно три вещи: она предоставляет способ генерации значений случайных типов; она генерирует случайные значения произвольных типов; она предоставляет способ проверить, успешно ли завершилась ваша программа или нет. То есть очевидные случаи сбоя обрабатываются автоматически: если ваша программа паникует, то это сбой тестового случая; если ваша программа возвращает ошибку, это также сбой тестового случая. Вы можете определить дополнительные свойства, которые вы считаете неудачным тестом, даже если нет никакой ошибки, например, фактического результата, ошибки, которая распространяется вверх, нет паники, но вы объявляете, что программа завершилась с ошибкой. Это второе, что она предоставляет, а третье — это способ уменьшить входные данные, чтобы найти наименьшие входные данные, наименьшие случайные входные данные, которые воспроизводят наблюдаемую вами ошибку. Давайте рассмотрим случай сортировки массива. Я предполагаю, что он случайным образом генерирует массив из 100 элементов и обнаруживает, что при вызове функции сортировки для этого конкретного массива он получает из 99 элементов, и он говорит: «Очевидно, это неправильно». Он может просто остановиться на этом и сказать: «Я дал этот массив в качестве входных данных, а получил выходных данных, и это неправильно согласно этому свойству». Но QuickCheck делает еще кое-что: у него есть понятие «уменьшителя». Идея уменьшения заключается в том, что вы нашли случай, когда ошибки пытаются уменьшить входные данные. Если вы все еще можете воспроизвести ту же ошибку, попробуйте уменьшить ее размер и посмотрите, сможете ли вы воспроизвести ошибку. Идея или надежда состоит в том, что в итоге вы получите массив, скажем, из четырех элементов, который после сортировки возвращает массив из трех элементов, и это, вероятно, будет проще отлаживать программисту. Итак, это три основных свойства: генерация случайных входных данных, проверка соответствия определенному свойству и уменьшение входных данных, когда вы обнаруживаете, что ваше значение нарушает определенное свойство для этих входных данных. Позвольте мне сделать паузу. Это, по сути, все, что касается тестирования свойств. Мы подробнее расскажем о том, как это делается, и о некоторых терминах и названиях вещей в этом мире. Но имеет ли смысл основная идея? Сколько проверок может проверить быстрая проверка? Если быстрая проверка может проверить несколько проверок, это довольно хорошо. Забавно. Хорошо, я не вижу никаких конкретных вопросов по поводу того, что я сказал до сих пор, что имеет смысл. Тестирование свойств на самом деле не является Всё это сложно... Реализация может быть сложной. Не в смысле самой библиотеки Quick Check, а в смысле написания собственных тестов свойств. Написание собственных тестов свойств может стать довольно запутанным процессом, во-первых, потому что генерация входных данных может иметь довольно сложный формат, а во-вторых, проверка свойств не всегда проста. Но сама идея проста. Итак, давайте посмотрим на крейт Quick Check. Начнём с файла README, где есть очень простой пример. Я немного увеличим масштаб. Вот как это работает: крейт Quick Check довольно старый, он всё ещё поддерживается, но, кажется, он поддерживает Rust начиная с версии 13 или около того. Есть что-то действительно невероятное... они обновились до версии 165. Думаю, он, вероятно, поддерживает и более старые версии с довольно небольшими проверками. В любом случае, пример здесь немного лишний, потому что он поддерживает гораздо более старые версии Rust. Но основная идея здесь заключается в том, что у нас есть функция reverse, которая принимает срез элементов и возвращает этот срез элементов. в виде вектора, но в обратном порядке, так что это функция, которую мы тестируем. Свойство, которое мы хотим проверить, заключается в том, что обратный порядок элементов соответствует исходному массиву. Другими словами, если у вас есть массив из четырех элементов, вы переворачиваете его, а затем переворачиваете еще раз, и в итоге должны получиться исходные четыре элемента. Есть еще множество других свойств, которые вы можете проверить, и это конкретное свойство демонстрируется здесь. Итак, тестируемую функцию вы пишете, не задумываясь о QuickCheck, вы просто пишете функцию так, как, по вашему

### [10:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=600s) Segment 3 (10:00 - 15:00)

мнению, она должна быть реализована. Вам не нужно думать о требованиях к библиотеке QuickCheck на этом этапе. Затем, когда вы хотите написать тест, модуль `mod test` — это своего рода стандартная номенклатура R. В этом случае используется макрос QuickCheck, и мы чуть позже рассмотрим, что делает этот макрос QuickCheck. Внутри макроса QuickCheck вы просто объявляете набор функций, каждая из которых названа в честь определенного свойства, которое вы хотите проверить. Можно сказать, что каждая из них в конечном итоге превращается в своего рода... Эм, вы знаете, функция проверки свойства. Эм, по сути, это просто добавляет немного шаблонного кода до и после этой функции, но каждая функция, которую вы объявляете здесь, будет проверять определенное свойство и превращаться в свою собственную тестовую функцию. Эм, здесь это просто свойство, но здесь можно просто сказать, эм, обратное обратимо или что-то в этом роде. Эм, я думаю, для этого есть математическое название, эм, я не помню, эм, это не имеет значения. Эм, способ объявления этих функций заключается в том, что аргументы функции — это то, что QuickCheck сгенерирует для вас, поэтому вы никогда не будете вызывать эту функцию сами. Вместо этого вы говорите: «Я хочу проверить свойство, что обратное является исходным». Эм, я хочу проверить это, предоставив мне случайный вектор u38. Задача QuickCheck — сгенерировать это для вас, сгенерировать множество разных случайных векторов, а затем вызвать эту функцию для каждого сгенерированного. Эм, эта функция будет вызываться, эм, тысячи раз с разными векторами u32. Функция возвращает bu, и здесь bu — это результат проверки свойства, которое мы ищем. В данном случае реализация функции заключается в том, что заданный нам вектор должен быть равен обратному значению этого входного вектора. Таким образом, свойство довольно просто выразить в данном случае. Просто вызовите функцию дважды и убедитесь, что вы получили то, что имели изначально. Это и есть вся быстрая проверка. Сейчас я хочу показать вам, как это работает на практике. Итак, давайте перейдем сюда. Мы выполним команду `cargo new`, затем `lib`, ` test` и `QC test`. Мы также добавим `cargo add` и `quick check`. Если мы запустим `cargo test` на этом объекте, то получим инволюцию — это действительно так называется? Инволюция — довольно необычное название. Мне также нужно использовать ` Quick Check`, чтобы получить макрос здесь. Хотите это сюда? Используйте Super Revere, хорошо? Так вот, вы увидите, что когда я запускаю Cargo Test, появляется свойство Tests, верно? Это вот эта функция, и мы могли бы назвать её, как она называлась, Involution? Отлично, Involution. Вы видите, что функция или тест теперь называется Involution, и она возвращает... хорошо? И это мало что вам говорит, потому что тест только что прошёл... но одна из вещей, которую мы можем сделать здесь, чтобы показать, что он действительно работает... я спущусь сюда и немного пропущу вперёд... и возьму эту переменную окружения, чтобы вы могли быстро проверить, сколько... ой, почему? Хорошо, я думаю, это либо Test, либо Test. Почему он не скопировался? Странно, Tests. Хорошо, Tests... так вот, если я запущу это... это фактически означает, и я думаю, что тысяча может быть значением по умолчанию, это означает, что он сгенерирует тысячу различных векторов U32, и для каждого из них проверит Involution. Здесь свойство, что я могу сделать, это увеличить это число, и этот тест все равно не займет много времени, потому что компьютеру довольно быстро выполнить 100 000 действий, а что насчет миллиона? Смотрите, теперь тест выполняется дольше, на самом деле, он выполняется примерно в 10 раз дольше, но в конце концов он возвращает « хорошо». И на самом деле произошло то, что он генерировал все больше и больше тестовых случаев для проверки этого свойства. Так что это, очевидно, не гарантирует правильность вашей программы, потому что случайность не может, знаете, найти

### [15:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=900s) Segment 4 (15:00 - 20:00)

конкретное свойство, но это означает, что вы обретаете уверенность в том, что для произвольных входных данных эта программа в конечном итоге оказывается правильной. Инвертировать, я думаю, инвертируемость правильнее, чем инволюция, независимо от всего остального. Хорошо, мы можем сделать это сообщение, чтобы просто убрать предупреждение. Итак, это основная идея проверки свойств. У вас есть какое-то свойство, которое вы хотите проверить, вы можете выразить его в виде кода, и вы выражаете его, по сути, вы можете рассматривать это как своего рода математический или квантор для всех, хорошо, так что для всех векторов u32, которое может обеспечить быстрая проверка, это свойство должно выполняться, и задача быстрой проверки — сгенерировать множество экземпляров этого типа, чтобы, по сути, протестировать его эмпирически, верно? Она не выполняет проверку модели, для этого можно использовать, знаете, есть очень классный крейт под названием Connie, если вы его еще не видели, то я настоятельно рекомендую вам взглянуть на него. Connie — это средство проверки моделей, которое фактически пытается исчерпывающе проверить вашу программу, не вас, выполняя ее много раз, а стратегически выбирая пути к вашей программе. Но она также медленнее в других отношениях, потому что ей, по сути, приходится запускать интерпретатор, средство проверки моделей и решатель SMT. Поэтому она работает совсем иначе, чем быстрая проверка, но основная идея та же: для всех входных данных этого типа это свойство должно выполняться. И вы можете представить, что мы фактически придумали другое свойство 2, которое, что-то вроде сохранения длины, где мы могли бы сказать, что длина XS должна быть Длина реверса XS, верно? Это тоже должно быть верно. И я сейчас уберу эту лишнюю штуку. И видите, оба эти свойства независимо проверены. Вы можете себе представить, что это подразумевает то, что... Но это не совсем так. Вы можете представить, что реверс — это очень сложный процесс, например, добавление и удаление элементов и всё такое. Мы знаем, что это не так, но это возможно. В таком случае вы можете представить себе способ написать реверс так, чтобы он проходил этот тест, но не проходил этот. Итак, вы снова создаёте своего рода список свойств, и каждое из них будет проверено с помощью Quick Check. Хорошо, так что обратимость подразумевает, что существует некоторое значение, которое его инвертирует, но не то, что это его собственная инверсия. Инволюция — это верно. Хорошо, мы оставим инволюцию. Всё в порядке. Итак, это основная идея использования Quick Check. Вам не обязательно использовать макрос Quick Check. Стоит посмотреть, что... Макрос Quick Check действительно работает, и я собираюсь сделать это в редакторе, потому что так удобнее. Итак, если мы зайдем сюда, в папку Source lib, вы увидите, что макрос находится прямо здесь, и он на самом деле не очень сложный. Мы даже можем сделать это, чтобы посмотреть, во что он разворачивается: тесты, Cargo expand. Cargo expand не может развернуть lib tests. Это звучит очень печально. Ничего страшного, ладно, ладно. Просто потому что я хочу иметь возможность это сделать, я использовать touch tests QC. Я собираюсь переместить эти быстрые проверки в другой файл, вот так. Вы увидите, что тесты работают точно так же, они просто теперь интеграционные тесты вместо модульных тестов, потому что теперь я могу запустить Cargo expand. И, очевидно, это включает в себя кучу дополнительных вещей. Но если мы теперь посмотрим вниз, вы увидите, что Involution теперь просто функция верхнего уровня, внутри которой есть функция, называемая prop, которая может быть телом нашего кода. Вызывает функцию Quick Check, например, Quick Check, используя указатель на наше свойство, нашу функцию проверки свойств. И действительно, если мы вернемся и посмотрим, что делает Quick Check внутри этого макроса Quick Check, он принимает... ну, да, он принимает здесь элемент. Элемент здесь — это что-то вроде функции. Элемент — это объявление типа, объявление функции, объявление модуля. Так что это своего рода элемент, это не просто оператор или выражение.

### [20:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=1200s) Segment 5 (20:00 - 25:00)

И что он будет делать... вот первая часть, которую мы можем в основном игнорировать. Эта часть действительно важна для нас. Она будет сопоставлять все, что выглядит как объявление функции. Вы увидите, она сопоставляет атрибуты, имя функции как идентификатор, все аргументы функции и тип возвращаемого значения, а затем весь код, и то, чем она его заменяет, находится здесь... атрибут проверки и любые другие атрибуты, которые у вас изначально были во входных данных. э-э, функция с тем же именем, что и ваша функция, а затем в строках в свойстве FN она имеет те же аргументы, то же время возврата, тот же код, и затем она вызывает функцию Quick Check с указателем на эту функцию. Итак, опять же, то же самое, что мы видели, когда макрос разворачивается, и вы можете подумать: «Хорошо, это просто означает, что магия здесь, верно? Конечно, где-то есть магия». Итак, давайте посмотрим на это. Но сам макрос просто превращает это в то, и на самом деле мы могли бы написать это сами, например, вместо того, чтобы писать тест таким образом, мы могли бы написать его так: э-э, FN, вот так: э-э, свойство FN, Quick Check, свойство Quick Check вот так. И я думаю, он будет недоволен мной, если я не сделаю это правильно. Итак, опять же, вот во что разворачивается макрос. И я имею в виду, я могу доказать это, запустив программу, и вы увидите, что тест выполнился. Итак, макрос не делает ничего волшебного, но что делает эта функция? Давайте выясним. Итак, функция Quick Check появляется. Давайте перейдем в раздел «Быстрая проверка» в настройках тестера и найдем функцию «Быстрая проверка». Это не вот эта, а вот эта. Хорошо, это просто «Быстрая проверка». Quick Check, хорошо, что такое Quick Check? Quick Check — это структура, просто очень любопытно. Конфигурация для теста, генерация — вот она, мы поговорим об этом, когда будем говорить о том, как генерировать произвольные значения, но это, по сути, источник случайности. Затем в Quick Check вызывается new, что это делает? Это просто устанавливает значения по умолчанию, хорошо? Затем вызывается функция Quick Check, которая инициирует логирование, и эта функция называется self. Быстрый тест с той же функцией выводит результат на основе выходных данных. Хорошо, значит, здесь тоже нет никакой магии, так что, похоже, в быстром тесте должна быть магия. Ладно, быстрый тест, похоже, именно он проверяет свойство. Здесь F — это указатель на тест свойства, который мы хотим выполнить. Что он делает? Он перебирает максимальное количество настроенных тестов. Если мы проходим заданное количество тестов, то останавливаемся, а затем останавливаемся, когда выполняется совпадение с F. Результат: хорошо, а что такое f. Результат... это должен быть метод Testable, то есть трейт, который нам нужен и который должен быть реализован любой функцией, которую мы сюда передаём. Итак, мы просто выполняем сопоставление, и если он говорит, что мы прошли проверку, то мы добавляем к предыдущему результату; если отбрасываем, мы продолжаем; если не проходим, мы возвращаем ошибку. Хорошо, значит, магия должна быть в методе REST. Давайте посмотрим на трейт Testable. Трейт Testable — это всё, что может быть выражено как свойство, проверено с помощью QuickCheck. И вы увидите, что здесь есть таблица тестов для..., но есть Testable для..., поэтому, конечно же, здесь должен быть Testable для... функций. А, хорошо, это выглядит как магия, верно? Это реализует Testable для... FN с множеством имён аргументов, которые возвращают... Хорошо, это, я согласен, выглядит как магия. Итак, это, в конечном итоге, функция в QuickCheck, которая фактически проверяет, соответствует ли набор входных данных определённому свойству. Итак, давайте посмотрим, что она делает. Есть случай сбоя сжатия. Я кратко упомянул сжатие, мы рассмотрим это чуть позже. Но если мы проигнорируем сжатие, то это тело функции, которая выполняет некоторые действия, необходимые макросам, например, чтобы имена переменных не создавали проблем. Видите, здесь тип переменной — это кортеж, где тип элементов кортежа совпадает с типом

### [25:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=1500s) Segment 6 (25:00 - 30:00)

аргументов функции. В нашем случае это будет кортеж из v32, и он преобразуется в произвольное значение. Итак, он генерирует произвольное значение для всех типов наших аргументов. Мы можем посмотреть, что делает функция arbitrary, чуть позже. Затем она вызывает self, используя безопасную обертку. Помните, self здесь — это указатель на функцию. По сути, это просто вызов функции свойства с этими аргументами, а затем вызов функции `. result` для возвращаемого значения. Возвращаемое значение здесь — это логическое значение. Именно его возвращает наша функция проверки свойства, и логическое значение указывает, поддерживаем ли мы свойство или нет. Поэтому, если мы посмотрим на `result` для ` bull`, это вызовет `testResult` из `bull`. Если условие истинно, значит, тест пройден, если ложно — провален. Так что особой магии тут нет. Мы вызываем указатель на функцию со случайно сгенерированными аргументами, а затем преобразуем это в « пройдено» или «провалено» в зависимости от логического значения. Это сопоставление также показывает, что если наше свойство вернуло результат между « хорошо» и «ошибка», то ошибка превратится в провал, а «хорошо» — в « пройдено». Вот что показывает результат. Функция safe — интересная функция. Если мы посмотрим на FN safe, что она делает? Она просто перехватывает паники. Идея в том, что если у нас есть тестируемое свойство, и во время тестирования программа паникует, это явно означает, что с этим свойством что-то не так. Мы могли не провалить этот конкретный тест, но программа завершилась с ошибкой, и это следует считать провалом. Поэтому мы перехватываем паники и преобразуем их в ошибки, как вы видите, результат возвращается сюда. Таким образом, быстрая проверка покажет, что вы провалили этот тестовый случай. Поэтому мы безопасно вызываем функцию. Указатель со случайными аргументами, мы преобразуем это в результат «пройдено/не пройдено», а затем это r. Здесь аргументы сводятся к тому, что мы берем сгенерированные случайные аргументы AR и преобразуем их в строковые представления, чтобы вывести их пользователю. Так что, если быстрая проверка обнаружит некорректный случай, например, массив из четырех элементов, который внезапно возвращает три после переворачивания, и вы хотите узнать, что это за четыре элемента, вы можете выполнить отладку. Именно это и делает эта функция: она берет эти аргументы и выводит их в режиме отладки, чтобы мы могли вывести их пользователю позже и помочь ему в отладке. Затем мы просто сопоставляем статус: если получаем «пройдено» или «отброшено», то возвращаем R (о «отброшено» поговорим позже). Если же мы терпим неудачу, то называем это « сжатием». Идея заключается в том, что если вы можете создать меньший входной массив, который все еще выдает ошибку, мы предпочитаем предоставить его пользователю. Поэтому мы поговорим о сжатии чуть позже, но пока это единственная магия, и на самом деле она не такая уж и магия, так что, возможно, в сжатии есть какая-то магия, о которой мы еще не говорили. Возможно, в произвольном порядке есть какая-то магия, но я сделаю паузу, чтобы рассказать о структуре выполнения этих тестов, которая, на самом деле, кажется довольно простой. Давайте сначала ответим на вопросы по этой части, а затем перейдем к произвольному порядку, а потом к сокращению. Хорошо, что означает «r at test result in the или «at match»? Это было здесь. Итак, в быстром тесте, когда мы находим совпадение, «r at» — это полезный инструмент. Когда вы находите совпадение, вы можете привязать переменную с именем «r» к любой части шаблона. Вы обычно к этому привыкли, верно? Например, «status». Если бы я просто сделал это, то внутри элемента match у меня был бы доступ к переменной с именем «status», которая фактически является значением поля «status» внутри результата теста. И синтаксис «r at» означает, что я хочу, чтобы переменная, привязывающая «r», указывала на весь результат теста. В данном случае, вы можете представить, что это то же самое, за исключением того, что она будет введена только в том случае, если это был результат теста, который Статус был " fail", как видите, мы используем это, чтобы в случае сбоя возвращать ошибку со всем результатом теста.

### [30:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=1800s) Segment 7 (30:00 - 35:00)

Генерация значений с сохранением состояния? Нет. Это одно из отличий между Quick Check и prop test. Prop test — это гораздо более сложная библиотека для тестирования свойств. Она включает в себя такие вещи, как генерация значений с сохранением состояния, где вы пытаетесь избежать повторной генерации того же значения, потому что вы уже его протестировали. Quick Check ничего подобного не делает, он просто случайным образом генерирует входные данные. В этом отношении он гораздо ближе к Fuss Testing, это означает, что он быстрее генерирует значения, но может генерировать перекрывающиеся значения. Например, этот синтаксис, похожий на регулярное выражение, в блоке реализации для меня новый. Что это такое? Синтаксис, похожий на регулярное выражение? О, этот фрагмент — это определение Mac. Если вы посмотрите на правила макросов, я рекомендую посмотреть «Маленькую книгу о макросах Rust». Это действительно хорошее введение в декларативные макросы. У меня также есть поток информации об этом, я думаю, это поток информации о декларативных макросах Rust. Итак, давайте посмотрим, что делает функция `arbitary`, потому что, похоже, это та самая магия, которая генерирует случайное значение, или, в данном случае, генерирует множество случайных значений для нас — по одному случайному значению для каждого аргумента функции. Итак, ` arbitary` — мы на секунду пропустим `gen` и посмотрим на трейт `arbitary`. Трейт `arbitary` имеет две функции: ` arbitary`, которая генерирует новый объект `self`. Представьте себе реализацию ` arbitary` для `u32`. Затем, когда вы вызываете `arbitary` (`col` и ` col`), вы передаете `gen` ( источник случайности) и возвращаете `i32`, потому что это тип `self`. Это то, что реализует `arbitary`. Затем это ` shrink`. А сигнатура ` shrink` — мы вернемся к ней, когда будем говорить о сжатии, потому что это довольно интересно. Сжатие — это просто способ преобразовать сгенерированное значение, например, ` i32`, в более простое значение. Для многих типов нет более простых значений, опять же, для В i32 неясно, является ли ноль более простым значением, чем 42, поэтому, например, для логического значения false проще, чем true, неясно. Таким образом, реализация по умолчанию для shrink — это пустой shrinker, который просто говорит, что это значение нельзя уменьшить дальше. Если вы столкнулись с этим случаем, то вы можете просто вернуть значение. Вы не собираетесь упрощать это для пользователя. А генератор случайных чисел здесь, вы можете представить, что это на самом деле что-то вроде... из библиотеки Rand, настоящий генератор случайных чисел, который был предоставлен. Я думаю, главная причина, почему это не так, заключается в том, что это потребовало бы от QuickCheck зависимости от библиотеки Rand, и есть ряд причин, почему, я думаю, относительно фундаментальная, но также простая библиотека, такая как QuickCheck, не хочет этого делать. Одна из главных причин заключается в том, что библиотека Rand прошла через множество основных версий за эти годы, и поскольку это было бы в публичном API библиотеки QuickCheck, каждая новая основная версия библиотеки Rand потребовала бы новой основной версии QuickCheck. крейт, который слишком сильно меняет правила игры, не говоря уже о быстрой проверке. Я думаю, автор, Burn Sushi, очень хотел убедиться, что он остается достаточно стабильным и его можно использовать со старыми версиями Rust. Крейт Rand довольно быстро меняется, поэтому вы, вероятно, столкнетесь со случаем, когда крейту Quick Check придется обновить версию Rust, с которой он работает, чтобы перейти на новую версию Rand, а он этого не хочет, потому что хочет сохранить или по-прежнему поддерживать пользователей на старых версиях Rust. Поэтому этот тип генерации — это просто очень упрощенная версия генератора случайных чисел, и на самом деле, если вы посмотрите на него, эта структура Pub struct gen внутри содержит Rand, и я думаю, это из крейта Rand, который вы видите здесь, из Rand 085 или 085. Так что внутри он просто содержит генератор случайных чисел, просто снаружи это свой собственный тип, чтобы любые обновления крейта Rand не влияли на публичный API. В Quick Check также есть параметр размера, поэтому идея размера здесь заключается в том, что иногда, как бы это объяснить, вы представляете, что вам нужно сгенерировать случайный вектор. Если вы действительно сгенерировали случайный вектор, то средний возвращаемый вектор будет иметь длину, например, 2 в 63-й степени, потому что это половина пространства, которое занимает U-образный размер (предположим

### [35:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=2100s) Segment 8 (35:00 - 40:00)

64 бита). Реалистично говоря, нас интересуют не такие векторы, и маловероятно, что возникнут ошибки, если векторы будут такого размера. Не говоря уже о том, что вы не сможете выделить такой большой вектор. Поэтому генератор имеет свойство размера, которое определяет, насколько большими должны быть генерируемые данные. Я думаю, что размер по умолчанию для Quick Check, который вы можете изменить при желании, составляет 100. Поэтому, когда вы начинаете генерировать случайные значения, он будет генерировать данные с помощью генератора, размер которого равен 100. И если мы посмотрим на реализацию произвольного значения для Множество типов, так что, видите, для BU это просто генерация случайного бычьего символа, как подбрасывание монеты, произвольного, для UN ничего не было, но если мы посмотрим на что-то вроде реализации произвольного для вектора, вы увидите, что он получает размер текущего генератора, генерирует число в диапазоне от нуля до этого размера, а затем генерирует вектор этой длины, то есть он как бы рассматривает размер как верхнюю границу того, насколько большим должен быть вектор, и случайным образом выбирает длину, а затем все элементы в этом диапазоне. Приятное свойство здесь также означает, что вы можете либо расширить пространство, которое разрешено для поиска, либо сузить его. Один из способов, которым я использовал это в прошлом, — это генерация, по сути, документа JSON. И когда вы генерируете документ JSON, например, JSON, он, как известно, бесконечно вложен, верно? Вы можете поместить карту внутрь карты, внутри карты, там не нужно ничего набирать, вы можете буквально бесконечно продолжать. Но в реальности вы не захотите измерять или запускать свои тесты. В JSON-документах, которые, знаете ли, имеют миллион уровней вложенности, это не особенно интересно, и один только размер здесь не спасет, потому что на каждом уровне генерируется только один элемент, просто они продолжают вкладываться друг в друга, поэтому что-то должно остановить эту вложенность. И вот что я сделал: когда я реализовал `array` (вы можете реализовать этот трейт самостоятельно), это обычно то, как вы должны делать вещи, например, иметь поддержку `array` для структуры, которой владеет ваш тип, например, которая используется вашей библиотекой. Когда вы реализовали `array` для этого типа, который представлял собой JSON- документ, всякий раз, когда я решал, или реализация `array` решала реализовать вложенное значение, например, вложенную карту или массив, она делала это, имея размер генератора, который она передавала в реализацию `array` базового значения. Так что представьте себе, что вы находитесь в блоке, где я решаю сгенерировать вложенный объект, поэтому мне нужно вызвать `array` несколько раз, чтобы сгенерировать ключи и значения внутренней карты. При этом внутреннем вызове `array` я задавал размер, равный половине предоставленного мне размера, и затем... Рекурсивно генерирует свои значения, и таким образом, по мере углубления размер уменьшается, и в конечном итоге вы достигаете точки, где размер равен нулю, и вы прекращаете генерировать больше значений, потому что это можно рассматривать как своего рода лимит, в рамках которого вам разрешено генерировать значения. Вот что такое Siz, но в остальном реализация функции arbitrary для V здесь довольно проста. И, знаете, shrink (опять же, мы еще не говорили о shrink, я покажу вам несколько примеров shrink, чтобы сделать это немного более наглядным). И вы видите, здесь есть реализация arbitrary для кортежей, которая опять же заключается в том, что вы вызываете arbitrary для каждого элемента кортежа, используя конкретный тип этого элемента кортежа. Так что ничего особенно волшебного в arbitrary здесь нет. Если мы посмотрим на опцию arbitrary для, то это, если вы знаете g. Функция `gen` здесь генерирует просто логическое значение. Это универсальная функция из библиотеки Rand, которая в контексте логических значений генерирует значение. Если это значение истинно, мы ничего не генерируем, в противном случае генерируем значение, а затем рекурсивно вызываем `arjustice` для внутреннего типа параметра. Таким образом, сама функция `arjustice` довольно проста. Итак, это произвольная часть системы. Теперь мы поговорили о том, как генерируются значения. И если вам интересно, как бы я это сделал, если бы у меня был входной параметр в виде структуры? Это то же самое, верно? Вы реализуете... Вот пример, возможно, B- дерева. Чтобы реализовать ` arjustice` для B-дерева (это B-дерево из стандартной библиотеки), вы просто генерируете вектор ключ-значение, а затем преобразуете его в B-дерево. Вы можете сделать то же самое для хеш- карт. Но если бы у вас была структура... Интересно, есть ли здесь пример структуры? Возможно, нет. Да, мы видим IP-адрес. То же самое, просто решите, хотите ли вы использовать IPv4. или

### [40:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=2400s) Segment 9 (40:00 - 45:00)

IPv6, и вы рекурсивно вызываете метод `call arbitrary`, э-э, ни один из них не заблокирован, потому что это стандартные библиотечные типы, которые обычно не имеют открытых полей. Но если бы вам пришлось реализовать `disrupt` для своей собственной структуры, вы бы на самом деле написали... э-э, мы можем сделать это так, верно? Структура `Fu`, которая имеет поля... э-э, давайте сделаем `point`, верно? `i32`, `Y` — `i32`, тогда я мог бы реализовать `Quick Check arbitrary` для ` point`, и реализация будет просто `X` — `i32`, `disrupt` от `G`, и ` Y` `G`. Вот и все. Теперь ` disrupt` требует, чтобы тип был `clone`, и я думаю, что он также должен быть ` bug`, чтобы вы могли передать его в ` Quick Check` вот так. Если мы поместим `point` сюда, и он не реализует `debug`, компилятор выдаст ошибку, потому что ему нужно иметь возможность отлаживать, выводить аргументы, чтобы показать пользователю путь к ошибке... э-э, но он также должен быть `clone`, э- э, и требование `clone` здесь необходимо для уменьшения размера, так что если вы... Если вы хотите уменьшить размер, это означает, что вам нужно будет скопировать все элементы, а затем попытаться сделать их меньше, но это значит, что вам придется их клонировать. Вот тут и возникает требование клонирования. Но вы увидите, что реализация арбитража для других структур обычно довольно проста, но, конечно, она становится сложнее, верно? Представьте, что у Point есть, я не знаю, тип, который является строкой, и этот тип должен быть одним из трех строковых значений, и это единственные значения, о которых мы могли бы бесконечно спорить, например, что это должно быть перечисление и все такое. Но представьте, что это строка по какой-то причине. Тогда вам пришлось бы сделать что-то вроде: если G выбирает, например, a, b или c, и нам пришлось бы сопоставить это, а затем, если это a, мы возвращаем, знаете, строку из a, и затем нам пришлось бы сделать это здесь. И последний случай недостижим, потому что мы не выбрали, мы можем сгенерировать только одно из этих трех значений, так что это выполнимо, но становится более запутанным, когда у вас появляется больше ограничений на внутренние типы. Вы часто будете видеть, что с помощью `prop test` это, как правило, улучшает ситуацию. Так вот, с `prop test` вы можете описывать ограничения, например, генерировать случайные значения, которые соответствуют следующему регулярному выражению. Таким образом, у него есть более мощные инструменты для этого, но это также означает, что в библиотеке больше механизмов и в некотором смысле больше накладных расходов, тогда как с `quick check` вам нужно делать все это самостоятельно, нет никакой магии, которая бы это обеспечивала. И это также означает, что часто в `quick check` вводятся дополнительные новые типы. У вас будет `kind` здесь, а затем структура `kind`, которая на самом деле является строкой, и вы реализуете ` quick check` для произвольного `kind`. И в реализации этого вы делаете это. Таким образом, теперь здесь может быть просто произвольный `kind`. Это довольно распространенная ситуация, когда вы в итоге получаете... Так что это довольно распространенный шаблон, который вы видите в `quick check`: у вас есть новые типы, которые представляют... Знайте, что ограничения по значениям — это то же самое, что и в случае, если вы представите, что разрешены только значения I32 от 0 до 100, например. В этом случае вы не получите такой гарантии, поэтому вам нужно написать свой собственный новый тип, который будет гарантировать, что сгенерированное вами значение I32 находится в заданном диапазоне. Опять же, Quick Check не предоставляет никаких способов помочь в этом. Таким образом, возникают определенные трудности и сложности, когда дело доходит до более произвольных или более сложных ограничений на генерируемые вами типы. Quick Check не имеет производного макроса для произвольных значений, по крайней мере, насколько мне известно. Существует библиотека макросов Quick Check, но, насколько я понимаю, единственное, что она предоставляет, это атрибут Quick Check, который добавляется к функции, чтобы сделать ее Quick Check. Таким образом, это здесь или здесь. Мы также могли бы описать это с помощью этого, если бы мы использовали

### [45:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=2700s) Segment 10 (45:00 - 50:00)

библиотеку макросов Quick Check, но, что крайне важно, нет такого атрибута для производных значений. В тесте свойств также есть понятие произвольного трейта, но он использует произвольный трейт Arbitrate из крейта Arbitrate, который имеет функцию derive, но QuickCheck этого не делает. Это очень самодостаточный крейт, который пытается решить проблему целиком внутри крейта. Да, так вот, наш Комментатор в чате указывает нам на это совершенно верно. Поэтому это может показаться немного волшебным, когда все складывается воедино, когда вы просто видите, что он волшебным образом находит ошибки в вашем коде и говорит: «О, с этой конфигурацией Vector у вас ошибка». Но при более внимательном рассмотрении мелкие части не волшебны. И я думаю, что это очень часто случается с библиотеками Rust, и на самом деле с компилятором Rust тоже: это кажется очень волшебным, когда вы смотрите на всю совокупность результатов, но как только вы начинаете копать в деталях... Я имею в виду, это звучит глупо, но это всего лишь код, верно? И чем больше вы на него смотрите, тем больше понимаете: « О, это просто вот это, потом это, потом это... » Да, пользователь Структура с полем email — хороший пример того, как поле email явно подчиняется определенным правилам, поэтому вам не нужно генерировать случайные строки. На самом деле, если мы посмотрим на реализацию функции генерации произвольных строк, вы увидите, что она, по сути, генерирует вектор символов — вот и все, что она делает. Очевидно, что у нее нет правил, которые могли бы совпадать с теми, что есть в вашем приложении для данной строки. Итак, немного истории: Quick Check — это практически прямой перенос из библиотеки H&D под названием Quick Check, которая делает то же самое, и вся логика здесь практически дословно взята из реализации H&D. Хорошо, мы поговорили о том, как генерировать произвольные значения, как проверять результат. Единственное, о чем мы не говорили, когда дело доходит до проверки результата, — это понятие результата теста. Если я вернусь сюда, к тестировщику, то у нас есть это понятие результата теста, которое... Что вы получаете в ответ от тестируемого объекта? Результат теста имеет аргументы статуса и ошибку. Ошибка — это своего рода описание паники или, знаете, случая ошибки, с которым вы столкнулись, или какие-либо другие аргументы. Какие аргументы я сгенерировал, чтобы вызвать этот случай ошибки? А статус — это был пройденный тест, проваленный тест или отброшенный тест? Стоит отметить, что здесь стоит обратить внимание на отбрасывание. Когда вы реализуете или пишете свойство здесь, это будет немного странно для векторов, но давайте посмотрим, есть ли способ сделать это реальным. Это хороший пример. Иногда у вас есть дополнительные ограничения, которые вы хотите наложить на свойство, поэтому вы можете сказать, что это глупый случай, я не хочу приводить глупый пример. Интересно, есть ли в примерах пример, где используется отбрасывание? Давайте посмотрим: отбрасывание — выход за пределы допустимого диапазона. Давайте посмотрим на диапазон B-дерева. Это хороший пример, кстати. Мне очень нравится каталог примеров для крейта QuickCheck. В нём есть довольно хорошие примеры использования QuickCheck для проверки реальных свойств. В данном случае, мы пытаемся проверить тип B-дерева из стандартной библиотеки, и в частности, метод range объекта betri set. Представьте, что вы хотите написать для этого QuickCheck. Причина, по которой они объявляют свой собственный диапазон, заключается в том, что они поддерживают гораздо более старую версию Rust, где rangeBalient ещё не был в стандартной библиотеке. Поэтому мы можем игнорировать большую часть того, что находится в начале. Но что вы хотите сделать при проверке диапазона? Вы берёте множество и диапазон.

### [50:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=3000s) Segment 11 (50:00 - 55:00)

И свойство, которое вы хотите проверить, заключается в том, что множество, которое вы получаете обратно, если вы возьмёте множество и вырежете всё, что находится в заданном диапазоне, должно остаться тем же. Оно должно содержать только те значения множества, которые находятся в диапазоне. Я так и скажу. Итак, если вы извлекаете элементы из диапазона из множества, то полученное значение должно быть точно таким же, как если бы вы прошли по всему множеству и отфильтровали только те, которые находятся в диапазоне. В обоих случаях это должно быть одно и то же утверждение, и именно это проверяется. Единственное замечание: если вы генерируете случайный диапазон, то диапазон — это кортеж из начала и конца. Но у диапазонов есть дополнительное ограничение: конец должен быть после начала. Однако произвольная реализация этого не знает. Если вы попросите ее сгенерировать кортеж из 1, 3, 2, она с таким же успехом может сгенерировать начало 4, 2 и конец 2, и это недействительный диапазон. Поэтому вы можете написать свойство, которое вместо возврата результата теста возвращает один из этих результатов. Тогда вы можете указать, что если диапазон недействителен, и здесь происходит проверка, что для этого свойства существуют только определенные действительные диапазоны, то если диапазон недействителен, если случайно сгенерированный диапазон недействителен, то мы хотим пропустить этот тестовый запуск. Дело не в том, что тест пройден или провален, а в том, что входные данные не имеют смысла для тестирования, и именно для этого сейчас и нужен механизм отбрасывания. Неудобство механизма отбрасывания заключается в том, что когда мы отбрасываем тест, нет никакой гарантии, что эти входные данные не будут сгенерированы снова или что другие снова, которые также не будут соответствовать этому свойству. Поэтому, чем выше вероятность того, что входные данные недействительны, тем больше сгенерированных значений будет отброшено, и, следовательно, это будет расточительно. Представьте, что у вас есть какой-то тип данных, где 90% случайно сгенерированных значений должны быть отброшены, поскольку они по какой-то причине недействительны. Это означает, что эффективность выполнения вашей быстрой проверки составляет 10%, верно? Она генерирует 100 значений, но только 10 из них фактически используются для тестирования. В результате ваш набор тестов становится намного медленнее, потому что вы в конечном итоге генерируете гораздо больше значений, чем выполняете тестов. Вот где вы можете использовать механизм отбрасывания результатов теста как полезный инструмент, но он не должен быть способом ограничения вашего входного пространства. В идеале, вместо реализации функции `arjustice` для входного типа, следует убедиться, что все генерируемые ею числа уже удовлетворяют большей части инвариантности ваших входных данных. Таким образом, при генерации случайных значений они очень редко отбрасываются, что повышает эффективность выполнения быстрой проверки. Это то, что хорошо делает проверка свойств, и чего немного сложнее добиться с помощью быстрой проверки. Например, предположим, вы хотите взять на вход целые числа от нуля до 100. Если в качестве аргумента используется i32, то быстрая проверка сгенерирует для вас произвольные числа от 0 до 2 миллиардов и выше, и вы сохраните только те, которые находятся в диапазоне от 0 до 100; все остальное вы отбросите. В реальности ваш тестовый случай будет работать крайне медленно, потому что быстрая проверка потратит огромное количество времени на генерацию входных данных, которые вы отбросите. Фактически, вы можете никогда не запустить даже один тестовый случай, потому что диапазон проверки очень мал, поэтому вам гораздо лучше иметь Реализация произвольного типа для некоторого нового типа, например, i32, гарантирует генерацию значений в диапазоне от 0 до 100, используя, например, модуль деления. Хотя модуль деления имеет некоторые проблемы, когда дело доходит до случайности с точки зрения равномерности распределения, тем не менее, для этого и существует функция отбрасывания. Итак, поговорили о произвольном типе, мы поговорили об идее написания быстрой проверки и о том, как она, э-э, оценивает, обладает ли свойство. Никакой магии здесь нет. Итак, главное, что нам осталось, это сжатие. У нас осталось четыре минуты. Я думал, что это займет около часа, но до часа осталось четыре минуты, однако сжатие займет больше четырех минут. Я уже беру свои слова обратно, но мы довольно близки к цели. Хорошо, давайте теперь посмотрим на сжатие. Итак, какой пример я хочу использовать для этого?

### [55:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=3300s) Segment 12 (55:00 - 60:00)

Вот что я хочу использовать для этого. Давайте возьмем сортировку. Хорошо, да, как сказано в примере, это... Ошибка в реализации быстрой сортировки. Быстрая проверка. Мы найдем для вас ошибку, и я хочу показать вам, как это выглядит на самом деле. Итак, я запускаю тест Cargo на этом... нет, захват... нет, мне не нужна основная переменная, мне нужно сохранить длину... здесь мне нужна быстрая проверка этих свойств. Хорошо, вы видите, здесь написано, что сортировка завершена, произошел сбой из-за чего-то... тест не пройден с аргументами 1 и 0. Таким образом, реализация сортировки здесь каким-то образом неправильна. Когда мы предоставили 1 и 0, мы не получили обратно 01, на самом деле тест завершился с ошибкой. Хорошо, значит, в этой реализации сортировки есть ошибка, и теперь она дала нам конкретный пример, с которым мы можем поработать. Но 1 — очень полезный вывод. Что, если мы не позволим ей сжиматься? И есть ли простой способ сказать ей не сжиматься? Думаю, есть. В противном случае мы всегда можем просто сделать это... могу ли я ограничить сжатие? Интересно, может быть, нет? О нет, могу. Хорошо... давайте сделаем это. Это принимает VEC из... Итак, что мы сделаем? У нас будет собственный VI, который будет иметь размер V0, а затем мы реализуем Quick Check arbitrary для VC. Ой, почему мой анализатор R сломался? Может, теперь он будет доволен? Ура! Хорошо, я собираюсь реализовать трейт arbitrary здесь, и я скажу self, и я фактически собираюсь импортировать это, чтобы мне не приходилось постоянно это писать. Итак, я реализую arbitrary для VC, я собираюсь вывести clone и debug для VC, и я скажу, что это будет принимать VC, а это также будет принимать vc0 do0 Z. Ого, не то, что я имел в виду. Теперь причина, по которой это не будет уменьшаться, заключается в том, что когда я реализовал трейт arbitrary, я реализовал arbitrary, но не реализовал shrink, и поэтому в результате Quick Check никак не может получить доступ к реализации shrink для VEC, и поэтому он будет думать, что он не может уменьшить входные данные, или, скорее, он будет Чтобы использовать реализацию по умолчанию, которая говорит: «Я не знаю, как уменьшить этот объект», — если я запущу этот тест, вы увидите здесь результаты. Вот они, вот что я получил: гигантский вектор с множеством очень больших значений, потому что все они сгенерированы случайным образом, и это гораздо сложнее отлаживать. Вот что делает «сжиматель»: он берет эти входные данные и преобразует их в аккуратные входные данные, например, одну запятую и ноль. Как он это делает? Итак, идея сжатия. И вот здесь мы вернемся к тому, что на самом деле делает тестер. Вы видите, если тест не пройден, если мы попали в случай сбоя, мы вызываем метод shrink failure unwrap или R. Метод shrink failure здесь попытается уменьшить значение ошибки, и если он не сможет этого сделать, мы вернем исходную ошибку, которую получили. Если все более короткие версии проходят тест, и мы не смогли сгенерировать меньшую версию, мы вернем исходную ошибку R. Что же делает метод shrink failure? Метод shrink failure здесь вызывает метод do shrink. Это метод shrink из трейта arbitrary, мы рассмотрим его позже. Во- вторых, нет, мы сейчас это рассмотрим. Итак, если я вернусь к трейту произвольного типа, вы увидите, что сигнатура shrink принимает ссылку на self. Помните, self — это

### [1:00:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=3600s) Segment 13 (60:00 - 65:00)

тип, для которого мы ранее генерировали случайное значение. Это может быть i32, это может быть вектор размером i, каким угодно. Это текущее значение, которое воспроизводит ошибку, и оно возвращает итератор типа boxD. Другими словами, он возвращает итератор, элементом которого является self. альтернативные значения для self, другие варианты значения self, которые вы должны попробовать. Итак, если мы прокрутим вниз и посмотрим на что-то вроде реализации shrink в Bull, то если значение в данный момент истинно, мы возвращаем один shrinker. Один shrinker здесь — это просто обертка вокруг вектора с одним элементом или строки false. Другими словами, если текущее значение истинно, мы предлагаем вызывающей стороне попробовать значение false. Если текущее значение ложно, то ничего не нужно уменьшать, поэтому мы уменьшаем значение с истинного на ложное, а не с ложного на истинное, это бы закончилось. бесконечно рекурсивно, если текущее значение равно None, мы ничего не можем уменьшить; если текущее значение равно Some, мы попробуем None. И цепочка здесь выглядит так, будто мы возвращаем итератор. Итак, первым элементом в итераторе будет попытка None, а вторым — попытка Some путем уменьшения значения внутри суммы. Так что, если у вас есть Option Bull, допустим, у вас есть сумма True, то цепочка уменьшения для этого итератора, возвращаемого функцией уменьшения значения, будет None, а затем Some, False, потому что она вызывается для внутреннего значения. И вы увидите здесь, что, помните, уменьшение — это итератор. Так что то, с чем мы работаем в цепочке, — это уменьшение внутреннего значения, которое само по себе возвращает итератор. В случае с Bull он возвращает итератор только из одного элемента, но даже в этом случае это итератор, и мы отображаем каждый элемент так, чтобы вокруг него была сумма. Так что итератор здесь дает только один результат. И это единственное ложное значение, которое мы затем сопоставляем с некоторым другим ложным значением, которое затем соответствует варианту "bull" здесь. Если мы посмотрим на что-то более сложное, например, на кортежи, потому что кортежи тоже довольно просты. Для кортежа способ уменьшения размера кортежа заключается в том, что вы берете каждый элемент кортежа и уменьшаете его. В частности, здесь это немного сложно, видите ли, это повторяется здесь для каждого элемента кортежа. В итоге это приводит к тому, что вызывается метод shrink для первого элемента, затем map, и это даст новые альтернативные значения только для первого элемента кортежа. Таким образом, сам итератор используется для каждого элемента, который клонирует все остальные значения кортежа. Поэтому меняется только первое значение, а остальные значения остаются неизменными. Это первый фрагмент итератора, следующий фрагмент итератора — это первый элемент. все Третий элемент и Beyond клонированы, поэтому они одинаковы, но второе значение уменьшается, а его итератор как бы заменяется. То же самое происходит с третьим элементом, четвертым и пятым, и так далее по цепочке итераторов. В итоге получается длинная цепочка итераторов. Можно представить это как список. Первый блок списка будет состоять из всех элементов на второй позиции. Второй и Beyond одинаковы, но один из них — это альтернативные значения для первого элемента. Так что, если первым элементом кортежа будет SumTrue, то в итоге вы получите None (все существующие значения других элементов кортежа) и SomeFals кортежа). Затем следующая строка будет SumTrue, то есть исходное значение, а затем уменьшенное значение второго элемента кортежа, и так далее по цепочке. Идея уменьшения заключается в том, что вы Сначала следует вернуть наименьшие элементы, и мы увидим это, когда вернемся к тому, что на самом деле с этим делается. Итак, помните, мы вернулись к коду, где у нас есть ошибка теста для заданного набора аргументов, и теперь мы хотим уменьшить эти аргументы, а затем попробовать снова и посмотреть, можем ли мы воспроизвести ошибку. Что мы делаем? Мы вызываем метод `srink` для текущего набора аргументов, который мы используем для воспроизведения ошибки. Это дает нам новый набор аргументов, верно? Потому что, если вы уменьшаете, то это, по сути

### [1:05:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=3900s) Segment 14 (65:00 - 70:00)

кортеж, верно? Это кортеж списка аргументов. Поэтому мы уменьшаем список аргументов, что фактически превращается в уменьшение каждого из его аргументов. Но здесь это новый список аргументов, это новый кортеж, где по крайней мере один из элементов изменяется на некоторое меньшее значение. Затем мы просто снова вызываем `Self`. Вы заметите, что эта строка здесь на самом деле та же самая, что и строка ниже. Она просто вызывает ` Self`, которая является указателем на функцию с этим новым набором аргументов вместо старого. набор аргументов... здесь есть небольшая макро-хитрость, чтобы имена работали из переменных... Итак, мы просто вызовем указатель на функцию с новым набором аргументов и посмотрим на результат. Если новый результат также окажется неудачным, то мы хотим... теперь попытаться уменьшить новый результат, верно? Потому что мы знаем, что новые аргументы меньше старых аргументов — это условие сжатия. И теперь, когда у нас есть новый... сжатый, воспроизводящий... случай, мы попытаемся рекурсивно уменьшить его... и затем вернем... сжатый список аргументов. Если же, с другой стороны, мы пройдемся по всем различным сжатиям текущего списка аргументов, и ни одно из них не воспроизведет неудачу, то есть мы никогда не достигнем этого случая неудачи для нового результата, тогда мы скажем, что нам не удалось уменьшить, мы достигнем случая... тогда мы достигнем случая... здесь мы достигнем случая развертки и в итоге вернем исходную неудачу, потому что мы больше не смогли уменьшить... То, что вы видите здесь, — это то, что ошибка сжатия фактически рекурсивно распространяется вниз. Она пытается найти самый ранний элемент в возвращаемом функции shrink, который все еще воспроизводит проблему, а затем она сжимает его, затем сжимает его и так далее, пока не останется места для сжатия. Например, на практике, если вы посмотрите на все выполнения, это будет выглядеть так: сначала она пытается сжать первый аргумент настолько, насколько это возможно, все еще воспроизводя ошибку, затем она начинает сжимать второй аргумент, затем третий, затем четвертый и так далее. И есть способ посмотреть на это так: если я сейчас отменю удаление функции shrink, а затем запущу программу, и в логе Rust будет Trace... Я надеялся, что сжатие будет немного более сложным, но здесь вы видите, что это более раннее выполнение, верно, тест не пройден с этими аргументами, а затем вы видите, что оно сжалось до этого, а затем... Опять ошибка. Если бы у вас была более сложная генерация, позвольте мне посмотреть, смогу ли я это воспроизвести. Я утверждаю, что это, вероятно, ошибка. О, здесь вообще не нужно было уменьшать размер после 100 тестов Quick Check. Это для длины Keep. Попытка выяснить, где эта ошибка, — забавная штука. Итак, это находит все X, которые больше опорного элемента. меньше опорного элемента. Таким образом, мы сортируем все, что меньше, сортируем все, что больше, добавляем X. Добавляем больше, получаем результат. Если список пуст, то, вероятно, здесь ошибка. Где ошибка? Кто-нибудь сразу заметил ошибку в реализации Quick Check? Сортировка всего, что меньше. Я не думаю, что вам нужен базовый случай с одним элементом, потому что у нас есть базовый случай, если список пуст. О, нам не хватает случая, когда XS

### [1:10:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=4200s) Segment 15 (70:00 - 75:00)

пуст, но это тоже не должно иметь значения, потому что если элемент Если список содержит только один элемент, то это будет опорный элемент, а это — остальная пустая часть списка, что означает, что вызов функции сортировки затронет этот случай, мы затем добавим опорный элемент и расширим его пустым итератором, так что это нормально. Если же это список из двух элементов, его длина будет равна единице, это будет всё, что меньше этого, больше этого. Это не очень актуально для потока, я скорее хотел, чтобы, по сути, я хотел ввести ошибку, которая показывает, что список уменьшается. Я не думаю, что это строгое равенство, в этом проблема, потому что, на самом деле, я знаю, как показать вам это. Вместо этого я просто скажу: вот результат теста, импортируйте быстрый if xs. Лен меньше, я не знаю, 50, тогда вернуть результат теста, отбросить 50 — это слишком много, давайте сделаем 10, а затем протестируем это. На самом деле, я не думаю, что это сработает, но мы посмотрим результат теста от wol [ Музыка] false. Интересно, поэтому я просто хочу запустить, отсортировано хм, я думал, что он выведет это, но, похоже, нет. Похоже, он не выводит свои промежуточные данные, это было бы неплохо. Хорошо, я думаю, я мог бы изменить саму быструю проверку, но это кажется... о, на самом деле я могу это сделать, конечно, kgot toml, патч. crates. io, быстрая проверка: равно path, равно home. Джон Дев, другие, быстрая проверка: а теперь, если я перейду сюда, я могу сделать следующее: ePrint line, R new. аргументы, и теперь, может быть, да, отлично, хорошо, вот вы видите процесс, и на самом деле мне даже не нужна моя маленькая штучка, потому что она просто сделает все правильно. Отлично, видите, изначально был сгенерирован этот массив входных данных, это был исходный набор аргументов, а затем вы видите, что это вектор, верно? вектор уменьшается вдвое, а затем вы видите, что он снова уменьшает длину вектора вдвое, а затем снова получает длину вектора, а затем он снова уменьшает и округляет длину вектора вверх. И в этот момент, если он пытался получить вектор, он, должно быть, не смог воспроизвести ошибку, потому что он больше не имеет вектора. Поэтому теперь вместо уменьшения самого вектора он уменьшает отдельные элементы вектора. Сначала он уменьшает первый элемент, и это удается, но ошибка все еще воспроизводится. Затем он пытается уменьшить второй аргумент, вероятно, пытается установить его равным нулю, но 0 не воспроизводит результат, и поэтому вместо этого он переходит к и мы можем здесь посмотреть на реализацию произвольного для числа... Давайте посмотрим, сможем ли мы найти здесь что-то вроде i32 или что-то подобное. Где находятся числа? Символы строки C. Сжимающий беззнаковый тип. Итак, что касается сжатия беззнаковых чисел, вы увидите, что он сначала генерирует ноль, а затем каждую половину этого числа. Если ему не удается использовать ноль, он сначала пытается использовать ноль, в противном случае он пытается использовать половину числа

### [1:15:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=4500s) Segment 16 (75:00 - 80:00)

затем половину этого числа, затем половину этого числа и так далее. Вот что мы видим здесь. Правильно? Он попытался использовать ноль здесь, и это все еще воспроизводилось. Затем не удалось. Таким образом, он продолжает использовать это число до тех пор, пока не достигнет минус единицы. И в этот момент сжатие этого вектора все еще воспроизводит ошибку, и поэтому он возвращает это значение, поскольку это место, где возникает ошибка. Это самый маленький тестовый пример, который я могу использовать для воспроизведения проблемы. Вот что делает сжатие. Вы увидите, что для векторов это сделало нечто интересное, и, возможно, то же самое произошло и с числами. Если вы реализуете произвольный тип, то реализация механизма сжатия (shrink) — действительно хорошая идея, потому что в противном случае вы получите очень сложные отчеты об ошибках, где, например, « я нашел ошибку», когда на вход поступают лишние значения, большие числа и все такое, особенно длинные строки. А с помощью механизма сжатия вы можете очень быстро и значительно упростить отладку. Стратегия сжатия будет варьироваться в зависимости от типа. Иногда сжатие довольно простое, например, для логических значений вы уменьшаете значение с true до false, а false оставляете как есть. Иногда это более сложно, например, для векторов. Мы можем посмотреть на стратегию сжатия для векторов, и вы увидите, что она использует отдельный тип, потому что ей требуется много логики. Здесь она отслеживает, следует ли продолжать уменьшать размер, следует ли элементы. В целом, стратегия VEX Shrinker заключается в том, что сначала он пытается уменьшить размер. Вектор укорачивается за счет сохранения элементов неизменными, а затем, когда он больше не может уменьшать вектор, потому что не может воспроизвести проблему, он начинает уменьшать отдельные элементы. И это, кстати, интересный фрагмент кода, который можно почитать самостоятельно, чтобы понять, как именно это достигается. Это своего рода интересные манипуляции с векторами, но в конечном итоге он делает именно это: уменьшает вектор, а затем уменьшает и отдельные элементы. Главный вывод из кода, который мы прочитали выше, в случае ошибки сжатия, заключается в том, что сначала нужно вернуть самый маленький элемент, а затем увеличивать его размер. Например, если вы дошли до следующего элемента итератора, это означает, что предыдущий элемент не провалил проверку. Если бы предыдущий элемент все еще провалил проверку, мы бы рекурсивно столкнулись с ошибкой сжатия, вместо этого мы бы не продолжили работу с тем же итератором. Поэтому нужно уменьшать вектор как можно сильнее и как можно быстрее. Следовательно, первые элементы, которые вы возвращаете из итератора сжатия, должны быть меньшими, а затем вы увеличиваете их размер, пытаясь снова воспроизвести ошибку. Итак, это, э-э, уменьшение, но я с удовольствием отвечу на вопросы об уменьшении, потому что это немного запутанно, но опять же, это не совсем магия, верно? Просто у вас есть вектор из 100 элементов, и вы уменьшаете его. Есть два способа уменьшить его: либо сделать его менее длинным, либо сделать элементы менее сложными. И это обе стратегии, э-э, уменьшения. Большой вектор больших чисел постепенно уменьшается до небольшого вектора меньших чисел, и поэтому в конечном итоге он дает вам один ноль. Да, совершенно верно. И что важно, это делается по порядку: сначала вектор уменьшается, затем элемент уменьшается. Действительно ли он начинается с нуля, или ноль — это базовый случай? Я не знаю, говорите ли вы об уменьшении знаковых чисел или все еще говорите о сортировке, но для уменьшения беззнаковых чисел, и я предполагаю, что для знаковых чисел то же самое, э-э, ноль здесь означает, что если вы пытаетесь уменьшить число, то если число уже равно нулю, мы больше не можем его уменьшить. Если число еще не равно нулю, то сначала попробуйте ноль, затем половину этого числа, затем половину того числа, или, точнее, это может быть просто единица. Нет, оно продолжает перебирать половины числа, чтобы посмотреть, сможет ли оно воспроизвести результат. На самом деле, этот алгоритм работает наоборот, потому что он сначала генерирует большие числа. Я бы предположил, что этот алгоритм сжатия должен работать наоборот: сначала он должен генерировать ноль, а затем постепенно увеличивать число, пока не достигнет исходного числа.

### [1:20:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=4800s) Segment 17 (80:00 - 85:00)

Таким образом, воспроизведение здесь было бы намного быстрее. Если бы мы начали с минус одного, затем попробовали минус два, затем минус четыре, затем минус восемь и так далее, пока не достигли бы исходного числа, то сжатие происходило бы намного быстрее. Это может быть хорошим дополнением к быстрой проверке. Если кто-то хочет попробовать, зачем мне нужно реализовывать функцию `disrupt. ` для моей структуры, поля которой уже имеют реализованную функцию `disrupt. `? Так вот почему вам нужно реализовывать `disrupt. ` для вашей структуры. В противном случае, для вашей структуры нет реализации произвольного типа данных. В Rust структуры не реализуют трейты автоматически, если у них нет общих реализаций. Нет общей реализации для трейта arbitrary, потому что если кто-то попытается в быстром фреймворке Quick Check написать, например, imple T, imple generic over T arbitrary for T, он не знает, как сгенерировать вашу структуру. Вы только что определили точку с X и Y. В этом блоке imple в фреймворке Quick Check нет способа узнать о ваших полях, поэтому нужен небольшой связующий код, который говорит, что если вам нужна произвольная точка, то вы генерируете два произвольных значения I32 и помещаете их в эти поля, и этот связующий код — это реализация, которую мы должны написать. Может ли функция shrink возвращать трейт? В наши дни, вероятно, может, однако это может быть немного сложно или нежелательно, потому что иногда то, что вы хотите сделать с shrink, — это делегировать вызов функции shrink. два внутренних поля или, например, вы хотите связать несколько итераторов, и, я думаю, вы все еще можете это сделать с помощью impul trait, но когда у вас есть итератор в оболочке, нет, знаете что, я думаю, возможно, в наши дни это возможно, я не знаю, насколько это повлияет, но я думаю, что, вероятно, это возможно. Я думаю, что QuickCheck не внесет это изменение по двум причинам. Во- первых, это будет критическое изменение, и оно было в основной версии с момента ее выпуска, я думаю, сейчас это версия 103 CU, это очень простая реализация, верно? И, э-э, другая причина заключается в том, что это будет работать только в более новых версиях Rust, где вы можете использовать impul trait и возвращать позицию в трейтах, э-э, но кроме этого, нет, я думаю, вы правы, я думаю, что это возможно. Так что да, еще одна причина, почему вы не хотите, чтобы произвольный тип автоматически реализовывался для типов только потому, что они содержат определенный набор внутренних типов, заключается в том, что иногда внешний Тип имеет дополнительную инвариантность относительно внутренних типов, верно? Например, если у вас есть новый тип под названием email, который содержит строку, вы не хотите, чтобы реализация arbitrary для email была просто реализацией arbitrary для string. Вам нужна пользовательская реализация с дополнительными ограничениями. Нет, кто-то спрашивал ранее, что нет производного макроса для arbitrary. И я думаю, основная причина этого в том, что крейт Quick Check стремится быть очень быстрым в использовании, очень простым, быстро компилируемым и быстро запускаемым. Как только вы начинаете вводить процедурный макрос, все замедляется, потому что вам приходится подключать зависимость sin и все остальное. Существует крейт Quick Check Macros, который использует sin в качестве зависимости, но единственное, что он предоставляет, это аннотация Quick Check для функций, с помощью которой, вероятно, можно было бы создать производный макрос для arbitrary, но я точно не знаю, почему они этого не делают. На самом деле, вероятно, есть обсуждение этого, я думаю, что здесь есть предположение. Я думаю, что, возможно, часть первоначального аргумента заключалась в том, что Ну, если вам нужны все эти изыски чего-то более, скажем так, эргономичного, то Prop Test предоставляет вам это. Prop Test использует библиотеку ArrayRight, нет, AR, которая имеет функцию Derived, в то время как Quick Check имеет все необходимое самостоятельно, что влечет за собой

### [1:25:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=5100s) Segment 18 (85:00 - 90:00)

множество компромиссов в обоих направлениях. Давайте посмотрим на Derived Array и посмотрим, что это такое. Derived Array — дубликат 98. Давайте посмотрим, к чему это привело. Я часто думал о чем-то подобном, но на практике почти каждый написанный мной пример ArrayRight должен кодировать какой-то ключ и варианты вокруг генерируемых или сжимаемых значений. Это свело бы на нет использование плагина для автоматического вывода Derived. Да, вы знаете, но это из 2015 года, и было бы довольно почетно включить в него функцию Derived. 2020 году появилась библиотека Derived для Quick Check. Интересно. Проверка диска. Отлично, хорошо, да, теперь есть крейт, он не поддерживается Burn Sushi, это просто отдельный крейт, но он позволяет вам выводить произвольные значения, в частности, выводить произвольный трейт из крейта Quick Check, так что он теперь существует, и я думаю, что многое здесь связано с явным указанием, верно? Что касается реализаций произвольных значений, вам действительно нужно об этом подумать, не всегда следует делегировать это внутренним значениям, потому что часто у вас есть дополнительная инвариантность либо для ваших полей, либо для сжатия этих полей, и часто и то, и другое, тогда как с производным вы просто говорите, что это просто то, что представляют собой внутренние поля, что не редкость, но также неясно, является ли это самым распространенным случаем, возвращает ли он последнее значение итератора сжатия, которое все еще вызывает ошибку, или первое, первое. Итак, если мы посмотрим на реализацию здесь, верно? Мы проходим циклом, это внутренняя функция сжатия при ошибке, здесь мы проходим циклом по сжатию, и в первый раз, когда мы находим ошибку, мы пытаемся сжать этот случай, например... Мы создаём новый случай, а затем возвращаем либо уменьшение этого элемента, либо тот, с которым мы столкнулись. Таким образом, каждый раз, когда мы вызываем метод ` shrink failure`, мы возвращаем первое, что всё ещё выдаёт ошибку, и фактически прекращаем итерацию. Итак, тот факт, что здесь есть оператор `return`, означает, что в момент возникновения новой ошибки вы прекращаете итерацию по текущему уменьшению и начинаете итерацию по уменьшению нового значения. Хорошо, я думаю, это всё. Думаю, мы подошли к концу. На самом деле, больше особо нечего говорить о Quick Check. Если вы посмотрите на код крейта Quick Check, о котором мы только что говорили, то увидите, что там практически нет другого кода, есть множество произвольных реализаций для разных типов, но всё довольно просто. Есть некоторые механизмы, например, конфигурация Quick Check, сколько итераций он должен выполнять и тому подобное, но всё это очень просто. Больше нет никакой, так сказать, магии. Это отличный крейт, я его часто использую. С помощью ArrayRapid можно делать действительно крутые вещи. Например, вам не нужно генерировать только один тип U. Вы можете иметь тип, который представляет, скажем, порядок выполнения распределенной системы, и у вас есть реализация ArrayRapid для этого. Она генерирует трассировку системы ArrayRapid, затем вы ее выполняете, и свойство, которое вы проверяете, заключается в том, что в конце выполнения этой трассировки распределенной системы вы оказываетесь в некотором известном состоянии. И, возможно, это еще один важный момент: один из распространенных способов проверки свойств — сравнение их с реализацией, которая тривиальна и корректна, но, вероятно, довольно медленна. Например, представьте, что вы написали свою собственную реализацию хэш-карты. Хэш-карты могут быть чрезвычайно сложными, но есть и очень простые способы реализовать карту, верно? Или вы можете просто использовать B-дерево. Один из способов быстро проверить свою собственную реализацию хэш-карты — использовать в качестве точки сравнения либо хэш-карту из стандартной библиотеки, либо B-дерево, потому что у вас вряд ли будут те же случаи ошибок, что и у них, и, кроме того, они, как правило, более вероятно будут корректными. Они существуют уже давно, и вот что вы делаете: пишете быструю проверку, где поколения произвольных ключей и значений дают вам произвольные значения. Вы берете эти ключи и значения, передаете их в свою реализацию хеш-карты, а также в стандартную библиотечную реализацию хеш-карты. В конце вы проверяете, равны ли две карты, потому что они должны быть равны. Это то, что известно как проверка свойства Oracle: вы сравниваете с Oracle, который, как вы знаете, является правильным. Это особенно распространенный способ

### [1:30:00](https://www.youtube.com/watch?v=64t-gPC33cc&t=5400s) Segment 19 (90:00 - 91:00)

проверки свойства — сравнение с очевидно правильной реализацией. Очевидно правильная реализация может быть и той, которую вы написали сами, но она до смешного проста, в ней никак не может быть тех же ошибок, что и в вашей реальной, или, может быть, вообще нет ошибок. Например, вы представляете свою карту как вектор пар ключ-значение. Ну, это сложнее, потому что это сложно, но вы понимаете, о чем я говорю: вы пишете очень простую реализацию, а затем у вас есть реальная Сложная реализация на IPL, и даже если самый простой вариант вы никогда не будете использовать в продакшене, потому что он невероятно медленный, вы все равно сможете использовать его для проверки того, проданы ли определенные объекты. Хорошо, я думаю, на этом все. Мы сделали это за полтора часа. Я очень доволен полутора часами. Думаю, это, возможно, самый короткий стрим, недалеко от этого, по крайней мере. Отлично. У меня нет плана на следующий стрим, мне задают много вопросов по этому поводу. Но вы услышите его, когда я спланирую следующий стрим. Я надеюсь, что скоро снова сделаю стрим по Crust of Rust. Мне просто нужно точно определиться, на какой теме его сделать. Но следующий стрим обычно будет примерно через месяц, поэтому я обычно провожу стримы примерно раз в месяц. Хорошо, спасибо всем, кто пришел. Если вы смотрите это видео по запросу, вы можете сейчас. Я помахаю вам на прощание, и если вы смотрите стрим, я тоже помахаю вам на прощание. Я не знаю. Куда я клонил? Увидимся позже, пока, друзья!

---
*Источник: https://ekstraktznaniy.ru/video/22636*