Санкт-Петербургский национальный исследовательский университет
информационных технологий, механики и оптики
Факультет информационных технологий и программирования
Кафедра компьютерных технологий
Рост Аркадий Юрьевич
Методы автоматизированного покрытия кода тестами
на основе эволюционных алгоритмов
Научный руководитель: ассистент кафедры ТП М. B. Буздалов Санкт-Петербург 2013 Содержание Введение................................. 5 Глава 1. Обзор предметной области................ 6 1.1 Метрики покрытия кода.................... 1.2 Эволюционные алгоритмы................... 1.3 Существующие подходы.................... 1.3.1 Применение алгоритмов оптимизации......... 1.3.2 Символьное выполнение................. 1.4 Java Virtual Machine...................... 1.4.1 Типы данных....................... 1.4.2 Среда выполнения.................... 1.4.3 Набор инструкций.................... 1.4.3.1 Инструкции сохранения и загрузки........ 1.4.3.2 Инструкции ветвления............... 1.5 Scala............................... 1.6 Выводы по главе 1....................... Глава 2. Описание реализованного подхода........... 2.1 Постановка задачи....................... 2.2 Общая схема решения..................... 2.3 Функция приспособленности.................. 2.3.1 Функция расстояния до ветви............. 2.3.2 Следование опорной траектории............ 2.3.3 Приближение по многим направлениям........ 2.4 Модификация кода....................... 2.4.1 Граф потока управления................ 2.4.2 Считывание траектории выполнения программы... 2.5 Минимизация набора тестов.................. 2.6 Выводы по главе 2....................... Глава 3. Результаты.......................... 3.1 Покрытие тестами модельных задач............. 3.1.1 Описание модельных задач............... 3.1.1.1 Задача 1....................... 3.1.1.2 Задача 2....................... 3.1.1.3 Задача 3....................... 3.1.1.4 Задача 4....................... 3.1.1.5 Задача 5....................... 3.1.2 Результаты эксперимента................ 3.2 Покрытие тестами олимпиадных задач............ 3.2.1 Условие задачи...................... 3.2.2 Описание эксперимента................. 3.2.3 Результаты........................ 3.3 Выводы по главе 3....................... Заключение................................ Список литературы........................... Введение При разработке современного программного обеспечения значительную часть времени и усилий занимает процесс тестирования. Зачастую он сводится к анализу поведения программы на специально подобранном наборе тестов. Поскольку перебор всех возможных ситуаций неосуществим на практике, стараются выбрать небольшое число тестов, таких чтобы по поведению программы на них можно было судить о ее поведении в целом.
Используются различные метрики покрытия кода для количественной оценки качества набора тестов. Качественным считается набор тестов, удовлетворяющий некоторому критерию полноты. Зачастую критерий полноты определяется с помощью выбранной метрики покрытия кода.
Автоматизация процесса построения набора тестов позволит существенно сократить затраты на тестирование. Существует множество подходов к автоматизированному построению набора тестов. Однако лишь немногие из них имеют программную реализацию, например Microsoft Pex.
Все эти реализации нацелены на модульное тестирование, при котором покрытие фрагментов программы происходит независимо друг от друга.
Вследствие этого большое число генерируемых тестов содержат недопустимые значения для тестируемых фрагментов кода, так как не учитывается внутренняя структура программы.
При тестировании некоторых программ, таких как решения олимпиадных задач, нет необходимости проводить модульное тестирование. Для полноценного покрытия тестами олимпиадных задач необходимо генерировать тесты, подходящие под условие задачи. В данной работе разрабатывается метод построения набора тестов на основе эволюционных алгоритмов, учитывающий особенности всей программы в целом.
Глава 1. Обзор предметной области Целью тестирования программного обеспечения является проверка требуемой функциональности. Существуют различные виды тестирования, например тестирование производительности или безопасности. Для оценки того, насколько хорошо данный набор тестов покрывает код, используются различные метрики. Зачастую полное покрытие кода в рамках выбранной метрики невозможно, поэтому набор тестов считается хорошим, если он покрывает 90 %–95 % кода.
Существует несколько различных способов задать метрику покрытия кода [1, 2]. Основные из них:
• покрытие классов/методов проверяется, что каждый класс/метод вызывается;
• покрытие строк кода проверяется, что каждая строка программы выполняется;
• покрытие условий перебираются все значения условий;
• покрытие траекторий выполнения все ли возможные траектории через заданную часть кода были пройдены.
Метрика покрытия классов/методов не применима для тестирования олимпиадных задач, поскольку при использовании данной метрики может быть не выявлено большое число ошибок. Однако, применение этих метрик оправдано при необходимости покрыть код, часть которого закрыта или не может быть профилирована по лицензионным соображениям.
В отличие от метрики покрытия строк кода, покрытие условий требует рассмотреть все варианты выполнения условия. Например, для условия xy при использовании метрики покрытия условий требуется рассмотреть варианты:
Однако, используя метрику покрытия строк кода, можно ограничиться рассмотрением первого и третьего вариантов.
Для рассмотрения всех возможных траекторий обычно требуется перебрать большее число вариантов, чем при покрытии условий. При этом число траекторий может расти экспоненциально, что требует большого времени выполнения тестов. Поэтому данную метрику применяют лишь при разработке систем с повышенными требованиями к надежности. В данной работе используется метрика покрытия условий.
Эволюционный алгоритм [3] это метод решения задач оптимизации. Данный подход основан на идеях, заимствованных из биологической эволюции: естественный отбор, мутация, скрещивания и наследование признаков. Каждая итерация алгоритма характеризуется набором особей, называемым поколением. На множестве особей вводят функции приспособленности, чтобы количественно оценивать, насколько заданная особь близка к верному решению. При помощи оператора скрещивания (кроссовера) по двум особям генерируется особь для следующего поколения. Оператор мутации вносит малые случайные изменения особи. Начальное поколение обычно формируется случайным образом. При выборе особей для создания нового поколения наиболее приспособленные имеют больше шансов.
Общая схема эволюционного алгоритма представлена на листинге 1.2.1.
В качестве критерия останова часто используют следующие условия:
• найдено верное решение;
• достигнуто заданное количество поколений;
Листинг 1.2.1 Общая схема эволюционного алгоритма 1: Создать начальное поколение 2: Вычислить значение функции приспособленности для каждой особи 3: while (условие останова эволюционного алгоритма не выполнено) do Выбирается подмножество особей текущего поколения Применяя операторы мутации и кроссовера к выбранным особям, генерируются новые Вычисляется значение функции приспособленности для сгенерированных особей Формируется новое поколение, заменяя новыми особями наименее приспособленных 8: end while • превышено заданное время работы;
• превышено заданное число вызовов функции приспособленности;
• за заданное число поколений не произошло улучшение.
Эволюционные алгоритмы применяются для решения задач, к которым не применимы традиционные методы оптимизации. Одной из областей применения эволюционных алгоритмов является автоматическая генерация тестов для программного обеспечения.
Рассмотрим некоторые методы, применяемые для автоматической генерации покрывающего набора тестов.
1.3.1. Применение алгоритмов оптимизации Покрытие фрагмента кода тестом можно рассматривать как задачу оптимизации. В качестве оптимизируемой функции используется количественная оценка того, насколько сгенерированный тест покрывает заданный фрагмент кода.
Рассмотрим некоторые алгоритмы, применяемые для решения данной задачи [4, 5].
• Алгоритм восхождения на вершину (Hill climbing) [6]. На каждой итерации алгоритма генерируется набор “ближайших соседей”, полученный при помощи небольших изменений текущего кандидата.
Существует два способа выбора кандидата для следующей итерации. Можно перебирать соседей, пока не найдется лучший кандидат, либо выбрать лучшего из всех соседей. Алгоритм завершает работу, если невозможно улучшить текущее решение.
• Метод отжига [7]. В отличие от предыдущего алгоритма может выбрать худшее решение с некоторой вероятностью. Эта вероятность уменьшается с номером итерации алгоритма. Вследствие этого данный подход менее подвержен схождению в локальному оптимуму вместо глобального.
• Применение эволюционных алгоритмов для построения набора тестов получило широкое распространение [8 10]. Используются различные эволюционные операторы, функции приспособленности, а также по-разному кодируются особи. Однако не существует оптимального подхода.
При символьном выполнении [11] моделируется выполнение программы, при котором часть входных переменных представляется в символьном виде. Можно выделить два основных подхода [12]: на основе статического или динамического анализа программы.
При статическом подходе для каждой возможной траектории выполнения с помощью символьного выполнения задаются ограничения на параметры тестируемой программы. Если для некоторой траектории возможно удовлетворить все ограничения, то вдоль нее будет сгенерирован тест.
Однако статический подход неприменим, если тестируемая программа содержит ограничения вне области видимости статического анализатора. Рассмотрим программу, приведенную на листинге 1.3.1.
Листинг 1.3.1: Пример, на котором не применим статический подход def testMethod(x, y) { Допустим, что в целях безопасности, анализатор кода не имеет доступ к функции hash. Значит, без запуска программы невозможно подобрать x так, чтобы он был равен hash(y).
Для обхода таких ситуаций используется динамический подход. При этом генерация теста происходит следующим образом:
1. Тестируемая программа выполняется на случайных входных данных.
2. Динамический анализатор записывает ограничения на входные параметры тестируемой программы вдоль траектории выполнения.
3. Удовлетворив ограничениям, подбирается такой тест, чтобы следующий запуск прошел по другой траектории.
Таким образом, при запуске программы 1.3.1 на случайных значениях, будет посчитано hash(y). Взяв посчитанное значение hash(y) вместо x, можно удовлетворить ограничениям.
При использовании данного подхода возникают проблемы с масштабированием [13]. При тестировании более сложных программ возрастает как длина траекторий выполнения, так и число символьных переменных.
При увеличении количества ограничений, накладываемых на параметры тестируемой программы, существенно увеличивается время, затрачиваемое на их удовлетворение.
В данной работе рассматривается покрытие тестами программ, работающих на Java Virtual Machine (JVM). JVM [14] основа Java платформы, отвечающая за аппаратную и операционную независимость программ. JVM имеет определенный набор инструкций и управляет памятью во время выполнения программы. JVM работает с файлами определенного бинарного формата, называемыми class-файлами, в которых содержатся последовательность инструкций, таблица символов и другая вспомогательная информация.
Типы данных JVM делятся на примитивные и ссылочные. Для каждой инструкции определен тип аргументов, к которым она применяется.
Например, инструкции iadd, ladd, fadd и dadd складывают два численных значения и возвращают результат, но каждая из этих инструкций предназначена для определенного типа данных: int, long, float и double соответственно.
Примитивные типы данных делятся на численные, логический (boolean) тип и тип адреса возврата (returnAddress). В свою очередь, числовые типы данных делятся на:
• целочисленные – char, символ Unicode, 2 байта • числа с плавающей точкой, соответствующие стандарту IEEE – float, 32-битное вещественное число одинарной точности – double, 64-битное вещественное число двойной точности Значениями, имеющими тип адреса возврата, являются указатели на инструкции JVM. В отличие от данных численных типов, значения типа адреса возврата не могут быть изменены в процессе работы программы.
Несмотря на то, что JVM декларирует тип boolean, для него предоставляется лишь ограниченная поддержка. JVM не имеет инструкций, специально предназначенных для значений типа boolean. Для работы со значениями типа boolean используются инструкции, предназначенные для работы со значениями типа int, причем true соответствует единица, а false Cсылочные типы данных делятся на классы, массивы и интерфейсы.
Данными, имеющими такие типы, соответственно являются динамически создаваемые экземпляры классов, массивов, а также экземпляры классов и массивы, которые реализуют заданный интерфейс.
В процессе выполнения программы JVM некоторые типы данных хранятся одинаково, причем размер выделяемой памяти зависит от категории типа. Способ хранения типы данных зависит от соответствующего ему типа данных времени выполнения. Соответствие фактических типов данных и типов времени выполнения, а также категории указаны в таблице 1.1.
Таблица 1.1: Таблица соответствий типов данных JVM Фактический тип Тип времени выполнения Категория returnAddress returnAddress Во время работы JVM использует память двух видов: куча и стек.
Куча предназначена для хранения экземпляров классов и массивов.
Очистка кучи происходит автоматически с помощью сборщика мусора. Куча общая для всех потоков JVM и инициализируется при старте JVM.
В отличие от кучи, стек для каждого потока свой. Его инициализация происходит в момент создания потока.
При выполнении программы на каждый вызов метода создается фрейм данные, необходимые для выполнения метода который кладется на стек. После завершения выполнения метода фрейм удаляется со стека, вне зависимости от того, было ли брошено необработанное исключение или нет.
Фрейм, соответствующий методу, выполняемому в данный момент, называется активным. После завершения текущего метода, активным становится предыдущий фрейм. Каждый фрейм выделяется локально для каждого потока, поэтому остальные потоки не могут на него ссылаться.
Каждый фрейм содержит:
• набор локальных переменных;
• стек операндов;
• ссылку на набор констант текущего метода.
Локальные переменные пронумерованы, начиная с нуля, и обращение к ним происходит по соответствующему номеру. JVM использует локальные переменные для передачи параметров при вызове метода, причем нулевым параметром является ссылка на объект, чей метод был вызван.
Для хранения значений типа, относящегося первой категории, используется одна локальная переменная, а для значений типа, относящегося ко второй категории, используются две последовательные локальные переменные.
Максимальный размер стека операндов определяется на этапе компиляции. При выполнении инструкции аргументы забираются со стека операндов и результат выполнения кладется на стек. Кроме того, стек операндов используется для передачи аргументов вызываемым методам. При вызове метода аргументы забираются со стека операндов текущего фрейма, создается новый фрейм и аргументы сохраняются в локальных переменных созданного фрейма. Результат кладется на стек операндов того фрейма, из которого был вызван метод. После этого созданный фрейм удаляется. Значения типов данных, относящихся первой категории, занимают одну ячейку стека операндов, в то время как значения типов, относящихся второй категории две.
Каждый класс и интерфейс имеет набор констант. В нем содержатся как численные константы, известные на момент компиляции, так и ссылки на методы и поля, значения которых вычисляются во время выполнения.
Перечислим инструкции JVM, которые затронуты в данной работе.
1.4.3.1. Инструкции сохранения и загрузки Инструкции загрузки и сохранения предназначены для передачи значений между стеком операндов и локальными переменными:
• Загрузка значений из локальной переменной на стек операндов:
iload, fload, dload, lload, aload;
• Сохранение значений со стека операндов в локальные переменные:
istore, fstore, dstore, lstore, astore;
• Загрузка констант на стек операндов: bipush, sipush, ldc, aconst_null, iconst, fconst, lconst, dconst.
• Сравнение двух операндов типа int: if_icmpeq (=), if_icmpne (=), if_icmplt (), if_icmple ();
• Сравнение операнда типа int c нулем: ifeq, ifne, iflt, ifge, ifgt, • Сравнение ссылочных типов: if_acmpeq, if_acmpne, ifnonnull, • Сравнения: dcmpg, dcmpl, fcmpg, fcmpl, lcmp.
Scala [15] статически типизированный язык программирования, сочетающий в себе возможности объектно-ориентированного и функционального программирования. Система типов Scala специально спроектирована для удобства создания компонентного программного обеспечения.
При этом Scala полностью совместима с Java.
В отличие от Java, в Scala имеется поддержка типажей (trait), которые подобны интерфейсам, но могут содержать реализацию методов.
Как и в случае с интерфейсами, возможно множественное наследование от типажей. При определении типажа можно указывать дополнительные типажи, необходимые создания его экземпляра. Это можно использовать для конфигурации эволюционного алгоритма (ЭА).
Рассмотрим пример, приведенный на листинге 1.5.2. При создании экземпляра ЭА (EvolutionaryAlgorithm) необходимо указать оператор мутации (Mutation).Таким образом, можно использовать один и тот же ЭА, но с различными операторами мутации. В приведенном примере определено два типажа мутации целого числа: IncrementMutation и SquareMutation.
Листинг 1.5.2: Пример конфигурации эволюционного алгоритма trait Mutation[G] { def mutate(g : G) : G trait EvolutionaryAlgorithm[G] { needs : Mutation[G] => // определение необходимого типажа trait IncrementMutation extends Mutation[Int] { def mutate(g : Int) = g + traint SquareMutation extends Mutation[Int] { def mutate(g : Int) = g * g val incEA = new EvolutionaryAlgorithm[Int] with IncrementMutation val squareEA = new EvolutionaryAlgorithm[Int] with SquareMutation Рассмотрены некоторые существующие подходы к автоматизированному покрытию кода тестами. Описаны различные структурные метрики покрытия кода. Кратко описана спецификация JVM. Кратко описаны особенности языка программирования Scala.
Глава 2. Описание реализованного подхода В данной главе описывается разработанный метод автоматизированного покрытия кода тестами на основе эволюционных алгоритмов.
Целью данной работы является создание платформы для автоматизированного покрытия программ тестами, учитывающими внутреннюю структуру тестируемой программы, на основе эволюционных алгоритмов и проверка разработанного метода на ряде модельных задач. Требования к данной работе:
• Разработать метод автоматизированного покрытия тестами кода программ, работающих на JVM.
• Провести сравнение разработанного метода с методом генерации случайных тестов на ряде модельных задач.
• Апробировать предложенный подход для покрытия тестами решения олимпиадной задачи Huzita Axiom 6.
На рисунке 2.1 показана общая схема решения. При загрузке classфайла тестируемой программы происходит модификация кода с целью получения траектории выполнения, а также считывания значений, переданных инструкциям ветвления, в процессе выполнения модифицированной программы. Заметим, что модификация кода тестируемой программы происходит таким образом, чтобы не изменился результат ее работы.
После загрузки модифицированного кода происходит инициализация списка ветвлений, покрываемых инструкций. Будем называть целью заданное ветвление выбранной инструкции. Дальнейшее решение сводится к поиску набора тестов, обеспечивающих покрытие каждой цели.
Построение теста, покрывающего заданную цель, рассматривается как задачу оптимизации, решаемая с помощью эволюционных алгоритмов (ЭА). В качестве особи ЭА кодируется тест. С учетом требований к искомому тесту для особи определяются необходимые эволюционные операции:
мутация и кроссовер.
Для покрытия каждой цели используется свой экземпляр ЭА. Экземпляры ЭА создаются лишь для непокрытых целей. Если в процессе работы находится тест, покрывающий ранее непокрытую цель, то такой тест сохраняется.
После получения набора тестов, покрывающего выбранные цели, производится его минимизация с целью уменьшения накладных расходов при дальнейшем тестировании.
Таким образом, для запуска алгоритма необходимо:
• задать список целей;
• сконфигурировать ЭА;
• обеспечить возможность многочисленных расчетов функции приспособленности.
Функция приспособленности (ФП) дает количественную оценку того, насколько заданный тест покрывает выбранную цель. В данной работе ФП выбирается так, чтобы оценивать, насколько близка траектория выполнения программы к заданной цели.
У некоторых инструкций ветвления лишь определенные ветви способствуют покрытию тестами выбранной цели, поскольку только они приводят к выполнению нужного фрагмента кода. Функция расстояния до ветви определяется таким образом, что ее минимальное значение достигается при прохождении по желаемому ветвлению.
Рассмотрим построение функции расстояния до нужной ветви на примере метода, код которого приведен на листинге 2.3.1.
Листинг 2.3.1: Пример функции расстояния до ветви def testMethod(x : Int) { protected override def onMethod(cn : ClassNode, mn: MethodNode) { 2.4.2. Считывание траектории выполнения программы Для анализа выполнения программы на заданном тесте считывается траектория выполнения, а именно последовательность инструкций ветвления и значения переданные им во время выполнения. Для идентификации инструкций ветвления используется нумерация, введенная при построении графа потока управления.
Для считывания значений во время выполнения программы определен ряд методов, каждый из которых соответствует определенному типу инструкции ветвления. При работе с несколькими потоками последовательность инструкций ветвления будет составлена для каждого потока в отдельности [17].
Рассмотрим модификацию кода программы, необходимую для считывания траектории выполнения. Для каждого профилируемого метода вводятся дополнительные локальные переменные. Перед выполнением инструкции ветвления выполняются следующие действия:
1. Значения со стека операндов сохраняются в дополнительные локальные переменные.
2. Состояние стека операндов восстанавливается из локальных переменных.
3. Вызывается метод для считывания значений во время выполнения.
4. Восстанавливается состояние стека операндов для дальнейшей работы программы.
Минимизация набора тестов является частным случаем задачи о минимальном покрытии множества. Пусть имеется множество тестов T.
Для каждого теста t T известно множество фрагментов кода, которые он покрывает. Требуется построить минимальное по мощности множество В работе [18] показано, что данная задача является NP -полной. По причине чрезмерных затрат вычислительных ресурсов требуется применять приближенные методы.
Рассмотрим жадный алгоритм, решающий данную задачу:
1. Пусть множество X множество выбранных тестов, P = Cov(t), а множество Z множество покрытых им фрагментов кода. Изначально X, Z =.
2. Если Z = P, прекратить работу, решение задачи Topt = X.
3. Выбрать такое t T, что Cov(t) (P/Z) максимально.
Время работы простейшая реализация данного алгоритма имеет асимптотическую оценку O(|T |2 · |P |). В работе [19] показано, что данный алгоритм генерирует в худшем случае ответ, превосходящий оптимальный в O(log|P |) раз. Однако на практике для многих входных данных он дает ответ, отличающийся от оптимального не более чем на 10%.
Формализована цель работы: создание платформы для автоматизированного покрытия программ тестами на основе эволюционных алгоритмов. Описан предлагаемый подход. Предложен способ задания функции приспособленности, рассматривающий приближение по многим направлениям.
Глава 3. Результаты В данном разделе описываются результаты экспериментов, демонстрирующих работу предложенного метода генерации покрывающего набора тестов.
3.1. Покрытие тестами модельных задач Исследования проводились на пяти модельных задачах, взятых с сайта инструментального средства Microsoft Pex http://pexforfun.com.
Для каждой задачи приводится исходный код на языке C#, описывается способ кодирования теста и оператор мутации. Для построения покрывающего набора тестов использовалась (1+1)-эволюционная стратегия.
В рассматриваемых задачах при применении оператора мутации для изменения целого числа x к нему добавлялось число вида (r(19) 9) · 10r(10), где r(a) случайное целое число в диапазоне [0, a).
Если x должен находиться в диапазоне от 105 до 105, то он заменяется на max(105, min(105, x + (r(19) 9) · 10r(4) )).
Код тестируемой программы приведен на листинге 3.1.1. Наиболее сложным для покрытия является случай, когда сумма элементов массива равняется 42. В качестве тестов генерируются целочисленные массивы длины шесть. Оператор мутации выбирает случайный элемент массива и изменяет его описанным ранее способом.
Листинг 3.1.1: Код задачи 1 с сайта pexforfun using System;
public class Program { //#What values of v to cause the exception? Ask Pex to find out!# public static int Puzzle(int[] v) { foreach (int x in v) throw new Exception("hidden bug!");
return sum;
Код тестируемой программы приведен на листинге 3.1.2. Сложность данной задачи заключается в покрытии случая, когда элемент списка, следующий за указанным, на единицу больше. Тестом является список, состоящий из шести целых чисел, и число из множества {0, 1, 2, 3, 4} в качестве индекса элемента в списке. Оператор мутации случайным образом выбирает другой индекс, либо изменяет элемент списка, выбранный случайным образом.
Листинг 3.1.2: Код задачи 2 с сайта pexforfun using System;
using System.Collections.Generic;
public class Program //# What values of list and i can cause exceptions? Ask Pex to find out!# public static void Puzzle(List list, int i) if (list[i] + 1 == list[i + 1]) throw new Exception("hidden bug!");
Код тестируемой программы приведен на листинге 3.1.3. Сложнее всего покрыть случай, когда сумма выбранного элемента массива и равняется 42. Тест представляется в виде массива из шести целых чисел и числа из множества {0, 1, 2, 3, 4, 5} индекса элемента в массиве. Оператор мутации случайным образом выбирает другой индекс, либо изменяет элемент массива, выбранный случайным образом.
Листинг 3.1.3: Код задачи 3 с сайта pexforfun using System;
public class Program { //# What values of v and i can cause an exception? Ask Pex to find out!# public static void Puzzle(int[] v, int i) { if (v[i] + 27277 == 42) throw new Exception("hidden bug!");
Код тестируемой программы приведен на листинге 3.1.4. Чтобы полностью покрыть данную задачу, необходимо подобрать корень линейного уравнения. В качестве теста берется целое число в диапазоне от 105 до 105. Оператор мутации изменяет текущее решение описанным ранее образом.
Листинг 3.1.4: Код задачи 4 с сайта pexforfun using System;
public class Program public static void Puzzle(int x) // What value of x solves this equation? Ask Pex to find out!
Console.WriteLine("equation solved");
Код тестируемой программы приведен на листинге 3.1.5. Сложнее всего покрыть тестом строку, в которой производится вывод на консоль.
Это эквивалентно решению нелинейного уравнения второй степени в целых числах с двумя переменными и наличием ограничений. Тест представляется в виде кортежа из двух целых чисел в диапазоне от 105 до 105.
Оператор мутации изменяет одно из них.
Листинг 3.1.5: Код задачи 5 с сайта pexforfun using System;
public class Program { public static void Puzzle(int x, int y) { //# What values of x and y solve this equation? Ask Pex to find out!#