Skip to content

vladisnut/code-tester

Repository files navigation

Code tester

Status: Active License Python Version Code Style Imports Linter

CLI-проект для тестирования кода алгоритмических задач на Python. Поддерживает несколько видов тестирования (в зависимости от формата задачи) и загрузку задач с некоторых онлайн-ресурсов.

Оглавление

Возможности

  • Загрузка задач с LeetCode и Codeforces.
  • Тестирование как единицы (функции, класса), так и через поток ввода-вывода.
  • Конфигурация тестирования (можно настроить генерацию тестовых случаев, свою валидацию теста, свой скрипт выполнения теста).
  • Удобный ввод тестовых случаев через текстовый файл.

Установка

  1. Клонируйте репозиторий:

    git clone https://github.com/vladisnut/codetester.git
    cd codetester
  2. Создайте виртуальное окружение (опционально):

    python -m venv venv
    source venv/bin/activate  # Linux/Mac
    venv\Scripts\activate     # Windows
  3. Установите зависимости:

    pip install -r requirements.txt

Пример использования

  1. Загрузим любую задачу с LeetCode, например, с ID=1 (вместо ID можно указать slug (two-sum) или ссылку на задачу).

    python main.py load -s leetcode 1
  2. В папке solutions была создана новая папка. Это новое решение, созданное для загруженной задачи. В этой папке в файле solution.py появилась заготовка для решения задачи. Можем дописать код решения задачи.

  3. В этой же папке в файле tests.txt находятся тестовые случаи, которые загрузились вместе с задачей. Можно добавить собственные тесты. Форматы их записей описан здесь.

  4. Запустим тест решения (по умолчанию будет запущено последнее изменённое решение).

    python main.py test
  5. Готово! Таким образом можно загружать и тестировать задачи. Чтобы узнать больше возможностей, читайте далее.

Команды

Создать решение

Создает папку в solutions с базовой структурой файлов решения.

create [solution] [--template|-t <name>]
Параметр Тип Обязательный Описание Допустимые значения
solution Позиционный Нет Имя решения Допустимое имя папки, по умолчанию: значение параметра конфигурации SOLUTION_DEFAULT_NAME
template Именованный Нет Имя шаблона скрипта для решения задачи Имя файла из папки assets/templates/solution, по умолчанию: значение параметра конфигурации MAIN_FUNCTION_NAME

Загрузить задачу

Загружает данные задачи с внешнего ресурса и создает для нее решение. Если параметр конфигурации CREATE_NEW_SOLUTIONS = True, то будет создано новое решение с slug'ом задачи, иначе с именем main.

load <slug> [--source|-s <leetcode|codeforces>] [--open|-o]
Параметр Тип Обязательный Описание Допустимые значения
slug Позиционный Да Slug, ID или URL задачи URL задачи, если source не задан, иначе slug, ID, URL задачи или !daily для ежедневной задачи LeetCode
source Именованный Нет Имя ресурса с задачей leetcode, codeforces
open Именованный Нет Открыть задачу в браузере Флаг (не требует значения)

Тестировать решение

Запускает тесты решения.

test [solution] [--time|-t] [--debug|-d]
Параметр Тип Обязательный Описание Допустимые значения
solution Позиционный Нет Имя решения. Если значение не задано и параметр конфигурации LAUNCH_LAST_MODIFIED_SOLUTION = True, то будет запущено последнее измененное решение Имя существующего решения (имя одной из папок в solutions)
time Именованный Нет Показывать время выполнения каждого теста Флаг (не требует значения)
debug Именованный Нет Режим отладки, при котором тестировщики могут выводить дополнительную информацию Флаг (не требует значения)

Структура проекта

Корень проекта имеет следующие файлы папки:

Имя файла/папки Описание
main.py Основной запускающий скрипт
config.py Конфигурация проекта
assets/ Шаблоны скриптов для решения
solutions/ Содержит решения, каждая папка – отдельное решение
src/ Содержит исходный код проекта

Структура исходного кода:

.
├── api/                # Модули для работы с API (LeetCode, Codeforces)
├── commands/           # Пакет доступных CLI-команд
├── sources/            # Пакет для работы с задачами из определенных ресурсов
├── nodes/              # Пакет для работы с Node-классами (для задач с LeetCode)
├── testing/            # Тестирование решений
│   ├── results/        # Пакет с классами для работы с результатами тестов
│   └── testers/        # Пакет с тестировщиками
└── utils/              # Утилиты

Решение

Решение — это папка, содержащая все необходимые компоненты для работы с конкретной задачей, включая её условие, тесты и код решения. Решения хранятся в папке solutions.

Структура файлов

Типичное решение имеет следующую структуру:

Имя файла Обязательный Описание
data.json Нет Метаданные задачи (название, сложность, теги, исходный ресурс и т. д.) в формате JSON
description.md Нет Условие задачи в формате Markdown (генерируется автоматически при загрузке с внешнего ресурса)
settings.py Нет Конфигурация тестирования
solution.py Да Основной исполняемый скрипт с алгоритмом решения задачи
tests.txt Нет Набор тестовых случаев (входные данные и ожидаемые результаты). Формат ввода зависит от тестировщика

Как видно из таблицы, для тестирования решения достаточно одного файла solution.py, однако в этом случае оно не содержит ни одного тестового случая.

Конфигурация

Конфигурация тестирования решения в файле settings.py имеет следующие параметры:

Параметр Описание Случай применения Допустимые значения
TARGET Цель (функция, класс, метод класса), которую нужно тестировать. Если не указана, то будет выбрана автоматически исходя из анализа файла solution.py Когда автоматический анализ выбирает не ту цель Строка с именем любой функции, класса или метода класса (в формате ClassName.methodName) из solution.py
TESTER Тестировщик – класс, тестирующий код Когда автоматический анализ даёт не того тестировщика Строка с именем любого тестировщика из пакета src.testing.testers (class, function, method, stream)
RUNNER Пользовательская функция запуска теста (определяет как будет запускаться тест) Когда нужно запускать тест по-своему Функция
VALIDATOR Пользовательский валидатор результата теста (функция, которая будет выдавать вердикт: прошел тест или нет) Когда нужно по-своему настроить проверку результата работы кода Функция
TESTS Набор тестовых случаев, задаваемый программно (ручные и генеративные) Когда ручного ввода в текстовом файле недостаточно Последовательность тестовых случаев, представленных словарями

Note

Наличие значения любого из параметров необязательно: они лишь позволяют настроить тестирование под свои нужды.

Runner

Обычно тест запускается так, как этого хочет выбранный тестировщик. Однако можно запустить его иначе, по-своему обращаясь с целью тестирования (функция или класс). Например, можно взять результат работы одного метода класса и передать его в другой. Ниже представлена аннотация пользовательской функции запуска теста. Чтобы он был активирован, его нужно присвоить переменной RUNNER.

def runner(target: type | Callable, args: Sequence) -> Any:
    """
    Пользовательская настройка запуска тестируемой цели.

    :param target: Тестируемая цель (функция или класс).
    :param args: Аргументы теста.
    :returns: Результат выполнения кода.
    """
    pass


RUNNER = runner
Пример
def runner(target: type | Callable, args: Sequence) -> Any:
    """
    Сериализация и десериализация аргумента.
    """
    obj = target()
    s = obj.serialize(args[0])
    result = obj.deserialize(s)

    return result

Validator

Иногда результат выполнения кода может иметь несколько допустимых ответов, например, не зависеть от порядка элементов. В таком случае пригодится написание своего валидатора, который будет сам выносить вердикт теста. Ниже представлена аннотация пользовательского валидатора результата теста. Аналогично, чтобы он был активирован, его нужно присвоить переменной VALIDATOR.

def validator(
        args_before: Sequence,
        args_after: Sequence,
        expected: Any,
        result: Any
) -> bool:
    """
    Валидация результата теста.

    :param args_before: Аргументы теста до выполнения кода.
    :param args_after: Аргументы теста после выполнения кода.
    :param expected: Ожидаемый результат.
    :param result: Результат выполнения кода.
    :returns: True, если тест пройден, иначе False.
    """
    pass


VALIDATOR = validator
Пример
def validator(
        args_before: Sequence,
        args_after: Sequence,
        expected: Any,
        result: Any
) -> bool:
    """
    Убрать зависимость от порядка элементов.
    """
    return sorted(expected) == sorted(result)

Тестовые случаи

Тестовые случаи можно создавать следующими способами:

  1. Вводить вручную в текстовом файле tests.txt
  2. Вводить программно в setting.py
  3. Писать функции, которые их генерируют в setting.py

Формат аргументов тестов и ожидаемых значений зависит от тестировщика, поэтому формат тестовых случаев нужно смотреть в для каждого из тестировщиков в отдельности (это касается ввода тестовых случаев как в tests.txt, так и в setting.py).

Рассмотрим лишь как в целом вводить тестовые случаи программно. Они хранятся в переменной TESTS. Каждый тестовый случай – словарь. Всего есть два формата (ручной и генеративный):

def generator() -> tuple[Sequence, Any]:
    """
    Генерация тестового случая.

    :returns: Кортеж из двух элементов:
    последовательность аргументов теста, ожидаемое значение.
    """
    pass


TESTS = [
    # Ручная запись тестового случая:
    {
        'args': [],              # Аргументы теста.
        'expected': None         # Ожидаемое значение (не обязательно).
    },
    # Генерация тестовых случаев:
    {
        'generator': generator,  # Функция, возвращающая тестовый случай.
        'count': 100             # Количество вызовов функции (по умолчанию 1).
    },
]

Тестировщики

Тестировщик Описание Назначение
function Выполняет функцию с заданными аргументами Тестирование функции
method Выполняет метод класса с заданными аргументами Тестирование метода класса
class Выполняет команды над классом, создавая его объект и вызывая его методы Тестирование класса и его методов
stream Записывает входные данные в стандартный поток ввода, запускает функцию, считывает выходные данные с потока вывода Тестирование модуля через стандартный поток ввода-вывода

function, method

Используется, когда нужно тестировать одну функцию / один метод класса.

Формат в tests.txt:

  • Каждый аргумент находится в отдельной строке
  • Наборы тестовых данных разделяются хотя бы одной пустой строкой
  • Все значения должны быть типами данных, допустимыми форматом JSON
№ аргумента Обязательный Описание
1 .. N Да Аргумент теста
N + 1 Нет Ожидаемый результат
Пример
def remove_element(nums: List[int], val: int) -> int:

Тестовый случай 1:
Ввод: nums = [3,2,2,3], val = 3
Вывод: 2

Тестовый случай 2:
Ввод: nums = [0,1,2,2,3,0,4,2], val = 2
Вывод: 5

Формат записи тестовых случаев в tests.txt:

[3,2,2,3]
3
2

[0,1,2,2,3,0,4,2]
2
5

class

Используется, когда нужно создавать объект тестируемого класса и вызывать его методы.

Формат в tests.txt:

  • Каждый аргумент находится в отдельной строке
  • Наборы тестовых данных разделяются хотя бы одной пустой строкой
  • Все значения должны быть типами данных, допустимыми форматом JSON
№ аргумента Обязательный Описание
1 Да Список строковых команд (первая – название класса, остальные – названия вызываемых методов)
2 Да Списки аргументов каждой команды
3 Нет Список ожидаемых результатов для каждой команды
Пример
class LRUCache:

    def __init__(self, capacity: int):

    def get(self, key: int) -> int:

    def put(self, key: int, value: int) -> None:

Тестовый случай:

Аргумент Значение
Список команд ["LRUCache","put","put","get","put","get","put","get","get","get"]
Список аргументов команд [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
Список ожидаемых значений команд [null,null,null,1,null,-1,null,-1,3,4]

Как бы это выглядело в коде:

obj = LRUCache(2)  # Создание объекта
obj.put(1, 1)      # Ожидается, что вернет None
obj.put(2, 2)      # Ожидается, что вернет None
obj.get(1)         # Ожидается, что вернет 1
obj.put(3, 3)      # Ожидается, что вернет None
obj.get(2)         # Ожидается, что вернет -1
obj.put(4, 4)      # Ожидается, что вернет None
obj.get(1)         # Ожидается, что вернет -1
obj.get(3)         # Ожидается, что вернет 3
obj.get(4)         # Ожидается, что вернет 4

Формат записи тестовых случаев в tests.txt:

["LRUCache","put","put","get","put","get","put","get","get","get"]
[[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
[null,null,null,1,null,-1,null,-1,3,4]

stream

Используется, когда нужно тестировать код используя операции ввода-вывода.

Формат в tests.txt:

  • Аргументы можно писать как в одной строке, так и разделив их на несколько строк.
  • Входные и выходные данные разделяются минимум одной пустой строкой. Наборы тестовых данных разделяются каждой второй последовательностью, состоящей минимум из одной пустой строки. То есть нечетная последовательность пустых строк разделяет входные и выходные данные, а четная – тестовые случаи. Для удобства входные и выходные данные можно разделять одной пустой строкой, а тестовые случаи – тремя.
№ аргумента Обязательный Описание
1 Да Строка входных данных
2 Нет Строка ожидаемых выходных данных
Пример

Задача:
Вычислить сумму элементов для каждой из заданных последовательностей.

Формат входных данных:
Первая строка содержит целое число N — количество последовательностей. Далее следуют N блоков данных, каждый из которых состоит из двух строк: M — длина последовательности (целое число). Последовательность чисел — строка из M чисел, разделённых пробелами.

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

Тестовый случай 1:

Ввод:

3
5
1 2 3 4 5
3
10 20 30
4
-1 5 0 -3

Вывод:

15
60
1

Тестовый случай 2:

Ввод:

2
2
100 -100
1
42

Вывод:

0
42

Формат записи тестовых случаев в tests.txt:

3
5
1 2 3 4 5
3
10 20 30
4
-1 5 0 -3

15
60
1



2
2
100 -100
1
42

0
42

Преобразование типов

Классы, наследованные от класса Node (односвязный список ListNode, бинарное дерево BinaryTreeNode и N-дерево NTreeNode) могут конвертироваться в список (объект Python типа list) и обратно. Формат хранения данных в виде списка аналогичен тому как это сделано в LeetCode.

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

Примеры

1. Односвязный список.

  • В формате списка: [1, 2, 3, 4]
  • Схематичный вид: 1 -> 2 -> 3 -> 4

2. Бинарное дерево.

В формате списка:

[1, 2, 3, 4, 5, null, 8, null, null, 6, 7, 9]

Схематичный вид:

          (1)
        /     \
     (2)       (3)
    /   \         \
  (4)   (5)       (8)
       /   \      /
     (6)   (7)  (9)

Алгоритм конвертации можно посмотреть здесь.

3. N-дерево.

В формате списка:

[1, null, 2, 3, 4, 5, null, null, 6, 7, 8, null, 9, null, null, 10, 11]

Схематичный вид:

    _____________(1)______________
   /         |         |          \
  (2)     __(3)__     (4)        (5)
         /   |   \     |
        (6) (7) (8)   (9)
       /   \
     (10) (11)

Алгоритм конвертации можно посмотреть здесь.

Примеры решений

Примеры решений с комментариями можно найти в папке solutions.

About

CLI-project for testing the code of algorithmic problems in Python.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages