Общая задача: написать utbot для языка Python, использующий fuzzing при генерации тестов.
Основные подзадачи:
- Получить функции, которые нужно протестировать
- Сгенерировать значения для аргументов
- Вычислить значения функции на этих аргументах
- Отрендерить тесты для функций
Список функций формируется через UI (Intellij IDEA / консоль).
Данные о функциях, которые должны передаваться:
- название
- список аргументов
- код функции
- аннотации типов аргументов (опционально)
- аннотация типа возвращаемого значения (опционально)
Хотим генерировать значения для всех аргументов функции.
Если есть аннотация, то достаточно научиться генерировать экземпляры нужного типа.
Проблема: как сузить значения при генерации для аргумента без аннотации?
Решение: можем составить базу из всех примитивных типов в Python, научиться создавать их представителей (дефолтные, крайние, критические значения, может быть с возможностью подставить константы из кода), далее для аргумента нужно найти все типы, имеющие нужные методы, и подставлять их.
Для сложных / пользовательских классов без представителей нужно рекурсивно для каждого поля создать представителя. Это может быть достаточно сложно, так как не все поля могут иницализироваться в __init__ и для получения данных о них нужно проанализировать все методы исходного класса. Первым шагом здесь будет случай, когда все поля можно получить из функции инициализации и они имеют примитивные типы.
Проверять можно только те типы, которые доступны (импортированы) в содержащем файле.
Чтобы получить требования к методам аргумента, нужно построить дерево исполнения функции, конечные методы в каждой ветке будут соответствовать одному из подходящих вариантов. Для этого есть несколько библиотек на Python:
- pycallgraph (очень старая, есть более новая версия pycallgraph2: июль 2019) динамический анализ
- pyan (последний релиз: февраль 2021) статический, но неполный
- Jonga (релиз: ноябрь 2018) динамический анализ
Либо можно делать это самостоятельно.
Для описанного выше алгоритма необходимо составить большую базу о встроенных типах. Их много поэтому, возможно, придется автоматизировать этот процесс.
Составлять можно либо по коду CPython (можно по тестам), либо по коду GraalPython, либо руками.
О каждом типе нужно хранить следующие данные:
- имя
- методы: имя + аргументы (+аннотации)
- базовые поля?
- доступ к конструктору
Два последних поля скорее всего не нужны, и сложно определяются для built-in типов. Мы будем создавать тестовые экземляры руками.
Примеры можно брать из cpyhton/Lib/test.
Поддерживаемые операции:
- Генерировать объект с дефолтными значениями
- Генерировать с константами из кода
- имя
- аргументы с аннотациями
- где лежит
Для каждого типа мы должны уметь создавать представителей, можно хранить код для этого в текстовом виде.
Пока что проводим запуск функции непосредственно через запуск python в отдельном процессе. Для этого нужно сгенерировать код, который будет содержать код функции / умеет ее импортировать и сохранять в восстанавливаемом формате (можно попробовать json, для некоторых типов есть сериализатор по умолчанию, для остальных придется писать самим, но это должно быть проще, чем писать польностью свой код).
Некоторые типы очень сложно (невозможно) сохранить и создать такой же, будем считать, что такие типы не можем обрабатывать как результат функции.
Пример: socket.
Сначала нужно построить AST того кода, который мы хотим выполнять, далее сгенерировать соответствующий код. Нужно либо пользоваться ANTLR, либо написать самостоятельно.
Потом нужно записать этот код в правильный файл.